Skip to content

fix(firmware): take the control snapshot per reading, not per pass - #85

Merged
CameronBrooks11 merged 1 commit into
mainfrom
fix/mask-once-per-snapshot
Nov 2, 2026
Merged

CameronBrooks11 merged 1 commit into
mainfrom
fix/mask-once-per-snapshot

Conversation

@CameronBrooks11

Copy link
Copy Markdown
Member

Fixes #81, the GET_STATE replies cut short on the gen2 Nano.

The 2026-11-01 bisect (#84) traced the cut-short replies to how often the loop masks interrupts. With main.cpp's masked windows removed, or with the loop slowed by 300 us a pass, the bad-reply rate went from 187 in 300 s to 0. relayControlLogic() opened four masked windows on every pass: three snapshots and the on-time commit.

What changes:

  • relayControlLogic() takes its snapshot, runs the PIDs and publishes the on-times only on a new thermocouple reading, or once RLHT_SNAPSHOT_INTERVAL_MS (default 10, a -D override) has passed. In between, actuateRelay() drives from the last snapshot.
  • The PIDs still compute once per reading (fix(firmware): compute the heater PIDs once per thermocouple reading #76), since a reading always triggers a snapshot.
  • The e-stop, watchdog-trip and fault hold is checked on every pass with no masked window. Each flag is one byte and only the main loop sets them. The relay state fields are written only when they change.
  • A command now waits up to the interval to reach a relay: 10 ms, next to a 1000 ms relay period.
  • An invalid mode now holds both relays off through drives of 0, where before it returned early from each pass.

I rejected a per-command "changed" flag set by every handler: a handler that missed setting it would leave a stale setpoint or mode. A fixed interval can't miss anything.

Test

test_mask_rate counts noInterrupts() calls through the host stub over 3 s of closed-loop heating:

Passes that opened a masked window
main's relayControlLogic() 1500 of 1500: the check fails
this branch 302 of 1500, against an allowance of 312 (one per 10 ms plus one per reading)

It also checks that a duty change reaches relay 1 within the interval (10 ms) and that the e-stop drops relay 1 on the next pass.

make -C test/host: 732 ok, 0 failed, across the four envs. That's main's 696 plus test_mask_rate's 36.

pio run, 0 warnings:

env Flash RAM Flash vs main
gen1_nano 23018 B 918 B
gen2_nano 24246 B 934 B +208 B, +12 B RAM
gen1_nanoevery 24505 B 996 B
gen2_nanoevery 25717 B 1014 B

Bench

gen2 Nano on the backplane, Pi 4 at 50 kHz, replylog.py 19 1 300 with retries off:

Build Bad replies in 300 s
main a251158 (2026-11-01, two captures) 187 / 2552 and 175 / 2552
this branch (2026-11-02) 1 / 2551

The one bad frame went to 0xFF at the CRC byte and was caught. That's the rate Slice_DCMT's gen2 Nano showed on the same bus on 2026-10-31 (1 / 2555).

Not run on the board: the heaters under closed-loop control on this build. The host tests cover the control paths.

relayControlLogic() opened four masked windows on every loop pass,
often enough to delay the TWI interrupt and cut GET_STATE replies
short (#81). It now snapshots, runs the PIDs and publishes the
on-times on a new reading or every RLHT_SNAPSHOT_INTERVAL_MS (10),
and drives the relays from that snapshot in between. The e-stop,
watchdog and fault hold is still checked every pass, unmasked.

Adds test_mask_rate; noInterrupts() in the host stubs now counts.

Closes #81
@CameronBrooks11
CameronBrooks11 merged commit 6b9b81f into main Nov 2, 2026
9 checks passed
@CameronBrooks11
CameronBrooks11 deleted the fix/mask-once-per-snapshot branch November 2, 2026 16:39
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.

GET_STATE replies from the gen2 Nano arrive cut short in 2-4% of polls

1 participant