#!/usr/bin/env python3 """lvx-resolve-links — create /dev/lvx6048-N symlinks keyed to PI18 serial. Globs /dev/hidraw*, sends PI18 `ID` to each, and creates /dev/lvx6048-1 -> /dev/hidrawX where X's serial matches SERIAL_UNIT_1 /dev/lvx6048-2 -> /dev/hidrawX where X's serial matches SERIAL_UNIT_2 Must run as root. Intended as a systemd oneshot before powermon*.service. Runs a single discovery pass with exclusive access — unlike powermon's native resolve_path which each service performs independently at startup, causing collisions when a sibling service is already holding the target hidraw. Edit SERIAL_UNIT_1 / SERIAL_UNIT_2 when a unit is replaced. """ from __future__ import annotations import asyncio import glob import os import sys SERIAL_UNIT_1 = "1496142109100037000000" SERIAL_UNIT_2 = "1496142408100255000000" LINK_FOR_SERIAL = { SERIAL_UNIT_1: "/dev/lvx6048-1", SERIAL_UNIT_2: "/dev/lvx6048-2", } sys.path.insert(0, "/home/noise/.local/share/uv/tools/powermon/lib/python3.11/site-packages") from powermon.protocols import get_protocol_definition # noqa: E402 from powermon.ports.usbport import USBPort # noqa: E402 async def probe_serial(path: str) -> str | None: proto = get_protocol_definition(protocol="PI18") port = USBPort(path=path, protocol=proto) port.path = path try: await port.connect() if not port.is_connected(): return None cmd = proto.get_id_command() res = await port.send_and_receive(command=cmd) await port.disconnect() except Exception as e: print(f" probe {path}: {e.__class__.__name__}: {e}", file=sys.stderr) return None if res is None or not getattr(res, "is_valid", False) or not res.readings: return None return str(res.readings[0].data_value) def _relink(link: str, target: str) -> None: target_basename = os.path.basename(target) try: if os.path.islink(link) or os.path.exists(link): os.unlink(link) except FileNotFoundError: pass os.symlink(target_basename, link) async def main() -> int: candidates = sorted(glob.glob("/dev/hidraw*")) if not candidates: print("no /dev/hidraw* devices present", file=sys.stderr) return 1 sn_to_path: dict[str, str] = {} for p in candidates: sn = await probe_serial(p) if sn: print(f"{p}: serial {sn}") sn_to_path[sn] = p else: print(f"{p}: no PI18 response (probably not an LVX6048)") missing = [] for sn, link in LINK_FOR_SERIAL.items(): if sn in sn_to_path: _relink(link, sn_to_path[sn]) print(f"symlink {link} -> {os.path.basename(sn_to_path[sn])}") else: missing.append((link, sn)) try: if os.path.islink(link): os.unlink(link) except FileNotFoundError: pass print(f"WARNING: {link} serial {sn} not found on any /dev/hidraw*") return 0 if not missing else 2 if __name__ == "__main__": sys.exit(asyncio.run(main()))