--- 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 @@
#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
- 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 @@
+wampconf = "wampconf.json" @@ -58,6 +63,8 @@
@@ -79,10 +86,18 @@
+ webport = None if a == "off" else int(a) + wampconf = None if a == "off" else a +beremiz_dir = os.path.dirname(os.path.realpath(__file__)) @@ -99,26 +114,28 @@
- from threading import Thread, currentThread
+ wxversion.select('2.8') + from threading import Thread, currentThread app=wx.App(redirect=False)
# Import module for internationalization
- CWD = os.path.split(os.path.realpath(__file__))[0]
- 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)
- 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)
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())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.GetSizer().GetItem(3).GetSizer().GetChildren()[0].GetSizer().GetChildren()[0].GetWindow().GetId())
texts = {"value" : value}
@@ -176,13 +193,13 @@
return self.GetSizer().GetItem(1).GetWindow().GetValue()
def SetTests(self, tests):
class BeremizTaskBarIcon(wx.TaskBarIcon):
TBMENU_START = wx.NewId()
@@ -193,14 +210,14 @@
TBMENU_WXINSPECTOR = wx.NewId()
TBMENU_CHANGE_WD = wx.NewId()
def __init__(self, pyroserver, level):
wx.TaskBarIcon.__init__(self)
self.pyroserver = pyroserver
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.Append(self.TBMENU_QUIT, _("Quit"))
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() )
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()
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())
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()
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}
def OnTaskBarLiveShell(self, evt):
frame = py.crust.CrustFrame(**self._LiveShellLocals())
def OnTaskBarWXInspector(self, evt):
# Activate the widget inspection tool
from wx.lib.inspection import InspectionTool
@@ -304,13 +321,13 @@
InspectionTool().Show(wnd, True)
def OnTaskBarQuit(self, evt):
if wx.Platform == '__WXMSW__':
Thread(target=self.pyroserver.Quit).start()
wx.CallAfter(wx.GetApp().ExitMainLoop)
def UpdateIcon(self, plcstatus):
if plcstatus is "Started" :
currenticon = self.MakeIcon(starticon)
@@ -334,7 +351,10 @@
- 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, self.servicename = servicename
@@ -347,12 +367,12 @@
self.autostart = autostart
self.statuschange = statuschange
self.evaluator = evaluator
+ self.pyruntimevars = pyruntimevars
@@ -365,30 +385,34 @@
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, 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":
+ if self.plcobj.GetPLCstatus()[0] != "Empty":
self.daemon.requestLoop()
if self.plcobj is not None:
@@ -406,205 +430,57 @@
from twisted.internet import wxreactor
- 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 - print "Twisted unavailable !"
+ print "Twisted unavailable."
- xhtml_header = '''<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
- class PLCHMI(athena.LiveElement):
- 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.resetPLCStartedHMI()
- def setPLCState(self, state):
- if self.HMI is not None:
- self.callRemote('updateHMI')
- def setPLCStartedHMI(self, hmi):
- self.PLCStartedHMIClass = hmi
- def resetPLCStartedHMI(self):
- self.PLCStartedHMIClass = DefaultPLCStartedHMI
- def HMIexec(self, function, *args, **kwargs):
- if self.HMI is not None:
- getattr(self.HMI, function, lambda:None)(*args, **kwargs)
- def PLCElement(self, ctx, data):
- return self.getPLCElement()
- def getPLCElement(self):
- self.detachFragmentChildren()
- f = self.PLCStartedHMIClass()
- f.setFragmentParent(self)
- athena.expose(getPLCElement)
- def detachFragmentChildren(self):
- for child in self.liveFragmentChildren[:]:
- 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.div( render = tags.directive( "MainPage" ))
- 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)
- 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)
- self.MainPage.resetPLCStartedHMI()
- self.MainPage.setPLCState(True)
- 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.setFragmentParent(self)
- 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 "We will be called back when the client disconnects"
reactor.registerWxApp(app)
- website = WebInterface()
- site = appserver.NevowSite(website)
- reactor.listenTCP(website_port, site)
- print "Http interface port :",website_port
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)
def evaluator(tocall, *args, **kwargs):
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)
o=type('',(object,),dict(call=(tocall, args, kwargs), res=None))
wx.CallAfter(wx_evaluator,o)
- 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)
- pyroserver = Server(servicename, given_ip, port, WorkingDir, argv, autostart, website=website)
+ pyroserver = Server(servicename, given_ip, port, + WorkingDir, argv, autostart, + statuschange, pyruntimevars=pyruntimevars) import threading, traceback
@@ -631,6 +507,46 @@
threading.Thread.__init__ = init
installThreadExcepthook()
+ if webport is not None : + import runtime.NevowServer as NS + print "Nevow/Athena import failed :", e + NS.WorkingDir = WorkingDir + if wampconf is not None : + import runtime.WampClient as WC + print "WAMP import failed :", e +for extfilename in extensions: + extension_folder = os.path.split(os.path.realpath(extfilename))[0] + sys.path.append(extension_folder) + execfile(extfilename, locals()) + if webport is not None : + website = NS.RegisterWebsite(webport) + pyruntimevars["website"] = website + statuschange.append(NS.website_statuslistener_factory(website)) + print "Nevow Web service failed.", e + if wampconf is not None : + WC.RegisterWampClient(wampconf) + pyruntimevars["wampsession"] = WC.GetSession + WC.SetServer(pyroserver) + print "WAMP client startup failed.", e if havetwisted or havewx:
pyro_thread=Thread(target=pyroserver.Loop)
--- 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 @@
#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 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 print "\nUsage of PLCOpenEditor.py :"
@@ -43,13 +43,13 @@
# print help information and exit:
# Extract if help has been requested
if o in ("-h", "--help"):
# Extract the optional filename to open
@@ -57,13 +57,13 @@
# Create wxApp (Need to create App before internationalization because of
from util.misc import InstallLocalRessources
- InstallLocalRessources(CWD)
+ InstallLocalRessources(beremiz_dir) 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 @@
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)
# Open the filepath if defined
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)
_("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")):
@@ -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, def OnOpenProjectMenu(self, event):
@@ -279,9 +279,9 @@
directory = os.path.dirname(filepath)
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)
_("PLC syntax error at line %d:\n%s") % result)
def OnCloseProjectMenu(self, event):
if not self.CheckSaveBeforeClosing():
@@ -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)) result = self.Controler.SaveXMLFile()
@@ -351,7 +351,7 @@
self._Refresh(TITLE, FILEMENU, PAGETITLES)
filepath = self.Controler.GetFilePath()
@@ -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)
cap = wx.Window_GetCapture()
- dlg = wx.SingleChoiceDialog(None,
+ dlg = wx.SingleChoiceDialog(None, @@ -403,7 +403,7 @@
- str(e_type) + _(" : ") + str(e_value),
+ str(e_type) + _(" : ") + str(e_value), @@ -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')
@@ -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)
--- 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 @@ -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_REREGISTER = 4
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(
- "\"%s\" -f -l -p -I \"%s\" -T \"%s\" \"%s\""%(
+ buildcmd = "\"%s\" -f -l -p -I \"%s\" -T \"%s\" \"%s\""%( - self._getIECcodepath()),
- no_stdout=True, no_stderr=True).spin()
+ self._getIECcodepath()) + # 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() + self.logger.write_error(buildcmd + "\n") + self.logger.write_error(repr(e) + "\n") @@ -732,9 +735,11 @@
self._VariablesList = None
+ self._DbgVariablesList = None + self.TracedIECTypes = [] def GetIECProgramsAndVariables(self):
@@ -750,6 +755,7 @@
VariablesListAttributeName = ["num", "vartype", "IEC_path", "C_path", "type"]
+ self._DbgVariablesList = [] @@ -774,6 +780,7 @@
# second section contains all variables
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 self._VariablesList.append(attrs)
- # Fill in IEC<->C translation dicts
- IEC_path=attrs["IEC_path"]
- self._IECPathToIdx[IEC_path]=(Idx, attrs["type"])
# third section contains ticktime
@@ -816,8 +828,21 @@
self.GetIECProgramsAndVariables()
- 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 = [] + for v in self._DbgVariablesList : + sz = DebugTypesSize.get(v["type"], 0) + variable_decl_array += [ + {"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 + + debug_code = targets.GetCode("plc_debug.c") % { "\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([
- " *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) @@ -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)])
- plc_main_code = targets.GetCode("plc_main_head") % {
+ plc_main_code = targets.GetCode("plc_main_head.c") % { @@ -887,7 +898,7 @@
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") @@ -975,7 +986,7 @@
- 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, []
@@ -1214,6 +1225,7 @@
+ self.TracedIECTypes = [] if self._connector is not None:
self.IECdebug_lock.acquire()
@@ -1238,8 +1250,10 @@
- self.TracedIECPath = zip(*Idxs)[3]
- self._connector.SetTraceVariablesList(zip(*zip(*Idxs)[0:3]))
+ self.TracedIECPath = IdxsT[3] + self.TracedIECTypes = IdxsT[1] + self._connector.SetTraceVariablesList(zip(*IdxsT[0:3])) self._connector.SetTraceVariablesList([])
@@ -1267,11 +1281,11 @@
Idx, IEC_Type = self._IECPathToIdx.get(IECPath,(None,None))
- 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):
@@ -1290,7 +1304,7 @@
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 @@
IECdebug_data[4] = reduce(
- [buffer_list for buffer_list,args,kwargs
- in IECdebug_data[0].itervalues()],
+ IECdebug_data[0].itervalues(), 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)
- function(*(cargs + args), **kwargs)
- function(*(tuple([lst[-1] for lst in cargs]) + args), **kwargs)
+ function(*tuple([lst[-1] for lst in cargs])) @@ -1380,38 +1393,34 @@
while (not self.debug_break) and (self._connector is not None):
- Trace = self._connector.GetTraceVariables()
- plc_status, debug_tick, debug_vars = Trace
+ plc_status, Traces = self._connector.GetTraceVariables() #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"))
- 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)
- 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" : + 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.DebugValuesBuffers, + 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) + values_buffer.append((value, forced)) + self.DebugTicks.append(debug_tick) + 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 @@ -1426,7 +1435,7 @@
self.IECdebug_lock.release()
if len(self.TracedIECPath) == len(buffers):
- for IECPath, values in zip(self.TracedIECPath, buffers):
+ for IECPath, values in izip(self.TracedIECPath, buffers): self.CallWeakcallables(IECPath, "NewValues", debug_ticks, values)
--- 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 @@
#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
width, height = self.GetClientSize()
return wx.Rect(0, width, width, height - 2 * width)
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)
def OnLeftDown(self, event):
posx, posy = event.GetPosition()
@@ -103,14 +104,14 @@
elif posy > height - width:
self.Parent.ScrollMessagePanelByPage(-1)
def OnLeftUp(self, event):
self.ThumbScrollingStartPos = None
self.RefreshThumbPosition(0.)
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.)))
def OnResize(self, event):
def OnEraseBackground(self, event):
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self)
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.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)
def __init__(self, label, callback):
self.Position = wx.Point(0, 0)
self.Size = wx.Size(*BUTTON_SIZE)
def SetPosition(self, x, y):
self.Position = wx.Point(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)
def ProcessCallback(self):
if self.Callback is not None:
wx.CallAfter(self.Callback)
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)
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
def __cmp__(self, other):
if self.Date == other.Date:
return cmp(self.Seconds, other.Seconds)
return cmp(self.Date, other.Date)
date = self.Date.replace(second=int(self.Seconds))
nsec = (self.Seconds % 1.) * 1e9
@@ -245,25 +246,25 @@
def Draw(self, dc, offset, width, 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)
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):
return DATE_INFO_SIZE + MESSAGE_INFO_SIZE
@@ -280,18 +281,18 @@
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"))
@@ -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)
- 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))
- 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.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.LastStartTime = None
self.ScrollTimer = wx.Timer(self, -1)
self.Bind(wx.EVT_TIMER, self.OnScrollTimer, self.ScrollTimer)
self.MessageToolTip = None
self.MessageToolTipTimer = wx.Timer(self, -1)
self.Bind(wx.EVT_TIMER, self.OnMessageToolTipTimer, self.MessageToolTipTimer)
def ResetLogMessages(self):
self.previous_log_count = [None]*LogLevelsCount
@@ -383,14 +384,14 @@
self.LogMessagesTimestamp = numpy.array([])
self.CurrentMessage = None
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:
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)
def SetLogCounters(self, log_count):
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)
def GetNextMessage(self, msgidx):
while msgidx < len(self.LogMessages) - 1:
message = self.LogMessages[msgidx + 1]
@@ -463,7 +464,7 @@
return message, msgidx + 1
def GetPreviousMessage(self, msgidx, timestamp=None):
while 0 < msgidx < len(self.LogMessages):
@@ -490,7 +491,7 @@
self.OldestMessages[level] = (-1, None)
- while (message_idx < len(self.LogMessages) and
+ while (message_idx < len(self.LogMessages) and self.LogMessages[message_idx] < message):
if len(self.LogMessages) > 0:
@@ -499,8 +500,8 @@
current_message = message
self.LogMessages.insert(message_idx, message)
self.LogMessagesTimestamp = numpy.insert(
- self.LogMessagesTimestamp,
+ self.LogMessagesTimestamp, 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):
def RefreshNewData(self, *args, **kwargs):
DebugViewer.RefreshNewData(self, *args, **kwargs)
width, height = self.MessagePanel.GetClientSize()
bitmap = wx.EmptyBitmap(width, height)
dc = wx.BufferedDC(wx.ClientDC(self.MessagePanel), bitmap)
if self.CurrentMessage is not None:
for button in self.LeftButtons + self.RightButtons:
message_idx = self.CurrentMessage
message = self.LogMessages[message_idx]
@@ -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
self.MessageScrollBar.RefreshThumbPosition()
def IsMessagePanelTop(self, message_idx=None):
message_idx = self.CurrentMessage
if message_idx is not None:
return self.GetNextMessage(message_idx)[0] is None
def IsMessagePanelBottom(self, message_idx=None):
message_idx = self.CurrentMessage
@@ -570,7 +571,7 @@
message = previous_message
def ScrollMessagePanel(self, scroll):
if self.CurrentMessage is not None:
message = self.LogMessages[self.CurrentMessage]
@@ -585,13 +586,13 @@
self.CurrentMessage = msgidx
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 @@
self.CurrentMessage = msgidx
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)
def OnMessageFilterChanged(self, event):
self.CurrentFilter = self.LevelFilters[self.MessageFilter.GetSelection()]
def OnSearchMessageChanged(self, event):
self.CurrentSearchValue = self.SearchMessage.GetValue()
def OnSearchMessageSearchButtonClick(self, event):
self.CurrentSearchValue = self.SearchMessage.GetValue()
def OnSearchMessageCancelButtonClick(self, event):
self.CurrentSearchValue = ""
self.SearchMessage.SetValue("")
def OnCleanButton(self, event):
if self.LogSource is not None:
self.LogSource.ResetLogCount()
def GenerateOnDurationButton(self, duration):
self.ScrollMessagePanelByTimestamp(duration)
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]
while offset < height and message is not None:
if offset <= posy < offset + MESSAGE_INFO_SIZE:
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
def OnMessagePanelLeftUp(self, event):
if self.CurrentMessage is not None:
posx, posy = event.GetPosition()
@@ -681,32 +682,32 @@
def OnMessagePanelRightUp(self, event):
message = self.GetMessageByScreenPos(*event.GetPosition())
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)
def OnMessagePanelLeftDCLick(self, event):
message = self.GetMessageByScreenPos(*event.GetPosition())
self.SearchMessage.SetFocus()
self.SearchMessage.SetValue(message.Message)
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()
def OnMessagePanelMotion(self, event):
self.ResetMessageToolTip()
self.LastMousePos = event.GetPosition()
self.MessageToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True)
def OnMessagePanelLeaveWindow(self, event):
self.ResetMessageToolTip()
self.MessageToolTipTimer.Stop()
def OnMessagePanelMouseWheel(self, event):
self.ScrollMessagePanel(event.GetWheelRotation() / event.GetWheelDelta())
def OnMessagePanelEraseBackground(self, event):
def OnMessagePanelPaint(self, event):
def OnMessagePanelResize(self, event):
width, height = self.MessagePanel.GetClientSize()
@@ -761,7 +762,7 @@
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)
def SetScrollSpeed(self, speed):
@@ -788,8 +789,8 @@
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
--- 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 @@
-CWD = os.path.split(os.path.realpath(__file__))[0]
- return os.path.join(CWD,*args)
class GenBitmapTextButton(wx.lib.buttons.GenBitmapTextButton):
@@ -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,
bitmap = GetBitmap(bitmapname)
bitmap = wx.EmptyBitmap(0, 0)
- wx.StaticBitmap.__init__(self, parent, ID,
+ wx.StaticBitmap.__init__(self, parent, ID, class ConfTreeNodeEditor(EditorPanel):
def _init_Editor(self, parent):
tabs_num = len(self.CONFNODEEDITOR_TABS)
if self.SHOW_PARAMS and len(self.Controler.GetParamsAttributes()) > 0:
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,
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), 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), updownsizer.AddWindow(self.IECCDownButton, flag=wx.ALIGN_LEFT)
- self.ConfNodeName = wx.TextCtrl(self.Editor,
+ self.ConfNodeName = wx.TextCtrl(self.Editor, 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), - 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)
self.ConfNodeNoteBook = wx.Notebook(self.Editor)
parent = self.ConfNodeNoteBook
@@ -171,12 +166,12 @@
self.ConfNodeNoteBook = None
self.Editor.SetSizer(self.MainSizer)
self.ConfNodeNoteBook = 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)
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, 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
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))
self.SetIcon(GetBitmap("Extension"))
self.Controler.OnCloseEditor(self)
return self.Controler.CTNFullName()
fullname = self.Controler.CTNFullName()
if self.Controler.CTNTestModified():
def GetBufferState(self):
EditorPanel.RefreshView(self)
if self.SHOW_BASE_PARAMS:
@@ -257,33 +252,33 @@
if self.ParamsEditor is not None:
self.RefreshConfNodeParamsSizer()
def RefreshIECChannelControlsState(self):
self.FullIECChannel.SetLabel(self.Controler.GetFullIEC_Channel())
self.IECCDownButton.Enable(self.Controler.BaseParams.getIEC_Channel() > 0)
def RefreshConfNodeParamsSizer(self):
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()
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 @@
msizer.AddWindow(button, flag=wx.ALIGN_CENTER)
def GenerateSizerElements(self, sizer, elements, path, clean = True):
@@ -321,44 +316,44 @@
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)
- sizer.AddSizer(staticboxsizer, border=5,
+ sizer.AddSizer(staticboxsizer, border=5, flag=wx.GROW|wx.TOP|wx.BOTTOM)
- sizer.AddSizer(staticboxsizer, border=5,
+ sizer.AddSizer(staticboxsizer, border=5, - self.GenerateSizerElements(staticboxsizer,
- element_infos["children"],
+ self.GenerateSizerElements(staticboxsizer, + element_infos["children"], boxsizer = wx.FlexGridSizer(cols=3, rows=1)
boxsizer.AddGrowableCol(1)
- sizer.AddSizer(boxsizer, border=5,
+ sizer.AddSizer(boxsizer, border=5, - 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 @@
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), - 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":
if len(element_infos["type"]) > 0 and isinstance(element_infos["type"][0], types.TupleType):
@@ -386,8 +381,8 @@
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 @@
combobox.SetStringSelection(element_infos["value"])
combobox.Bind(wx.EVT_COMBOBOX, callback, combobox)
elif isinstance(element_infos["type"], types.DictType):
@@ -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),
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),
elif element_infos["type"] in ["unsignedLong", "long","integer"]:
if element_infos["type"].startswith("unsigned"):
- 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),
choices = self.ParentWindow.GetConfigEntry(element_path, [""])
- textctrl = TextCtrlAutoComplete(name=element_infos["name"],
- parent=self.ParamsEditor,
+ textctrl = TextCtrlAutoComplete(name=element_infos["name"], + parent=self.ParamsEditor, element_path=element_path,
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)
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)
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)
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()
getattr(self.Controler,method)()
event.GetEventObject().Enable()
def GetChoiceCallBackFunction(self, choicectrl, path):
def OnChoiceChanged(event):
res = self.SetConfNodeParamsAttribute(path, choicectrl.GetStringSelection())
@@ -501,14 +496,14 @@
choicectrl.SetStringSelection(res)
def GetChoiceContentCallBackFunction(self, choicectrl, staticboxsizer, path):
def OnChoiceContentChanged(event):
res = self.SetConfNodeParamsAttribute(path, choicectrl.GetStringSelection())
wx.CallAfter(self.RefreshConfNodeParamsSizer)
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())
def GetCheckBoxCallBackFunction(self, chkbx, path):
def OnCheckBoxChanged(event):
res = self.SetConfNodeParamsAttribute(path, chkbx.IsChecked())
def GetBrowseCallBackFunction(self, name, textctrl, library, value_infos, path):
def OnBrowseButton(event):
@@ -541,7 +536,7 @@
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):
def OnParamsEditorScroll(self, event):
control = self.ParamsEditor.FindFocus()
if isinstance(control, TextCtrlAutoComplete):
--- 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):
@@ -16,45 +16,45 @@
EditorType = PythonEditor
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()
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"')
(re.compile("(?<!<xhtml:p>)(?:<!\[CDATA\[)"), "<xhtml:p><![CDATA["),
(re.compile("(?:]]>)(?!</xhtml:p>)"), "]]></xhtml:p>")]:
pythonfile_xml = cre.sub(repl, pythonfile_xml)
python_code, error = PythonParser.LoadXMLString(pythonfile_xml)
self.CodeFile.globals.setanyText(python_code.getanyText())
self.CreateCodeFileBuffer(False)
self.GetCTRoot().logger.write_error(
- _("Couldn't import old %s file.") % CTNName)
+ _("Couldn't import old %s file.") % self.CTNName()) 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(( + for varinfo in varinfos]) # Runtime calls (start, stop, init, and cleanup)
@@ -101,36 +117,41 @@
- globalsection = self.GetSection("globals")
+ globalsection = self.GetSection("globals") ## Code generated by Beremiz python mixin confnode
## Code for PLC global variable access
from targets.typemapping import TypeTranslator
+_%(pyextname)sGlobalsDesc = [] +__ext_name__ = "%(pyextname)s" +PLCGlobalsDesc.append(( "%(pyextname)s" , _%(pyextname)sGlobalsDesc ))
## User code in "global" scope
## Beremiz python runtime calls
# 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'))
# C code for safe global variables access
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; if(!AtomicCompareExchange(&__%(name)s_wlock, 0, 1)){
if(__%(name)s_wbuffer_written == 1){
@@ -159,34 +185,51 @@
AtomicCompareExchange((long*)&__%(name)s_wlock, 1, 0);
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); + 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 + for varinfo in varinfos]) + varinit = "\n".join([varinitonchangefmt % dict( + onchangelen = len(varinfo["onchangecode"]),**varinfo) + for varinfo in varinfos if varinfo["onchange"]]) - 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)]))
- vardec, varret, varpub = var_str
- vardec = varret = varpub = ""
+ # 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.
- * Code generated by Beremiz py_ext confnode
+ * Code generated by Beremiz py_ext confnode * for safe global variables access
#include "iec_types_all.h"
/* User variables reference */
@@ -194,6 +237,7 @@
/* Beremiz confnode functions */
int __init_%(location_str)s(int argc,char **argv){
@@ -208,15 +252,15 @@
Gen_PyCfile_path = os.path.join(buildpath, "PyCFile_%s.c"%location_str)
pycfile = open(Gen_PyCfile_path,'w')
pycfile.write(PyCFileContent)
matiec_flags = '"-l -p -I%s"'%os.path.abspath(
self.GetCTRoot().GetIECLibPath())
return ([(Gen_PyCfile_path, matiec_flags)],
--- 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 @@
#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
-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
if os.name in ("nt", "ce"):
@@ -50,13 +51,12 @@
class PLCObject(pyro.ObjBase):
- 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.statuschange = statuschange
+ self.pyruntimevars = pyruntimevars self._loading_error = None
self.python_runtime_vars = None
+ self.TraceThread = None + self.TraceLock = Lock() + self.TraceWakeup = Event() # Get the last transfered PLC if connector must be restart
self.CurrentPLCFilename=open(
@@ -81,7 +86,8 @@
if self.statuschange is not None:
- self.statuschange(self.PLCStatus)
+ for callee in self.statuschange: def LogMessage(self, *args):
@@ -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,
@@ -131,52 +137,56 @@
Declare all functions, arguments and return values
+ md5 = open(self._GetMD5FileName(), "r").read() 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") + 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
# 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() self._PythonIterator = PythonIterator
- self.PythonIteratorLock.release()
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()
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)
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) def __getattr__(_self, name):
@@ -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, + "PLCBinary" : self.PLClibraryHandle, + "PLCGlobalsDesc" : []}) + for methodname in MethodNames : + self.python_runtime_vars["_runtime_%s"%methodname] = [] - for filename in os.listdir(self.workingdir):
+ filenames = os.listdir(self.workingdir) + 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)
self.python_runtime_vars["_runtime_%s"%methodname].append(method)
self.LogMessage(0,traceback.format_exc())
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.PythonRuntimeCall("start")
res,cmd,blkid = "None","None",ctypes.c_void_p()
# print "_PythonIterator(", res, ")",
cmd = self._PythonIterator(res,blkid)
# print " -> ", cmd, blkid
@@ -330,7 +340,7 @@
AST = compile(cmd, '<plc>', 'eval')
compile_cache[FBID]=(cmd,AST)
result,exp = self.evaluator(eval,AST,self.python_runtime_vars)
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 @@
res = "#EXCEPTION : "+str(e)
self.LogMessage(1,('PyEval@0x%x(Code="%s") Exception "%s"')%(FBID,cmd,str(e)))
- self.PLCStatus = "Stopped"
- self.PythonRuntimeCall("stop")
if self.CurrentPLCFilename is not None and self.PLCStatus == "Stopped":
c_argv = ctypes.c_char_p * len(self.argv)
res = self._startPLC(len(self.argv),c_argv(*self.argv))
+ self.PLCStatus = "Started" + 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"
if self.PLCStatus == "Started":
self.LogMessage("PLC stopped")
+ self.PLCStatus = "Stopped" + self.PythonRuntimeCall("stop") + if self.TraceThread is not None : + self.TraceThread.join() + self.TraceThread = None @@ -382,7 +399,7 @@
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 @@
open(os.path.join(self.workingdir,NewFileName),
# Store new PLC filename based on md5 key
open(self._GetMD5FileName(), "w").write(md5sum)
log = file(extra_files_log, "w")
for fname,fdata in extrafiles:
@@ -442,62 +459,102 @@
last_md5 = open(self._GetMD5FileName(), "r").read()
def SetTraceVariablesList(self, idxs):
- Call ctype imported function to append
+ Call ctype imported function to append these indexes to registred variables in PLC debugger
# suspend but dont disable
if self._suspendDebug(False) == 0:
# keep a copy of requested idx
self._ResetDebugVariables()
for idx,iectype,force in idxs:
c_type,unpack_func, pack_func = \
TypeTranslator.get(iectype,
- force = ctypes.byref(pack_func(c_type,force))
+ force = ctypes.byref(pack_func(c_type,force)) self._RegisterDebugVariable(idx, force)
+ def _TracesPush(self, trace): + self.TraceLock.acquire() + if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024 : + self.Traces.append(trace) + self.TraceLock.release() + self.LastSwapTrace = time() + if self.TraceThread is None and self.PLCStatus == "Started": + self.TraceThread = Thread(target=self.TraceThreadProc) + self.TraceThread.start() + self.TraceLock.acquire() + self.TraceLock.release() + def _TracesAutoSuspend(self): + # TraceProc stops here if Traces not polled for 3 seconds + traces_age = time() - self.LastSwapTrace + self.TraceLock.acquire() + 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.TraceLock.release() def GetTraceVariables(self):
+ return self.PLCStatus, self._TracesSwap() + def TraceThreadProc(self): - Return a list of variables, corresponding to the list of required idx
+ Return a list of traces, corresponding to the list of required idx - if self.PLCStatus == "Started":
+ while self.PLCStatus == "Started" :
if self.PLClibraryLock.acquire(False):
if self._GetDebugData(ctypes.byref(tick),
ctypes.byref(buff)) == 0:
- TraceVariables = UnpackDebugBuffer(buff, size.value, self._Idxs)
+ TraceBuffer = ctypes.string_at(buff.value, size.value) self.PLClibraryLock.release()
- if TraceVariables is not None:
- return self.PLCStatus, tick.value, TraceVariables
- return self.PLCStatus, None, []
+ if TraceBuffer is not None: + self._TracesPush((tick.value, TraceBuffer)) + self._TracesAutoSuspend() - def RemoteExec(self, script, **kwargs):
+ def RemoteExec(self, script, *kwargs): 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))