Skip to content

fix(plc-modbus): comm health is its own signal; never claim a current E-stop across a dead link (#207) - #209

Merged
Mikecranesync merged 1 commit into
mainfrom
fix/207-state-semantics
Aug 2, 2026
Merged

fix(plc-modbus): comm health is its own signal; never claim a current E-stop across a dead link (#207)#209
Mikecranesync merged 1 commit into
mainfrom
fix/207-state-semantics

Conversation

@Mikecranesync

Copy link
Copy Markdown
Owner

Closes #207. Three review items from MIRA PRD #3048 that merged with #198 at the reviewed head, unaddressed. All three feed MIRA's technician-visible live evidence, so they gate promoting that feed as trusted.

1. Comm health was being error code 5 — in two places, independently

machine_state_from_snapshot branched on code == COMM_LOSS_ERROR_CODE, and modbus_tag_source.canonical_tags_from_snapshot separately derived conv_simple.comm_ok from int(snapshot.error_code) != 5. Two independent readings of the same register standing in for a different fact — renumbering ERROR_CODES would silently change what machine_state means, in two files, with nothing linking them.

TagSnapshot now carries an optional comm_ok link indicator, and comm_ok_from() is the one definition both call sites use: authoritative when the source supplies it, falling back to the documented error-code proxy only when absent.

Being straight about the limit: the Micro820 bridge exposes no independent link signal today, so the proxy is still what actually runs. What changed is that it is now one named, documented seam instead of two silent duplications — swapping in a real indicator is a one-line change in one place rather than a semantic drift across two files.

2. e_stop_active was asserted across a dead link — the safety one

if bool(snapshot.e_stop):
    conditions.append("e_stop_active")   # unconditional
if code == COMM_LOSS_ERROR_CODE:
    ...
    return "comm_down", conditions

_tag_quality in the same file already reasons this correctly — with comms lost the values are "whatever the bridge last held", uncertain, excepting only comm_ok/fault_code as the bridge's own state. e_stop crosses the same dead link and got no such exception.

Direction matters: claiming an E-stop is engaged when it may have been released since comms dropped is the direction that can lead a technician to believe a machine is safed when it isn't. Now reported as e_stop_active_last_known so the consumer renders uncertainty rather than false certainty.

A pre-existing test asserted the old behaviour while its own docstring said the opposite"With the link down the bridge cannot vouch for E-stop either" — and then assert "e_stop_active" in conditions. Updated to match its stated reasoning.

3. Substring denylist rejected ordinary provenance

FORBIDDEN_FIELD_TOKENS was substring-matched over every key at every depth, so controller_model, controller_serial and control_panel_id were all rejected as "forbidden command/write field". Live rather than theoretical since MIRA #3059 began writing snapshot fields into per-tag metadata.

Now matched on whole _-separated segments. My first pass at this was still wrong — segment-matching alone still rejects control_panel_id, because bare control is genuinely ambiguous. Dropped it as a verb and named the actual control-word shapes (control_word, control_bit, control_register, control_command) explicitly. A real command / write / actuate / setpoint_write field is still rejected at any depth.

Evidence

  • 195 passed on origin/main → 204 passed here (+9), zero regressions. Baseline measured on a clean detached checkout of 4521224, not assumed.
  • Counterfactuals included throughout so the guards can't pass by rejecting everything: an observed E-stop with good comms still asserts e_stop_active and estopped; a genuine command field is still rejected.
  • Local runs on Python 3.12 (CHARLIE's system python3 is 3.9 and can't import this package — Dict[str, Any] | None). CI runs 3.11.

No behaviour change for a healthy snapshot. No new dependency, no wire-format change: comm_ok is additive and optional, and the envelope shape is untouched.

… E-stop across a dead link (#207)

Three review items from MIRA PRD #3048 that merged with #198 unaddressed. All
three feed MIRA's technician-visible live evidence.

1. Communication health was BEING error code 5, in two places independently
   (machine_state_from_snapshot, and canonical_tags_from_snapshot deriving
   conv_simple.comm_ok from `int(error_code) != 5`). Renumbering ERROR_CODES
   would silently change what machine_state means. TagSnapshot now carries an
   optional `comm_ok` link indicator, and comm_ok_from() is THE one definition
   both call sites use -- authoritative when the source supplies it, falling
   back to the documented error-code proxy only when it does not (which is all
   the Micro820 bridge exposes today).

2. active_conditions asserted e_stop_active unconditionally, then returned
   comm_down -- claiming a CURRENT E-stop read across the same dead link that
   _tag_quality already declares unreliable. That is the dangerous direction:
   an E-stop reported as engaged when it may have been released since comms
   dropped can lead a technician to believe a machine is safed when it isn't.
   It is now reported as e_stop_active_last_known so the consumer can render
   the uncertainty. A pre-existing test asserted the old behaviour while its
   own docstring said the opposite ("the bridge cannot vouch for E-stop
   either") -- updated to match its stated reasoning.

3. The forbidden-field scan was a SUBSTRING denylist, so controller_model,
   controller_serial and control_panel_id were all rejected as command fields.
   Live rather than theoretical since MIRA #3059 began writing snapshot fields
   into per-tag metadata. Now matched on whole _-separated segments, with bare
   `control` dropped (ambiguous -- control_panel_id is provenance) and the
   actual control-word shapes named explicitly. A real command/write/actuate/
   setpoint_write field is still rejected at any depth.

195 passed on main -> 204 passed here (+9), zero regressions.
@Mikecranesync
Mikecranesync merged commit e3ea260 into main Aug 2, 2026
2 checks passed
@Mikecranesync
Mikecranesync deleted the fix/207-state-semantics branch August 2, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(plc-modbus): machine_snapshot state semantics — comm health, stale E-stop, substring denylist

1 participant