greg@229: #!/usr/bin/env python greg@229: # -*- coding: utf-8 -*- greg@229: greg@229: #This file is part of Beremiz, a Integrated Development Environment for greg@229: #programming IEC 61131-3 automates supporting plcopen standard and CanFestival. greg@229: # greg@229: #Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD greg@229: # greg@229: #See COPYING file for copyrights details. greg@229: # greg@229: #This library is free software; you can redistribute it and/or greg@229: #modify it under the terms of the GNU General Public greg@229: #License as published by the Free Software Foundation; either greg@229: #version 2.1 of the License, or (at your option) any later version. greg@229: # greg@229: #This library is distributed in the hope that it will be useful, greg@229: #but WITHOUT ANY WARRANTY; without even the implied warranty of greg@229: #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU greg@229: #General Public License for more details. greg@229: # greg@229: #You should have received a copy of the GNU General Public greg@229: #License along with this library; if not, write to the Free Software greg@229: #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA greg@229: greg@229: import Pyro.core as pyro Edouard@690: from threading import Timer, Thread, Lock, Semaphore etisserant@301: import ctypes, os, commands, types, sys Edouard@1075: from targets.typemapping import LogLevelsDefault, LogLevelsCount, TypeTranslator, UnpackDebugBuffer Edouard@917: etisserant@301: greg@229: if os.name in ("nt", "ce"): greg@229: from _ctypes import LoadLibrary as dlopen greg@229: from _ctypes import FreeLibrary as dlclose greg@229: elif os.name == "posix": greg@229: from _ctypes import dlopen, dlclose greg@229: greg@344: import traceback laurent@699: def get_last_traceback(tb): laurent@699: while tb.tb_next: laurent@699: tb = tb.tb_next laurent@699: return tb greg@229: greg@229: lib_ext ={ greg@229: "linux2":".so", greg@229: "win32":".dll", greg@229: }.get(sys.platform, "") greg@229: etisserant@291: def PLCprint(message): etisserant@291: sys.stdout.write("PLCobject : "+message+"\n") etisserant@291: sys.stdout.flush() etisserant@291: greg@229: class PLCObject(pyro.ObjBase): etisserant@235: _Idxs = [] laurent@368: def __init__(self, workingdir, daemon, argv, statuschange, evaluator, website): greg@229: pyro.ObjBase.__init__(self) etisserant@301: self.evaluator = evaluator greg@229: self.argv = [workingdir] + argv # force argv[0] to be "path" to exec... greg@229: self.workingdir = workingdir greg@229: self.PLCStatus = "Stopped" greg@229: self.PLClibraryHandle = None greg@352: self.PLClibraryLock = Lock() laurent@366: self.DummyIteratorLock = None greg@229: # Creates fake C funcs proxies greg@229: self._FreePLC() greg@229: self.daemon = daemon greg@269: self.statuschange = statuschange etisserant@301: self.hmi_frame = None laurent@368: self.website = website Edouard@914: self._loading_error = None Edouard@1014: self.python_runtime_vars = None greg@229: greg@229: # Get the last transfered PLC if connector must be restart greg@229: try: greg@229: self.CurrentPLCFilename=open( greg@229: self._GetMD5FileName(), greg@229: "r").read().strip() + lib_ext Laurent@1121: self.LoadPLC() greg@229: except Exception, e: greg@229: self.PLCStatus = "Empty" greg@229: self.CurrentPLCFilename=None greg@229: etisserant@286: def StatusChange(self): etisserant@286: if self.statuschange is not None: etisserant@286: self.statuschange(self.PLCStatus) etisserant@286: Edouard@917: def LogMessage(self, *args): Edouard@917: if len(args) == 2: Edouard@917: level, msg = args Edouard@917: else: Edouard@917: level = LogLevelsDefault Edouard@917: msg, = args Edouard@917: return self._LogMessage(level, msg, len(msg)) Edouard@917: Laurent@1093: def ResetLogCount(self): Laurent@1093: if self._ResetLogCount is not None: Laurent@1093: self._ResetLogCount() Edouard@917: Edouard@917: def GetLogCount(self, level): Edouard@911: if self._GetLogCount is not None : Edouard@917: return int(self._GetLogCount(level)) Edouard@917: elif self._loading_error is not None and level==0: Laurent@1093: return 1 Edouard@914: Edouard@917: def GetLogMessage(self, level, msgid): Edouard@921: tick = ctypes.c_uint32() Edouard@921: tv_sec = ctypes.c_uint32() Edouard@921: tv_nsec = ctypes.c_uint32() Edouard@914: if self._GetLogMessage is not None: Edouard@914: maxsz = len(self._log_read_buffer)-1 Edouard@921: sz = self._GetLogMessage(level, msgid, Edouard@921: self._log_read_buffer, maxsz, Edouard@921: ctypes.byref(tick), Edouard@921: ctypes.byref(tv_sec), Edouard@921: ctypes.byref(tv_nsec)) Edouard@914: if sz and sz <= maxsz: Edouard@914: self._log_read_buffer[sz] = '\x00' Edouard@921: return self._log_read_buffer.value,tick.value,tv_sec.value,tv_nsec.value Edouard@917: elif self._loading_error is not None and level==0: Edouard@921: return self._loading_error,0,0,0 Edouard@914: return None Edouard@911: greg@229: def _GetMD5FileName(self): greg@229: return os.path.join(self.workingdir, "lasttransferedPLC.md5") greg@229: greg@229: def _GetLibFileName(self): greg@229: return os.path.join(self.workingdir,self.CurrentPLCFilename) greg@229: greg@229: Edouard@1027: def LoadPLC(self): greg@229: """ greg@229: Load PLC library greg@229: Declare all functions, arguments and return values greg@229: """ greg@229: try: greg@229: self._PLClibraryHandle = dlopen(self._GetLibFileName()) greg@229: self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle) greg@229: greg@229: self._startPLC = self.PLClibraryHandle.startPLC greg@229: self._startPLC.restype = ctypes.c_int greg@229: self._startPLC.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)] greg@229: ed@455: self._stopPLC_real = self.PLClibraryHandle.stopPLC ed@455: self._stopPLC_real.restype = None laurent@366: laurent@366: self._PythonIterator = getattr(self.PLClibraryHandle, "PythonIterator", None) laurent@366: if self._PythonIterator is not None: laurent@366: self._PythonIterator.restype = ctypes.c_char_p Edouard@851: self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)] laurent@366: edouard@483: self._stopPLC = self._stopPLC_real laurent@366: else: Edouard@717: # If python confnode is not enabled, we reuse _PythonIterator ed@455: # as a call that block pythonthread until StopPLC ed@455: self.PythonIteratorLock = Lock() ed@455: self.PythonIteratorLock.acquire() Edouard@868: def PythonIterator(res, blkid): ed@455: self.PythonIteratorLock.acquire() ed@455: self.PythonIteratorLock.release() laurent@366: return None ed@455: self._PythonIterator = PythonIterator laurent@366: edouard@483: def __StopPLC(): ed@455: self._stopPLC_real() ed@455: self.PythonIteratorLock.release() edouard@483: self._stopPLC = __StopPLC edouard@483: greg@229: greg@229: self._ResetDebugVariables = self.PLClibraryHandle.ResetDebugVariables greg@229: self._ResetDebugVariables.restype = None greg@229: etisserant@235: self._RegisterDebugVariable = self.PLClibraryHandle.RegisterDebugVariable greg@229: self._RegisterDebugVariable.restype = None edouard@477: self._RegisterDebugVariable.argtypes = [ctypes.c_int, ctypes.c_void_p] greg@229: greg@229: self._FreeDebugData = self.PLClibraryHandle.FreeDebugData greg@229: self._FreeDebugData.restype = None greg@229: edouard@450: self._GetDebugData = self.PLClibraryHandle.GetDebugData edouard@450: self._GetDebugData.restype = ctypes.c_int edouard@450: self._GetDebugData.argtypes = [ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)] etisserant@235: etisserant@235: self._suspendDebug = self.PLClibraryHandle.suspendDebug Edouard@614: self._suspendDebug.restype = ctypes.c_int edouard@462: self._suspendDebug.argtypes = [ctypes.c_int] etisserant@235: etisserant@235: self._resumeDebug = self.PLClibraryHandle.resumeDebug etisserant@235: self._resumeDebug.restype = None Edouard@906: Laurent@1093: self._ResetLogCount = self.PLClibraryHandle.ResetLogCount Laurent@1093: self._ResetLogCount.restype = None Laurent@1093: Edouard@906: self._GetLogCount = self.PLClibraryHandle.GetLogCount Edouard@906: self._GetLogCount.restype = ctypes.c_uint32 Edouard@917: self._GetLogCount.argtypes = [ctypes.c_uint8] Edouard@906: Edouard@911: self._LogMessage = self.PLClibraryHandle.LogMessage Edouard@911: self._LogMessage.restype = ctypes.c_int Edouard@971: self._LogMessage.argtypes = [ctypes.c_uint8, ctypes.c_char_p, ctypes.c_uint32] Laurent@1093: Edouard@911: self._log_read_buffer = ctypes.create_string_buffer(1<<14) #16K Edouard@911: self._GetLogMessage = self.PLClibraryHandle.GetLogMessage Edouard@911: self._GetLogMessage.restype = ctypes.c_uint32 Edouard@921: self._GetLogMessage.argtypes = [ctypes.c_uint8, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)] Edouard@911: Edouard@914: self._loading_error = None Edouard@1035: Edouard@1035: self.PythonRuntimeInit() Edouard@1035: greg@229: return True greg@229: except: Edouard@914: self._loading_error = traceback.format_exc() Edouard@914: PLCprint(self._loading_error) greg@229: return False greg@229: Edouard@1045: def UnLoadPLC(self): Edouard@1045: self.PythonRuntimeCleanup() Edouard@1045: self._FreePLC() Edouard@1045: greg@229: def _FreePLC(self): greg@229: """ greg@229: Unload PLC library. greg@229: This is also called by __init__ to create dummy C func proxies greg@229: """ greg@352: self.PLClibraryLock.acquire() greg@229: # Forget all refs to library Edouard@1027: self._startPLC = lambda x,y:None greg@229: self._stopPLC = lambda:None greg@229: self._ResetDebugVariables = lambda:None laurent@479: self._RegisterDebugVariable = lambda x, y:None greg@229: self._IterDebugData = lambda x,y:None greg@229: self._FreeDebugData = lambda:None edouard@450: self._GetDebugData = lambda:-1 Edouard@614: self._suspendDebug = lambda x:-1 etisserant@235: self._resumeDebug = lambda:None etisserant@280: self._PythonIterator = lambda:"" Edouard@911: self._GetLogCount = None Edouard@917: self._LogMessage = lambda l,m,s:PLCprint("OFF LOG :"+m) Edouard@914: self._GetLogMessage = None greg@229: self.PLClibraryHandle = None greg@229: # Unload library explicitely greg@229: if getattr(self,"_PLClibraryHandle",None) is not None: greg@229: dlclose(self._PLClibraryHandle) laurent@393: self._PLClibraryHandle = None laurent@393: greg@352: self.PLClibraryLock.release() greg@229: return False greg@229: Edouard@1014: def PythonRuntimeCall(self, methodname): Edouard@1014: """ Edouard@1014: Calls init, start, stop or cleanup method provided by Edouard@1014: runtime python files, loaded when new PLC uploaded Edouard@1014: """ Edouard@1051: for method in self.python_runtime_vars.get("_runtime_%s"%methodname, []): Edouard@1051: res,exp = self.evaluator(method) Edouard@1051: if exp is not None: Edouard@1052: self.LogMessage(0,'\n'.join(traceback.format_exception(*exp))) Edouard@1014: Edouard@1014: def PythonRuntimeInit(self): Edouard@1014: MethodNames = ["init", "start", "stop", "cleanup"] Edouard@1014: self.python_runtime_vars = globals().copy() Edouard@1014: self.python_runtime_vars["WorkingDir"] = self.workingdir Edouard@1014: self.python_runtime_vars["website"] = self.website Edouard@1014: for methodname in MethodNames : Edouard@1014: self.python_runtime_vars["_runtime_%s"%methodname] = [] Edouard@1014: self.python_runtime_vars["PLCObject"] = self Edouard@1014: self.python_runtime_vars["PLCBinary"] = self.PLClibraryHandle Edouard@1144: class PLCSafeGlobals: Edouard@1145: def __getattr__(_self, name): Edouard@1156: try : Edouard@1156: t = self.python_runtime_vars["_"+name+"_ctype"] Edouard@1156: except KeyError: Edouard@1156: raise KeyError("Try to get unknown shared global variable : %s"%name) Edouard@1156: v = t() Edouard@1145: r = self.python_runtime_vars["_PySafeGetPLCGlob_"+name](ctypes.byref(v)) Edouard@1145: return self.python_runtime_vars["_"+name+"_unpack"](v) Edouard@1145: def __setattr__(_self, name, value): Edouard@1156: try : Edouard@1156: t = self.python_runtime_vars["_"+name+"_ctype"] Edouard@1156: except KeyError: Edouard@1156: raise KeyError("Try to set unknown shared global variable : %s"%name) Edouard@1145: v = self.python_runtime_vars["_"+name+"_pack"](t,value) Edouard@1145: self.python_runtime_vars["_PySafeSetPLCGlob_"+name](ctypes.byref(v)) Edouard@1144: self.python_runtime_vars["PLCGlobals"] = PLCSafeGlobals() Edouard@972: try: Edouard@972: for filename in os.listdir(self.workingdir): Edouard@972: name, ext = os.path.splitext(filename) Edouard@972: if name.upper().startswith("RUNTIME") and ext.upper() == ".PY": Edouard@1014: execfile(os.path.join(self.workingdir, filename), self.python_runtime_vars) Edouard@1014: for methodname in MethodNames: Edouard@1014: method = self.python_runtime_vars.get("_%s_%s" % (name, methodname), None) Edouard@1014: if method is not None: Edouard@1014: self.python_runtime_vars["_runtime_%s"%methodname].append(method) Edouard@972: except: Edouard@972: self.LogMessage(0,traceback.format_exc()) Edouard@972: raise laurent@368: Edouard@1014: self.PythonRuntimeCall("init") Edouard@1014: laurent@368: if self.website is not None: laurent@368: self.website.PLCStarted() etisserant@291: Edouard@1014: Edouard@1014: def PythonRuntimeCleanup(self): Edouard@1014: if self.python_runtime_vars is not None: Edouard@1014: self.PythonRuntimeCall("cleanup") Edouard@1014: laurent@368: if self.website is not None: laurent@368: self.website.PLCStopped() Edouard@1014: Edouard@1014: self.python_runtime_vars = None etisserant@291: edouard@462: def PythonThreadProc(self): laurent@798: self.PLCStatus = "Started" laurent@798: self.StatusChange() laurent@798: self.StartSem.release() Edouard@1014: self.PythonRuntimeCall("start") Edouard@851: res,cmd,blkid = "None","None",ctypes.c_void_p() Edouard@867: compile_cache={} laurent@798: while True: Edouard@851: # print "_PythonIterator(", res, ")", Edouard@851: cmd = self._PythonIterator(res,blkid) Edouard@867: FBID = blkid.value Edouard@851: # print " -> ", cmd, blkid laurent@798: if cmd is None: laurent@798: break laurent@798: try : Edouard@1014: self.python_runtime_vars["FBID"]=FBID Edouard@867: ccmd,AST =compile_cache.get(FBID, (None,None)) Edouard@867: if ccmd is None or ccmd!=cmd: Edouard@867: AST = compile(cmd, '', 'eval') Edouard@867: compile_cache[FBID]=(cmd,AST) Edouard@1014: result,exp = self.evaluator(eval,cmd,self.python_runtime_vars) Edouard@867: if exp is not None: Edouard@1052: res = "#EXCEPTION : "+str(exp[1]) Edouard@1052: self.LogMessage(1,('PyEval@0x%x(Code="%s") Exception "%s"')%(FBID,cmd, Edouard@1052: '\n'.join(traceback.format_exception(*exp)))) Edouard@867: else: Edouard@867: res=str(result) Edouard@1014: self.python_runtime_vars["FBID"]=None laurent@798: except Exception,e: laurent@798: res = "#EXCEPTION : "+str(e) Edouard@972: self.LogMessage(1,('PyEval@0x%x(Code="%s") Exception "%s"')%(FBID,cmd,str(e))) laurent@798: self.PLCStatus = "Stopped" laurent@798: self.StatusChange() Edouard@1014: self.PythonRuntimeCall("stop") greg@350: edouard@462: def StartPLC(self): edouard@465: if self.CurrentPLCFilename is not None and self.PLCStatus == "Stopped": laurent@798: c_argv = ctypes.c_char_p * len(self.argv) laurent@798: error = None Edouard@906: res = self._startPLC(len(self.argv),c_argv(*self.argv)) Edouard@906: if res == 0: Edouard@906: self.StartSem=Semaphore(0) Edouard@906: self.PythonThread = Thread(target=self.PythonThreadProc) Edouard@906: self.PythonThread.start() Edouard@906: self.StartSem.acquire() Edouard@917: self.LogMessage("PLC started") laurent@798: else: Edouard@972: self.LogMessage(0,_("Problem starting PLC : error %d" % res)) laurent@798: self.PLCStatus = "Broken" laurent@798: self.StatusChange() greg@352: greg@229: def StopPLC(self): greg@229: if self.PLCStatus == "Started": Edouard@917: self.LogMessage("PLC stopped") greg@352: self._stopPLC() laurent@798: self.PythonThread.join() greg@229: return True greg@229: return False greg@229: greg@229: def _Reload(self): greg@229: self.daemon.shutdown(True) greg@229: self.daemon.sock.close() greg@229: os.execv(sys.executable,[sys.executable]+sys.argv[:]) greg@229: # never reached greg@229: return 0 greg@229: greg@229: def ForceReload(self): greg@229: # respawn python interpreter greg@229: Timer(0.1,self._Reload).start() greg@229: return True greg@229: greg@229: def GetPLCstatus(self): Edouard@917: return self.PLCStatus, map(self.GetLogCount,xrange(LogLevelsCount)) greg@229: greg@229: def NewPLC(self, md5sum, data, extrafiles): laurent@393: if self.PLCStatus in ["Stopped", "Empty", "Broken"]: greg@229: NewFileName = md5sum + lib_ext greg@229: extra_files_log = os.path.join(self.workingdir,"extra_files.txt") Edouard@906: Edouard@1045: self.UnLoadPLC() Edouard@1045: Laurent@1011: self.LogMessage("NewPLC (%s)"%md5sum) Edouard@906: self.PLCStatus = "Empty" Edouard@906: greg@229: try: greg@229: os.remove(os.path.join(self.workingdir, greg@229: self.CurrentPLCFilename)) laurent@364: for filename in file(extra_files_log, "r").readlines() + [extra_files_log]: greg@229: try: laurent@364: os.remove(os.path.join(self.workingdir, filename.strip())) greg@229: except: greg@229: pass greg@229: except: greg@229: pass greg@229: greg@229: try: greg@229: # Create new PLC file greg@229: open(os.path.join(self.workingdir,NewFileName), greg@229: 'wb').write(data) greg@229: greg@229: # Store new PLC filename based on md5 key greg@229: open(self._GetMD5FileName(), "w").write(md5sum) greg@229: greg@229: # Then write the files greg@229: log = file(extra_files_log, "w") greg@229: for fname,fdata in extrafiles: greg@229: fpath = os.path.join(self.workingdir,fname) greg@229: open(fpath, "wb").write(fdata) greg@229: log.write(fname+'\n') greg@229: greg@229: # Store new PLC filename greg@229: self.CurrentPLCFilename = NewFileName greg@229: except: Edouard@906: self.PLCStatus = "Broken" Edouard@906: self.StatusChange() etisserant@291: PLCprint(traceback.format_exc()) greg@229: return False Edouard@906: Edouard@1027: if self.LoadPLC(): greg@229: self.PLCStatus = "Stopped" Edouard@906: else: Edouard@1035: self.PLCStatus = "Broken" Edouard@906: self._FreePLC() Edouard@906: self.StatusChange() Edouard@906: Edouard@906: return self.PLCStatus == "Stopped" greg@229: return False greg@229: greg@229: def MatchMD5(self, MD5): greg@229: try: greg@229: last_md5 = open(self._GetMD5FileName(), "r").read() greg@229: return last_md5 == MD5 greg@229: except: greg@229: return False greg@229: edouard@477: Edouard@592: greg@229: def SetTraceVariablesList(self, idxs): greg@229: """ greg@229: Call ctype imported function to append greg@229: these indexes to registred variables in PLC debugger greg@229: """ edouard@462: if idxs: edouard@462: # suspend but dont disable Edouard@614: if self._suspendDebug(False) == 0: Edouard@614: # keep a copy of requested idx Edouard@614: self._Idxs = idxs[:] Edouard@614: self._ResetDebugVariables() Edouard@614: for idx,iectype,force in idxs: Edouard@614: if force !=None: Edouard@614: c_type,unpack_func, pack_func = \ Edouard@614: TypeTranslator.get(iectype, Edouard@614: (None,None,None)) Edouard@614: force = ctypes.byref(pack_func(c_type,force)) Edouard@614: self._RegisterDebugVariable(idx, force) Edouard@614: self._resumeDebug() edouard@462: else: edouard@462: self._suspendDebug(True) edouard@462: self._Idxs = [] etisserant@280: greg@229: def GetTraceVariables(self): greg@229: """ greg@339: Return a list of variables, corresponding to the list of required idx greg@229: """ etisserant@286: if self.PLCStatus == "Started": edouard@450: tick = ctypes.c_uint32() edouard@450: size = ctypes.c_uint32() Edouard@1075: buff = ctypes.c_void_p() Edouard@1075: TraceVariables = None laurent@795: if self.PLClibraryLock.acquire(False): laurent@795: if self._GetDebugData(ctypes.byref(tick), laurent@795: ctypes.byref(size), Edouard@1075: ctypes.byref(buff)) == 0: laurent@795: if size.value: Edouard@1075: TraceVariables = UnpackDebugBuffer(buff, size.value, self._Idxs) laurent@795: self._FreeDebugData() edouard@483: self.PLClibraryLock.release() Edouard@1075: if TraceVariables is not None: Edouard@1075: return self.PLCStatus, tick.value, TraceVariables Edouard@917: return self.PLCStatus, None, [] Edouard@592: laurent@699: def RemoteExec(self, script, **kwargs): laurent@699: try: laurent@699: exec script in kwargs laurent@699: except: laurent@699: e_type, e_value, e_traceback = sys.exc_info() laurent@699: line_no = traceback.tb_lineno(get_last_traceback(e_traceback)) laurent@699: return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" % laurent@699: (line_no, e_value, script.splitlines()[line_no - 1])) laurent@699: return (0, kwargs.get("returnVal", None)) laurent@699: laurent@699: