# HG changeset patch # User Edouard Tisserant # Date 1732289511 -3600 # Node ID b217fb36757440b74450af5c049647e912d589aa # Parent 03df7946c2fab1831997133196f4ece6853445b2# Parent 102555078a0ce1468a0e11fa4a1eecb38613aa37 Merge remote-tracking branch 'hggit/python3' into python3 diff -r 102555078a0c -r b217fb367574 ProjectController.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( diff -r 102555078a0c -r b217fb367574 connectors/ERPC/__init__.py --- 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[^\s:]+):?(?P\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: diff -r 102555078a0c -r b217fb367574 connectors/ERPC_URI.py --- /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} + diff -r 102555078a0c -r b217fb367574 connectors/ERPC_dialog.py --- 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()) diff -r 102555078a0c -r b217fb367574 connectors/ZeroConfListener.py --- /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() diff -r 102555078a0c -r b217fb367574 connectors/__init__.py --- 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 diff -r 102555078a0c -r b217fb367574 controls/DiscoveryPanel.py --- 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): ''' diff -r 102555078a0c -r b217fb367574 runtime/ServicePublisher.py --- 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 diff -r 102555078a0c -r b217fb367574 runtime/eRPCServer.py --- 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) diff -r 102555078a0c -r b217fb367574 util/paths.py --- 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):