Conversation
Previous review (2026-09-15)Automated review note — AI-generated (Claude), validated against the live diff (Claude + Codex cross-checked). Please sanity-check before acting. Reviewed at head Verdict: REQUEST CHANGES. The mechanism works, and the tests catch it: with AP_Mount/AP_Camera reverted, Bug
Issues
Notes
Checked:
CI: 101 passing, 1 failing (copter-tests1d ModeLoiter), which looks unrelated. |
23826e4 to
ad770ae
Compare
|
Thanks — that was a useful review. Pushed The bug: fixed
|
| Copter | Plane | |
|---|---|---|
| as pushed | Mode=0 (retracted) | Mode=0 (retracted) |
| with the gate | Mode=0 (retracted) | Mode=3 — not applied until the RC read |
RC_Channels_Plane::has_valid_input() goes through in_rc_failsafe() → Plane::rc_failsafe_active(), which keys off failsafe.last_valid_rc_ms, written only inside the scheduled Plane::read_radio() — which has not run at the mount's first update(). So the gate closes the failsafe case on Copter and turns the feature off entirely on Plane. The three tests staying green is consistent with that: they are all Copter.
I've added Plane.MountAuxFunctionAtBoot so the trade can't be made by accident, and recorded the reason in its docstring and in the description. If the dev call decides the failsafe exposure is not acceptable, the answer is not this gate — it would be dropping the read, which leaves the mount case unfixed.
Your reproduction itself I'm not disputing: SIM_RC_FAIL=2 with a throttle failsafe does get the boot position applied here where master logs nothing. That is the open question in the description, and it is why ICE_START_STOP being outside this PR's function list matters.
Gain over master: agreed, and the description now says so
You're right on both counts, and I've rewritten that part rather than leave it overstated. The 4.7.1-beta report is MOUNT_RP_LOCK HIGH at boot, and HIGH sets _roll_lock/_pitch_lock true — already their initialiser values in AP_Mount_Backend.h and re-derived every update in RC targeting, so a lost boot position changes nothing there. I carried that citation over from #34166 without re-checking it and should not have. The description now states plainly that this is worth the ~200 ms window while disarmed plus the never-valid-RC case, and explicitly that it does not explain the linked report.
Duplicate suppression: now tested
Correct that removing both recordings left the tests green. Both tests now assert the AUXF count is exactly 1; with the recording removed they fail with "RETRACT_MOUNT1 applied 2 times, want 1" and "CAMERA_REC_VIDEO applied 2 times, want 1".
Duplicated loop: collapsed
Replaced by a single RC_Channels::apply_aux_switch_positions(functions, count). Since RC_Channels is already a friend of RC_Channel, that also removes both public helpers the previous push added (read_aux_switch_position(), record_aux_switch_position()) and the repeated find_channel_for_option() lookup you flagged. AP_Mount.cpp 118 → 59 lines, AP_Camera.cpp 87 → 42. MatekF405 is now +172 B (you measured +184 B).
Comments and style
Trimmed throughout; the "45 lines ahead" / "50 lines ahead" references are gone, as are the redundant constructor assignments.
AuxFunctionAtBootWithoutRC
Fixed — it now enables RC_PROTOCOLS in the same boot rather than rebooting, so it covers what the docstring claims.
CI
Copter.ModeLoiter passes locally on this branch; no aux involvement in it. Agreed it's unrelated.
Previous review (2026-09-15)Automated review note — AI-generated (Claude), validated against the live diff (Claude + Codex cross-checked). Please sanity-check before acting. Re-reviewed at head Verdict: still REQUEST CHANGES, but for a smaller set of problems.
Resolved
You were right
Still open
New
Checked:
CI at |
ad770ae to
dfa3395
Compare
|
Pushed Failsafe positions: taken, with a regression testAdopted as suggested: the one-shot is now held pending rather than consumed at the first
I've added I've also dropped the "RC never becomes valid at all" benefit from the description, as you said to — with frames arriving that is the failsafe case, so losing it is the point rather than a cost. Your correction on the Plane cause is right and I've fixed it in the commit message, the test docstring and the description:
|
Previous review (2026-09-15)Automated review note — AI-generated (Claude), validated against the live diff (Claude + Codex cross-checked). Please sanity-check before acting. Re-reviewed at head Verdict: COMMENT (was REQUEST CHANGES): no blockers left in the code.
Previous round
Test coverage (worth fixing, not blocking)
Note
Checked:
CI: 97 passing, 3 pending, 2 failing. Both failures look unrelated: |
dfa3395 to
8e6d2c2
Compare
|
Pushed Exactly-once is tested againYou're right that I lost this when I reworked the assertion to fix the flake — the absence-of-RC-sourced-AUXF check says nothing about how many times initialisation ran. Added Reproduced your mutation (deleting The camera test now requires the camera to have done somethingYour "record the position but skip That also gives the AP_Camera side of the failsafe gate its own coverage, which was your third point: the first half of the test asserts nothing is applied while the receiver is in failsafe. With your skip-dispatch mutation it now fails: CommentsBoth stale ones moved back: WordingCorrected — "the switch positions the vehicle booted with" isn't accurate once the one-shot can fire later. The comments now say it applies the current switch position, and note explicitly that where RC becomes valid late that is the position the switch is in then, which is what the first debounced I left the State479 insertions, 0 deletions, 4 commits. Agreed on the two CI failures — |
Previous review (2026-09-15)Automated review note — AI-generated (Claude), validated against the live diff (Claude + Codex cross-checked). Please sanity-check before acting. Re-reviewed at head Verdict: APPROVE (was COMMENT): no blockers.
Previous round
Worth hardening (not blocking)
Minor
Checked:
CI: 102 passing. |
peterbarker
left a comment
There was a problem hiding this comment.
We also have to be careful around
RC_Channel::init_position_on_first_radio_read
This PR doesn't introduce a problem, but adds structures which could allow for bypassing things in that list. It's worth a comment, probably.
| */ | ||
| void AP_Camera::init_aux_functions() | ||
| { | ||
| RC_Channels *rc_channels = RC_Channels::get_singleton(); |
| #if AP_RC_CHANNEL_ENABLED | ||
| /* | ||
| apply the current position of this library's auxiliary switches, as | ||
| AP_Mount does; the |
| /* | ||
| apply the current position of this library's auxiliary switches, as | ||
| AP_Mount does; the | ||
| camera backends and the RunCam driver are allocated inside camera.init(), |
There was a problem hiding this comment.
we don't need the history lesson
| // Deliberately outside the semaphore taken below, since the handlers | ||
| // take it themselves. | ||
| if (!_aux_functions_initialised) { | ||
| const RC_Channels *rc_channels = RC_Channels::get_singleton(); |
| // take it themselves. | ||
| if (!_aux_functions_initialised) { | ||
| const RC_Channels *rc_channels = RC_Channels::get_singleton(); | ||
| if (rc_channels == nullptr || rc_channels->has_valid_input()) { |
There was a problem hiding this comment.
technically has_valid_input is not sufficient to init_aux_functions generally.
Just because you has_valid_input does not mean you has_valid_input for every single channel!
e.g. RC_CHANNELS_OVERRIDE
e.g. a multi-RC-receiver aircraft where one of the backends doesn't supply all channel values
I think we need to let this go for now, but assuming has_valid_input is sufficient is actually the cause of some fairly significant bugs in ArduPilot ATM.
| return; | ||
| } | ||
|
|
||
| RC_Channels *rc_channels = RC_Channels::get_singleton(); |
|
|
||
| RC_Channels *rc_channels = RC_Channels::get_singleton(); | ||
| if (rc_channels == nullptr) { | ||
| // e.g. Tools/Replay, which has no RC |
| // later, the position applied is the one the switch is in then, which | ||
| // is what the first debounced read_aux() would have applied anyway. | ||
| if (!_aux_functions_initialised) { | ||
| const RC_Channels *rc_channels = RC_Channels::get_singleton(); |
| void AP_Mount::update() | ||
| { | ||
| #if AP_RC_CHANNEL_ENABLED | ||
| // apply the switch positions which RC_Channels::init() could not. |
| self.assert_at_home() | ||
|
|
||
| def MountAuxFunctionAtBoot(self): | ||
| """test the mount applies its aux switch position at boot, rather than |
There was a problem hiding this comment.
Restrict the docstring for methods to one line; if you want more text then use another """ after it.
Really long docstrings screw up the test output
8e6d2c2 to
05a650f
Compare
|
Thanks Peter — all taken, pushed
|
Previous review (2026-09-16)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Re-reviewed at head Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_17_0037/devcall_pr_reviews.html#pr34413 Verdict: COMMENT (was APPROVE) — everything from my last round and all twelve of peterbarker's inline comments are addressed, and the tests are solid at 500/500. One behaviour change nobody has flagged, which is why this is not an APPROVE. Previous round — all resolvedAll 12 inline comments applied ( ISSUE —
|
Some auxiliary functions act on backends which do not exist when RC_Channels::init() runs them, so the library owning those backends has to establish its switch positions itself once it has created them. apply_aux_switch_positions() does that for a set of functions: it finds the channel carrying each one, reads the switch, and re-dispatches through RC_Channel::run_aux_function() so there remains one implementation of what each position means. A function whose switch cannot be read, or whose dispatch does not take, is left to the first debounced read_aux() exactly as it is today - nothing is invented. Functions in init_position_on_first_radio_read() are skipped outright. None of them are used by the libraries which call this, but reading a switch and acting on it is precisely what that list exists to prevent, and a generic helper must not become a way around it. A position which was applied is recorded as the channel's current switch position, so that read_aux() does not run the function a second time for a switch which has not moved. Initialisation does not touch switch_state on master and current_position starts at -1, so without this every recovered function would run twice: MOUNT_POI_LOCK re-saves the mode to revert to, a Siyi gimbal sends a second autofocus packet, and AP_RunCam::osd_option() latches a second menu action. A switch which genuinely moves is still picked up, since debounce_completed() compares against what was recorded. No behaviour change on its own; AP_Mount and AP_Camera use it next.
05a650f to
b8b1839
Compare
|
Pushed The lost boot announcement — fixedConfirmed exactly as described.
|
| board | merge base 35356c656c |
head b8b183995f |
delta |
|---|---|---|---|
| MatekF405 (mount off, strings off) | flash 874196, bss 80700 | flash 874396, bss 80700 | +200 B flash, 0 B RAM |
| CubeOrange (both libraries, strings on) | flash 1650396, bss 137388 | flash 1650748, bss 137396 | +352 B flash, +8 B RAM |
Re-run
test.Copter.MountAuxFunctionAtBoot CameraAuxFunctionAtBoot AuxFunctionAtBootWithoutRC MountAuxFunctionAtBootRCFailsafe, test.Plane.MountAuxFunctionAtBoot, plus test.Copter.Mount MountPOIFromAuxFunction AuxSwitchOptions AuxFunctionsInMission — all pass. check_branch_conventions.py clean, flake8 clean on the three autotest files (the one E741 is pre-existing).
Previous review (2026-09-20)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Re-reviewed at head Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_20_1145/devcall_pr_reviews.html#pr34413 Verdict: REQUEST CHANGES (was COMMENT). Everything from my last round is resolved, and I verified the fix rather than just its presence. But the independent second pass looked somewhere neither of us had looked, and chasing it down found a real behavioural regression in the default gimbal configuration. Details below, including two further claims from that pass that I investigated and refuted — please don't spend time on those two. Previous round — all resolvedThe lost GCS announcement is properly fixed (
Flash figures — your updated table is right on both boards. Re-measured from clean trees against The BUG —
|
| this head | base 35356c656c |
|
|---|---|---|
| capture time | 2459 ms (first AP_Mount::update()) |
2739 ms (first debounced read_aux()) |
| first Siyi attitude packet | 2499 ms — 40 ms after the capture | 2499 ms — 240 ms before it |
| gimbal body-frame yaw read | 0.000° (unpopulated) | 180.000° (real) |
| AHRS yaw read | −0.000° (uninitialised) | −19.820° (settled) |
| commanded EF gimbal yaw target at 6019 ms | −0.000° | +160.180° |
Master sits outside the unready window; this head is inside it by 40 ms. The 280 ms head start is the whole difference.
Two independent causes, and the second is the worse one:
AP_Mount_Siyi::get_attitude_quaternion()(AP_Mount_Siyi.cpp:131-136) returns true unconditionally from_current_angle_rad, zero until packets arrive. That isn't a Siyi quirk — 11 of the 13 mount backends always return true. (OnlyAlexmosandSoloGimbalcan return false, and that's no better:set_yaw_lock()then leaves_yaw_lock_heading_radat its zero-init value, the same wrong answer.)AP::ahrs().get_yaw_rad()is also still zero at 2459 ms, which is backend-independent. A Servo mount with no telemetry at all captured −0.000° at this head against −19.820° at base — so a ~20° error reaches every backend, with the gimbal-attitude error on top for those that report it.
It matters in the default configuration and it does not self-heal. _yaw_lock_heading_rad is consumed at exactly one place, AP_Mount_Backend.cpp:159, in update_mnt_target_from_rc_target()'s angle branch — reached with MNT1_DEFLT_MODE at its default RC_TARGETING (AP_Mount_Params.cpp:23) and MNT1_RC_RATE at its default 0 (:32). And set_yaw_lock() recaptures only when _yaw_lock is currently false, so once HIGH latches at boot the wrong heading persists until the pilot toggles the switch LOW→HIGH or a gimbal-manager command clears it.
Suggested direction (untested — a direction, not a patch): MOUNT_YAW_LOCK is the only one of your five mount functions whose handler captures state from the backend — RETRACT_MOUNT1/2 call set_mode(), MOUNT_RP_LOCK sets two bools, MOUNT_POI_LOCK is a no-op this early. Dropping it from the list at AP_Mount.cpp:228 restores master's behaviour for that one function while keeping the rest of the PR. Note a backend->healthy() gate would not be enough — AP_Mount_Backend::healthy() defaults to true (AP_Mount_Backend.h:57-58), so Servo/CADDX/SToRM32 slip through, and it does nothing about the AHRS-yaw half.
This is the same class of thing you already document for AP_Camera_MAVLinkCamV2 under "Known limitation" — a backend existing isn't a backend being ready. The difference, and why I think this one blocks: there the failure is a silent no-op, whereas here the PR latches a wrong value that master gets right.
Limit of this finding: SITL-verified and deterministic, but hardware timing is unverified. A real Siyi boots more slowly than the simulated one — but so does a real autopilot, and the AHRS-yaw half depends on EKF yaw-alignment timing, which I also could not measure here.
Two related claims I investigated and refuted — no action needed
The same second pass raised these; I'm recording them so you aren't sent after non-problems.
- Early SIYI record dispatch accepting an ineffective stop. The mechanism is real (
RecordingStatus::OFF == 0,_config_infoonly populated by the memcpy atAP_Mount_Siyi.cpp:399), but the timing refutes it: both trees reach the dispatch with identical unpopulated state (record_status=0,cfg_seen=0). Nothing requests the gimbal config before the firstrecord_video()call — that call makes the first request itself, and theupdate()path's first is at 4459 ms, ~1.7 s after even master's dispatch. Pre-existing and identical on master; if anything master is worse, sinceread_aux()latches the position regardless of the dispatch result (RC_Channel.cpp:1043) whereas your helper latches only on success. - POI init readiness. Both trees fail identically at their dispatch times.
AP::ahrs().get_location()first succeeds at 10019 ms — 7.56 s after this head's dispatch — so the 280 ms shift is 3.7 % of the margin.
Also checked and clean: the mount/camera AUX_FUNC lists match what those libraries initialise; failed-dispatch fallback, unreadable and unassigned channels, and the init_position_on_first_radio_read() exclusions (only ARMDISARM*, ARM_EMERGENCY_STOP, PARACHUTE_RELEASE — a no-op for these lists, which is the guard rail asked for); debounce bookkeeping on a later real switch change; no new or renumbered parameters; four commits, one per subsystem; git diff --check clean; 479 insertions / 0 deletions across 9 files, matching the description.
Tests: 4/4 on the boot tests, then 25 consecutive iterations of the two main ones — 54/54, no flakes, under a private netns with private buildlogs and no --uds. The ~3 % camera flake from earlier rounds did not reappear. CI: 102/102 passing.
One thing needing a human rather than another AI round: peterbarker's CHANGES_REQUESTED of 2026-09-16 is still the formal reviewDecision. All 12 inline comments have matching code changes, but he hasn't re-reviewed.
b8b1839 to
eca99fa
Compare
|
Pushed ReproducedInstrumented
Your 2459/2739 ms are exact. My captured value differs from your −19.820° only because my sim starts on a different heading — 3/3 identical runs on each tree. A ~103° error here, and from the AHRS half alone, on a backend that reports no attitude at all. That is a regression this PR introduced, in the default configuration, and it does not self-heal because Fixed by dropping it from the list
I took your point about
Flash, re-measuredDropping the entry saves a little, so the CubeOrange figure moved. Both boards rebuilt against
Re-runNine tests: the four boot tests, @peterbarker — all twelve of your inline comments have been addressed since your review, and two AI-found issues on top. A re-look when you have time would be welcome. |
Previous review (2026-09-20)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Re-reviewed at head Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_20_2330/devcall_pr_reviews.html#pr34413 Re-reviewed at head You took the suggested direction exactly: Previous round — resolvedBUG
I also checked the enum values your test hardcodes against the enum rather than the comment beside them: One correction to my own previous comment
The independent pass argued Two small notes
Also checked and cleanI audited every handler in both lists for handler-time state capture, which my previous round never did for the camera side: all seven camera handlers are pure switch-position dispatch, the two RunCam ones are flag setters, and every Tests, under a private network namespace with private Not APPROVE for one reason only, and it is not mine to clear: peterbarker's Admitted gaps: all timing evidence is SITL, hardware timing unverified; the |
RC_Channels::init() runs the auxiliary functions once, and on Copter it does so before camera_mount.init() has created the backends. RETRACT_MOUNT1/2, MOUNT_YAW_LOCK, MOUNT_RP_LOCK and MOUNT_POI_LOCK all reach AP_Mount's null-instance early return at that point, so the switch position the vehicle booted with is silently lost and the mount stays in its default mode until the first debounced read_aux(). Establish those positions from update() instead, via RC_Channels::apply_aux_switch_positions(). The one-shot is held pending until RC input is valid rather than taken at the first update. A receiver sitting in failsafe still streams frames with in-range PWM, so the switch reads cleanly and only has_valid_input() knows the difference; without this its failsafe positions are applied as though the pilot had selected them. Where RC only becomes valid later the position applied is the one the switch is in then, which is what the first debounced read_aux() would have applied anyway. Returning early instead of holding would disable this on Plane, where has_valid_input() is still false at the first update: read_radio() runs earlier in the same tick but has no fresh frame on it, so failsafe.last_valid_rc_ms is unset even though the startup failsafe path has already populated radio_in. From update() rather than the end of init(), because the switch cannot be read at the end of init() either: nothing has polled RC by then, and on Rover the mount is created before rc().init() at all. The switch is read rather than taken from the aux position cache (get_aux_cached()). That cache holds the position a function was last run with, and the run being recovered from is the one which had nothing real to record: nothing calls rc().read_input() before rc().init(), so radio_in is zero for every channel and RC_Channel::init_aux() substitutes LOW. Acting on that would act on a position the pilot never selected. Nothing is applied when no backend was created. The mount handlers return true whether or not an instance exists, so without that check a switch configured against MNT1_TYPE=0 would be recorded as applied and the first debounced read_aux() suppressed for a function which did nothing. Covered by Copter.MountAuxFunctionAtBoot, Plane.MountAuxFunctionAtBoot, Copter.MountAuxFunctionAtBootRCFailsafe and Copter.AuxFunctionAtBootWithoutRC.
The same defect AP_Mount has, and for the same reason: camera.init() allocates both the camera backends and the RunCam driver inside itself and the vehicle calls it after rc().init(), so CAMERA_REC_VIDEO, CAMERA_ZOOM, CAMERA_MANUAL_FOCUS, CAMERA_AUTO_FOCUS, CAMERA_LENS and the two RunCam functions all find a null primary instance at initialisation and return false. Establish their positions by the same route AP_Mount::init_aux_functions() uses, including holding the one-shot pending until RC input is valid. Deliberately outside the semaphore update() takes, since the handlers take it themselves. This only covers backends which are usable as soon as they are allocated. AP_Camera_MAVLinkCamV2 does not begin looking for its camera until ten seconds after boot and rejects commands until it has found it, so a switch position for one of those is still lost - as it is today, since the first debounced read_aux() records the position whether or not its handler succeeded. Closing that needs a backend readiness protocol. Covered by Copter.CameraAuxFunctionAtBoot.
Copter.MountAuxFunctionAtBoot boots with RETRACT_MOUNT1 already in the retract position and asserts the mount is retracted in the first MNT message logged. Fails on master, where that sample is Mode=3 (RC_TARGETING). Plane.MountAuxFunctionAtBoot asserts the same on Plane, which is worth covering separately because it is the vehicle where RC_Channels::has_valid_input() is still false at the mount's first update(): read_radio() runs earlier in the same tick but has no fresh frame on it, so failsafe.last_valid_rc_ms is unset even though the startup failsafe path has already populated radio_in. Returning early on that rather than holding the one-shot pending would leave this test at Mode=3 while Copter still passed. Copter.MountAuxFunctionAtBootRCFailsafe holds the vehicle in throttle failsafe from boot with the switch in the retract position, and asserts nothing is applied from it. A receiver in failsafe still streams frames with in-range PWM, so the switch reads cleanly; the test fails with "applied (source=0 pos=2) from a receiver in failsafe" if the one-shot is taken at the first update instead of held until the input is valid. Copter.CameraAuxFunctionAtBoot does the same for CAMERA_REC_VIDEO, and boots in failsafe for a second reason: the initialisation dispatch is then deferred until RC is recovered, well after the log file opens. A normal boot can dispatch before AP_Logger accepts writes and have the entry dropped, which would make an assertion on it flaky. AUXF.result then shows the dispatch reached a live backend, which is the thing being fixed. It covers the camera side of the failsafe gate at the same time. Both failsafe tests use FS_THR_VALUE 1100 rather than 1000, so that AP_RCProtocol_UDP's fabricated 1000us throttle is below the threshold and the failsafe holds from the first frame rather than only once the harness's own datagram lands. Copter.AuxFunctionAtBootWithoutRC boots with no RC protocol at all and asserts nothing is applied from a switch nobody could read, then enables RC in that same boot and asserts the position is applied after all. It passes on master too: it guards the property that no position is invented. The mount tests assert initialisation applied the switch as the absence of an RC-sourced AUXF rather than the presence of an INIT-sourced one, since that entry can be dropped as described above, and assert at most one INIT-sourced entry so that the one-shot cannot re-apply on every update. Both switches are on channels whose fabricated AP_RCProtocol_UDP default matches what the harness sends, reversed where needed, so the position read is not a race against the first datagram. Assertions wait for the vehicle to open a log other than the pre-reboot one, and poll for the AUXF they expect rather than assuming a fixed delay is enough; those helpers live in vehicle_test_suite.py so both vehicles can use them.
eca99fa to
19aaeae
Compare
|
Pushed The comment is down to three lines, essentially your wording, and it is folded into the AP_Mount commit rather than added on top, so the stack is still the same four commits: // MOUNT_YAW_LOCK is left out: set_yaw_lock() captures a heading from the
// gimbal attitude and AHRS yaw, neither ready this early, and only
// recaptures while unlockedNothing else moved — the tree differs from On the Thanks also for running down the |
|
Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/2026_09_22/devcall_pr_reviews.html#pr34413 Reviewed at head ISSUE — no "already applied" guard, so the reverse ordering causes a genuine double application
Both paths are gated on Your own PR body is the argument for why that matters: // read_aux() already established this position; nothing owed
if (c->switch_state.current_position >= 0) {
continue;
}
Being honest about the limits of this one: the starvation was modelled, not reproduced on firmware — a reduced scheduler model produced RC dispatch at 300 ms followed by INIT dispatch at 322.5 ms, which shows the ordering is permitted, not that it happens in practice. And NOTE — the INIT application bypasses debounce, and can now fire in flight
For a stable input the eventual command is the same, but for a transient it is not: HIGH at 0 ms then LOW at 100/200/300 ms dispatches only LOW on the master path, while the new helper dispatches HIGH immediately. Worth a sentence in the PR body, or gating the one-shot on NOTE — two latched mount/camera aux functions still lose their boot position
Process, not code
Checked and cleanThe aux-function lists in The design itself is not re-opened here: this is the shape @tridge asked for in #34166, and @peterbarker's separate caveat that Reviewed by Claude and independently cross-checked by a cold Codex pass shown no findings, which returned APPROVE. Codex confirmed the scheduler numbers and the mechanism of the race, and adjusted the strength of the claim — reflected above. |
Summary
Have
AP_MountandAP_Cameraestablish their own RC auxiliary switch positions on their firstupdate(), once their backends exist. Replaces #34166, which fixed the same defect by deferring all aux initialisation until after backend creation.Classification & Testing (check all that apply and add your own)
Testing performed:
./waf copter plane rover replayand./waf AP_Periph(sitl_periph_universal)Tools/autotest/autotest.py test.Copter.MountAuxFunctionAtBoot test.Copter.CameraAuxFunctionAtBoot test.Copter.AuxFunctionAtBootWithoutRC test.Copter.MountAuxFunctionAtBootRCFailsafe test.Plane.MountAuxFunctionAtBoot— all pass here; the mount/camera ones fail on the merge base (Mode=3in the firstMNTsample; first logged applicationsource=RC)Copter.CameraAuxFunctionAtBootrun 25 times consecutively, no failures (it was measured flaky at ~3% before the assertion was reworked); 10 more at this head with the announcement assertion addedMOUNT_YAW_LOCKback inAP_Mount's list makesMountAuxFunctionAtBootRCFailsafefail with "MOUNT_YAW_LOCK first applied by source=0, want RC"CameraAuxFunctionAtBootfail with "Failed to receive text: rc6: camera record video high"MountAuxFunctionAtBootRCFailsafefail with "applied (source=0 pos=2) from a receiver in failsafe"; removing the recorded position makes the RC-sourced-AUXF assertions failtest.Copter.Mount test.Copter.MountPOIFromAuxFunction test.Copter.AuxSwitchOptions test.Copter.AuxFunctionsInMission— 8/8, repeated to check the new tests are not flakytest.Rover.CameraMission— Rover creates the mount and camera beforerc().init(), so it exercises the opposite orderingtest.Copter.ModeLoiter— the one red CI job; passes locally, no aux involvement.bssdeltasizereports is a remainder rather than a measurement of RAMCameraAuxFunctionAtBootfail with no AUXF loggedTools/scripts/check_branch_conventions.py --base-branch upstream/master,git diff --check upstream/master...HEADDescription
RC_Channels::init()runs the auxiliary functions once, and on most vehicles it does so before the vehicle has created the backends those functions act on. On Copterrc().init()is ininit_ardupilot()well ahead ofcamera_mount.init()andcamera.init(), and both of those allocate their backends (NEW_NOTHROW) inside themselves.So
RETRACT_MOUNT1/2,MOUNT_YAW_LOCK,MOUNT_RP_LOCKandMOUNT_POI_LOCKreachAP_Mount's null-instance early return, andCAMERA_REC_VIDEO,CAMERA_ZOOM,CAMERA_MANUAL_FOCUS,CAMERA_AUTO_FOCUS,CAMERA_LENS,RUNCAM_CONTROLandRUNCAM_OSD_CONTROLreach the primary-instance overloads and returnfalseon a null primary. A guaranteed silent no-op.What this is worth, stated honestly
debounce_positionandcurrent_positionboth start at-1, so where RC becomes valid the first debouncedread_aux()applies the switch about 200 ms later. What this fixes is that window, while disarmed. That is the whole of it. It deliberately does not cover the case where RC never becomes valid: with frames still arriving that is the failsafe case, which is exactly what must not be acted on.It is not an explanation for this 4.7.1-beta report, which #34166 cited as motivation and which I should have checked more carefully before repeating. That report is
MOUNT_RP_LOCKHIGH at boot, and HIGH sets_roll_lock/_pitch_locktrue — already their initialiser values inAP_Mount_Backend.h, re-derived every update in RC targeting. A lost boot position would not change anything there, with or without this PR. Whatever that user hit, this does not fix it.I still think the defect is worth closing — a switch position that is silently discarded is a bug whatever its blast radius, and it is one of the few places in the codebase where initialisation is known to run against a null backend — but reviewers should weigh it at its real size.
Each library establishes its own positions
RC_Channels::apply_aux_switch_positions()takes a set of functions, finds the channel carrying each, reads the switch, and re-dispatches throughRC_Channel::run_aux_function()— not through a second copy of the handler, so there remains one implementation of what each switch position means.AP_MountandAP_Cameraeach call it once, from the top of their firstupdate(), with their own list.Why the first
update()and not the end ofinit(). The switch cannot be read at the end ofinit(): nothing has polled RC by then, and on Rovercamera_mount.init()runs beforerc().init()at all. By the firstupdate()the vehicle'ssetup()has finished and on most vehicles something has read the receiver during it — Copter'sesc_calibration_startup_check()waits up to two seconds for first RC input, Plane'sfailsafe_check()reads it once the main loop has stalled. Where nothing has (TradHeli and brushed-PWM Copter skip that poll; Rover, Sub, Blimp and Tracker have no equivalent) nothing is applied until RC does come up, at which point the position applied is the one the switch is in then - the same one the first debouncedread_aux()would have applied.Why not
get_aux_cached()This was the obvious cheap fix and it does not work, which is worth recording because it was suggested more than once.
RC_Channelsalready caches the position each function was last run with, and the run this is recovering from is exactly the one that had nothing real to record. Nothing callsrc().read_input()beforerc().init(), soradio_inis zero for every channel,read_3pos_switch()fails, andRC_Channel::init_aux()substitutesLOW. I instrumented master and confirmedlive=0 radio_in=0on every Copter boot.Replaying that cached
LOWwould not restore the pilot's switch position — it would act on a position the pilot never selected.CAMERA_ZOOMLOWis a continuous zoom-out rate command,CAMERA_MANUAL_FOCUSLOWa continuous focus-in,MOUNT_POI_LOCKLOWcallsclear_poi_lock()and announces "POI: Cleared",RETRACT_MOUNT1LOWcallsset_mode_to_default(), overriding theRC_TARGETINGSub deliberately sets right after creating the mount.Reading the switch at first update gives the position it is really in, and where it cannot be read nothing is applied at all.
Copter.AuxFunctionAtBootWithoutRCpins that: it boots with no RC protocol and asserts no position is invented.Only channel-assigned functions
A function invoked solely over MAVLink (
MAV_CMD_DO_AUX_FUNCTION), by a mission item, or by a script needs noRCx_OPTIONat all —GCS_MAVLINK::handle_command_do_aux_function()callsrc().run_aux_function()directly. Those are deliberately not recovered here: each of those callers is told when a dispatch fails (MAV_RESULT_FAILED). The RC path has no way to report anything, which is precisely what makes it the one that loses silently.The duplicate application
Neither master's
init_aux()nor this touchesswitch_state, andcurrent_positionstarts at-1, so without care the first debouncedread_aux()would run every function a second time. That is not harmless:set_poi_lock()re-savessaved_mount_mode, a Siyi gimbal sends a second autofocus packet, andAP_RunCam::osd_option()latches a menu action the update loop consumes.The helper therefore records an applied position as the channel's current switch position, under three conditions, each of which keeps an application the first debounced read is still owed:
run_aux_function()returning false (a camera backend that rejects commands until discovery completes) is not recorded, so the read retries it.truewhether or not an instance is there, soAP_Mountskips the whole thing when_num_instances == 0; otherwise a switch configured againstMNT1_TYPE=0would be recorded as applied and the read suppressed for a function that did nothing.A switch that genuinely moves between the two is still picked up, since
debounce_completed()compares against the recorded position. The tests assert at most one initialisation-sourced AUXF and no RC-sourced one, and go red if the recording or the one-shot latch is removed.Functions in
RC_Channel::init_position_on_first_radio_read()—ARMDISARM,ARM_EMERGENCY_STOP,PARACHUTE_RELEASE— are skipped outright by the helper. None are used by these two libraries, but reading a switch and acting on it is exactly what that list exists to prevent, and a generic helper must not become a way around it.The GCS announcement is preserved
read_aux()announces each position it applies —RC7: RetractMount1 HIGH— but only oncedebounce_completed()returns true, so recording the applied position, which is what stops the function running twice, also suppresses that announcement. Left alone the switch would be applied and nothing said, and that message is exactly what a user checks when a switch appears to do nothing.The helper therefore makes the same announcement itself, on a dispatch which took. One which did not is not recorded, so the first debounced read still announces it: one announcement either way, same format, same strings, same
AP_RC_CHANNEL_AUX_FUNCTION_STRINGS_ENABLEDguard.Copter.CameraAuxFunctionAtBootwaits forRC6: Camera Record Video HIGHand goes red without it.MOUNT_YAW_LOCKis deliberately not recoveredAP_Mount_Backend::set_yaw_lock()does not just set a flag: while the axis is unlocked it captures_yaw_lock_heading_radfrom the gimbal's attitude quaternion plusAP::ahrs().get_yaw_rad(), and it recaptures only from the unlocked state — so a heading taken at boot latches and stays until the pilot toggles the switch. Neither input is ready at the mount's firstupdate().Measured, servo mount,
MOUNT_YAW_LOCKHIGH at boot, instrumentedset_yaw_lock()on both trees (three runs each, identical every time):AP_Mount::update())35356c656cread_aux())A ~103° error, from the AHRS half alone, on a backend that reports no attitude at all. It is consumed at
AP_Mount_Backend.cpp:159under the defaultMNT1_DEFLT_MODE=RC_TARGETINGandMNT1_RC_RATE=0, so it reaches the default configuration.The other four functions do not do this —
RETRACT_MOUNT1/2callset_mode(),MOUNT_RP_LOCKsets two bools, andMOUNT_POI_LOCKstarts a POI search that times out identically on both trees (AP::ahrs().get_location()does not succeed until ~10 s). SoMOUNT_YAW_LOCKis left out ofAP_Mount's list and keeps master's behaviour: the first debouncedread_aux()applies it 200 ms later.Copter.MountAuxFunctionAtBootRCFailsafeasserts the split —RETRACT_MOUNT1first applied byINIT,MOUNT_YAW_LOCKfirst applied byRC— and goes red if it is put back.A
healthy()gate would not have been enough:AP_Mount_Backend::healthy()defaults to true, and it says nothing about AHRS yaw.Known limitation
This covers backends usable as soon as they are allocated.
AP_Camera_MAVLinkCamV2does not begin looking for its camera until ten seconds after boot and rejects commands until it has found it, so a switch position for one of those is still lost — as it is on master, since the first debouncedread_aux()records the position whether or not its handler succeeded. Closing that needs a backend readiness protocol rather than a one-shot at first update, and is out of scope here.Applying the position is held until RC input is valid
RC_Channels::read_aux_all()refuses to act when!has_valid_input(), and a receiver sitting in failsafe still streams frames with in-range PWM — so reading the switch at boot and acting on it immediately would apply the receiver's failsafe positions as though the pilot had selected them. That was the open trade-off in the first version of this PR, and it is now closed.The one-shot is held pending until
has_valid_input()is true, rather than consumed at the firstupdate().Copter.MountAuxFunctionAtBootRCFailsafeholds the vehicle in throttle failsafe from boot and asserts nothing is applied; it fails if the one-shot is taken early.Returning early on
!has_valid_input()instead of holding would not work, which is worth recording because it is the obvious version. Measured on the firstMNTsample withRETRACT_MOUNT1in the retract position at boot:Plane's
has_valid_input()is still false at the mount's firstupdate():read_radio()runs earlier in the same tick but has no fresh frame on it, sofailsafe.last_valid_rc_msis unset even though the startup failsafe path has already populatedradio_in.Plane.MountAuxFunctionAtBootexists so that difference cannot be lost by accident.This does mean an aux function can no longer be recovered where RC never becomes valid — which is the point: with frames arriving, that case is the failsafe case.
Diffstat
501 insertions, 0 deletions. No parameter changes, no new feature defines, and no change to the aux position cache, which stays scripting-only.
This contribution was developed with Codex and Claude AI assistance. The AI helped analyse the initialisation ordering, implement the change and its regression tests, and prepare this description.