diff options
| -rw-r--r-- | api_layers/arduino_layer.py | 40 | ||||
| -rw-r--r-- | core/profile.py | 28 | ||||
| -rw-r--r-- | devices/arduino_device.py | 8 | ||||
| -rw-r--r-- | devices/serial_device.py | 5 |
4 files changed, 78 insertions, 3 deletions
diff --git a/api_layers/arduino_layer.py b/api_layers/arduino_layer.py index a3c3584..07520d6 100644 --- a/api_layers/arduino_layer.py +++ b/api_layers/arduino_layer.py @@ -575,6 +575,46 @@ class ArduinoLayer: return desc return "" + @staticmethod + def port_serial_number(port: str) -> str: + """USB serial number for a currently-connected port, if the OS/driver + exposes one — empty string if unavailable or the port isn't enumerable.""" + if not _SERIAL_AVAILABLE: + return "" + try: + for p in serial.tools.list_ports.comports(): + if p.device == port: + return getattr(p, "serial_number", None) or "" + except Exception as e: + print(f"[ArduinoLayer] port_serial_number() error: {e}") + return "" + + @staticmethod + def find_port_by_identity(description: str = "", serial_number: str = "") -> Optional[str]: + """Re-map a device saved in a profile to whatever port it enumerates + as on *this* machine — COM/tty numbers aren't stable across machines + (or even across replugs on Linux). Prefers USB serial number (unique + even between two identical board models); falls back to an exact + description-string match. Returns None if nothing matches — caller + should fall back to the saved port as-is (device will show ERROR + until the operator fixes it manually).""" + if not _SERIAL_AVAILABLE: + return None + try: + ports = list(serial.tools.list_ports.comports()) + except Exception as e: + print(f"[ArduinoLayer] find_port_by_identity() error: {e}") + return None + if serial_number: + for p in ports: + if (getattr(p, "serial_number", None) or "") == serial_number: + return p.device + if description: + for p in ports: + if (p.description or "") == description: + return p.device + return None + def __repr__(self): mode = "SIM" if self.simulate else f"HW:{self.port}@{self.baud}" return f"<ArduinoLayer {mode} pins={self.analog_pins}>" diff --git a/core/profile.py b/core/profile.py index 1fa667a..c0ae5f1 100644 --- a/core/profile.py +++ b/core/profile.py @@ -325,9 +325,33 @@ class ProfileManager: # "name" is a display label, not a constructor arg — every device # factory builds its own default name internally, so apply it # after construction instead of passing it through. - custom_name = dev_cfg.get("name") + # "port_desc"/"port_serial" are identity metadata for port + # re-detection below, not constructor args either. + custom_name = dev_cfg.get("name") + port_desc = dev_cfg.get("port_desc", "") + port_serial = dev_cfg.get("port_serial", "") kwargs = {k: v for k, v in dev_cfg.items() - if k not in ("device_type", "name")} + if k not in ("device_type", "name", "port_desc", "port_serial")} + + # Re-map the saved port to whatever it enumerates as on *this* + # machine — COM/tty numbers aren't stable across machines (or + # even across replugs on some OSes). Only if the saved port + # isn't already present verbatim (same-machine reload — nothing + # to fix) and we have something to match against. + if not kwargs.get("simulate", True) and "port" in kwargs and (port_desc or port_serial): + from api_layers.arduino_layer import ArduinoLayer + current_ports = {dev for dev, _ in ArduinoLayer.list_ports()} + if kwargs["port"] not in current_ports: + found = ArduinoLayer.find_port_by_identity(port_desc, port_serial) + if found: + print(f"[Profile] '{dev_id}': port {kwargs['port']!r} not found, " + f"re-mapped to {found!r} by device identity") + kwargs["port"] = found + else: + print(f"[Profile] '{dev_id}': port {kwargs['port']!r} not found and " + f"no matching device on this machine — will show ERROR " + f"until reconfigured") + dev = factory(**kwargs) if custom_name: dev.info.name = custom_name diff --git a/devices/arduino_device.py b/devices/arduino_device.py index 3ad0c7b..1641eaa 100644 --- a/devices/arduino_device.py +++ b/devices/arduino_device.py @@ -206,7 +206,7 @@ class ArduinoDevice(BaseDevice): return False def get_save_config(self) -> dict: - return { + cfg = { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, "name": self.info.name, @@ -217,6 +217,12 @@ class ArduinoDevice(BaseDevice): "port": self._port, "baud": self._baud, } + if not self.simulate: + # Saved so a profile loaded on another machine (different COM/tty + # numbering) can still find this device — see ProfileManager.apply(). + cfg["port_desc"] = ArduinoLayer.describe_port(self._port) + cfg["port_serial"] = ArduinoLayer.port_serial_number(self._port) + return cfg def get_config_widget(self) -> QWidget: return ArduinoConfigWidget(self) diff --git a/devices/serial_device.py b/devices/serial_device.py index cef34e8..d402f09 100644 --- a/devices/serial_device.py +++ b/devices/serial_device.py @@ -210,6 +210,11 @@ class SerialDevice(BaseDevice): "parse_format": self._fmt, "simulate": self.simulate, } + if not self.simulate: + # Saved so a profile loaded on another machine (different COM/tty + # numbering) can still find this device — see ProfileManager.apply(). + cfg["port_desc"] = ArduinoLayer.describe_port(self._port) + cfg["port_serial"] = ArduinoLayer.port_serial_number(self._port) if self._fmt in _GENERIC_FORMATS: cfg.update({ "num_channels": len(self.info.channels), |
