fix(rudder): transition guard rollover, dropped irons params, and shipped-path test coverage - #65
Open
georgesleen wants to merge 3 commits into
Open
fix(rudder): transition guard rollover, dropped irons params, and shipped-path test coverage#65georgesleen wants to merge 3 commits into
georgesleen wants to merge 3 commits into
Conversation
The guard check uses a signed difference so it survives tick rollover. An unset guard is stored as 0, and the signed difference reports 0 as still pending once the tick counter passes 2^31 ms, which is 24.9 days of uptime. Every state transition would then be blocked for the rest of the run. Not reachable while STRAIGHT_ONLY is defined, because updateStateMachine returns early when the current and next states match. It becomes reachable as soon as the full model is enabled.
The module merge replaced upwindIronsAngle and downwindIronsAngle with upwindIronsRange and downwindIronsRange, but left the two original fields out of the initialiser, so they defaulted to 0. irons() still reads them. Restore the previous 30 and 150 values. In the same function, the port branch was written without an else, so it overwrote the starboard assignment on every call and the starboard case could never take effect. Both sites sit in the escape block of irons(), which does not currently execute: irons() runs only in state IRONS, and the state machine leaves IRONS in the same cycle isInIrons() goes false, so the "if (!isInIrons())" gate is never true when irons() runs. These are corrections to code that must be right before that path is revived, not live behaviour changes.
STRAIGHT_ONLY and TUNING_MODE are wrapped in "#ifndef RUDDER_TEST_BUILD", and the module's existing tests define RUDDER_TEST_BUILD. Everything those tests exercise is compiled out of the firmware, and straightLine(), the only path the boat runs, had no coverage. The module's tests were also not wired into CI. Add two host tests to firmware/tests, which CI already runs: - rudder_straight_test builds without RUDDER_TEST_BUILD, so it tests the firmware configuration. It pins the exact per-cycle integral update. A duplicated integral increment shipped on rudder-working-branch and made the integral run 1.5x high, and the PID gains were tuned against it. Both assertions were checked by reintroducing the defect. - rudder_state_machine_test defines RUDDER_TEST_BUILD to switch STRAIGHT_ONLY off and covers the transition guards. Supporting changes: - HAL_GetTick in the stub HAL returns 0 and cannot drive a PID. Tests that need time now define TEST_USE_FAKE_TICK and link common/fake_tick.c. The default stays the existing inline stub, so other tests are unaffected. - RUDDER_PRINT replaces the two unconditional printf calls in the PID hot path. It expands to printf unless RUDDER_QUIET is defined, so firmware output is unchanged and only the tests are quiet. - The two rudder targets drop -Werror. The module carries pre-existing unused-variable and const-qualifier warnings; cleaning those up is separate work.
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.
Follow-up to the module merge in #62. While checking whether Emma's refactor of the rudder control model is functionally equivalent to what
rudder-working-branchwas running, I found the refactor is equivalent on the shipped path, but a few defects came along with the merge. This fixes the ones that are safe to fix and adds the missing coverage.Context: the refactor itself is sound
The firmware compiles with
STRAIGHT_ONLY, sorunPIDonly ever reachesstraightLine()->getRudderAngle(). Those functions are byte-identical betweenb07fea8andmain, andEstimator_UpdateControlleris a faithful transliteration of the oldupdateAverages.Verified by differential test: 200 randomised trajectories, 10000 samples, identical stimulus into both builds. Patch two defects into the old code and the traces match exactly, 0 of 200 runs diverging. The refactor also repaired three real bugs: a duplicated integral increment, uninitialised
ControllerState, and a state machine that was dead becauserunPIDcalledreturnState(error)and discarded the result.One thing needs a human decision, not a code change: the May/June dryland gains were tuned against the build whose integral ran 1.5x high, so
Kiis effectively different now. Worth re-checking before the next on-water test.What this PR changes
1. Transition guard rollover. The guard uses a signed difference to survive tick rollover, but an unset guard is stored as
0, and the signed difference reads0as still pending once uptime passes 2^31 ms (24.9 days). Every transition would then be blocked for the rest of the run. Not reachable underSTRAIGHT_ONLY(from == toshort-circuits first); becomes reachable the moment the full model is enabled.2. Dropped irons parameters. The merge replaced
upwindIronsAngle/downwindIronsAnglewithupwindIronsRange/downwindIronsRangebut left the originals out of the initialiser, so they silently became0.irons()still reads them. Restored to 30/150.3. Missing
elseinirons(). The port branch had noelse, so it overwrote the starboard assignment on every call and the starboard case could never take effect.Fixes 2 and 3 are in the escape block of
irons(), which does not currently execute.irons()runs only in stateIRONS, and the state machine leavesIRONSin the same cycleisInIrons()goes false, so theif (!isInIrons())gate is never true whenirons()runs. Brute force over 20000 trials biased into the irons regime: 87242 calls, escape block executed 0 times. These are corrections to code that must be right before that path is revived, not live behaviour changes.4. Test coverage for the path that actually ships.
STRAIGHT_ONLYandTUNING_MODEare wrapped in#ifndef RUDDER_TEST_BUILD, and the module's existing tests defineRUDDER_TEST_BUILD. So everything they exercise is compiled out of the firmware,straightLine()had no coverage at all, and none of it ran in CI. That is why a 1.5x integral error survived to a merge review.Two new tests in
firmware/tests, which CI already runs:rudder_straight_testbuilds withoutRUDDER_TEST_BUILDand pins the exact per-cycle integral update.rudder_state_machine_testdefinesRUDDER_TEST_BUILDand covers the transition guards.Both assertions were mutation-checked: reintroducing the duplicated integral line fails the first, reverting the guard fix fails the second.
Supporting changes:
TEST_USE_FAKE_TICK+common/fake_tick.cgive tests a settableHAL_GetTick(the stub returns 0 and cannot drive a PID); the default stays the existing inline stub so other tests are untouched.RUDDER_PRINTreplaces the two unconditionalprintfcalls in the PID hot path, expanding toprintfunlessRUDDER_QUIETis defined, so firmware output is unchanged. The two rudder targets drop-Werrorbecause the module carries pre-existing warnings.Deliberately not fixed
The
irons()escape block being unreachable, andironsState.fixedHeadingbeing write-only, are the same underlying problem: the IRONS state has no working recovery behaviour. Fixing it means deciding what the boat should actually do to sail out of irons, and how long to hold the state while it does. That is a control-design call for whoever owns the model, so I have not invented steering logic for it.Also untouched:
SailingSM_Updateand the wholeSailingMode/EstimatedBoatStatemachine, plusupdateStateEstimateandapplyEstimateToController, are never called by firmware or tests (~400 lines). Left in place in case they are staged work.Verification
nix develop .#ci --command make -C firmware/tests testpasses, all 8 targets.STRAIGHT_ONLYon, prints on).STRAIGHT_ONLYare theprintfgating, which is a no-op unlessRUDDER_QUIETis defined.