Replace PYRO with ERPC. Work In Progress.
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Wed, 17 Jan 2024 22:09:32 +0100
changeset 3884 34da877021d5
parent 3883 a6e7dd8bac36
child 3885 22a009561502
Replace PYRO with ERPC. Work In Progress.
Beremiz_service.py
connectors/ConnectorBase.py
connectors/ERPC/PSK_Adapter.py
connectors/ERPC/__init__.py
connectors/ERPC_dialog.py
connectors/PYRO/PSK_Adapter.py
connectors/PYRO/__init__.py
connectors/PYRO_dialog.py
connectors/WAMP/__init__.py
connectors/__init__.py
erpc_interface/__init__.py
erpc_interface/erpc_PLCObject.erpc
erpc_interface/erpc_PLCObject/__init__.py
erpc_interface/erpc_PLCObject/client.py
erpc_interface/erpc_PLCObject/common.py
erpc_interface/erpc_PLCObject/interface.py
erpc_interface/erpc_PLCObject/server.py
requirements.txt
runtime/PLCObject.py
runtime/PyroServer.py
runtime/Stunnel.py
runtime/eRPCServer.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:
--- 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,
--- /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)
+
+
--- /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<host>[^\s:]+):?(?P<port>\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
--- /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 ''
--- 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'(?P<protocol>PYROLOCPSK)://(?P<hostname>[^\s:]+):?(?P<port>\d+)?/(?P<name>\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")
--- 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
--- 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 ''
--- 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)
--- 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()
--- /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
+#
+
--- /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<trace_sample> 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<extra_file> 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<trace_order> orders) -> uint32
+    StartPLC() -> uint32
+    StopPLC() -> uint32
+}
--- /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
--- /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
+
+
--- /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<trace_sample>
+
+    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__()
+
--- /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()
+
+
--- /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)
+
+
--- 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
--- 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):
--- 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
--- 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 ("","")
--- /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