fixed resolver
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user