# HG changeset patch # User Edouard Tisserant # Date 1705525772 -3600 # Node ID 34da877021d5661a192f06ce6cb089c109bc5cdc # Parent a6e7dd8bac36f5988a6f7e3f0b3f1088788851b7 Replace PYRO with ERPC. Work In Progress. diff -r a6e7dd8bac36 -r 34da877021d5 Beremiz_service.py --- a/Beremiz_service.py Sat Dec 09 01:03:43 2023 +0100 +++ b/Beremiz_service.py Wed Jan 17 22:09:32 2024 +0100 @@ -36,7 +36,7 @@ from functools import partial import runtime -from runtime.PyroServer import PyroServer +from runtime.eRPCServer import eRPCServer as RPCServer from runtime.xenomai import TryPreloadXenomai from runtime import LogMessageAndException from runtime import PlcStatus @@ -270,12 +270,12 @@ TBMENU_CHANGE_INTERFACE = wx.NewIdRef() TBMENU_LIVE_SHELL = wx.NewIdRef() TBMENU_WXINSPECTOR = wx.NewIdRef() - TBMENU_CHANGE_WD = wx.NewIdRef() + # TBMENU_CHANGE_WD = wx.NewIdRef() TBMENU_QUIT = wx.NewIdRef() - def __init__(self, pyroserver): + def __init__(self, rpc_server): wx.adv.TaskBarIcon.__init__(self) - self.pyroserver = pyroserver + self.rpc_server = rpc_server # Set the image self.UpdateIcon(None) @@ -287,7 +287,7 @@ self.Bind(wx.EVT_MENU, self.OnTaskBarLiveShell, id=self.TBMENU_LIVE_SHELL) self.Bind(wx.EVT_MENU, self.OnTaskBarWXInspector, id=self.TBMENU_WXINSPECTOR) self.Bind(wx.EVT_MENU, self.OnTaskBarChangePort, id=self.TBMENU_CHANGE_PORT) - self.Bind(wx.EVT_MENU, self.OnTaskBarChangeWorkingDir, id=self.TBMENU_CHANGE_WD) + # self.Bind(wx.EVT_MENU, self.OnTaskBarChangeWorkingDir, id=self.TBMENU_CHANGE_WD) self.Bind(wx.EVT_MENU, self.OnTaskBarQuit, id=self.TBMENU_QUIT) def CreatePopupMenu(self): @@ -304,7 +304,7 @@ 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.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")) @@ -332,37 +332,37 @@ runtime.GetPLCObjectSingleton().StopPLC() def OnTaskBarChangeInterface(self, evt): - ip_addr = self.pyroserver.ip_addr + ip_addr = self.rpc_server.ip_addr ip_addr = '' if ip_addr is None else ip_addr dlg = ParamsEntryDialog(None, _("Enter the IP of the interface to bind"), defaultValue=ip_addr) dlg.SetTests([(re.compile(r'\d{1,3}(?:\.\d{1,3}){3}$').match, _("IP is not valid!")), (lambda x:len([x for x in x.split(".") if 0 <= int(x) <= 255]) == 4, _("IP is not valid!"))]) if dlg.ShowModal() == wx.ID_OK: - self.pyroserver.ip_addr = dlg.GetValue() - self.pyroserver.Restart() + self.rpc_server.ip_addr = dlg.GetValue() + self.rpc_server.Restart() def OnTaskBarChangePort(self, evt): - dlg = ParamsEntryDialog(None, _("Enter a port number "), defaultValue=str(self.pyroserver.port)) + dlg = ParamsEntryDialog(None, _("Enter a port number "), defaultValue=str(self.rpc_server.port)) dlg.SetTests([(str.isdigit, _("Port number must be an integer!")), (lambda port: 0 <= int(port) <= 65535, _("Port number must be 0 <= port <= 65535!"))]) if dlg.ShowModal() == wx.ID_OK: - self.pyroserver.port = int(dlg.GetValue()) - self.pyroserver.Restart() - - def OnTaskBarChangeWorkingDir(self, evt): - dlg = wx.DirDialog(None, _("Choose a working directory "), self.pyroserver.workdir, wx.DD_NEW_DIR_BUTTON) - if dlg.ShowModal() == wx.ID_OK: - self.pyroserver.workdir = dlg.GetPath() - self.pyroserver.Restart() + self.rpc_server.port = int(dlg.GetValue()) + self.rpc_server.Restart() + + # def OnTaskBarChangeWorkingDir(self, evt): + # dlg = wx.DirDialog(None, _("Choose a working directory "), self.rpc_server.workdir, wx.DD_NEW_DIR_BUTTON) + # if dlg.ShowModal() == wx.ID_OK: + # self.rpc_server.workdir = dlg.GetPath() + # self.rpc_server.Restart() def OnTaskBarChangeName(self, evt): - _servicename = self.pyroserver.servicename + _servicename = self.rpc_server.servicename _servicename = '' if _servicename is None else _servicename dlg = ParamsEntryDialog(None, _("Enter a name "), defaultValue=_servicename) dlg.SetTests([(lambda name: len(name) != 0, _("Name must not be null!"))]) if dlg.ShowModal() == wx.ID_OK: - self.pyroserver.servicename = dlg.GetValue() - self.pyroserver.Restart() + self.rpc_server.servicename = dlg.GetValue() + self.rpc_server.Restart() def _LiveShellLocals(self): return {"locals": runtime.GetPLCObjectSingleton().python_runtime_vars} @@ -383,7 +383,7 @@ def OnTaskBarQuit(self, evt): if wx.Platform == '__WXMSW__': - Thread(target=self.pyroserver.Quit).start() + Thread(target=self.rpc_server.Quit).start() self.RemoveIcon() wx.CallAfter(wx.GetApp().ExitMainLoop) @@ -513,10 +513,10 @@ runtime.CreatePLCObjectSingleton( WorkingDir, argv, statuschange, evaluator, pyruntimevars) -pyroserver = PyroServer(servicename, interface, port) +rpc_server = RPCServer(servicename, interface, port) if havewx: - taskbar_instance = BeremizTaskBarIcon(pyroserver) + taskbar_instance = BeremizTaskBarIcon(rpc_server) if havetwisted: if webport is not None: @@ -533,28 +533,28 @@ except Exception: LogMessageAndException(_("WAMP client startup failed. ")) -pyro_thread = None +rpc_server_thread = None def FirstWorkerJob(): """ - RPC through pyro/wamp/UI may lead to delegation to Worker, + RPC through rpc/wamp/UI may lead to delegation to Worker, then this function ensures that Worker is already - created when pyro starts + created when rpc starts """ - global pyro_thread, pyroserver - - pyro_thread_started = Lock() - pyro_thread_started.acquire() - pyro_thread = Thread(target=pyroserver.PyroLoop, - kwargs=dict(when_ready=pyro_thread_started.release), - name="PyroThread") - - pyro_thread.start() - - # Wait for pyro thread to be effective - pyro_thread_started.acquire() - - pyroserver.PrintServerInfo() + global rpc_server_thread, rpc_server + + rpc_thread_started = Lock() + rpc_thread_started.acquire() + rpc_server_thread = Thread(target=rpc_server.Loop, + kwargs=dict(when_ready=rpc_thread_started.release), + name="RPCThread") + + rpc_server_thread.start() + + # Wait for rpc thread to be effective + rpc_thread_started.acquire() + + rpc_server.PrintServerInfo() # Beremiz IDE detects LOCAL:// runtime is ready by looking # for self.workdir in the daemon's stdout. @@ -616,8 +616,8 @@ pass -pyroserver.Quit() -pyro_thread.join() +rpc_server.Quit() +rpc_server_thread.join() plcobj = runtime.GetPLCObjectSingleton() try: diff -r a6e7dd8bac36 -r 34da877021d5 connectors/ConnectorBase.py --- a/connectors/ConnectorBase.py Sat Dec 09 01:03:43 2023 +0100 +++ b/connectors/ConnectorBase.py Wed Jan 17 22:09:32 2024 +0100 @@ -10,7 +10,7 @@ class ConnectorBase(object): - chuncksize = 1024*1024 + chuncksize = 0xfff # 4KB PLCObjDefaults = { "StartPLC": False, diff -r a6e7dd8bac36 -r 34da877021d5 connectors/ERPC/PSK_Adapter.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/ERPC/PSK_Adapter.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,61 @@ +#!/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) 2019: Edouard TISSERANT +# +# 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. + + +""" +The TLS-PSK adapter that handles SSL connections instead of regular sockets, +but using Pre Shared Keys instead of Certificates +""" + +import socket +import ssl + +try: + import sslpsk +except ImportError as e: + sslpsk = None + +from erpc.transport import TCPTransport + +class SSLPSKClientTransport(TCPTransport): + def __init__(self, host, port, psk): + """ overrides TCPTransport's __init__ to wrap socket in SSl wrapper """ + super(TCPTransport, self).__init__() + self._host = host + self._port = port + self._isServer = isServer + self._sock = None + + if sslpsk is None: + raise ImportError("sslpsk module is not available") + + raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + raw_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + raw_sock.connect((self._host, self._port)) + self._sock = sslpsk.wrap_socket( + raw_sock, psk=psk, server_side=False, + ciphers="PSK-AES256-CBC-SHA", # available in openssl 1.0.2 + ssl_version=ssl.PROTOCOL_TLSv1) + + diff -r a6e7dd8bac36 -r 34da877021d5 connectors/ERPC/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/ERPC/__init__.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Written by Edouard TISSERANT (C) 2024 +# This file is part of Beremiz IDE +# See COPYING file for copyrights details. + + +import os.path +import re +import traceback +from inspect import getmembers, isfunction + + +import erpc + +# eRPC service code +from erpc_interface.erpc_PLCObject.interface import IBeremizPLCObjectService +from erpc_interface.erpc_PLCObject.client import BeremizPLCObjectServiceClient +from erpc_interface.erpc_PLCObject.common import trace_order, extra_file, PLCstatus_enum + +import PSKManagement as PSK +from connectors.ERPC.PSK_Adapter import SSLPSKClientTransport +from connectors.ConnectorBase import ConnectorBase + +enum_to_PLCstatus = dict(map(lambda t:(t[1],t[0]),getmembers(PLCstatus_enum, lambda x:type(x)==int))) + +class MissingCallException(Exception): + pass + +def ExceptionFromERPCReturn(ret): + return {1:Exception, + 2:MissingCallException}.get(ret,ValueError) + +def ReturnAsLastOutput(client_method, obj, args_wrapper, *args): + retval = erpc.Reference() + ret = client_method(obj, *args_wrapper(*args), retval) + if ret != 0: + raise ExceptionFromERPCReturn(ret)(client_method.__name__) + return retval.value + +def TranslatedReturnAsLastOutput(translator): + def wrapper(client_method, obj, args_wrapper, *args): + res = ReturnAsLastOutput(client_method, obj, args_wrapper, *args) + return translator(res) + return wrapper + +ReturnWrappers = { + "AppendChunkToBlob":ReturnAsLastOutput, + "GetLogMessage":TranslatedReturnAsLastOutput( + lambda res:(res.msg, res.tick, res.sec, res.nsec)), + "GetPLCID":TranslatedReturnAsLastOutput( + lambda res:(res.ID, res.PSK)), + "GetPLCstatus":TranslatedReturnAsLastOutput( + lambda res:(enum_to_PLCstatus[res.PLCstatus], res.logcounts)), + "GetTraceVariables":TranslatedReturnAsLastOutput( + lambda res:(enum_to_PLCstatus[res.PLCstatus], + [(sample.tick, sample.TraceBuffer) for sample in res.traces])), + "MatchMD5":ReturnAsLastOutput, + "NewPLC":ReturnAsLastOutput, + "SeedBlob":ReturnAsLastOutput, +} + +ArgsWrappers = { + "NewPLC": + lambda md5sum, plcObjectBlobID, extrafiles: ( + md5sum, plcObjectBlobID, [extra_file(*f) for f in extrafiles]), + "SetTraceVariablesList": + lambda orders : ([trace_order(*order) for order in orders],) +} + +def ERPC_connector_factory(uri, confnodesroot): + """ + returns the ERPC connector + """ + confnodesroot.logger.write(_("ERPC connecting to URI : %s\n") % uri) + + # TODO add parsing for serial URI + # ERPC:///dev/ttyXX:baudrate or ERPC://:COM4:baudrate + + try: + _scheme, location = uri.split("://",1) + locator, *IDhash = location.split('#',1) + x = re.match(r'(?P[^\s:]+):?(?P\d+)?', locator) + host = x.group('host') + port = x.group('port') + if port: + port = int(port) + else: + port = 3000 + except Exception as e: + confnodesroot.logger.write_error( + 'Malformed URI "%s": %s\n' % (uri, str(e))) + return None + + def rpc_wrapper(method_name): + client_method = getattr(BeremizPLCObjectServiceClient, method_name) + return_wrapper = ReturnWrappers.get( + method_name, + lambda client_method, obj, args_wrapper, *args: client_method(obj, *args_wrapper(*args))) + args_wrapper = ArgsWrappers.get(method_name, lambda *x:x) + + def exception_wrapper(self, *args): + try: + print("Clt "+method_name) + return return_wrapper(client_method, self, args_wrapper, *args) + except erpc.transport.ConnectionClosed as e: + confnodesroot._SetConnector(None) + confnodesroot.logger.write_error(_("Connection lost!\n")) + except erpc.codec.CodecError as e: + confnodesroot.logger.write_warning(_("ERPC codec error: %s\n") % e) + except erpc.client.RequestError as e: + confnodesroot.logger.write_error(_("ERPC request error: %s\n") % e) + except MissingCallException as e: + confnodesroot.logger.write_warning(_("Remote call not supported: %s\n") % e.message) + except Exception as e: + errmess = _("Exception calling remote PLC object fucntio %s:\n") % method_name \ + + traceback.format_exc() + confnodesroot.logger.write_error(errmess + "\n") + print(errmess) + confnodesroot._SetConnector(None) + + return self.PLCObjDefaults.get(method_name) + return exception_wrapper + + + PLCObjectERPCProxy = type( + "PLCObjectERPCProxy", + (ConnectorBase, BeremizPLCObjectServiceClient), + {name: rpc_wrapper(name) + for name,_func in getmembers(IBeremizPLCObjectService, isfunction)}) + + try: + if IDhash: + ID = IDhash[0] + # 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: Pre-Shared-Key Secret in %s is missing!\n' % secpath) + return None + secret = open(secpath).read().partition(':')[2].rstrip('\n\r') + transport = SSLPSKClientTransport(host, port, (secret, ID)) + else: + # TODO if serial URI then + # transport = erpc.transport.SerialTransport(device, baudrate) + + transport = erpc.transport.TCPTransport(host, port, False) + + clientManager = erpc.client.ClientManager(transport, erpc.basic_codec.BasicCodec) + client = PLCObjectERPCProxy(clientManager) + + except Exception as e: + confnodesroot.logger.write_error( + _("Connection to {loc} failed with exception {ex}\n").format( + loc=locator, ex=str(e))) + return None + + # Check connection is effective. + IDPSK = client.GetPLCID() + if IDPSK: + ID, secret = IDPSK + PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri) + else: + confnodesroot.logger.write_warning(_("PLC did not provide identity and security infomation.\n")) + + return client diff -r a6e7dd8bac36 -r 34da877021d5 connectors/ERPC_dialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/connectors/ERPC_dialog.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# See COPYING file for copyrights details. + + + +from itertools import repeat, islice, chain + +from connectors.SchemeEditor import SchemeEditor + + +model = [('host', _("Host:")), + ('port', _("Port:"))] + +# (scheme, model, secure) +models = [("LOCAL", [], False), ("ERPC", model, False)] + +Schemes = list(zip(*models))[0] + +_PerSchemeConf = {sch: (mod, sec) for sch, mod, sec in models} + + +class ERPC_dialog(SchemeEditor): + def __init__(self, scheme, *args, **kwargs): + # ID selector is enabled only on ERPC (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 self.EnableIDSelector: + if fields['ID']: + template += "#{ID}" + + return template.format(**fields) + return '' diff -r a6e7dd8bac36 -r 34da877021d5 connectors/PYRO/PSK_Adapter.py --- a/connectors/PYRO/PSK_Adapter.py Sat Dec 09 01:03:43 2023 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +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) 2019: Edouard TISSERANT -# -# 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. - - -""" -The TLS-PSK adapter that handles SSL connections instead of regular sockets, -but using Pre Shared Keys instead of Certificates -""" - - - - -import socket -import re -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 - -try: - import sslpsk -except ImportError as e: - print(str(e)) - sslpsk = None - - -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 as 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) - - -_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) - - -def setupPSKAdapter(): - """ - Add PyroAdapter to the list of available in - Pyro adapters and handle new supported protocols - - This function should be called after - reimport of Pyro module to enable PYROS:// again. - """ - if sslpsk is not None: - Pyro.protocol.getProtocolAdapter = getProtocolAdapter - Pyro.core.processStringURI = processStringURI - else: - raise Exception("sslpsk python module unavailable") diff -r a6e7dd8bac36 -r 34da877021d5 connectors/PYRO/__init__.py --- a/connectors/PYRO/__init__.py Sat Dec 09 01:03:43 2023 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,119 +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 -# -# 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 time import sleep -import copy -import socket -import os.path - -import Pyro5 -import Pyro5.client -import Pyro5.errors - -# TODO: PSK - -import importlib - - -Pyro5.config.SERIALIZER = "msgpack" - - -def PYRO_connector_factory(uri, confnodesroot): - """ - This returns the connector to Pyro style PLCobject - """ - confnodesroot.logger.write(_("PYRO connecting to URI : %s\n") % uri) - - scheme, location = uri.split("://") - - # TODO: use ssl - - schemename = "PYRO" - - # Try to get the proxy object - try: - RemotePLCObjectProxy = Pyro5.client.Proxy(f"{schemename}:PLCObject@{location}") - except Exception as e: - confnodesroot.logger.write_error( - _("Connection to {loc} failed with exception {ex}\n").format( - loc=location, ex=str(e))) - return None - - RemotePLCObjectProxy._pyroTimeout = 60 - - class MissingCallException(Exception): - pass - - def PyroCatcher(func, default=None): - """ - A function that catch a Pyro exceptions, write error to logger - and return default value when it happen - """ - def catcher_func(*args, **kwargs): - try: - return func(*args, **kwargs) - except Pyro5.errors.ConnectionClosedError as e: - confnodesroot._SetConnector(None) - confnodesroot.logger.write_error(_("Connection lost!\n")) - except Pyro5.errors.ProtocolError as e: - confnodesroot.logger.write_error(_("Pyro exception: %s\n") % e) - except MissingCallException as e: - confnodesroot.logger.write_warning(_("Remote call not supported: %s\n") % e.message) - except Exception as e: - errmess = ''.join(Pyro5.errors.get_pyro_traceback()) - confnodesroot.logger.write_error(errmess + "\n") - print(errmess) - confnodesroot._SetConnector(None) - return default - return catcher_func - - # Check connection is effective. - # lambda is for getattr of GetPLCstatus to happen inside catcher - 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) - - class PyroProxyProxy(object): - """ - A proxy proxy class to handle Beremiz Pyro interface specific behavior. - And to put Pyro exception catcher in between caller and Pyro proxy - """ - def __getattr__(self, attrName): - member = self.__dict__.get(attrName, None) - if member is None: - def my_local_func(*args, **kwargs): - call = RemotePLCObjectProxy.__getattr__(attrName) - if call is None: - raise MissingCallException(attrName) - else: - return call(*args, **kwargs) - member = PyroCatcher(my_local_func, self.PLCObjDefaults.get(attrName, None)) - self.__dict__[attrName] = member - return member - - return PyroProxyProxy diff -r a6e7dd8bac36 -r 34da877021d5 connectors/PYRO_dialog.py --- a/connectors/PYRO_dialog.py Sat Dec 09 01:03:43 2023 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# See COPYING file for copyrights details. - - - -from itertools import repeat, islice, chain - -from connectors.SchemeEditor import SchemeEditor - - -model = [('host', _("Host:")), - ('port', _("Port:"))] - -# (scheme, model, secure) -models = [("LOCAL", [], False), ("PYRO", model, False)] - -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 self.EnableIDSelector: - if fields['ID']: - template += "#{ID}" - - return template.format(**fields) - return '' diff -r a6e7dd8bac36 -r 34da877021d5 connectors/WAMP/__init__.py --- a/connectors/WAMP/__init__.py Sat Dec 09 01:03:43 2023 +0100 +++ b/connectors/WAMP/__init__.py Wed Jan 17 22:09:32 2024 +0100 @@ -145,7 +145,7 @@ # TODO : GetPLCID() # TODO : PSK.UpdateID() - return WampPLCObjectProxy + return WampPLCObjectProxy() WAMP_connector_factory = partial(_WAMP_connector_factory, WampSession) diff -r a6e7dd8bac36 -r 34da877021d5 connectors/__init__.py --- a/connectors/__init__.py Sat Dec 09 01:03:43 2023 +0100 +++ b/connectors/__init__.py Wed Jan 17 22:09:32 2024 +0100 @@ -31,7 +31,7 @@ from os import listdir, path from connectors.ConnectorBase import ConnectorBase -connectors_packages = ["PYRO"] +connectors_packages = ["ERPC", "WAMP"] def _GetLocalConnectorClassFactory(name): @@ -71,38 +71,13 @@ """ _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 + # ERPC connection to local runtime # started on demand, listening on random port - scheme = "PYRO" + scheme = "ERPC" runtime_port = confnodesroot.StartLocalRuntime() - uri = f"PYRO://{LocalHost}:{runtime_port}" - - # 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 + uri = f"ERPC://{LocalHost}:{runtime_port}" elif _scheme in connectors: scheme = _scheme @@ -111,18 +86,9 @@ else: return None - # 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 type(_scheme + "_connector", - (ConnectorBase, connector_specific_class), {})() - + return (connectors[scheme] + () # triggers import + (uri, confnodesroot)) # creates object def EditorClassFromScheme(scheme): _Import_Dialogs() diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/__init__.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,6 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject.erpc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject.erpc Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,72 @@ +/* + Written by Edouard TISSERANT (C) 2024 + This file is part of Beremiz runtime and IDE + See COPYING.Runtime and COPYING file for copyrights details. +*/ + +program erpc_PLCObject + +struct PSKID { + string ID; + string PSK; +}; + +enum PLCstatus_enum { + Empty + Stopped, + Started, + Broken, + Disconnected +} + +struct PLCstatus { + PLCstatus_enum PLCstatus; + uint32[4] logcounts; +}; + +struct trace_sample { + uint32 tick; + binary TraceBuffer; +}; + +struct TraceVariables { + PLCstatus_enum PLCstatus; + list traces; +}; + +struct extra_file { + string fname; + binary blobID; +}; + +struct trace_order { + uint32 idx; + uint8 iectype; + binary force; +}; + +struct log_message { + string msg; + uint32 tick; + uint32 sec; + uint32 nsec; +}; + + +interface BeremizPLCObjectService { + AppendChunkToBlob(in binary data, in binary blobID, out binary newBlobID) -> uint32 + GetLogMessage(in uint8 level, in uint32 msgID, out log_message message) -> uint32 + GetPLCID(out PSKID plcID) -> uint32 + GetPLCstatus(out PLCstatus status) -> uint32 + GetTraceVariables(in uint32 debugToken, out TraceVariables traces) -> uint32 + MatchMD5(in string MD5, out bool match) -> uint32 + NewPLC(in string md5sum, in binary plcObjectBlobID, in list extrafiles, out bool success) -> uint32 + PurgeBlobs() -> uint32 + /* NOT TO DO : RemoteExec(in ) -> uint32 */ + RepairPLC() -> uint32 + ResetLogCount() -> uint32 + SeedBlob(in binary seed, out binary blobID) -> uint32 + SetTraceVariablesList(in list orders) -> uint32 + StartPLC() -> uint32 + StopPLC() -> uint32 +} diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject/__init__.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,19 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + +try: + from erpc import erpc_version + version = erpc_version.ERPC_VERSION +except ImportError: + version = "unknown" +if version != "1.11.0": + raise ValueError("The generated shim code version (1.11.0) is different to the rest of eRPC code (%s). \ +Install newer version by running \"python setup.py install\" in folder erpc/erpc_python/." % repr(version)) + +from . import common +from . import client +from . import server +from . import interface diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject/client.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject/client.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,289 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + +import erpc +from . import common, interface + +# Client for BeremizPLCObjectService +class BeremizPLCObjectServiceClient(interface.IBeremizPLCObjectService): + def __init__(self, manager): + super(BeremizPLCObjectServiceClient, self).__init__() + self._clientManager = manager + + def AppendChunkToBlob(self, data, blobID, newBlobID): + assert type(newBlobID) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.APPENDCHUNKTOBLOB_ID, + sequence=request.sequence)) + if data is None: + raise ValueError("data is None") + codec.write_binary(data) + if blobID is None: + raise ValueError("blobID is None") + codec.write_binary(blobID) + + # Send request and process reply. + self._clientManager.perform_request(request) + newBlobID.value = codec.read_binary() + _result = codec.read_uint32() + return _result + + def GetLogMessage(self, level, msgID, message): + assert type(message) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.GETLOGMESSAGE_ID, + sequence=request.sequence)) + if level is None: + raise ValueError("level is None") + codec.write_uint8(level) + if msgID is None: + raise ValueError("msgID is None") + codec.write_uint32(msgID) + + # Send request and process reply. + self._clientManager.perform_request(request) + message.value = common.log_message()._read(codec) + _result = codec.read_uint32() + return _result + + def GetPLCID(self, plcID): + assert type(plcID) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.GETPLCID_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + plcID.value = common.PSKID()._read(codec) + _result = codec.read_uint32() + return _result + + def GetPLCstatus(self, status): + assert type(status) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.GETPLCSTATUS_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + status.value = common.PLCstatus()._read(codec) + _result = codec.read_uint32() + return _result + + def GetTraceVariables(self, debugToken, traces): + assert type(traces) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.GETTRACEVARIABLES_ID, + sequence=request.sequence)) + if debugToken is None: + raise ValueError("debugToken is None") + codec.write_uint32(debugToken) + + # Send request and process reply. + self._clientManager.perform_request(request) + traces.value = common.TraceVariables()._read(codec) + _result = codec.read_uint32() + return _result + + def MatchMD5(self, MD5, match): + assert type(match) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.MATCHMD5_ID, + sequence=request.sequence)) + if MD5 is None: + raise ValueError("MD5 is None") + codec.write_string(MD5) + + # Send request and process reply. + self._clientManager.perform_request(request) + match.value = codec.read_bool() + _result = codec.read_uint32() + return _result + + def NewPLC(self, md5sum, plcObjectBlobID, extrafiles, success): + assert type(success) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.NEWPLC_ID, + sequence=request.sequence)) + if md5sum is None: + raise ValueError("md5sum is None") + codec.write_string(md5sum) + if plcObjectBlobID is None: + raise ValueError("plcObjectBlobID is None") + codec.write_binary(plcObjectBlobID) + if extrafiles is None: + raise ValueError("extrafiles is None") + codec.start_write_list(len(extrafiles)) + for _i0 in extrafiles: + _i0._write(codec) + + + # Send request and process reply. + self._clientManager.perform_request(request) + success.value = codec.read_bool() + _result = codec.read_uint32() + return _result + + def PurgeBlobs(self): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.PURGEBLOBS_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + def RepairPLC(self): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.REPAIRPLC_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + def ResetLogCount(self): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.RESETLOGCOUNT_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + def SeedBlob(self, seed, blobID): + assert type(blobID) is erpc.Reference, "out parameter must be a Reference object" + + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.SEEDBLOB_ID, + sequence=request.sequence)) + if seed is None: + raise ValueError("seed is None") + codec.write_binary(seed) + + # Send request and process reply. + self._clientManager.perform_request(request) + blobID.value = codec.read_binary() + _result = codec.read_uint32() + return _result + + def SetTraceVariablesList(self, orders): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.SETTRACEVARIABLESLIST_ID, + sequence=request.sequence)) + if orders is None: + raise ValueError("orders is None") + codec.start_write_list(len(orders)) + for _i0 in orders: + _i0._write(codec) + + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + def StartPLC(self): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.STARTPLC_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + def StopPLC(self): + # Build remote function invocation message. + request = self._clientManager.create_request() + codec = request.codec + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kInvocationMessage, + service=self.SERVICE_ID, + request=self.STOPPLC_ID, + sequence=request.sequence)) + + # Send request and process reply. + self._clientManager.perform_request(request) + _result = codec.read_uint32() + return _result + + diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject/common.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,215 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + + +# Enumerators data types declarations +class PLCstatus_enum: + Empty = 0 + Stopped = 1 + Started = 2 + Broken = 3 + Disconnected = 4 + + +# Structures data types declarations +class log_message(object): + def __init__(self, msg=None, tick=None, sec=None, nsec=None): + self.msg = msg # string + self.tick = tick # uint32 + self.sec = sec # uint32 + self.nsec = nsec # uint32 + + def _read(self, codec): + self.msg = codec.read_string() + self.tick = codec.read_uint32() + self.sec = codec.read_uint32() + self.nsec = codec.read_uint32() + return self + + def _write(self, codec): + if self.msg is None: + raise ValueError("msg is None") + codec.write_string(self.msg) + if self.tick is None: + raise ValueError("tick is None") + codec.write_uint32(self.tick) + if self.sec is None: + raise ValueError("sec is None") + codec.write_uint32(self.sec) + if self.nsec is None: + raise ValueError("nsec is None") + codec.write_uint32(self.nsec) + + def __str__(self): + return "<%s@%x msg=%s tick=%s sec=%s nsec=%s>" % (self.__class__.__name__, id(self), self.msg, self.tick, self.sec, self.nsec) + + def __repr__(self): + return self.__str__() + +class PSKID(object): + def __init__(self, ID=None, PSK=None): + self.ID = ID # string + self.PSK = PSK # string + + def _read(self, codec): + self.ID = codec.read_string() + self.PSK = codec.read_string() + return self + + def _write(self, codec): + if self.ID is None: + raise ValueError("ID is None") + codec.write_string(self.ID) + if self.PSK is None: + raise ValueError("PSK is None") + codec.write_string(self.PSK) + + def __str__(self): + return "<%s@%x ID=%s PSK=%s>" % (self.__class__.__name__, id(self), self.ID, self.PSK) + + def __repr__(self): + return self.__str__() + +class PLCstatus(object): + def __init__(self, PLCstatus=None, logcounts=None): + self.PLCstatus = PLCstatus # PLCstatus_enum + self.logcounts = logcounts # uint32[4] + + + def _read(self, codec): + self.PLCstatus = codec.read_int32() + self.logcounts = [] + for _i0 in range(4): + _v0 = codec.read_uint32() + self.logcounts.append(_v0) + + return self + + def _write(self, codec): + if self.PLCstatus is None: + raise ValueError("PLCstatus is None") + codec.write_int32(self.PLCstatus) + if self.logcounts is None: + raise ValueError("logcounts is None") + for _i0 in self.logcounts: + codec.write_uint32(_i0) + + + def __str__(self): + return "<%s@%x PLCstatus=%s logcounts=%s>" % (self.__class__.__name__, id(self), self.PLCstatus, self.logcounts) + + def __repr__(self): + return self.__str__() + +class trace_sample(object): + def __init__(self, tick=None, TraceBuffer=None): + self.tick = tick # uint32 + self.TraceBuffer = TraceBuffer # binary + + def _read(self, codec): + self.tick = codec.read_uint32() + self.TraceBuffer = codec.read_binary() + return self + + def _write(self, codec): + if self.tick is None: + raise ValueError("tick is None") + codec.write_uint32(self.tick) + if self.TraceBuffer is None: + raise ValueError("TraceBuffer is None") + codec.write_binary(self.TraceBuffer) + + def __str__(self): + return "<%s@%x tick=%s TraceBuffer=%s>" % (self.__class__.__name__, id(self), self.tick, self.TraceBuffer) + + def __repr__(self): + return self.__str__() + +class TraceVariables(object): + def __init__(self, PLCstatus=None, traces=None): + self.PLCstatus = PLCstatus # PLCstatus_enum + self.traces = traces # list + + def _read(self, codec): + self.PLCstatus = codec.read_int32() + _n0 = codec.start_read_list() + self.traces = [] + for _i0 in range(_n0): + _v0 = trace_sample()._read(codec) + self.traces.append(_v0) + + return self + + def _write(self, codec): + if self.PLCstatus is None: + raise ValueError("PLCstatus is None") + codec.write_int32(self.PLCstatus) + if self.traces is None: + raise ValueError("traces is None") + codec.start_write_list(len(self.traces)) + for _i0 in self.traces: + _i0._write(codec) + + + def __str__(self): + return "<%s@%x PLCstatus=%s traces=%s>" % (self.__class__.__name__, id(self), self.PLCstatus, self.traces) + + def __repr__(self): + return self.__str__() + +class extra_file(object): + def __init__(self, fname=None, blobID=None): + self.fname = fname # string + self.blobID = blobID # binary + + def _read(self, codec): + self.fname = codec.read_string() + self.blobID = codec.read_binary() + return self + + def _write(self, codec): + if self.fname is None: + raise ValueError("fname is None") + codec.write_string(self.fname) + if self.blobID is None: + raise ValueError("blobID is None") + codec.write_binary(self.blobID) + + def __str__(self): + return "<%s@%x fname=%s blobID=%s>" % (self.__class__.__name__, id(self), self.fname, self.blobID) + + def __repr__(self): + return self.__str__() + +class trace_order(object): + def __init__(self, idx=None, iectype=None, force=None): + self.idx = idx # uint32 + self.iectype = iectype # uint8 + self.force = force # binary + + def _read(self, codec): + self.idx = codec.read_uint32() + self.iectype = codec.read_uint8() + self.force = codec.read_binary() + return self + + def _write(self, codec): + if self.idx is None: + raise ValueError("idx is None") + codec.write_uint32(self.idx) + if self.iectype is None: + raise ValueError("iectype is None") + codec.write_uint8(self.iectype) + if self.force is None: + raise ValueError("force is None") + codec.write_binary(self.force) + + def __str__(self): + return "<%s@%x idx=%s iectype=%s force=%s>" % (self.__class__.__name__, id(self), self.idx, self.iectype, self.force) + + def __repr__(self): + return self.__str__() + diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject/interface.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject/interface.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,67 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + +# Abstract base class for BeremizPLCObjectService +class IBeremizPLCObjectService(object): + SERVICE_ID = 1 + APPENDCHUNKTOBLOB_ID = 1 + GETLOGMESSAGE_ID = 2 + GETPLCID_ID = 3 + GETPLCSTATUS_ID = 4 + GETTRACEVARIABLES_ID = 5 + MATCHMD5_ID = 6 + NEWPLC_ID = 7 + PURGEBLOBS_ID = 8 + REPAIRPLC_ID = 9 + RESETLOGCOUNT_ID = 10 + SEEDBLOB_ID = 11 + SETTRACEVARIABLESLIST_ID = 12 + STARTPLC_ID = 13 + STOPPLC_ID = 14 + + def AppendChunkToBlob(self, data, blobID, newBlobID): + raise NotImplementedError() + + def GetLogMessage(self, level, msgID, message): + raise NotImplementedError() + + def GetPLCID(self, plcID): + raise NotImplementedError() + + def GetPLCstatus(self, status): + raise NotImplementedError() + + def GetTraceVariables(self, debugToken, traces): + raise NotImplementedError() + + def MatchMD5(self, MD5, match): + raise NotImplementedError() + + def NewPLC(self, md5sum, plcObjectBlobID, extrafiles, success): + raise NotImplementedError() + + def PurgeBlobs(self): + raise NotImplementedError() + + def RepairPLC(self): + raise NotImplementedError() + + def ResetLogCount(self): + raise NotImplementedError() + + def SeedBlob(self, seed, blobID): + raise NotImplementedError() + + def SetTraceVariablesList(self, orders): + raise NotImplementedError() + + def StartPLC(self): + raise NotImplementedError() + + def StopPLC(self): + raise NotImplementedError() + + diff -r a6e7dd8bac36 -r 34da877021d5 erpc_interface/erpc_PLCObject/server.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/erpc_interface/erpc_PLCObject/server.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,339 @@ +# +# Generated by erpcgen 1.11.0 on Wed Jan 17 21:59:20 2024. +# +# AUTOGENERATED - DO NOT EDIT +# + +import erpc +from . import common, interface + +# Client for BeremizPLCObjectService +class BeremizPLCObjectServiceService(erpc.server.Service): + def __init__(self, handler): + super(BeremizPLCObjectServiceService, self).__init__(interface.IBeremizPLCObjectService.SERVICE_ID) + self._handler = handler + self._methods = { + interface.IBeremizPLCObjectService.APPENDCHUNKTOBLOB_ID: self._handle_AppendChunkToBlob, + interface.IBeremizPLCObjectService.GETLOGMESSAGE_ID: self._handle_GetLogMessage, + interface.IBeremizPLCObjectService.GETPLCID_ID: self._handle_GetPLCID, + interface.IBeremizPLCObjectService.GETPLCSTATUS_ID: self._handle_GetPLCstatus, + interface.IBeremizPLCObjectService.GETTRACEVARIABLES_ID: self._handle_GetTraceVariables, + interface.IBeremizPLCObjectService.MATCHMD5_ID: self._handle_MatchMD5, + interface.IBeremizPLCObjectService.NEWPLC_ID: self._handle_NewPLC, + interface.IBeremizPLCObjectService.PURGEBLOBS_ID: self._handle_PurgeBlobs, + interface.IBeremizPLCObjectService.REPAIRPLC_ID: self._handle_RepairPLC, + interface.IBeremizPLCObjectService.RESETLOGCOUNT_ID: self._handle_ResetLogCount, + interface.IBeremizPLCObjectService.SEEDBLOB_ID: self._handle_SeedBlob, + interface.IBeremizPLCObjectService.SETTRACEVARIABLESLIST_ID: self._handle_SetTraceVariablesList, + interface.IBeremizPLCObjectService.STARTPLC_ID: self._handle_StartPLC, + interface.IBeremizPLCObjectService.STOPPLC_ID: self._handle_StopPLC, + } + + def _handle_AppendChunkToBlob(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + newBlobID = erpc.Reference() + + # Read incoming parameters. + data = codec.read_binary() + blobID = codec.read_binary() + + # Invoke user implementation of remote function. + _result = self._handler.AppendChunkToBlob(data, blobID, newBlobID) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.APPENDCHUNKTOBLOB_ID, + sequence=sequence)) + if newBlobID.value is None: + raise ValueError("newBlobID.value is None") + codec.write_binary(newBlobID.value) + codec.write_uint32(_result) + + def _handle_GetLogMessage(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + message = erpc.Reference() + + # Read incoming parameters. + level = codec.read_uint8() + msgID = codec.read_uint32() + + # Invoke user implementation of remote function. + _result = self._handler.GetLogMessage(level, msgID, message) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.GETLOGMESSAGE_ID, + sequence=sequence)) + if message.value is None: + raise ValueError("message.value is None") + message.value._write(codec) + codec.write_uint32(_result) + + def _handle_GetPLCID(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + plcID = erpc.Reference() + + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.GetPLCID(plcID) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.GETPLCID_ID, + sequence=sequence)) + if plcID.value is None: + raise ValueError("plcID.value is None") + plcID.value._write(codec) + codec.write_uint32(_result) + + def _handle_GetPLCstatus(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + status = erpc.Reference() + + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.GetPLCstatus(status) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.GETPLCSTATUS_ID, + sequence=sequence)) + if status.value is None: + raise ValueError("status.value is None") + status.value._write(codec) + codec.write_uint32(_result) + + def _handle_GetTraceVariables(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + traces = erpc.Reference() + + # Read incoming parameters. + debugToken = codec.read_uint32() + + # Invoke user implementation of remote function. + _result = self._handler.GetTraceVariables(debugToken, traces) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.GETTRACEVARIABLES_ID, + sequence=sequence)) + if traces.value is None: + raise ValueError("traces.value is None") + traces.value._write(codec) + codec.write_uint32(_result) + + def _handle_MatchMD5(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + match = erpc.Reference() + + # Read incoming parameters. + MD5 = codec.read_string() + + # Invoke user implementation of remote function. + _result = self._handler.MatchMD5(MD5, match) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.MATCHMD5_ID, + sequence=sequence)) + if match.value is None: + raise ValueError("match.value is None") + codec.write_bool(match.value) + codec.write_uint32(_result) + + def _handle_NewPLC(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + success = erpc.Reference() + + # Read incoming parameters. + md5sum = codec.read_string() + plcObjectBlobID = codec.read_binary() + _n0 = codec.start_read_list() + extrafiles = [] + for _i0 in range(_n0): + _v0 = common.extra_file()._read(codec) + extrafiles.append(_v0) + + + # Invoke user implementation of remote function. + _result = self._handler.NewPLC(md5sum, plcObjectBlobID, extrafiles, success) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.NEWPLC_ID, + sequence=sequence)) + if success.value is None: + raise ValueError("success.value is None") + codec.write_bool(success.value) + codec.write_uint32(_result) + + def _handle_PurgeBlobs(self, sequence, codec): + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.PurgeBlobs() + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.PURGEBLOBS_ID, + sequence=sequence)) + codec.write_uint32(_result) + + def _handle_RepairPLC(self, sequence, codec): + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.RepairPLC() + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.REPAIRPLC_ID, + sequence=sequence)) + codec.write_uint32(_result) + + def _handle_ResetLogCount(self, sequence, codec): + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.ResetLogCount() + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.RESETLOGCOUNT_ID, + sequence=sequence)) + codec.write_uint32(_result) + + def _handle_SeedBlob(self, sequence, codec): + # Create reference objects to pass into handler for out/inout parameters. + blobID = erpc.Reference() + + # Read incoming parameters. + seed = codec.read_binary() + + # Invoke user implementation of remote function. + _result = self._handler.SeedBlob(seed, blobID) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.SEEDBLOB_ID, + sequence=sequence)) + if blobID.value is None: + raise ValueError("blobID.value is None") + codec.write_binary(blobID.value) + codec.write_uint32(_result) + + def _handle_SetTraceVariablesList(self, sequence, codec): + # Read incoming parameters. + _n0 = codec.start_read_list() + orders = [] + for _i0 in range(_n0): + _v0 = common.trace_order()._read(codec) + orders.append(_v0) + + + # Invoke user implementation of remote function. + _result = self._handler.SetTraceVariablesList(orders) + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.SETTRACEVARIABLESLIST_ID, + sequence=sequence)) + codec.write_uint32(_result) + + def _handle_StartPLC(self, sequence, codec): + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.StartPLC() + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.STARTPLC_ID, + sequence=sequence)) + codec.write_uint32(_result) + + def _handle_StopPLC(self, sequence, codec): + # Read incoming parameters. + + # Invoke user implementation of remote function. + _result = self._handler.StopPLC() + + # Prepare codec for reply message. + codec.reset() + + # Construct reply message. + codec.start_write_message(erpc.codec.MessageInfo( + type=erpc.codec.MessageType.kReplyMessage, + service=interface.IBeremizPLCObjectService.SERVICE_ID, + request=interface.IBeremizPLCObjectService.STOPPLC_ID, + sequence=sequence)) + codec.write_uint32(_result) + + diff -r a6e7dd8bac36 -r 34da877021d5 requirements.txt --- a/requirements.txt Sat Dec 09 01:03:43 2023 +0100 +++ b/requirements.txt Wed Jan 17 22:09:32 2024 +0100 @@ -13,6 +13,7 @@ contourpy==1.0.7 cryptography==40.0.2 cycler==0.11.0 +erpc==1.11.0 fonttools==4.39.3 gattrdict==2.0.1 hyperlink==21.0.0 @@ -30,12 +31,11 @@ pycountry==22.3.5 pycparser==2.21 pyparsing==3.0.9 -Pyro5==5.14 python-dateutil==2.8.2 pytz==2023.3 -serpent==1.41 six==1.16.0 sortedcontainers==2.4.0 +sslpsk3==1.1.1 Twisted==22.10.0 txaio==23.1.1 typing_extensions==4.5.0 diff -r a6e7dd8bac36 -r 34da877021d5 runtime/PLCObject.py --- a/runtime/PLCObject.py Sat Dec 09 01:03:43 2023 +0100 +++ b/runtime/PLCObject.py Wed Jan 17 22:09:32 2024 +0100 @@ -148,6 +148,7 @@ return int(self._GetLogCount(level)) elif self._loading_error is not None and level == 0: return 1 + return 0 @RunInMain def GetLogMessage(self, level, msgid): @@ -557,7 +558,7 @@ try: return self._GetPLCstatus() except EOFError: - return (PlcStatus.Disconnected, None) + return (PlcStatus.Disconnected, [0]*LogLevelsCount) @RunInMain def _GetPLCstatus(self): diff -r a6e7dd8bac36 -r 34da877021d5 runtime/PyroServer.py --- a/runtime/PyroServer.py Sat Dec 09 01:03:43 2023 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +0,0 @@ -#!/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. - - - -import sys -import os - -import Pyro5 -import Pyro5.server - -import runtime -from runtime.ServicePublisher import ServicePublisher - -Pyro5.config.SERIALIZER = "msgpack" - -def make_pyro_exposed_stub(method_name): - stub = lambda self, *args, **kwargs: \ - getattr(self.plc_object_instance, method_name)(*args, **kwargs) - stub.__name__ = method_name - Pyro5.server.expose(stub) - return stub - - -class PLCObjectPyroAdapter(type("PLCObjectPyroStubs", (), { - name: make_pyro_exposed_stub(name) for name in [ - "AppendChunkToBlob", - "GetLogMessage", - "GetPLCID", - "GetPLCstatus", - "GetTraceVariables", - "MatchMD5", - "NewPLC", - "PurgeBlobs", - "RemoteExec", - "RepairPLC", - "ResetLogCount", - "SeedBlob", - "SetTraceVariablesList", - "StartPLC", - "StopPLC" - ] -})): - def __init__(self, plc_object_instance): - self.plc_object_instance = plc_object_instance - - -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 - self.piper, self.pipew = None, 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")) - - if sys.stdout: - sys.stdout.flush() - - def PyroLoop(self, when_ready): - if self._to_be_published(): - self.Publish() - - while self.continueloop: - self.daemon = Pyro5.server.Daemon(host=self.ip_addr, port=self.port) - - self.daemon.register(PLCObjectPyroAdapter(runtime.GetPLCObjectSingleton()), "PLCObject") - - when_ready() - - self.daemon.requestLoop() - - self.Unpublish() - - def Restart(self): - self.daemon.shutdown(True) - - def Quit(self): - self.continueloop = False - self.daemon.shutdown() - if not sys.platform.startswith('win'): - if self.pipew is not None: - os.write(self.pipew, "goodbye") - - 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 a6e7dd8bac36 -r 34da877021d5 runtime/Stunnel.py --- a/runtime/Stunnel.py Sat Dec 09 01:03:43 2023 +0100 +++ b/runtime/Stunnel.py Wed Jan 17 22:09:32 2024 +0100 @@ -50,8 +50,8 @@ if not os.path.exists(_PSKpath): errorlog( 'Error: Pre-Shared-Key Secret in %s is missing!\n' % _PSKpath) - return None + return ("","") ID, _sep, PSK = open(_PSKpath).read().partition(':') PSK = PSK.rstrip('\n\r') return (ID, PSK) - return None + return ("","") diff -r a6e7dd8bac36 -r 34da877021d5 runtime/eRPCServer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/eRPCServer.py Wed Jan 17 22:09:32 2024 +0100 @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Written by Edouard TISSERANT (C) 2024 +# This file is part of Beremiz runtime +# See COPYING.Runtime file for copyrights details. + +import sys +import traceback +from inspect import getmembers, isfunction + +import erpc + +# eRPC service code +from erpc_interface.erpc_PLCObject.common import PSKID, PLCstatus, TraceVariables, trace_sample, PLCstatus_enum, log_message +from erpc_interface.erpc_PLCObject.interface import IBeremizPLCObjectService +from erpc_interface.erpc_PLCObject.server import BeremizPLCObjectServiceService + +from runtime import GetPLCObjectSingleton as PLC +from runtime.loglevels import LogLevelsDict +from runtime.ServicePublisher import ServicePublisher + + +CRITICAL_LOG_LEVEL = LogLevelsDict["CRITICAL"] + +def ReturnAsLastOutput(method, args_wrapper, *args): + args[-1].value = method(*args_wrapper(*args[:-1])) + return 0 + +def TranslatedReturnAsLastOutput(translator): + def wrapper(method, args_wrapper, *args): + args[-1].value = translator(method(*args_wrapper(*args[:-1]))) + return 0 + return wrapper + + +ReturnWrappers = { + "AppendChunkToBlob":ReturnAsLastOutput, + "GetLogMessage":TranslatedReturnAsLastOutput( + lambda res:log_message(*res)), + "GetPLCID":TranslatedReturnAsLastOutput( + lambda res:PSKID(*res)), + "GetPLCstatus":TranslatedReturnAsLastOutput( + lambda res:PLCstatus(getattr(PLCstatus_enum, res[0]),res[1])), + "GetTraceVariables":TranslatedReturnAsLastOutput( + lambda res:TraceVariables(res[0],[trace_sample(*sample) for sample in res[1]])), + "MatchMD5":ReturnAsLastOutput, + "NewPLC":ReturnAsLastOutput, + "SeedBlob":ReturnAsLastOutput, +} + +ArgsWrappers = { + "AppendChunkToBlob": + lambda data, blobID:(data, bytes(blobID)), + "NewPLC": + lambda md5sum, plcObjectBlobID, extrafiles: ( + md5sum, bytes(plcObjectBlobID), [(f.fname, bytes(f.blobID)) for f in extrafiles]), + "SetTraceVariablesList": + lambda orders : ([(order.idx, order.iectype, order.force) for order in orders],) +} + +def rpc_wrapper(method_name): + PLCobj = PLC() + method=getattr(PLCobj, method_name) + args_wrapper = ArgsWrappers.get(method_name, lambda *x:x) + return_wrapper = ReturnWrappers.get(method_name, + lambda method, args_wrapper, *args: method(*args_wrapper(*args))) + + def exception_wrapper(self, *args): + try: + print("Srv "+method_name) + return_wrapper(method, args_wrapper, *args) + return 0 + except Exception as e: + print(traceback.format_exc()) + PLCobj.LogMessage(CRITICAL_LOG_LEVEL, f'eRPC call {method_name} Exception "{str(e)}"') + raise + + return exception_wrapper + + +class eRPCServer(object): + def __init__(self, servicename, ip_addr, port): + self.continueloop = True + self.server = None + self.transport = 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(_("eRPC port :"), self.port) + + if self._to_be_published(): + print(_("Publishing service on local network")) + + if sys.stdout: + sys.stdout.flush() + + def Loop(self, when_ready): + if self._to_be_published(): + self.Publish() + + while self.continueloop: + + # service handler calls PLC object though erpc_stubs's wrappers + handler = type( + "PLCObjectServiceHandlder", + (IBeremizPLCObjectService,), + {name: rpc_wrapper(name) + for name,_func in getmembers(IBeremizPLCObjectService, isfunction)})() + + service = BeremizPLCObjectServiceService(handler) + + # TODO initialize Serial transport layer if selected + # transport = erpc.transport.SerialTransport(device, baudrate) + + # initialize TCP transport layer + self.transport = erpc.transport.TCPTransport(self.ip_addr, int(self.port), True) + + self.server = erpc.simple_server.SimpleServer(self.transport, erpc.basic_codec.BasicCodec) + self.server.add_service(service) + + when_ready() + + self.server.run() + + self.Unpublish() + + def Restart(self): + self.server.stop() + self.transport.stop() + + def Quit(self): + self.continueloop = False + self.server.stop() + self.transport.stop() + + def Publish(self): + self.servicepublisher = ServicePublisher("ERPC") + 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