# HG changeset patch # User mjsousa # Date 1445436032 -3600 # Node ID de4ee16f7c6c168a284064e2e64f529a12b0d16d # Parent 28e9d479aa654d335290f72247abaa1358aa7afa# Parent 7df108e8cb18e06dbd10da2ab7b5efaa0be2e064 merge diff -r 28e9d479aa65 -r de4ee16f7c6c Beremiz.py --- a/Beremiz.py Sat Dec 06 19:31:51 2014 +0000 +++ b/Beremiz.py Wed Oct 21 15:00:32 2015 +0100 @@ -25,19 +25,23 @@ updateinfo_url = None -import os, sys, getopt, wx +import os, sys, getopt import __builtin__ -from wx.lib.agw.advancedsplash import AdvancedSplash import tempfile import shutil import random import time from types import ListType -CWD = os.path.split(os.path.realpath(__file__))[0] +beremiz_dir = os.path.dirname(os.path.realpath(__file__)) + +import wxversion +wxversion.select('2.8') +import wx +from wx.lib.agw.advancedsplash import AdvancedSplash def Bpath(*args): - return os.path.join(CWD,*args) + return os.path.join(beremiz_dir,*args) if __name__ == '__main__': def usage(): @@ -111,7 +115,7 @@ wx.Yield() from util.misc import InstallLocalRessources - InstallLocalRessources(CWD) + InstallLocalRessources(beremiz_dir) # Load extensions for extfilename in extensions: @@ -634,6 +638,13 @@ else: return IDEFrame.LoadTab(self, notebook, page_infos) + # Strange hack required by WAMP connector, using twisted. + # Twisted reactor needs to be stopped only before quit, + # since it cannot be restarted + ToDoBeforeQuit = [] + def AddToDoBeforeQuit(self, Thing): + self.ToDoBeforeQuit.append(Thing) + def OnCloseFrame(self, event): for evt_type in [wx.EVT_SET_FOCUS, wx.EVT_KILL_FOCUS, @@ -646,6 +657,10 @@ self.SaveLastState() + for Thing in self.ToDoBeforeQuit : + Thing() + self.ToDoBeforeQuit = [] + event.Skip() else: event.Veto() diff -r 28e9d479aa65 -r de4ee16f7c6c Beremiz_service.py --- a/Beremiz_service.py Sat Dec 06 19:31:51 2014 +0000 +++ b/Beremiz_service.py Wed Oct 21 15:00:32 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,12 +36,15 @@ -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) - + -w - web server port or "off" (default:8009) + -c - WAMP client config file or "off" (default:wampconf.json) + -e - python extension (absolute path .py) + working_dir - directory where are stored PLC files """%sys.argv[0] try: - opts, argv = getopt.getopt(sys.argv[1:], "i:p:n:x:t:a:h") + opts, argv = getopt.getopt(sys.argv[1:], "i:p:n:x:t:a:w:c:e:h") except getopt.GetoptError, err: # print help information and exit: print str(err) # will print something like "option -a not recognized" @@ -51,6 +54,8 @@ # default values given_ip = None port = 3000 +webport = 8009 +wampconf = "wampconf.json" servicename = None autostart = False enablewx = True @@ -58,6 +63,8 @@ enabletwisted = True havetwisted = False +extensions=[] + for o, a in opts: if o == "-h": usage() @@ -79,10 +86,18 @@ enabletwisted = int(a) elif o == "-a": autostart = int(a) + elif o == "-w": + webport = None if a == "off" else int(a) + elif o == "-c": + wampconf = None if a == "off" else a + elif o == "-e": + extensions.append(a) else: usage() sys.exit() +beremiz_dir = os.path.dirname(os.path.realpath(__file__)) + if len(argv) > 1: usage() sys.exit() @@ -99,26 +114,28 @@ if enablewx: try: - import wx, re - from threading import Thread, currentThread - from types import * + import wxversion + wxversion.select('2.8') + import wx havewx = True except: print "Wx unavailable !" havewx = False if havewx: + import re + from threading import Thread, currentThread + from types import * 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) - + return os.path.join(beremiz_dir,*args) + # Get folder containing translation files - localedir = os.path.join(CWD,"locale") + localedir = os.path.join(beremiz_dir,"locale") # Get the default language langid = wx.LANGUAGE_DEFAULT # Define translation domain (name of translation files) @@ -139,11 +156,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 +168,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 +181,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 +193,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 +210,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 +228,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 +251,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 +264,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 +281,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 +321,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) @@ -334,7 +351,10 @@ return res class Server(): - def __init__(self, servicename, ip_addr, port, workdir, argv, autostart=False, statuschange=None, evaluator=default_evaluator, website=None): + def __init__(self, servicename, ip_addr, port, + workdir, argv, autostart=False, + statuschange=None, evaluator=default_evaluator, + pyruntimevars=None): self.continueloop = True self.daemon = None self.servicename = servicename @@ -347,12 +367,12 @@ self.autostart = autostart self.statuschange = statuschange self.evaluator = evaluator - self.website = website - + self.pyruntimevars = pyruntimevars + def Loop(self): while self.continueloop: self.Start() - + def Restart(self): self.Stop() @@ -365,30 +385,34 @@ def Start(self): pyro.initServer() 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) + self.plcobj = PLCObject(self.workdir, self.daemon, self.argv, + self.statuschange, self.evaluator, + self.pyruntimevars) 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() - + + if self.autostart : + self.plcobj.AutoLoad() + if 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() @@ -406,205 +430,57 @@ if havewx: from twisted.internet import wxreactor wxreactor.install() - from twisted.internet import reactor, task - from twisted.python import log, util - from nevow import rend, appserver, inevow, tags, loaders, athena - from nevow.page import renderer - + from twisted.internet import reactor + havetwisted = True except: - print "Twisted unavailable !" + print "Twisted unavailable." havetwisted = False +pyruntimevars = {} +statuschange = [] + 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'))[ - 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(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: - f = self.PLCStartedHMIClass() - else: - f = PLCStoppedHMI() - f.setFragmentParent(self) - self.HMI = f - return f - athena.expose(getPLCElement) - - def detachFragmentChildren(self): - for child in self.liveFragmentChildren[:]: - child.detach() - - class WebInterface(athena.LivePage): - - docFactory = loaders.stan([tags.raw(xhtml_header), - tags.html(xmlns="http://www.w3.org/1999/xhtml")[ - tags.head(render=tags.directive('liveglue')), - tags.body[ - tags.div[ - tags.div( render = tags.directive( "MainPage" )) - ]]]]) - 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')) - self.plcState = plcState - self.MainPage.setPLCState(plcState) - - 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 - """ - req = inevow.IRequest(ctx) - req.setHeader('Content-type', 'application/xhtml+xml') - return super(WebInterface, self).renderHTTP(ctx) - - def render_MainPage(self, ctx, data): - f = self.MainPage - f.setFragmentParent(self) - return ctx.tag[f] - - 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: - try: - reactor.listenTCP(website_port, site) - listening = True - except: - website_port += 1 - print "Http interface port :",website_port -else: - website = None if havewx: from threading import Semaphore wx_eval_lock = Semaphore(0) main_thread = currentThread() - def statuschange(status): + def statuschangeTskBar(status): wx.CallAfter(taskbar_instance.UpdateIcon,status) - + + statuschange.append(statuschangeTskBar) + 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) + + pyroserver = Server(servicename, given_ip, port, + WorkingDir, argv, autostart, + statuschange, evaluator, pyruntimevars) + taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx) else: - pyroserver = Server(servicename, given_ip, port, WorkingDir, argv, autostart, website=website) + pyroserver = Server(servicename, given_ip, port, + WorkingDir, argv, autostart, + statuschange, pyruntimevars=pyruntimevars) + # Exception hooks s import threading, traceback @@ -631,6 +507,46 @@ threading.Thread.__init__ = init installThreadExcepthook() +if havetwisted: + if webport is not None : + try: + import runtime.NevowServer as NS + except Exception, e: + print "Nevow/Athena import failed :", e + webport = None + NS.WorkingDir = WorkingDir + + if wampconf is not None : + try: + import runtime.WampClient as WC + except Exception, e: + print "WAMP import failed :", e + wampconf = None + +# Load extensions +for extfilename in extensions: + extension_folder = os.path.split(os.path.realpath(extfilename))[0] + sys.path.append(extension_folder) + execfile(extfilename, locals()) + +if havetwisted: + if webport is not None : + try: + website = NS.RegisterWebsite(webport) + pyruntimevars["website"] = website + statuschange.append(NS.website_statuslistener_factory(website)) + except Exception, e: + print "Nevow Web service failed.", e + + if wampconf is not None : + try: + WC.RegisterWampClient(wampconf) + pyruntimevars["wampsession"] = WC.GetSession + WC.SetServer(pyroserver) + except Exception, e: + print "WAMP client startup failed.", e + + if havetwisted or havewx: pyro_thread=Thread(target=pyroserver.Loop) pyro_thread.start() diff -r 28e9d479aa65 -r de4ee16f7c6c CodeFileTreeNode.py --- a/CodeFileTreeNode.py Sat Dec 06 19:31:51 2014 +0000 +++ b/CodeFileTreeNode.py Wed Oct 21 15:00:32 2015 +0100 @@ -31,6 +31,9 @@ + + + @@ -119,6 +122,9 @@ variable.setname(var["Name"]) variable.settype(var["Type"]) variable.setinitial(var["Initial"]) + variable.setdesc(var["Description"]) + variable.setonchange(var["OnChange"]) + variable.setopts(var["Options"]) self.CodeFile.variables.appendvariable(variable) def GetVariables(self): @@ -126,7 +132,11 @@ for var in self.CodeFileVariables(self.CodeFile): datas.append({"Name" : var.getname(), "Type" : var.gettype(), - "Initial" : var.getinitial()}) + "Initial" : var.getinitial(), + "Description" : var.getdesc(), + "OnChange" : var.getonchange(), + "Options" : var.getopts(), + }) return datas def SetTextParts(self, parts): @@ -157,11 +167,15 @@ return True def CTNGlobalInstances(self): - current_location = self.GetCurrentLocation() - return [(variable.getname(), + variables = self.CodeFileVariables(self.CodeFile) + ret = [(variable.getname(), variable.gettype(), - variable.getinitial()) - for variable in self.CodeFileVariables(self.CodeFile)] + variable.getinitial()) + for variable in variables] + ret.extend([("On"+variable.getname()+"Change", "python_poll", "") + for variable in variables + if variable.getonchange()]) + return ret #------------------------------------------------------------------------------- # Current Buffering Management Functions diff -r 28e9d479aa65 -r de4ee16f7c6c PLCGenerator.py --- a/PLCGenerator.py Sat Dec 06 19:31:51 2014 +0000 +++ b/PLCGenerator.py Wed Oct 21 15:00:32 2015 +0100 @@ -1480,10 +1480,10 @@ self.TagName = self.ParentGenerator.Controler.ComputePouTransitionName(self.Name, transitionValues["value"]) if transitionType == "IL": transition_infos["content"] = [(":\n", ()), - (ReIndentText(transitionBody.getanyText(), len(self.CurrentIndent)), (self.TagName, "body", len(self.CurrentIndent)))] + (ReIndentText(transitionBody.getcontent().getanyText(), len(self.CurrentIndent)), (self.TagName, "body", len(self.CurrentIndent)))] elif transitionType == "ST": transition_infos["content"] = [("\n", ()), - (ReIndentText(transitionBody.getanyText(), len(self.CurrentIndent)), (self.TagName, "body", len(self.CurrentIndent)))] + (ReIndentText(transitionBody.getcontent().getanyText(), len(self.CurrentIndent)), (self.TagName, "body", len(self.CurrentIndent)))] else: for instance in transitionBody.getcontentInstances(): if isinstance(instance, OutVariableClass) and instance.getexpression() == transitionValues["value"]\ diff -r 28e9d479aa65 -r de4ee16f7c6c PLCOpenEditor.py --- a/PLCOpenEditor.py Sat Dec 06 19:31:51 2014 +0000 +++ b/PLCOpenEditor.py Wed Oct 21 15:00:32 2015 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor -#based on the plcopen standard. +#based on the plcopen standard. # #Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD # @@ -25,12 +25,12 @@ import wx import os, sys, platform, time, traceback, getopt -CWD = os.path.split(os.path.realpath(__file__))[0] +beremiz_dir = os.path.dirname(os.path.realpath(__file__)) __version__ = "$Revision: 1.130 $" if __name__ == '__main__': - # Usage message displayed when help request or when error detected in + # Usage message displayed when help request or when error detected in # command line def usage(): print "\nUsage of PLCOpenEditor.py :" @@ -43,13 +43,13 @@ # print help information and exit: usage() sys.exit(2) - + # Extract if help has been requested for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() - + # Extract the optional filename to open fileOpen = None if len(args) > 1: @@ -57,13 +57,13 @@ sys.exit() elif len(args) == 1: fileOpen = args[0] - + # Create wxApp (Need to create App before internationalization because of - # Windows) + # Windows) app = wx.PySimpleApp() from util.misc import InstallLocalRessources - InstallLocalRessources(CWD) + InstallLocalRessources(beremiz_dir) from docutil import * from IDEFrame import IDEFrame, AppendMenu @@ -78,7 +78,7 @@ #------------------------------------------------------------------------------- # Define PLCOpenEditor FileMenu extra items id -[ID_PLCOPENEDITORFILEMENUGENERATE, +[ID_PLCOPENEDITORFILEMENUGENERATE, ] = [wx.NewId() for _init_coll_FileMenu_Items in range(1)] class PLCOpenEditor(IDEFrame): @@ -120,7 +120,7 @@ parent.AppendSeparator() AppendMenu(parent, help='', id=wx.ID_EXIT, kind=wx.ITEM_NORMAL, text=_(u'Quit') + '\tCTRL+Q') - + self.Bind(wx.EVT_MENU, self.OnNewProjectMenu, id=wx.ID_NEW) self.Bind(wx.EVT_MENU, self.OnOpenProjectMenu, id=wx.ID_OPEN) self.Bind(wx.EVT_MENU, self.OnCloseTabMenu, id=wx.ID_CLOSE) @@ -134,15 +134,15 @@ self.Bind(wx.EVT_MENU, self.OnPrintMenu, id=wx.ID_PRINT) self.Bind(wx.EVT_MENU, self.OnPropertiesMenu, id=wx.ID_PROPERTIES) self.Bind(wx.EVT_MENU, self.OnQuitMenu, id=wx.ID_EXIT) - + self.AddToMenuToolBar([(wx.ID_NEW, "new", _(u'New'), None), (wx.ID_OPEN, "open", _(u'Open'), None), (wx.ID_SAVE, "save", _(u'Save'), None), (wx.ID_SAVEAS, "saveas", _(u'Save As...'), None), (wx.ID_PRINT, "print", _(u'Print'), None)]) - + def _init_coll_HelpMenu_Items(self, parent): - AppendMenu(parent, help='', id=wx.ID_HELP, + AppendMenu(parent, help='', id=wx.ID_HELP, kind=wx.ITEM_NORMAL, text=_(u'PLCOpenEditor') + '\tF1') #AppendMenu(parent, help='', id=wx.ID_HELP_CONTENTS, # kind=wx.ITEM_NORMAL, text=u'PLCOpen\tF2') @@ -161,9 +161,9 @@ # @param debug The filepath to open if no controler defined (default: False). def __init__(self, parent, fileOpen = None): IDEFrame.__init__(self, parent) - + result = None - + # Open the filepath if defined if fileOpen is not None: fileOpen = DecodeFileSystemPath(fileOpen, False) @@ -176,14 +176,14 @@ self.ProjectTree.Enable(True) self.PouInstanceVariablesPanel.SetController(controler) self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE) - + # Define PLCOpenEditor icon - self.SetIcon(wx.Icon(os.path.join(CWD, "images", "poe.ico"),wx.BITMAP_TYPE_ICO)) + self.SetIcon(wx.Icon(os.path.join(beremiz_dir, "images", "poe.ico"),wx.BITMAP_TYPE_ICO)) self.Bind(wx.EVT_CLOSE, self.OnCloseFrame) - + self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU, DISPLAYMENU) - + if result is not None: self.ShowErrorMessage( _("PLC syntax error at line %d:\n%s") % result) @@ -191,9 +191,9 @@ def OnCloseFrame(self, event): if self.Controler is None or self.CheckSaveBeforeClosing(_("Close Application")): self.AUIManager.UnInit() - + self.SaveLastState() - + event.Skip() else: event.Veto() @@ -266,7 +266,7 @@ self.Controler.CreateNewProject(properties) self.LibraryPanel.SetController(self.Controler) self.ProjectTree.Enable(True) - self._Refresh(TITLE, FILEMENU, EDITMENU, PROJECTTREE, POUINSTANCEVARIABLESPANEL, + self._Refresh(TITLE, FILEMENU, EDITMENU, PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE) def OnOpenProjectMenu(self, event): @@ -279,9 +279,9 @@ directory = os.path.dirname(filepath) else: directory = os.getcwd() - + result = None - + dialog = wx.FileDialog(self, _("Choose a file"), directory, "", _("PLCOpen files (*.xml)|*.xml|All files|*.*"), wx.OPEN) if dialog.ShowModal() == wx.ID_OK: filepath = dialog.GetPath() @@ -296,11 +296,11 @@ self._Refresh(PROJECTTREE, LIBRARYTREE) self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU) dialog.Destroy() - + if result is not None: self.ShowErrorMessage( _("PLC syntax error at line %d:\n%s") % result) - + def OnCloseProjectMenu(self, event): if not self.CheckSaveBeforeClosing(): return @@ -338,12 +338,12 @@ def OnPLCOpenEditorMenu(self, event): wx.MessageBox(_("No documentation available.\nComing soon.")) - + def OnPLCOpenMenu(self, event): - open_pdf(os.path.join(CWD, "plcopen", "TC6_XML_V101.pdf")) - + open_pdf(os.path.join(beremiz_dir, "plcopen", "TC6_XML_V101.pdf")) + def OnAboutMenu(self, event): - OpenHtmlFrame(self,_("About PLCOpenEditor"), os.path.join(CWD, "doc", "plcopen_about.html"), wx.Size(350, 350)) + OpenHtmlFrame(self,_("About PLCOpenEditor"), os.path.join(beremiz_dir, "doc", "plcopen_about.html"), wx.Size(350, 350)) def SaveProject(self): result = self.Controler.SaveXMLFile() @@ -351,7 +351,7 @@ self.SaveProjectAs() else: self._Refresh(TITLE, FILEMENU, PAGETITLES) - + def SaveProjectAs(self): filepath = self.Controler.GetFilePath() if filepath != "": @@ -386,13 +386,13 @@ trcbck += _("file : ") + str(line[0][len(os.getcwd()):]) + _(", ") trcbck += _("line : ") + str(line[1]) + _(", ") + _("function : ") + str(line[2]) trcbck_lst.append(trcbck) - + # Allow clicking.... cap = wx.Window_GetCapture() if cap: cap.ReleaseMouse() - dlg = wx.SingleChoiceDialog(None, + dlg = wx.SingleChoiceDialog(None, _(""" An error has occurred. @@ -403,7 +403,7 @@ Error: """) + - str(e_type) + _(" : ") + str(e_value), + str(e_type) + _(" : ") + str(e_value), _("Error"), trcbck_lst) try: @@ -431,7 +431,7 @@ ignored_exceptions = [] # a problem with a line in a module is only reported once per session def AddExceptHook(path, app_version='[No version]'):#, ignored_exceptions=[]): - + def handle_exception(e_type, e_value, e_traceback): traceback.print_exception(e_type, e_value, e_traceback) # this is very helpful when there's an exception in the rest of this func last_tb = get_last_traceback(e_traceback) @@ -461,7 +461,7 @@ info['locals'] = format_namespace(exception_locals) if 'self' in exception_locals: info['self'] = format_namespace(exception_locals['self'].__dict__) - + output = open(path+os.sep+"bug_report_"+info['date'].replace(':','-').replace(' ','_')+".txt",'w') lst = info.keys() lst.sort() @@ -473,12 +473,12 @@ if __name__ == '__main__': wx.InitAllImageHandlers() - + # Install a exception handle for bug reports AddExceptHook(os.getcwd(),__version__) - + frame = PLCOpenEditor(None, fileOpen=fileOpen) frame.Show() app.MainLoop() - + diff -r 28e9d479aa65 -r de4ee16f7c6c ProjectController.py --- a/ProjectController.py Sat Dec 06 19:31:51 2014 +0000 +++ b/ProjectController.py Wed Oct 21 15:00:32 2015 +0100 @@ -13,6 +13,7 @@ from time import localtime from datetime import datetime from weakref import WeakKeyDictionary +from itertools import izip import targets import connectors @@ -28,15 +29,13 @@ from PLCControler import PLCControler from plcopen.structures import IEC_KEYWORDS from targets.typemapping import DebugTypesSize, LogLevelsCount, LogLevels +from targets.typemapping import UnpackDebugBuffer from ConfigTreeNode import ConfigTreeNode, XSDSchemaErrorMessage -base_folder = os.path.split(sys.path[0])[0] +base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] 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): @@ -609,17 +608,21 @@ def _Compile_ST_to_SoftPLC(self): self.logger.write(_("Compiling IEC Program into C code...\n")) buildpath = self._getBuildPath() - - # Now compile IEC code into many C files - # files are listed to stdout, and errors to stderr. - status, result, err_result = ProcessLogger( - self.logger, - "\"%s\" -f -l -p -I \"%s\" -T \"%s\" \"%s\""%( + buildcmd = "\"%s\" -f -l -p -I \"%s\" -T \"%s\" \"%s\""%( self.iec2c_path, self.ieclib_path, buildpath, - self._getIECcodepath()), - no_stdout=True, no_stderr=True).spin() + self._getIECcodepath()) + + try: + # Invoke compiler. Output files are listed to stdout, errors to stderr + status, result, err_result = ProcessLogger(self.logger, buildcmd, + no_stdout=True, no_stderr=True).spin() + except Exception,e: + self.logger.write_error(buildcmd + "\n") + self.logger.write_error(repr(e) + "\n") + return False + if status: # Failed ! @@ -732,9 +735,11 @@ """ self._ProgramList = None self._VariablesList = None + self._DbgVariablesList = None self._IECPathToIdx = {} self._Ticktime = 0 self.TracedIECPath = [] + self.TracedIECTypes = [] def GetIECProgramsAndVariables(self): """ @@ -750,6 +755,7 @@ VariablesListAttributeName = ["num", "vartype", "IEC_path", "C_path", "type"] self._ProgramList = [] self._VariablesList = [] + self._DbgVariablesList = [] self._IECPathToIdx = {} # Separate sections @@ -774,6 +780,7 @@ # second section contains all variables config_FBs = {} + Idx = 0 for line in ListGroup[1]: # Split and Maps each field to dictionnary entries attrs = dict(zip(VariablesListAttributeName,line.strip().split(';'))) @@ -790,12 +797,17 @@ attrs["C_path"] = '__'.join(parts) if attrs["vartype"] == "FB": config_FBs[tuple(parts)] = attrs["C_path"] - # Push this dictionnary into result. + if attrs["vartype"] != "FB": + # Push this dictionnary into result. + self._DbgVariablesList.append(attrs) + # Fill in IEC<->C translation dicts + IEC_path=attrs["IEC_path"] + self._IECPathToIdx[IEC_path]=(Idx, attrs["type"]) + # Ignores numbers given in CSV file + # Idx=int(attrs["num"]) + # Count variables only, ignore FBs + Idx+=1 self._VariablesList.append(attrs) - # Fill in IEC<->C translation dicts - IEC_path=attrs["IEC_path"] - Idx=int(attrs["num"]) - self._IECPathToIdx[IEC_path]=(Idx, attrs["type"]) # third section contains ticktime if len(ListGroup) > 2: @@ -816,8 +828,21 @@ self.GetIECProgramsAndVariables() # prepare debug code - debug_code = targets.GetCode("plc_debug") % { - "buffer_size": reduce(lambda x, y: x + y, [DebugTypesSize.get(v["type"], 0) for v in self._VariablesList], 0), + variable_decl_array = [] + bofs = 0 + for v in self._DbgVariablesList : + sz = DebugTypesSize.get(v["type"], 0) + variable_decl_array += [ + "{&(%(C_path)s), "%v+ + {"EXT":"%(type)s_P_ENUM", + "IN":"%(type)s_P_ENUM", + "MEM":"%(type)s_O_ENUM", + "OUT":"%(type)s_O_ENUM", + "VAR":"%(type)s_ENUM"}[v["vartype"]]%v + + "}"] + bofs += sz + debug_code = targets.GetCode("plc_debug.c") % { + "buffer_size":bofs, "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;"%p for p in self._ProgramList]), "extern_variables_declarations":"\n".join([ @@ -828,22 +853,8 @@ "VAR":"extern __IEC_%(type)s_t %(C_path)s;", "FB":"extern %(type)s %(C_path)s;"}[v["vartype"]]%v for v in self._VariablesList if v["C_path"].find('.')<0]), - "for_each_variable_do_code":"\n".join([ - {"EXT":" (*fp)((void*)&(%(C_path)s),%(type)s_P_ENUM);\n", - "IN":" (*fp)((void*)&(%(C_path)s),%(type)s_P_ENUM);\n", - "MEM":" (*fp)((void*)&(%(C_path)s),%(type)s_O_ENUM);\n", - "OUT":" (*fp)((void*)&(%(C_path)s),%(type)s_O_ENUM);\n", - "VAR":" (*fp)((void*)&(%(C_path)s),%(type)s_ENUM);\n"}[v["vartype"]]%v - for v in self._VariablesList if v["vartype"] != "FB" and v["type"] in DebugTypesSize ]), - "find_variable_case_code":"\n".join([ - " case %(num)s:\n"%v+ - " *varp = (void*)&(%(C_path)s);\n"%v+ - {"EXT":" return %(type)s_P_ENUM;\n", - "IN":" return %(type)s_P_ENUM;\n", - "MEM":" return %(type)s_O_ENUM;\n", - "OUT":" return %(type)s_O_ENUM;\n", - "VAR":" return %(type)s_ENUM;\n"}[v["vartype"]]%v - for v in self._VariablesList if v["vartype"] != "FB" and v["type"] in DebugTypesSize ])} + "variable_decl_array": ",\n".join(variable_decl_array) + } return debug_code @@ -859,7 +870,7 @@ # Generate main, based on template if not self.BeremizRoot.getDisable_Extensions(): - plc_main_code = targets.GetCode("plc_main_head") % { + plc_main_code = targets.GetCode("plc_main_head.c") % { "calls_prototypes":"\n".join([( "int __init_%(s)s(int argc,char **argv);\n"+ "void __cleanup_%(s)s(void);\n"+ @@ -879,7 +890,7 @@ "__cleanup_%s();"%locstrs[i-1] for i in xrange(len(locstrs), 0, -1)]) } else: - plc_main_code = targets.GetCode("plc_main_head") % { + plc_main_code = targets.GetCode("plc_main_head.c") % { "calls_prototypes":"\n", "retrieve_calls":"\n", "publish_calls":"\n", @@ -887,7 +898,7 @@ "cleanup_calls":"\n" } plc_main_code += targets.GetTargetCode(self.GetTarget().getcontent().getLocalTag()) - plc_main_code += targets.GetCode("plc_main_tail") + plc_main_code += targets.GetCode("plc_main_tail.c") return plc_main_code @@ -975,7 +986,7 @@ self.ResetBuildMD5() return False - self.LocationCFilesAndCFLAGS = CTNLocationCFilesAndCFLAGS + LibCFilesAndCFLAGS + self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + CTNLocationCFilesAndCFLAGS self.LDFLAGS = CTNLDFLAGS + LibLDFLAGS ExtraFiles = CTNExtraFiles + LibExtraFiles @@ -1206,7 +1217,7 @@ def SnapshotAndResetDebugValuesBuffers(self): buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers, - [list() for iec_path in self.TracedIECPath]) + [list() for n in xrange(len(self.TracedIECPath))]) ticks, self.DebugTicks = self.DebugTicks, [] return ticks, buffers @@ -1214,6 +1225,7 @@ self.DebugTimer=None Idxs = [] self.TracedIECPath = [] + self.TracedIECTypes = [] if self._connector is not None: self.IECdebug_lock.acquire() IECPathsToPop = [] @@ -1238,8 +1250,10 @@ if Idxs: Idxs.sort() - self.TracedIECPath = zip(*Idxs)[3] - self._connector.SetTraceVariablesList(zip(*zip(*Idxs)[0:3])) + IdxsT = zip(*Idxs) + self.TracedIECPath = IdxsT[3] + self.TracedIECTypes = IdxsT[1] + self._connector.SetTraceVariablesList(zip(*IdxsT[0:3])) else: self.TracedIECPath = [] self._connector.SetTraceVariablesList([]) @@ -1267,11 +1281,11 @@ Idx, IEC_Type = self._IECPathToIdx.get(IECPath,(None,None)) return IEC_Type - def SubscribeDebugIECVariable(self, IECPath, callableobj, buffer_list=False, *args, **kwargs): + def SubscribeDebugIECVariable(self, IECPath, callableobj, buffer_list=False): """ Dispatching use a dictionnary linking IEC variable paths to a WeakKeyDictionary linking - weakly referenced callables to optionnal args + weakly referenced callables """ if IECPath != "__tick__" and not self._IECPathToIdx.has_key(IECPath): return None @@ -1290,7 +1304,7 @@ else: IECdebug_data[4] |= buffer_list - IECdebug_data[0][callableobj]=(buffer_list, args, kwargs) + IECdebug_data[0][callableobj]=buffer_list self.IECdebug_lock.release() @@ -1308,8 +1322,7 @@ else: IECdebug_data[4] = reduce( lambda x, y: x|y, - [buffer_list for buffer_list,args,kwargs - in IECdebug_data[0].itervalues()], + IECdebug_data[0].itervalues(), False) self.IECdebug_lock.release() @@ -1357,13 +1370,13 @@ if data_tuple is not None: WeakCallableDict, data_log, status, fvalue, buffer_list = data_tuple #data_log.append((debug_tick, value)) - for weakcallable,(buffer_list,args,kwargs) in WeakCallableDict.iteritems(): + for weakcallable,buffer_list in WeakCallableDict.iteritems(): function = getattr(weakcallable, function_name, None) if function is not None: if buffer_list: - function(*(cargs + args), **kwargs) + function(*cargs) else: - function(*(tuple([lst[-1] for lst in cargs]) + args), **kwargs) + function(*tuple([lst[-1] for lst in cargs])) def GetTicktime(self): return self._Ticktime @@ -1380,38 +1393,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_vars = 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": - self.IECdebug_lock.acquire() - if (debug_tick is not None and - len(debug_vars) == len(self.DebugValuesBuffers) 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 zip(self.TracedIECPath, self.DebugValuesBuffers, debug_vars): - IECdebug_data = self.IECdebug_datas.get(IECPath, None) - 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 @@ -1426,7 +1435,7 @@ self.IECdebug_lock.release() start_time = time.time() if len(self.TracedIECPath) == len(buffers): - for IECPath, values in zip(self.TracedIECPath, buffers): + for IECPath, values in izip(self.TracedIECPath, buffers): if len(values) > 0: self.CallWeakcallables(IECPath, "NewValues", debug_ticks, values) if len(debug_ticks) > 0: diff -r 28e9d479aa65 -r de4ee16f7c6c canfestival/canfestival.py --- a/canfestival/canfestival.py Sat Dec 06 19:31:51 2014 +0000 +++ b/canfestival/canfestival.py Wed Oct 21 15:00:32 2015 +0100 @@ -1,6 +1,6 @@ import os, sys, shutil -base_folder = os.path.split(sys.path[0])[0] +base_folder = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) CanFestivalPath = os.path.join(base_folder, "CanFestival-3") sys.path.append(os.path.join(CanFestivalPath, "objdictgen")) diff -r 28e9d479aa65 -r de4ee16f7c6c canfestival/cf_runtime.c --- a/canfestival/cf_runtime.c Sat Dec 06 19:31:51 2014 +0000 +++ b/canfestival/cf_runtime.c Wed Oct 21 15:00:32 2015 +0100 @@ -14,7 +14,7 @@ /* Keep track of init level to cleanup correctly */ static int init_level=0; /* Retrieve PLC cycle time */ -extern int common_ticktime__; +extern unsigned long long common_ticktime__; /* Per master node slavebootup callbacks. Checks that * every node have booted before calling Master_post_SlaveBootup */ @@ -34,7 +34,7 @@ nodename##_Data.CurrentCommunicationState.csSYNC = -1;\ /* Force sync period to common_ticktime__ so that other node can read it*/\ *nodename##_Data.COB_ID_Sync = 0x40000080;\ - *nodename##_Data.Sync_Cycle_Period = common_ticktime__ * 1000; + *nodename##_Data.Sync_Cycle_Period = common_ticktime__ / 1000; static void DeferedInitAlarm(CO_Data* d, UNS32 id){ /* Node will start beeing active on the network after this */ diff -r 28e9d479aa65 -r de4ee16f7c6c connectors/PYRO/__init__.py --- a/connectors/PYRO/__init__.py Sat Dec 06 19:31:51 2014 +0000 +++ b/connectors/PYRO/__init__.py Wed Oct 21 15:00:32 2015 +0100 @@ -1,32 +1,33 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -#Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD # -#See COPYING file for copyrights details. +# See COPYING file for copyrights details. # -#This library is free software; you can redistribute it and/or -#modify it under the terms of the GNU General Public -#License as published by the Free Software Foundation; either -#version 2.1 of the License, or (at your option) any later version. +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. # -#This library is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -#General Public License for more details. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. # -#You should have received a copy of the GNU General Public -#License along with this library; if not, write to the Free Software -#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import Pyro.core as pyro +# You should have received a copy of the GNU General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import Pyro +import Pyro.core +import Pyro.util from Pyro.errors import PyroError -import Pyro.util import traceback from time import sleep import copy import socket service_type = '_PYRO._tcp.local.' - +import os.path # this module attribute contains a list of DNS-SD (Zeroconf) service types # supported by this connector confnode. # @@ -37,67 +38,99 @@ """ This returns the connector to Pyro style PLCobject """ - confnodesroot.logger.write(_("Connecting to URI : %s\n")%uri) + confnodesroot.logger.write(_("PYRO connecting to URI : %s\n") % uri) servicetype, location = uri.split("://") + if servicetype == "PYROS": + schemename = "PYROLOCSSL" + # Protect against name->IP substitution in Pyro3 + Pyro.config.PYRO_DNS_URI = True + # Beware Pyro lib need str path, not unicode + # don't rely on PYRO_STORAGE ! see documentation + Pyro.config.PYROSSL_CERTDIR = os.path.abspath(str(confnodesroot.ProjectPath) + '/certs') + if not os.path.exists(Pyro.config.PYROSSL_CERTDIR): + confnodesroot.logger.write_error( + 'Error : the directory %s is missing for SSL certificates (certs_dir).' + 'Please fix it in your project.\n' % Pyro.config.PYROSSL_CERTDIR) + return None + else: + confnodesroot.logger.write(_("PYRO using certificates in '%s' \n") + % (Pyro.config.PYROSSL_CERTDIR)) + Pyro.config.PYROSSL_CERT = "client.crt" + Pyro.config.PYROSSL_KEY = "client.key" + # Ugly Monkey Patching + def _gettimeout(self): + return self.timeout + + def _settimeout(self, timeout): + self.timeout = timeout + from M2Crypto.SSL import Connection + Connection.timeout = None + Connection.gettimeout = _gettimeout + Connection.settimeout = _settimeout + # M2Crypto.SSL.Checker.WrongHost: Peer certificate commonName does not + # match host, expected 127.0.0.1, got server + Connection.clientPostConnectionCheck = None + else: + schemename = "PYROLOC" if location.find(service_type) != -1: - try : + try: from util.Zeroconf import Zeroconf r = Zeroconf() - i=r.getServiceInfo(service_type, location) - if i is None : raise Exception, "'%s' not found"%location + i = r.getServiceInfo(service_type, location) + if i is None: + raise Exception("'%s' not found" % location) ip = str(socket.inet_ntoa(i.getAddress())) port = str(i.getPort()) - newlocation = ip+':'+port - confnodesroot.logger.write(_("'%s' is located at %s\n")%(location, newlocation)) + newlocation = ip + ':' + port + confnodesroot.logger.write(_("'%s' is located at %s\n") % (location, newlocation)) location = newlocation r.close() except Exception, msg: - confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n")%location) + 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") + try: + RemotePLCObjectProxy = Pyro.core.getAttrProxyForURI(schemename + "://" + location + "/PLCObject") except Exception, msg: - confnodesroot.logger.write_error(_("Connection to '%s' failed.\n")%location) + confnodesroot.logger.write_error(_("Connection to '%s' failed.\n") % location) confnodesroot.logger.write_error(traceback.format_exc()) return None def PyroCatcher(func, default=None): """ - A function that catch a pyro exceptions, write error to logger - and return defaul value when it happen + A function that catch a Pyro exceptions, write error to logger + and return default value when it happen """ - def catcher_func(*args,**kwargs): + def catcher_func(*args, **kwargs): try: - return func(*args,**kwargs) + return func(*args, **kwargs) except Pyro.errors.ConnectionClosedError, e: confnodesroot.logger.write_error("Connection lost!\n") confnodesroot._SetConnector(None) except Pyro.errors.ProtocolError, e: - confnodesroot.logger.write_error("Pyro exception: "+str(e)+"\n") - except Exception,e: - #confnodesroot.logger.write_error(traceback.format_exc()) + confnodesroot.logger.write_error("Pyro exception: " + str(e) + "\n") + except Exception, e: + # confnodesroot.logger.write_error(traceback.format_exc()) errmess = ''.join(Pyro.util.getPyroTraceback(e)) - confnodesroot.logger.write_error(errmess+"\n") + confnodesroot.logger.write_error(errmess + "\n") print errmess confnodesroot._SetConnector(None) 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: + if PyroCatcher(lambda: RemotePLCObjectProxy.GetPLCstatus())() is None: confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n")) return None - - class PyroProxyProxy: + class PyroProxyProxy(object): """ A proxy proxy class to handle Beremiz Pyro interface specific behavior. - And to put pyro exception catcher in between caller and pyro proxy + And to put Pyro exception catcher in between caller and Pyro proxy """ def __init__(self): # for safe use in from debug thread, must create a copy @@ -112,9 +145,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() @@ -133,7 +166,6 @@ return confnodesroot._connector.GetPyroProxy().StartPLC(*args, **kwargs) StartPLC = PyroCatcher(_PyroStartPLC, False) - def _PyroGetTraceVariables(self): """ for safe use in from debug thread, must use the copy @@ -141,11 +173,11 @@ 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() - GetPLCstatus = PyroCatcher(_PyroGetPLCstatus, ("Broken",None)) + GetPLCstatus = PyroCatcher(_PyroGetPLCstatus, ("Broken", None)) def _PyroRemoteExec(self, script, **kwargs): return RemotePLCObjectProxy.RemoteExec(script, **kwargs) @@ -154,12 +186,10 @@ def __getattr__(self, attrName): member = self.__dict__.get(attrName, None) if member is None: - def my_local_func(*args,**kwargs): - return RemotePLCObjectProxy.__getattr__(attrName)(*args,**kwargs) + def my_local_func(*args, **kwargs): + return RemotePLCObjectProxy.__getattr__(attrName)(*args, **kwargs) member = PyroCatcher(my_local_func, None) self.__dict__[attrName] = member return member return PyroProxyProxy() - - diff -r 28e9d479aa65 -r de4ee16f7c6c connectors/WAMP/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/WAMP/__init__.py Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +#Copyright (C) 2015: Edouard TISSERANT +# +#See COPYING file for copyrights details. +# +#This library is free software; you can redistribute it and/or +#modify it under the terms of the GNU General Public +#License as published by the Free Software Foundation; either +#version 2.1 of the License, or (at your option) any later version. +# +#This library is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +#General Public License for more details. +# +#You should have received a copy of the GNU General Public +#License along with this library; if not, write to the Free Software +#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys, traceback, atexit +#from twisted.python import log +from twisted.internet import reactor, threads +from autobahn.twisted import wamp +from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS +from autobahn.wamp import types +from autobahn.wamp.exception import TransportLost +from autobahn.wamp.serializer import MsgPackSerializer +from threading import Thread, Event + +_WampSession = None +_WampConnection = None +_WampSessionEvent = Event() + +class WampSession(wamp.ApplicationSession): + def onJoin(self, details): + global _WampSession, _WampSessionEvent + _WampSession = self + _WampSessionEvent.set() + print 'WAMP session joined for :', self.config.extra["ID"] + + def onLeave(self, details): + global _WampSession, _WampSessionEvent + _WampSessionEvent.clear() + _WampSession = None + print 'WAMP session left' + +PLCObjDefaults = { "StartPLC": False, + "GetTraceVariables" : ("Broken",None), + "GetPLCstatus" : ("Broken",None), + "RemoteExec" : (-1, "RemoteExec script failed!")} + +def WAMP_connector_factory(uri, confnodesroot): + """ + WAMP://127.0.0.1:12345/path#realm#ID + WAMPS://127.0.0.1:12345/path#realm#ID + """ + servicetype, location = uri.split("://") + urlpath, realm, ID = location.split('#') + urlprefix = {"WAMP":"ws", + "WAMPS":"wss"}[servicetype] + url = urlprefix+"://"+urlpath + + def RegisterWampClient(): + + ## start logging to console + # log.startLogging(sys.stdout) + + # create a WAMP application session factory + component_config = types.ComponentConfig( + realm = realm, + extra = {"ID":ID}) + session_factory = wamp.ApplicationSessionFactory( + config = component_config) + session_factory.session = WampSession + + # create a WAMP-over-WebSocket transport client factory + transport_factory = WampWebSocketClientFactory( + session_factory, + url = url, + serializers = [MsgPackSerializer()], + debug = False, + debug_wamp = False) + + # start the client from a Twisted endpoint + conn = connectWS(transport_factory) + confnodesroot.logger.write(_("WAMP connecting to URL : %s\n")%url) + return conn + + AddToDoBeforeQuit = confnodesroot.AppFrame.AddToDoBeforeQuit + def ThreadProc(): + global _WampConnection + _WampConnection = RegisterWampClient() + AddToDoBeforeQuit(reactor.stop) + reactor.run(installSignalHandlers=False) + + def WampSessionProcMapper(funcname): + wampfuncname = '.'.join((ID,funcname)) + def catcher_func(*args,**kwargs): + global _WampSession + if _WampSession is not None : + try: + return threads.blockingCallFromThread( + reactor, _WampSession.call, wampfuncname, + *args,**kwargs) + except TransportLost, e: + confnodesroot.logger.write_error("Connection lost!\n") + confnodesroot._SetConnector(None) + except Exception,e: + errmess = traceback.format_exc() + confnodesroot.logger.write_error(errmess+"\n") + print errmess + #confnodesroot._SetConnector(None) + return PLCObjDefaults.get(funcname) + return catcher_func + + class WampPLCObjectProxy(object): + def __init__(self): + global _WampSessionEvent, _WampConnection + if not reactor.running: + Thread(target=ThreadProc).start() + else: + _WampConnection = threads.blockingCallFromThread( + reactor, RegisterWampClient) + if not _WampSessionEvent.wait(5): + _WampConnection = stopConnecting() + raise Exception, _("WAMP connection timeout") + + def __del__(self): + global _WampConnection + _WampConnection.disconnect() + # + # reactor.stop() + + def __getattr__(self, attrName): + member = self.__dict__.get(attrName, None) + if member is None: + member = WampSessionProcMapper(attrName) + self.__dict__[attrName] = member + return member + + # Try to get the proxy object + try : + return WampPLCObjectProxy() + except Exception, msg: + confnodesroot.logger.write_error(_("WAMP connection to '%s' failed.\n")%location) + confnodesroot.logger.write_error(traceback.format_exc()) + return None + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c connectors/__init__.py --- a/connectors/__init__.py Sat Dec 06 19:31:51 2014 +0000 +++ b/connectors/__init__.py Wed Oct 21 15:00:32 2015 +0100 @@ -1,23 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -#Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD # -#See COPYING file for copyrights details. +# See COPYING file for copyrights details. # -#This library is free software; you can redistribute it and/or -#modify it under the terms of the GNU General Public -#License as published by the Free Software Foundation; either -#version 2.1 of the License, or (at your option) any later version. +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. # -#This library is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -#General Public License for more details. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. # -#You should have received a copy of the GNU General Public -#License along with this library; if not, write to the Free Software -#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# You should have received a copy of the GNU General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Package initialisation @@ -28,27 +28,35 @@ def _GetLocalConnectorClassFactory(name): - return lambda:getattr(__import__(name,globals(),locals()), name + "_connector_factory") + return lambda: getattr(__import__(name, globals(), locals()), name + "_connector_factory") -connectors = {name:_GetLocalConnectorClassFactory(name) - for name in listdir(_base_path) - if path.isdir(path.join(_base_path, name)) +connectors = {name:_GetLocalConnectorClassFactory(name) + for name in listdir(_base_path) + if path.isdir(path.join(_base_path, name)) and not name.startswith("__")} + def ConnectorFactory(uri, confnodesroot): """ Return a connector corresponding to the URI or None if cannot connect to URI """ - servicetype = uri.split("://")[0] - if servicetype in connectors: - # import module according to uri type - connectorclass = connectors[servicetype]() - elif servicetype == "LOCAL": - from PYRO import PYRO_connector_factory as connectorclass - runtime_port = confnodesroot.AppFrame.StartLocalRuntime(taskbaricon=True) - uri="PYRO://127.0.0.1:"+str(runtime_port) - else : - return None + servicetype = uri.split("://")[0].upper() + if servicetype == "LOCAL": + # Local is special case + # pyro connection to local runtime + # started on demand, listening on random port + servicetype = "PYRO" + runtime_port = confnodesroot.AppFrame.StartLocalRuntime( + taskbaricon=True) + uri = "PYROLOC://127.0.0.1:" + str(runtime_port) + elif servicetype in connectors: + pass + elif servicetype[-1] == 'S' and servicetype[:-1] in connectors: + servicetype = servicetype[:-1] + else: + return None + + # import module according to uri type + connectorclass = connectors[servicetype]() return connectorclass(uri, confnodesroot) - diff -r 28e9d479aa65 -r de4ee16f7c6c controls/DebugVariablePanel/DebugVariablePanel.py --- a/controls/DebugVariablePanel/DebugVariablePanel.py Sat Dec 06 19:31:51 2014 +0000 +++ b/controls/DebugVariablePanel/DebugVariablePanel.py Wed Oct 21 15:00:32 2015 +0100 @@ -319,7 +319,7 @@ if self.DataProducer is not None: self.SetTickTime(self.DataProducer.GetTicktime()) - def RefreshNewData(self, *args, **kwargs): + def RefreshNewData(self): """ Called to refresh Panel according to values received by variables Can receive any parameters (not used here) @@ -329,9 +329,9 @@ self.HasNewData = False self.RefreshView() - DebugViewer.RefreshNewData(self, *args, **kwargs) - - def NewDataAvailable(self, ticks, *args, **kwargs): + DebugViewer.RefreshNewData(self) + + def NewDataAvailable(self, ticks): """ Called by DataProducer for each tick captured or by panel to refresh graphs @@ -363,14 +363,14 @@ self.RefreshView() else: - DebugViewer.NewDataAvailable(self, ticks, *args, **kwargs) + DebugViewer.NewDataAvailable(self, ticks) def ForceRefresh(self): """ Called to force refresh of graphs """ self.Force = True - wx.CallAfter(self.NewDataAvailable, None, True) + wx.CallAfter(self.NewDataAvailable, None) def SetCursorTick(self, cursor_tick): """ diff -r 28e9d479aa65 -r de4ee16f7c6c controls/LogViewer.py --- a/controls/LogViewer.py Sat Dec 06 19:31:51 2014 +0000 +++ b/controls/LogViewer.py Wed Oct 21 15:00:32 2015 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor -#based on the plcopen standard. +#based on the plcopen standard. # #Copyright (C) 2013: Edouard TISSERANT and Laurent BESSARD # @@ -32,6 +32,7 @@ from editors.DebugViewer import DebugViewer, REFRESH_PERIOD from targets.typemapping import LogLevelsCount, LogLevels from util.BitmapLibrary import GetBitmap +from weakref import proxy THUMB_SIZE_RATIO = 1. / 8. @@ -46,7 +47,7 @@ wx.Point(xoffset + width - 1, yoffset - height + 1)] class LogScrollBar(wx.Panel): - + def __init__(self, parent, size): wx.Panel.__init__(self, parent, size=size) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) @@ -55,14 +56,14 @@ self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnResize) - + self.ThumbPosition = 0. # -1 <= ThumbPosition <= 1 self.ThumbScrollingStartPos = None - + def GetRangeRect(self): width, height = self.GetClientSize() return wx.Rect(0, width, width, height - 2 * width) - + def GetThumbRect(self): width, height = self.GetClientSize() range_rect = self.GetRangeRect() @@ -72,7 +73,7 @@ thumb_start = int(thumb_center_position - thumb_size / 2.) thumb_end = int(thumb_center_position + thumb_size / 2.) return wx.Rect(0, range_rect.y + thumb_start, width, thumb_end - thumb_start) - + def RefreshThumbPosition(self, thumb_position=None): if thumb_position is None: thumb_position = self.ThumbPosition @@ -84,7 +85,7 @@ self.ThumbPosition = thumb_position self.Parent.SetScrollSpeed(self.ThumbPosition) self.Refresh() - + def OnLeftDown(self, event): self.CaptureMouse() posx, posy = event.GetPosition() @@ -103,14 +104,14 @@ elif posy > height - width: self.Parent.ScrollMessagePanelByPage(-1) event.Skip() - + def OnLeftUp(self, event): self.ThumbScrollingStartPos = None self.RefreshThumbPosition(0.) if self.HasCapture(): self.ReleaseMouse() event.Skip() - + def OnMotion(self, event): if event.Dragging() and self.ThumbScrollingStartPos is not None: posx, posy = event.GetPosition() @@ -121,32 +122,32 @@ self.RefreshThumbPosition( max(-1., min((posy - self.ThumbScrollingStartPos.y) * 2. / thumb_range, 1.))) event.Skip() - + def OnResize(self, event): self.Refresh() event.Skip() - + def OnEraseBackground(self, event): pass - + def OnPaint(self, event): dc = wx.BufferedPaintDC(self) dc.Clear() dc.BeginDrawing() - + gc = wx.GCDC(dc) - + width, height = self.GetClientSize() - + gc.SetPen(wx.Pen(wx.NamedColour("GREY"), 3)) gc.SetBrush(wx.GREY_BRUSH) - + gc.DrawLines(ArrowPoints(wx.TOP, width * 0.75, width * 0.5, 2, (width + height) / 4 - 3)) gc.DrawLines(ArrowPoints(wx.TOP, width * 0.75, width * 0.5, 2, (width + height) / 4 + 3)) - + gc.DrawLines(ArrowPoints(wx.BOTTOM, width * 0.75, width * 0.5, 2, (height * 3 - width) / 4 + 3)) gc.DrawLines(ArrowPoints(wx.BOTTOM, width * 0.75, width * 0.5, 2, (height * 3 - width) / 4 - 3)) - + thumb_rect = self.GetThumbRect() exclusion_rect = wx.Rect(thumb_rect.x, thumb_rect.y, thumb_rect.width, thumb_rect.height) @@ -158,71 +159,71 @@ colour = wx.NamedColour("LIGHT GREY") gc.SetPen(wx.Pen(colour)) gc.SetBrush(wx.Brush(colour)) - - gc.DrawRectangle(exclusion_rect.x, exclusion_rect.y, + + gc.DrawRectangle(exclusion_rect.x, exclusion_rect.y, exclusion_rect.width, exclusion_rect.height) - + gc.SetPen(wx.GREY_PEN) gc.SetBrush(wx.GREY_BRUSH) - + gc.DrawPolygon(ArrowPoints(wx.TOP, width, width, 0, 0)) - + gc.DrawPolygon(ArrowPoints(wx.BOTTOM, width, width, 0, height)) - - gc.DrawRectangle(thumb_rect.x, thumb_rect.y, + + gc.DrawRectangle(thumb_rect.x, thumb_rect.y, thumb_rect.width, thumb_rect.height) - + dc.EndDrawing() event.Skip() BUTTON_SIZE = (30, 15) class LogButton(): - + def __init__(self, label, callback): self.Position = wx.Point(0, 0) self.Size = wx.Size(*BUTTON_SIZE) self.Label = label self.Shown = True self.Callback = callback - + def __del__(self): self.callback = None - + def GetSize(self): return self.Size - + def SetPosition(self, x, y): self.Position = wx.Point(x, y) - + def HitTest(self, x, y): - rect = wx.Rect(self.Position.x, self.Position.y, + rect = wx.Rect(self.Position.x, self.Position.y, self.Size.width, self.Size.height) if rect.InsideXY(x, y): return True return False - + def ProcessCallback(self): if self.Callback is not None: wx.CallAfter(self.Callback) - + def Draw(self, dc): dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(wx.NamedColour("LIGHT GREY"))) - - dc.DrawRectangle(self.Position.x, self.Position.y, + + dc.DrawRectangle(self.Position.x, self.Position.y, self.Size.width, self.Size.height) - + w, h = dc.GetTextExtent(self.Label) - dc.DrawText(self.Label, - self.Position.x + (self.Size.width - w) / 2, + dc.DrawText(self.Label, + self.Position.x + (self.Size.width - w) / 2, self.Position.y + (self.Size.height - h) / 2) DATE_INFO_SIZE = 10 MESSAGE_INFO_SIZE = 18 class LogMessage: - + def __init__(self, tv_sec, tv_nsec, level, level_bitmap, msg): self.Date = datetime.utcfromtimestamp(tv_sec) self.Seconds = self.Date.second + tv_nsec * 1e-9 @@ -232,12 +233,12 @@ self.LevelBitmap = level_bitmap self.Message = msg self.DrawDate = True - + def __cmp__(self, other): if self.Date == other.Date: return cmp(self.Seconds, other.Seconds) return cmp(self.Date, other.Date) - + def GetFullText(self): date = self.Date.replace(second=int(self.Seconds)) nsec = (self.Seconds % 1.) * 1e9 @@ -245,25 +246,25 @@ LogLevels[self.Level], str(date), nsec, self.Message) - + def Draw(self, dc, offset, width, draw_date): if draw_date: datetime_text = self.Date.strftime("%d/%m/%y %H:%M") dw, dh = dc.GetTextExtent(datetime_text) dc.DrawText(datetime_text, (width - dw) / 2, offset + (DATE_INFO_SIZE - dh) / 2) offset += DATE_INFO_SIZE - + seconds_text = "%12.9f" % self.Seconds sw, sh = dc.GetTextExtent(seconds_text) dc.DrawText(seconds_text, 5, offset + (MESSAGE_INFO_SIZE - sh) / 2) - + bw, bh = self.LevelBitmap.GetWidth(), self.LevelBitmap.GetHeight() dc.DrawBitmap(self.LevelBitmap, 10 + sw, offset + (MESSAGE_INFO_SIZE - bh) / 2) - + text = self.Message.replace("\n", " ") mw, mh = dc.GetTextExtent(text) dc.DrawText(text, 15 + sw + bw, offset + (MESSAGE_INFO_SIZE - mh) / 2) - + def GetHeight(self, draw_date): if draw_date: return DATE_INFO_SIZE + MESSAGE_INFO_SIZE @@ -280,18 +281,18 @@ (_("1s"), SECOND)] class LogViewer(DebugViewer, wx.Panel): - + def __init__(self, parent, window): wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER) DebugViewer.__init__(self, None, False, False) - + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5) main_sizer.AddGrowableCol(0) main_sizer.AddGrowableRow(1) - + filter_sizer = wx.BoxSizer(wx.HORIZONTAL) main_sizer.AddSizer(filter_sizer, border=5, flag=wx.TOP|wx.LEFT|wx.RIGHT|wx.GROW) - + self.MessageFilter = wx.ComboBox(self, style=wx.CB_READONLY) self.MessageFilter.Append(_("All")) levels = LogLevels[:3] @@ -300,28 +301,28 @@ self.MessageFilter.Append(_(level)) self.Bind(wx.EVT_COMBOBOX, self.OnMessageFilterChanged, self.MessageFilter) filter_sizer.AddWindow(self.MessageFilter, 1, border=5, flag=wx.RIGHT|wx.ALIGN_CENTER_VERTICAL) - + self.SearchMessage = wx.SearchCtrl(self, style=wx.TE_PROCESS_ENTER) self.SearchMessage.ShowSearchButton(True) self.SearchMessage.ShowCancelButton(True) self.Bind(wx.EVT_TEXT_ENTER, self.OnSearchMessageChanged, self.SearchMessage) - self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, + self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.OnSearchMessageSearchButtonClick, self.SearchMessage) - self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, + self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnSearchMessageCancelButtonClick, self.SearchMessage) filter_sizer.AddWindow(self.SearchMessage, 3, border=5, flag=wx.RIGHT|wx.ALIGN_CENTER_VERTICAL) - - self.CleanButton = wx.lib.buttons.GenBitmapButton(self, bitmap=GetBitmap("Clean"), + + self.CleanButton = wx.lib.buttons.GenBitmapButton(self, bitmap=GetBitmap("Clean"), size=wx.Size(28, 28), style=wx.NO_BORDER) self.CleanButton.SetToolTipString(_("Clean log messages")) self.Bind(wx.EVT_BUTTON, self.OnCleanButton, self.CleanButton) filter_sizer.AddWindow(self.CleanButton) - + message_panel_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0) message_panel_sizer.AddGrowableCol(0) message_panel_sizer.AddGrowableRow(0) main_sizer.AddSizer(message_panel_sizer, border=5, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM|wx.GROW) - + self.MessagePanel = wx.Panel(self) if wx.Platform == '__WXMSW__': self.Font = wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier New') @@ -337,45 +338,45 @@ self.MessagePanel.Bind(wx.EVT_PAINT, self.OnMessagePanelPaint) self.MessagePanel.Bind(wx.EVT_SIZE, self.OnMessagePanelResize) message_panel_sizer.AddWindow(self.MessagePanel, flag=wx.GROW) - + self.MessageScrollBar = LogScrollBar(self, wx.Size(16, -1)) message_panel_sizer.AddWindow(self.MessageScrollBar, flag=wx.GROW) - + self.SetSizer(main_sizer) - + self.LeftButtons = [] - for label, callback in [("+" + text, self.GenerateOnDurationButton(duration)) + for label, callback in [("+" + text, self.GenerateOnDurationButton(duration)) for text, duration in CHANGE_TIMESTAMP_BUTTONS]: self.LeftButtons.append(LogButton(label, callback)) - + self.RightButtons = [] - for label, callback in [("-" + text, self.GenerateOnDurationButton(-duration)) + for label, callback in [("-" + text, self.GenerateOnDurationButton(-duration)) for text, duration in CHANGE_TIMESTAMP_BUTTONS]: self.RightButtons.append(LogButton(label, callback)) - + self.MessageFilter.SetSelection(0) self.LogSource = None self.ResetLogMessages() self.ParentWindow = window - + self.LevelIcons = [GetBitmap("LOG_" + level) for level in LogLevels] self.LevelFilters = [range(i) for i in xrange(4, 0, -1)] self.CurrentFilter = self.LevelFilters[0] self.CurrentSearchValue = "" - + self.ScrollSpeed = 0. self.LastStartTime = None self.ScrollTimer = wx.Timer(self, -1) self.Bind(wx.EVT_TIMER, self.OnScrollTimer, self.ScrollTimer) - + self.LastMousePos = None self.MessageToolTip = None self.MessageToolTipTimer = wx.Timer(self, -1) self.Bind(wx.EVT_TIMER, self.OnMessageToolTipTimer, self.MessageToolTipTimer) - + def __del__(self): self.ScrollTimer.Stop() - + def ResetLogMessages(self): self.previous_log_count = [None]*LogLevelsCount self.OldestMessages = [] @@ -383,14 +384,14 @@ self.LogMessagesTimestamp = numpy.array([]) self.CurrentMessage = None self.HasNewData = False - + def SetLogSource(self, log_source): - self.LogSource = log_source + self.LogSource = proxy(log_source) if log_source else None self.CleanButton.Enable(self.LogSource is not None) if log_source is not None: self.ResetLogMessages() self.RefreshView() - + def GetLogMessageFromSource(self, msgidx, level): if self.LogSource is not None: answer = self.LogSource.GetLogMessage(level, msgidx) @@ -398,7 +399,7 @@ msg, tick, tv_sec, tv_nsec = answer return LogMessage(tv_sec, tv_nsec, level, self.LevelIcons[level], msg) return None - + def SetLogCounters(self, log_count): new_messages = [] for level, count, prev in zip(xrange(LogLevelsCount), log_count, self.previous_log_count): @@ -441,12 +442,12 @@ self.MessageToolTipTimer.Stop() self.ParentWindow.SelectTab(self) self.NewDataAvailable(None) - + def FilterLogMessage(self, message, timestamp=None): - return (message.Level in self.CurrentFilter and + return (message.Level in self.CurrentFilter and message.Message.find(self.CurrentSearchValue) != -1 and (timestamp is None or message.Timestamp < timestamp)) - + def GetMessageByTimestamp(self, timestamp): if self.CurrentMessage is not None: msgidx = numpy.argmin(abs(self.LogMessagesTimestamp - timestamp)) @@ -455,7 +456,7 @@ return self.GetPreviousMessage(msgidx, timestamp) return message, msgidx return None, None - + def GetNextMessage(self, msgidx): while msgidx < len(self.LogMessages) - 1: message = self.LogMessages[msgidx + 1] @@ -463,7 +464,7 @@ return message, msgidx + 1 msgidx += 1 return None, None - + def GetPreviousMessage(self, msgidx, timestamp=None): message = None while 0 < msgidx < len(self.LogMessages): @@ -490,7 +491,7 @@ self.OldestMessages[level] = (-1, None) if message is not None: message_idx = 0 - while (message_idx < len(self.LogMessages) and + while (message_idx < len(self.LogMessages) and self.LogMessages[message_idx] < message): message_idx += 1 if len(self.LogMessages) > 0: @@ -499,8 +500,8 @@ current_message = message self.LogMessages.insert(message_idx, message) self.LogMessagesTimestamp = numpy.insert( - self.LogMessagesTimestamp, - [message_idx], + self.LogMessagesTimestamp, + [message_idx], [message.Timestamp]) self.CurrentMessage = self.LogMessages.index(current_message) if message_idx == 0 and self.FilterLogMessage(message, timestamp): @@ -509,27 +510,27 @@ if msg is not None and (message is None or msg > message): message = msg return None, None - + def RefreshNewData(self, *args, **kwargs): if self.HasNewData: self.HasNewData = False self.RefreshView() DebugViewer.RefreshNewData(self, *args, **kwargs) - + def RefreshView(self): width, height = self.MessagePanel.GetClientSize() bitmap = wx.EmptyBitmap(width, height) dc = wx.BufferedDC(wx.ClientDC(self.MessagePanel), bitmap) dc.Clear() dc.BeginDrawing() - + if self.CurrentMessage is not None: - + dc.SetFont(self.Font) - + for button in self.LeftButtons + self.RightButtons: button.Draw(dc) - + message_idx = self.CurrentMessage message = self.LogMessages[message_idx] draw_date = True @@ -537,23 +538,23 @@ while offset < height and message is not None: message.Draw(dc, offset, width, draw_date) offset += message.GetHeight(draw_date) - + previous_message, message_idx = self.GetPreviousMessage(message_idx) if previous_message is not None: draw_date = message.Date != previous_message.Date message = previous_message - + dc.EndDrawing() - + self.MessageScrollBar.RefreshThumbPosition() - + def IsMessagePanelTop(self, message_idx=None): if message_idx is None: message_idx = self.CurrentMessage if message_idx is not None: return self.GetNextMessage(message_idx)[0] is None return True - + def IsMessagePanelBottom(self, message_idx=None): if message_idx is None: message_idx = self.CurrentMessage @@ -570,7 +571,7 @@ message = previous_message return offset < height return True - + def ScrollMessagePanel(self, scroll): if self.CurrentMessage is not None: message = self.LogMessages[self.CurrentMessage] @@ -585,13 +586,13 @@ self.CurrentMessage = msgidx scroll += 1 self.RefreshView() - + def ScrollMessagePanelByPage(self, page): if self.CurrentMessage is not None: width, height = self.MessagePanel.GetClientSize() message_per_page = max(1, (height - DATE_INFO_SIZE) / MESSAGE_INFO_SIZE - 1) self.ScrollMessagePanel(page * message_per_page) - + def ScrollMessagePanelByTimestamp(self, seconds): if self.CurrentMessage is not None: current_message = self.LogMessages[self.CurrentMessage] @@ -603,7 +604,7 @@ msgidx += 1 self.CurrentMessage = msgidx self.RefreshView() - + def ResetMessagePanel(self): if len(self.LogMessages) > 0: self.CurrentMessage = len(self.LogMessages) - 1 @@ -611,45 +612,45 @@ while message is not None and not self.FilterLogMessage(message): message, self.CurrentMessage = self.GetPreviousMessage(self.CurrentMessage) self.RefreshView() - + def OnMessageFilterChanged(self, event): self.CurrentFilter = self.LevelFilters[self.MessageFilter.GetSelection()] self.ResetMessagePanel() event.Skip() - + def OnSearchMessageChanged(self, event): self.CurrentSearchValue = self.SearchMessage.GetValue() self.ResetMessagePanel() event.Skip() - + def OnSearchMessageSearchButtonClick(self, event): self.CurrentSearchValue = self.SearchMessage.GetValue() self.ResetMessagePanel() event.Skip() - + def OnSearchMessageCancelButtonClick(self, event): self.CurrentSearchValue = "" self.SearchMessage.SetValue("") self.ResetMessagePanel() event.Skip() - + def OnCleanButton(self, event): if self.LogSource is not None: self.LogSource.ResetLogCount() self.ResetLogMessages() self.RefreshView() event.Skip() - + def GenerateOnDurationButton(self, duration): def OnDurationButton(): self.ScrollMessagePanelByTimestamp(duration) return OnDurationButton - + def GetCopyMessageToClipboardFunction(self, message): def CopyMessageToClipboardFunction(event): self.ParentWindow.SetCopyBuffer(message.GetFullText()) return CopyMessageToClipboardFunction - + def GetMessageByScreenPos(self, posx, posy): if self.CurrentMessage is not None: width, height = self.MessagePanel.GetClientSize() @@ -657,22 +658,22 @@ message = self.LogMessages[message_idx] draw_date = True offset = 5 - + while offset < height and message is not None: if draw_date: offset += DATE_INFO_SIZE - + if offset <= posy < offset + MESSAGE_INFO_SIZE: return message - + offset += MESSAGE_INFO_SIZE - + previous_message, message_idx = self.GetPreviousMessage(message_idx) if previous_message is not None: draw_date = message.Date != previous_message.Date message = previous_message return None - + def OnMessagePanelLeftUp(self, event): if self.CurrentMessage is not None: posx, posy = event.GetPosition() @@ -681,32 +682,32 @@ button.ProcessCallback() break event.Skip() - + def OnMessagePanelRightUp(self, event): message = self.GetMessageByScreenPos(*event.GetPosition()) if message is not None: menu = wx.Menu(title='') - + new_id = wx.NewId() menu.Append(help='', id=new_id, kind=wx.ITEM_NORMAL, text=_("Copy")) self.Bind(wx.EVT_MENU, self.GetCopyMessageToClipboardFunction(message), id=new_id) - + self.MessagePanel.PopupMenu(menu) menu.Destroy() event.Skip() - + def OnMessagePanelLeftDCLick(self, event): message = self.GetMessageByScreenPos(*event.GetPosition()) if message is not None: self.SearchMessage.SetFocus() self.SearchMessage.SetValue(message.Message) event.Skip() - + def ResetMessageToolTip(self): if self.MessageToolTip is not None: self.MessageToolTip.Destroy() self.MessageToolTip = None - + def OnMessageToolTipTimer(self, event): if self.LastMousePos is not None: message = self.GetMessageByScreenPos(*self.LastMousePos) @@ -719,31 +720,31 @@ self.MessageToolTip.SetToolTipPosition(tooltip_pos) self.MessageToolTip.Show() event.Skip() - + def OnMessagePanelMotion(self, event): if not event.Dragging(): self.ResetMessageToolTip() self.LastMousePos = event.GetPosition() self.MessageToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) event.Skip() - + def OnMessagePanelLeaveWindow(self, event): self.ResetMessageToolTip() self.LastMousePos = None self.MessageToolTipTimer.Stop() event.Skip() - + def OnMessagePanelMouseWheel(self, event): self.ScrollMessagePanel(event.GetWheelRotation() / event.GetWheelDelta()) event.Skip() - + def OnMessagePanelEraseBackground(self, event): pass - + def OnMessagePanelPaint(self, event): self.RefreshView() event.Skip() - + def OnMessagePanelResize(self, event): width, height = self.MessagePanel.GetClientSize() offset = 2 @@ -761,7 +762,7 @@ else: self.RefreshView() event.Skip() - + def OnScrollTimer(self, event): if self.ScrollSpeed != 0.: speed_norm = abs(self.ScrollSpeed) @@ -770,7 +771,7 @@ self.LastStartTime = gettime() self.ScrollTimer.Start(int(period * 1000), True) event.Skip() - + def SetScrollSpeed(self, speed): if speed == 0.: self.ScrollTimer.Stop() @@ -788,8 +789,8 @@ else: self.LastStartTime = current_time self.ScrollTimer.Start(int(period * 1000), True) - self.ScrollSpeed = speed - + self.ScrollSpeed = speed + def ScrollToLast(self, refresh=True): if len(self.LogMessages) > 0: self.CurrentMessage = len(self.LogMessages) - 1 diff -r 28e9d479aa65 -r de4ee16f7c6c doc/manual/connectors.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/manual/connectors.rst Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,107 @@ +Beremiz and Beremiz_service connectors +====================================== + +To connect a PLC, Beremiz provides 2 types of connectors : + * a Pyro connector + * a WAMP connector + +To configure the connection, you have to set the *URI_location* in your project Config tab according to this documentation. + +The Pyro connector +---------------------------- + +Pyro is an advanced and powerful Distributed Object Technology system written entirely in Python. +Beremiz_service spawns a Pyro server, serving a PLCObject (see runtime/PLCObject.py). Therefore, Beremiz acts as a Pyro client. + +TODO:: link to PLCObject API documentation + +URI_location : + * LOCAL:// is a facility that starts the PLC service locally and connect Beremiz to it via Pyro. + This is intended for use in development stage. + * PYRO:// normal connection to a remote PLC. PLC default port is 3000. + * PYROS:// SSL connection to a remote PLC, see below. + +more information about Pyro can be found on http://pythonhosted.org//Pyro/1-intro.html + +=========================== +Setup a Pyro SSL connection +=========================== + +Pyro v3 has a limited TLS/SSL support based on m2crypto. Pyro v4 had dropped it. +In order to have a full and reliable SSL, we recommand to use a TLS/SSL wrapper as nginx, stub or stunnel. + +-------------------- +TLS-PSK with stunnel +-------------------- + +In this example, we setup a simple TLS-PSK connection according to rfc4279. +This ciphersuite avoid the need for public key operations and certificate management. +It is perfect for a performance-constrained environments with limited CPU power as a PLC. + + +Needed : + * stunnel >= 5.09 + +verify openssl support for PSK cipher:: + + openssl ciphers -v 'PSK' + +---------------------- +Client setup (Beremiz) +---------------------- + +You need to choose an identity for your client, here *client1*. +generate a valid and strong key:: + + $ echo client1:$(openssl rand -base64 48) > pskclient1.txt + +write a stunnel client configuration file *stunnel-client.conf*:: + + output = stunnel-client.log + client = yes + + [beremiz] + accept = 3002 + connect = [PLC]:3001 + PSKidentity = client1 + PSKsecrets = pskclient1.txt + +start stunnel client side:: + + stunnel stunnel-client.conf + +You could now connect beremiz with classic URI_location = PYRO://127.0.0.1:3002 + +-------------------- +Server setup (PLC) +-------------------- + +import the client key in a keyfile psk.txt, concatening all client key. + +write a stunnel server configuration file *stunnel-server.conf*:: + + output = stunnel-server.log + + [beremiz] + accept = 3001 + connect = 127.0.0.1:3000 + PSKsecrets = psk.txt + +start stunnel server side:: + + stunnel stunnel-server.conf + +more documentation on stunnel http://www.stunnel.org/docs.html + +The WAMP connector +------------------ + +WAMP is an open standard WebSocket subprotocol that provides two application messaging +patterns in one unified protocol: Remote Procedure Calls + Publish & Subscribe. + +Beremiz WAMP connector implementation uses Autobahn and crossbar. + +URI_location : + * WAMP://127.0.0.1:8888#Automation#2534667845 + +more information about WAMP can be found on http://wamp.ws/ diff -r 28e9d479aa65 -r de4ee16f7c6c doc/manual/index.rst --- a/doc/manual/index.rst Sat Dec 06 19:31:51 2014 +0000 +++ b/doc/manual/index.rst Wed Oct 21 15:00:32 2015 +0100 @@ -10,7 +10,5 @@ start edit build + connectors debug - - - diff -r 28e9d479aa65 -r de4ee16f7c6c editors/CodeFileEditor.py --- a/editors/CodeFileEditor.py Sat Dec 06 19:31:51 2014 +0000 +++ b/editors/CodeFileEditor.py Wed Oct 21 15:00:32 2015 +0100 @@ -607,7 +607,7 @@ renderer = None colname = self.GetColLabelValue(col, False) - if colname in ["Name", "Initial"]: + if colname in ["Name", "Initial", "Description", "OnChange", "Options"]: editor = wx.grid.GridCellTextEditor() elif colname == "Class": editor = wx.grid.GridCellChoiceEditor() @@ -658,10 +658,13 @@ self.ParentWindow = window self.Controler = controler - self.VariablesDefaultValue = {"Name" : "", "Type" : DefaultType, "Initial": ""} - self.Table = VariablesTable(self, [], ["#", "Name", "Type", "Initial"]) - self.ColAlignements = [wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT] - self.ColSizes = [40, 200, 150, 150] + self.VariablesDefaultValue = {"Name" : "", "Type" : DefaultType, "Initial": "", + "Description":"", "OnChange":"", "Options":""} + self.Table = VariablesTable(self, [], ["#", "Name", "Type", "Initial", + "Description", "OnChange", "Options"]) + self.ColAlignements = [wx.ALIGN_RIGHT] + \ + [wx.ALIGN_LEFT]*(len(self.VariablesDefaultValue)) + self.ColSizes = [20, 150] + [100]*(len(self.VariablesDefaultValue)-1) self.VariablesGrid.SetTable(self.Table) self.VariablesGrid.SetButtons({"Add": self.AddVariableButton, "Delete": self.DeleteVariableButton, diff -r 28e9d479aa65 -r de4ee16f7c6c editors/ConfTreeNodeEditor.py --- a/editors/ConfTreeNodeEditor.py Sat Dec 06 19:31:51 2014 +0000 +++ b/editors/ConfTreeNodeEditor.py Wed Oct 21 15:00:32 2015 +0100 @@ -29,11 +29,6 @@ SCROLLBAR_UNIT = 10 -CWD = os.path.split(os.path.realpath(__file__))[0] - -def Bpath(*args): - return os.path.join(CWD,*args) - class GenBitmapTextButton(wx.lib.buttons.GenBitmapTextButton): def _GetLabelSize(self): """ used internally """ @@ -88,82 +83,82 @@ class GenStaticBitmap(wx.StaticBitmap): - """ Customized GenStaticBitmap, fix transparency redraw bug on wx2.8/win32, + """ Customized GenStaticBitmap, fix transparency redraw bug on wx2.8/win32, and accept image name as __init__ parameter, fail silently if file do not exist""" def __init__(self, parent, ID, bitmapname, pos = wx.DefaultPosition, size = wx.DefaultSize, style = 0, name = "genstatbmp"): - + bitmap = GetBitmap(bitmapname) if bitmap is None: bitmap = wx.EmptyBitmap(0, 0) - - wx.StaticBitmap.__init__(self, parent, ID, + + wx.StaticBitmap.__init__(self, parent, ID, bitmap, pos, size, style, name) class ConfTreeNodeEditor(EditorPanel): - + SHOW_BASE_PARAMS = True SHOW_PARAMS = True CONFNODEEDITOR_TABS = [] - + def _init_Editor(self, parent): tabs_num = len(self.CONFNODEEDITOR_TABS) if self.SHOW_PARAMS and len(self.Controler.GetParamsAttributes()) > 0: tabs_num += 1 - + if tabs_num > 1 or self.SHOW_BASE_PARAMS: - self.Editor = wx.Panel(parent, + self.Editor = wx.Panel(parent, style=wx.SUNKEN_BORDER|wx.SP_3D) - + self.MainSizer = wx.BoxSizer(wx.VERTICAL) - + if self.SHOW_BASE_PARAMS: baseparamseditor_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.MainSizer.AddSizer(baseparamseditor_sizer, border=5, + self.MainSizer.AddSizer(baseparamseditor_sizer, border=5, flag=wx.GROW|wx.ALL) - + self.FullIECChannel = wx.StaticText(self.Editor, -1) self.FullIECChannel.SetFont( - wx.Font(faces["size"], wx.DEFAULT, wx.NORMAL, + wx.Font(faces["size"], wx.DEFAULT, wx.NORMAL, wx.BOLD, faceName = faces["helv"])) - baseparamseditor_sizer.AddWindow(self.FullIECChannel, + baseparamseditor_sizer.AddWindow(self.FullIECChannel, flag=wx.ALIGN_CENTER_VERTICAL) - + updownsizer = wx.BoxSizer(wx.VERTICAL) - baseparamseditor_sizer.AddSizer(updownsizer, border=5, + baseparamseditor_sizer.AddSizer(updownsizer, border=5, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL) - - self.IECCUpButton = wx.lib.buttons.GenBitmapTextButton(self.Editor, + + self.IECCUpButton = wx.lib.buttons.GenBitmapTextButton(self.Editor, bitmap=GetBitmap('IECCDown'), size=wx.Size(16, 16), style=wx.NO_BORDER) - self.IECCUpButton.Bind(wx.EVT_BUTTON, self.GetItemChannelChangedFunction(1), + self.IECCUpButton.Bind(wx.EVT_BUTTON, self.GetItemChannelChangedFunction(1), self.IECCUpButton) updownsizer.AddWindow(self.IECCUpButton, flag=wx.ALIGN_LEFT) - - self.IECCDownButton = wx.lib.buttons.GenBitmapButton(self.Editor, + + self.IECCDownButton = wx.lib.buttons.GenBitmapButton(self.Editor, bitmap=GetBitmap('IECCUp'), size=wx.Size(16, 16), style=wx.NO_BORDER) - self.IECCDownButton.Bind(wx.EVT_BUTTON, self.GetItemChannelChangedFunction(-1), + self.IECCDownButton.Bind(wx.EVT_BUTTON, self.GetItemChannelChangedFunction(-1), self.IECCDownButton) updownsizer.AddWindow(self.IECCDownButton, flag=wx.ALIGN_LEFT) - - self.ConfNodeName = wx.TextCtrl(self.Editor, + + self.ConfNodeName = wx.TextCtrl(self.Editor, size=wx.Size(150, 25)) self.ConfNodeName.SetFont( - wx.Font(faces["size"] * 0.75, wx.DEFAULT, wx.NORMAL, + wx.Font(faces["size"] * 0.75, wx.DEFAULT, wx.NORMAL, wx.BOLD, faceName = faces["helv"])) - self.ConfNodeName.Bind(wx.EVT_TEXT, - self.GetTextCtrlCallBackFunction(self.ConfNodeName, "BaseParams.Name", True), + self.ConfNodeName.Bind(wx.EVT_TEXT, + self.GetTextCtrlCallBackFunction(self.ConfNodeName, "BaseParams.Name", True), self.ConfNodeName) - baseparamseditor_sizer.AddWindow(self.ConfNodeName, border=5, + baseparamseditor_sizer.AddWindow(self.ConfNodeName, border=5, flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL) - + buttons_sizer = self.GenerateMethodButtonSizer() baseparamseditor_sizer.AddSizer(buttons_sizer, flag=wx.ALIGN_CENTER) - + if tabs_num > 1: self.ConfNodeNoteBook = wx.Notebook(self.Editor) parent = self.ConfNodeNoteBook @@ -171,12 +166,12 @@ else: parent = self.Editor self.ConfNodeNoteBook = None - + self.Editor.SetSizer(self.MainSizer) else: self.ConfNodeNoteBook = None self.Editor = None - + for title, create_func_name in self.CONFNODEEDITOR_TABS: editor = getattr(self, create_func_name)(parent) if self.ConfNodeNoteBook is not None: @@ -185,28 +180,28 @@ self.MainSizer.AddWindow(editor, 1, flag=wx.GROW) else: self.Editor = editor - + if self.SHOW_PARAMS and len(self.Controler.GetParamsAttributes()) > 0: - + panel_style = wx.TAB_TRAVERSAL|wx.HSCROLL|wx.VSCROLL if self.ConfNodeNoteBook is None and parent != self.Editor: panel_style |= wx.SUNKEN_BORDER - self.ParamsEditor = wx.ScrolledWindow(parent, + self.ParamsEditor = wx.ScrolledWindow(parent, style=panel_style) self.ParamsEditor.Bind(wx.EVT_SIZE, self.OnParamsEditorResize) self.ParamsEditor.Bind(wx.EVT_SCROLLWIN, self.OnParamsEditorScroll) - + self.ParamsEditorSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=1, vgap=5) self.ParamsEditorSizer.AddGrowableCol(0) self.ParamsEditorSizer.AddGrowableRow(0) self.ParamsEditor.SetSizer(self.ParamsEditorSizer) - + self.ConfNodeParamsSizer = wx.BoxSizer(wx.VERTICAL) - self.ParamsEditorSizer.AddSizer(self.ConfNodeParamsSizer, border=5, + self.ParamsEditorSizer.AddSizer(self.ConfNodeParamsSizer, border=5, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM) - + self.RefreshConfNodeParamsSizer() - + if self.ConfNodeNoteBook is not None: self.ConfNodeNoteBook.AddPage(self.ParamsEditor, _("Config")) elif self.SHOW_BASE_PARAMS: @@ -215,40 +210,40 @@ self.Editor = self.ParamsEditor else: self.ParamsEditor = None - + def __init__(self, parent, controler, window, tagname=""): EditorPanel.__init__(self, parent, tagname, window, controler) - + icon_name = self.Controler.GetIconName() if icon_name is not None: self.SetIcon(GetBitmap(icon_name)) else: self.SetIcon(GetBitmap("Extension")) - + def __del__(self): self.Controler.OnCloseEditor(self) - + def GetTagName(self): return self.Controler.CTNFullName() - + def GetTitle(self): fullname = self.Controler.CTNFullName() if self.Controler.CTNTestModified(): return "~%s~" % fullname return fullname - + def HasNoModel(self): return False - + def GetBufferState(self): return False, False - + def Undo(self): pass - + def Redo(self): pass - + def RefreshView(self): EditorPanel.RefreshView(self) if self.SHOW_BASE_PARAMS: @@ -257,33 +252,33 @@ if self.ParamsEditor is not None: self.RefreshConfNodeParamsSizer() self.RefreshScrollbars() - + def RefreshIECChannelControlsState(self): self.FullIECChannel.SetLabel(self.Controler.GetFullIEC_Channel()) self.IECCDownButton.Enable(self.Controler.BaseParams.getIEC_Channel() > 0) self.MainSizer.Layout() - + def RefreshConfNodeParamsSizer(self): self.Freeze() self.ConfNodeParamsSizer.Clear(True) - + confnode_infos = self.Controler.GetParamsAttributes() if len(confnode_infos) > 0: self.GenerateSizerElements(self.ConfNodeParamsSizer, confnode_infos, None, False) - + self.ParamsEditorSizer.Layout() self.Thaw() - + def GenerateMethodButtonSizer(self): normal_bt_font=wx.Font(faces["size"] / 3, wx.DEFAULT, wx.NORMAL, wx.NORMAL, faceName = faces["helv"]) mouseover_bt_font=wx.Font(faces["size"] / 3, wx.DEFAULT, wx.NORMAL, wx.NORMAL, underline=True, faceName = faces["helv"]) - + msizer = wx.BoxSizer(wx.HORIZONTAL) - + for confnode_method in self.Controler.ConfNodeMethods: if "method" in confnode_method and confnode_method.get("shown",True): button = GenBitmapTextButton(self.Editor, - bitmap=GetBitmap(confnode_method.get("bitmap", "Unknown")), + bitmap=GetBitmap(confnode_method.get("bitmap", "Unknown")), label=confnode_method["name"], style=wx.NO_BORDER) button.SetFont(normal_bt_font) button.SetToolTipString(confnode_method["tooltip"]) @@ -305,7 +300,7 @@ button.Disable() msizer.AddWindow(button, flag=wx.ALIGN_CENTER) return msizer - + def GenerateSizerElements(self, sizer, elements, path, clean = True): if clean: sizer.Clear(True) @@ -321,44 +316,44 @@ label = _(name) if value is not None: label += " - %s" % _(value) - staticbox = wx.StaticBox(self.ParamsEditor, + staticbox = wx.StaticBox(self.ParamsEditor, label=_(label), size=wx.Size(10, 0)) staticboxsizer = wx.StaticBoxSizer(staticbox, wx.VERTICAL) if first: - sizer.AddSizer(staticboxsizer, border=5, + sizer.AddSizer(staticboxsizer, border=5, flag=wx.GROW|wx.TOP|wx.BOTTOM) else: - sizer.AddSizer(staticboxsizer, border=5, + sizer.AddSizer(staticboxsizer, border=5, flag=wx.GROW|wx.BOTTOM) - self.GenerateSizerElements(staticboxsizer, - element_infos["children"], + self.GenerateSizerElements(staticboxsizer, + element_infos["children"], element_path) else: boxsizer = wx.FlexGridSizer(cols=3, rows=1) boxsizer.AddGrowableCol(1) if first: - sizer.AddSizer(boxsizer, border=5, + sizer.AddSizer(boxsizer, border=5, flag=wx.GROW|wx.ALL) else: - sizer.AddSizer(boxsizer, border=5, + sizer.AddSizer(boxsizer, border=5, flag=wx.GROW|wx.LEFT|wx.RIGHT|wx.BOTTOM) - + staticbitmap = GenStaticBitmap(ID=-1, bitmapname=element_infos["name"], name="%s_bitmap"%element_infos["name"], parent=self.ParamsEditor, pos=wx.Point(0, 0), size=wx.Size(24, 24), style=0) boxsizer.AddWindow(staticbitmap, border=5, flag=wx.RIGHT) - - statictext = wx.StaticText(self.ParamsEditor, + + statictext = wx.StaticText(self.ParamsEditor, label="%s:"%_(element_infos["name"])) - boxsizer.AddWindow(statictext, border=5, + boxsizer.AddWindow(statictext, border=5, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT) - + if isinstance(element_infos["type"], types.ListType): if isinstance(element_infos["value"], types.TupleType): browse_boxsizer = wx.BoxSizer(wx.HORIZONTAL) boxsizer.AddSizer(browse_boxsizer) - - textctrl = wx.TextCtrl(self.ParamsEditor, + + textctrl = wx.TextCtrl(self.ParamsEditor, size=wx.Size(275, -1), style=wx.TE_READONLY) if element_infos["value"] is not None: textctrl.SetValue(element_infos["value"][0]) @@ -366,19 +361,19 @@ else: value_infos = None browse_boxsizer.AddWindow(textctrl) - - button = wx.Button(self.ParamsEditor, + + button = wx.Button(self.ParamsEditor, label="...", size=wx.Size(25, 25)) browse_boxsizer.AddWindow(button) - button.Bind(wx.EVT_BUTTON, - self.GetBrowseCallBackFunction(element_infos["name"], textctrl, element_infos["type"], - value_infos, element_path), + button.Bind(wx.EVT_BUTTON, + self.GetBrowseCallBackFunction(element_infos["name"], textctrl, element_infos["type"], + value_infos, element_path), button) else: - combobox = wx.ComboBox(self.ParamsEditor, + combobox = wx.ComboBox(self.ParamsEditor, size=wx.Size(300, -1), style=wx.CB_READONLY) boxsizer.AddWindow(combobox) - + if element_infos["use"] == "optional": combobox.Append("") if len(element_infos["type"]) > 0 and isinstance(element_infos["type"][0], types.TupleType): @@ -386,8 +381,8 @@ combobox.Append(choice) name = element_infos["name"] value = element_infos["value"] - - staticbox = wx.StaticBox(self.ParamsEditor, + + staticbox = wx.StaticBox(self.ParamsEditor, label="%s - %s"%(_(name), _(value)), size=wx.Size(10, 0)) staticboxsizer = wx.StaticBoxSizer(staticbox, wx.VERTICAL) sizer.AddSizer(staticboxsizer, border=5, flag=wx.GROW|wx.BOTTOM) @@ -402,7 +397,7 @@ else: combobox.SetStringSelection(element_infos["value"]) combobox.Bind(wx.EVT_COMBOBOX, callback, combobox) - + elif isinstance(element_infos["type"], types.DictType): scmin = -(2**31) scmax = 2**31-1 @@ -410,50 +405,50 @@ scmin = element_infos["type"]["min"] if "max" in element_infos["type"]: scmax = element_infos["type"]["max"] - spinctrl = wx.SpinCtrl(self.ParamsEditor, + spinctrl = wx.SpinCtrl(self.ParamsEditor, size=wx.Size(300, -1), style=wx.SP_ARROW_KEYS|wx.ALIGN_RIGHT) spinctrl.SetRange(scmin, scmax) boxsizer.AddWindow(spinctrl) if element_infos["value"] is not None: spinctrl.SetValue(element_infos["value"]) - spinctrl.Bind(wx.EVT_SPINCTRL, + spinctrl.Bind(wx.EVT_SPINCTRL, self.GetTextCtrlCallBackFunction(spinctrl, element_path), spinctrl) - + else: if element_infos["type"] == "boolean": checkbox = wx.CheckBox(self.ParamsEditor, size=wx.Size(17, 25)) boxsizer.AddWindow(checkbox) if element_infos["value"] is not None: checkbox.SetValue(element_infos["value"]) - checkbox.Bind(wx.EVT_CHECKBOX, - self.GetCheckBoxCallBackFunction(checkbox, element_path), + checkbox.Bind(wx.EVT_CHECKBOX, + self.GetCheckBoxCallBackFunction(checkbox, element_path), checkbox) - + elif element_infos["type"] in ["unsignedLong", "long","integer"]: if element_infos["type"].startswith("unsigned"): scmin = 0 else: scmin = -(2**31) scmax = 2**31-1 - spinctrl = wx.SpinCtrl(self.ParamsEditor, + spinctrl = wx.SpinCtrl(self.ParamsEditor, size=wx.Size(300, -1), style=wx.SP_ARROW_KEYS|wx.ALIGN_RIGHT) spinctrl.SetRange(scmin, scmax) boxsizer.AddWindow(spinctrl) if element_infos["value"] is not None: spinctrl.SetValue(element_infos["value"]) - spinctrl.Bind(wx.EVT_SPINCTRL, - self.GetTextCtrlCallBackFunction(spinctrl, element_path), + spinctrl.Bind(wx.EVT_SPINCTRL, + self.GetTextCtrlCallBackFunction(spinctrl, element_path), spinctrl) - + else: choices = self.ParentWindow.GetConfigEntry(element_path, [""]) - textctrl = TextCtrlAutoComplete(name=element_infos["name"], - parent=self.ParamsEditor, - choices=choices, + textctrl = TextCtrlAutoComplete(name=element_infos["name"], + parent=self.ParamsEditor, + choices=choices, element_path=element_path, size=wx.Size(300, -1)) - + boxsizer.AddWindow(textctrl) if element_infos["value"] is not None: textctrl.ChangeValue(str(element_infos["value"])) @@ -461,8 +456,8 @@ textctrl.Bind(wx.EVT_TEXT_ENTER, callback) textctrl.Bind(wx.EVT_KILL_FOCUS, callback) first = False - - + + def GetItemChannelChangedFunction(self, dir): def OnConfNodeTreeItemChannelChanged(event): confnode_IECChannel = self.Controler.BaseParams.getIEC_Channel() @@ -471,28 +466,28 @@ wx.CallAfter(self.ParentWindow._Refresh, TITLE, FILEMENU, PROJECTTREE) event.Skip() return OnConfNodeTreeItemChannelChanged - + def SetConfNodeParamsAttribute(self, *args, **kwargs): res, StructChanged = self.Controler.SetParamsAttribute(*args, **kwargs) if StructChanged and self.ParamsEditor is not None: wx.CallAfter(self.RefreshConfNodeParamsSizer) wx.CallAfter(self.ParentWindow._Refresh, TITLE, FILEMENU) return res - + def GetButtonCallBackFunction(self, method, push=False): """ Generate the callbackfunc for a given confnode method""" def OnButtonClick(event): - # Disable button to prevent re-entrant call + # Disable button to prevent re-entrant call event.GetEventObject().Disable() # Call getattr(self.Controler,method)() - # Re-enable button + # Re-enable button event.GetEventObject().Enable() - + if not push: event.Skip() return OnButtonClick - + def GetChoiceCallBackFunction(self, choicectrl, path): def OnChoiceChanged(event): res = self.SetConfNodeParamsAttribute(path, choicectrl.GetStringSelection()) @@ -501,14 +496,14 @@ choicectrl.SetStringSelection(res) event.Skip() return OnChoiceChanged - + def GetChoiceContentCallBackFunction(self, choicectrl, staticboxsizer, path): def OnChoiceContentChanged(event): res = self.SetConfNodeParamsAttribute(path, choicectrl.GetStringSelection()) wx.CallAfter(self.RefreshConfNodeParamsSizer) event.Skip() return OnChoiceContentChanged - + def GetTextCtrlCallBackFunction(self, textctrl, path, refresh=False): def OnTextCtrlChanged(event): res = self.SetConfNodeParamsAttribute(path, textctrl.GetValue()) @@ -522,14 +517,14 @@ wx.CallAfter(self.ParentWindow.SelectProjectTreeItem, self.GetTagName()) event.Skip() return OnTextCtrlChanged - + def GetCheckBoxCallBackFunction(self, chkbx, path): def OnCheckBoxChanged(event): res = self.SetConfNodeParamsAttribute(path, chkbx.IsChecked()) chkbx.SetValue(res) event.Skip() return OnCheckBoxChanged - + def GetBrowseCallBackFunction(self, name, textctrl, library, value_infos, path): infos = [value_infos] def OnBrowseButton(event): @@ -541,7 +536,7 @@ dialog.Destroy() event.Skip() return OnBrowseButton - + def RefreshScrollbars(self): self.ParamsEditor.GetBestSize() xstart, ystart = self.ParamsEditor.GetViewStart() @@ -550,17 +545,17 @@ posx = max(0, min(xstart, (maxx - window_size[0]) / SCROLLBAR_UNIT)) posy = max(0, min(ystart, (maxy - window_size[1]) / SCROLLBAR_UNIT)) self.ParamsEditor.Scroll(posx, posy) - self.ParamsEditor.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, + self.ParamsEditor.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, maxx / SCROLLBAR_UNIT, maxy / SCROLLBAR_UNIT, posx, posy) - + def OnParamsEditorResize(self, event): self.RefreshScrollbars() event.Skip() - + def OnParamsEditorScroll(self, event): control = self.ParamsEditor.FindFocus() if isinstance(control, TextCtrlAutoComplete): control.DismissListBox() self.Refresh() event.Skip() - + diff -r 28e9d479aa65 -r de4ee16f7c6c editors/DebugViewer.py --- a/editors/DebugViewer.py Sat Dec 06 19:31:51 2014 +0000 +++ b/editors/DebugViewer.py Wed Oct 21 15:00:32 2015 +0100 @@ -246,7 +246,7 @@ if self.DataProducer is not None: self.DataProducer.ReleaseDebugIECVariable(iec_path) - def NewDataAvailable(self, ticks, *args, **kwargs): + def NewDataAvailable(self, ticks): """ Called by DataProducer for each tick captured @param tick: PLC tick captured @@ -267,19 +267,19 @@ # two refresh has expired if gettime() - self.LastRefreshTime > REFRESH_PERIOD and \ DEBUG_REFRESH_LOCK.acquire(False): - self.StartRefreshing(*args, **kwargs) + self.StartRefreshing() # If common lock wasn't acquired for any reason, restart last # refresh timer else: - self.StartLastRefreshTimer(*args, **kwargs) + self.StartLastRefreshTimer() # In the case that DebugViewer isn't visible on screen and has already # acquired common refresh lock, reset DebugViewer elif not self.IsShown() and self.HasAcquiredLock: DebugViewer.RefreshNewData(self) - def ShouldRefresh(self, *args, **kwargs): + def ShouldRefresh(self): """ Callback function called when last refresh timer expired All parameters are passed to refresh function @@ -289,13 +289,13 @@ # Try to acquire common refresh lock if DEBUG_REFRESH_LOCK.acquire(False): - self.StartRefreshing(*args, **kwargs) + self.StartRefreshing() # Restart last refresh timer if common refresh lock acquired failed else: - self.StartLastRefreshTimer(*args, **kwargs) - - def StartRefreshing(self, *args, **kwargs): + self.StartLastRefreshTimer() + + def StartRefreshing(self): """ Called to initiate a refresh of DebugViewer All parameters are passed to refresh function @@ -311,9 +311,9 @@ self.Inhibit(True) # Initiate DebugViewer refresh - wx.CallAfter(self.RefreshNewData, *args, **kwargs) - - def StartLastRefreshTimer(self, *args, **kwargs): + wx.CallAfter(self.RefreshNewData) + + def StartLastRefreshTimer(self): """ Called to start last refresh timer for the minimum time between 2 refresh @@ -321,11 +321,11 @@ """ self.TimerAccessLock.acquire() self.LastRefreshTimer = Timer( - REFRESH_PERIOD, self.ShouldRefresh, args, kwargs) + REFRESH_PERIOD, self.ShouldRefresh) self.LastRefreshTimer.start() self.TimerAccessLock.release() - def RefreshNewData(self, *args, **kwargs): + def RefreshNewData(self): """ Called to refresh DebugViewer according to values received by data consumers diff -r 28e9d479aa65 -r de4ee16f7c6c editors/Viewer.py --- a/editors/Viewer.py Sat Dec 06 19:31:51 2014 +0000 +++ b/editors/Viewer.py Wed Oct 21 15:00:32 2015 +0100 @@ -1114,7 +1114,7 @@ self.ElementRefreshList.append(element) self.ElementRefreshList_lock.release() - def NewDataAvailable(self, ticks, *args, **kwargs): + def NewDataAvailable(self, ticks): if self.IsShown(): refresh_rect = None self.ElementRefreshList_lock.acquire() diff -r 28e9d479aa65 -r de4ee16f7c6c py_ext/PythonFileCTNMixin.py --- a/py_ext/PythonFileCTNMixin.py Sat Dec 06 19:31:51 2014 +0000 +++ b/py_ext/PythonFileCTNMixin.py Wed Oct 21 15:00:32 2015 +0100 @@ -7,7 +7,7 @@ from PythonEditor import PythonEditor class PythonFileCTNMixin(CodeFile): - + CODEFILE_NAME = "PyFile" SECTIONS_NAMES = [ "globals", @@ -16,45 +16,45 @@ "start", "stop"] EditorType = PythonEditor - + def __init__(self): CodeFile.__init__(self) - + filepath = self.PythonFileName() - + if os.path.isfile(filepath): PythonParser = GenerateParserFromXSD( - os.path.join(os.path.dirname(__file__), "py_ext_xsd.xsd")) - + os.path.join(os.path.dirname(__file__), "py_ext_xsd.xsd")) + xmlfile = open(filepath, 'r') pythonfile_xml = xmlfile.read() xmlfile.close() - + pythonfile_xml = pythonfile_xml.replace( - 'xmlns="http://www.w3.org/2001/XMLSchema"', + 'xmlns="http://www.w3.org/2001/XMLSchema"', 'xmlns:xhtml="http://www.w3.org/1999/xhtml"') for cre, repl in [ (re.compile("(?)(?:)(?!)"), "]]>")]: pythonfile_xml = cre.sub(repl, pythonfile_xml) - + try: python_code, error = PythonParser.LoadXMLString(pythonfile_xml) - if error is None: + if error is None: self.CodeFile.globals.setanyText(python_code.getanyText()) os.remove(filepath) self.CreateCodeFileBuffer(False) self.OnCTNSave() except Exception, exc: error = unicode(exc) - + if error is not None: self.GetCTRoot().logger.write_error( - _("Couldn't import old %s file.") % CTNName) - + _("Couldn't import old %s file.") % self.CTNName()) + def CodeFileName(self): return os.path.join(self.CTNPath(), "pyfile.xml") - + def PythonFileName(self): return os.path.join(self.CTNPath(), "py_ext.xml") @@ -64,30 +64,46 @@ return self.PreSectionsTexts.get(section,"") + "\n" + \ getattr(self.CodeFile, section).getanyText() + "\n" + \ self.PostSectionsTexts.get(section,"") - def CTNGenerate_C(self, buildpath, locations): - # location string for that CTN - location_str = "_".join(map(lambda x:str(x), + # location string for that CTN + location_str = "_".join(map(lambda x:str(x), self.GetCurrentLocation())) configname = self.GetCTRoot().GetProjectConfigNames()[0] - - + + pyextname = self.CTNName() + varinfos = map(lambda variable : { + "name": variable.getname(), + "desc" : repr(variable.getdesc()), + "onchangecode" : '"'+variable.getonchange()+\ + "('"+variable.getname()+"')\"" \ + if variable.getonchange() else '""', + "onchange" : repr(variable.getonchange()) \ + if variable.getonchange() else None, + "opts" : repr(variable.getopts()), + "configname" : configname.upper(), + "uppername" : variable.getname().upper(), + "IECtype" : variable.gettype(), + "pyextname" :pyextname}, + self.CodeFile.variables.variable) # python side PLC global variables access stub globalstubs = "\n".join(["""\ _%(name)s_ctype, _%(name)s_unpack, _%(name)s_pack = \\ TypeTranslator["%(IECtype)s"] _PySafeGetPLCGlob_%(name)s = PLCBinary.__SafeGetPLCGlob_%(name)s _PySafeGetPLCGlob_%(name)s.restype = None -_PySafeGetPLCGlob_%(name)s.argtypes = [ctypes.POINTER(_%(name)s_ctype)] +_PySafeGetPLCGlob_%(name)s.argtypes = [ctypes.POINTER(_%(name)s_ctype)] _PySafeSetPLCGlob_%(name)s = PLCBinary.__SafeSetPLCGlob_%(name)s _PySafeSetPLCGlob_%(name)s.restype = None _PySafeSetPLCGlob_%(name)s.argtypes = [ctypes.POINTER(_%(name)s_ctype)] -""" % { "name": variable.getname(), - "configname": configname.upper(), - "uppername": variable.getname().upper(), - "IECtype": variable.gettype()} - for variable in self.CodeFile.variables.variable]) +_%(pyextname)sGlobalsDesc.append(( + "%(name)s", + "%(IECtype)s", + %(desc)s, + %(onchange)s, + %(opts)s)) +""" % varinfo + for varinfo in varinfos]) # Runtime calls (start, stop, init, and cleanup) rtcalls = "" @@ -101,36 +117,41 @@ else: rtcalls += " pass\n\n" - globalsection = self.GetSection("globals") + globalsection = self.GetSection("globals") PyFileContent = """\ #!/usr/bin/env python # -*- coding: utf-8 -*- ## Code generated by Beremiz python mixin confnode -## - +## + ## Code for PLC global variable access from targets.typemapping import TypeTranslator -import ctypes +import ctypes +_%(pyextname)sGlobalsDesc = [] +__ext_name__ = "%(pyextname)s" +PLCGlobalsDesc.append(( "%(pyextname)s" , _%(pyextname)sGlobalsDesc )) %(globalstubs)s - + ## User code in "global" scope %(globalsection)s ## Beremiz python runtime calls %(rtcalls)s +del __ext_name__ + """ % locals() # write generated content to python file - runtimefile_path = os.path.join(buildpath, + runtimefile_path = os.path.join(buildpath, "runtime_%s.py"%location_str) runtimefile = open(runtimefile_path, 'w') runtimefile.write(PyFileContent.encode('utf-8')) runtimefile.close() # C code for safe global variables access - + vardecfmt = """\ extern __IEC_%(IECtype)s_t %(configname)s__%(uppername)s; IEC_%(IECtype)s __%(name)s_rbuffer = __INIT_%(IECtype)s; @@ -151,6 +172,11 @@ } """ + + vardeconchangefmt = """\ +PYTHON_POLL* __%(name)s_notifier; +""" + varretfmt = """\ if(!AtomicCompareExchange(&__%(name)s_wlock, 0, 1)){ if(__%(name)s_wbuffer_written == 1){ @@ -159,34 +185,51 @@ } AtomicCompareExchange((long*)&__%(name)s_wlock, 1, 0); } -""" +""" varpubfmt = """\ if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){ - __%(name)s_rbuffer = %(configname)s__%(uppername)s.value; + __%(name)s_rbuffer = __GET_VAR(%(configname)s__%(uppername)s); AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0); } -""" - - var_str = map("\n".join, zip(*[ - map(lambda f : f % varinfo, - (vardecfmt, varretfmt, varpubfmt)) - for varinfo in map(lambda variable : { - "name": variable.getname(), - "configname": configname.upper(), - "uppername": variable.getname().upper(), - "IECtype": variable.gettype()}, - self.CodeFile.variables.variable)])) - if len(var_str) > 0: - vardec, varret, varpub = var_str - else: - vardec = varret = varpub = "" - +""" + + varpubonchangefmt = """\ + if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){ + IEC_%(IECtype)s tmp = __GET_VAR(%(configname)s__%(uppername)s); + if(__%(name)s_rbuffer != tmp){ + __%(name)s_rbuffer = %(configname)s__%(uppername)s.value; + PYTHON_POLL_body__(__%(name)s_notifier); + } + AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0); + } +""" + varinitonchangefmt = """\ + __%(name)s_notifier = __GET_GLOBAL_ON%(uppername)sCHANGE(); + __SET_VAR(__%(name)s_notifier->,TRIG,,__BOOL_LITERAL(TRUE)); + __SET_VAR(__%(name)s_notifier->,CODE,,__STRING_LITERAL(%(onchangelen)d,%(onchangecode)s)); +""" + vardec = "\n".join([(vardecfmt + vardeconchangefmt + if varinfo["onchange"] else vardecfmt)% varinfo + for varinfo in varinfos]) + varret = "\n".join([varretfmt % varinfo for varinfo in varinfos]) + varpub = "\n".join([(varpubonchangefmt if varinfo["onchange"] else + varpubfmt) % varinfo + for varinfo in varinfos]) + varinit = "\n".join([varinitonchangefmt % dict( + onchangelen = len(varinfo["onchangecode"]),**varinfo) + for varinfo in varinfos if varinfo["onchange"]]) + + # TODO : use config name obtained from model instead of default + # "config.h". User cannot change config name, but project imported + # or created in older beremiz vesion could use different name. PyCFileContent = """\ -/* - * Code generated by Beremiz py_ext confnode +/* + * Code generated by Beremiz py_ext confnode * for safe global variables access */ #include "iec_types_all.h" +#include "POUS.h" +#include "config.h" #include "beremiz.h" /* User variables reference */ @@ -194,6 +237,7 @@ /* Beremiz confnode functions */ int __init_%(location_str)s(int argc,char **argv){ +%(varinit)s return 0; } @@ -208,15 +252,15 @@ %(varpub)s } """ % locals() - + Gen_PyCfile_path = os.path.join(buildpath, "PyCFile_%s.c"%location_str) pycfile = open(Gen_PyCfile_path,'w') pycfile.write(PyCFileContent) pycfile.close() - + matiec_flags = '"-l -p -I%s"'%os.path.abspath( self.GetCTRoot().GetIECLibPath()) - + return ([(Gen_PyCfile_path, matiec_flags)], "", True, diff -r 28e9d479aa65 -r de4ee16f7c6c runtime/NevowServer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/NevowServer.py Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,173 @@ +import os +from nevow import rend, appserver, inevow, tags, loaders, athena +from nevow.page import renderer +from twisted.python import util +from twisted.internet import reactor + +xhtml_header = ''' + +''' + +WorkingDir = None + +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'))[ + 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(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: + f = self.PLCStartedHMIClass() + else: + f = PLCStoppedHMI() + f.setFragmentParent(self) + self.HMI = f + return f + athena.expose(getPLCElement) + + def detachFragmentChildren(self): + for child in self.liveFragmentChildren[:]: + child.detach() + +class WebInterface(athena.LivePage): + + docFactory = loaders.stan([tags.raw(xhtml_header), + tags.html(xmlns="http://www.w3.org/1999/xhtml")[ + tags.head(render=tags.directive('liveglue')), + tags.body[ + tags.div[ + tags.div( render = tags.directive( "MainPage" )) + ]]]]) + 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__, 'webinterface.js') + self.plcState = plcState + self.MainPage.setPLCState(plcState) + + 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 + """ + req = inevow.IRequest(ctx) + req.setHeader('Content-type', 'application/xhtml+xml') + return super(WebInterface, self).renderHTTP(ctx) + + def render_MainPage(self, ctx, data): + f = self.MainPage + f.setFragmentParent(self) + return ctx.tag[f] + + 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" + +def RegisterWebsite(port): + website = WebInterface() + site = appserver.NevowSite(website) + + listening = False + reactor.listenTCP(port, site) + print "Http interface port :",port + return website + +class statuslistener: + def __init__(self, site): + self.oldstate = None + self.site = site + + def listen(self, state): + if state != self.oldstate: + action = {'Started': self.site.PLCStarted, + 'Stopped': self.site.PLCStopped}.get(state, None) + if action is not None: action () + self.oldstate = state + +def website_statuslistener_factory(site): + return statuslistener(site).listen diff -r 28e9d479aa65 -r de4ee16f7c6c runtime/PLCObject.py --- a/runtime/PLCObject.py Sat Dec 06 19:31:51 2014 +0000 +++ b/runtime/PLCObject.py Wed Oct 21 15:00:32 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 # @@ -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,13 +51,12 @@ sys.stdout.flush() class PLCObject(pyro.ObjBase): - _Idxs = [] - def __init__(self, workingdir, daemon, argv, statuschange, evaluator, website): + def __init__(self, workingdir, daemon, argv, statuschange, evaluator, pyruntimevars): pyro.ObjBase.__init__(self) self.evaluator = evaluator self.argv = [workingdir] + argv # force argv[0] to be "path" to exec... self.workingdir = workingdir - self.PLCStatus = "Stopped" + self.PLCStatus = "Empty" self.PLClibraryHandle = None self.PLClibraryLock = Lock() self.DummyIteratorLock = None @@ -65,10 +65,15 @@ self.daemon = daemon self.statuschange = statuschange self.hmi_frame = None - self.website = website + self.pyruntimevars = pyruntimevars self._loading_error = None self.python_runtime_vars = None - + self.TraceThread = None + self.TraceLock = Lock() + self.TraceWakeup = Event() + self.Traces = [] + + def AutoLoad(self): # Get the last transfered PLC if connector must be restart try: self.CurrentPLCFilename=open( @@ -81,7 +86,8 @@ def StatusChange(self): if self.statuschange is not None: - self.statuschange(self.PLCStatus) + for callee in self.statuschange: + callee(self.PLCStatus) def LogMessage(self, *args): if len(args) == 2: @@ -107,7 +113,7 @@ tv_nsec = ctypes.c_uint32() if self._GetLogMessage is not None: maxsz = len(self._log_read_buffer)-1 - sz = self._GetLogMessage(level, msgid, + sz = self._GetLogMessage(level, msgid, self._log_read_buffer, maxsz, ctypes.byref(tick), ctypes.byref(tv_sec), @@ -131,52 +137,56 @@ Load PLC library Declare all functions, arguments and return values """ + md5 = open(self._GetMD5FileName(), "r").read() try: self._PLClibraryHandle = dlopen(self._GetLibFileName()) self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle) - + + self.PLC_ID = ctypes.c_char_p.in_dll(self.PLClibraryHandle, "PLC_ID") + if len(md5) == 32 : + self.PLC_ID.value = md5 + self._startPLC = self.PLClibraryHandle.startPLC self._startPLC.restype = ctypes.c_int self._startPLC.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)] - + self._stopPLC_real = self.PLClibraryHandle.stopPLC self._stopPLC_real.restype = None - + self._PythonIterator = getattr(self.PLClibraryHandle, "PythonIterator", None) if self._PythonIterator is not None: self._PythonIterator.restype = ctypes.c_char_p self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)] - + self._stopPLC = self._stopPLC_real else: # If python confnode is not enabled, we reuse _PythonIterator - # as a call that block pythonthread until StopPLC - self.PythonIteratorLock = Lock() - self.PythonIteratorLock.acquire() + # as a call that block pythonthread until StopPLC + self.PlcStopping = Event() def PythonIterator(res, blkid): - self.PythonIteratorLock.acquire() - self.PythonIteratorLock.release() + self.PlcStopping.clear() + self.PlcStopping.wait() return None self._PythonIterator = PythonIterator - + def __StopPLC(): self._stopPLC_real() - self.PythonIteratorLock.release() + self.PlcStopping.set() self._stopPLC = __StopPLC - - + + self._ResetDebugVariables = self.PLClibraryHandle.ResetDebugVariables self._ResetDebugVariables.restype = None - + self._RegisterDebugVariable = self.PLClibraryHandle.RegisterDebugVariable self._RegisterDebugVariable.restype = None self._RegisterDebugVariable.argtypes = [ctypes.c_int, ctypes.c_void_p] - + self._FreeDebugData = self.PLClibraryHandle.FreeDebugData self._FreeDebugData.restype = None - + self._GetDebugData = self.PLClibraryHandle.GetDebugData - self._GetDebugData.restype = ctypes.c_int + self._GetDebugData.restype = ctypes.c_int self._GetDebugData.argtypes = [ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)] self._suspendDebug = self.PLClibraryHandle.suspendDebug @@ -233,7 +243,7 @@ self._suspendDebug = lambda x:-1 self._resumeDebug = lambda:None self._PythonIterator = lambda:"" - self._GetLogCount = None + self._GetLogCount = None self._LogMessage = lambda l,m,s:PLCprint("OFF LOG :"+m) self._GetLogMessage = None self.PLClibraryHandle = None @@ -241,29 +251,25 @@ if getattr(self,"_PLClibraryHandle",None) is not None: dlclose(self._PLClibraryHandle) self._PLClibraryHandle = None - + self.PLClibraryLock.release() return False def PythonRuntimeCall(self, methodname): - """ - Calls init, start, stop or cleanup method provided by + """ + Calls init, start, stop or cleanup method provided by runtime python files, loaded when new PLC uploaded """ for method in self.python_runtime_vars.get("_runtime_%s"%methodname, []): res,exp = self.evaluator(method) - if exp is not None: + if exp is not None: self.LogMessage(0,'\n'.join(traceback.format_exception(*exp))) def PythonRuntimeInit(self): MethodNames = ["init", "start", "stop", "cleanup"] self.python_runtime_vars = globals().copy() - self.python_runtime_vars["WorkingDir"] = self.workingdir - self.python_runtime_vars["website"] = self.website - for methodname in MethodNames : - self.python_runtime_vars["_runtime_%s"%methodname] = [] - self.python_runtime_vars["PLCObject"] = self - self.python_runtime_vars["PLCBinary"] = self.PLClibraryHandle + self.python_runtime_vars.update(self.pyruntimevars) + class PLCSafeGlobals: def __getattr__(_self, name): try : @@ -280,46 +286,50 @@ raise KeyError("Try to set unknown shared global variable : %s"%name) v = self.python_runtime_vars["_"+name+"_pack"](t,value) self.python_runtime_vars["_PySafeSetPLCGlob_"+name](ctypes.byref(v)) - self.python_runtime_vars["PLCGlobals"] = PLCSafeGlobals() + + self.python_runtime_vars.update({ + "PLCGlobals" : PLCSafeGlobals(), + "WorkingDir" : self.workingdir, + "PLCObject" : self, + "PLCBinary" : self.PLClibraryHandle, + "PLCGlobalsDesc" : []}) + + for methodname in MethodNames : + self.python_runtime_vars["_runtime_%s"%methodname] = [] + try: - for filename in os.listdir(self.workingdir): + filenames = os.listdir(self.workingdir) + filenames.sort() + for filename in filenames: name, ext = os.path.splitext(filename) if name.upper().startswith("RUNTIME") and ext.upper() == ".PY": execfile(os.path.join(self.workingdir, filename), self.python_runtime_vars) - for methodname in MethodNames: + for methodname in MethodNames: method = self.python_runtime_vars.get("_%s_%s" % (name, methodname), None) if method is not None: self.python_runtime_vars["_runtime_%s"%methodname].append(method) except: self.LogMessage(0,traceback.format_exc()) raise - + self.PythonRuntimeCall("init") - if self.website is not None: - self.website.PLCStarted() def PythonRuntimeCleanup(self): if self.python_runtime_vars is not None: self.PythonRuntimeCall("cleanup") - if self.website is not None: - self.website.PLCStopped() - self.python_runtime_vars = None def PythonThreadProc(self): - self.PLCStatus = "Started" - self.StatusChange() self.StartSem.release() - self.PythonRuntimeCall("start") res,cmd,blkid = "None","None",ctypes.c_void_p() compile_cache={} while True: # print "_PythonIterator(", res, ")", cmd = self._PythonIterator(res,blkid) - FBID = blkid.value + FBID = blkid.value # print " -> ", cmd, blkid if cmd is None: break @@ -330,7 +340,7 @@ AST = compile(cmd, '', 'eval') compile_cache[FBID]=(cmd,AST) result,exp = self.evaluator(eval,AST,self.python_runtime_vars) - if exp is not None: + if exp is not None: res = "#EXCEPTION : "+str(exp[1]) self.LogMessage(1,('PyEval@0x%x(Code="%s") Exception "%s"')%(FBID,cmd, '\n'.join(traceback.format_exception(*exp)))) @@ -340,16 +350,16 @@ except Exception,e: res = "#EXCEPTION : "+str(e) self.LogMessage(1,('PyEval@0x%x(Code="%s") Exception "%s"')%(FBID,cmd,str(e))) - self.PLCStatus = "Stopped" - self.StatusChange() - self.PythonRuntimeCall("stop") - + def StartPLC(self): if self.CurrentPLCFilename is not None and self.PLCStatus == "Stopped": c_argv = ctypes.c_char_p * len(self.argv) error = None res = self._startPLC(len(self.argv),c_argv(*self.argv)) if res == 0: + self.PLCStatus = "Started" + self.StatusChange() + self.PythonRuntimeCall("start") self.StartSem=Semaphore(0) self.PythonThread = Thread(target=self.PythonThreadProc) self.PythonThread.start() @@ -359,12 +369,19 @@ self.LogMessage(0,_("Problem starting PLC : error %d" % res)) self.PLCStatus = "Broken" self.StatusChange() - + def StopPLC(self): if self.PLCStatus == "Started": self.LogMessage("PLC stopped") self._stopPLC() self.PythonThread.join() + self.PLCStatus = "Stopped" + self.StatusChange() + self.PythonRuntimeCall("stop") + if self.TraceThread is not None : + self.TraceWakeup.set() + self.TraceThread.join() + self.TraceThread = None return True return False @@ -382,7 +399,7 @@ def GetPLCstatus(self): return self.PLCStatus, map(self.GetLogCount,xrange(LogLevelsCount)) - + def NewPLC(self, md5sum, data, extrafiles): if self.PLCStatus in ["Stopped", "Empty", "Broken"]: NewFileName = md5sum + lib_ext @@ -403,15 +420,15 @@ pass except: pass - + try: # Create new PLC file open(os.path.join(self.workingdir,NewFileName), 'wb').write(data) - + # Store new PLC filename based on md5 key open(self._GetMD5FileName(), "w").write(md5sum) - + # Then write the files log = file(extra_files_log, "w") for fname,fdata in extrafiles: @@ -442,62 +459,102 @@ last_md5 = open(self._GetMD5FileName(), "r").read() return last_md5 == MD5 except: - return False - - - + pass + return False + def SetTraceVariablesList(self, idxs): """ - Call ctype imported function to append + Call ctype imported function to append these indexes to registred variables in PLC debugger """ if idxs: # 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: c_type,unpack_func, pack_func = \ TypeTranslator.get(iectype, (None,None,None)) - force = ctypes.byref(pack_func(c_type,force)) + 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.clear() + 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() - TraceVariables = None + TraceBuffer = None if self.PLClibraryLock.acquire(False): if self._GetDebugData(ctypes.byref(tick), ctypes.byref(size), ctypes.byref(buff)) == 0: if size.value: - TraceVariables = UnpackDebugBuffer(buff, size.value, self._Idxs) + TraceBuffer = ctypes.string_at(buff.value, size.value) self._FreeDebugData() self.PLClibraryLock.release() - if TraceVariables is not None: - return self.PLCStatus, tick.value, TraceVariables - return self.PLCStatus, None, [] - - def RemoteExec(self, script, **kwargs): + if TraceBuffer is not None: + self._TracesPush((tick.value, TraceBuffer)) + self._TracesAutoSuspend() + self._TracesFlush() + + + def RemoteExec(self, script, *kwargs): try: exec script in kwargs except: e_type, e_value, e_traceback = sys.exc_info() line_no = traceback.tb_lineno(get_last_traceback(e_traceback)) - return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" % + return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" % (line_no, e_value, script.splitlines()[line_no - 1])) return (0, kwargs.get("returnVal", None)) - - + + diff -r 28e9d479aa65 -r de4ee16f7c6c runtime/WampClient.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/WampClient.py Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +#from twisted.python import log +from autobahn.twisted import wamp +from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS +from twisted.internet.defer import inlineCallbacks +from autobahn.wamp import types +from autobahn.wamp.serializer import MsgPackSerializer +from twisted.internet.protocol import ReconnectingClientFactory +import json + +_WampSession = None +_PySrv = None + +ExposedCalls = ["StartPLC", + "StopPLC", + "ForceReload", + "GetPLCstatus", + "NewPLC", + "MatchMD5", + "SetTraceVariablesList", + "GetTraceVariables", + "RemoteExec", + "GetLogMessage", + "ResetLogCount", + ] + +SubscribedEvents = [] + +DoOnJoin = [] + +def GetCallee(name): + """ Get Callee or Subscriber corresponding to '.' spearated object path """ + global _PySrv + names = name.split('.') + obj = _PySrv.plcobj + while names: obj = getattr(obj, names.pop(0)) + return obj + +class WampSession(wamp.ApplicationSession): + + @inlineCallbacks + def onJoin(self, details): + global _WampSession + _WampSession = self + ID = self.config.extra["ID"] + print 'WAMP session joined by :', ID + for name in ExposedCalls: + reg = yield self.register(GetCallee(name), '.'.join((ID,name))) + + for name in SubscribedEvents: + reg = yield self.subscribe(GetCallee(name), name) + + for func in DoOnJoin: + yield func(self) + + def onLeave(self, details): + global _WampSession + _WampSession = None + print 'WAMP session left' + +class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): + def clientConnectionFailed(self, connector, reason): + print("WAMP Client connection failed .. retrying ..") + self.retry(connector) + def clientConnectionLost(self, connector, reason): + print("WAMP Client connection lost .. retrying ..") + self.retry(connector) + +def LoadWampClientConf(wampconf): + + WSClientConf = json.load(open(wampconf)) + return WSClientConf + +def RegisterWampClient(wampconf): + + WSClientConf = LoadWampClientConf(wampconf) + + ## start logging to console + # log.startLogging(sys.stdout) + + # create a WAMP application session factory + component_config = types.ComponentConfig( + realm = WSClientConf["realm"], + extra = {"ID":WSClientConf["ID"]}) + session_factory = wamp.ApplicationSessionFactory( + config = component_config) + session_factory.session = WampSession + + # create a WAMP-over-WebSocket transport client factory + transport_factory = ReconnectingWampWebSocketClientFactory( + session_factory, + url = WSClientConf["url"], + serializers = [MsgPackSerializer()], + debug = False, + debug_wamp = False) + + # start the client from a Twisted endpoint + conn = connectWS(transport_factory) + print "WAMP client connecting to :",WSClientConf["url"] + return conn + +def GetSession(): + global _WampSession + return _WampSession + +def SetServer(pysrv): + global _PySrv + _PySrv = pysrv + diff -r 28e9d479aa65 -r de4ee16f7c6c targets/Linux/plc_Linux_main.c --- a/targets/Linux/plc_Linux_main.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/Linux/plc_Linux_main.c Wed Oct 21 15:00:32 2015 +0100 @@ -119,7 +119,7 @@ timer_create (CLOCK_REALTIME, &sigev, &PLC_timer); if( __init(argc,argv) == 0 ){ - PLC_SetTimer(Ttick,Ttick); + PLC_SetTimer(common_ticktime__,common_ticktime__); /* install signal handler for manual break */ signal(SIGINT, catch_signal); @@ -232,6 +232,14 @@ pthread_mutex_lock(&python_mutex); } +void InitRetain(void) +{ +} + +void CleanupRetain(void) +{ +} + int CheckRetainBuffer(void) { return 1; diff -r 28e9d479aa65 -r de4ee16f7c6c targets/Win32/plc_Win32_main.c --- a/targets/Win32/plc_Win32_main.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/Win32/plc_Win32_main.c Wed Oct 21 15:00:32 2015 +0100 @@ -76,8 +76,6 @@ BOOL tmp; setlocale(LC_NUMERIC, "C"); - InitializeCriticalSection(&Atomic64CS); - debug_sem = CreateSemaphore( NULL, // default security attributes 1, // initial count @@ -136,7 +134,7 @@ } if( __init(argc,argv) == 0 ) { - PLC_SetTimer(Ttick,Ttick); + PLC_SetTimer(common_ticktime__,common_ticktime__); PLC_thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PlcLoop, NULL, 0, &thread_id); } else{ @@ -170,7 +168,6 @@ CloseHandle(PLC_timer); WaitForSingleObject(PLC_thread, INFINITE); __cleanup(); - DeleteCriticalSection(&Atomic64CS); CloseHandle(debug_wait_sem); CloseHandle(debug_sem); CloseHandle(python_wait_sem); @@ -244,6 +241,14 @@ WaitForSingleObject(python_sem, INFINITE); } +void InitRetain(void) +{ +} + +void CleanupRetain(void) +{ +} + int CheckRetainBuffer(void) { return 1; @@ -271,3 +276,16 @@ { } +static void __attribute__((constructor)) +beremiz_dll_init(void) +{ + InitializeCriticalSection(&Atomic64CS); + +} + +static void __attribute__((destructor)) +beremiz_dll_destroy(void) +{ + DeleteCriticalSection(&Atomic64CS); +} + diff -r 28e9d479aa65 -r de4ee16f7c6c targets/Xenomai/plc_Xenomai_main.c --- a/targets/Xenomai/plc_Xenomai_main.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/Xenomai/plc_Xenomai_main.c Wed Oct 21 15:00:32 2015 +0100 @@ -13,7 +13,6 @@ #include #include -#include #include #include @@ -75,7 +74,7 @@ void PLC_task_proc(void *arg) { - PLC_SetTimer(Ttick, Ttick); + PLC_SetTimer(common_ticktime__, common_ticktime__); while (!PLC_shutdown) { PLC_GetTime(&__CURRENT_TIME); @@ -364,23 +363,3 @@ } /* as plc does not wait for lock. */ } -int CheckRetainBuffer(void) -{ - return 1; -} - -void ValidateRetainBuffer(void) -{ -} - -void InValidateRetainBuffer(void) -{ -} - -void Retain(unsigned int offset, unsigned int count, void *p) -{ -} - -void Remind(unsigned int offset, unsigned int count, void *p) -{ -} diff -r 28e9d479aa65 -r de4ee16f7c6c targets/Xenomai/plc_Xenomai_noretain.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/targets/Xenomai/plc_Xenomai_noretain.c Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,20 @@ +int CheckRetainBuffer(void) +{ + return 1; +} + +void ValidateRetainBuffer(void) +{ +} + +void InValidateRetainBuffer(void) +{ +} + +void Retain(unsigned int offset, unsigned int count, void *p) +{ +} + +void Remind(unsigned int offset, unsigned int count, void *p) +{ +} diff -r 28e9d479aa65 -r de4ee16f7c6c targets/__init__.py --- a/targets/__init__.py Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/__init__.py Wed Oct 21 15:00:32 2015 +0100 @@ -38,7 +38,10 @@ targets = dict([(name, {"xsd":path.join(_base_path, name, "XSD"), "class":_GetLocalTargetClassFactory(name), - "code": path.join(path.split(__file__)[0],name,"plc_%s_main.c"%name)}) + "code": { fname: path.join(_base_path, name, fname) + for fname in listdir(path.join(_base_path, name)) + if fname.startswith("plc_%s_main"%name) and + fname.endswith(".c")}}) for name in listdir(_base_path) if path.isdir(path.join(_base_path, name)) and not name.startswith("__")]) @@ -67,13 +70,15 @@ return targetchoices def GetTargetCode(targetname): - return open(targets[targetname]["code"]).read() + codedesc = targets[targetname]["code"] + code = "\n".join([open(fpath).read() for fname, fpath in sorted(codedesc.items())]) + return code def GetHeader(): filename = path.join(path.split(__file__)[0],"beremiz.h") return open(filename).read() def GetCode(name): - filename = path.join(path.split(__file__)[0],name + ".c") + filename = path.join(path.split(__file__)[0],name) return open(filename).read() diff -r 28e9d479aa65 -r de4ee16f7c6c targets/plc_debug.c --- a/targets/plc_debug.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/plc_debug.c Wed Oct 21 15:00:32 2015 +0100 @@ -39,19 +39,23 @@ **/ %(extern_variables_declarations)s -typedef void(*__for_each_variable_do_fp)(void*, __IEC_types_enum); +typedef const struct { + void *ptr; + __IEC_types_enum type; +} dbgvardsc_t; + +static dbgvardsc_t dbgvardsc[] = { +%(variable_decl_array)s +}; + +typedef void(*__for_each_variable_do_fp)(dbgvardsc_t*); void __for_each_variable_do(__for_each_variable_do_fp fp) { -%(for_each_variable_do_code)s -} - -__IEC_types_enum __find_variable(unsigned int varindex, void ** varp) -{ - switch(varindex){ -%(find_variable_case_code)s - default: - *varp = NULL; - return UNKNOWN_ENUM; + int i; + for(i = 0; i < sizeof(dbgvardsc)/sizeof(dbgvardsc_t); i++){ + dbgvardsc_t *dsc = &dbgvardsc[i]; + if(dsc->type != UNKNOWN_ENUM) + (*fp)(dsc); } } @@ -70,12 +74,13 @@ forced_value_p = &((__IEC_##TYPENAME##_p *)varp)->fvalue;\ break; -void* UnpackVar(void* varp, __IEC_types_enum vartype, void **real_value_p, char *flags) -{ +void* UnpackVar(dbgvardsc_t *dsc, void **real_value_p, char *flags) +{ + void *varp = dsc->ptr; void *forced_value_p = NULL; *flags = 0; /* find data to copy*/ - switch(vartype){ + switch(dsc->type){ __ANY(__Unpack_case_t) __ANY(__Unpack_case_p) default: @@ -88,14 +93,14 @@ void Remind(unsigned int offset, unsigned int count, void * p); -void RemindIterator(void* varp, __IEC_types_enum vartype) +void RemindIterator(dbgvardsc_t *dsc) { void *real_value_p = NULL; char flags = 0; - UnpackVar(varp, vartype, &real_value_p, &flags); + UnpackVar(dsc, &real_value_p, &flags); if(flags & __IEC_RETAIN_FLAG){ - USINT size = __get_type_enum_size(vartype); + USINT size = __get_type_enum_size(dsc->type); /* compute next cursor positon*/ unsigned int next_retain_offset = retain_offset + size; /* if buffer not full */ @@ -106,6 +111,7 @@ } extern int CheckRetainBuffer(void); +extern void InitRetain(void); void __init_debug(void) { @@ -113,13 +119,19 @@ buffer_cursor = debug_buffer; retain_offset = 0; buffer_state = BUFFER_FREE; + InitRetain(); /* Iterate over all variables to fill debug buffer */ - if(CheckRetainBuffer()) + if(CheckRetainBuffer()){ __for_each_variable_do(RemindIterator); + }else{ + char mstr[] = "RETAIN memory invalid - defaults used"; + LogMessage(LOG_WARNING, mstr, sizeof(mstr)); + } retain_offset = 0; } extern void InitiateDebugTransfer(void); +extern void CleanupRetain(void); extern unsigned long __tick; @@ -127,6 +139,7 @@ { buffer_cursor = debug_buffer; InitiateDebugTransfer(); + CleanupRetain(); } void __retrieve_debug(void) @@ -136,23 +149,23 @@ void Retain(unsigned int offset, unsigned int count, void * p); -inline void BufferIterator(void* varp, __IEC_types_enum vartype, int do_debug) +inline void BufferIterator(dbgvardsc_t *dsc, int do_debug) { void *real_value_p = NULL; void *visible_value_p = NULL; char flags = 0; - visible_value_p = UnpackVar(varp, vartype, &real_value_p, &flags); + visible_value_p = UnpackVar(dsc, &real_value_p, &flags); if(flags & ( __IEC_DEBUG_FLAG | __IEC_RETAIN_FLAG)){ - USINT size = __get_type_enum_size(vartype); + USINT size = __get_type_enum_size(dsc->type); if(flags & __IEC_DEBUG_FLAG){ /* copy visible variable to buffer */; if(do_debug){ /* compute next cursor positon. No need to check overflow, as BUFFER_SIZE is computed large enough */ - if(vartype == STRING_ENUM){ + if(dsc->type == STRING_ENUM){ /* optimization for strings */ size = ((STRING*)visible_value_p)->len + 1; } @@ -178,12 +191,12 @@ } } -void DebugIterator(void* varp, __IEC_types_enum vartype){ - BufferIterator(varp, vartype, 1); -} - -void RetainIterator(void* varp, __IEC_types_enum vartype){ - BufferIterator(varp, vartype, 0); +void DebugIterator(dbgvardsc_t *dsc){ + BufferIterator(dsc, 1); +} + +void RetainIterator(dbgvardsc_t *dsc){ + BufferIterator(dsc, 0); } extern void PLC_GetTime(IEC_TIME*); @@ -251,13 +264,18 @@ break; void RegisterDebugVariable(int idx, void* force) { - void *varp = NULL; - unsigned char flags = force ? __IEC_DEBUG_FLAG | __IEC_FORCE_FLAG : __IEC_DEBUG_FLAG; - switch(__find_variable(idx, &varp)){ - __ANY(__RegisterDebugVariable_case_t) - __ANY(__RegisterDebugVariable_case_p) - default: - break; + if(idx < sizeof(dbgvardsc)/sizeof(dbgvardsc_t)){ + unsigned char flags = force ? + __IEC_DEBUG_FLAG | __IEC_FORCE_FLAG : + __IEC_DEBUG_FLAG; + dbgvardsc_t *dsc = &dbgvardsc[idx]; + void *varp = dsc->ptr; + switch(dsc->type){ + __ANY(__RegisterDebugVariable_case_t) + __ANY(__RegisterDebugVariable_case_p) + default: + break; + } } } @@ -272,10 +290,11 @@ ((__IEC_##TYPENAME##_p *)varp)->flags &= ~(__IEC_DEBUG_FLAG|__IEC_FORCE_FLAG);\ break; -void ResetDebugVariablesIterator(void* varp, __IEC_types_enum vartype) +void ResetDebugVariablesIterator(dbgvardsc_t *dsc) { /* force debug flag to 0*/ - switch(vartype){ + void *varp = dsc->ptr; + switch(dsc->type){ __ANY(__ResetDebugVariablesIterator_case_t) __ANY(__ResetDebugVariablesIterator_case_p) default: diff -r 28e9d479aa65 -r de4ee16f7c6c targets/plc_main_head.c --- a/targets/plc_main_head.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/plc_main_head.c Wed Oct 21 15:00:32 2015 +0100 @@ -25,15 +25,13 @@ IEC_TIME __CURRENT_TIME; IEC_BOOL __DEBUG = 0; unsigned long __tick = 0; +char *PLC_ID = NULL; /* * Variable generated by C softPLC and plugins **/ extern unsigned long greatest_tick_count__; -/* Effective tick time with 1ms default value */ -static long long Ttick = 1000000; - /* Help to quit cleanly when init fail at a certain level */ static int init_level = 0; @@ -72,8 +70,9 @@ int res = 0; init_level = 0; - if(common_ticktime__) - Ttick = common_ticktime__; + /* Effective tick time with 1ms default value */ + if(!common_ticktime__) + common_ticktime__ = 1000000; config_init__(); __init_debug(); diff -r 28e9d479aa65 -r de4ee16f7c6c targets/plc_main_tail.c --- a/targets/plc_main_tail.c Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/plc_main_tail.c Wed Oct 21 15:00:32 2015 +0100 @@ -175,10 +175,10 @@ /* compute mean of Tsync, over calibration period */ Tsync = ((long long)(cal_end.tv_sec - cal_begin.tv_sec) * (long long)1000000000 + (cal_end.tv_nsec - cal_begin.tv_nsec)) / calibration_count; - if( (Nticks = (Tsync / Ttick)) > 0){ - FreqCorr = (Tsync % Ttick); /* to be divided by Nticks */ + if( (Nticks = (Tsync / common_ticktime__)) > 0){ + FreqCorr = (Tsync % common_ticktime__); /* to be divided by Nticks */ }else{ - FreqCorr = Tsync - (Ttick % Tsync); + FreqCorr = Tsync - (common_ticktime__ % Tsync); } /* printf("Tsync = %ld\n", Tsync); @@ -197,19 +197,19 @@ PLC_GetTime(&now); elapsed = (now.tv_sec - __CURRENT_TIME.tv_sec) * 1000000000 + now.tv_nsec - __CURRENT_TIME.tv_nsec; if(Nticks > 0){ - PhaseCorr = elapsed - (Ttick + FreqCorr/Nticks)*sync_align_ratio/100; /* to be divided by Nticks */ - Tcorr = Ttick + (PhaseCorr + FreqCorr) / Nticks; + PhaseCorr = elapsed - (common_ticktime__ + FreqCorr/Nticks)*sync_align_ratio/100; /* to be divided by Nticks */ + Tcorr = common_ticktime__ + (PhaseCorr + FreqCorr) / Nticks; if(Nticks < 2){ /* When Sync source period is near Tick time */ /* PhaseCorr may not be applied to Periodic time given to timer */ - PeriodicTcorr = Ttick + FreqCorr / Nticks; + PeriodicTcorr = common_ticktime__ + FreqCorr / Nticks; }else{ PeriodicTcorr = Tcorr; } }else if(__tick > last_tick){ last_tick = __tick; PhaseCorr = elapsed - (Tsync*sync_align_ratio/100); - PeriodicTcorr = Tcorr = Ttick + PhaseCorr + FreqCorr; + PeriodicTcorr = Tcorr = common_ticktime__ + PhaseCorr + FreqCorr; }else{ /*PLC did not run meanwhile. Nothing to do*/ return; diff -r 28e9d479aa65 -r de4ee16f7c6c targets/typemapping.py --- a/targets/typemapping.py Sat Dec 06 19:31:51 2014 +0000 +++ b/targets/typemapping.py Wed Oct 21 15:00:32 2015 +0100 @@ -19,6 +19,11 @@ #License along with this library; if not, write to the Free Software #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import ctypes +ctypes.pythonapi.PyString_AsString.argtypes = (ctypes.c_void_p,) +ctypes.pythonapi.PyString_AsString.restype = ctypes.POINTER(ctypes.c_char) + + from ctypes import * from datetime import timedelta as td @@ -27,7 +32,7 @@ Must be changed according to changes in iec_types.h """ _fields_ = [("len", c_uint8), - ("body", c_char * 126)] + ("body", c_char * 126)] class IEC_TIME(Structure): """ @@ -37,8 +42,8 @@ ("ns", c_long)] #tv_nsec def _t(t, u=lambda x:x.value, p=lambda t,x:t(x)): return (t, u, p) -def _ttime(): return (IEC_TIME, - lambda x:td(0, x.s, x.ns/1000), +def _ttime(): return (IEC_TIME, + lambda x:td(0, x.s, x.ns/1000), lambda t,x:t(x.days * 24 * 3600 + x.seconds, x.microseconds*1000)) SameEndianessTypeTranslator = { @@ -49,8 +54,8 @@ "SINT" : _t(c_int8), "USINT" : _t(c_uint8), "BYTE" : _t(c_uint8), - "STRING" : (IEC_STRING, - lambda x:x.body[:x.len], + "STRING" : (IEC_STRING, + lambda x:x.body[:x.len], lambda t,x:t(len(x),x)), "INT" : _t(c_int16), "UINT" : _t(c_uint16), @@ -67,38 +72,35 @@ "TOD" : _ttime(), "DATE" : _ttime(), "DT" : _ttime(), - } + } SwapedEndianessTypeTranslator = { #TODO - } + } TypeTranslator=SameEndianessTypeTranslator # Construct debugger natively supported types DebugTypesSize = dict([(key,sizeof(t)) for key,(t,p,u) in SameEndianessTypeTranslator.iteritems() if t is not None]) -def UnpackDebugBuffer(buff, size, indexes): - res = [] - offset = 0 - for idx, iectype, forced in indexes: - cursor = c_void_p(buff.value + offset) +def UnpackDebugBuffer(buff, indexes): + res = [] + buffoffset = 0 + buffsize = len(buff) + buffptr = cast(ctypes.pythonapi.PyString_AsString(id(buff)),c_void_p).value + for iectype in indexes: c_type,unpack_func, pack_func = \ TypeTranslator.get(iectype, (None,None,None)) - if c_type is not None and offset < size: - res.append(unpack_func( - cast(cursor, - POINTER(c_type)).contents)) - offset += sizeof(c_type) if iectype != "STRING" else len(res[-1])+1 + if c_type is not None and buffoffset < buffsize: + cursor = c_void_p( buffptr + buffoffset) + value = unpack_func( cast(cursor, + POINTER(c_type)).contents) + buffoffset += sizeof(c_type) if iectype != "STRING" else len(value)+1 + res.append(value) else: - #if c_type is None: - # PLCprint("Debug error - " + iectype + - # " not supported !") - #if offset >= size: - # PLCprint("Debug error - buffer too small ! %d != %d"%(offset, size)) break - if offset and offset == size: + if buffoffset and buffoffset == buffsize: return res return None diff -r 28e9d479aa65 -r de4ee16f7c6c tests/python/c_code@c_ext/cfile.xml --- a/tests/python/c_code@c_ext/cfile.xml Sat Dec 06 19:31:51 2014 +0000 +++ b/tests/python/c_code@c_ext/cfile.xml Wed Oct 21 15:00:32 2015 +0100 @@ -14,6 +14,7 @@ volatile char PtoC=1,CtoP=2; extern long AtomicCompareExchange(long*,long, long); +extern char *PLC_ID; int Simple_C_Call(int val){ return val+1; @@ -34,6 +35,7 @@ res=1; } printf("C code called by Python: toC %d fromC %d\n",toC,*fromC); + printf("PLC_ID id %s\n",PLC_ID); return res; } diff -r 28e9d479aa65 -r de4ee16f7c6c tests/python/plc.xml --- a/tests/python/plc.xml Sat Dec 06 19:31:51 2014 +0000 +++ b/tests/python/plc.xml Wed Oct 21 15:00:32 2015 +0100 @@ -1,7 +1,7 @@ - + @@ -1297,7 +1297,7 @@ - + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/python/py_ext_0@py_ext/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/python/py_ext_0@py_ext/baseconfnode.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,2 @@ + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/python/py_ext_0@py_ext/pyfile.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/python/py_ext_0@py_ext/pyfile.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/svgui/plc.xml --- a/tests/svgui/plc.xml Sat Dec 06 19:31:51 2014 +0000 +++ b/tests/svgui/plc.xml Wed Oct 21 15:00:32 2015 +0100 @@ -497,7 +497,7 @@ - + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/.crossbar/config.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/.crossbar/config.json Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,43 @@ + +{ + "controller": { + }, + "workers": [ + { + "type": "router", + "options": { + "pythonpath": [".."] + }, + "realms": [ + { + "name": "Automation", + "roles": [ + { + "name": "anonymous", + "permissions": [ + { + "uri": "*", + "publish": true, + "subscribe": true, + "call": true, + "register": true + } + ] + } + ] + } + ], + "transports": [ + { + "type": "websocket", + "endpoint": { + "type": "tcp", + "port": 8888 + }, + "url": "ws://127.0.0.1:8888/", + "serializers" : ["msgpack"] + } + ] + } + ] +} diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/README Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,12 @@ +This project contains wamp client config to be loaded at runtime startup. + +project_files/wampconf.json + +wampconf.json is in "Project Files", so it is copied to runtime's working directory, and then loaded after program transfer + runtime restart. + +Otherwise, wamp config file path can be forced : +./Beremiz_service.py -c /path/to/my/wampconf.json /working/dir + +Crossbar test router configuration is available in .crossbar directory. Start with : +crossbar -d start + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/beremiz.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,4 @@ + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/plc.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PyVar0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LocalVar0 + + + + + + + + + + + PyVar1 + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/project_files/wampconf.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/project_files/wampconf.json Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,7 @@ +{ + "url":"ws://127.0.0.1:8888", + "realm":"Automation", + "ID":"wamptest", + "password":"1234567890", + "key":"ABCDEFGHIJ" +} diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/py_ext_0@py_ext/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/py_ext_0@py_ext/baseconfnode.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,2 @@ + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wamp/py_ext_0@py_ext/pyfile.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wamp/py_ext_0@py_ext/pyfile.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxGlade/HMIFrame@wxglade_hmi/py_ext.xml --- a/tests/wxGlade/HMIFrame@wxglade_hmi/py_ext.xml Sat Dec 06 19:31:51 2014 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ - - - - diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxGlade/HMIFrame@wxglade_hmi/pyfile.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxGlade/HMIFrame@wxglade_hmi/pyfile.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxGlade/plc.xml --- a/tests/wxGlade/plc.xml Sat Dec 06 19:31:51 2014 +0000 +++ b/tests/wxGlade/plc.xml Wed Oct 21 15:00:32 2015 +0100 @@ -419,7 +419,7 @@ - + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/HMI@wxglade_hmi/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/HMI@wxglade_hmi/baseconfnode.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,2 @@ + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/HMI@wxglade_hmi/hmi.wxg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/HMI@wxglade_hmi/hmi.wxg Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,99 @@ + + + + + + + frame_1 + + 0 + 0 + 1 + 0 + 4 + 0 + + wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL + 0 + + + # WHERE IS THAT ?\nprint "hello"\n + + $parent + $id + + 400,400 + + + + wxEXPAND + 0 + + + 0 + 0 + 2 + 1 + 0 + + wxEXPAND + 0 + + + wxVERTICAL + + + + + + + + wxEXPAND + 0 + + + 0 + 4 + 2 + 0 + + + + + + + + + + + + + + 0 + + + + + SetPLCGlobalVar + + + "DrawTest" + + + + + 0 + + + + + SetPLCGlobalVar + + + "DrawEscher" + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/HMI@wxglade_hmi/hmi.wxg.bak --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/HMI@wxglade_hmi/hmi.wxg.bak Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,71 @@ + + + + + + + frame_1 + + 0 + 0 + 1 + 0 + 2 + 0 + + wxEXPAND + 0 + + + + $parent + $id + + 400,400 + + + + wxEXPAND + 0 + + + 0 + 2 + 1 + 0 + + wxEXPAND + 0 + + + wxVERTICAL + + + + + + + + wxEXPAND + 0 + + + 0 + 4 + 2 + 0 + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/HMI@wxglade_hmi/pyfile.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/HMI@wxglade_hmi/pyfile.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/beremiz.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,5 @@ + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/wxHMI/plc.xml Wed Oct 21 15:00:32 2015 +0100 @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Power_ON + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Power_OFF + + + + + + + + + + + + + power + + + + + + + DrawTest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ZaxisPos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XaxisPos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tmp + + + + + + + 1 + + + + + + + tmp + + + + + + + + + + + + + YaxisPos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + power + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BOOL#TRUE + + + + + + + 'wxglade_hmi.UpdPos()' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LocalVar0 + + + + + + + + + + + + + LocalVar1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/Detect_Circle.png Binary file tests/wxHMI/project_files/Detect_Circle.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/DrawEscher.png Binary file tests/wxHMI/project_files/DrawEscher.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/DrawLogo.png Binary file tests/wxHMI/project_files/DrawLogo.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/DrawTest.png Binary file tests/wxHMI/project_files/DrawTest.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/Power_OFF.png Binary file tests/wxHMI/project_files/Power_OFF.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/Power_ON.png Binary file tests/wxHMI/project_files/Power_ON.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/TaxisMinus.png Binary file tests/wxHMI/project_files/TaxisMinus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/TaxisPlus.png Binary file tests/wxHMI/project_files/TaxisPlus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/XaxisMinus.png Binary file tests/wxHMI/project_files/XaxisMinus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/XaxisPlus.png Binary file tests/wxHMI/project_files/XaxisPlus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/YaxisMinus.png Binary file tests/wxHMI/project_files/YaxisMinus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/YaxisPlus.png Binary file tests/wxHMI/project_files/YaxisPlus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/ZaxisMinus.png Binary file tests/wxHMI/project_files/ZaxisMinus.png has changed diff -r 28e9d479aa65 -r de4ee16f7c6c tests/wxHMI/project_files/ZaxisPlus.png Binary file tests/wxHMI/project_files/ZaxisPlus.png has changed