fix(plc-modbus): comm health is its own signal; never claim a current E-stop across a dead link (#207) - #209
Merged
Merged
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_snapshotbranched oncode == COMM_LOSS_ERROR_CODE, andmodbus_tag_source.canonical_tags_from_snapshotseparately derivedconv_simple.comm_okfromint(snapshot.error_code) != 5. Two independent readings of the same register standing in for a different fact — renumberingERROR_CODESwould silently change whatmachine_statemeans, in two files, with nothing linking them.TagSnapshotnow carries an optionalcomm_oklink indicator, andcomm_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_activewas asserted across a dead link — the safety one_tag_qualityin the same file already reasons this correctly — with comms lost the values are "whatever the bridge last held",uncertain, excepting onlycomm_ok/fault_codeas the bridge's own state.e_stopcrosses 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_knownso 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_TOKENSwas substring-matched over every key at every depth, socontroller_model,controller_serialandcontrol_panel_idwere all rejected as "forbidden command/write field". Live rather than theoretical since MIRA #3059 began writing snapshot fields into per-tagmetadata.Now matched on whole
_-separated segments. My first pass at this was still wrong — segment-matching alone still rejectscontrol_panel_id, because barecontrolis genuinely ambiguous. Dropped it as a verb and named the actual control-word shapes (control_word,control_bit,control_register,control_command) explicitly. A realcommand/write/actuate/setpoint_writefield is still rejected at any depth.Evidence
origin/main→ 204 passed here (+9), zero regressions. Baseline measured on a clean detached checkout of4521224, not assumed.e_stop_activeandestopped; a genuine command field is still rejected.python3is 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_okis additive and optional, and the envelope shape is untouched.