fixed resolver

This commit is contained in:
2026-05-09 10:18:57 -04:00
parent a1acf479f0
commit 8c7b5fb711
3 changed files with 70 additions and 11 deletions

View File

@@ -18,11 +18,18 @@ from __future__ import annotations
import asyncio
import glob
import os
import re
import sys
SERIAL_UNIT_1 = "1496142109100037000000"
SERIAL_UNIT_2 = "1496142408100255000000"
# Real LVX PI18 serials are long ASCII digit strings (the two known units are
# 22 digits each). Anything else — null-byte garbage, the literal "Invalid
# response …" error wrapper string, an empty payload, etc. — is a stuck-firmware
# response that must be classified as a failed probe so we'll retry it.
_SERIAL_RE = re.compile(r"^\d{18,24}$")
LINK_FOR_SERIAL = {
SERIAL_UNIT_1: "/dev/lvx6048-1",
SERIAL_UNIT_2: "/dev/lvx6048-2",
@@ -49,7 +56,12 @@ async def probe_serial(path: str) -> str | None:
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)
sn = str(res.readings[0].data_value)
# Filter out malformed responses (null-byte garbage, error-wrapper strings)
# — caller treats None as "retry this path on the next tick".
if not _SERIAL_RE.fullmatch(sn):
return None
return sn
def _relink(link: str, target: str) -> None:
@@ -65,27 +77,45 @@ def _relink(link: str, target: str) -> None:
async def main() -> int:
# Hot-plug case: udev fires this script as soon as one hidraw appears, but
# a sibling inverter coming up at nearly the same moment may still be
# enumerating. Retry-probe up to ~10 s waiting for all expected serials,
# so a transient single-device sighting doesn't leave one symlink missing.
# enumerating. Also covers the stuck-HID-endpoint case: a unit that just
# came up may answer the first PI18 ID query with null bytes for a few
# seconds before its firmware populates the response. Retry-probe up to
# ~20 s with per-path exponential backoff (1 s → 2 → 4 → 8 cap). Backing
# off between failed probes — rather than hammering at fixed 0.5 s pacing
# — gives a confused HID endpoint time to recover instead of compounding
# the confusion.
expected = set(LINK_FOR_SERIAL.keys())
deadline = asyncio.get_event_loop().time() + 10.0
loop_time = asyncio.get_event_loop().time
deadline = loop_time() + 20.0
sn_to_path: dict[str, str] = {}
seen_paths: set[str] = set()
# Per-path retry state. Paths are removed from `attempts` once they
# yield a recognized serial; paths still in it get re-probed when their
# `next_attempt_at` falls due. Unknown / not-an-LVX paths stay in the
# map and back off to 8 s, so we don't busy-poll dead ports either.
attempts: dict[str, int] = {}
next_attempt_at: dict[str, float] = {}
while True:
candidates = sorted(glob.glob("/dev/hidraw*"))
for p in candidates:
if p in seen_paths:
now = loop_time()
resolved_paths = {p for sn, p in sn_to_path.items() if sn in expected}
for p in sorted(glob.glob("/dev/hidraw*")):
if p in resolved_paths:
continue
if next_attempt_at.get(p, 0.0) > now:
continue
seen_paths.add(p)
sn = await probe_serial(p)
n = attempts[p] = attempts.get(p, 0) + 1
if sn:
print(f"{p}: serial {sn}")
sn_to_path[sn] = p
next_attempt_at.pop(p, None)
else:
print(f"{p}: no PI18 response (probably not an LVX6048)")
backoff = min(2 ** (n - 1), 8) # 1, 2, 4, 8, 8, …
next_attempt_at[p] = now + backoff
print(f"{p}: no valid PI18 serial (attempt {n}, retry in {backoff}s)")
if expected.issubset(sn_to_path):
break
if asyncio.get_event_loop().time() >= deadline:
if loop_time() >= deadline:
break
await asyncio.sleep(0.5)