# HG changeset patch # User Edouard Tisserant # Date 1552308667 -3600 # Node ID 48ebcbe7f19b617949dd4ceb697e198ca123e705 # Parent 0ad5e616d37f4f4396306d3f3f1c927e2078127a# Parent 3a1c0c161f80f79180d88a8c9fd1dacfc799a2b2 Long due merge diff -r 3a1c0c161f80 -r 48ebcbe7f19b BeremizIDE.py --- a/BeremizIDE.py Tue Mar 05 11:43:20 2019 +0300 +++ b/BeremizIDE.py Mon Mar 11 13:51:07 2019 +0100 @@ -25,6 +25,7 @@ from __future__ import absolute_import +from __future__ import print_function import os import sys import tempfile @@ -208,10 +209,6 @@ def write_error(self, s): self.write(s, self.red_yellow) - def writeyield(self, s): - self.write(s) - wx.GetApp().Yield() - def flush(self): # Temporary deactivate read only mode on StyledTextCtrl for clearing # text. It seems that text modifications, even programmatically, are @@ -614,11 +611,7 @@ def AddToDoBeforeQuit(self, Thing): self.ToDoBeforeQuit.append(Thing) - def OnCloseFrame(self, event): - for evt_type in [wx.EVT_SET_FOCUS, - wx.EVT_KILL_FOCUS, - wx.stc.EVT_STC_UPDATEUI]: - self.LogConsole.Unbind(evt_type) + def TryCloseFrame(self): if self.CTR is None or self.CheckSaveBeforeClosing(_("Close Application")): if self.CTR is not None: self.CTR.KillDebugThread() @@ -630,8 +623,14 @@ Thing() self.ToDoBeforeQuit = [] + return True + return False + + def OnCloseFrame(self, event): + if self.TryCloseFrame(): event.Skip() else: + # prevent event to continue, i.e. cancel closing event.Veto() def RefreshFileMenu(self): diff -r 3a1c0c161f80 -r 48ebcbe7f19b Beremiz_service.py --- a/Beremiz_service.py Tue Mar 05 11:43:20 2019 +0300 +++ b/Beremiz_service.py Mon Mar 11 13:51:07 2019 +0100 @@ -30,17 +30,18 @@ import sys import getopt import threading -from threading import Thread, Semaphore, Lock -import traceback +from threading import Thread, Semaphore, Lock, currentThread +import __builtin__ from builtins import str as text from past.builtins import execfile from six.moves import builtins -import Pyro -import Pyro.core as pyro - -from runtime import PLCObject, ServicePublisher, MainWorker + +import runtime +from runtime.PyroServer import PyroServer from runtime.xenomai import TryPreloadXenomai +from runtime import LogMessageAndException from runtime import PlcStatus +from runtime.Stunnel import ensurePSK import util.paths as paths @@ -54,7 +55,7 @@ print(""" Usage of Beremiz PLC execution service :\n %s {[-n servicename] [-i IP] [-p port] [-x enabletaskbar] [-a autostart]|-h|--help} working_dir - -n zeroconf service name (default:disabled) + -n service name (default:None, zeroconf discovery disabled) -i IP address of interface to bind to (default:localhost) -p port number default:3000 -h print this help text and quit @@ -63,7 +64,7 @@ -t enable/disable Twisted web interface (0:disable 1:enable) (default:1) -w web server port or "off" to disable web server (default:8009) -c WAMP client config file (can be overriden by wampconf.json in project) - -s WAMP client secret, given as a file (can be overriden by wamp.secret in project) + -s PSK secret path (default:PSK disabled) -e python extension (absolute path .py) working_dir - directory where are stored PLC files @@ -79,10 +80,10 @@ sys.exit(2) # default values -given_ip = None +interface = '' port = 3000 webport = 8009 -wampsecret = None +PSKpath = None wampconf = None servicename = None autostart = False @@ -101,8 +102,10 @@ version() sys.exit() elif o == "-i": - if len(a.split(".")) == 4 or a == "localhost": - given_ip = a + if len(a.split(".")) == 4: + interface = a + elif a == "localhost": + interface = '127.0.0.1' else: usage() sys.exit() @@ -122,7 +125,7 @@ elif o == "-c": wampconf = None if a == "off" else a elif o == "-s": - wampsecret = None if a == "off" else a + PSKpath = None if a == "off" else a elif o == "-e": fnameanddirname = list(os.path.split(os.path.realpath(a))) fnameanddirname.reverse() @@ -144,11 +147,10 @@ WorkingDir = os.getcwd() argv = [WorkingDir] -if __name__ == '__main__': - builtins.__dict__['_'] = lambda x: x - # TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem - TryPreloadXenomai() - version() +builtins.__dict__['_'] = lambda x: x +# TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem +TryPreloadXenomai() +version() def Bpath(*args): @@ -187,9 +189,8 @@ def unicode_translation(message): return wx.GetTranslation(message).encode(default_locale) - if __name__ == '__main__': - builtins.__dict__['_'] = unicode_translation - # builtins.__dict__['_'] = wx.GetTranslation + builtins.__dict__['_'] = unicode_translation + # builtins.__dict__['_'] = wx.GetTranslation # Life is hard... have a candy. @@ -256,12 +257,11 @@ TBMENU_CHANGE_WD = wx.NewId() TBMENU_QUIT = wx.NewId() - def __init__(self, pyroserver, level): + def __init__(self, pyroserver): 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) @@ -284,15 +284,14 @@ menu = wx.Menu() menu.Append(self.TBMENU_START, _("Start PLC")) menu.Append(self.TBMENU_STOP, _("Stop PLC")) - if self.level == 1: - menu.AppendSeparator() - menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name")) - menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind")) - menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number")) - menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory")) - menu.AppendSeparator() - menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell")) - menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector")) + menu.AppendSeparator() + menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name")) + menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind")) + menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number")) + menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory")) + menu.AppendSeparator() + menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell")) + menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector")) menu.AppendSeparator() menu.Append(self.TBMENU_QUIT, _("Quit")) return menu @@ -311,19 +310,10 @@ return icon def OnTaskBarStartPLC(self, evt): - if self.pyroserver.plcobj is not None: - plcstatus = self.pyroserver.plcobj.GetPLCstatus()[0] - if plcstatus is PlcStatus.Stopped: - self.pyroserver.plcobj.StartPLC() - else: - print(_("PLC is empty or already started.")) + runtime.GetPLCObjectSingleton().StartPLC() def OnTaskBarStopPLC(self, evt): - if self.pyroserver.plcobj is not None: - if self.pyroserver.plcobj.GetPLCstatus()[0] == PlcStatus.Started: - Thread(target=self.pyroserver.plcobj.StopPLC).start() - else: - print(_("PLC is not started.")) + runtime.GetPLCObjectSingleton().StopPLC() def OnTaskBarChangeInterface(self, evt): ip_addr = self.pyroserver.ip_addr @@ -350,19 +340,16 @@ self.pyroserver.Restart() def OnTaskBarChangeName(self, evt): - servicename = self.pyroserver.servicename - servicename = '' if servicename is None else servicename - dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=servicename) + _servicename = self.pyroserver.servicename + _servicename = '' if _servicename is None else _servicename + dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=_servicename) dlg.SetTests([(lambda name: len(name) is not 0, _("Name must not be null!"))]) if dlg.ShowModal() == wx.ID_OK: self.pyroserver.servicename = 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 {} + return {"locals": runtime.GetPLCObjectSingleton().python_runtime_vars} def OnTaskBarLiveShell(self, evt): from wx import py @@ -406,89 +393,6 @@ return res -class Server(object): - def __init__(self, servicename, ip_addr, port, - workdir, argv, - statuschange=None, evaluator=default_evaluator, - pyruntimevars=None): - self.continueloop = True - self.daemon = None - self.servicename = servicename - self.ip_addr = ip_addr - self.port = port - self.workdir = workdir - self.argv = argv - self.servicepublisher = None - self.statuschange = statuschange - self.evaluator = evaluator - self.pyruntimevars = pyruntimevars - self.plcobj = PLCObject(self) - - def _to_be_published(self): - return 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" - - def PrintServerInfo(self): - print(_("Pyro port :"), self.port) - - # Beremiz IDE detects LOCAL:// runtime is ready by looking - # for self.workdir in the daemon's stdout. - print(_("Current working directory :"), self.workdir) - - if self._to_be_published(): - print(_("Publishing service on local network")) - - sys.stdout.flush() - - def PyroLoop(self, when_ready): - while self.continueloop: - Pyro.config.PYRO_MULTITHREADED = 0 - pyro.initServer() - self.daemon = pyro.Daemon(host=self.ip_addr, port=self.port) - - # pyro never frees memory after connection close if no timeout set - # taking too small timeout value may cause - # unwanted diconnection when IDE is kept busy for long periods - self.daemon.setTimeout(60) - - self.daemon.connect(self.plcobj, "PLCObject") - - if self._to_be_published(): - self.servicepublisher = ServicePublisher.ServicePublisher() - self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port) - - when_ready() - self.daemon.requestLoop() - self.daemon.sock.close() - - def Restart(self): - self._stop() - - def Quit(self): - self.continueloop = False - if self.plcobj is not None: - self.plcobj.StopPLC() - self.plcobj.UnLoadPLC() - self._stop() - - def _stop(self): - if self.plcobj is not None: - self.plcobj.StopPLC() - if self.servicepublisher is not None: - self.servicepublisher.UnRegisterService() - self.servicepublisher = None - self.daemon.shutdown(True) - - def AutoLoad(self): - self.plcobj.AutoLoad() - if self.plcobj.GetPLCstatus()[0] == PlcStatus.Stopped: - if autostart: - self.plcobj.StartPLC() - self.plcobj.StatusChange() - - if enabletwisted: import warnings with warnings.catch_warnings(): @@ -511,10 +415,11 @@ if havewx: reactor.registerWxApp(app) +twisted_reactor_thread_id = None +ui_thread = None + if havewx: wx_eval_lock = Semaphore(0) - # FIXME : beware wx mainloop is _not_ running in main thread - # main_thread = currentThread() def statuschangeTskBar(status): wx.CallAfter(taskbar_instance.UpdateIcon, status) @@ -527,40 +432,30 @@ wx_eval_lock.release() def evaluator(tocall, *args, **kwargs): - # FIXME : should implement anti-deadlock - # if main_thread == currentThread(): - # # 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, - statuschange, evaluator, pyruntimevars) - - taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx) + # To prevent deadlocks, check if current thread is not one of the UI + # UI threads can be either the one from WX main loop or + # worker thread from twisted "threadselect" reactor + current_id = currentThread().ident + + if ui_thread is not None \ + and ui_thread.ident != current_id \ + and (not havetwisted or ( + twisted_reactor_thread_id is not None + and twisted_reactor_thread_id != current_id)): + + o = type('', (object,), dict(call=(tocall, args, kwargs), res=None)) + wx.CallAfter(wx_evaluator, o) + wx_eval_lock.acquire() + return o.res + else: + # avoid dead lock if called from the wx mainloop + return default_evaluator(tocall, *args, **kwargs) else: - pyroserver = Server(servicename, given_ip, port, - WorkingDir, argv, - statuschange, pyruntimevars=pyruntimevars) - + evaluator = default_evaluator # Exception hooks -def LogMessageAndException(msg, exp=None): - if exp is None: - exp = sys.exc_info() - if pyroserver.plcobj is not None: - pyroserver.plcobj.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp))) - else: - print(msg) - traceback.print_exception(*exp) - - def LogException(*exp): LogMessageAndException("", exp) @@ -610,10 +505,24 @@ sys.path.append(extension_folder) execfile(os.path.join(extension_folder, extention_file), locals()) +# Service name is used as an ID for stunnel's PSK +# Some extension may set 'servicename' to a computed ID or Serial Number +# instead of using commandline '-n' +if servicename is not None and PSKpath is not None: + ensurePSK(servicename, PSKpath) + +runtime.CreatePLCObjectSingleton( + WorkingDir, argv, statuschange, evaluator, pyruntimevars) + +pyroserver = PyroServer(servicename, interface, port) + +if havewx: + taskbar_instance = BeremizTaskBarIcon(pyroserver) + if havetwisted: if webport is not None: try: - website = NS.RegisterWebsite(webport) + website = NS.RegisterWebsite(interface, webport) pyruntimevars["website"] = website NS.SetServer(pyroserver) statuschange.append(NS.website_statuslistener_factory(website)) @@ -623,7 +532,7 @@ if havewamp: try: WC.SetServer(pyroserver) - WC.RegisterWampClient(wampconf, wampsecret) + WC.RegisterWampClient(wampconf, PSKpath) WC.RegisterWebSettings(NS) except Exception: LogMessageAndException(_("WAMP client startup failed. ")) @@ -639,6 +548,11 @@ pyroserver.PrintServerInfo() +# Beremiz IDE detects LOCAL:// runtime is ready by looking +# for self.workdir in the daemon's stdout. +sys.stdout.write(_("Current working directory :") + WorkingDir + "\n") +sys.stdout.flush() + if havetwisted or havewx: ui_thread_started = Lock() ui_thread_started.acquire() @@ -656,7 +570,11 @@ # This order ui loop to unblock main thread when ready. if havetwisted: - reactor.callLater(0, ui_thread_started.release) + def signal_uithread_started(): + global twisted_reactor_thread_id + twisted_reactor_thread_id = currentThread().ident + ui_thread_started.release() + reactor.callLater(0, signal_uithread_started) else: wx.CallAfter(ui_thread_started.release) @@ -665,9 +583,15 @@ print("UI thread started successfully.") try: - MainWorker.runloop(pyroserver.AutoLoad) + runtime.MainWorker.runloop( + runtime.GetPLCObjectSingleton().AutoLoad, autostart) except KeyboardInterrupt: pass pyroserver.Quit() + +plcobj = runtime.GetPLCObjectSingleton() +plcobj.StopPLC() +plcobj.UnLoadPLC() + sys.exit(0) diff -r 3a1c0c161f80 -r 48ebcbe7f19b PSKManagement.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PSKManagement.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import +import os +import time +import json +from zipfile import ZipFile + +# PSK Management Data model : +# [[ID,Desc, LastKnownURI, LastConnect]] +COL_ID, COL_URI, COL_DESC, COL_LAST = range(4) +REPLACE, REPLACE_ALL, KEEP, KEEP_ALL, CANCEL = range(5) + + +def _pskpath(project_path): + return os.path.join(project_path, 'psk') + + +def _mgtpath(project_path): + return os.path.join(_pskpath(project_path), 'management.json') + + +def _ensurePSKdir(project_path): + pskpath = _pskpath(project_path) + if not os.path.exists(pskpath): + os.mkdir(pskpath) + return pskpath + + +def _default(ID): + return [ID, + '', # default description + None, # last known URI + None] # last connection date + + +def _dataByID(data): + return {row[COL_ID]: row for row in data} + + +def _LoadData(project_path): + """ load known keys metadata """ + if os.path.isdir(_pskpath(project_path)): + _path = _mgtpath(project_path) + if os.path.exists(_path): + return json.loads(open(_path).read()) + return [] + + +def _filterData(psk_files, data_input): + input_by_ID = _dataByID(data_input) + output = [] + # go through all secret files available an build data + # out of data recoverd from json and list of secret. + # this implicitly filters IDs out of metadata who's + # secret is missing + for filename in psk_files: + if filename.endswith('.secret'): + ID = filename[:-7] # strip filename extension + output.append(input_by_ID.get(ID, _default(ID))) + return output + + +def GetData(project_path): + loaded_data = _LoadData(project_path) + if loaded_data: + psk_files = os.listdir(_pskpath(project_path)) + return _filterData(psk_files, loaded_data) + return [] + + +def DeleteID(project_path, ID): + secret_path = os.path.join(_pskpath(project_path), ID+'.secret') + os.remove(secret_path) + + +def SaveData(project_path, data): + _ensurePSKdir(project_path) + with open(_mgtpath(project_path), 'w') as f: + f.write(json.dumps(data)) + + +def UpdateID(project_path, ID, secret, URI): + pskpath = _ensurePSKdir(project_path) + if not os.path.exists(pskpath): + os.mkdir(pskpath) + + secpath = os.path.join(pskpath, ID+'.secret') + with open(secpath, 'w') as f: + f.write(ID+":"+secret) + + # here we directly use _LoadData, avoiding filtering that could be long + data = _LoadData(project_path) + idata = _dataByID(data) + dataForID = idata.get(ID, None) if data else None + + _is_new_ID = dataForID is None + if _is_new_ID: + dataForID = _default(ID) + + dataForID[COL_URI] = URI + # FIXME : could store time instead os a string and use DVC model's cmp + # then date display could be smarter, etc - sortable sting hack for now + dataForID[COL_LAST] = time.strftime('%y/%M/%d-%H:%M:%S') + + if _is_new_ID: + data.append(dataForID) + + SaveData(project_path, data) + + +def ExportIDs(project_path, export_zip): + with ZipFile(export_zip, 'w') as zf: + path = _pskpath(project_path) + for nm in os.listdir(path): + if nm.endswith('.secret') or nm == 'management.json': + zf.write(os.path.join(path, nm), nm) + + +def ImportIDs(project_path, import_zip, should_I_replace_callback): + zf = ZipFile(import_zip, 'r') + data = GetData(project_path) + + zip_loaded_data = json.loads(zf.open('management.json').read()) + name_list = zf.namelist() + zip_filtered_data = _filterData(name_list, zip_loaded_data) + + idata = _dataByID(data) + + keys_to_import = [] + result = None + + for imported_row in zip_filtered_data: + ID = imported_row[COL_ID] + existing_row = idata.get(ID, None) + if existing_row is None: + data.append(imported_row) + else: + # callback returns the selected list for merge or none if canceled + if result not in [REPLACE_ALL, KEEP_ALL]: + result = should_I_replace_callback(existing_row, imported_row) + + if result == CANCEL: + return + + if result in [REPLACE_ALL, REPLACE]: + # replace with imported + existing_row[:] = imported_row + # copy the key of selected + keys_to_import.append(ID) + + for ID in keys_to_import: + zf.extract(ID+".secret", _pskpath(project_path)) + + SaveData(project_path, data) + + return data diff -r 3a1c0c161f80 -r 48ebcbe7f19b ProjectController.py --- a/ProjectController.py Tue Mar 05 11:43:20 2019 +0300 +++ b/ProjectController.py Mon Mar 11 13:51:07 2019 +0100 @@ -56,7 +56,7 @@ from editors.ProjectNodeEditor import ProjectNodeEditor from editors.IECCodeViewer import IECCodeViewer from editors.DebugViewer import DebugViewer, REFRESH_PERIOD -from dialogs import DiscoveryDialog +from dialogs import UriEditor, IDManager from PLCControler import PLCControler from plcopen.structures import IEC_KEYWORDS from plcopen.types_enums import ComputeConfigurationResourceName, ITEM_CONFNODE @@ -269,8 +269,10 @@ self._setBuildPath(None) self.debug_break = False self.previous_plcstate = None - # copy ConfNodeMethods so that it can be later customized + # copy StatusMethods so that it can be later customized self.StatusMethods = [dic.copy() for dic in self.StatusMethods] + self.DebugToken = None + self.debug_status = PlcStatus.Stopped def __del__(self): if self.DebugTimer: @@ -280,10 +282,14 @@ def LoadLibraries(self): self.Libraries = [] TypeStack = [] - for libname, clsname, _default in features.libraries: - if self.BeremizRoot.Libraries is not None and \ - getattr(self.BeremizRoot.Libraries, - "Enable_" + libname + "_Library"): + for libname, clsname, lib_enabled in features.libraries: + if self.BeremizRoot.Libraries is not None: + enable_attr = getattr(self.BeremizRoot.Libraries, + "Enable_" + libname + "_Library") + if enable_attr is not None: + lib_enabled = enable_attr + + if lib_enabled: Lib = GetClassImporter(clsname)()(self, libname, TypeStack) TypeStack.append(Lib.GetTypes()) self.Libraries.append(Lib) @@ -897,19 +903,6 @@ self._builder = targetclass(self) return self._builder - def ResetBuildMD5(self): - builder = self.GetBuilder() - if builder is not None: - builder.ResetBinaryCodeMD5() - self.EnableMethod("_Transfer", False) - - def GetLastBuildMD5(self): - builder = self.GetBuilder() - if builder is not None: - return builder.GetBinaryCodeMD5() - else: - return None - # # # C CODE GENERATION METHODS @@ -1137,7 +1130,6 @@ # If IEC code gen fail, bail out. if not IECGenRes: self.logger.write_error(_("PLC code generation failed !\n")) - self.ResetBuildMD5() return False # Reset variable and program list that are parsed from @@ -1153,7 +1145,6 @@ builder = self.GetBuilder() if builder is None: self.logger.write_error(_("Fatal : cannot get builder.\n")) - self.ResetBuildMD5() return False # Build @@ -1162,9 +1153,9 @@ self.logger.write_error(_("C Build failed.\n")) return False except Exception: + builder.ResetBinaryMD5() self.logger.write_error(_("C Build crashed !\n")) self.logger.write_error(traceback.format_exc()) - self.ResetBuildMD5() return False self.logger.write(_("Successfully built.\n")) @@ -1184,7 +1175,6 @@ self.logger.write_error( _("Runtime IO extensions C code generation failed !\n")) self.logger.write_error(traceback.format_exc()) - self.ResetBuildMD5() return False # Generate C code and compilation params from liraries @@ -1195,7 +1185,6 @@ self.logger.write_error( _("Runtime library extensions C code generation failed !\n")) self.logger.write_error(traceback.format_exc()) - self.ResetBuildMD5() return False self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + \ @@ -1245,7 +1234,6 @@ except Exception: self.logger.write_error(name + _(" generation failed !\n")) self.logger.write_error(traceback.format_exc()) - self.ResetBuildMD5() return False self.logger.write(_("C code generated successfully.\n")) return True @@ -1270,6 +1258,11 @@ _IECCodeView = None + def _showIDManager(self): + dlg = IDManager(self.AppFrame, self) + dlg.ShowModal() + dlg.Destroy() + def _showIECcode(self): self._OpenView("IEC code") @@ -1498,11 +1491,12 @@ self.UpdateMethodsFromPLCStatus() def SnapshotAndResetDebugValuesBuffers(self): - if self._connector is not None: - plc_status, Traces = self._connector.GetTraceVariables() + debug_status = PlcStatus.Disconnected + if self._connector is not None and self.DebugToken is not None: + debug_status, Traces = self._connector.GetTraceVariables(self.DebugToken) # print [dict.keys() for IECPath, (dict, log, status, fvalue) in # self.IECdebug_datas.items()] - if plc_status == PlcStatus.Started: + if debug_status == PlcStatus.Started: if len(Traces) > 0: for debug_tick, debug_buff in Traces: debug_vars = UnpackDebugBuffer( @@ -1530,14 +1524,14 @@ ticks, self.DebugTicks = self.DebugTicks, [] - return ticks, buffers + return debug_status, ticks, buffers def RegisterDebugVarToConnector(self): self.DebugTimer = None Idxs = [] self.TracedIECPath = [] self.TracedIECTypes = [] - if self._connector is not None: + if self._connector is not None and self.debug_status != PlcStatus.Broken: IECPathsToPop = [] for IECPath, data_tuple in self.IECdebug_datas.iteritems(): WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple @@ -1566,11 +1560,12 @@ IdxsT = zip(*Idxs) self.TracedIECPath = IdxsT[3] self.TracedIECTypes = IdxsT[1] - self._connector.SetTraceVariablesList(zip(*IdxsT[0:3])) + self.DebugToken = self._connector.SetTraceVariablesList(zip(*IdxsT[0:3])) else: self.TracedIECPath = [] self._connector.SetTraceVariablesList([]) - self.SnapshotAndResetDebugValuesBuffers() + self.DebugToken = None + self.debug_status, _debug_ticks, _buffers = self.SnapshotAndResetDebugValuesBuffers() def IsPLCStarted(self): return self.previous_plcstate == PlcStatus.Started @@ -1686,7 +1681,7 @@ return self._connector.RemoteExec(script, **kwargs) def DispatchDebugValuesProc(self, event): - debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers() + self.debug_status, debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers() start_time = time.time() if len(self.TracedIECPath) == len(buffers): for IECPath, values in zip(self.TracedIECPath, buffers): @@ -1697,11 +1692,15 @@ self.CallWeakcallables( "__tick__", "NewDataAvailable", debug_ticks) - delay = time.time() - start_time - next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay) - if self.DispatchDebugValuesTimer is not None: - self.DispatchDebugValuesTimer.Start( - int(next_refresh * 1000), oneShot=True) + if self.debug_status == PlcStatus.Broken: + self.logger.write_warning( + _("Debug: token rejected - other debug took over - reconnect to recover\n")) + else: + delay = time.time() - start_time + next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay) + if self.DispatchDebugValuesTimer is not None: + self.DispatchDebugValuesTimer.Start( + int(next_refresh * 1000), oneShot=True) event.Skip() def KillDebugThread(self): @@ -1712,6 +1711,9 @@ self.previous_plcstate = None if self.AppFrame: self.AppFrame.ResetGraphicViewers() + + self.debug_status = PlcStatus.Started + self.RegisterDebugVarToConnector() if self.DispatchDebugValuesTimer is not None: self.DispatchDebugValuesTimer.Start( @@ -1748,7 +1750,6 @@ if connector is not None: if self.StatusTimer is not None: # Start the status Timer - wx.Yield() self.StatusTimer.Start(milliseconds=500, oneShot=False) else: if self.StatusTimer is not None: @@ -1771,9 +1772,9 @@ if uri == "": try: # Launch Service Discovery dialog - dialog = DiscoveryDialog(self.AppFrame) + dialog = UriEditor(self.AppFrame, self) answer = dialog.ShowModal() - uri = dialog.GetURI() + uri = str(dialog.GetURI()) dialog.Destroy() except Exception: self.logger.write_error(_("Local service discovery failed!\n")) @@ -1798,10 +1799,10 @@ # Get connector from uri try: self._SetConnector(connectors.ConnectorFactory(uri, self)) - except Exception: + except Exception as e: self.logger.write_error( - _("Exception while connecting %s!\n") % uri) - self.logger.write_error(traceback.format_exc()) + _("Exception while connecting to '{uri}': {ex}\n").format( + uri=uri, ex=e)) # Did connection success ? if self._connector is None: @@ -1823,26 +1824,20 @@ def CompareLocalAndRemotePLC(self): if self._connector is None: return - # We are now connected. Update button status - MD5 = self.GetLastBuildMD5() + builder = self.GetBuilder() + if builder is None: + return + MD5 = builder.GetBinaryMD5() + if MD5 is None: + return # Check remote target PLC correspondance to that md5 - if MD5 is not None: - if not self._connector.MatchMD5(MD5): - # self.logger.write_warning( - # _("Latest build does not match with target, please - # transfer.\n")) - self.EnableMethod("_Transfer", True) - else: - # self.logger.write( - # _("Latest build matches target, no transfer needed.\n")) - self.EnableMethod("_Transfer", True) - # warns controller that program match - self.ProgramTransferred() - # self.EnableMethod("_Transfer", False) + if self._connector.MatchMD5(MD5): + self.logger.write( + _("Latest build matches with connected target.\n")) + self.ProgramTransferred() else: - # self.logger.write_warning( - # _("Cannot compare latest build to target. Please build.\n")) - self.EnableMethod("_Transfer", False) + self.logger.write( + _("Latest build does not match with connected target.\n")) def _Disconnect(self): self._SetConnector(None) @@ -1858,8 +1853,13 @@ else: return - # Get the last build PLC's - MD5 = self.GetLastBuildMD5() + builder = self.GetBuilder() + if builder is None: + self.logger.write_error(_("Fatal : cannot get builder.\n")) + return False + + # recover md5 from last build + MD5 = builder.GetBinaryMD5() # Check if md5 file is empty : ask user to build PLC if MD5 is None: @@ -1872,35 +1872,44 @@ self.logger.write( _("Latest build already matches current target. Transfering anyway...\n")) - # Get temprary directory path + # purge any non-finished transfer + # note: this would abord any runing transfer with error + self._connector.PurgeBlobs() + + # transfer extra files extrafiles = [] for extrafilespath in [self._getExtraFilesPath(), self._getProjectFilesPath()]: - extrafiles.extend( - [(name, open(os.path.join(extrafilespath, name), - 'rb').read()) - for name in os.listdir(extrafilespath)]) + for name in os.listdir(extrafilespath): + extrafiles.append(( + name, + self._connector.BlobFromFile( + # use file name as a seed to avoid collisions + # with files having same content + os.path.join(extrafilespath, name), name))) # Send PLC on target - builder = self.GetBuilder() - if builder is not None: - data = builder.GetBinaryCode() - if data is not None: - if self._connector.NewPLC(MD5, data, extrafiles) and self.GetIECProgramsAndVariables(): - self.UnsubscribeAllDebugIECVariable() - self.ProgramTransferred() - if self.AppFrame is not None: - self.AppFrame.CloseObsoleteDebugTabs() - self.AppFrame.RefreshPouInstanceVariablesPanel() - self.logger.write(_("Transfer completed successfully.\n")) - self.AppFrame.LogViewer.ResetLogCounters() - else: - self.logger.write_error(_("Transfer failed\n")) - self.HidePLCProgress() + object_path = builder.GetBinaryPath() + # arbitrarily use MD5 as a seed, could be any string + object_blob = self._connector.BlobFromFile(object_path, MD5) + + self.HidePLCProgress() + + self.logger.write(_("PLC data transfered successfully.\n")) + + if self._connector.NewPLC(MD5, object_blob, extrafiles): + if self.GetIECProgramsAndVariables(): + self.UnsubscribeAllDebugIECVariable() + self.ProgramTransferred() + self.AppFrame.CloseObsoleteDebugTabs() + self.AppFrame.RefreshPouInstanceVariablesPanel() + self.AppFrame.LogViewer.ResetLogCounters() + self.logger.write(_("PLC installed successfully.\n")) else: - self.logger.write_error( - _("No PLC to transfer (did build succeed ?)\n")) + self.logger.write_error(_("Missing debug data\n")) + else: + self.logger.write_error(_("PLC couldn't be installed\n")) wx.CallAfter(self.UpdateMethodsFromPLCStatus) @@ -1953,6 +1962,12 @@ "shown": False, }, { + "bitmap": "IDManager", + "name": _("ID Manager"), + "tooltip": _("Manage secure connection identities"), + "method": "_showIDManager", + }, + { "bitmap": "ShowIECcode", "name": _("Show code"), "tooltip": _("Show IEC code generated by PLCGenerator"), diff -r 3a1c0c161f80 -r 48ebcbe7f19b bacnet/runtime/device.c --- a/bacnet/runtime/device.c Tue Mar 05 11:43:20 2019 +0300 +++ b/bacnet/runtime/device.c Mon Mar 11 13:51:07 2019 +0100 @@ -398,7 +398,7 @@ PROP_OBJECT_TYPE, /* R R ( 79) */ PROP_SYSTEM_STATUS, /* R R (112) */ PROP_VENDOR_NAME, /* R R (121) */ - PROP_VENDOR_IDENTIFIER, /* W R (120) */ + PROP_VENDOR_IDENTIFIER, /* R R (120) */ PROP_MODEL_NAME, /* W R ( 70) */ PROP_FIRMWARE_REVISION, /* R R ( 44) */ PROP_APPLICATION_SOFTWARE_VERSION, /* R R ( 12) */ @@ -1366,16 +1366,16 @@ apdu_timeout_set((uint16_t) value.type.Unsigned_Int); } break; - case PROP_VENDOR_IDENTIFIER: - status = - WPValidateArgType(&value, BACNET_APPLICATION_TAG_UNSIGNED_INT, - &wp_data->error_class, &wp_data->error_code); - if (status) { - /* FIXME: bounds check? */ - Device_Set_Vendor_Identifier((uint16_t) value. - type.Unsigned_Int); - } - break; +// case PROP_VENDOR_IDENTIFIER: +// status = +// WPValidateArgType(&value, BACNET_APPLICATION_TAG_UNSIGNED_INT, +// &wp_data->error_class, &wp_data->error_code); +// if (status) { +// /* FIXME: bounds check? */ +// Device_Set_Vendor_Identifier((uint16_t) value. +// type.Unsigned_Int); +// } +// break; // case PROP_SYSTEM_STATUS: // status = // WPValidateArgType(&value, BACNET_APPLICATION_TAG_ENUMERATED, @@ -1453,6 +1453,7 @@ case PROP_OBJECT_TYPE: case PROP_SYSTEM_STATUS: case PROP_VENDOR_NAME: + case PROP_VENDOR_IDENTIFIER: case PROP_FIRMWARE_REVISION: case PROP_APPLICATION_SOFTWARE_VERSION: case PROP_LOCAL_TIME: diff -r 3a1c0c161f80 -r 48ebcbe7f19b bacnet/runtime/server.c --- a/bacnet/runtime/server.c Tue Mar 05 11:43:20 2019 +0300 +++ b/bacnet/runtime/server.c Mon Mar 11 13:51:07 2019 +0100 @@ -517,7 +517,7 @@ if (elapsed_seconds) { last_seconds = current_seconds; dcc_timer_seconds(elapsed_seconds); - bvlc_maintenance_timer(elapsed_seconds); + //bvlc_maintenance_timer(elapsed_seconds); // already called by dlenv_maintenance_timer() => do _not_ call here! dlenv_maintenance_timer(elapsed_seconds); elapsed_milliseconds = elapsed_seconds * 1000; tsm_timer_milliseconds(elapsed_milliseconds); diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/ConnectorBase.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/ConnectorBase.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import +import md5 + + +class ConnectorBase(object): + + chuncksize = 1024*1024 + + def BlobFromFile(self, filepath, seed): + s = md5.new() + s.update(seed) + blobID = self.SeedBlob(seed) + with open(filepath, "rb") as f: + while blobID == s.digest(): + chunk = f.read(self.chuncksize) + if len(chunk) == 0: + return blobID + blobID = self.AppendChunkToBlob(chunk, blobID) + s.update(chunk) diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/PYRO/PSK_Adapter.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/PYRO/PSK_Adapter.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,94 @@ +from __future__ import absolute_import +from __future__ import print_function + +import socket +import re +import sslpsk +import ssl +import Pyro +from Pyro.core import PyroURI +from Pyro.protocol import _connect_socket, TCPConnection, PYROAdapter +from Pyro.errors import ConnectionDeniedError, ProtocolError +from Pyro.util import Log + + +# The TLS-PSK adapter that handles SSL connections instead of regular sockets, +# but using Pre Shared Keys instead of Certificates +# +class PYROPSKAdapter(PYROAdapter): + # This is essentialy the same as in Pyro/protocol.py + # only raw_sock wrapping into sock through sslpsk.wrap_socket was added + # Pyro unfortunately doesn't allow cleaner customization + def bindToURI(self, URI): + with self.lock: # only 1 thread at a time can bind the URI + try: + self.URI = URI + + # This are the statements that differ from Pyro/protocol.py + raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _connect_socket(raw_sock, URI.address, URI.port, self.timeout) + sock = sslpsk.wrap_socket( + raw_sock, psk=Pyro.config.PYROPSK, server_side=False, + ciphers="PSK-AES256-CBC-SHA", # available in openssl 1.0.2 + ssl_version=ssl.PROTOCOL_TLSv1) + # all the rest is the same as in Pyro/protocol.py + + conn = TCPConnection(sock, sock.getpeername()) + # receive the authentication challenge string, and use that to build the actual identification string. + try: + authChallenge = self.recvAuthChallenge(conn) + except ProtocolError, x: + # check if we were denied + if hasattr(x, "partialMsg") and x.partialMsg[:len(self.denyMSG)] == self.denyMSG: + raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(x.partialMsg[-1])]) + else: + raise + # reply with our ident token, generated from the ident passphrase and the challenge + msg = self._sendConnect(sock, self.newConnValidator.createAuthToken(self.ident, authChallenge, conn.addr, self.URI, None)) + if msg == self.acceptMSG: + self.conn = conn + self.conn.connected = 1 + Log.msg('PYROAdapter', 'connected to', str(URI)) + if URI.protocol == 'PYROLOCPSK': + self.resolvePYROLOC_URI("PYROPSK") # updates self.URI + elif msg[:len(self.denyMSG)] == self.denyMSG: + try: + raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(msg[-1])]) + except (KeyError, ValueError): + raise ConnectionDeniedError('invalid response') + except socket.error: + Log.msg('PYROAdapter', 'connection failed to URI', str(URI)) + raise ProtocolError('connection failed') + + +_getProtocolAdapter = Pyro.protocol.getProtocolAdapter + + +def getProtocolAdapter(protocol): + if protocol in ('PYROPSK', 'PYROLOCPSK'): + return PYROPSKAdapter() + return _getProtocolAdapter(protocol) + + +Pyro.protocol.getProtocolAdapter = getProtocolAdapter + + +_processStringURI = Pyro.core.processStringURI + + +def processStringURI(URI): + x = re.match(r'(?PPYROLOCPSK)://(?P[^\s:]+):?(?P\d+)?/(?P\S*)', URI) + if x: + protocol = x.group('protocol') + hostname = x.group('hostname') + port = x.group('port') + if port: + port = int(port) + else: + port = 0 + name = x.group('name') + return PyroURI(hostname, name, port, protocol) + return _processStringURI(URI) + + +Pyro.core.processStringURI = processStringURI diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/PYRO/__init__.py --- a/connectors/PYRO/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/connectors/PYRO/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -36,9 +36,9 @@ import Pyro.util from Pyro.errors import PyroError +import PSKManagement as PSK from runtime import PlcStatus -service_type = '_PYRO._tcp.local.' # this module attribute contains a list of DNS-SD (Zeroconf) service types # supported by this connector confnode. # @@ -52,66 +52,35 @@ """ 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): + scheme, location = uri.split("://") + if scheme == "PYROS": + import connectors.PYRO.PSK_Adapter # pylint: disable=wrong-import-order,unused-import,wrong-import-position + schemename = "PYROLOCPSK" + url, ID = location.split('#') # TODO fix exception when # not found + # load PSK from project + secpath = os.path.join(str(confnodesroot.ProjectPath), 'psk', ID+'.secret') + if not os.path.exists(secpath): 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) + 'Error: Pre-Shared-Key Secret in %s is missing!\n' % secpath) 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 # pylint: disable=import-error - 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 + secret = open(secpath).read().partition(':')[2].rstrip('\n\r') + Pyro.config.PYROPSK = (secret, ID) + # strip ID from URL, so that pyro can understand it. + location = url else: schemename = "PYROLOC" - if location.find(service_type) != -1: - try: - from zeroconf import Zeroconf - r = Zeroconf() - i = r.get_service_info(service_type, location) - if i is None: - raise Exception("'%s' not found" % location) - ip = str(socket.inet_ntoa(i.address)) - port = str(i.port) - newlocation = ip + ':' + port - confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation)) - location = newlocation - r.close() - except Exception: - 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.core.getAttrProxyForURI(schemename + "://" + location + "/PLCObject") - except Exception: - confnodesroot.logger.write_error(_("Connection to '%s' failed.\n") % location) - confnodesroot.logger.write_error(traceback.format_exc()) + except Exception, e: + confnodesroot.logger.write_error( + _("Connection to {loc} failed with exception {ex}\n").format( + loc=location, exo=str(e))) return None + RemotePLCObjectProxy.adapter.setTimeout(60) + def PyroCatcher(func, default=None): """ A function that catch a Pyro exceptions, write error to logger @@ -136,13 +105,16 @@ # Check connection is effective. # lambda is for getattr of GetPLCstatus to happen inside catcher - if PyroCatcher(RemotePLCObjectProxy.GetPLCstatus)() is None: - confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n")) - return None + IDPSK = PyroCatcher(RemotePLCObjectProxy.GetPLCID)() + if IDPSK is None: + confnodesroot.logger.write_warning(_("PLC did not provide identity and security infomation.\n")) + else: + ID, secret = IDPSK + PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri) _special_return_funcs = { "StartPLC": False, - "GetTraceVariables": ("Broken", None), + "GetTraceVariables": (PlcStatus.Broken, None), "GetPLCstatus": (PlcStatus.Broken, None), "RemoteExec": (-1, "RemoteExec script failed!") } @@ -161,4 +133,4 @@ self.__dict__[attrName] = member return member - return PyroProxyProxy() + return PyroProxyProxy diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/PYRO/dialog.py --- a/connectors/PYRO/dialog.py Tue Mar 05 11:43:20 2019 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2018: Smarteh -# -# See COPYING file for copyrights details. - - -from __future__ import absolute_import -from __future__ import print_function - -import wx -from zope.interface import implementer - -from controls.UriLocationEditor import IConnectorPanel - -URITypes = ["LOCAL", "PYRO", "PYROS"] - - -def PYRO_connector_dialog(confnodesroot): - [ID_IPTEXT, ID_PORTTEXT] = [wx.NewId() for _init_ctrls in range(2)] - - @implementer(IConnectorPanel) - class PYROConnectorPanel(wx.Panel): - def __init__(self, typeConnector, parrent, *args, **kwargs): - self.type = typeConnector - self.parrent = parrent - wx.Panel.__init__(self, parrent, *args, **kwargs) - self._init_ctrls() - self._init_sizers() - self.uri = None - - def _init_ctrls(self): - self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1)) - self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1)) - - def _init_sizers(self): - self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10) - self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.IpText, flag=wx.GROW) - - self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.PortText, flag=wx.GROW) - self.SetSizer(self.mainSizer) - - def SetURI(self, uri): - self.uri = uri - uri_list = uri.strip().split(":") - length = len(uri_list) - if length == 3: - self.IpText.SetValue(uri_list[1].strip("/")) - self.PortText.SetValue(uri_list[2]) - elif length == 2: - self.IpText.SetValue(uri_list[1].strip("/")) - - def GetURI(self): - self.uri = self.type+"://"+self.IpText.GetValue()+":"+self.PortText.GetValue() - return self.uri - - return PYROConnectorPanel("PYRO", confnodesroot) diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/PYRO_dialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/PYRO_dialog.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import + +from itertools import repeat, islice, chain +import wx + +from connectors.SchemeEditor import SchemeEditor + + +model = [('host', _("Host:")), + ('port', _("Port:"))] + +# (scheme, model, secure) +models = [("LOCAL", [], False), ("PYRO", model, False), ("PYROS", model, True)] + +Schemes = list(zip(*models)[0]) + +_PerSchemeConf = {sch: (mod, sec) for sch, mod, sec in models} + + +class PYRO_dialog(SchemeEditor): + def __init__(self, scheme, *args, **kwargs): + # ID selector is enabled only on PYROS (secure) + self.model, self.EnableIDSelector = _PerSchemeConf[scheme] + + SchemeEditor.__init__(self, scheme, *args, **kwargs) + + # pylint: disable=unused-variable + def SetLoc(self, loc): + hostport, ID = list(islice(chain(loc.split("#"), repeat("")), 2)) + host, port = list(islice(chain(hostport.split(":"), repeat("")), 2)) + self.SetFields(locals()) + + def GetLoc(self): + if self.model: + fields = self.GetFields() + template = "{host}" + if fields['port']: + template += ":{port}" + if fields['ID']: + template += "#{ID}" + + return template.format(**fields) + return '' diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/SchemeEditor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/SchemeEditor.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import + +from functools import partial +import wx + +from controls.IDBrowser import IDBrowser + + +class SchemeEditor(wx.Panel): + def __init__(self, scheme, parent, *args, **kwargs): + self.txtctrls = {} + wx.Panel.__init__(self, parent, *args, **kwargs) + + self.fieldsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10) + + if self.EnableIDSelector: + self.model = self.model + [("ID", _("ID:"))] + + for tag, label in self.model: + txtctrl = wx.TextCtrl(parent=self, size=wx.Size(200, -1)) + self.txtctrls[tag] = txtctrl + for win, flag in [ + (wx.StaticText(self, label=label), + wx.ALIGN_CENTER_VERTICAL), + (txtctrl, wx.GROW)]: + self.fieldsizer.AddWindow(win, flag=flag) + + self.fieldsizer.AddSpacer(20) + + if self.EnableIDSelector: + self.mainsizer = wx.FlexGridSizer(cols=2, hgap=10, vgap=10) + self.mainsizer.AddSizer(self.fieldsizer) + self.idselector = IDBrowser( + self, parent.ctr, + # use a callafter, as editor can be deleted by calling SetURI + partial(wx.CallAfter, parent.SetURI), + self.txtctrls[tag].SetValue) + self.mainsizer.AddWindow(self.idselector) + self.SetSizer(self.mainsizer) + else: + self.SetSizer(self.fieldsizer) + + def SetFields(self, fields): + for tag, _label in self.model: + self.txtctrls[tag].SetValue(fields[tag]) + + def GetFields(self): + return {tag: self.txtctrls[tag].GetValue() for tag, _label in self.model} diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/WAMP/__init__.py --- a/connectors/WAMP/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/connectors/WAMP/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -27,8 +27,9 @@ from __future__ import print_function import sys import traceback +from functools import partial from threading import Thread, Event -from builtins import str as text +from six import text_type as text from twisted.internet import reactor, threads from autobahn.twisted import wamp @@ -66,15 +67,15 @@ } -def WAMP_connector_factory(uri, confnodesroot): +def _WAMP_connector_factory(cls, uri, confnodesroot): """ WAMP://127.0.0.1:12345/path#realm#ID WAMPS://127.0.0.1:12345/path#realm#ID """ - servicetype, location = uri.split("://") + scheme, location = uri.split("://") urlpath, realm, ID = location.split('#') urlprefix = {"WAMP": "ws", - "WAMPS": "wss"}[servicetype] + "WAMPS": "wss"}[scheme] url = urlprefix+"://"+urlpath def RegisterWampClient(): @@ -88,7 +89,7 @@ extra={"ID": ID}) session_factory = wamp.ApplicationSessionFactory( config=component_config) - session_factory.session = WampSession + session_factory.session = cls # create a WAMP-over-WebSocket transport client factory transport_factory = WampWebSocketClientFactory( @@ -153,10 +154,10 @@ self.__dict__[attrName] = member return member - # Try to get the proxy object - try: - return WampPLCObjectProxy() - except Exception: - confnodesroot.logger.write_error(_("WAMP connection to '%s' failed.\n") % location) - confnodesroot.logger.write_error(traceback.format_exc()) - return None + # TODO : GetPLCID() + # TODO : PSK.UpdateID() + + return WampPLCObjectProxy + + +WAMP_connector_factory = partial(_WAMP_connector_factory, WampSession) diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/WAMP/dialog.py --- a/connectors/WAMP/dialog.py Tue Mar 05 11:43:20 2019 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2018: Smarteh -# -# See COPYING file for copyrights details. - - -from __future__ import absolute_import -from __future__ import print_function - -import wx -from zope.interface import implementer - -from controls.UriLocationEditor import IConnectorPanel - -URITypes = ["WAMP", "WAMPS"] - - -def WAMP_connector_dialog(confnodesroot): - [ID_IPTEXT, ID_PORTTEXT, ID_REALMTEXT, ID_WAMPIDTEXT, ID_SECURECHECKBOX] = [wx.NewId() for _init_ctrls in range(5)] - - @implementer(IConnectorPanel) - class WAMPConnectorPanel(wx.Panel): - def __init__(self, typeConnector, parrent, *args, **kwargs): - self.type = typeConnector - self.parrent = parrent - wx.Panel.__init__(self, parrent, *args, **kwargs) - self._init_ctrls() - self._init_sizers() - self.uri = None - - def _init_ctrls(self): - self.IpText = wx.TextCtrl(parent=self, id=ID_IPTEXT, size=wx.Size(200, -1)) - self.PortText = wx.TextCtrl(parent=self, id=ID_PORTTEXT, size=wx.Size(200, -1)) - self.RealmText = wx.TextCtrl(parent=self, id=ID_REALMTEXT, size=wx.Size(200, -1)) - self.WAMPIDText = wx.TextCtrl(parent=self, id=ID_WAMPIDTEXT, size=wx.Size(200, -1)) - self.SecureCheckbox = wx.CheckBox(self, ID_SECURECHECKBOX, _("Is connection secure?")) - - def _init_sizers(self): - self.mainSizer = wx.FlexGridSizer(cols=2, hgap=10, rows=5, vgap=10) - self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI host:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.IpText, flag=wx.GROW) - - self.mainSizer.AddWindow(wx.StaticText(self, label=_("URI port:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.PortText, flag=wx.GROW) - - self.mainSizer.AddWindow(wx.StaticText(self, label=_("Realm:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.RealmText, flag=wx.GROW) - - self.mainSizer.AddWindow(wx.StaticText(self, label=_("WAMP ID:")), - flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.WAMPIDText, flag=wx.GROW) - - self.mainSizer.AddWindow(wx.StaticText(self, label=""), flag=wx.ALIGN_CENTER_VERTICAL) - self.mainSizer.AddWindow(self.SecureCheckbox, flag=wx.GROW) - - self.SetSizer(self.mainSizer) - - def SetURI(self, uri): - self.uri = uri - uri_list = uri.strip().split(":") - length = len(uri_list) - - if length > 0: - if uri_list[0] == URITypes[1]: - self.SecureCheckbox.SetValue(True) - - if length > 2: - self.IpText.SetValue(uri_list[1].strip("/")) - wampSett = uri_list[2].split("#") - length2 = len(wampSett) - if length2 > 0: - self.PortText.SetValue(wampSett[0]) - if length2 > 1: - self.RealmText.SetValue(wampSett[1]) - if length2 > 2: - self.WAMPIDText.SetValue(wampSett[2]) - - def GetURI(self): - if self.IpText.Validate(): - typeForURI = self.type + "S" if self.SecureCheckbox.GetValue() else self.type - self.uri = typeForURI + "://" + self.IpText.GetValue() + ":" + self.PortText.GetValue() + "#" + self.RealmText.GetValue() + "#" + self.WAMPIDText.GetValue() - return self.uri - else: - return "" - - return WAMPConnectorPanel("WAMP", confnodesroot) diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/WAMP_dialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/WAMP_dialog.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import + +from itertools import repeat, islice, chain + +from connectors.SchemeEditor import SchemeEditor + +Schemes = ["WAMP", "WAMPS"] + +model = [('host', _("Host:")), + ('port', _("Port:")), + ('realm', _("Realm:"))] + + +class WAMP_dialog(SchemeEditor): + def __init__(self, *args, **kwargs): + self.model = model + self.EnableIDSelector = True + SchemeEditor.__init__(self, *args, **kwargs) + + # pylint: disable=unused-variable + def SetLoc(self, loc): + hostport, realm, ID = list(islice(chain(loc.split("#"), repeat("")), 3)) + host, port = list(islice(chain(hostport.split(":"), repeat("")), 2)) + self.SetFields(locals()) + + def GetLoc(self): + fields = self.GetFields() + + # TODO : input validation test + + template = "{host}" + \ + (":{port}" if fields['port'] else '') +\ + "#{realm}#{ID}" + + return template.format(**fields) diff -r 3a1c0c161f80 -r 48ebcbe7f19b connectors/__init__.py --- a/connectors/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/connectors/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -29,33 +29,37 @@ from __future__ import absolute_import from os import listdir, path import util.paths as paths +from connectors.ConnectorBase import ConnectorBase +from types import ClassType -_base_path = paths.AbsDir(__file__) +connectors_packages = ["PYRO", "WAMP"] def _GetLocalConnectorClassFactory(name): return lambda: getattr(__import__(name, globals(), locals()), name + "_connector_factory") -def _GetLocalConnectorClassDialog(name): - return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), name + "_connector_dialog") +connectors = {name: _GetLocalConnectorClassFactory(name) + for name in connectors_packages} + +_dialogs_imported = False +per_URI_connectors = None +schemes = None -def _GetLocalConnectorURITypes(name): - return lambda: getattr(__import__(name + '.dialog', globals(), locals(), fromlist=['dialog']), "URITypes", None) +# lazy import of connectors dialogs, only if used +def _Import_Dialogs(): + global per_URI_connectors, schemes, _dialogs_imported + if not _dialogs_imported: + _dialogs_imported = True + per_URI_connectors = {} + schemes = [] + for con_name in connectors_packages: + module = __import__(con_name + '_dialog', globals(), locals()) - -connectors = {name: - _GetLocalConnectorClassFactory(name) - for name in listdir(_base_path) - if (path.isdir(path.join(_base_path, name)) and - not name.startswith("__"))} - -connectors_dialog = {name: - {"function": _GetLocalConnectorClassDialog(name), "URITypes": _GetLocalConnectorURITypes(name)} - for name in listdir(_base_path) - if (path.isdir(path.join(_base_path, name)) and - not name.startswith("__"))} + for scheme in module.Schemes: + per_URI_connectors[scheme] = getattr(module, con_name + '_dialog') + schemes += [scheme] def ConnectorFactory(uri, confnodesroot): @@ -63,41 +67,67 @@ Return a connector corresponding to the URI or None if cannot connect to URI """ - servicetype = uri.split("://")[0].upper() - if servicetype == "LOCAL": + _scheme = uri.split("://")[0].upper() + + # commented code to enable for MDNS:// support + # _scheme, location = uri.split("://") + # _scheme = _scheme.upper() + + if _scheme == "LOCAL": # Local is special case # pyro connection to local runtime # started on demand, listening on random port - servicetype = "PYRO" + scheme = "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] + + # commented code to enable for MDNS:// support + # elif _scheme == "MDNS": + # try: + # from zeroconf import Zeroconf + # r = Zeroconf() + # i = r.get_service_info(zeroconf_service_type, location) + # if i is None: + # raise Exception("'%s' not found" % location) + # ip = str(socket.inet_ntoa(i.address)) + # port = str(i.port) + # newlocation = ip + ':' + port + # confnodesroot.logger.write(_("'{a1}' is located at {a2}\n").format(a1=location, a2=newlocation)) + # location = newlocation + # # not a bug, but a workaround against obvious downgrade attack + # scheme = "PYROS" + # r.close() + # except Exception: + # confnodesroot.logger.write_error(_("MDNS resolution failure for '%s'\n") % location) + # confnodesroot.logger.write_error(traceback.format_exc()) + # return None + + elif _scheme in connectors: + scheme = _scheme + elif _scheme[-1] == 'S' and _scheme[:-1] in connectors: + scheme = _scheme[:-1] else: return None - # import module according to uri type - connectorclass = connectors[servicetype]() - return connectorclass(uri, confnodesroot) + # import module according to uri type and get connector specific baseclass + # first call to import the module, + # then call with parameters to create the class + connector_specific_class = connectors[scheme]()(uri, confnodesroot) + + if connector_specific_class is None: + return None + + # new class inheriting from generic and specific connector base classes + return ClassType(_scheme + "_connector", + (ConnectorBase, connector_specific_class), {})() -def ConnectorDialog(conn_type, confnodesroot): - if conn_type not in connectors_dialog: - return None - - connectorclass = connectors_dialog[conn_type]["function"]() - return connectorclass(confnodesroot) +def EditorClassFromScheme(scheme): + _Import_Dialogs() + return per_URI_connectors.get(scheme, None) -def GetConnectorFromURI(uri): - typeOfConnector = None - for conn_type in connectors_dialog: - connectorTypes = connectors_dialog[conn_type]["URITypes"]() - if connectorTypes and uri in connectorTypes: - typeOfConnector = conn_type - break - - return typeOfConnector +def ConnectorSchemes(): + _Import_Dialogs() + return schemes diff -r 3a1c0c161f80 -r 48ebcbe7f19b controls/DiscoveryPanel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/controls/DiscoveryPanel.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz, a Integrated Development Environment for +# programming IEC 61131-3 automates supporting plcopen standard and CanFestival. +# +# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# Copyright (C) 2017: Andrey Skvortsov +# +# See COPYING file for copyrights details. +# +# This program 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 +# of the License, or (at your option) any later version. +# +# This program 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 program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +from __future__ import absolute_import +import socket +from six.moves import xrange +import wx +import wx.lib.mixins.listctrl as listmix +from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses + +service_type = '_Beremiz._tcp.local.' + + +class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): + def __init__(self, parent, id, name, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0): + wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name) + listmix.ListCtrlAutoWidthMixin.__init__(self) + + +class DiscoveryPanel(wx.Panel, listmix.ColumnSorterMixin): + + def _init_coll_MainSizer_Items(self, parent): + parent.AddWindow(self.staticText1, 0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW) + parent.AddWindow(self.ServicesList, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW) + parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW) + + def _init_coll_MainSizer_Growables(self, parent): + parent.AddGrowableCol(0) + parent.AddGrowableRow(1) + + def _init_coll_ButtonGridSizer_Items(self, parent): + parent.AddWindow(self.RefreshButton, 0, border=0, flag=0) + # parent.AddWindow(self.ByIPCheck, 0, border=0, flag=0) + + def _init_coll_ButtonGridSizer_Growables(self, parent): + parent.AddGrowableCol(0) + parent.AddGrowableRow(0) + + def _init_sizers(self): + self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10) + self.ButtonGridSizer = wx.FlexGridSizer(cols=2, hgap=5, rows=1, vgap=0) + + self._init_coll_MainSizer_Items(self.MainSizer) + self._init_coll_MainSizer_Growables(self.MainSizer) + self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer) + self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer) + + self.SetSizer(self.MainSizer) + + def _init_list_ctrl(self): + # Set up list control + listID = wx.NewId() + self.ServicesList = AutoWidthListCtrl( + id=listID, + name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0), + style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL) + self.ServicesList.InsertColumn(0, _('NAME')) + self.ServicesList.InsertColumn(1, _('TYPE')) + self.ServicesList.InsertColumn(2, _('IP')) + self.ServicesList.InsertColumn(3, _('PORT')) + self.ServicesList.SetColumnWidth(0, 150) + self.ServicesList.SetColumnWidth(1, 150) + self.ServicesList.SetColumnWidth(2, 150) + self.ServicesList.SetColumnWidth(3, 150) + self.ServicesList.SetInitialSize(wx.Size(-1, 300)) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=listID) + self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=listID) + + def _init_ctrls(self, prnt): + self.staticText1 = wx.StaticText( + label=_('Services available:'), name='staticText1', parent=self, + pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) + + self.RefreshButton = wx.Button( + label=_('Refresh'), name='RefreshButton', parent=self, + pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) + self.RefreshButton.Bind(wx.EVT_BUTTON, self.OnRefreshButton) + + # self.ByIPCheck = wx.CheckBox(self, label=_("Use IP instead of Service Name")) + # self.ByIPCheck.SetValue(True) + + self._init_sizers() + self.Fit() + + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + self.parent = parent + + self._init_list_ctrl() + listmix.ColumnSorterMixin.__init__(self, 4) + + self._init_ctrls(parent) + + self.itemDataMap = {} + self.nextItemId = 0 + + self.URI = None + self.Browser = None + self.ZeroConfInstance = None + + self.RefreshList() + self.LatestSelection = None + + self.IfacesMonitorState = None + self.IfacesMonitorTimer = wx.Timer(self) + self.IfacesMonitorTimer.Start(2000) + self.Bind(wx.EVT_TIMER, self.IfacesMonitor, self.IfacesMonitorTimer) + + def __del__(self): + self.IfacesMonitorTimer.Stop() + self.Browser.cancel() + self.ZeroConfInstance.close() + + def IfacesMonitor(self, event): + NewState = get_all_addresses(socket.AF_INET) + + if self.IfacesMonitorState != NewState: + if self.IfacesMonitorState is not None: + # refresh only if a new address appeared + for addr in NewState: + if addr not in self.IfacesMonitorState: + self.RefreshList() + break + self.IfacesMonitorState = NewState + event.Skip() + + def RefreshList(self): + self.ServicesList.DeleteAllItems() + if self.Browser is not None: + self.Browser.cancel() + if self.ZeroConfInstance is not None: + self.ZeroConfInstance.close() + self.ZeroConfInstance = Zeroconf() + self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self) + + def OnRefreshButton(self, event): + self.RefreshList() + + # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py + def GetListCtrl(self): + return self.ServicesList + + def getColumnText(self, index, col): + item = self.ServicesList.GetItem(index, col) + return item.GetText() + + def OnItemSelected(self, event): + self.SetURI(event.m_itemIndex) + event.Skip() + + def OnItemActivated(self, event): + self.SetURI(event.m_itemIndex) + self.parent.EndModal(wx.ID_OK) + event.Skip() + +# def SetURI(self, idx): +# connect_type = self.getColumnText(idx, 1) +# connect_address = self.getColumnText(idx, 2) +# connect_port = self.getColumnText(idx, 3) +# +# self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port) + + def SetURI(self, idx): + self.LatestSelection = idx + + def GetURI(self): + if self.LatestSelection is not None: + # if self.ByIPCheck.IsChecked(): + svcname, scheme, host, port = \ + map(lambda col: self.getColumnText(self.LatestSelection, col), + range(4)) + return ("%s://%s:%s#%s" % (scheme, host, port, svcname)) \ + if scheme[-1] == "S" \ + else ("%s://%s:%s" % (scheme, host, port)) + # else: + # svcname = self.getColumnText(self.LatestSelection, 0) + # connect_type = self.getColumnText(self.LatestSelection, 1) + # return str("MDNS://%s" % svcname) + return None + + def remove_service(self, zeroconf, _type, name): + wx.CallAfter(self._removeService, name) + + def _removeService(self, name): + ''' + called when a service with the desired type goes offline. + ''' + + # loop through the list items looking for the service that went offline + for idx in xrange(self.ServicesList.GetItemCount()): + # this is the unique identifier assigned to the item + item_id = self.ServicesList.GetItemData(idx) + + # this is the full typename that was received by addService + item_name = self.itemDataMap[item_id][4] + + if item_name == name: + self.ServicesList.DeleteItem(idx) + break + + def add_service(self, zeroconf, _type, name): + wx.CallAfter(self._addService, _type, name) + + def _addService(self, _type, name): + ''' + called when a service with the desired type is discovered. + ''' + info = self.ZeroConfInstance.get_service_info(_type, name) + if info is None: + return + svcname = name.split(".")[0] + typename = info.properties.get("protocol", None) + ip = str(socket.inet_ntoa(info.address)) + port = info.port + + num_items = self.ServicesList.GetItemCount() + + # display the new data in the list + new_item = self.ServicesList.InsertStringItem(num_items, svcname) + self.ServicesList.SetStringItem(new_item, 1, "%s" % typename) + self.ServicesList.SetStringItem(new_item, 2, "%s" % ip) + self.ServicesList.SetStringItem(new_item, 3, "%s" % port) + + # record the new data for the ColumnSorterMixin + # we assign every list item a unique id (that won't change when items + # are added or removed) + self.ServicesList.SetItemData(new_item, self.nextItemId) + + # the value of each column has to be stored in the itemDataMap + # so that ColumnSorterMixin knows how to sort the column. + + # "name" is included at the end so that self.removeService + # can access it. + self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name] + + self.nextItemId += 1 diff -r 3a1c0c161f80 -r 48ebcbe7f19b controls/IDBrowser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/controls/IDBrowser.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import +import wx +import wx.dataview as dv +import PSKManagement as PSK +from PSKManagement import * +from dialogs.IDMergeDialog import IDMergeDialog + + +class IDBrowserModel(dv.PyDataViewIndexListModel): + def __init__(self, project_path, columncount): + self.project_path = project_path + self.columncount = columncount + self.data = PSK.GetData(project_path) + dv.PyDataViewIndexListModel.__init__(self, len(self.data)) + + def _saveData(self): + PSK.SaveData(self.project_path, self.data) + + def GetColumnType(self, col): + return "string" + + def GetValueByRow(self, row, col): + return self.data[row][col] + + def SetValueByRow(self, value, row, col): + self.data[row][col] = value + self._saveData() + + def GetColumnCount(self): + return len(self.data[0]) if self.data else self.columncount + + def GetCount(self): + return len(self.data) + + def Compare(self, item1, item2, col, ascending): + if not ascending: # swap sort order? + item2, item1 = item1, item2 + row1 = self.GetRow(item1) + row2 = self.GetRow(item2) + if col == 0: + return cmp(int(self.data[row1][col]), int(self.data[row2][col])) + else: + return cmp(self.data[row1][col], self.data[row2][col]) + + def DeleteRows(self, rows): + rows = list(rows) + rows.sort(reverse=True) + + for row in rows: + PSK.DeleteID(self.project_path, self.data[row][COL_ID]) + del self.data[row] + self.RowDeleted(row) + self._saveData() + + def AddRow(self, value): + self.data.append(value) + self.RowAppended() + self._saveData() + + def Import(self, filepath, sircb): + data = PSK.ImportIDs(self.project_path, filepath, sircb) + if data is not None: + self.data = data + self.Reset(len(self.data)) + + def Export(self, filepath): + PSK.ExportIDs(self.project_path, filepath) + + +colflags = dv.DATAVIEW_COL_RESIZABLE | dv.DATAVIEW_COL_SORTABLE + + +class IDBrowser(wx.Panel): + def __init__(self, parent, ctr, SelectURICallBack=None, SelectIDCallBack=None, **kwargs): + big = self.isManager = SelectURICallBack is None and SelectIDCallBack is None + wx.Panel.__init__(self, parent, -1, size=(800 if big else 450, + 600 if big else 200)) + + self.SelectURICallBack = SelectURICallBack + self.SelectIDCallBack = SelectIDCallBack + + dvStyle = wx.BORDER_THEME | dv.DV_ROW_LINES + if self.isManager: + # no multiple selection in selector mode + dvStyle |= dv.DV_MULTIPLE + self.dvc = dv.DataViewCtrl(self, style=dvStyle) + + def args(*a, **k): + return (a, k) + + ColumnsDesc = [ + args(_("ID"), COL_ID, width=70), + args(_("Last URI"), COL_URI, width=300 if big else 80), + args(_("Description"), COL_DESC, width=300 if big else 200, + mode=dv.DATAVIEW_CELL_EDITABLE + if self.isManager + else dv.DATAVIEW_CELL_INERT), + args(_("Last connection"), COL_LAST, width=120), + ] + + self.model = IDBrowserModel(ctr.ProjectPath, len(ColumnsDesc)) + self.dvc.AssociateModel(self.model) + + col_list = [] + for a, k in ColumnsDesc: + col_list.append( + self.dvc.AppendTextColumn(*a, **dict(k, flags=colflags))) + col_list[COL_LAST].SetSortOrder(False) + + # TODO : sort by last bvisit by default + + self.Sizer = wx.BoxSizer(wx.VERTICAL) + self.Sizer.Add(self.dvc, 1, wx.EXPAND) + + btnbox = wx.BoxSizer(wx.HORIZONTAL) + if self.isManager: + + # deletion of secret and metadata + deleteButton = wx.Button(self, label=_("Delete ID")) + self.Bind(wx.EVT_BUTTON, self.OnDeleteButton, deleteButton) + btnbox.Add(deleteButton, 0, wx.LEFT | wx.RIGHT, 5) + + # export all + exportButton = wx.Button(self, label=_("Export all")) + self.Bind(wx.EVT_BUTTON, self.OnExportButton, exportButton) + btnbox.Add(exportButton, 0, wx.LEFT | wx.RIGHT, 5) + + # import with a merge -> duplicates are asked for + importButton = wx.Button(self, label=_("Import")) + self.Bind(wx.EVT_BUTTON, self.OnImportButton, importButton) + btnbox.Add(importButton, 0, wx.LEFT | wx.RIGHT, 5) + + else: + # selector mode + self.useURIButton = wx.Button(self, label=_("Use last URI")) + self.Bind(wx.EVT_BUTTON, self.OnUseURIButton, self.useURIButton) + self.useURIButton.Disable() + btnbox.Add(self.useURIButton, 0, wx.LEFT | wx.RIGHT, 5) + + self.Sizer.Add(btnbox, 0, wx.TOP | wx.BOTTOM, 5) + self.Bind(dv.EVT_DATAVIEW_SELECTION_CHANGED, self.OnSelectionChanged, self.dvc) + + def OnDeleteButton(self, evt): + items = self.dvc.GetSelections() + rows = [self.model.GetRow(item) for item in items] + + # Ask if user really wants to delete + if wx.MessageBox(_('Are you sure to delete selected IDs?'), + _('Delete IDs'), + wx.YES_NO | wx.CENTRE | wx.NO_DEFAULT) != wx.YES: + return + + self.model.DeleteRows(rows) + + def OnSelectionChanged(self, evt): + if not self.isManager: + items = self.dvc.GetSelections() + somethingSelected = len(items) > 0 + self.useURIButton.Enable(somethingSelected) + if somethingSelected: + row = self.model.GetRow(items[0]) + ID = self.model.GetValueByRow(row, COL_ID) + self.SelectIDCallBack(ID) + + def OnUseURIButton(self, evt): + row = self.model.GetRow(self.dvc.GetSelections()[0]) + URI = self.model.GetValueByRow(row, COL_URI) + if URI: + self.SelectURICallBack(URI) + + def OnExportButton(self, evt): + dialog = wx.FileDialog(self, _("Choose a file"), + wildcard=_("PSK ZIP files (*.zip)|*.zip"), + style=wx.SAVE | wx.OVERWRITE_PROMPT) + if dialog.ShowModal() == wx.ID_OK: + self.model.Export(dialog.GetPath()) + + # pylint: disable=unused-variable + def ShouldIReplaceCallback(self, existing, replacement): + ID, URI, DESC, LAST = existing + _ID, _URI, _DESC, _LAST = replacement + dlg = IDMergeDialog( + self, + _("Import IDs"), + (_("Replace information for ID {ID} ?") + "\n\n" + + _("Existing:") + "\n " + + _("Description:") + " {DESC}\n " + + _("Last known URI:") + " {URI}\n " + + _("Last connection:") + " {LAST}\n\n" + + _("Replacement:") + "\n " + + _("Description:") + " {_DESC}\n " + + _("Last known URI:") + " {_URI}\n " + + _("Last connection:") + " {_LAST}\n").format(**locals()), + _("Do the same for following IDs"), + [_("Replace"), _("Keep"), _("Cancel")]) + + answer = dlg.ShowModal() # return value ignored as we have "Ok" only anyhow + if answer == wx.ID_CANCEL: + return CANCEL + + if dlg.OptionChecked(): + if answer == wx.ID_YES: + return REPLACE_ALL + return KEEP_ALL + else: + if answer == wx.ID_YES: + return REPLACE + return KEEP + + def OnImportButton(self, evt): + dialog = wx.FileDialog(self, _("Choose a file"), + wildcard=_("PSK ZIP files (*.zip)|*.zip"), + style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) + if dialog.ShowModal() == wx.ID_OK: + self.model.Import(dialog.GetPath(), + self.ShouldIReplaceCallback) diff -r 3a1c0c161f80 -r 48ebcbe7f19b controls/UriLocationEditor.py --- a/controls/UriLocationEditor.py Tue Mar 05 11:43:20 2019 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -from __future__ import absolute_import - -import wx -from zope.interface import Interface, Attribute -from zope.interface.verify import verifyObject -from connectors import connectors_dialog, ConnectorDialog, GetConnectorFromURI - - -[ID_URIWIZARDDIALOG, ID_URITYPECHOICE] = [wx.NewId() for _init_ctrls in range(2)] - - -class IConnectorPanel(Interface): - """This is interface for panel of seperate connector type""" - uri = Attribute("""uri of connections""") - type = Attribute("""type of connector""") - - def SetURI(uri): # pylint: disable=no-self-argument - """methode for set uri""" - - def GetURI(): # pylint: disable=no-self-argument - """metohde for get uri""" - - -class UriLocationEditor(wx.Dialog): - def _init_ctrls(self, parent): - self.UriTypeChoice = wx.Choice(parent=self, id=ID_URIWIZARDDIALOG, choices=self.URITYPES) - self.UriTypeChoice.SetSelection(0) - self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice) - self.PanelSizer = wx.BoxSizer(wx.HORIZONTAL) - self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) - - def _init_sizers(self): - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - typeSizer = wx.BoxSizer(wx.HORIZONTAL) - typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("URI type:")), border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL) - typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL) - self.mainSizer.Add(typeSizer) - - self.mainSizer.Add(self.PanelSizer, border=5, flag=wx.ALL) - self.mainSizer.Add(self.ButtonSizer, border=5, flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL) - self.SetSizer(self.mainSizer) - self.Layout() - self.Fit() - - def __init__(self, parent, uri): - wx.Dialog.__init__(self, id=ID_URIWIZARDDIALOG, - name='UriLocationEditor', parent=parent, - title='Uri location') - self.URITYPES = [_("- Select URI type -")] - for connector_type, connector_function in connectors_dialog.iteritems(): - try: - connector_function['function']() - self.URITYPES.append(connector_type) - except Exception: - pass - - self.selected = None - self.parrent = parent - self.logger = self.parrent.CTR.logger - self._init_ctrls(parent) - self._init_sizers() - self.SetURI(uri) - self.CenterOnParent() - - def OnTypeChoice(self, event): - self._removePanelType() - index = event.GetSelection() - if index > 0: - self.selected = event.GetString() - self.panelType = self._getConnectorDialog(self.selected) - if self.panelType: - self.PanelSizer.Add(self.panelType) - self.mainSizer.Layout() - self.Fit() - self.panelType.Refresh() - - def SetURI(self, uri): - self._removePanelType() - uri_list = uri.strip().split(":") - if uri_list: - uri_type = uri_list[0].upper() - type = GetConnectorFromURI(uri_type) - if type: - self.selected = type - self.UriTypeChoice.SetStringSelection(self.selected) - self.panelType = self._getConnectorDialog(self.selected) - if self.panelType: - self.panelType.SetURI(uri) - self.PanelSizer.Add(self.panelType) - self.PanelSizer.Layout() - self.mainSizer.Layout() - self.Fit() - self.panelType.Refresh() - - def GetURI(self): - if not self.selected or not self.panelType: - return "" - else: - return self.panelType.GetURI() - - def _removePanelType(self): - for i in range(self.PanelSizer.GetItemCount()): - item = self.PanelSizer.GetItem(i) - item.DeleteWindows() - self.PanelSizer.Remove(i) - self.Fit() - self.PanelSizer.Layout() - - def _getConnectorDialog(self, connectorType): - connector = ConnectorDialog(connectorType, self) - if connector and IConnectorPanel.providedBy(connector): - if verifyObject(IConnectorPanel, connector): - return connector - else: - return None diff -r 3a1c0c161f80 -r 48ebcbe7f19b dialogs/DiscoveryDialog.py --- a/dialogs/DiscoveryDialog.py Tue Mar 05 11:43:20 2019 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,268 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This file is part of Beremiz, a Integrated Development Environment for -# programming IEC 61131-3 automates supporting plcopen standard and CanFestival. -# -# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD -# Copyright (C) 2017: Andrey Skvortsov -# -# See COPYING file for copyrights details. -# -# This program 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 -# of the License, or (at your option) any later version. -# -# This program 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 program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - -from __future__ import absolute_import -import socket -from six.moves import xrange -import wx -import wx.lib.mixins.listctrl as listmix -from zeroconf import ServiceBrowser, Zeroconf - - -service_type = '_PYRO._tcp.local.' - - -class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): - def __init__(self, parent, id, name, pos=wx.DefaultPosition, - size=wx.DefaultSize, style=0): - wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name) - listmix.ListCtrlAutoWidthMixin.__init__(self) - - -[ - ID_DISCOVERYDIALOG, ID_DISCOVERYDIALOGSTATICTEXT1, - ID_DISCOVERYDIALOGSERVICESLIST, ID_DISCOVERYDIALOGREFRESHBUTTON, - ID_DISCOVERYDIALOGLOCALBUTTON, ID_DISCOVERYDIALOGIPBUTTON, -] = [wx.NewId() for _init_ctrls in range(6)] - - -class DiscoveryDialog(wx.Dialog, listmix.ColumnSorterMixin): - - def _init_coll_MainSizer_Items(self, parent): - parent.AddWindow(self.staticText1, 0, border=20, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW) - parent.AddWindow(self.ServicesList, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.GROW) - parent.AddSizer(self.ButtonGridSizer, 0, border=20, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW) - - def _init_coll_MainSizer_Growables(self, parent): - parent.AddGrowableCol(0) - parent.AddGrowableRow(1) - - def _init_coll_ButtonGridSizer_Items(self, parent): - parent.AddWindow(self.RefreshButton, 0, border=0, flag=0) - parent.AddWindow(self.LocalButton, 0, border=0, flag=0) - parent.AddWindow(self.IpButton, 0, border=0, flag=0) - parent.AddSizer(self.ButtonSizer, 0, border=0, flag=0) - - def _init_coll_ButtonGridSizer_Growables(self, parent): - parent.AddGrowableCol(0) - parent.AddGrowableCol(1) - parent.AddGrowableRow(0) - - def _init_sizers(self): - self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=10) - self.ButtonGridSizer = wx.FlexGridSizer(cols=4, hgap=5, rows=1, vgap=0) - - self._init_coll_MainSizer_Items(self.MainSizer) - self._init_coll_MainSizer_Growables(self.MainSizer) - self._init_coll_ButtonGridSizer_Items(self.ButtonGridSizer) - self._init_coll_ButtonGridSizer_Growables(self.ButtonGridSizer) - - self.SetSizer(self.MainSizer) - - def _init_list_ctrl(self): - # Set up list control - self.ServicesList = AutoWidthListCtrl( - id=ID_DISCOVERYDIALOGSERVICESLIST, - name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0), - style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL) - self.ServicesList.InsertColumn(0, _('NAME')) - self.ServicesList.InsertColumn(1, _('TYPE')) - self.ServicesList.InsertColumn(2, _('IP')) - self.ServicesList.InsertColumn(3, _('PORT')) - self.ServicesList.SetColumnWidth(0, 150) - self.ServicesList.SetColumnWidth(1, 150) - self.ServicesList.SetColumnWidth(2, 150) - self.ServicesList.SetColumnWidth(3, 150) - self.ServicesList.SetInitialSize(wx.Size(-1, 300)) - self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, id=ID_DISCOVERYDIALOGSERVICESLIST) - self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, id=ID_DISCOVERYDIALOGSERVICESLIST) - - def _init_ctrls(self, prnt): - self.staticText1 = wx.StaticText( - id=ID_DISCOVERYDIALOGSTATICTEXT1, - label=_('Services available:'), name='staticText1', parent=self, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.RefreshButton = wx.Button( - id=ID_DISCOVERYDIALOGREFRESHBUTTON, - label=_('Refresh'), name='RefreshButton', parent=self, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - self.Bind(wx.EVT_BUTTON, self.OnRefreshButton, id=ID_DISCOVERYDIALOGREFRESHBUTTON) - - self.LocalButton = wx.Button( - id=ID_DISCOVERYDIALOGLOCALBUTTON, - label=_('Local'), name='LocalButton', parent=self, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - self.Bind(wx.EVT_BUTTON, self.OnLocalButton, id=ID_DISCOVERYDIALOGLOCALBUTTON) - - self.IpButton = wx.Button( - id=ID_DISCOVERYDIALOGIPBUTTON, - label=_('Add IP'), name='IpButton', parent=self, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - self.Bind(wx.EVT_BUTTON, self.OnIpButton, id=ID_DISCOVERYDIALOGIPBUTTON) - - self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL | wx.CENTER) - - self._init_sizers() - self.Fit() - - def __init__(self, parent): - wx.Dialog.__init__( - self, id=ID_DISCOVERYDIALOG, - name='DiscoveryDialog', parent=parent, - style=wx.DEFAULT_DIALOG_STYLE, - title=_('Service Discovery')) - - self._init_list_ctrl() - listmix.ColumnSorterMixin.__init__(self, 4) - - self._init_ctrls(parent) - - self.itemDataMap = {} - self.nextItemId = 0 - - self.URI = None - self.Browser = None - - self.ZeroConfInstance = Zeroconf() - self.RefreshList() - self.LatestSelection = None - - def __del__(self): - if self.Browser is not None: - self.Browser.cancel() - self.ZeroConfInstance.close() - - def RefreshList(self): - if self.Browser is not None: - self.Browser.cancel() - self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self) - - def OnRefreshButton(self, event): - self.ServicesList.DeleteAllItems() - self.RefreshList() - - def OnLocalButton(self, event): - self.URI = "LOCAL://" - self.EndModal(wx.ID_OK) - event.Skip() - - def OnIpButton(self, event): - def GetColText(col): - return self.getColumnText(self.LatestSelection, col) - - if self.LatestSelection is not None: - self.URI = "%s://%s:%s" % tuple(map(GetColText, (1, 2, 3))) - self.EndModal(wx.ID_OK) - event.Skip() - - # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py - def GetListCtrl(self): - return self.ServicesList - - def getColumnText(self, index, col): - item = self.ServicesList.GetItem(index, col) - return item.GetText() - - def OnItemSelected(self, event): - self.SetURI(event.m_itemIndex) - event.Skip() - - def OnItemActivated(self, event): - self.SetURI(event.m_itemIndex) - self.EndModal(wx.ID_OK) - event.Skip() - -# def SetURI(self, idx): -# connect_type = self.getColumnText(idx, 1) -# connect_address = self.getColumnText(idx, 2) -# connect_port = self.getColumnText(idx, 3) -# -# self.URI = "%s://%s:%s"%(connect_type, connect_address, connect_port) - - def SetURI(self, idx): - self.LatestSelection = idx - svcname = self.getColumnText(idx, 0) - connect_type = self.getColumnText(idx, 1) - self.URI = "%s://%s" % (connect_type, svcname + '.' + service_type) - - def GetURI(self): - return self.URI - - def remove_service(self, zeroconf, _type, name): - wx.CallAfter(self._removeService, name) - - def _removeService(self, name): - ''' - called when a service with the desired type goes offline. - ''' - - # loop through the list items looking for the service that went offline - for idx in xrange(self.ServicesList.GetItemCount()): - # this is the unique identifier assigned to the item - item_id = self.ServicesList.GetItemData(idx) - - # this is the full typename that was received by addService - item_name = self.itemDataMap[item_id][4] - - if item_name == name: - self.ServicesList.DeleteItem(idx) - break - - def add_service(self, zeroconf, _type, name): - wx.CallAfter(self._addService, _type, name) - - def _addService(self, _type, name): - ''' - called when a service with the desired type is discovered. - ''' - info = self.ZeroConfInstance.get_service_info(_type, name) - svcname = name.split(".")[0] - typename = _type.split(".")[0][1:] - ip = str(socket.inet_ntoa(info.address)) - port = info.port - - num_items = self.ServicesList.GetItemCount() - - # display the new data in the list - new_item = self.ServicesList.InsertStringItem(num_items, svcname) - self.ServicesList.SetStringItem(new_item, 1, "%s" % typename) - self.ServicesList.SetStringItem(new_item, 2, "%s" % ip) - self.ServicesList.SetStringItem(new_item, 3, "%s" % port) - - # record the new data for the ColumnSorterMixin - # we assign every list item a unique id (that won't change when items - # are added or removed) - self.ServicesList.SetItemData(new_item, self.nextItemId) - - # the value of each column has to be stored in the itemDataMap - # so that ColumnSorterMixin knows how to sort the column. - - # "name" is included at the end so that self.removeService - # can access it. - self.itemDataMap[self.nextItemId] = [svcname, typename, ip, port, name] - - self.nextItemId += 1 diff -r 3a1c0c161f80 -r 48ebcbe7f19b dialogs/IDManager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogs/IDManager.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,24 @@ +from __future__ import absolute_import + +import wx +from controls.IDBrowser import IDBrowser + + +class IDManager(wx.Dialog): + def __init__(self, parent, ctr): + self.ctr = ctr + wx.Dialog.__init__(self, + name='IDManager', parent=parent, + title=_('URI Editor'), + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + size=(800, 600)) + # start IDBrowser in manager mode + self.browser = IDBrowser(self, ctr) + self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey) + + def OnEscapeKey(self, event): + keycode = event.GetKeyCode() + if keycode == wx.WXK_ESCAPE: + self.EndModal(wx.ID_CANCEL) + else: + event.Skip() diff -r 3a1c0c161f80 -r 48ebcbe7f19b dialogs/IDMergeDialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogs/IDMergeDialog.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + +from __future__ import absolute_import +import wx + + +# class RichMessageDialog is still not available in wxPython 3.0.2 +class IDMergeDialog(wx.Dialog): + def __init__(self, parent, title, question, optiontext, button_texts): + wx.Dialog.__init__(self, parent, title=title) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + + message = wx.StaticText(self, label=question) + main_sizer.AddWindow(message, border=20, + flag=wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.LEFT | wx.RIGHT) + + self.check = wx.CheckBox(self, label=optiontext) + main_sizer.AddWindow(self.check, border=20, + flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL) + + buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + for label, wxID in zip(button_texts, [wx.ID_YES, wx.ID_NO, wx.ID_CANCEL]): + Button = wx.Button(self, label=label) + + def OnButtonFactory(_wxID): + return lambda event: self.EndModal(_wxID) + + self.Bind(wx.EVT_BUTTON, OnButtonFactory(wxID), Button) + buttons_sizer.AddWindow(Button) + + main_sizer.AddSizer(buttons_sizer, border=20, + flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.ALIGN_RIGHT) + + self.SetSizer(main_sizer) + self.Fit() + + self.Bind(wx.EVT_CHAR_HOOK, self.OnEscapeKey) + + def OnEscapeKey(self, event): + keycode = event.GetKeyCode() + if keycode == wx.WXK_ESCAPE: + self.EndModal(wx.ID_CANCEL) + else: + event.Skip() + + def OptionChecked(self): + return self.check.GetValue() diff -r 3a1c0c161f80 -r 48ebcbe7f19b dialogs/UriEditor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogs/UriEditor.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,92 @@ +from __future__ import absolute_import + +import wx +from connectors import ConnectorSchemes, EditorClassFromScheme +from controls.DiscoveryPanel import DiscoveryPanel + + +class UriEditor(wx.Dialog): + def _init_ctrls(self, parent): + self.UriTypeChoice = wx.Choice(parent=self, choices=self.choices) + self.UriTypeChoice.SetSelection(0) + self.Bind(wx.EVT_CHOICE, self.OnTypeChoice, self.UriTypeChoice) + self.editor_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.ButtonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) + + def _init_sizers(self): + self.mainSizer = wx.BoxSizer(wx.VERTICAL) + typeSizer = wx.BoxSizer(wx.HORIZONTAL) + typeSizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scheme :")), border=5, + flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL) + typeSizer.Add(self.UriTypeChoice, border=5, flag=wx.ALL) + self.mainSizer.Add(typeSizer) + + self.mainSizer.Add(self.editor_sizer, border=5, flag=wx.ALL) + self.mainSizer.Add(self.ButtonSizer, border=5, + flag=wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL) + self.SetSizer(self.mainSizer) + self.Layout() + self.Fit() + + def __init__(self, parent, ctr, uri=''): + self.ctr = ctr + wx.Dialog.__init__(self, + name='UriEditor', parent=parent, + title=_('URI Editor')) + self.choices = [_("- Search local network -")] + ConnectorSchemes() + self._init_ctrls(parent) + self._init_sizers() + self.scheme = None + self.scheme_editor = None + self.SetURI(uri) + self.CenterOnParent() + + def OnTypeChoice(self, event): + index = event.GetSelection() + self._replaceSchemeEditor(event.GetString() if index > 0 else None) + + def SetURI(self, uri): + try: + scheme, loc = uri.strip().split("://", 1) + scheme = scheme.upper() + except Exception: + scheme = None + + if scheme in ConnectorSchemes(): + self.UriTypeChoice.SetStringSelection(scheme) + else: + self.UriTypeChoice.SetSelection(0) + scheme = None + + self._replaceSchemeEditor(scheme) + + if scheme is not None: + self.scheme_editor.SetLoc(loc) + + def GetURI(self): + if self.scheme is None: + return self.scheme_editor.GetURI() + else: + return self.scheme+"://"+self.scheme_editor.GetLoc() + + def _replaceSchemeEditor(self, scheme): + self.scheme = scheme + + if self.scheme_editor is not None: + self.editor_sizer.Detach(self.scheme_editor) + self.scheme_editor.Destroy() + self.scheme_editor = None + + if scheme is not None: + EditorClass = EditorClassFromScheme(scheme) + self.scheme_editor = EditorClass(scheme, self) + else: + # None is for searching local network + self.scheme_editor = DiscoveryPanel(self) + + self.editor_sizer.Add(self.scheme_editor) + self.scheme_editor.Refresh() + + self.editor_sizer.Layout() + self.mainSizer.Layout() + self.Fit() diff -r 3a1c0c161f80 -r 48ebcbe7f19b dialogs/__init__.py --- a/dialogs/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/dialogs/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -49,4 +49,5 @@ from dialogs.PouActionDialog import PouActionDialog from dialogs.FindInPouDialog import FindInPouDialog from dialogs.BrowseValuesLibraryDialog import BrowseValuesLibraryDialog -from dialogs.DiscoveryDialog import DiscoveryDialog +from dialogs.UriEditor import UriEditor +from dialogs.IDManager import IDManager diff -r 3a1c0c161f80 -r 48ebcbe7f19b editors/ConfTreeNodeEditor.py --- a/editors/ConfTreeNodeEditor.py Tue Mar 05 11:43:20 2019 +0300 +++ b/editors/ConfTreeNodeEditor.py Mon Mar 11 13:51:07 2019 +0100 @@ -33,8 +33,8 @@ from IDEFrame import TITLE, FILEMENU, PROJECTTREE, PAGETITLES -from controls import TextCtrlAutoComplete, UriLocationEditor -from dialogs import BrowseValuesLibraryDialog +from controls import TextCtrlAutoComplete +from dialogs import BrowseValuesLibraryDialog, UriEditor from util.BitmapLibrary import GetBitmap if wx.Platform == '__WXMSW__': @@ -345,7 +345,7 @@ # Get connector uri uri = CTR_BeremizRoot.getURI_location().strip() - dialog = UriLocationEditor.UriLocationEditor(CTR_AppFrame, uri) + dialog = UriEditor(CTR_AppFrame, CTR, uri) if dialog.ShowModal() == wx.ID_OK: CTR_BeremizRoot.setURI_location(dialog.GetURI()) diff -r 3a1c0c161f80 -r 48ebcbe7f19b etherlab/plc_cia402node.c --- a/etherlab/plc_cia402node.c Tue Mar 05 11:43:20 2019 +0300 +++ b/etherlab/plc_cia402node.c Mon Mar 11 13:51:07 2019 +0100 @@ -74,6 +74,31 @@ static IEC_BOOL __FirstTick = 1; +typedef enum { + mc_mode_none, // No motion mode + mc_mode_csp, // Continuous Synchronous Positionning mode + mc_mode_csv, // Continuous Synchronous Velocity mode + mc_mode_cst, // Continuous Synchronous Torque mode +} mc_axismotionmode_enum; + +typedef struct { + IEC_BOOL Power; + IEC_BOOL CommunicationReady; + IEC_UINT NetworkPosition; + IEC_BOOL ReadyForPowerOn; + IEC_BOOL PowerFeedback; + IEC_DINT ActualRawPosition; + IEC_DINT ActualRawVelocity; + IEC_DINT ActualRawTorque; + IEC_DINT RawPositionSetPoint; + IEC_DINT RawVelocitySetPoint; + IEC_DINT RawTorqueSetPoint; + mc_axismotionmode_enum AxisMotionMode; + IEC_LREAL ActualVelocity; + IEC_LREAL ActualPosition; + IEC_LREAL ActualTorque; +}axis_s; + typedef struct { %(entry_variables)s axis_s* axis; diff -r 3a1c0c161f80 -r 48ebcbe7f19b images/IDManager.png Binary file images/IDManager.png has changed diff -r 3a1c0c161f80 -r 48ebcbe7f19b images/icons.svg --- a/images/icons.svg Tue Mar 05 11:43:20 2019 +0300 +++ b/images/icons.svg Mon Mar 11 13:51:07 2019 +0100 @@ -15,7 +15,7 @@ height="1052.3622" id="svg2" sodipodi:version="0.32" - inkscape:version="0.92.3 (2405546, 2018-03-11)" + inkscape:version="0.91 r13725" sodipodi:docname="icons.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape"> %% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug %% + style="font-size:12.76095104px;line-height:1.25">%% editIECrawcode editWXGLADE editPYTHONcode EditCfile Transfer Connect Disconnect Debug IDManager %% %% fullscreen %% + + + + + + diff -r 3a1c0c161f80 -r 48ebcbe7f19b modbus/mb_runtime.c --- a/modbus/mb_runtime.c Tue Mar 05 11:43:20 2019 +0300 +++ b/modbus/mb_runtime.c Mon Mar 11 13:51:07 2019 +0100 @@ -1,614 +1,614 @@ -/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */ - -/* - * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt) - * - * This file is part of the Modbus library for Beremiz and matiec. - * - * This Modbus library is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 Lesser - * General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this Modbus library. If not, see . - * - * This code is made available on the understanding that it will not be - * used in safety-critical situations without a full and competent review. - */ - - -#include -#include /* required for memcpy() */ -#include "mb_slave_and_master.h" -#include "MB_%(locstr)s.h" - - -#define MAX_MODBUS_ERROR_CODE 11 -static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = { - /* 0 */ "", /* un-used -> no error! */ - /* 1 */ "illegal/unsuported function", - /* 2 */ "illegal data address", - /* 3 */ "illegal data value", - /* 4 */ "slave device failure", - /* 5 */ "acknowledge -> slave intends to reply later", - /* 6 */ "slave device busy", - /* 7 */ "negative acknowledge", - /* 8 */ "memory parity error", - /* 9 */ "", /* undefined by Modbus */ - /* 10*/ "gateway path unavalilable", - /* 11*/ "gateway target device failed to respond" -}; - - -/* Execute a modbus client transaction/request */ -static int __execute_mb_request(int request_id){ - switch (client_requests[request_id].mb_function){ - - case 1: /* read coils */ - return read_output_bits(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - (int) client_requests[request_id].count, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 2: /* read discrete inputs */ - return read_input_bits( client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - (int) client_requests[request_id].count, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 3: /* read holding registers */ - return read_output_words(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - (int) client_requests[request_id].count, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 4: /* read input registers */ - return read_input_words(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - (int) client_requests[request_id].count, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 5: /* write single coil */ - return write_output_bit(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].coms_buffer[0], - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 6: /* write single register */ - return write_output_word(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].coms_buffer[0], - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 7: break; /* function not yet supported */ - case 8: break; /* function not yet supported */ - case 9: break; /* function not yet supported */ - case 10: break; /* function not yet supported */ - case 11: break; /* function not yet supported */ - case 12: break; /* function not yet supported */ - case 13: break; /* function not yet supported */ - case 14: break; /* function not yet supported */ - - case 15: /* write multiple coils */ - return write_output_bits(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - case 16: /* write multiple registers */ - return write_output_words(client_requests[request_id].slave_id, - client_requests[request_id].address, - client_requests[request_id].count, - client_requests[request_id].coms_buffer, - client_nodes[client_requests[request_id].client_node_id].mb_nd, - client_requests[request_id].retries, - &(client_requests[request_id].error_code), - &(client_requests[request_id].resp_timeout), - &(client_requests[request_id].coms_buf_mutex)); - - default: break; /* should never occur, if file generation is correct */ - } - - fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */ - return -1; -} - - - -/* pack bits from unpacked_data to packed_data */ -static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) { - u8 bit; - u16 byte, coils_processed; - - if ((0 == bit_count) || (65535-start_addr < bit_count-1)) - return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ - - for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) { - packed_data[byte] = 0; - for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) { - if(unpacked_data[start_addr + coils_processed]) - packed_data[byte] |= bit; /* set bit */ - else packed_data[byte] &= ~bit; /* reset bit */ - } - } - return 0; -} - - -/* unpack bits from packed_data to unpacked_data */ -static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) { - u8 temp, bit; - u16 byte, coils_processed; - - if ((0 == bit_count) || (65535-start_addr < bit_count-1)) - return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ - - for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) { - temp = packed_data[byte] ; - for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) { - unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0; - } - } - return 0; -} - - -static int __read_inbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) - {return __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);} -static int __read_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) - {return __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);} -static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) - {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); } - - - -static int __read_inwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { - - if ((start_addr + word_count) > MEM_AREA_SIZE) - return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ - - /* use memcpy() because loop with pointers (u16 *) caused alignment problems */ - memcpy(/* dest */ (void *)data_words, - /* src */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]), - /* size */ word_count * 2); - return 0; -} - - - -static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { - - if ((start_addr + word_count) > MEM_AREA_SIZE) - return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ - - /* use memcpy() because loop with pointers (u16 *) caused alignment problems */ - memcpy(/* dest */ (void *)data_words, - /* src */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]), - /* size */ word_count * 2); - return 0; -} - - - - -static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { - - if ((start_addr + word_count) > MEM_AREA_SIZE) - return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ - - /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned. - * It is not therefore safe to cast it to an u16 data type. - * The following code cannot be used. memcpy() is used instead. - */ - /* - for (count = 0; count < word_count ; count++) - ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count]; - */ - memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]), - /* src */ (void *)data_words, - /* size */ word_count * 2); - return 0; -} - - - - -#include - -static void *__mb_server_thread(void *_server_node) { - server_node_t *server_node = _server_node; - mb_slave_callback_t callbacks = { - &__read_inbits, - &__read_outbits, - &__write_outbits, - &__read_inwords, - &__read_outwords, - &__write_outwords, - (void *)&(server_node->mem_area) - }; - - // Enable thread cancelation. Enabled is default, but set it anyway to be safe. - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - - // mb_slave_run() should never return! - mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id); - fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */ - return NULL; -} - - -#define timespec_add(ts, sec, nsec) { \ - ts.tv_sec += sec; \ - ts.tv_nsec += nsec; \ - if (ts.tv_nsec >= 1000000000) { \ - ts.tv_sec ++; \ - ts.tv_nsec -= 1000000000; \ - } \ -} - - -static void *__mb_client_thread(void *_index) { - int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast) - struct timespec next_cycle; - int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */ - int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */ - - // Enable thread cancelation. Enabled is default, but set it anyway to be safe. - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - - // get the current time - clock_gettime(CLOCK_MONOTONIC, &next_cycle); - - // loop the communication with the client - while (1) { - /* - struct timespec cur_time; - clock_gettime(CLOCK_MONOTONIC, &cur_time); - fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec); - */ - int req; - for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){ - /*just do the requests belonging to the client */ - if (client_requests[req].client_node_id != client_node_id) - continue; - int res_tmp = __execute_mb_request(req); - switch (res_tmp) { - case PORT_FAILURE: { - if (res_tmp != client_nodes[client_node_id].prev_error) - fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location); - client_nodes[client_node_id].prev_error = res_tmp; - break; - } - case INVALID_FRAME: { - if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error)) - fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location); - client_requests[req].prev_error = res_tmp; - break; - } - case TIMEOUT: { - if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error)) - fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location); - client_requests[req].prev_error = res_tmp; - break; - } - case MODBUS_ERROR: { - if (client_requests[req].prev_error != client_requests[req].error_code) { - fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code); - if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) { - fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]); - fprintf(stderr, ".\n"); - } - } - client_requests[req].prev_error = client_requests[req].error_code; - break; - } - default: { - if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) { - fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location); - } - if ((res_tmp >= 0) && (client_requests[req] .prev_error != 0)) { - fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location); - } - client_nodes[client_node_id].prev_error = 0; - client_requests[req] .prev_error = 0; - break; - } - } - } - // Determine absolute time instant for starting the next cycle - struct timespec prev_cycle, now; - prev_cycle = next_cycle; - timespec_add(next_cycle, period_sec, period_nsec); - /* NOTE A: - * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will - * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously - * for some time until we catch up. - * This is undesirable, so we detect it by making sure the next_cycle will start in the future. - * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval. - * - * NOTE B: - * It probably does not make sense to check for overflow of timer - so we don't do it for now! - * Even in 32 bit systems this will take at least 68 years since the computer booted - * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0 - * every time the system boots). On 64 bit systems, it will take over - * 10^11 years to overflow. - */ - clock_gettime(CLOCK_MONOTONIC, &now); - if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec))) - /* We are falling behind. See NOTE A above */ - || (next_cycle.tv_sec < prev_cycle.tv_sec) - /* Timer overflow. See NOTE B above */ - ) { - next_cycle = now; - timespec_add(next_cycle, period_sec, period_nsec); - } - - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL); - } - - // humour the compiler. - return NULL; -} - - -int __cleanup_%(locstr)s (); -int __init_%(locstr)s (int argc, char **argv){ - int index; - - for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) - client_nodes[index].mb_nd = -1; - for (index=0; index < NUMBER_OF_SERVER_NODES;index++) - // mb_nd with negative numbers indicate how far it has been initialised (or not) - // -2 --> no modbus node created; no thread created - // -1 --> modbus node created!; no thread created - // >=0 --> modbus node created!; thread created! - server_nodes[index].mb_nd = -2; - - /* modbus library init */ - /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus - * extension currently in the user's project. This file (MB_xx.c) is handling only one instance, - * but must initialize the library for all instances. Only the first call to mb_slave_and_master_init() - * will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored - * by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments. - */ - if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) { - fprintf(stderr, "Modbus plugin: Error starting modbus library\n"); - // return imediately. Do NOT goto error_exit, as we did not get to - // start the modbus library! - return -1; - } - - /* init the mutex for each client request */ - /* Must be done _before_ launching the client threads!! */ - for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ - if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) { - fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location); - goto error_exit; - } - } - - /* init each client connection to remote modbus server, and launch thread */ - /* NOTE: All client_nodes[].init_state are initialised to 0 in the code - * generated by the modbus plugin - */ - for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){ - /* establish client connection */ - client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address); - if (client_nodes[index].mb_nd < 0){ - fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location); - goto error_exit; - } - client_nodes[index].init_state = 1; // we have created the node - - /* launch a thread to handle this client node */ - { - int res = 0; - pthread_attr_t attr; - res |= pthread_attr_init(&attr); - res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index)); - if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location); - goto error_exit; - } - } - client_nodes[index].init_state = 2; // we have created the node and a thread - } - - /* init each local server */ - /* NOTE: All server_nodes[].init_state are initialised to 0 in the code - * generated by the modbus plugin - */ - for (index=0; index < NUMBER_OF_SERVER_NODES;index++){ - /* create the modbus server */ - server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address); - if (server_nodes[index].mb_nd < 0){ - fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location); - goto error_exit; - } - server_nodes[index].init_state = 1; // we have created the node - - /* launch a thread to handle this server node */ - { - int res = 0; - pthread_attr_t attr; - res |= pthread_attr_init(&attr); - res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index])); - if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location); - goto error_exit; - } - } - server_nodes[index].init_state = 2; // we have created the node and thread - } - - return 0; - -error_exit: - __cleanup_%(locstr)s (); - return -1; -} - - - - - -void __publish_%(locstr)s (){ - int index; - - for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ - /*just do the output requests */ - if (client_requests[index].req_type == req_output){ - if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){ - // copy from plcv_buffer to coms_buffer - memcpy((void *)client_requests[index].coms_buffer /* destination */, - (void *)client_requests[index].plcv_buffer /* source */, - REQ_BUF_SIZE * sizeof(u16) /* size in bytes */); - pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); - } - } - } -} - - - - - -void __retrieve_%(locstr)s (){ - int index; - - for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ - /*just do the input requests */ - if (client_requests[index].req_type == req_input){ - if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){ - // copy from coms_buffer to plcv_buffer - memcpy((void *)client_requests[index].plcv_buffer /* destination */, - (void *)client_requests[index].coms_buffer /* source */, - REQ_BUF_SIZE * sizeof(u16) /* size in bytes */); - pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); - } - } - } -} - - - - - -int __cleanup_%(locstr)s (){ - int index, close; - int res = 0; - - /* kill thread and close connections of each modbus client node */ - for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) { - close = 0; - if (client_nodes[index].init_state >= 2) { - // thread was launched, so we try to cancel it! - close = pthread_cancel(client_nodes[index].thread_id); - close |= pthread_join (client_nodes[index].thread_id, NULL); - if (close < 0) - fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location); - } - res |= close; - - close = 0; - if (client_nodes[index].init_state >= 1) { - // modbus client node was created, so we try to close it! - close = mb_master_close (client_nodes[index].mb_nd); - if (close < 0){ - fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location); - // We try to shut down as much as possible, so we do not return noW! - } - client_nodes[index].mb_nd = -1; - } - res |= close; - client_nodes[index].init_state = 0; - } - - /* kill thread and close connections of each modbus server node */ - for (index=0; index < NUMBER_OF_SERVER_NODES; index++) { - close = 0; - if (server_nodes[index].init_state >= 2) { - // thread was launched, so we try to cancel it! - close = pthread_cancel(server_nodes[index].thread_id); - close |= pthread_join (server_nodes[index].thread_id, NULL); - if (close < 0) - fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location); - } - res |= close; - - close = 0; - if (server_nodes[index].init_state >= 1) { - // modbus server node was created, so we try to close it! - close = mb_slave_close (server_nodes[index].mb_nd); - if (close < 0) { - fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd); - // We try to shut down as much as possible, so we do not return noW! - } - server_nodes[index].mb_nd = -1; - } - res |= close; - server_nodes[index].init_state = 0; - } - - /* destroy the mutex of each client request */ - for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) { - if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) { - fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location); - // We try to shut down as much as possible, so we do not return noW! - res |= -1; - } - } - - /* modbus library close */ - //fprintf(stderr, "Shutting down modbus library...\n"); - if (mb_slave_and_master_done()<0) { - fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n"); - res |= -1; - } - - return res; -} - +/* File generated by Beremiz (PlugGenerate_C method of Modbus plugin) */ + +/* + * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt) + * + * This file is part of the Modbus library for Beremiz and matiec. + * + * This Modbus library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this Modbus library. If not, see . + * + * This code is made available on the understanding that it will not be + * used in safety-critical situations without a full and competent review. + */ + + +#include +#include /* required for memcpy() */ +#include "mb_slave_and_master.h" +#include "MB_%(locstr)s.h" + + +#define MAX_MODBUS_ERROR_CODE 11 +static const char *modbus_error_messages[MAX_MODBUS_ERROR_CODE+1] = { + /* 0 */ "", /* un-used -> no error! */ + /* 1 */ "illegal/unsuported function", + /* 2 */ "illegal data address", + /* 3 */ "illegal data value", + /* 4 */ "slave device failure", + /* 5 */ "acknowledge -> slave intends to reply later", + /* 6 */ "slave device busy", + /* 7 */ "negative acknowledge", + /* 8 */ "memory parity error", + /* 9 */ "", /* undefined by Modbus */ + /* 10*/ "gateway path unavalilable", + /* 11*/ "gateway target device failed to respond" +}; + + +/* Execute a modbus client transaction/request */ +static int __execute_mb_request(int request_id){ + switch (client_requests[request_id].mb_function){ + + case 1: /* read coils */ + return read_output_bits(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + (int) client_requests[request_id].count, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 2: /* read discrete inputs */ + return read_input_bits( client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + (int) client_requests[request_id].count, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 3: /* read holding registers */ + return read_output_words(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + (int) client_requests[request_id].count, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 4: /* read input registers */ + return read_input_words(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + (int) client_requests[request_id].count, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 5: /* write single coil */ + return write_output_bit(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].coms_buffer[0], + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 6: /* write single register */ + return write_output_word(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].coms_buffer[0], + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 7: break; /* function not yet supported */ + case 8: break; /* function not yet supported */ + case 9: break; /* function not yet supported */ + case 10: break; /* function not yet supported */ + case 11: break; /* function not yet supported */ + case 12: break; /* function not yet supported */ + case 13: break; /* function not yet supported */ + case 14: break; /* function not yet supported */ + + case 15: /* write multiple coils */ + return write_output_bits(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + case 16: /* write multiple registers */ + return write_output_words(client_requests[request_id].slave_id, + client_requests[request_id].address, + client_requests[request_id].count, + client_requests[request_id].coms_buffer, + client_nodes[client_requests[request_id].client_node_id].mb_nd, + client_requests[request_id].retries, + &(client_requests[request_id].error_code), + &(client_requests[request_id].resp_timeout), + &(client_requests[request_id].coms_buf_mutex)); + + default: break; /* should never occur, if file generation is correct */ + } + + fprintf(stderr, "Modbus plugin: Modbus function %%d not supported\n", request_id); /* should never occur, if file generation is correct */ + return -1; +} + + + +/* pack bits from unpacked_data to packed_data */ +static inline int __pack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) { + u8 bit; + u16 byte, coils_processed; + + if ((0 == bit_count) || (65535-start_addr < bit_count-1)) + return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ + + for( byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) { + packed_data[byte] = 0; + for( bit = 0x01; (bit & 0xFF) && (coils_processed < bit_count); bit <<= 1, coils_processed++ ) { + if(unpacked_data[start_addr + coils_processed]) + packed_data[byte] |= bit; /* set bit */ + else packed_data[byte] &= ~bit; /* reset bit */ + } + } + return 0; +} + + +/* unpack bits from packed_data to unpacked_data */ +static inline int __unpack_bits(u16 *unpacked_data, u16 start_addr, u16 bit_count, u8 *packed_data) { + u8 temp, bit; + u16 byte, coils_processed; + + if ((0 == bit_count) || (65535-start_addr < bit_count-1)) + return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ + + for(byte = 0, coils_processed = 0; coils_processed < bit_count; byte++) { + temp = packed_data[byte] ; + for(bit = 0x01; (bit & 0xff) && (coils_processed < bit_count); bit <<= 1, coils_processed++) { + unpacked_data[start_addr + coils_processed] = (temp & bit)?1:0; + } + } + return 0; +} + + +static int __read_inbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) + {return __pack_bits(((server_mem_t *)mem_map)->ro_bits, start_addr, bit_count, data_bytes);} +static int __read_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) + {return __pack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes);} +static int __write_outbits (void *mem_map, u16 start_addr, u16 bit_count, u8 *data_bytes) + {return __unpack_bits(((server_mem_t *)mem_map)->rw_bits, start_addr, bit_count, data_bytes); } + + + +static int __read_inwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { + + if ((start_addr + word_count) > MEM_AREA_SIZE) + return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ + + /* use memcpy() because loop with pointers (u16 *) caused alignment problems */ + memcpy(/* dest */ (void *)data_words, + /* src */ (void *)&(((server_mem_t *)mem_map)->ro_words[start_addr]), + /* size */ word_count * 2); + return 0; +} + + + +static int __read_outwords (void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { + + if ((start_addr + word_count) > MEM_AREA_SIZE) + return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ + + /* use memcpy() because loop with pointers (u16 *) caused alignment problems */ + memcpy(/* dest */ (void *)data_words, + /* src */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]), + /* size */ word_count * 2); + return 0; +} + + + + +static int __write_outwords(void *mem_map, u16 start_addr, u16 word_count, u16 *data_words) { + + if ((start_addr + word_count) > MEM_AREA_SIZE) + return -ERR_ILLEGAL_DATA_ADDRESS; /* ERR_ILLEGAL_DATA_ADDRESS defined in mb_util.h */ + + /* WARNING: The data returned in the data_words[] array is not guaranteed to be 16 bit aligned. + * It is not therefore safe to cast it to an u16 data type. + * The following code cannot be used. memcpy() is used instead. + */ + /* + for (count = 0; count < word_count ; count++) + ((server_mem_t *)mem_map)->rw_words[count + start_addr] = data_words[count]; + */ + memcpy(/* dest */ (void *)&(((server_mem_t *)mem_map)->rw_words[start_addr]), + /* src */ (void *)data_words, + /* size */ word_count * 2); + return 0; +} + + + + +#include + +static void *__mb_server_thread(void *_server_node) { + server_node_t *server_node = _server_node; + mb_slave_callback_t callbacks = { + &__read_inbits, + &__read_outbits, + &__write_outbits, + &__read_inwords, + &__read_outwords, + &__write_outwords, + (void *)&(server_node->mem_area) + }; + + // Enable thread cancelation. Enabled is default, but set it anyway to be safe. + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + // mb_slave_run() should never return! + mb_slave_run(server_node->mb_nd /* nd */, callbacks, server_node->slave_id); + fprintf(stderr, "Modbus plugin: Modbus server for node %%s died unexpectedly!\n", server_node->location); /* should never occur */ + return NULL; +} + + +#define timespec_add(ts, sec, nsec) { \ + ts.tv_sec += sec; \ + ts.tv_nsec += nsec; \ + if (ts.tv_nsec >= 1000000000) { \ + ts.tv_sec ++; \ + ts.tv_nsec -= 1000000000; \ + } \ +} + + +static void *__mb_client_thread(void *_index) { + int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast) + struct timespec next_cycle; + int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */ + int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */ + + // Enable thread cancelation. Enabled is default, but set it anyway to be safe. + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + // get the current time + clock_gettime(CLOCK_MONOTONIC, &next_cycle); + + // loop the communication with the client + while (1) { + /* + struct timespec cur_time; + clock_gettime(CLOCK_MONOTONIC, &cur_time); + fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec); + */ + int req; + for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){ + /*just do the requests belonging to the client */ + if (client_requests[req].client_node_id != client_node_id) + continue; + int res_tmp = __execute_mb_request(req); + switch (res_tmp) { + case PORT_FAILURE: { + if (res_tmp != client_nodes[client_node_id].prev_error) + fprintf(stderr, "Modbus plugin: Error connecting Modbus client %%s to remote server.\n", client_nodes[client_node_id].location); + client_nodes[client_node_id].prev_error = res_tmp; + break; + } + case INVALID_FRAME: { + if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error)) + fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned an invalid/corrupted frame.\n", client_requests[req].location); + client_requests[req].prev_error = res_tmp; + break; + } + case TIMEOUT: { + if ((res_tmp != client_requests[req].prev_error) && (0 == client_nodes[client_node_id].prev_error)) + fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s timed out waiting for reply from server.\n", client_requests[req].location); + client_requests[req].prev_error = res_tmp; + break; + } + case MODBUS_ERROR: { + if (client_requests[req].prev_error != client_requests[req].error_code) { + fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s was unsuccesful. Server/slave returned error code 0x%%2x", client_requests[req].location, client_requests[req].error_code); + if (client_requests[req].error_code <= MAX_MODBUS_ERROR_CODE ) { + fprintf(stderr, "(%%s)", modbus_error_messages[client_requests[req].error_code]); + fprintf(stderr, ".\n"); + } + } + client_requests[req].prev_error = client_requests[req].error_code; + break; + } + default: { + if ((res_tmp >= 0) && (client_nodes[client_node_id].prev_error != 0)) { + fprintf(stderr, "Modbus plugin: Modbus client %%s has reconnected to server/slave.\n", client_nodes[client_node_id].location); + } + if ((res_tmp >= 0) && (client_requests[req] .prev_error != 0)) { + fprintf(stderr, "Modbus plugin: Modbus client request configured at location %%s has succesfully resumed comunication.\n", client_requests[req].location); + } + client_nodes[client_node_id].prev_error = 0; + client_requests[req] .prev_error = 0; + break; + } + } + } + // Determine absolute time instant for starting the next cycle + struct timespec prev_cycle, now; + prev_cycle = next_cycle; + timespec_add(next_cycle, period_sec, period_nsec); + /* NOTE A: + * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will + * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously + * for some time until we catch up. + * This is undesirable, so we detect it by making sure the next_cycle will start in the future. + * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval. + * + * NOTE B: + * It probably does not make sense to check for overflow of timer - so we don't do it for now! + * Even in 32 bit systems this will take at least 68 years since the computer booted + * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0 + * every time the system boots). On 64 bit systems, it will take over + * 10^11 years to overflow. + */ + clock_gettime(CLOCK_MONOTONIC, &now); + if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec))) + /* We are falling behind. See NOTE A above */ + || (next_cycle.tv_sec < prev_cycle.tv_sec) + /* Timer overflow. See NOTE B above */ + ) { + next_cycle = now; + timespec_add(next_cycle, period_sec, period_nsec); + } + + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL); + } + + // humour the compiler. + return NULL; +} + + +int __cleanup_%(locstr)s (); +int __init_%(locstr)s (int argc, char **argv){ + int index; + + for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) + client_nodes[index].mb_nd = -1; + for (index=0; index < NUMBER_OF_SERVER_NODES;index++) + // mb_nd with negative numbers indicate how far it has been initialised (or not) + // -2 --> no modbus node created; no thread created + // -1 --> modbus node created!; no thread created + // >=0 --> modbus node created!; thread created! + server_nodes[index].mb_nd = -2; + + /* modbus library init */ + /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus + * extension currently in the user's project. This file (MB_xx.c) is handling only one instance, + * but must initialize the library for all instances. Only the first call to mb_slave_and_master_init() + * will result in memory being allocated. All subsequent calls (by other MB_xx,c files) will be ignored + * by the mb_slave_and_master_init() funtion, as long as they are called with the same arguments. + */ + if (mb_slave_and_master_init(TOTAL_TCPNODE_COUNT, TOTAL_RTUNODE_COUNT, TOTAL_ASCNODE_COUNT) <0) { + fprintf(stderr, "Modbus plugin: Error starting modbus library\n"); + // return imediately. Do NOT goto error_exit, as we did not get to + // start the modbus library! + return -1; + } + + /* init the mutex for each client request */ + /* Must be done _before_ launching the client threads!! */ + for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ + if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) { + fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location); + goto error_exit; + } + } + + /* init each client connection to remote modbus server, and launch thread */ + /* NOTE: All client_nodes[].init_state are initialised to 0 in the code + * generated by the modbus plugin + */ + for (index=0; index < NUMBER_OF_CLIENT_NODES;index++){ + /* establish client connection */ + client_nodes[index].mb_nd = mb_master_connect (client_nodes[index].node_address); + if (client_nodes[index].mb_nd < 0){ + fprintf(stderr, "Modbus plugin: Error creating modbus client node %%s\n", client_nodes[index].location); + goto error_exit; + } + client_nodes[index].init_state = 1; // we have created the node + + /* launch a thread to handle this client node */ + { + int res = 0; + pthread_attr_t attr; + res |= pthread_attr_init(&attr); + res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index)); + if (res != 0) { + fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location); + goto error_exit; + } + } + client_nodes[index].init_state = 2; // we have created the node and a thread + } + + /* init each local server */ + /* NOTE: All server_nodes[].init_state are initialised to 0 in the code + * generated by the modbus plugin + */ + for (index=0; index < NUMBER_OF_SERVER_NODES;index++){ + /* create the modbus server */ + server_nodes[index].mb_nd = mb_slave_new (server_nodes[index].node_address); + if (server_nodes[index].mb_nd < 0){ + fprintf(stderr, "Modbus plugin: Error creating modbus server node %%s\n", server_nodes[index].location); + goto error_exit; + } + server_nodes[index].init_state = 1; // we have created the node + + /* launch a thread to handle this server node */ + { + int res = 0; + pthread_attr_t attr; + res |= pthread_attr_init(&attr); + res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index])); + if (res != 0) { + fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location); + goto error_exit; + } + } + server_nodes[index].init_state = 2; // we have created the node and thread + } + + return 0; + +error_exit: + __cleanup_%(locstr)s (); + return -1; +} + + + + + +void __publish_%(locstr)s (){ + int index; + + for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ + /*just do the output requests */ + if (client_requests[index].req_type == req_output){ + if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){ + // copy from plcv_buffer to coms_buffer + memcpy((void *)client_requests[index].coms_buffer /* destination */, + (void *)client_requests[index].plcv_buffer /* source */, + REQ_BUF_SIZE * sizeof(u16) /* size in bytes */); + pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); + } + } + } +} + + + + + +void __retrieve_%(locstr)s (){ + int index; + + for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ + /*just do the input requests */ + if (client_requests[index].req_type == req_input){ + if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){ + // copy from coms_buffer to plcv_buffer + memcpy((void *)client_requests[index].plcv_buffer /* destination */, + (void *)client_requests[index].coms_buffer /* source */, + REQ_BUF_SIZE * sizeof(u16) /* size in bytes */); + pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); + } + } + } +} + + + + + +int __cleanup_%(locstr)s (){ + int index, close; + int res = 0; + + /* kill thread and close connections of each modbus client node */ + for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) { + close = 0; + if (client_nodes[index].init_state >= 2) { + // thread was launched, so we try to cancel it! + close = pthread_cancel(client_nodes[index].thread_id); + close |= pthread_join (client_nodes[index].thread_id, NULL); + if (close < 0) + fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location); + } + res |= close; + + close = 0; + if (client_nodes[index].init_state >= 1) { + // modbus client node was created, so we try to close it! + close = mb_master_close (client_nodes[index].mb_nd); + if (close < 0){ + fprintf(stderr, "Modbus plugin: Error closing modbus client node %%s\n", client_nodes[index].location); + // We try to shut down as much as possible, so we do not return noW! + } + client_nodes[index].mb_nd = -1; + } + res |= close; + client_nodes[index].init_state = 0; + } + + /* kill thread and close connections of each modbus server node */ + for (index=0; index < NUMBER_OF_SERVER_NODES; index++) { + close = 0; + if (server_nodes[index].init_state >= 2) { + // thread was launched, so we try to cancel it! + close = pthread_cancel(server_nodes[index].thread_id); + close |= pthread_join (server_nodes[index].thread_id, NULL); + if (close < 0) + fprintf(stderr, "Modbus plugin: Error closing thread for modbus server %%s\n", server_nodes[index].location); + } + res |= close; + + close = 0; + if (server_nodes[index].init_state >= 1) { + // modbus server node was created, so we try to close it! + close = mb_slave_close (server_nodes[index].mb_nd); + if (close < 0) { + fprintf(stderr, "Modbus plugin: Error closing node for modbus server %%s (%%d)\n", server_nodes[index].location, server_nodes[index].mb_nd); + // We try to shut down as much as possible, so we do not return noW! + } + server_nodes[index].mb_nd = -1; + } + res |= close; + server_nodes[index].init_state = 0; + } + + /* destroy the mutex of each client request */ + for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++) { + if (pthread_mutex_destroy(&(client_requests[index].coms_buf_mutex))) { + fprintf(stderr, "Modbus plugin: Error destroying request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location); + // We try to shut down as much as possible, so we do not return noW! + res |= -1; + } + } + + /* modbus library close */ + //fprintf(stderr, "Shutting down modbus library...\n"); + if (mb_slave_and_master_done()<0) { + fprintf(stderr, "Modbus plugin: Error shutting down modbus library\n"); + res |= -1; + } + + return res; +} + diff -r 3a1c0c161f80 -r 48ebcbe7f19b modbus/modbus.py --- a/modbus/modbus.py Tue Mar 05 11:43:20 2019 +0300 +++ b/modbus/modbus.py Mon Mar 11 13:51:07 2019 +0100 @@ -789,7 +789,7 @@ LDFLAGS = [] LDFLAGS.append(" \"-L" + ModbusPath + "\"") - LDFLAGS.append(os.path.join(ModbusPath, "libmb.a")) + LDFLAGS.append(" \"" + os.path.join(ModbusPath, "libmb.a") + "\"") LDFLAGS.append(" \"-Wl,-rpath," + ModbusPath + "\"") # LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave_and_master.o") + "\"") # LDFLAGS.append("\"" + os.path.join(ModbusPath, "mb_slave.o") + "\"") diff -r 3a1c0c161f80 -r 48ebcbe7f19b py_ext/plc_python.c --- a/py_ext/plc_python.c Tue Mar 05 11:43:20 2019 +0300 +++ b/py_ext/plc_python.c Mon Mar 11 13:51:07 2019 +0100 @@ -89,6 +89,10 @@ */ void __PythonEvalFB(int poll, PYTHON_EVAL* data__) { + if(!__GET_VAR(data__->TRIG)){ + /* ACK is False when TRIG is false, except a pulse when receiving result */ + __SET_VAR(data__->, ACK,, 0); + } /* detect rising edge on TRIG to trigger evaluation */ if(((__GET_VAR(data__->TRIG) && !__GET_VAR(data__->TRIGM1)) || /* polling is equivalent to trig on value rather than on rising edge*/ @@ -109,7 +113,7 @@ if(__GET_VAR(data__->STATE) == PYTHON_FB_ANSWERED){ /* Copy buffer content into result*/ __SET_VAR(data__->, RESULT,, __GET_VAR(data__->BUFFER)); - /* signal result presece to PLC*/ + /* signal result presence to PLC*/ __SET_VAR(data__->, ACK,, 1); /* Mark as free */ __SET_VAR(data__->, STATE,, PYTHON_FB_FREE); diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/NevowServer.py --- a/runtime/NevowServer.py Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime/NevowServer.py Mon Mar 11 13:51:07 2019 +0100 @@ -30,6 +30,7 @@ from zope.interface import implements from nevow import appserver, inevow, tags, loaders, athena, url, rend from nevow.page import renderer +from nevow.static import File from formless import annotate from formless import webform from formless import configurable @@ -77,10 +78,12 @@ 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'))] - ]) + tags.invisible[ + tags.div(render=tags.directive('liveElement'))[ + tags.div(id='content')[ + tags.div(render=tags.directive('PLCElement'))] + ], + tags.a(href='settings')['Settings']]) def __init__(self, *a, **kw): athena.LiveElement.__init__(self, *a, **kw) @@ -135,11 +138,34 @@ def __init__(self): configurable.Configurable.__init__(self, None) self.bindingsNames = [] + self.infostringcount = 0 def getBindingNames(self, ctx): return self.bindingsNames - def addExtension(self, name, desc, fields, btnlabel, callback): + def addInfoString(self, label, value, name=None): + if isinstance(value, str): + def default(*k): + return value + else: + def default(*k): + return value() + + if name is None: + name = "_infostring_" + str(self.infostringcount) + self.infostringcount = self.infostringcount + 1 + + def _bind(ctx): + return annotate.Property( + name, + annotate.String( + label=label, + default=default, + immutable=True)) + setattr(self, 'bind_' + name, _bind) + self.bindingsNames.append(name) + + def addSettings(self, name, desc, fields, btnlabel, callback): def _bind(ctx): return annotate.MethodBinding( 'action_' + name, @@ -192,25 +218,27 @@ # This makes webform_css url answer some default CSS child_webform_css = webform.defaultCSS + child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css') implements(ISettings) - docFactory = loaders.stan([ - tags.html[ - tags.head[ - tags.title[_("Beremiz Runtime Settings")], - tags.link(rel='stylesheet', - type='text/css', - href=url.here.child("webform_css")) - ], - tags.body[ - tags.h1["Runtime settings:"], - webform.renderForms('staticSettings'), - tags.h2["Extensions settings:"], - webform.renderForms('dynamicSettings'), - ] - ] - ]) + docFactory = loaders.stan([tags.html[ + tags.head[ + tags.title[_("Beremiz Runtime Settings")], + tags.link(rel='stylesheet', + type='text/css', + href=url.here.child("webform_css")), + tags.link(rel='stylesheet', + type='text/css', + href=url.here.child("webinterface_css")) + ], + tags.body[ + tags.a(href='/')['Back'], + tags.h1["Runtime settings:"], + webform.renderForms('staticSettings'), + tags.h1["Extensions settings:"], + webform.renderForms('dynamicSettings'), + ]]]) def configurable_staticSettings(self, ctx): return configurable.TypedInterfaceConfigurable(self) @@ -305,11 +333,11 @@ # print "We will be called back when the client disconnects" -def RegisterWebsite(port): +def RegisterWebsite(iface, port): website = WebInterface() site = appserver.NevowSite(website) - reactor.listenTCP(port, site) + reactor.listenTCP(port, site, interface=iface) print(_('HTTP interface port :'), port) return website diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/PLCObject.py --- a/runtime/PLCObject.py Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime/PLCObject.py Mon Mar 11 13:51:07 2019 +0100 @@ -23,21 +23,25 @@ from __future__ import absolute_import -from threading import Thread, Lock, Semaphore, Event, Condition +from threading import Thread, Lock, Semaphore, Event import ctypes import os import sys import traceback from time import time import _ctypes # pylint: disable=wrong-import-order +from six.moves import xrange from past.builtins import execfile -import Pyro.core as pyro -import six -from six.moves import _thread, xrange +import md5 +from tempfile import mkstemp +import shutil +from functools import wraps, partial from runtime.typemapping import TypeTranslator from runtime.loglevels import LogLevelsDefault, LogLevelsCount +from runtime.Stunnel import getPSKID from runtime import PlcStatus +from runtime import MainWorker if os.name in ("nt", "ce"): dlopen = _ctypes.LoadLibrary @@ -64,141 +68,41 @@ sys.stdout.flush() -class job(object): - """ - job to be executed by a worker - """ - def __init__(self, call, *args, **kwargs): - self.job = (call, args, kwargs) - self.result = None - self.success = False - self.exc_info = None - - def do(self): - """ - do the job by executing the call, and deal with exceptions - """ - try: - call, args, kwargs = self.job - self.result = call(*args, **kwargs) - self.success = True - except Exception: - self.success = False - self.exc_info = sys.exc_info() - - -class worker(object): - """ - serialize main thread load/unload of PLC shared objects - """ - def __init__(self): - # Only one job at a time - self._finish = False - self._threadID = None - self.mutex = Lock() - self.todo = Condition(self.mutex) - self.done = Condition(self.mutex) - self.free = Condition(self.mutex) - self.job = None - - def runloop(self, *args, **kwargs): - """ - meant to be called by worker thread (blocking) - """ - self._threadID = _thread.get_ident() - if args or kwargs: - job(*args, **kwargs).do() - # result is ignored - self.mutex.acquire() - while not self._finish: - self.todo.wait() - if self.job is not None: - self.job.do() - self.done.notify() - else: - self.free.notify() - self.mutex.release() - - def call(self, *args, **kwargs): - """ - creates a job, execute it in worker thread, and deliver result. - if job execution raise exception, re-raise same exception - meant to be called by non-worker threads, but this is accepted. - blocking until job done - """ - - _job = job(*args, **kwargs) - - if self._threadID == _thread.get_ident() or self._threadID is None: - # if caller is worker thread execute immediately - _job.do() - else: - # otherwise notify and wait for completion - self.mutex.acquire() - - while self.job is not None: - self.free.wait() - - self.job = _job - self.todo.notify() - self.done.wait() - _job = self.job - self.job = None - self.mutex.release() - - if _job.success: - return _job.result - else: - exc_type = _job.exc_info[0] - exc_value = _job.exc_info[1] - exc_traceback = _job.exc_info[2] - six.reraise(exc_type, exc_value, exc_traceback) - - def quit(self): - """ - unblocks main thread, and terminate execution of runloop() - """ - # mark queue - self._finish = True - self.mutex.acquire() - self.job = None - self.todo.notify() - self.mutex.release() - - -MainWorker = worker() - - def RunInMain(func): + @wraps(func) def func_wrapper(*args, **kwargs): return MainWorker.call(func, *args, **kwargs) return func_wrapper -class PLCObject(pyro.ObjBase): - def __init__(self, server): - pyro.ObjBase.__init__(self) - self.evaluator = server.evaluator - self.argv = [server.workdir] + server.argv # force argv[0] to be "path" to exec... - self.workingdir = server.workdir +class PLCObject(object): + def __init__(self, WorkingDir, argv, statuschange, evaluator, pyruntimevars): + self.workingdir = WorkingDir # must exits already + self.tmpdir = os.path.join(WorkingDir, 'tmp') + if os.path.exists(self.tmpdir): + shutil.rmtree(self.tmpdir) + os.mkdir(self.tmpdir) + # FIXME : is argv of any use nowadays ? + self.argv = [WorkingDir] + argv # force argv[0] to be "path" to exec... + self.statuschange = statuschange + self.evaluator = evaluator + self.pyruntimevars = pyruntimevars self.PLCStatus = PlcStatus.Empty self.PLClibraryHandle = None self.PLClibraryLock = Lock() - self.DummyIteratorLock = None # Creates fake C funcs proxies self._InitPLCStubCalls() - self.daemon = server.daemon - self.statuschange = server.statuschange - self.hmi_frame = None - self.pyruntimevars = server.pyruntimevars self._loading_error = None self.python_runtime_vars = None self.TraceThread = None self.TraceLock = Lock() self.Traces = [] + self.DebugToken = 0 + + self._init_blobs() # First task of worker -> no @RunInMain - def AutoLoad(self): + def AutoLoad(self, autostart): # Get the last transfered PLC try: self.CurrentPLCFilename = open( @@ -206,10 +110,15 @@ "r").read().strip() + lib_ext if self.LoadPLC(): self.PLCStatus = PlcStatus.Stopped + if autostart: + self.StartPLC() + return except Exception: self.PLCStatus = PlcStatus.Empty self.CurrentPLCFilename = None + self.StatusChange() + def StatusChange(self): if self.statuschange is not None: for callee in self.statuschange: @@ -545,7 +454,56 @@ return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount)) @RunInMain - def NewPLC(self, md5sum, data, extrafiles): + def GetPLCID(self): + return getPSKID(partial(self.LogMessage, 0)) + + def _init_blobs(self): + self.blobs = {} + if os.path.exists(self.tmpdir): + shutil.rmtree(self.tmpdir) + os.mkdir(self.tmpdir) + + @RunInMain + def SeedBlob(self, seed): + blob = (mkstemp(dir=self.tmpdir) + (md5.new(),)) + _fobj, _path, md5sum = blob + md5sum.update(seed) + newBlobID = md5sum.digest() + self.blobs[newBlobID] = blob + return newBlobID + + @RunInMain + def AppendChunkToBlob(self, data, blobID): + blob = self.blobs.pop(blobID, None) + + if blob is None: + return None + + fobj, _path, md5sum = blob + md5sum.update(data) + newBlobID = md5sum.digest() + os.write(fobj, data) + self.blobs[newBlobID] = blob + return newBlobID + + @RunInMain + def PurgeBlobs(self): + for fobj, _path, _md5sum in self.blobs: + os.close(fobj) + self._init_blobs() + + def _BlobAsFile(self, blobID, newpath): + blob = self.blobs.pop(blobID, None) + + if blob is None: + raise Exception(_("Missing data to create file: {}").format(newpath)) + + fobj, path, _md5sum = blob + os.close(fobj) + shutil.move(path, newpath) + + @RunInMain + def NewPLC(self, md5sum, plc_object, extrafiles): if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]: NewFileName = md5sum + lib_ext extra_files_log = os.path.join(self.workingdir, "extra_files.txt") @@ -555,18 +513,13 @@ else None new_PLC_filename = os.path.join(self.workingdir, NewFileName) - # Some platform (i.e. Xenomai) don't like reloading same .so file - replace_PLC_shared_object = new_PLC_filename != old_PLC_filename - - if replace_PLC_shared_object: - self.UnLoadPLC() + self.UnLoadPLC() self.LogMessage("NewPLC (%s)" % md5sum) self.PLCStatus = PlcStatus.Empty try: - if replace_PLC_shared_object: - os.remove(old_PLC_filename) + os.remove(old_PLC_filename) for filename in open(extra_files_log, "rt").readlines() + [extra_files_log]: try: os.remove(os.path.join(self.workingdir, filename.strip())) @@ -577,17 +530,16 @@ try: # Create new PLC file - if replace_PLC_shared_object: - open(new_PLC_filename, 'wb').write(data) + self._BlobAsFile(plc_object, new_PLC_filename) # Store new PLC filename based on md5 key open(self._GetMD5FileName(), "w").write(md5sum) # Then write the files log = open(extra_files_log, "w") - for fname, fdata in extrafiles: + for fname, blobID in extrafiles: fpath = os.path.join(self.workingdir, fname) - open(fpath, "wb").write(fdata) + self._BlobAsFile(blobID, fpath) log.write(fname+'\n') # Store new PLC filename @@ -598,9 +550,7 @@ PLCprint(traceback.format_exc()) return False - if not replace_PLC_shared_object: - self.PLCStatus = PlcStatus.Stopped - elif self.LoadPLC(): + if self.LoadPLC(): self.PLCStatus = PlcStatus.Stopped else: self.PLCStatus = PlcStatus.Broken @@ -617,11 +567,13 @@ pass return False + @RunInMain def SetTraceVariablesList(self, idxs): """ Call ctype imported function to append these indexes to registred variables in PLC debugger """ + self.DebugToken += 1 if idxs: # suspend but dont disable if self._suspendDebug(False) == 0: @@ -636,8 +588,10 @@ self._RegisterDebugVariable(idx, force) self._TracesSwap() self._resumeDebug() + return self.DebugToken else: self._suspendDebug(True) + return None def _TracesSwap(self): self.LastSwapTrace = time() @@ -651,8 +605,10 @@ return Traces @RunInMain - def GetTraceVariables(self): - return self.PLCStatus, self._TracesSwap() + def GetTraceVariables(self, DebugToken): + if (DebugToken is not None and DebugToken == self.DebugToken): + return self.PLCStatus, self._TracesSwap() + return PlcStatus.Broken, [] def TraceThreadProc(self): """ diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/PyroServer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/PyroServer.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz runtime. + +# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# Copyright (C) 2017: Andrey Skvortsov +# Copyright (C) 2018: Edouard TISSERANT + +# See COPYING file for copyrights details. + +from __future__ import absolute_import +from __future__ import print_function +import sys + +import Pyro +import Pyro.core as pyro +import runtime +from runtime.ServicePublisher import ServicePublisher + + +class PyroServer(object): + def __init__(self, servicename, ip_addr, port): + self.continueloop = True + self.daemon = None + self.servicename = servicename + self.ip_addr = ip_addr + self.port = port + self.servicepublisher = None + + def _to_be_published(self): + return self.servicename is not None and \ + self.ip_addr not in ["", "localhost", "127.0.0.1"] + + def PrintServerInfo(self): + print(_("Pyro port :"), self.port) + + if self._to_be_published(): + print(_("Publishing service on local network")) + + sys.stdout.flush() + + def PyroLoop(self, when_ready): + if self._to_be_published(): + self.Publish() + + while self.continueloop: + Pyro.config.PYRO_MULTITHREADED = 0 + pyro.initServer() + self.daemon = pyro.Daemon(host=self.ip_addr, port=self.port) + + # pyro never frees memory after connection close if no timeout set + # taking too small timeout value may cause + # unwanted diconnection when IDE is kept busy for long periods + self.daemon.setTimeout(60) + + pyro_obj = Pyro.core.ObjBase() + pyro_obj.delegateTo(runtime.GetPLCObjectSingleton()) + + self.daemon.connect(pyro_obj, "PLCObject") + + when_ready() + self.daemon.requestLoop() + self.daemon.sock.close() + self.Unpublish() + + def Restart(self): + self.daemon.shutdown(True) + + def Quit(self): + self.continueloop = False + self.daemon.shutdown(True) + + def Publish(self): + self.servicepublisher = ServicePublisher("PYRO") + self.servicepublisher.RegisterService(self.servicename, + self.ip_addr, self.port) + + def Unpublish(self): + if self.servicepublisher is not None: + self.servicepublisher.UnRegisterService() + self.servicepublisher = None diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/ServicePublisher.py --- a/runtime/ServicePublisher.py Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime/ServicePublisher.py Mon Mar 11 13:51:07 2019 +0100 @@ -28,13 +28,17 @@ import threading import zeroconf -service_type = '_PYRO._tcp.local.' + +service_type = '_Beremiz._tcp.local.' class ServicePublisher(object): - def __init__(self): + def __init__(self, protocol): # type: fully qualified service type name - self.serviceproperties = {'description': 'Beremiz remote PLC'} + self.serviceproperties = { + 'description': 'Beremiz remote PLC', + 'protocol': protocol + } self.name = None self.ip_32b = None @@ -52,15 +56,19 @@ def _RegisterService(self, name, ip, port): # name: fully qualified service name - self.service_name = 'Beremiz_%s.%s' % (name, service_type) + self.service_name = '%s.%s' % (name, service_type) self.name = name self.port = port - self.server = zeroconf.Zeroconf() - print("MDNS brodcasting on :" + ip) + if ip == "0.0.0.0": + print("MDNS brodcasted on all interfaces") + interfaces = zeroconf.InterfaceChoice.All + ip = self.gethostaddr() + else: + interfaces = [ip] - if ip == "0.0.0.0": - ip = self.gethostaddr() + self.server = zeroconf.Zeroconf(interfaces=interfaces) + print("MDNS brodcasted service address :" + ip) self.ip_32b = socket.inet_aton(ip) diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/Stunnel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Stunnel.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,44 @@ +from __future__ import absolute_import +import os +from binascii import b2a_hqx +try: + from runtime.spawn_subprocess import call +except ImportError: + from subprocess import call + +restart_stunnel_cmdline = ["/etc/init.d/S50stunnel", "restart"] + +_PSKpath = None + + +def PSKgen(ID, PSKpath): + + # b2a_hqx output len is 4/3 input len + secret = os.urandom(192) # int(256/1.3333) + secretstring = b2a_hqx(secret) + + PSKstring = ID+":"+secretstring + with open(PSKpath, 'w') as f: + f.write(PSKstring) + call(restart_stunnel_cmdline) + + +def ensurePSK(ID, PSKpath): + global _PSKpath + _PSKpath = PSKpath + # check if already there + if not os.path.exists(PSKpath): + # create if needed + PSKgen(ID, PSKpath) + + +def getPSKID(errorlog): + if _PSKpath is not None: + if not os.path.exists(_PSKpath): + errorlog( + 'Error: Pre-Shared-Key Secret in %s is missing!\n' % _PSKpath) + return None + ID, _sep, PSK = open(_PSKpath).read().partition(':') + PSK = PSK.rstrip('\n\r') + return (ID, PSK) + return None diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/WampClient.py --- a/runtime/WampClient.py Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime/WampClient.py Mon Mar 11 13:51:07 2019 +0100 @@ -33,13 +33,13 @@ from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS from autobahn.wamp import types, auth from autobahn.wamp.serializer import MsgPackSerializer -from twisted.internet.defer import inlineCallbacks from twisted.internet.protocol import ReconnectingClientFactory from twisted.python.components import registerAdapter from formless import annotate, webform import formless from nevow import tags, url, static +from runtime import GetPLCObjectSingleton mandatoryConfigItems = ["ID", "active", "realm", "url"] @@ -56,6 +56,10 @@ ("StartPLC", {}), ("StopPLC", {}), ("GetPLCstatus", {}), + ("GetPLCID", {}), + ("SeedBlob", {}), + ("AppendChunkToBlob", {}), + ("PurgeBlobs", {}), ("NewPLC", {}), ("MatchMD5", {}), ("SetTraceVariablesList", {}), @@ -88,7 +92,7 @@ def GetCallee(name): """ Get Callee or Subscriber corresponding to '.' spearated object path """ names = name.split('.') - obj = _PySrv.plcobj + obj = GetPLCObjectSingleton() while names: obj = getattr(obj, names.pop(0)) return obj @@ -116,7 +120,6 @@ raise Exception( "don't know how to handle authmethod {}".format(challenge.method)) - @inlineCallbacks def onJoin(self, details): global _WampSession _WampSession = self @@ -129,13 +132,13 @@ registerOptions = None print(_("TypeError register option: {}".format(e))) - yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) + self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) for name in SubscribedEvents: - yield self.subscribe(GetCallee(name), text(name)) + self.subscribe(GetCallee(name), unicode(name)) for func in DoOnJoin: - yield func(self) + func(self) print(_('WAMP session joined (%s) by:' % time.ctime()), ID) @@ -146,6 +149,10 @@ _transportFactory = None print(_('WAMP session left')) + def publishWithOwnID(self, eventID, value): + ID = self.config.extra["ID"] + self.publish(unicode(ID+'.'+eventID), value) + class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): @@ -167,22 +174,16 @@ return ReconnectingClientFactory.buildProtocol(self, addr) def clientConnectionFailed(self, connector, reason): - if self.continueTrying: - print(_("WAMP Client connection failed (%s) .. retrying ..") % - time.ctime()) - super(ReconnectingWampWebSocketClientFactory, - self).clientConnectionFailed(connector, reason) - else: - del connector + print(_("WAMP Client connection failed (%s) .. retrying ..") % + time.ctime()) + super(ReconnectingWampWebSocketClientFactory, + self).clientConnectionFailed(connector, reason) def clientConnectionLost(self, connector, reason): - if self.continueTrying: - print(_("WAMP Client connection lost (%s) .. retrying ..") % - time.ctime()) - super(ReconnectingWampWebSocketClientFactory, - self).clientConnectionFailed(connector, reason) - else: - del connector + print(_("WAMP Client connection lost (%s) .. retrying ..") % + time.ctime()) + super(ReconnectingWampWebSocketClientFactory, + self).clientConnectionFailed(connector, reason) def CheckConfiguration(WampClientConf): @@ -226,12 +227,9 @@ with open(os.path.realpath(_WampConf), 'w') as f: json.dump(WampClientConf, f, sort_keys=True, indent=4) + StopReconnectWampClient() if 'active' in WampClientConf and WampClientConf['active']: - if _transportFactory and _WampSession: - StopReconnectWampClient() StartReconnectWampClient() - else: - StopReconnectWampClient() return WampClientConf @@ -343,6 +341,16 @@ _PySrv = pysrv +def PublishEvent(eventID, value): + if getWampStatus() == "Attached": + _WampSession.publish(unicode(eventID), value) + + +def PublishEventWithOwnID(eventID, value): + if getWampStatus() == "Attached": + _WampSession.publishWithOwnID(unicode(eventID), value) + + # WEB CONFIGURATION INTERFACE WAMP_SECRET_URL = "secret" webExposedConfigItems = ['active', 'url', 'ID'] @@ -428,7 +436,8 @@ def RegisterWebSettings(NS): - NS.ConfigurableSettings.addExtension( + + NS.ConfigurableSettings.addSettings( "wamp", _("Wamp Settings"), webFormInterface, diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/Worker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Worker.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz runtime. +# +# Copyright (C) 2018: Edouard TISSERANT +# +# See COPYING.Runtime file for copyrights details. + +from __future__ import absolute_import +import sys +import thread +from threading import Lock, Condition +import six + + +class job(object): + """ + job to be executed by a worker + """ + def __init__(self, call, *args, **kwargs): + self.job = (call, args, kwargs) + self.result = None + self.success = False + self.exc_info = None + + def do(self): + """ + do the job by executing the call, and deal with exceptions + """ + try: + call, args, kwargs = self.job + self.result = call(*args, **kwargs) + self.success = True + except Exception: + self.success = False + self.exc_info = sys.exc_info() + + +class worker(object): + """ + serialize main thread load/unload of PLC shared objects + """ + def __init__(self): + # Only one job at a time + self._finish = False + self._threadID = None + self.mutex = Lock() + self.todo = Condition(self.mutex) + self.done = Condition(self.mutex) + self.free = Condition(self.mutex) + self.job = None + + def runloop(self, *args, **kwargs): + """ + meant to be called by worker thread (blocking) + """ + self._threadID = thread.get_ident() + self.mutex.acquire() + if args or kwargs: + _job = job(*args, **kwargs) + _job.do() + if _job.success: + # result is ignored + pass + else: + raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2] + + while not self._finish: + self.todo.wait() + if self.job is not None: + self.job.do() + self.done.notify() + else: + break + + self.mutex.release() + + def call(self, *args, **kwargs): + """ + creates a job, execute it in worker thread, and deliver result. + if job execution raise exception, re-raise same exception + meant to be called by non-worker threads, but this is accepted. + blocking until job done + """ + + _job = job(*args, **kwargs) + + if self._threadID == thread.get_ident(): + # if caller is worker thread execute immediately + _job.do() + else: + # otherwise notify and wait for completion + self.mutex.acquire() + + while self.job is not None: + self.free.wait() + + self.job = _job + self.todo.notify() + self.done.wait() + self.job = None + self.free.notify() + self.mutex.release() + + if _job.success: + return _job.result + else: + exc_type = _job.exc_info[0] + exc_value = _job.exc_info[1] + exc_traceback = _job.exc_info[2] + six.reraise(exc_type, exc_value, exc_traceback) + + def quit(self): + """ + unblocks main thread, and terminate execution of runloop() + """ + # mark queue + self._finish = True + self.mutex.acquire() + self.job = None + self.todo.notify() + self.mutex.release() diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/__init__.py --- a/runtime/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -1,28 +1,32 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# This file is part of Beremiz runtime. -# -# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD -# -# See COPYING.Runtime file for copyrights details. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. +from __future__ import absolute_import +from __future__ import print_function +import traceback +import sys -# 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 -# Lesser General Public License for more details. +from runtime.Worker import worker +MainWorker = worker() -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +_PLCObjectSingleton = None -from __future__ import absolute_import -import os -from runtime.PLCObject import PLCObject, PLCprint, MainWorker -import runtime.ServicePublisher +def GetPLCObjectSingleton(): + assert _PLCObjectSingleton is not None + return _PLCObjectSingleton + + +def LogMessageAndException(msg, exp=None): + if exp is None: + exp = sys.exc_info() + if _PLCObjectSingleton is not None: + _PLCObjectSingleton.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp))) + print(msg) + traceback.print_exception(*exp) + + +def CreatePLCObjectSingleton(*args, **kwargs): + global _PLCObjectSingleton + from runtime.PLCObject import PLCObject # noqa # pylint: disable=wrong-import-position + _PLCObjectSingleton = PLCObject(*args, **kwargs) diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/spawn_subprocess.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/spawn_subprocess.py Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# subset of subprocess built-in module using posix_spawn rather than fork. + +from __future__ import absolute_import +import os +import signal +import posix_spawn + +PIPE = "42" + + +class Popen(object): + def __init__(self, args, stdin=None, stdout=None): + self.returncode = None + self.stdout = None + self.stdin = None + # TODO: stderr + file_actions = posix_spawn.FileActions() + if stdout is not None: + # child's stdout, child 2 parent pipe + c2pread, c2pwrite = os.pipe() + # attach child's stdout to writing en of c2p pipe + file_actions.add_dup2(c2pwrite, 1) + # close other end + file_actions.add_close(c2pread) + if stdin is not None: + # child's stdin, parent to child pipe + p2cread, p2cwrite = os.pipe() + # attach child's stdin to reading en of p2c pipe + file_actions.add_dup2(p2cread, 0) + # close other end + file_actions.add_close(p2cwrite) + self.pid = posix_spawn.posix_spawnp(args[0], args, file_actions=file_actions) + if stdout is not None: + self.stdout = os.fdopen(c2pread) + os.close(c2pwrite) + if stdin is not None: + self.stdin = os.fdopen(p2cwrite, 'w') + os.close(p2cread) + + def _wait(self): + if self.returncode is None: + self.returncode = os.waitpid(self.pid, 0)[1] + + def communicate(self): + if self.stdin is not None: + self.stdin.close() + self.stdin = None + if self.stdout is not None: + stdoutdata = self.stdout.read() + else: + stdoutdata = "" + + # TODO + stderrdata = "" + + self._wait() + if self.stdout is not None: + self.stdout.close() + self.stdout = None + + return (stdoutdata, stderrdata) + + def wait(self): + if self.stdin is not None: + self.stdin.close() + self.stdin = None + self._wait() + if self.stdout is not None: + self.stdout.close() + self.stdout = None + return self.returncode + + def poll(self): + if self.returncode is None: + pid, ret = os.waitpid(self.pid, os.WNOHANG) + if (pid, ret) != (0, 0): + self.returncode = ret + + if self.stdin is not None: + self.stdin.close() + self.stdin = None + if self.stdout is not None: + self.stdout.close() + self.stdout = None + + return self.returncode + + def kill(self): + os.kill(self.pid, signal.SIGKILL) + + if self.stdin is not None: + self.stdin.close() + self.stdin = None + if self.stdout is not None: + self.stdout.close() + self.stdout = None + + +def call(*args): + cmd = [] + if isinstance(args[0], str): + if len(args) == 1: + # oversimplified splitting of arguments, + # TODO: care about use of simple and double quotes + cmd = args[0].split() + else: + cmd = args + elif isinstance(args[0], list) and len(args) == 1: + cmd = args[0] + else: + raise Exception("Wrong arguments passed to subprocess.call") + pid = posix_spawn.posix_spawnp(cmd[0], cmd) + return os.waitpid(pid, 0) + + +if __name__ == '__main__': + # unit test + + p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE) + p.stdin.write("blah") + p.stdin.close() + print(p.stdout.read()) + p.wait() + + p = Popen(["tr", "abc", "def"], stdin=PIPE, stdout=PIPE) + p.stdin.write("blah") + print(p.communicate()) + + call("echo blah0") + call(["echo", "blah1"]) + call("echo", "blah2") diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime/webinterface.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/webinterface.css Mon Mar 11 13:51:07 2019 +0100 @@ -0,0 +1,6 @@ + +.freeform-label { + float: left; + width: 30%; +} + diff -r 3a1c0c161f80 -r 48ebcbe7f19b runtime_files.list --- a/runtime_files.list Tue Mar 05 11:43:20 2019 +0300 +++ b/runtime_files.list Mon Mar 11 13:51:07 2019 +0100 @@ -10,6 +10,7 @@ runtime/PLCObject.py runtime/NevowServer.py runtime/webinterface.js +runtime/webinterface.css runtime/__init__.py runtime/ServicePublisher.py runtime/typemapping.py diff -r 3a1c0c161f80 -r 48ebcbe7f19b targets/__init__.py --- a/targets/__init__.py Tue Mar 05 11:43:20 2019 +0300 +++ b/targets/__init__.py Mon Mar 11 13:51:07 2019 +0100 @@ -74,9 +74,10 @@ DictXSD_toolchain["toolchain_"+toolchainname] = open(xsdfilename).read() # Get all xsd targets - for _targetname, nfo in targets.iteritems(): + for target_name, nfo in targets.iteritems(): xsd_string = open(nfo["xsd"]).read() - targetchoices += xsd_string % DictXSD_toolchain + targetchoices += xsd_string % dict(DictXSD_toolchain, + target_name=target_name) return targetchoices diff -r 3a1c0c161f80 -r 48ebcbe7f19b targets/toolchain_gcc.py --- a/targets/toolchain_gcc.py Tue Mar 05 11:43:20 2019 +0300 +++ b/targets/toolchain_gcc.py Mon Mar 11 13:51:07 2019 +0100 @@ -72,23 +72,20 @@ """ return self.CTRInstance.GetTarget().getcontent().getLinker() - def GetBinaryCode(self): - try: - return open(self.exe_path, "rb").read() - except Exception: - return None + def GetBinaryPath(self): + return self.bin_path def _GetMD5FileName(self): return os.path.join(self.buildpath, "lastbuildPLC.md5") - def ResetBinaryCodeMD5(self): + def ResetBinaryMD5(self): self.md5key = None try: os.remove(self._GetMD5FileName()) except Exception: pass - def GetBinaryCodeMD5(self): + def GetBinaryMD5(self): if self.md5key is not None: return self.md5key else: @@ -100,8 +97,8 @@ def SetBuildPath(self, buildpath): if self.buildpath != buildpath: self.buildpath = buildpath - self.exe = self.CTRInstance.GetProjectName() + self.extension - self.exe_path = os.path.join(self.buildpath, self.exe) + self.bin = self.CTRInstance.GetProjectName() + self.extension + self.bin_path = os.path.join(self.buildpath, self.bin) self.md5key = None self.srcmd5 = {} @@ -152,9 +149,6 @@ wholesrcdata += self.concat_deps(CFileName) return hashlib.md5(wholesrcdata).hexdigest() - def calc_md5(self): - return hashlib.md5(self.GetBinaryCode()).hexdigest() - def build(self): # Retrieve compiler and linker self.compiler = self.getCompiler() @@ -165,7 +159,7 @@ # ----------------- GENERATE OBJECT FILES ------------------------ obns = [] objs = [] - relink = self.GetBinaryCode() is None + relink = not os.path.exists(self.bin_path) for Location, CFilesAndCFLAGS, _DoCalls in self.CTRInstance.LocationCFilesAndCFLAGS: if CFilesAndCFLAGS: if Location: @@ -213,14 +207,14 @@ ALLldflags = ' '.join(self.getBuilderLDFLAGS()) - self.CTRInstance.logger.write(" [CC] " + ' '.join(obns)+" -> " + self.exe + "\n") + self.CTRInstance.logger.write(" [CC] " + ' '.join(obns)+" -> " + self.bin + "\n") status, _result, _err_result = ProcessLogger( self.CTRInstance.logger, "\"%s\" %s -o \"%s\" %s" % (self.linker, listobjstring, - self.exe_path, + self.bin_path, ALLldflags) ).spin() @@ -228,10 +222,10 @@ return False else: - self.CTRInstance.logger.write(" [pass] " + ' '.join(obns)+" -> " + self.exe + "\n") + self.CTRInstance.logger.write(" [pass] " + ' '.join(obns)+" -> " + self.bin + "\n") # Calculate md5 key and get data for the new created PLC - self.md5key = self.calc_md5() + self.md5key = hashlib.md5(open(self.bin_path, "rb").read()).hexdigest() # Store new PLC filename based on md5 key f = open(self._GetMD5FileName(), "w") diff -r 3a1c0c161f80 -r 48ebcbe7f19b targets/toolchain_makefile.py --- a/targets/toolchain_makefile.py Tue Mar 05 11:43:20 2019 +0300 +++ b/targets/toolchain_makefile.py Mon Mar 11 13:51:07 2019 +0100 @@ -47,20 +47,20 @@ self.buildpath = buildpath self.md5key = None - def GetBinaryCode(self): + def GetBinaryPath(self): return None def _GetMD5FileName(self): return os.path.join(self.buildpath, "lastbuildPLC.md5") - def ResetBinaryCodeMD5(self): + def ResetBinaryMD5(self): self.md5key = None try: os.remove(self._GetMD5FileName()) except Exception: pass - def GetBinaryCodeMD5(self): + def GetBinaryMD5(self): if self.md5key is not None: return self.md5key else: diff -r 3a1c0c161f80 -r 48ebcbe7f19b wxglade_hmi/wxglade_hmi.py --- a/wxglade_hmi/wxglade_hmi.py Tue Mar 05 11:43:20 2019 +0300 +++ b/wxglade_hmi/wxglade_hmi.py Mon Mar 11 13:51:07 2019 +0100 @@ -132,32 +132,39 @@ else: define_hmi = "" - declare_hmi = "\n".join(["%(name)s = None\n" % x for x in main_frames]) - declare_hmi += "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" % - dict(x, h=h) for h in x['handlers']]) - for x in hmi_objects]) global_hmi = ("global %s\n" % ",".join( [x["name"] for x in main_frames]) if len(main_frames) > 0 else "") - init_hmi = "\n".join(["""\ + + declare_hmi = \ + "\n".join(["%(name)s = None\n" % x for x in main_frames]) + \ + "\n".join(["\n".join(["%(class)s.%(h)s = %(h)s" % dict(x, h=h) + for h in x['handlers']]) + for x in hmi_objects]) + """\ + def OnCloseFrame(evt): wx.MessageBox(_("Please stop PLC to close")) -%(name)s = %(class)s(None) -%(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame) -%(name)s.Show() +def InitHMI(): + """ + global_hmi + "\n" + "\n".join(["""\ + %(name)s = %(class)s(None) + %(name)s.Bind(wx.EVT_CLOSE, OnCloseFrame) + %(name)s.Show() + +""" % x for x in main_frames]) + """\ +def CleanupHMI(): + """ + global_hmi + "\n" + "\n".join(["""\ + if %(name)s is not None: + %(name)s.Destroy() """ % x for x in main_frames]) - cleanup_hmi = "\n".join( - ["if %(name)s is not None: %(name)s.Destroy()" % x - for x in main_frames]) self.PreSectionsTexts = { "globals": define_hmi, "start": global_hmi, - "stop": global_hmi + cleanup_hmi + "stop": "CleanupHMI()\n" } self.PostSectionsTexts = { "globals": declare_hmi, - "start": init_hmi, + "start": "InitHMI()\n", } if len(main_frames) == 0 and \