# HG changeset patch # User Edouard Tisserant # Date 1422646944 -3600 # Node ID 6e0cd0ceabb78d30b7e8d885ac80c483640816fd # Parent 4a45f6642523c1781e74a0075b2523aeb5cc20a0 Added runtime side trace buffer, handled in a separate thread, limited to 1MB, and dropped after 3 seconds if not used by IDE. GetTraceVariables is not anymore blocking on next PLC cycle diff -r 4a45f6642523 -r 6e0cd0ceabb7 Beremiz_service.py --- a/Beremiz_service.py Fri Jan 30 10:45:11 2015 +0100 +++ b/Beremiz_service.py Fri Jan 30 20:42:24 2015 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- #This file is part of Beremiz, a Integrated Development Environment for -#programming IEC 61131-3 automates supporting plcopen standard and CanFestival. +#programming IEC 61131-3 automates supporting plcopen standard and CanFestival. # #Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD # @@ -36,7 +36,7 @@ -a - autostart PLC (0:disable 1:enable) (default:0) -x - enable/disable wxTaskbarIcon (0:disable 1:enable) (default:1) -t - enable/disable Twisted web interface (0:disable 1:enable) (default:1) - + working_dir - directory where are stored PLC files """%sys.argv[0] @@ -83,6 +83,8 @@ usage() sys.exit() +CWD = os.path.split(os.path.realpath(__file__))[0] + if len(argv) > 1: usage() sys.exit() @@ -109,14 +111,13 @@ if havewx: app=wx.App(redirect=False) - + # Import module for internationalization import gettext - - CWD = os.path.split(os.path.realpath(__file__))[0] + def Bpath(*args): return os.path.join(CWD,*args) - + # Get folder containing translation files localedir = os.path.join(CWD,"locale") # Get the default language @@ -139,11 +140,11 @@ if __name__ == '__main__': __builtin__.__dict__['_'] = wx.GetTranslation#unicode_translation - + defaulticon = wx.Image(Bpath("images", "brz.png")) starticon = wx.Image(Bpath("images", "icoplay24.png")) stopicon = wx.Image(Bpath("images", "icostop24.png")) - + class ParamsEntryDialog(wx.TextEntryDialog): if wx.VERSION < (2, 6, 0): def Bind(self, event, function, id = None): @@ -151,12 +152,12 @@ event(self, id, function) else: event(self, function) - - - def __init__(self, parent, message, caption = "Please enter text", defaultValue = "", + + + def __init__(self, parent, message, caption = "Please enter text", defaultValue = "", style = wx.OK|wx.CANCEL|wx.CENTRE, pos = wx.DefaultPosition): wx.TextEntryDialog.__init__(self, parent, message, caption, defaultValue, style, pos) - + self.Tests = [] if wx.VERSION >= (2, 8, 0): self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetAffirmativeId()) @@ -164,7 +165,7 @@ self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetSizer().GetItem(3).GetSizer().GetAffirmativeButton().GetId()) else: self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetSizer().GetItem(3).GetSizer().GetChildren()[0].GetSizer().GetChildren()[0].GetWindow().GetId()) - + def OnOK(self, event): value = self.GetValue() texts = {"value" : value} @@ -176,13 +177,13 @@ return self.EndModal(wx.ID_OK) event.Skip() - + def GetValue(self): return self.GetSizer().GetItem(1).GetWindow().GetValue() - + def SetTests(self, tests): self.Tests = tests - + class BeremizTaskBarIcon(wx.TaskBarIcon): TBMENU_START = wx.NewId() TBMENU_STOP = wx.NewId() @@ -193,14 +194,14 @@ TBMENU_WXINSPECTOR = wx.NewId() TBMENU_CHANGE_WD = wx.NewId() TBMENU_QUIT = wx.NewId() - + def __init__(self, pyroserver, level): wx.TaskBarIcon.__init__(self) self.pyroserver = pyroserver # Set the image self.UpdateIcon(None) self.level = level - + # bind some events self.Bind(wx.EVT_MENU, self.OnTaskBarStartPLC, id=self.TBMENU_START) self.Bind(wx.EVT_MENU, self.OnTaskBarStopPLC, id=self.TBMENU_STOP) @@ -211,7 +212,7 @@ self.Bind(wx.EVT_MENU, self.OnTaskBarChangePort, id=self.TBMENU_CHANGE_PORT) self.Bind(wx.EVT_MENU, self.OnTaskBarChangeWorkingDir, id=self.TBMENU_CHANGE_WD) self.Bind(wx.EVT_MENU, self.OnTaskBarQuit, id=self.TBMENU_QUIT) - + def CreatePopupMenu(self): """ This method is called by the base class when it needs to popup @@ -234,7 +235,7 @@ menu.AppendSeparator() menu.Append(self.TBMENU_QUIT, _("Quit")) return menu - + def MakeIcon(self, img): """ The various platforms have different requirements for the @@ -247,15 +248,15 @@ # wxMac can be any size upto 128x128, so leave the source img alone.... icon = wx.IconFromBitmap(img.ConvertToBitmap() ) return icon - + def OnTaskBarStartPLC(self, evt): - if self.pyroserver.plcobj is not None: + if self.pyroserver.plcobj is not None: self.pyroserver.plcobj.StartPLC() - + def OnTaskBarStopPLC(self, evt): if self.pyroserver.plcobj is not None: Thread(target=self.pyroserver.plcobj.StopPLC).start() - + def OnTaskBarChangeInterface(self, evt): dlg = ParamsEntryDialog(None, _("Enter the IP of the interface to bind"), defaultValue=self.pyroserver.ip_addr) dlg.SetTests([(re.compile('\d{1,3}(?:\.\d{1,3}){3}$').match, _("IP is not valid!")), @@ -264,38 +265,38 @@ if dlg.ShowModal() == wx.ID_OK: self.pyroserver.ip_addr = dlg.GetValue() self.pyroserver.Stop() - + def OnTaskBarChangePort(self, evt): dlg = ParamsEntryDialog(None, _("Enter a port number "), defaultValue=str(self.pyroserver.port)) dlg.SetTests([(UnicodeType.isdigit, _("Port number must be an integer!")), (lambda port : 0 <= int(port) <= 65535 , _("Port number must be 0 <= port <= 65535!"))]) if dlg.ShowModal() == wx.ID_OK: self.pyroserver.port = int(dlg.GetValue()) self.pyroserver.Stop() - + def OnTaskBarChangeWorkingDir(self, evt): dlg = wx.DirDialog(None, _("Choose a working directory "), self.pyroserver.workdir, wx.DD_NEW_DIR_BUTTON) if dlg.ShowModal() == wx.ID_OK: self.pyroserver.workdir = dlg.GetPath() self.pyroserver.Stop() - + def OnTaskBarChangeName(self, evt): dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=self.pyroserver.name) dlg.SetTests([(lambda name : len(name) is not 0 , _("Name must not be null!"))]) if dlg.ShowModal() == wx.ID_OK: self.pyroserver.name = dlg.GetValue() self.pyroserver.Restart() - + def _LiveShellLocals(self): if self.pyroserver.plcobj is not None: return {"locals":self.pyroserver.plcobj.python_runtime_vars} else: return {} - + def OnTaskBarLiveShell(self, evt): from wx import py frame = py.crust.CrustFrame(**self._LiveShellLocals()) frame.Show() - + def OnTaskBarWXInspector(self, evt): # Activate the widget inspection tool from wx.lib.inspection import InspectionTool @@ -304,13 +305,13 @@ wnd = wx.GetApp() InspectionTool().Show(wnd, True) - + def OnTaskBarQuit(self, evt): if wx.Platform == '__WXMSW__': Thread(target=self.pyroserver.Quit).start() self.RemoveIcon() wx.CallAfter(wx.GetApp().ExitMainLoop) - + def UpdateIcon(self, plcstatus): if plcstatus is "Started" : currenticon = self.MakeIcon(starticon) @@ -348,11 +349,11 @@ self.statuschange = statuschange self.evaluator = evaluator self.website = website - + def Loop(self): while self.continueloop: self.Start() - + def Restart(self): self.Stop() @@ -367,28 +368,28 @@ self.daemon=pyro.Daemon(host=self.ip_addr, port=self.port) self.plcobj = PLCObject(self.workdir, self.daemon, self.argv, self.statuschange, self.evaluator, self.website) uri = self.daemon.connect(self.plcobj,"PLCObject") - + print "Pyro port :",self.port print "Pyro object's uri :",uri print "Current working directory :",self.workdir - + # Configure and publish service # Not publish service if localhost in address params - if (self.servicename is not None and - self.ip_addr is not None and - self.ip_addr != "localhost" and + if (self.servicename is not None and + self.ip_addr is not None and + self.ip_addr != "localhost" and self.ip_addr != "127.0.0.1"): print "Publishing service on local network" self.servicepublisher = ServicePublisher.ServicePublisher() self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port) - + if self.autostart and self.plcobj.GetPLCstatus()[0] != "Empty": self.plcobj.StartPLC() - + sys.stdout.flush() - + self.daemon.requestLoop() - + def Stop(self): if self.plcobj is not None: self.plcobj.StopPLC() @@ -410,78 +411,78 @@ from twisted.python import log, util from nevow import rend, appserver, inevow, tags, loaders, athena from nevow.page import renderer - + havetwisted = True except: print "Twisted unavailable !" havetwisted = False if havetwisted: - + xhtml_header = ''' ''' class PLCHMI(athena.LiveElement): - + initialised = False - + def HMIinitialised(self, result): self.initialised = True - + def HMIinitialisation(self): self.HMIinitialised(None) - + class DefaultPLCStartedHMI(PLCHMI): - docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[ + docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[ tags.h1["PLC IS NOW STARTED"], ]) - + class PLCStoppedHMI(PLCHMI): docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[ tags.h1["PLC IS STOPPED"], ]) - + class MainPage(athena.LiveElement): jsClass = u"WebInterface.PLC" docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))[ - tags.div(id='content')[ + tags.div(id='content')[ tags.div(render = tags.directive('PLCElement')), ]]) - + def __init__(self, *a, **kw): athena.LiveElement.__init__(self, *a, **kw) self.pcl_state = False self.HMI = None self.resetPLCStartedHMI() - + def setPLCState(self, state): self.pcl_state = state if self.HMI is not None: self.callRemote('updateHMI') - + def setPLCStartedHMI(self, hmi): self.PLCStartedHMIClass = hmi - + def resetPLCStartedHMI(self): self.PLCStartedHMIClass = DefaultPLCStartedHMI - + def getHMI(self): return self.HMI - + def HMIexec(self, function, *args, **kwargs): if self.HMI is not None: getattr(self.HMI, function, lambda:None)(*args, **kwargs) athena.expose(HMIexec) - + def resetHMI(self): self.HMI = None - + def PLCElement(self, ctx, data): return self.getPLCElement() renderer(PLCElement) - + def getPLCElement(self): self.detachFragmentChildren() if self.pcl_state: @@ -496,7 +497,7 @@ def detachFragmentChildren(self): for child in self.liveFragmentChildren[:]: child.detach() - + class WebInterface(athena.LivePage): docFactory = loaders.stan([tags.raw(xhtml_header), @@ -508,7 +509,7 @@ ]]]]) MainPage = MainPage() PLCHMI = PLCHMI - + def __init__(self, plcState=False, *a, **kw): super(WebInterface, self).__init__(*a, **kw) self.jsModules.mapping[u'WebInterface'] = util.sibpath(__file__, os.path.join('runtime', 'webinterface.js')) @@ -517,23 +518,23 @@ def getHMI(self): return self.MainPage.getHMI() - + def LoadHMI(self, hmi, jsmodules): for name, path in jsmodules.iteritems(): self.jsModules.mapping[name] = os.path.join(WorkingDir, path) self.MainPage.setPLCStartedHMI(hmi) - + def UnLoadHMI(self): self.MainPage.resetPLCStartedHMI() - + def PLCStarted(self): self.plcState = True self.MainPage.setPLCState(True) - + def PLCStopped(self): self.plcState = False self.MainPage.setPLCState(False) - + def renderHTTP(self, ctx): """ Force content type to fit with SVG @@ -550,21 +551,21 @@ def child_(self, ctx): self.MainPage.detachFragmentChildren() return WebInterface(plcState=self.plcState) - + def beforeRender(self, ctx): d = self.notifyOnDisconnect() d.addErrback(self.disconnected) - + def disconnected(self, reason): self.MainPage.resetHMI() #print reason #print "We will be called back when the client disconnects" - + if havewx: reactor.registerWxApp(app) website = WebInterface() site = appserver.NevowSite(website) - + website_port = 8009 listening = False while not listening: @@ -584,23 +585,23 @@ def statuschange(status): wx.CallAfter(taskbar_instance.UpdateIcon,status) - + def wx_evaluator(obj, *args, **kwargs): tocall,args,kwargs = obj.call obj.res = default_evaluator(tocall, *args, **kwargs) wx_eval_lock.release() - + def evaluator(tocall, *args, **kwargs): global main_thread if(main_thread == currentThread()): - # avoid dead lock if called from the wx mainloop + # avoid dead lock if called from the wx mainloop return default_evaluator(tocall, *args, **kwargs) else: o=type('',(object,),dict(call=(tocall, args, kwargs), res=None)) wx.CallAfter(wx_evaluator,o) wx_eval_lock.acquire() return o.res - + pyroserver = Server(servicename, given_ip, port, WorkingDir, argv, autostart, statuschange, evaluator, website) taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx) else: diff -r 4a45f6642523 -r 6e0cd0ceabb7 ProjectController.py --- a/ProjectController.py Fri Jan 30 10:45:11 2015 +0100 +++ b/ProjectController.py Fri Jan 30 20:42:24 2015 +0100 @@ -36,9 +36,6 @@ MATIEC_ERROR_MODEL = re.compile(".*\.st:(\d+)-(\d+)\.\.(\d+)-(\d+): (?:error)|(?:warning) : (.*)$") -DEBUG_RETRIES_WARN = 3 -DEBUG_RETRIES_REREGISTER = 4 - ITEM_CONFNODE = 25 def ExtractChildrenTypesFromCatalog(catalog): @@ -1392,41 +1389,34 @@ self.debug_break = False debug_getvar_retry = 0 while (not self.debug_break) and (self._connector is not None): - Trace = self._connector.GetTraceVariables() - if(Trace): - plc_status, debug_tick, debug_buff = Trace - else: - plc_status = None + plc_status, Traces = self._connector.GetTraceVariables() debug_getvar_retry += 1 #print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()] - if plc_status == "Started" and debug_buff is not None: - self.IECdebug_lock.acquire() - debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes) - if (debug_tick is not None and debug_vars is not None and - len(debug_vars) == len(self.TracedIECPath)): - if debug_getvar_retry > DEBUG_RETRIES_WARN: - self.logger.write(_("... debugger recovered\n")) - debug_getvar_retry = 0 - for IECPath, values_buffer, value in izip( - self.TracedIECPath, - self.DebugValuesBuffers, - debug_vars): - IECdebug_data = self.IECdebug_datas.get(IECPath, None) #FIXME get - if IECdebug_data is not None and value is not None: - forced = IECdebug_data[2:4] == ["Forced", value] - if not IECdebug_data[4] and len(values_buffer) > 0: - values_buffer[-1] = (value, forced) - else: - values_buffer.append((value, forced)) - self.DebugTicks.append(debug_tick) - self.IECdebug_lock.release() - if debug_getvar_retry == DEBUG_RETRIES_WARN: - self.logger.write(_("Waiting debugger to recover...\n")) - if debug_getvar_retry == DEBUG_RETRIES_REREGISTER: - # re-register debug registry to PLC - wx.CallAfter(self.RegisterDebugVarToConnector) + if plc_status == "Started" : + if len(Traces) > 0: + Failed = False + self.IECdebug_lock.acquire() + for debug_tick, debug_buff in Traces : + debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes) + if (debug_vars is not None and + len(debug_vars) == len(self.TracedIECPath)): + for IECPath, values_buffer, value in izip( + self.TracedIECPath, + self.DebugValuesBuffers, + debug_vars): + IECdebug_data = self.IECdebug_datas.get(IECPath, None) #FIXME get + if IECdebug_data is not None and value is not None: + forced = IECdebug_data[2:4] == ["Forced", value] + if not IECdebug_data[4] and len(values_buffer) > 0: + values_buffer[-1] = (value, forced) + else: + values_buffer.append((value, forced)) + self.DebugTicks.append(debug_tick) + debug_getvar_retry = 0 + self.IECdebug_lock.release() + if debug_getvar_retry != 0: - # Be patient, tollerate PLC to come up before debugging + # Be patient, tollerate PLC to come with fresh samples time.sleep(0.1) else: self.debug_break = True diff -r 4a45f6642523 -r 6e0cd0ceabb7 connectors/PYRO/__init__.py --- a/connectors/PYRO/__init__.py Fri Jan 30 10:45:11 2015 +0100 +++ b/connectors/PYRO/__init__.py Fri Jan 30 20:42:24 2015 +0100 @@ -56,7 +56,7 @@ confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n")%location) confnodesroot.logger.write_error(traceback.format_exc()) return None - + # Try to get the proxy object try : RemotePLCObjectProxy = pyro.getAttrProxyForURI("PYROLOC://"+location+"/PLCObject") @@ -87,7 +87,7 @@ return default return catcher_func - # Check connection is effective. + # Check connection is effective. # lambda is for getattr of GetPLCstatus to happen inside catcher if PyroCatcher(lambda:RemotePLCObjectProxy.GetPLCstatus())() is None: confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n")) @@ -112,9 +112,9 @@ def _PyroStartPLC(self, *args, **kwargs): """ - confnodesroot._connector.GetPyroProxy() is used + confnodesroot._connector.GetPyroProxy() is used rather than RemotePLCObjectProxy because - object is recreated meanwhile, + object is recreated meanwhile, so we must not keep ref to it here """ current_status, log_count = confnodesroot._connector.GetPyroProxy().GetPLCstatus() @@ -141,7 +141,7 @@ if self.RemotePLCObjectProxyCopy is None: self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy()) return self.RemotePLCObjectProxyCopy.GetTraceVariables() - GetTraceVariables = PyroCatcher(_PyroGetTraceVariables,("Broken",None,None)) + GetTraceVariables = PyroCatcher(_PyroGetTraceVariables,("Broken",None)) def _PyroGetPLCstatus(self): return RemotePLCObjectProxy.GetPLCstatus() @@ -161,5 +161,5 @@ return member return PyroProxyProxy() - + diff -r 4a45f6642523 -r 6e0cd0ceabb7 runtime/PLCObject.py --- a/runtime/PLCObject.py Fri Jan 30 10:45:11 2015 +0100 +++ b/runtime/PLCObject.py Fri Jan 30 20:42:24 2015 +0100 @@ -23,9 +23,10 @@ #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import Pyro.core as pyro -from threading import Timer, Thread, Lock, Semaphore +from threading import Timer, Thread, Lock, Semaphore, Event import ctypes, os, commands, types, sys from targets.typemapping import LogLevelsDefault, LogLevelsCount, TypeTranslator, UnpackDebugBuffer +from time import time if os.name in ("nt", "ce"): @@ -50,7 +51,6 @@ sys.stdout.flush() class PLCObject(pyro.ObjBase): - _Idxs = [] def __init__(self, workingdir, daemon, argv, statuschange, evaluator, website): pyro.ObjBase.__init__(self) self.evaluator = evaluator @@ -68,6 +68,10 @@ self.website = website self._loading_error = None self.python_runtime_vars = None + self.TraceThread = None + self.TraceLock = Lock() + self.TraceWakeup = Event() + self.Traces = [] # Get the last transfered PLC if connector must be restart try: @@ -365,6 +369,10 @@ self.LogMessage("PLC stopped") self._stopPLC() self.PythonThread.join() + if self.TraceThread is not None : + self.TraceWakeup.set() + self.TraceThread.join() + self.TraceThread = None return True return False @@ -453,7 +461,6 @@ # suspend but dont disable if self._suspendDebug(False) == 0: # keep a copy of requested idx - self._Idxs = idxs[:] self._ResetDebugVariables() for idx,iectype,force in idxs: if force !=None: @@ -462,16 +469,55 @@ (None,None,None)) force = ctypes.byref(pack_func(c_type,force)) self._RegisterDebugVariable(idx, force) + self._TracesSwap() self._resumeDebug() else: self._suspendDebug(True) - self._Idxs = [] + + def _TracesPush(self, trace): + self.TraceLock.acquire() + lT = len(self.Traces) + if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024 : + self.Traces.pop(0) + self.Traces.append(trace) + self.TraceLock.release() + + def _TracesSwap(self): + self.LastSwapTrace = time() + if self.TraceThread is None and self.PLCStatus == "Started": + self.TraceThread = Thread(target=self.TraceThreadProc) + self.TraceThread.start() + self.TraceLock.acquire() + Traces = self.Traces + self.Traces = [] + self.TraceLock.release() + self.TraceWakeup.set() + return Traces + + def _TracesAutoSuspend(self): + # TraceProc stops here if Traces not polled for 3 seconds + traces_age = time() - self.LastSwapTrace + if traces_age > 3: + self.TraceLock.acquire() + self.Traces = [] + self.TraceLock.release() + self._suspendDebug(True) # Disable debugger + self.TraceWakeup.wait() + self._resumeDebug() # Re-enable debugger + + def _TracesFlush(self): + self.TraceLock.acquire() + self.Traces = [] + self.TraceLock.release() def GetTraceVariables(self): - """ - Return a list of variables, corresponding to the list of required idx - """ - if self.PLCStatus == "Started": + return self.PLCStatus, self._TracesSwap() + + def TraceThreadProc(self): + """ + Return a list of traces, corresponding to the list of required idx + """ + while self.PLCStatus == "Started" : tick = ctypes.c_uint32() size = ctypes.c_uint32() buff = ctypes.c_void_p() @@ -485,8 +531,10 @@ self._FreeDebugData() self.PLClibraryLock.release() if TraceBuffer is not None: - return self.PLCStatus, tick.value, TraceBuffer - return self.PLCStatus, None, None + self._TracesPush((tick.value, TraceBuffer)) + self._TracesAutoSuspend() + self._TracesFlush() + def RemoteExec(self, script, **kwargs): try: