Merge remote-tracking branch 'hggit/python3' into python3
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Fri, 22 Nov 2024 16:31:51 +0100
changeset 4043 b217fb367574
parent 4042 03df7946c2fa (diff)
parent 4036 102555078a0c (current diff)
child 4044 2a4b8b59d165
Merge remote-tracking branch 'hggit/python3' into python3
ProjectController.py
connectors/ERPC/__init__.py
runtime/eRPCServer.py
--- a/ProjectController.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/ProjectController.py	Fri Nov 22 16:31:51 2024 +0100
@@ -46,7 +46,7 @@
 
 import features
 import connectors
-import util.paths as paths
+import util.paths as pathutils
 from util.misc import CheckPathPerm, GetClassImporter
 from util.MiniTextControler import MiniTextControler
 from util.ProcessLogger import ProcessLogger
@@ -65,7 +65,6 @@
 from ConfigTreeNode import ConfigTreeNode, XSDSchemaErrorMessage
 from POULibrary import UserAddressedException
 
-base_folder = paths.AbsParentDir(__file__)
 
 MATIEC_ERROR_MODEL = re.compile(
     r".*\.st:(\d+)-(\d+)\.\.(\d+)-(\d+): (?:error)|(?:warning) : (.*)$")
@@ -116,7 +115,7 @@
     def findCmd(self):
         cmd = "iec2c" + (".exe" if os.name == 'nt' else "")
         paths = [
-            os.path.join(base_folder, "matiec")
+            pathutils.ThirdPartyPath("matiec")
         ]
         path = self.findObject(
             paths, lambda p: os.path.isfile(os.path.join(p, cmd)))
@@ -129,7 +128,7 @@
 
     def findLibPath(self):
         paths = [
-            os.path.join(base_folder, "matiec", "lib"),
+            pathutils.ThirdPartyPath("matiec", "lib"),
             "/usr/lib/matiec"
         ]
         path = self.findObject(
--- a/connectors/ERPC/__init__.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/connectors/ERPC/__init__.py	Fri Nov 22 16:31:51 2024 +0100
@@ -22,6 +22,7 @@
 import PSKManagement as PSK
 from connectors.ERPC.PSK_Adapter import SSLPSKClientTransport
 from connectors.ConnectorBase import ConnectorBase
+from connectors.ERPC_URI import per_scheme_model
 
 enum_to_PLCstatus = dict(map(lambda t:(t[1],t[0]),getmembers(PLCstatus_enum, lambda x:type(x)==int)))
 
@@ -74,6 +75,36 @@
             for idx, force in orders],)
 }
 
