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 greg@352: from threading import Timer, Thread, Lock etisserant@301: import ctypes, os, commands, types, sys 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 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 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 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: 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: greg@229: def _LoadNewPLC(self): greg@229: """ greg@229: Load PLC library greg@229: Declare all functions, arguments and return values greg@229: """ etisserant@291: PLCprint("Load PLC") 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 laurent@366: self._PythonIterator.argtypes = [ctypes.c_char_p] laurent@366: laurent@366: def StopPLCLock(): laurent@366: self.PLClibraryLock.acquire() ed@455: self._stopPLC_real() laurent@366: self.PLClibraryLock.release() laurent@366: laurent@366: else: ed@455: # If python plugin 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() ed@455: def PythonIterator(res): ed@455: self.PythonIteratorLock.acquire() ed@455: self.PythonIteratorLock.release() laurent@366: return None ed@455: self._PythonIterator = PythonIterator laurent@366: laurent@366: def StopPLCLock(): laurent@366: self.PLClibraryLock.acquire() ed@455: self._stopPLC_real() ed@455: self.PythonIteratorLock.release() laurent@366: self.PLClibraryLock.release() greg@352: greg@352: self._stopPLC = StopPLCLock 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 etisserant@235: self._RegisterDebugVariable.argtypes = [ctypes.c_int] 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 etisserant@235: self._suspendDebug.restype = None edouard@462: self._suspendDebug.argtypes = [ctypes.c_int] etisserant@235: etisserant@235: self._resumeDebug = self.PLClibraryHandle.resumeDebug etisserant@235: self._resumeDebug.restype = None etisserant@235: greg@229: return True greg@229: except: etisserant@291: PLCprint(traceback.format_exc()) greg@229: return False greg@229: 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 greg@229: self._startPLC = lambda:None greg@229: self._stopPLC = lambda:None greg@229: self._ResetDebugVariables = lambda:None greg@229: self._RegisterDebugVariable = lambda x:None greg@229: self._IterDebugData = lambda x,y:None greg@229: self._FreeDebugData = lambda:None edouard@450: self._GetDebugData = lambda:-1 etisserant@235: self._suspendDebug = lambda:None etisserant@235: self._resumeDebug = lambda:None etisserant@280: self._PythonIterator = lambda:"" 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: etisserant@299: def PrepareRuntimePy(self): etisserant@290: self.python_threads_vars = globals().copy() greg@344: self.python_threads_vars["WorkingDir"] = self.workingdir laurent@368: self.python_threads_vars["website"] = self.website laurent@366: self.python_threads_vars["_runtime_begin"] = [] laurent@366: self.python_threads_vars["_runtime_cleanup"] = [] laurent@368: laurent@366: for filename in os.listdir(self.workingdir): laurent@366: name, ext = os.path.splitext(filename) greg@391: if name.upper().startswith("RUNTIME") and ext.upper() == ".PY": laurent@366: try: laurent@366: # TODO handle exceptions in runtime.py laurent@366: # pyfile may redefine _runtime_cleanup laurent@366: # or even call _PythonThreadProc itself. laurent@366: execfile(os.path.join(self.workingdir, filename), self.python_threads_vars) laurent@366: except: laurent@366: PLCprint(traceback.format_exc()) laurent@366: runtime_begin = self.python_threads_vars.get("_%s_begin" % name, None) laurent@366: if runtime_begin is not None: laurent@366: self.python_threads_vars["_runtime_begin"].append(runtime_begin) laurent@366: runtime_cleanup = self.python_threads_vars.get("_%s_cleanup" % name, None) laurent@366: if runtime_cleanup is not None: laurent@366: self.python_threads_vars["_runtime_cleanup"].append(runtime_cleanup) laurent@366: laurent@366: for runtime_begin in self.python_threads_vars.get("_runtime_begin", []): greg@329: runtime_begin() laurent@368: laurent@368: if self.website is not None: laurent@368: self.website.PLCStarted() etisserant@291: etisserant@291: def FinishRuntimePy(self): laurent@366: for runtime_cleanup in self.python_threads_vars.get("_runtime_cleanup", []): laurent@366: runtime_cleanup() laurent@368: if self.website is not None: laurent@368: self.website.PLCStopped() etisserant@291: self.python_threads_vars = None etisserant@291: edouard@462: def PythonThreadProc(self): etisserant@291: PLCprint("PythonThreadProc started") greg@350: c_argv = ctypes.c_char_p * len(self.argv) greg@350: error = None greg@350: if self._LoadNewPLC(): greg@350: if self._startPLC(len(self.argv),c_argv(*self.argv)) == 0: greg@229: self.PLCStatus = "Started" etisserant@286: self.StatusChange() greg@350: self.evaluator(self.PrepareRuntimePy) greg@350: res,cmd = "None","None" greg@352: while True: greg@350: #print "_PythonIterator(", res, ")", greg@350: cmd = self._PythonIterator(res) greg@350: #print " -> ", cmd greg@350: if cmd is None: greg@350: break greg@350: try : greg@350: res = str(self.evaluator(eval,cmd,self.python_threads_vars)) greg@350: except Exception,e: greg@350: res = "#EXCEPTION : "+str(e) greg@350: PLCprint(res) greg@350: self.PLCStatus = "Stopped" greg@350: self.StatusChange() greg@350: self.evaluator(self.FinishRuntimePy) greg@229: else: greg@350: error = "starting" greg@350: else: greg@350: error = "loading" greg@350: if error is not None: greg@350: PLCprint("Problem %s PLC"%error) greg@350: self.PLCStatus = "Broken" greg@350: self._FreePLC() greg@350: PLCprint("PythonThreadProc interrupted") greg@350: edouard@462: def StartPLC(self): greg@350: PLCprint("StartPLC") greg@352: if self.CurrentPLCFilename is not None: ed@446: self.PLCStatus = "Started" edouard@462: self.PythonThread = Thread(target=self.PythonThreadProc) greg@350: self.PythonThread.start() greg@352: greg@229: def StopPLC(self): greg@350: PLCprint("StopPLC") greg@229: if self.PLCStatus == "Started": greg@352: self._stopPLC() 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): greg@229: return self.PLCStatus greg@229: greg@229: def NewPLC(self, md5sum, data, extrafiles): etisserant@291: PLCprint("NewPLC (%s)"%md5sum) 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") 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: etisserant@291: PLCprint(traceback.format_exc()) greg@229: return False greg@229: if self.PLCStatus == "Empty": greg@229: self.PLCStatus = "Stopped" greg@229: return True 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: 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@462: self._suspendDebug(False) edouard@462: # keep a copy of requested idx edouard@462: self._Idxs = idxs[:] edouard@462: self._ResetDebugVariables() edouard@462: for idx,iectype in idxs: edouard@462: self._RegisterDebugVariable(idx) edouard@462: self._resumeDebug() edouard@462: else: edouard@462: self._suspendDebug(True) edouard@462: self._Idxs = [] etisserant@280: etisserant@280: class IEC_STRING(ctypes.Structure): etisserant@280: """ etisserant@280: Must be changed according to changes in iec_types.h etisserant@280: """ etisserant@280: _fields_ = [("len", ctypes.c_uint8), edouard@459: ("body", ctypes.c_char * 126)] greg@229: etisserant@238: TypeTranslator = {"BOOL" : (ctypes.c_uint8, lambda x:x.value!=0), etisserant@238: "STEP" : (ctypes.c_uint8, lambda x:x.value), etisserant@238: "TRANSITION" : (ctypes.c_uint8, lambda x:x.value), etisserant@238: "ACTION" : (ctypes.c_uint8, lambda x:x.value), etisserant@238: "SINT" : (ctypes.c_int8, lambda x:x.value), etisserant@238: "USINT" : (ctypes.c_uint8, lambda x:x.value), etisserant@238: "BYTE" : (ctypes.c_uint8, lambda x:x.value), etisserant@280: "STRING" : (IEC_STRING, lambda x:x.body[:x.len]), etisserant@238: "INT" : (ctypes.c_int16, lambda x:x.value), etisserant@238: "UINT" : (ctypes.c_uint16, lambda x:x.value), etisserant@238: "WORD" : (ctypes.c_uint16, lambda x:x.value), etisserant@238: "WSTRING" : (None, None),#TODO etisserant@238: "DINT" : (ctypes.c_int32, lambda x:x.value), etisserant@238: "UDINT" : (ctypes.c_uint32, lambda x:x.value), etisserant@238: "DWORD" : (ctypes.c_uint32, lambda x:x.value), etisserant@238: "LINT" : (ctypes.c_int64, lambda x:x.value), etisserant@238: "ULINT" : (ctypes.c_uint64, lambda x:x.value), etisserant@238: "LWORD" : (ctypes.c_uint64, lambda x:x.value), etisserant@238: "REAL" : (ctypes.c_float, lambda x:x.value), etisserant@238: "LREAL" : (ctypes.c_double, lambda x:x.value), greg@229: } greg@229: 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": ed@455: res=[] greg@353: self.PLClibraryLock.acquire() edouard@450: tick = ctypes.c_uint32() edouard@450: size = ctypes.c_uint32() edouard@450: buffer = ctypes.c_void_p() edouard@450: if self._GetDebugData(ctypes.byref(tick),ctypes.byref(size),ctypes.byref(buffer)) == 0 : edouard@450: offset = 0 edouard@450: for idx, iectype in self._Idxs: edouard@450: cursor = ctypes.c_void_p(buffer.value + offset) edouard@450: c_type,unpack_func = self.TypeTranslator.get(iectype, (None,None)) edouard@450: if c_type is not None and offset < size: edouard@450: res.append(unpack_func(ctypes.cast(cursor, etisserant@286: ctypes.POINTER(c_type)).contents)) edouard@459: offset += ctypes.sizeof(c_type) etisserant@286: else: edouard@460: if c_type is None: edouard@460: PLCprint("Debug error - " + iectype + " not supported !") edouard@460: if offset >= size: edouard@460: PLCprint("Debug error - buffer too small !") edouard@450: break etisserant@286: self._FreeDebugData() greg@353: self.PLClibraryLock.release() edouard@461: if offset == size.value: edouard@460: return self.PLCStatus, tick.value, res edouard@460: else: edouard@460: PLCprint("Debug error - bad buffer unpack !") ed@446: return self.PLCStatus, None, None ed@446: