* docs(instructions): keep agents out of the Crowdin-owned translation trees
docs/ko, docs/zh and docs/uk regenerate from docs/en, but nothing in the scoped guidance said so, and repo-wide sweeps edit them along with the English source.
Assisted-by: Claude:claude-opus-5-5
* docs(agents): state the translation rule in AGENTS.md
Agents load AGENTS.md directly; the .github/instructions file only reached them through a pointer to go read it.
Assisted-by: Claude:claude-opus-5-5
The commit hash compiled into px4_firmware_version_binary() changes its code size, so a PR with no code change reported -8 B on v6x and got a comment. Changes that only touch agent instructions also no longer build the targets.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* docs(agents): let a user's own instructions set the worktree location
A fixed ../PX4-Autopilot-worktrees/ path overrode contributors who keep worktrees elsewhere. Their own agent instructions now win, with the sibling directory as the default.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* docs(agents): italicize the PR body's Assisted-by line
Set apart in italics, the disclosure reads as a footnote rather than part of the Solution section above it.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
---------
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Use the arming-check events to explain initialization waits without misleading legacy preflight messages.
Assisted-by: Codex
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Daniel Agar, Paul Riseborough, and David Sidrane no longer work on the project. Architecture has been vacant in practice.
Assisted-by: Grok:grok-4.6
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(ci): report flash and static RAM usage separately
Bloaty's VM total includes zero-initialized RAM and understates the combined cost of data and code copied from flash into RAM. Report the flash load image and static RAM allocations independently so the PR headline reflects each resource's actual change.
Assisted-by: Codex
* docs(ci): trim memory analysis comment
Assisted-by: Codex
* fix(ci): diff PR flash usage against the merge commit's base
The base branch tip is fetched when the job runs, so a PR merged while its job was queued was compared against itself and reported a near-zero change, and commits landed in the meantime showed up as reverted.
* fix(ci): measure flash image from section headers
ld maps the ELF header into the first LOAD segment when its max page size exceeds the flash origin's alignment (binutils 2.34 defaults to 64 KiB), so segment-based accounting rejected valid firmware as outside the flash region.
* feat(ci): flag notable flash and RAM changes
Growth over 100 B and 1000 B gets yellow and red markers, savings over 100 B green, so reviewers can spot size regressions without reading the numbers.
* refactor(ci): derive flash and RAM totals without per-target constants
The matrix duplicated each board's flash-analysis linker region, which would drift silently. Section headers alone distinguish code executing in place (VMA == LMA) from data copied to or reserved in RAM, and the image span matches objcopy -O binary.
* fix(ci): limit the bloaty breakdown to VM sizes
Debug, symbol and string table rows only change the file size and made up most of the comment.
* fix(ci): post flash analysis only when size changes
A push that puts flash and static RAM back to zero was still rewriting the sticky comment, so a PR with no size change carried a zero report. Remove that comment instead. Fork tokens cannot delete it, so those PRs hand a delete artifact to the poster.
Assisted-by: Grok:grok-4.7
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(ci): retain zero-delta flash analysis comments
Existing reports can be updated when size deltas return to zero, avoiding a separate privileged deletion path.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Assisted-by: Codex:gpt-6
---------
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* chore(agents): move Claude instructions and skills to AGENTS.md and .agents
Claude Code now reads AGENTS.md and .agents/skills, so one client-agnostic copy serves every assistant and the Codex adapters that pointed back into .claude go away. The build skill splits by host: Linux builds natively, macOS needs the container.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* chore(agents): expose .agents to Claude Code via imports and symlinks
Claude Code reads AGENTS.md only when no CLAUDE.md or CLAUDE.local.md exists and never reads .agents/, so the skills and instructions were invisible to it. Worktrees move outside the repository, where no tool's recursive search reaches them.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* chore(agents): refine build, commit, pr and review-pr skills
Linux builds run in whatever checkout the agent is in. PR bodies now carry the Assisted-by disclosure and an optional Testing section for testing worth reporting. Commits preserve history for reviewers since PRs are squash-merged. Reviews can check the area's history for intentional or in-flight work.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* feat(agents): push and refresh the PR description from the commit skill
A commit on a pushed branch otherwise leaves the PR stale until someone remembers to push and re-edit it.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(agents): drop trailing blank line in .gitignore
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
---------
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(flashparams): append unsaved deltas and compact at boot
Same-bank program of a full BSON snapshot stalls instruction fetch for tens of ms, which drops a high-rate IMU FIFO and DShot while disarmed. The 1 s sector erase belongs at boot, not on a wrap mid-session.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(flashparams): serialize compact and encode before erase
param_reset_all() queued an autosave that could program flash during the boot compact erase. Reset without autosave under file_mutex, build the snapshot in RAM before erasing, and compact only when a burst of deltas would not fit so COM_FLIGHT_UUID is not a 1 s erase every flight.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(flashparams): append in log order and encode resets as null
Walk order was not write order on multi-sector maps, and a torn H7 header skipped the rest of the sector so a successful retry vanished. Resets encoded as the current default also came back as overrides after a firmware default change.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(flashparams): version the log token and keep default-equal overrides
Records carry a new `plog` token. Firmware before the log format wrote one
`parm` snapshot and cannot replay deltas; it ignores `plog` records and
treats a store without `parm` as empty (STM32H7 erases it at init), so a
downgrade boots on defaults instead of loading a stale snapshot and then
failing every save into the deltas behind it. A `parm` record is imported
alone as the newest state and the store is rewritten as a `plog` snapshot.
The boot snapshot and import no longer drop values equal to the current
default. They run before the airframe applies its set-default values, so
the comparison discarded user overrides that happened to match the firmware
default. Deltas still tombstone a value matching the default in force, as
the file backend does at save time.
Boot compaction stages the snapshot before deciding, and a failure before
the erase keeps the log and returns success: RAM holds what the log said,
so the store is not corrupt. Compaction is skipped when the compacted
layout would lack the headroom too, so a large snapshot does not erase on
every boot. Appends are row aligned on H7, header fields are read only
once the header lies inside the sector, and the append scan stops at a
blank header like the replay walker.
* fix(param): clear only the unsaved bits that were pending when the save began
A param_set that lands while the export runs is not in what gets written.
Clearing the whole bitset afterwards lost its bit. Full-snapshot saves
picked the value up on the next save; flash deltas never wrote it.
* fix(flashfs): keep every record inside the range the walkers read
The walkers stop at a sector's last word and reject a record that ends past it, but the writer accepted a record that ends anywhere inside the sector. A record landing in the last four bytes was reported written and never replayed, and a snapshot of exactly max_payload bytes was unreadable, so import returned -EILSEQ. The F7 writer also padded the entry to a word inside a heap block allocated without room for the padding.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(param): keep the unsaved bit of a param set during a save
Clearing the bits that were pending when the save began still dropped a param_set that landed after the exporter had read that param's value. Move the pending bits to a saving set before the export so a set during the save marks the param again, and put them back if the save fails.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(flashparams): cover file imports, tombstones and stale records
A delta carries only params with the unsaved bit, and three paths left RAM ahead of the log with no bit set. A file import (param import, param load, the rcS backup recovery) set values as saved, so the param save that followed wrote nothing and the values reverted on reboot; on flash boards an import now leaves its changes unsaved, as the file backend leaves them until its next full export. A BSON null reached param_modify_on_import with the previous node's value still in the union, so a translation reading it without a type check could fire on a tombstone; decode tombstones first. A record that no longer names a param, or names one of another type, was replayed through a translation on every boot and kept until a compaction happened to run; treat it as a reason to compact at boot. A log whose replay failed inside a CRC-valid record, or that has no snapshot, gets a snapshot on the next save instead of a delta nothing would reach.
The buffer encoder wrote 0 as the document length, which the file importer takes as a blank store; record the real length so a buffered document is a valid BSON file.
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
---------
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Neither topic was published, so ekf2 kept its accel and gyro device ids at
0, skipped the bias reset that follows a device change, and reported
estimator_sensor_bias with zeroed ids and accel_bias_valid false.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
Until the attitude became valid, replay mode published a zero-filled
vehicle_attitude carrying a null quaternion, so a replayed log opened with
samples a reader has to know to discard.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
The estimator was started near the top of the script and logging at the
very end, so its startup - the alignment and the first resets - was never
in the log and an ekf2 replay could not reproduce it.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
The replay barrier only waited for the logger, so the next imu sample
could be published before ekf2, running on a work queue, had consumed the
previous one.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
sensor_combined carries the same timestamp as ekf2_timestamps and won the
main loop's tie, so it was consumed without being published and every
barometer, magnetometer and range sample reached ekf2 one update late.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
vehicle_gps_position, vehicle_land_detected and vehicle_status were
published from the main loop, so which ekf2 update they landed in was a
race that came out differently on two replays of the same log.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
ekf2_timestamps is the last publication of an ekf2 update and the
only one produced on every update, so waking on it logs whole updates
and keeps the lockstep barrier fed before the attitude becomes valid.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: bresch <brescianimathieu@gmail.com>
* fix(sensors): do not register a callback for a disabled barometer
Since disabled barometers are fed to the voter at priority 0 they also
register a sensor_baro callback while nothing is selected. Callbacks are
only cleared on a selection change, and with every barometer disabled
the selection stays at -1, so a lone disabled barometer woke Run() and
UpdateStatus() at its sample rate indefinitely instead of on the 50 ms
schedule. The scheduled cycle already keeps the disabled validator fed,
and a re-enabled sensor registers again when it is selected.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* test(sensors): cover the disabled-barometer callback gate
Same harness as the vehicle_magnetometer functional test: a lone
barometer disabled from boot must leave no sensor_baro callback
registered, and must register one once it is re-enabled and selected.
Fails without the gate in Run().
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
---------
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
* fix(sensors): keep disabled magnetometers in the voter at priority 0
The magnetometer module skipped a disabled sensor entirely, so the voter
never learned it was disabled. Disabling the selected sensor at runtime
timed it out, reported it as failed and raised its priority back to 1,
which put it into the inconsistency check with a frozen field vector. A
sensor disabled at boot never got a validator, so the samples of the next
uORB instance were dropped and it could not be selected.
Feed every advertised magnetometer to the voter with its priority, where 0
excludes it from selection and failover accounting, sum and publish only
enabled ones, never raise a disabled sensor's priority on failover, and
clear the selection when no magnetometer is left. This is the handling
#28351 gave the barometer.
Three functional tests cover the runtime disable, a disabled slot ahead of
a live sensor, and disabling the only magnetometer and enabling it again.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
* fix(sensors): do not register a callback for a disabled magnetometer
Feeding disabled sensors to the voter also made them register a
sensor_mag callback while nothing is selected. Callbacks are only
cleared on a selection change, and with every magnetometer disabled the
selection stays at -1, so a lone disabled magnetometer woke Run() and
UpdateStatus() at its sample rate indefinitely instead of on the 50 ms
schedule. The scheduled cycle already keeps the disabled validator fed,
and a re-enabled sensor registers again when it is selected.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
---------
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Each default driver instantiation emits the same allocation and initialization failure handling. Share that code outside the template while keeping init() resolved on the concrete driver. Allocation failures are now logged under SPI_I2C.
Extracted from Balduin (mbjd) in PX4/PX4-Autopilot#27816, commit 8a2391730f.
Assisted-by: Codex
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Balduin <balduin@auterion.com>
The header currently gives each consumer its own copy of the navigation state name table. Use C++17 inline linkage so the linker can retain one copy.
Extracted from Balduin (mbjd) in PX4/PX4-Autopilot#27816, commit a0a0ebf69e.
Assisted-by: Codex
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Balduin <balduin@auterion.com>
px4_fmu-v2_default was already at 99.98% flash on main (1031972 of
1032192 bytes), leaving 220 bytes of headroom, so any change touching
common code overflows it.
The Septentrio driver was inherited by this board when it was split out
of the gps driver, rather than chosen for it, and it costs 12.5 KB here.
A high-end RTK receiver is an unlikely pairing with fmu-v2 anyway, and
the other v2 variants are overlays on this config, so all four regain
the same headroom:
default 100.03% -> 98.81%
fixedwing 99.59% -> 98.42%
multicopter 99.19% -> 98.02%
rover 97.86% -> 96.69%
It remains enabled on fmu-v3 and fmu-v4, which have 2 MB of flash.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Julian Oes <julian@oes.ch>
In rate mode the RC input only flagged the axes as angular rate, without
saying whether they are locked or following the vehicle. The outputs
then kept using the frame of whatever setpoint came before, so e.g. RC
yaw rate went out with yaw lock after a ROI but without it after boot.
Set the frame the same way as it is already done for RC angle input:
roll and pitch relative to the horizon, and yaw according to
MNT_DO_STAB. This matches what the MAVLink gimbal v2 input does for
rate-only setpoints.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Julian Oes <julian@oes.ch>
Control of the external gimbal manager was only requested once, when
onboard intent started. RC input stays active once it has been used, so
if that request got lost or the manager restarted, we never asked again
and a manager which only accepts setpoints from whoever is in control
ignored us from then on.
Ask again, rate limited, while we have onboard intent and the manager
reports that nobody is in control. This can't take control away from
another client, so the arbitration stays with the manager.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Julian Oes <julian@oes.ch>
A gimbal with its own gimbal manager is typically connected to a MAVLink
instance in gimbal mode. That mode only handles a whitelist of incoming
messages and only streams a few messages, so the external gimbal manager
was never discovered there and would not have received any setpoints.
Accept GIMBAL_MANAGER_STATUS as well as COMMAND_ACK (for the
DO_GIMBAL_MANAGER_CONFIGURE that we send, otherwise it is retried until
it times out) and stream GIMBAL_MANAGER_SET_PITCHYAW in gimbal mode.
This also makes the check to not ingest the gimbal device information of
an external gimbal manager work on such a link, as that depends on
having seen its GIMBAL_MANAGER_STATUS.
While at it, ignore the status of gimbal managers of other systems, e.g.
forwarded from another vehicle. Only a manager on our vehicle is ours to
talk to.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Julian Oes <julian@oes.ch>
Tracking the manager's control state ourselves could desync from the
manager and stop setpoints. Acquire once when onboard intent starts,
stream setpoints while it lasts, release with -3 (was -2, which means
"set myself in control").
Assisted-by: Claude:claude-opus-5
Signed-off-by: Julian Oes <julian@oes.ch>
vehicle_roi (ROI_WPNEXT/ROI_LOCATION) set the gimbal setpoint but left
sysid/compid_primary_control untouched. The external gimbal manager output only
forwards intent that originated onboard (primary control == autopilot), so ROI
was not forwarded unless something else (e.g. a gimbal test) had already seeded
the primary control. ROI is autopilot-driven, so set it accordingly.
Signed-off-by: Julian Oes <julian@oes.ch>
When another component is a gimbal manager (we see its GIMBAL_MANAGER_STATUS),
its GIMBAL_DEVICE_INFORMATION describes a gimbal we don't manage. Publishing it
into our own gimbal_device_information topic made OutputMavlinkV2 latch onto the
foreign device id, so PX4 then advertised its own gimbal manager with the wrong
gimbal_device_id - and a ground station could no longer match it to its device
attitude, dropping our gimbal entirely in a two-gimbal setup.
Track external gimbal manager component ids and skip ingesting their device
information. The message is still forwarded to the ground station, so it can
still show that gimbal's name. A plain gimbal device never sends
GIMBAL_MANAGER_* and is therefore unaffected.
Signed-off-by: Julian Oes <julian@oes.ch>
Refine the external gimbal manager client so it coexists with a ground
station instead of fighting it:
- Only forward setpoints that originated onboard (primary control is the
autopilot). A ground station commands the external manager directly, so
relaying its commands would make PX4 fight it for control.
- Acquire control only on a fresh onboard-intent edge, and yield instead
of re-grabbing when another controller takes over - this removes the
acquire/release ping-pong. A new onboard edge is needed to reclaim.
- Apply the setpoint every cycle while in control, not only on the
new_setpoints edge: the control handshake can delay reaching the
in-control state past that edge, which would otherwise stream NaN.
Signed-off-by: Julian Oes <julian@oes.ch>
The test input's angle branch set only the quaternion, leaving
angular_velocity at its stale (zero) value. A downstream that prioritizes
rate over angle - e.g. the SITL gz gimbal, which holds position on a
finite near-zero rate - then ignores the commanded angle, so
'gimbal test pitch X' did not move the gimbal. Set the rates to NaN for
angle commands, matching InputRC's angle mode.
Signed-off-by: Julian Oes <julian@oes.ch>
The ROI forwarding path only needs GIMBAL_MANAGER_STATUS from an external
manager: it provides discovery (manager sysid/compid, gimbal device id) and
control ownership. GIMBAL_MANAGER_INFORMATION only added capability flags and
angle limits, which nothing uses until RC forwarding (deferred).
Carrying the extra uORB topic and its receiver handler pushed the tightest
board variant over its flash budget (ark_pi6x_encrypted_logs overflowed by
500 bytes). Drop the external_gimbal_manager_information topic; discovery and
ownership come from the status topic. It can return with RC, which actually
needs the angle limits.
Signed-off-by: Julian Oes <julian@oes.ch>
Adding publishes_mount_orientation() with 'override' made OutputRC mix
marked and unmarked overrides, which clang rejects under
-Winconsistent-missing-override (macOS build and clang-tidy). gcc doesn't
flag it, so the NuttX builds passed. Mark update()/print_status() override.
Signed-off-by: Julian Oes <julian@oes.ch>
Upstream renamed the gps_inject_data topic to rtcm_data. A rebase left the
old include alongside the new rtcm_data one, breaking a clean build of the
mavlink module (the header no longer exists).
Signed-off-by: Julian Oes <julian@oes.ch>
Add an output mode (MNT_MODE_OUT[2]=3) that makes PX4 act as a gimbal
manager client rather than driving a gimbal device: it discovers an
external gimbal manager from the awareness topics, requests primary
control via DO_GIMBAL_MANAGER_CONFIGURE while there is an active setpoint,
and streams the computed pitch/yaw as GIMBAL_MANAGER_SET_PITCHYAW.
This lets a smart camera-gimbal that runs its own manager still be pointed
by PX4's ROI/mission logic: PX4 does the geo-pointing (reusing the same
OutputBase math as the v2 device output) and forwards the resulting
pitch/yaw, using the standard MAVLink gimbal manager messages.
Control ownership follows the spec: it changes only via acquire/release,
and control is released on an idle (neutral) setpoint so a ground station
can take over. While this mode is selected PX4 no longer advertises itself
as the gimbal manager, since it is a client of the external one.
RC forwarding (GIMBAL_MANAGER_SET_MANUAL_CONTROL) and vehicle-yaw-assisted
ROI are intentionally left for follow-ups.
Signed-off-by: Julian Oes <julian@oes.ch>
PX4 today only produces GIMBAL_MANAGER_INFORMATION/STATUS (as its own gimbal
manager); it has no way to learn about a gimbal manager running on another
component. To interoperate with an external gimbal manager (e.g. a smart
camera-gimbal that runs its own manager), PX4 needs to discover it and track
who currently controls it.
Decode incoming GIMBAL_MANAGER_INFORMATION and GIMBAL_MANAGER_STATUS from
other components into the new external_gimbal_manager_information and
external_gimbal_manager_status uORB topics, carrying the manager's sysid and
compid from the message frame. Messages originating from our own
system/component are ignored so we don't ingest our own streamed manager
messages.
This is the awareness layer that a gimbal-manager client output builds on.
Signed-off-by: Julian Oes <julian@oes.ch>
Generalize the single output backend into a list of up to two outputs,
driven with the same setpoints each cycle. A new MNT_MODE_OUT2 parameter
selects an optional second output (disabled by default).
This is behavior-preserving groundwork: with MNT_MODE_OUT2 disabled the
driver runs exactly one output as before. It enables driving a second
gimbal in addition to the primary one, e.g. forwarding to an external
gimbal manager.
The two outputs must use different modes to avoid two instances fighting
over the same device or uORB topic. Whether the estimated mount
orientation is published is now decided per output via a virtual, since a
MAVLink gimbal reports its own orientation while an AUX gimbal does not.
Signed-off-by: Julian Oes <julian@oes.ch>
The schema path still says component_information/, so --skip-if-no-schema makes the check a no-op. QGC only accepts parameter metadata version 1, while the schema floor rose for optional fields. Override that floor and keep emitting version 1.
Assisted-by: Grok:4.7
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>