+def rpc_wrapper(method_name, confnodesroot):
+    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:
+            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)
+        except Exception as e:
+            errmess = _("Exception calling remote PLC object fucntion %s:\n") % method_name \
+                        + traceback.format_exc()
+            confnodesroot.logger.write_error(errmess + "\n")
+            confnodesroot._SetConnector(None)
+
+        return self.PLCObjDefaults.get(method_name)
+    return exception_wrapper
+
+
+
 def ERPC_connector_factory(uri, confnodesroot):
     """
     returns the ERPC connector
@@ -84,92 +115,71 @@
     # 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:
-            # default port depends on security
-            port = 4000 if IDhash else 3000
-
-        if not IDhash and _scheme=="ERPCS":
-            confnodesroot.logger.write_error(
-                f'Invalid URI "{uri}": ERPCS requires PLC ID after "#"\n')
-            return None
-        elif IDhash and _scheme!="ERPCS":
-            confnodesroot.logger.write_error(
-                f'URI "{uri}": Non-encrypted ERPC does not take a PLC ID after "#"\n')
-            return None
-
+        scheme, location = uri.split("://",1)
+        _model, _useID, parser, _builder = per_scheme_model[scheme]
+        location_data = parser(location)
     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:
-                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)
-            except Exception as e:
-                errmess = _("Exception calling remote PLC object fucntion %s:\n") % method_name \
-                          + traceback.format_exc()
-                confnodesroot.logger.write_error(errmess + "\n")
-                confnodesroot._SetConnector(None)
-
-            return self.PLCObjDefaults.get(method_name)
-        return exception_wrapper
-
-
     PLCObjectERPCProxy = type(
         "PLCObjectERPCProxy",
         (ConnectorBase, BeremizPLCObjectServiceClient),
-        {name: rpc_wrapper(name)
+        {name: rpc_wrapper(name, confnodesroot)
             for name,_func in getmembers(IBeremizPLCObjectService, isfunction)})
 
-    try:
-        if IDhash:
-            ID = IDhash[0]
-            # load PSK from project
-            secpath = os.path.join(confnodesroot.ProjectPath, 'psk', ID + '.secret')
-            if not os.path.exists(secpath):
+    if scheme in ["ERPCS", "ERPC"]:
+        if scheme=="ERPCS":
+            ID = location_data["ID"]
+            if not ID:
                 confnodesroot.logger.write_error(
-                    'Error: Pre-Shared-Key Secret in %s is missing!\n' % secpath)
+                    f'Invalid URI "{uri}": ERPCS requires PLC ID after "#"\n')
                 return None
-            secret = open(secpath).read().partition(':')[2].rstrip('\n\r').encode()
-            transport = SSLPSKClientTransport(host, port, (secret, ID.encode()))
+            default_port = 4000
         else:
-            # TODO if serial URI then 
-            # transport = erpc.transport.SerialTransport(device, baudrate)
+            ID = None
+            if "#" in location:
+                confnodesroot.logger.write_error(
+                    f'URI "{uri}": Non-encrypted ERPC does not take a PLC ID after "#"\n')
+                return None
+            default_port = 3000
 
-            transport = erpc.transport.TCPTransport(host, port, False)
+        host = location_data["host"]
+        port = location_data["port"]
+        port = int(port) if port else default_port
 
-        clientManager = erpc.client.ClientManager(transport, erpc.basic_codec.BasicCodec)
-        client = PLCObjectERPCProxy(clientManager)
+        try:
+            if ID:
+                # load PSK from project
+                secpath = os.path.join(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').encode()
+                transport = SSLPSKClientTransport(host, port, (secret, ID.encode())) # type: ignore
+            else:
 
-    except Exception as e:
+                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=uri, ex=str(e)))
+            return None
+
+    else:
+        # TODO if serial URI then 
+        # transport = erpc.transport.SerialTransport(device, baudrate)
+
         confnodesroot.logger.write_error(
-            _("Connection to {loc} failed with exception {ex}\n").format(
-                loc=locator, ex=str(e)))
+            _("Unknown scheme {scheme} in URI {uri}\n").format(
+                scheme=scheme, uri=uri))
         return None
-
     # Check connection is effective.
     IDPSK = client.GetPLCID()
     if IDPSK:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/ERPC_URI.py	Fri Nov 22 16:31:51 2024 +0100
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+from itertools import repeat, islice, chain
+
+## URI parsing functions
+
+def split_as_dict(s, sep, labels):
+    return dict(zip(labels, islice(chain(s.split(sep), repeat("")), len(labels))))
+
+def parse_tcp(loc):
+    return split_as_dict(loc, ":", ["host", "port"])
+
+def parse_sslpsk(loc):
+    locals().update(**split_as_dict(loc, "#", ["hostport", "ID"]))
+    return dict(**parse_tcp(hostport), ID=ID) # type: ignore
+
+def parse_serial(loc):
+    return split_as_dict(loc, "@", ["device", "baudrate"])
+
+def parse_usb(loc):
+    return split_as_dict(loc, ":", ["VID", "PID", "serialnumber"])
+
+## URI building functions
+
+def build_tcp(fields):
+    if fields['port']:
+        return "{host}:{port}".format(**fields)
+    return fields['host']
+
+def build_sslpsk(fields):
+    return "{hostport}#{ID}".format(hostport=build_tcp(fields), **fields)
+
+def build_serial(fields):
+    if fields['baudrate']:
+        return "{device}@{baudrate}".format(**fields)
+    return fields['device']
+
+def build_usb(fields):
+    if fields['serialnumber']:
+        return "{VID}:{PID}:{serialnumber}".format(**fields)
+    if fields['PID']:
+        return "{VID}:{PID}".format(**fields)
+    return fields['VID']
+    
+## Dialog fields definition
+
+model_tcp = [('host', _("Host:")),
+             ('port', _("Port:"))]
+
+model_serial = [('device', _("Device:")),
+                ('baudrate', _("Baud rate:"))]
+
+model_usb = [('VID', _("Vendor ID:")),
+             ('PID', _("Product ID:")),
+             ('serialnumber', _("Serial number:"))]
+
+
+## Schemes description
+
+schemes_desc = [
+#   ( scheme name ,  data model , use ID,   parser   ,    builder  )
+    ("LOCAL",       [],           False, lambda x: {}, lambda x: ""),
+    ("ERPC",        model_tcp,    False, parse_tcp,    build_tcp   ),
+    ("ERPCS",       model_tcp,    True,  parse_sslpsk, build_sslpsk),
+    ("ERPC-SERIAL", model_serial, False, parse_serial, build_serial),
+    ("ERPC-USB",    model_usb,    False, parse_usb,    build_usb   )]
+
+per_scheme_model = {sch: desc for sch, *desc in schemes_desc}
+
--- a/connectors/ERPC_dialog.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/connectors/ERPC_dialog.py	Fri Nov 22 16:31:51 2024 +0100
@@ -3,46 +3,24 @@
 
 # See COPYING file for copyrights details.
 
+from connectors.ERPC_URI import schemes_desc, per_scheme_model
+from connectors.SchemeEditor import SchemeEditor
+
+## Scheme list for the dialog's combobox
+
+Schemes = list(zip(*schemes_desc))[0]
 
 
-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), ("ERPCS", model, True)]
-
-Schemes = list(zip(*models))[0]
-
-_PerSchemeConf = {sch: (mod, sec) for sch, mod, sec in models}
-
+## Specialized SchemeEditor panel for ERPC 
 
 class ERPC_dialog(SchemeEditor):
     def __init__(self, scheme, *args, **kwargs):
-        # ID selector is enabled only on ERPC (secure)
-        self.model, self.EnableIDSelector = _PerSchemeConf[scheme]
+        self.model, self.EnableIDSelector, self.parser, self.builder = per_scheme_model[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())
+        self.SetFields(self.parser(loc))
 
     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 ''
+        return self.builder(self.GetFields())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/ZeroConfListener.py	Fri Nov 22 16:31:51 2024 +0100
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+import weakref
+from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses
+import threading
+
+service_type = '_Beremiz._tcp.local.'
+
+class ZeroConfListenerClass:
+    def __init__(self, dialog):
+        self.dialog = weakref.ref(dialog)
+
+        self.IfacesMonitorState = None
+        self.IfacesMonitorTimer = None
+        self.Browser = None
+        self.ZeroConfInstance = None
+        self.PublishedServices = set()
+
+        self.start()
+
+    def __del__(self):
+        self.stop()
+
+    def start(self):
+        self.ZeroConfInstance = Zeroconf()
+        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
+        # Start the ifaces_monitor timer thread
+        self.IfacesMonitorTimer = threading.Timer(1.0, self.ifaces_monitor)
+        self.IfacesMonitorTimer.start()
+
+    def stop(self):
+        if self.IfacesMonitorTimer is not None:
+            self.IfacesMonitorTimer.cancel()
+            self.IfacesMonitorTimer = None
+
+        if self.Browser is not None:
+            self.Browser.cancel()
+            self.Browser = None
+
+        if  self.ZeroConfInstance is not None:
+            self.ZeroConfInstance.close()
+            self.ZeroConfInstance = None
+
+    def update_service(self, zeroconf, _type, name):
+        self.remove_service(zeroconf, _type, name)
+        self.add_service(zeroconf, _type, name)
+
+    def add_service(self, zeroconf, _type, name):
+        dialog = self.dialog()
+        if not dialog:
+            return
+
+        info = self.ZeroConfInstance.get_service_info(_type, name)
+        if info is None:
+            return
+
+        typename = info.properties.get(b"protocol", None).decode()
+        ip = str(info.parsed_addresses()[0])
+        port = info.port
+        dialog.addService(typename, ip, port, name)
+        self.PublishedServices.add(name)
+
+    def remove_service(self, zeroconf, _type, name):
+        dialog = self.dialog()
+        if not dialog:
+            return
+        
+        if name in self.PublishedServices:
+            dialog.removeService(name)
+            self.PublishedServices.discard(name)
+
+    def ifaces_monitor(self):
+        dialog = self.dialog()
+        if not dialog:
+            return
+
+        NewState = get_all_addresses()
+        OldState = self.IfacesMonitorState
+        self.IfacesMonitorState = NewState
+        do_restart = False
+        
+        if OldState is not None:
+            # detect if a new address appeared
+            for addr in NewState:
+                if addr not in OldState:
+                    do_restart = True
+                    break
+                else:
+                    OldState.remove(addr)
+            # detect if an address disappeared
+            if len(OldState) > 0:
+                do_restart = True
+
+            
+        if do_restart:
+            self.stop()
+            
+            while self.PublishedServices:
+                dialog.removeService(self.PublishedServices.pop())
+        
+            self.start()
+        else:
+            # Restart the ifaces_monitor timer thread
+            self.IfacesMonitorTimer = threading.Timer(1.0, self.ifaces_monitor)
+            self.IfacesMonitorTimer.start()
--- a/connectors/__init__.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/connectors/__init__.py	Fri Nov 22 16:31:51 2024 +0100
@@ -83,6 +83,8 @@
         scheme = _scheme
     elif _scheme[-1] == 'S' and _scheme[:-1] in connectors:
         scheme = _scheme[:-1]
+    elif _scheme.split("-")[0] in connectors:
+        scheme = _scheme.split("-")[0]
     else:
         return None
 
--- a/controls/DiscoveryPanel.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/controls/DiscoveryPanel.py	Fri Nov 22 16:31:51 2024 +0100
@@ -24,57 +24,9 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 
-import socket
-import weakref
 import wx
 import wx.lib.mixins.listctrl as listmix
-from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses
-
-service_type = '_Beremiz._tcp.local.'
-
-class ZeroConfListenerClass:
-    def __init__(self, dialog):
-        self.dialog = weakref.ref(dialog)
-        self.ZeroConfInstance = Zeroconf()
-        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
-
-    def clean(self):
-        if self.Browser is not None:
-            self.Browser.cancel()
-            self.Browser = None
-        if  self.ZeroConfInstance is not None:
-            self.ZeroConfInstance.close()
-            self.ZeroConfInstance = None
-
-    def __del__(self):
-        self.clean()
-
-    def update_service(self, zeroconf, _type, name):
-        self.remove_service(zeroconf, _type, name)
-        self.add_service(zeroconf, _type, name)
-
-    def add_service(self, zeroconf, _type, name):
-        info = self.ZeroConfInstance.get_service_info(_type, name)
-        if info is None:
-            return
-        typename = info.properties.get(b"protocol", None).decode()
-        ip = str(info.parsed_addresses()[0])
-        port = info.port
-
-        wx.CallAfter(self._add_service, typename, ip, port, name)
-
-    def _add_service(self, typename, ip, port, name):
-        dialog = self.dialog()
-        if not dialog: return
-        dialog._addService(typename, ip, port, name)
-
-    def remove_service(self, zeroconf, _type, name):
-        wx.CallAfter(self._remove_service, name)
-
-    def _remove_service(self, name):
-        dialog = self.dialog()
-        if not dialog: return
-        dialog._removeService(name)
+from connectors.ZeroConfListener import ZeroConfListenerClass
 
 class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
     def __init__(self, parent, name, pos=wx.DefaultPosition,
@@ -82,7 +34,6 @@
         wx.ListCtrl.__init__(self, parent, wx.ID_ANY, pos, size, style, name=name)
         listmix.ListCtrlAutoWidthMixin.__init__(self)
 
-
 class DiscoveryPanel(wx.Panel, listmix.ColumnSorterMixin):
 
     def _init_coll_MainSizer_Items(self, parent):
@@ -118,15 +69,11 @@
         self.ServicesList = AutoWidthListCtrl(
             name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
             style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
-        self.ServicesList.InsertColumn(0, _('NAME'))
-        self.ServicesList.InsertColumn(1, _('TYPE'))
-        self.ServicesList.InsertColumn(2, _('IP'))
-        self.ServicesList.InsertColumn(3, _('PORT'))
-        self.ServicesList.SetColumnWidth(0, 150)
-        self.ServicesList.SetColumnWidth(1, 150)
-        self.ServicesList.SetColumnWidth(2, 150)
-        self.ServicesList.SetColumnWidth(3, 150)
+        for col, (label, width) in enumerate([
+                (_('NAME'), 150), (_('TYPE'), 150), (_('IP'), 150), (_('PORT'), 150)]):
+            self.ServicesList.InsertColumn(col, label)
         self.ServicesList.SetInitialSize(wx.Size(-1, 300))
+            
         self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self.ServicesList)
         self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, self.ServicesList)
 
@@ -156,46 +103,27 @@
 
         self._init_ctrls(parent)
 
+        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
+
         self.itemDataMap = {}
         self.nextItemId = 0
 
         self.URI = None
 
+        self.LatestSelection = None
+
+        self.ZeroConfListener = None
+
         self.RefreshList()
-        self.LatestSelection = None
-
-        self.IfacesMonitorState = None
-        self.IfacesMonitorTimer = wx.Timer(self)
-        self.IfacesMonitorTimer.Start(2000)
-        self.ZeroConfListener = None
-        self.Bind(wx.EVT_TIMER, self.IfacesMonitor, self.IfacesMonitorTimer)
+
 
     def _cleanup(self):
-        if self.IfacesMonitorTimer is not None:
-            self.IfacesMonitorTimer.Stop()
-            self.IfacesMonitorTimer = None
         if self.ZeroConfListener is not None:
-            self.ZeroConfListener.clean()
+            self.ZeroConfListener.stop()
             self.ZeroConfListener = None
 
-    def __del__(self):
+    def OnDestroy(self, event):
         self._cleanup()
-
-    def Destroy(self):
-        self._cleanup()
-        wx.Panel.Destroy(self)
-
-    def IfacesMonitor(self, event):
-        NewState = get_all_addresses()
-
-        if self.IfacesMonitorState != NewState:
-            if self.IfacesMonitorState is not None:
-                # refresh only if a new address appeared
-                for addr in NewState:
-                    if addr not in self.IfacesMonitorState:
-                        self.RefreshList()
-                        break
-            self.IfacesMonitorState = NewState
         event.Skip()
 
     def RefreshList(self):
@@ -246,6 +174,9 @@
             #     return str("MDNS://%s" % svcname)
         return None
 
+    def removeService(self, name):
+        wx.CallAfter(self._removeService, name)
+
     def _removeService(self, name):
         '''
         called when a service with the desired type goes offline.
@@ -263,6 +194,8 @@
                 self.ServicesList.DeleteItem(idx)
                 break
 
+    def addService(self, typename, ip, port, name):
+        wx.CallAfter(self._addService, typename, ip, port, name)
 
     def _addService(self, typename, ip, port, name):
         '''
--- a/runtime/ServicePublisher.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/runtime/ServicePublisher.py	Fri Nov 22 16:31:51 2024 +0100
@@ -34,7 +34,6 @@
 
 class ServicePublisher(object):
     def __init__(self, protocol):
-        # type: fully qualified service type name
         self.serviceproperties = {
             'description': 'Beremiz remote PLC',
             'protocol': protocol
@@ -50,12 +49,12 @@
     def RegisterService(self, name, ip, port):
         try:
             self._RegisterService(name, ip, port)
-        except Exception:
+        except Exception as e:
+            print(f"Failed to register service ({str(e)}), retrying in 2 seconds")
             self.retrytimer = threading.Timer(2, self.RegisterService, [name, ip, port])
             self.retrytimer.start()
 
     def _RegisterService(self, name, ip, port):
-        # name: fully qualified service name
         self.service_name = '%s.%s' % (name, service_type)
         self.name = name
         self.port = port
@@ -72,12 +71,14 @@
         print("MDNS brodcasted service address :" + ip)
         self.ip_32b = socket.inet_aton(ip)
 
-        self.server.register_service(
-            zeroconf.ServiceInfo(service_type,
-                                 self.service_name,
-                                 self.ip_32b,
-                                 self.port,
-                                 properties=self.serviceproperties))
+        self.service_info = zeroconf.ServiceInfo(
+            service_type,
+            self.service_name,
+            self.port,
+            addresses=[self.ip_32b],
+            properties=self.serviceproperties)
+
+        self.server.register_service(self.service_info)
         self.retrytimer = None
 
     def UnRegisterService(self):
@@ -85,12 +86,7 @@
             self.retrytimer.cancel()
 
         if self.server is not None:
-            self.server.unregister_service(
-                zeroconf.ServiceInfo(service_type,
-                                     self.service_name,
-                                     self.ip_32b,
-                                     self.port,
-                                     properties=self.serviceproperties))
+            self.server.unregister_service(self.service_info)
             self.server.close()
             self.server = None
 
--- a/runtime/eRPCServer.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/runtime/eRPCServer.py	Fri Nov 22 16:31:51 2024 +0100
@@ -87,7 +87,7 @@
         self.transport = None
         self.servicename = servicename
         self.ip_addr = ip_addr
-        self.port = port
+        self.port = int(port)
         self.servicepublisher = None
 
     def _to_be_published(self):
@@ -120,7 +120,7 @@
         # transport = erpc.transport.SerialTransport(device, baudrate)
 
         # initialize TCP transport layer
-        self.transport = erpc.transport.TCPTransport(self.ip_addr, int(self.port), True)
+        self.transport = erpc.transport.TCPTransport(self.ip_addr, self.port, True)
 
         self.server = erpc.simple_server.SimpleServer(self.transport, erpc.basic_codec.BasicCodec)
         self.server.add_service(service)
--- a/util/paths.py	Mon Nov 11 15:22:44 2024 +0100
+++ b/util/paths.py	Fri Nov 22 16:31:51 2024 +0100
@@ -50,6 +50,10 @@
     """
     Return folder where to find sibling projects like Modbus, CanFestival, BACnet
     """
+    env_name = name.upper() + "_PATH"
+    if env_name in os.environ:
+        return os.path.join(os.environ[env_name], *suffixes)
+    
     return os.path.join(AbsParentDir(__file__, 2), name, *suffixes)
 
 def Bpath(*names):