Repository navigation
Add a state counter so a host sees every transition of a state machine it samples too rarely - #374
Johannes-Thiel wants to merge 12 commits into
Conversation
A host reads module properties less often than a short move changes state, so it can miss a move that started and ended between two reads and cannot tell it from one the module refused. StateCounter is an integer property whose value modulo the number of states is the current state, idle being 0. It counts forward on every transition, skipped states included, and wraps at the largest multiple of the number of states up to 255. An accepted command first counts up to the next idle, as if a running cycle had ended, so every accepted command takes the counter exactly as many steps as there are states, and a refused one none: a host can count its commands ahead instead of waiting for each one to show. StepperMotor reports its idle/speeding/positioning machine through the new `state` property next to `idle`. Assisted-by: Claude:claude-opus-5-5
The stepper motor is no ring-shaped state machine: speeding and positioning both start from idle, and only positioning returns to idle on its own, so counting through skipped states there is an artifact. The counter stays as a utility for a module whose commands run through a fixed cycle, such as the RMD motor. The counting rule moves from the module reference into the header, including that a host compares values modulo the period and cannot tell which of several unawaited commands was refused. The constructor checks the number of states before it computes the period, and enter() and restart() reject states out of range. Assisted-by: Claude:claude-opus-5-5
…essors Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ugh idle Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
|
As discussed with Johannes, I "kneaded" the design some more. 6d85a88 i'm kind of on the fence about, but leaning towards keeping it since it simplifies the host side. Another open point: If we make Also, here is some example host code for motivation: And example module code: |
How a module reports an accepted command (idle first, then the command's state, so each command takes one epoch) is a decision of that module, not of the counter. It moves to the PR of the first module that uses the counter; the counter documents only its bit layout, the set() rule and how a host compares values. Assisted-by: Claude:claude-opus-5-5
|
Just a quick look in between, not a review yet. My main question is what exactly this is for. I understand the general idea from the motivation and from the first commit message: a host misses short moves between two reads and can't tell them apart from refused ones. But I can't find the concrete case behind it. Two things to keep in mind for that:
|
|
Concrete case is
Not really. In the mentioned example (1 -> 3 -> 2) the move to 2 increases the epoch, but that's fine. The only purpose of the ladder is that we don't miss a transition x -> y -> ... -> x. The ladder is just a simple way to ensure that. One thing that this made me think about, though (and maybe this is what you were talking about?): The host side is not robust to changes to the lizard side (1 -> 3 -> 2 example). What we want instead (maybe with some sugar):
Hm, good point, I just assumed that... So ensuring that is up to the user (i.e. each module) I guess? We could make it safer by adding a |
|
Thanks, that makes it a lot clearer. Since the only user is Moving it there also settles the other points without touching Lizard: the delta arm can reject writes to So I'd suggest closing this PR and opening it against lizard-zz instead. @Johannes-Thiel @ftilde, does that work for you? |
Motivation
A host reads module properties less often than a module's state machine changes state, so it misses intermediate states: a short move that goes from idle through accepted and moving back to idle between two reads looks like nothing happened.
This PR adds a reusable state counter whose value only climbs, so a host can tell from any later reading whether a state was reached, even if it never saw it; the RMD motor is meant to be its first user in a separate PR.
Implementation
The ladder
StateCounter<State>is anIntegerVariable, templated on the module's state enum, which ends with aCOUNTsentinel one past the last state.set(state)is the only way to change it: a later state keeps the epoch, an earlier state starts the next epoch, and staying in the same state changes nothing. The value therefore never goes back, and skipped states are simply passed.is_after(a, b)compares two values across the wrap, which is valid as long as they are less than 128 steps apart.state()andepoch()read the two parts.static_assertrequires at least 2 states and a full cycle to fit in half a period (at most 64 states);set()throws on a state out of range.Changed files
main/utils/state_counter.h: new header-onlyStateCounter<State>.docs/module_reference.md: new section "State Counters" with the bit layout, theset()rule and how a host compares values.Open: the property is still writable from the host through
set_integer_value, which bypassesset()and can move the value backwards. Makingset_integer_valuevirtual and routing it toset()would close that, but changesVariablefor all modules. A safer alternative is to forbid the setting ofset_integer_valuefor this counter.Progress
No module uses the counter yet, so there is nothing to test on hardware, and since no source file includes the header, the firmware build does not compile it either.
A host test (clang++, C++17, minimal stand-in for
IntegerVariable) checked the counting rule for a 3-state enum (later state, earlier state, staying, wrap past 256),is_after()across the wrap, the out-of-range throw, and thestatic_assert(64 states compile, 65 do not); the header also compiles against the realvariable.h.※