Helios: don't let an unlabelled reply pass for a register's value

_value_in()'s last resort was to accept any line it could not attribute as
the answer to whatever had just been asked.  That fallback exists for the
serial numbers, which come back bare — but it applied to every query, so a
stray "32" could be read as a diode current of 32 mA.  32 is also what a
status register reads with bit 5 set (LER "Over voltage laser diode", LCE
"Door switch open", CCE "Q-switch under/over temperature"), which is
exactly the value the panel is stuck on.

Only CSR and HSR now accept an unlabelled reply; every other read has to
see its own mnemonic in the line.  A read that cannot be attributed returns
None, and the panel shows "laser: ? mA" instead of leaving the last good
value on screen looking live — a stale reading and a setpoint that refuses
to move are indistinguishable otherwise.

tools/helios_lds_probe.py is the diagnostic for the underlying question:
it talks to the controller with no reply parsing at all and prints every
byte, so the transcript says whether LDS answers for itself, whether the
write is taken, and which flags the registers hold before and after.  The
status-register tables move to hardware/helios_registers.py so the probe
can decode them without importing the Qt app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-09-08 07:48:06 -05:00
parent c7eb33891c
commit eff589d901
6 changed files with 389 additions and 111 deletions
+13 -5
View File
@@ -130,15 +130,21 @@ class HeliosLaser:
return lines
time.sleep(0.005)
@staticmethod
def _value_in(line: str, mnemonic: str) -> Optional[str]:
# The only replies that come back without naming what they answer.
# Every other line has to identify itself: an unlabelled number is not
# evidence that it is *this* register's number, and taking one on faith
# is how a status register's value ends up displayed as a diode current.
UNLABELLED_REPLIES = frozenset({"CSR", "HSR"})
@classmethod
def _value_in(cls, line: str, mnemonic: str) -> Optional[str]:
"""The value `line` holds for `mnemonic`, or None if it isn't its reply.
The controller answers "LDF = 20000 ns". A line naming a different
mnemonic is the tail of an earlier reply, and "Bit 15..0: ..." is a
status register's decode line; neither is an answer to this query.
A line naming nothing is taken as the value — that is how the serial
numbers come back.
A line naming nothing counts only for the serial numbers, which is
the one reply known to come back bare.
"""
head, sep, tail = line.partition('=')
if sep:
@@ -152,7 +158,9 @@ class HeliosLaser:
fields = line.split()
if fields and fields[0].upper() == mnemonic:
return fields[1] if len(fields) > 1 else None
return line
if mnemonic in cls.UNLABELLED_REPLIES:
return line
return None
def _query(self, command: str) -> Optional[str]:
"""Send a query and return the value from its response.