The fail status union and the check mask enum each restated a check layout by hand next to the msg constants, joined by a per-check map, so appending a check to one copy silently misassigned it in the others. GnssChecks now keeps one fail word indexed by the msg constants, and one table maps each check to its EKF2_GPS_CHECK bit, checked at compile time. Saved parameters, logs and events keep their bit layouts.
Assisted-by: Claude:claude-opus-5-5
#28873 merged after the GPS to GNSS rename was tested, so its position-loss report still read sensor_gps_s from vehicle_gps_position and main doesn't build.
Assisted-by: Claude:claude-opus-5-5
Templating GainCompression3d on its parameter IDs made the compiler emit
every method twice on boards that build both the fixed-wing and the
multicopter rate controller, as each instantiation is a separate class.
Inlining does not remove the duplicate because update() is too large to
be inlined.
Make GainCompression3d a plain class configured through setters and let
each rate controller own its FW_GC_* or MC_GC_* parameters. This saves
1040 bytes of flash on px4_fmu-v6x_default with no change in behaviour.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Andrea Bernasconi <andrea.bernasconi@auterion.com>
* refactor(msg): rename SensorGps to SensorGnss and add VehicleGnss
GPS is one constellation; the receivers and the rest of PX4 handle GNSS. Field names lose their units (latitude, speed_accuracy, course, ...), which move into the metadata comments. The sensors module's output becomes VehicleGnss on vehicle_gnss: the selected receiver's sample next to what the sensors module adds to it (corrected sample time, antenna position, selected instance), so a consumer can tell the two apart and later selection and check results have a place to go.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/gps): publish sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/septentrio): publish sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/pps_capture): read sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(lib/gnss): select receivers on sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(sensors): publish VehicleGnss on vehicle_gnss and rename SENS_GPS* to SENS_GNSS*
The sensors module's output nests the selected receiver's sample, with the corrected sample time, antenna position and selected instance next to it. SENS_GPS_MASK/TAU/PRIME and SENS_GPSn_ID/OFFX/OFFY/OFFZ/DELAY take the SENS_GNSS name the heading parameters already use; saved parameters migrate on import, and the EKF2_GPS_POS/EKF2_GPS_DELAY translations now land on the new names directly.
Assisted-by: Claude:claude-opus-5-5
* refactor(logger): log sensor_gnss and vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(ekf2): fuse vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(replay): replay vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(local_position_estimator): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(attitude_estimator_q): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(lib/terrain_estimation): take sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(lib/wind_estimator): read vehicle_gnss in the replay script
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(vision_target_estimator): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(navigator): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(commander): read sensor_gnss and vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(mavlink): stream sensor_gnss and vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(uxrce_dds_client): bridge vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(zenoh): bridge vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(failure_injection): inject GNSS failures on sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/ins): publish sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/uavcan): publish sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/uavcannode): read sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/cyphal): bridge sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/telemetry): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/rc): read vehicle_gnss for telemetry
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/osd): read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(drivers/transponder): read sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(boards/modalai): publish sensor_gnss from the voxl2 drivers
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(examples): publish sensor_gnss and read vehicle_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(simulation): publish sensor_gnss
Follows the SensorGps to SensorGnss and vehicle_gps_position to VehicleGnss rename.
Assisted-by: Claude:claude-opus-5-5
* refactor(gnss): name GNSS variables gnss in the sensors module and receiver drivers
GPS is one constellation. Identifiers that hold or handle GNSS samples, or bind to the renamed parameters, follow the message and parameter rename; classes, files, modules and names tied to parameters that keep GPS stay.
Assisted-by: Claude:claude-opus-5-5
* refactor(gnss): name GNSS variables gnss in the estimators and navigator
GPS is one constellation. Identifiers that hold or handle GNSS samples, or bind to the renamed parameters, follow the message and parameter rename; classes, files, modules and names tied to parameters that keep GPS stay.
Assisted-by: Claude:claude-opus-5-5
* refactor(gnss): name GNSS variables gnss in commander, mavlink and failure injection
GPS is one constellation. Identifiers that hold or handle GNSS samples, or bind to the renamed parameters, follow the message and parameter rename; classes, files, modules and names tied to parameters that keep GPS stay.
Assisted-by: Claude:claude-opus-5-5
* refactor(gnss): name GNSS variables gnss in the remaining drivers and simulation
GPS is one constellation. Identifiers that hold or handle GNSS samples, or bind to the renamed parameters, follow the message and parameter rename; classes, files, modules and names tied to parameters that keep GPS stay.
Assisted-by: Claude:claude-opus-5-5
* docs(gnss): reference sensor_gnss, vehicle_gnss and SENS_GNSS* parameters
The upgrade guide covers the topic, field, ROS 2, parameter and Cyphal register renames.
Assisted-by: Claude:claude-opus-5-5
* fix(boards): name EKF2::UpdateGnssSample in the fmu-v6xrt ITCM lists
The rename left the old UpdateGpsSample symbol in the lists, so the ITCM check fails and the function drops out of ITCM on the boards it doesn't check.
Assisted-by: Claude:claude-opus-5-5
* chore(drivers/gps): bump PX4-GPSDrivers to the merged sensor_gnss rename
The submodule pointed at the PX4-GPSDrivers#243 branch commit; #243 is merged.
Assisted-by: Claude:claude-opus-5-5
* docs(msg): restore WGS84 in SensorGnss and point VehicleGlobalPosition at vehicle_gnss
The rename dropped the only mention of the WGS84 datum and ellipsoid, and left VehicleGlobalPosition referring to the removed vehicle_gps_position topic.
Assisted-by: Claude:claude-opus-5-5
* refactor(msg): drop selected_instance from VehicleGnss
receiver.device_id already names the selected receiver, and 0 while blended. The sensor_gnss instance depends on the order drivers advertise at boot, so it doesn't identify a receiver across boots or logs, and nothing reads it. It would also be meaningless on the per-receiver VehicleGnss instances proposed in #28894.
Assisted-by: Claude:claude-opus-5-5
* refactor(gnss): fuse vehicle_gnss.timestamp_sample directly and document the receiver's corrected time
vehicle_gnss.timestamp is now the sensors module's publish time, so EKF2's check of timestamp_sample against it was always true; the sensors module always fills timestamp_sample. The message docs described receiver.timestamp_sample as the driver's uncorrected value, but the sensors module writes the corrected time into it as well.
Assisted-by: Claude:claude-opus-5-5
* docs(msg): point VehicleGnss users at the top-level timestamp_sample
uXRCE-DDS shifts only top-level timestamp and timestamp_sample into agent time, so receiver.timestamp_sample reaches ROS 2 on the PX4 boot clock even though it holds the same value on uORB.
Assisted-by: Claude:claude-opus-5-5
* docs(releases): tell ROS 2 users which VehicleGnss timestamps to read
Only top-level timestamps are converted to ROS time, so a port that reads receiver.timestamp gets the PX4 boot clock, and vehicle_gnss.timestamp is now the sensors module's publish time rather than the receiver's.
Assisted-by: Claude:claude-opus-5-5
* feat(mc_rate_control): add rate loop gain compression
Extend the gain compression already available on fixed-wing to the
multicopter rate controller, so that an oscillation (limit cycle) on the
torque setpoint dynamically reduces the loop gain instead of requiring a
manual retune. The gain recovers to 1.0 once the oscillation stops.
GainCompression3d is templated on the enable and minimum gain parameter
IDs so both airframe types share one implementation while keeping their
own MC_GC_* / FW_GC_* parameters. Behaviour is opt-in: MC_GC_EN defaults
to disabled.
The dt clamp in the spectral damper is widened from 1 ms to 0.125 ms
because multicopter rate loops run well above the 1 kHz that the previous
floor assumed, which would otherwise mis-scale the filter coefficients.
Compression is reset while landed so that ground contact vibration does
not compress the gains before takeoff.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Andrea Bernasconi <andrea.bernasconi@auterion.com>
* fix(rate_control): fully restart gain compression on reset
reset() only restored the scalar compression gains, leaving the spectral
damper history and the cached gain vector untouched. A reset therefore did
not really restart at gain 1: the next cycle still multiplied the torque
setpoint by the previously compressed vector, and the stale HPF/LPF state
could immediately recreate compression. Clear the detector history and the
cached gains so that disarming, a non-rotary-wing transition or ground
contact genuinely restart from an uncompressed loop.
Seed the high-pass filter from the first sample after a reset instead of
zeroing the input history. Since the multicopter controller resets on every
cycle while landed, a zeroed history would make the first airborne sample
look like a step edge and inject a spurious spike into the spectral damper
right at takeoff.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Andrea Bernasconi <andrea.bernasconi@auterion.com>
* docs(mc_rate_control): document multicopter gain compression
Add a feature page for gain compression on multicopters, mirroring the
fixed-wing one but covering the multicopter specifics: it is opt-in
(MC_GC_EN defaults to disabled), and compression is reset while disarmed
or landed so ground contact cannot compress the gains before takeoff.
The fixed-wing block diagram is linked rather than duplicated, since it
contains an airspeed scaling stage that does not exist in the multicopter
rate loop.
Also note in the multicopter PID tuning guide that compression should be
off during manual tuning, as it damps out the very oscillation the tuning
process looks for.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Andrea Bernasconi <andrea.bernasconi@auterion.com>
---------
Signed-off-by: Andrea Bernasconi <andrea.bernasconi@auterion.com>
MC_REF_FF does not scale the path a wound-up reference takes to the output,
so the fixture's default gain of 1 was not covering the flown configuration
This keeps the reference heading on the setpoint heading instead of
integrating the commanded yaw rate, which grew the yaw rate setpoint far
past the command whenever the vehicle could not follow it
* feat(commander): name the GNSS reason when position is lost in flight
When GNSS goes bad in flight and takes the local position estimate with
it, the operator got "GNSS data fusion stopped" about two seconds after
the position went, and nothing said why. EKF2 computes which receiver
check failed every cycle and publishes it in
estimator_status.gps_check_fail_flags, and outside of spoofing and
jamming commander only reports those bits while disarmed. A receiver
that stops sending altogether sets no bit at all.
Report it the moment the local position estimate becomes invalid in
flight, when GNSS position was fused within the last ten seconds. A
failing check gives one event carrying every failing check as a
bitfield, a receiver whose last sample is over a second old gives one
event saying so, and boards with room for it also get a statustext. A
vehicle that never fused GNSS gets nothing, since neither says anything
about why its estimate went. What the estimator decides is untouched.
In flight EKF2 raises only the fix, horizontal, vertical and speed
accuracy, spoofing and jamming checks, so any bit set at that moment is a
real cause. Once the position is invalid the existing fusion stopped
event already drops to Info, so the log now reads reason, then
consequence.
Closes#24355
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
* test(commander): cover the GNSS reason reported when position is lost
Eight cases on EstimatorChecks driven directly, with the failsafe flags
kept across cycles as commander keeps them: the failing check is named
when position is lost in flight, with every bit in the event, it is
reported once and not again while the position stays lost, spoofing is
named ahead of an accuracy check, a receiver that stopped sending is
named as such, and nothing is reported when no check fails with the
receiver alive, when GNSS was not in use, when the vehicle has no GNSS
configured, or while disarmed.
Without the change four of them fail.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
* fix(commander): remember the failing GNSS checks and name a silent receiver first
The flags in estimator_status describe only the newest sample, while
EKF2 keeps rejecting samples for at least a second after one failed and
keeps fusing for up to seven, so the check that kept GNSS out can have
passed again by the time the position goes and nothing was reported.
Every check that failed is now remembered until none has failed for the
same ten seconds that bound the recently fused window.
EKF2 runs the checks only on new samples, so a receiver that stopped
keeps the flags of its last sample. The silence is tested first and is
the reason reported.
The statustext could never be sent. Commander runs the checks without a
mavlink log publisher first, that pass consumes the position validity
edge, and the legacy pass that has the publisher sees the flag already
set. The event reaches the ground station and the log on its own, so the
statustext, its text selection and the test hook for it are gone.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
* test(commander): cover a check that cleared before position was lost
Two more cases: the check fails on one sample and passes on the next,
and the position goes a cycle later, which still names that check; and a
receiver that stops after a failing sample is reported as silent rather
than as that check. The statustext assertions are gone with the
statustext.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
* fix(commander): expire each failed GNSS check on its own timer
The shared latch cleared only after ten seconds without any failure, so a check that kept flapping held every earlier failure in the reason, e.g. a one-off spoofing flag or a drift check from the ground before takeoff. Each check now drops out ten seconds after it last failed.
The fail time is the estimator_status timestamp, when EKF2 reported the check, which also lets the test age a report.
Assisted-by: Claude:claude-opus-5-5
---------
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
Co-authored-by: Jacob Dahl <dahl.jakejacob@gmail.com>
It never really made sense to me to have the max cell voltage at this
arbitrary value, just to - presumably - make the estimation better.
I'm tempted to set this to what is "right" and then see what the effects
are.
* fix(gps): keep fixed-base corrections away from a moving-base rover
Since #27097 every u-blox receiver gets the autopilot's fixed-base corrections, on the claim that a UART2 moving-base rover still wants them over its main link. An RTK engine works against one reference station: u-blox's moving-base application note (UBX-19009093, figure 2) sends static-base corrections to the moving base only, so the rover inherits an absolute fix through the baseline, and the ZED-F9P integration manual (UBX-18010802, 3.1.5.4) requires the reference station ID of the observations to match the station message, which a rover fed two bases cannot rely on. ArduPilot's AP_GPS::inject_data skips moving-baseline rovers, and before #27097 this driver skipped the UART2 rover too. A moving-base rover on either UART now takes only its baseline.
* fix(gps): skip the autopilot's corrections for a UART2 static-base rover
GPS_UBX_MODE 5 feeds the rover a static base's RTCM on UART2, yet the driver still injected the autopilot's fixed-base corrections on UART1. Unless both links carry the same base, the rover then sees two reference stations and can settle on either. It now skips injection like the moving-base rovers.
* feat(gnss): add a correction injector that runs on the serial port's work queue
The gps and septentrio drivers each select a corrections source, reassemble frames and write them from their reader thread, which blocks on the UART between epochs, so a correction waits for the read to return: up to one output interval in the septentrio driver. The injector does this once for every driver, woken by the correction topics on the port's work queue. It writes a frame only when the TX buffer has room for all of it, since a cut frame fails its CRC at the receiver; it feeds a receiver one stream, fixed-base corrections or its moving base's baseline, since an RTK engine works against one reference station; and it drops frame formats the receiver does not accept. It opens its own descriptor on the work queue because on NuttX a descriptor belongs to the task that opened it.
* refactor(gps): inject corrections through the gnss correction injector
Corrections were written from the driver's reader thread between reads, so each waited for a read to time out, and the driver carried its own copy of the source selection, framing and TX-space checks. The injector writes them from the port's work queue as they are published. It runs only while receiverReady() holds, as the injection did before, and is stopped before the driver reconfigures the receiver.
Receivers other than u-blox now get RTCM3 only: SPARTN and AssistNow MGA frames are u-blox formats. A UART1 moving-base rover reports its baseline's injection rate in rtcm_injection_rate. The full communication dump (GPS_DUMP_COMM 1) no longer includes injected corrections, which are written outside the driver thread.
* refactor(septentrio): inject corrections through the gnss correction injector
Corrections were written only after receive() returned with a complete SBF block, so each could wait up to one output interval, and they were written in every driver state, baud-rate detection and configuration included, where they share the line with commands. The injector writes them from the port's work queue as they are published, only while the receiver is streaming, and is stopped around a scheduled reset so no correction lands between forcing command input and the reset command.
A moving-base rover now takes only its moving base's stream, not fixed-base corrections as well, since an RTK engine works against one reference station; the fixed-base corrections still reach the moving base. Only RTCM3 is written: SPARTN and AssistNow MGA are u-blox formats. Injected corrections no longer count toward the controller -> receiver data rate or appear in the SEP_DUMP_COMM dump.
* refactor(gnss): give the correction injector the baudrate, not the driver's port
The injector held a reference to the driver's serial port only to read its baudrate, from the work queue while the driver thread owns the port. The baudrate is fixed while the injector runs, so the driver now passes it in the start() config.
* feat(sensors/vehicle_gps_position): publish GNSS heading on its own topic
Dual-antenna heading was only reachable through the blended
vehicle_gps_position, so it carried the position timestamp, followed the
position blending weights and was published at the position rate.
vehicle_gnss_heading carries one selected source at a time, preferring
sensor_gnss_relative (callback driven) and falling back to
sensor_gps.heading for receivers that do not publish it. Per-receiver
delay and the driver heading offset are resolved by device_id; a sample
timestamp within 10 ms of the publish timestamp is treated as unset so
the configured delay applies.
* refactor(ekf2): fuse GNSS heading from its own sample buffer
Heading rode on gnssSample and was only fused when a position sample was
popped: faster heading was dropped by the observation rate limit, its
timestamp was the position timestamp and a position outage blocked the
start. gnssYawSample has its own TimestampedRingBuffer and pop. Non-finite
yaw is rejected at the interface and a NaN heading_offset is treated as
zero instead of reaching the fusion Jacobian. The test simulator feeds
heading as an independent sensor.
* feat(sensors/vehicle_gps_position): per-receiver baseline rotation SENS_GPSn_ROT
The antenna baseline orientation lived in three places depending on the
transport: the serial driver (GPS_YAW_OFFSET), the Septentrio receiver
(SEP_YAW_OFFS, SEP_PITCH_OFFS) and EKF2 for anything arriving with a NaN
offset, in practice CAN receivers (EKF2_GPS_YAW_OFF). A second receiver
with a different baseline had no parameter at all. Drivers now publish
the raw baseline heading and vehicle_gps_position rotates it into the
body frame from the receiver's SENS_GPSn_* slot, the same Rotation enum
the other sensors use, before blending and before publishing
vehicle_gnss_heading. Bumps PX4-GPSDrivers so sensor_gnss_relative
carries a real UTC time and no parse-time sample timestamp, which lets
the relative-position heading be delay-compensated and PPS-aligned.
* refactor(drivers/gps): drop the UBX heading_offset setting
Bumps PX4-GPSDrivers to PX4/PX4-GPSDrivers#241, which removes the
driver-side heading offset now that SENS_GPSn_ROT is the only place the
baseline is rotated.
Assisted-by: Claude:claude-opus-5-5
* refactor(sensors/vehicle_gps_position): remove sensor_gps.heading_offset
With the baseline rotation in SENS_GPSn_ROT, a finite offset only meant
"the driver already rotated this heading, skip SENS_GPSn_ROT". The only
remaining writer was older DroneCAN node firmware through the Fix2 ECEF
hack, which made a CAN node's heading switch between two conventions
depending on whether a RelPosHeading arrived between Fix2 messages: with
a non-zero SENS_GPSn_ROT yaw the heading jumped by that angle. Every
receiver's heading is now the measured baseline and SENS_GPSn_ROT is the
only rotation. MicroStrain external heading aiding, which recovered the
baseline heading from vehicle_gps_position, reads vehicle_gnss_heading.
Assisted-by: Claude:claude-opus-5-5
* fix(mavlink): report the body-frame GNSS heading in GPS_RAW_INT and GPS2_RAW
sensor_gps now carries the measured baseline heading, so for a baseline
not along the body x axis the yaw field was off by the mounting angle.
The body-frame heading is taken from vehicle_gnss_heading when the
streamed receiver is the heading source.
Assisted-by: Claude:claude-opus-5-5
* fix(replay): replay vehicle_gnss_heading to EKF2
ReplayEkf2 publishes only the topics it lists from inside the lockstep
barrier, so EKF2 replay never received the heading.
Assisted-by: Claude:claude-opus-5-5
* feat(parameters): migrate GNSS heading offsets to SENS_GPSn_YAW
GPS_YAW_OFFSET, SEP_YAW_OFFS and EKF2_GPS_YAW_OFF were removed without a
migration, so an upgraded vehicle silently lost its antenna mounting
offset and its GNSS yaw was off by that angle.
Assisted-by: Claude:claude-opus-5-5
* fix(sensors/vehicle_gps_position): build GNSS heading only where it is consumed
The heading selection, baseline rotation and SENS_GPSn_ROT parameters
overflowed diatone_mamba-f405-mk2 flash by 657 bytes. Boards built
without EKF2 GNSS yaw fusion or MicroStrain have no consumer for
vehicle_gnss_heading, so SENSORS_VEHICLE_GNSS_HEADING defaults on only
with one of them. The parameter migration also drops fmodf, since the
old parameters never exceeded one wrap.
Assisted-by: Claude:claude-opus-5-5
* feat(sensors/vehicle_gps_position): carry the heading receiver's jamming and spoofing state
So that EKF2 can gate GNSS yaw on the receiver providing the heading, which is not necessarily the position receiver.
Assisted-by: Claude:claude-fable-5-1
* fix(ekf2): gate GNSS yaw on the heading, not the position checks
Yaw fusion could only start once the position checks passed, and stopped with them. On an ARK G5 outdoors the heading was good from 36 s after power-up and was first fused at 104 s, held back by EPV while Galileo HAS converged; a 0.45 s PDOP failure while the vehicle was handled on the ground stopped yaw for 10 s with its innovations under 5.4 deg.
The heading is its own observation, often from its own receiver: yaw now starts on fresh data, tilt alignment and the heading receiver's spoofing and jamming state under the EKF2_GPS_CHECK bits, and keeps running when position and velocity fusion stop. The hold-off after a yaw fusion failure was a side effect of the position checks resetting; it is now a yaw timer that withholds the heading from a reset for the GNSS health time.
Assisted-by: Claude:claude-fable-5-1
* fix(sensors/vehicle_gps_position): carry an unknown GNSS sample time as 0
A GNSS sample time within 10 ms of the publish time was treated as unset and replaced by SENS_GPSn_DELAY. An ARK G5 over DroneCAN measures 8.3 to 21.5 ms from epoch to receipt, so 0.4% of its headings were re-stamped 110 ms early; at 20 Hz EKF2 dropped them as out of order, at a lower rate they would have been fused 100 ms late. A stamp meaning unset that crossed a busy bus in more than 10 ms would have been trusted instead.
Unknown is now 0 end to end: the serial driver leaves timestamp_sample at 0, the CAN node sends RelPosHeading's timestamp as UNKNOWN for it, and the autopilot's bridge publishes 0 when the node timestamp can't be used. Any other sample time before the publish time is trusted.
Assisted-by: Claude:claude-opus-5-5
* feat(sensors/vehicle_gps_position): derive the GNSS heading baseline from antenna positions
SENS_GPSn_ROT and its Euler angles carried one number: roll never moves a baseline along x, pitch only flagged a vertical one, and the heading model uses the baseline yaw alone. Nothing checked the heading against the installation either, so a receiver that flagged float solutions as valid reset yaw 87 deg wrong at first fix on an ARK G5 moving-base pair.
As in ArduPilot, the baseline now comes from the antenna positions: SENS_GPSn_HDG selects the other receiver's antenna (moving base, from both slots' SENS_GPSn_OFF) or a custom SENS_GPSn_BLX/Y/Z (single dual-antenna receiver), and a heading is used only when the receiver reports a baseline within 20% of it in length and in its vertical component over the sample's attitude. Those float solutions reported 10.9 m and 1.04 m for a 0.347 m baseline. A source is published once it has passed for 1 s: the headings right after the base antenna was re-plugged passed for 0.9 s while up to 25 deg wrong. vehicle_gnss_heading logs the reported baseline length.
A yaw offset can't be turned into a baseline, so GPS_YAW_OFFSET, SEP_YAW_OFFS and EKF2_GPS_YAW_OFF are not migrated; GNSS yaw stays off, with a warning, until SENS_GPSn_HDG is set.
Assisted-by: Claude:claude-opus-5-5
* fix(ekf2): stop GNSS yaw on a spoofed or jammed heading receiver
The heading receiver's spoofing and jamming state only kept yaw fusion from starting; a receiver that reported spoofing in flight kept feeding yaw, where position fusion stops. Its samples are now not fused, the fusion stops once none has been for the reset timeout, and a reset to its heading waits for the GNSS health time, as for position.
Assisted-by: Claude:claude-opus-5-5
* docs(dronecan): configure the ARK G5 heading baseline on the autopilot
The page still configured the heading offset with the module's SEP_OFFS_YAW, and required GPS blending for the heading to be published, which no longer carries it.
Assisted-by: Claude:claude-opus-5-5
* fix(sensors/vehicle_gps_position): drop a mismatched GNSS heading without restarting the settle
Outdoors on an ARK G5 moving-base pair 0.35 m apart, the reported down component of the baseline scattered by up to 0.16 m while the airframe was turned, and 9 cm at rest, against the 7 cm the vertical check allows. The rejected headings were as good as the accepted ones, but every rejection restarted the 1 s settle, so during 720 deg turns a heading was published for 4 to 62% of the time.
A sample that fails the baseline checks is now only dropped; the settle restarts when the receiver reports no heading. Replayed on that log, 85 to 99% of the turns get a heading.
Assisted-by: Claude:claude-opus-5-5
* fix(sensors/vehicle_gps_position): drop the GNSS heading baseline's vertical check
The check compared the reported down component of the baseline with the configured baseline rotated by the vehicle attitude, within 20% of the baseline length. On an ARK G5 moving-base pair 0.35 m apart the down component scattered by up to 9 cm at rest and 0.16 m while the airframe was turned, so it rejected good headings: across seven outdoor logs it rejected 184 samples under 5 deg of error and 6 over 10 deg, all of which the EKF innovation gate would handle, and nothing the length check missed on a receiver that withholds float headings.
The length check and the near-vertical check on the reported baseline stay. The sensors module no longer reads the vehicle attitude to check the heading.
* feat(sensors/vehicle_gps_position): SENS_GNSSn_HDG with the auxiliary antenna position
The heading parameters are new, so they take the GNSS name the rest of the SENS_GPS family moves to rather than being renamed again shortly. The setup names what the receiver is: the rover of a moving base pair, or a receiver with two antennas. For the latter the second antenna is given by its position, like the main one's SENS_GPSn_OFF, so every setup derives the baseline from antenna positions.
Assisted-by: Claude:claude-opus-5-5
* feat(drivers/septentrio): publish the heading baseline on sensor_gnss_relative
The heading only reached PX4 through sensor_gps, without the baseline the receiver measured, so the sensors module could not check it against the configured one and a heading from float ambiguities, confident on a baseline metres off, was used. AuxAntPositions (dual antenna) or BaseVectorGeod (moving base rover) now go out with the heading of the same epoch on sensor_gnss_relative at EndOfAtt, the heading valid only with fixed ambiguities. Ported from ARK's driver, where it was tested outdoors on mosaic-G5 P3H, P6 and P8.
A receiver that rejects the extra blocks falls back to the previous output without a heading. The heading accuracy is the square root of the reported variance; it was the deg^2 variance scaled as if it were degrees.
Assisted-by: Claude:claude-opus-5-5
* refactor(sensors/vehicle_gps_position): take GNSS heading only from sensor_gnss_relative
The sensor_gps fallback carried no reported baseline, so its headings skipped the length check that rejects a wrong ambiguity fix. Every receiver that matters now reports on sensor_gnss_relative: u-blox, Septentrio, the Unicore UM982 (PX4/PX4-GPSDrivers bump) and DroneCAN RelPosHeading. The heading fields of sensor_gps go, with the paths that filled them: NMEA HDT, Trimble MB-Two, Femtomes, SBG and MicroStrain, the heading in DroneCAN Fix2 on both the node and the autopilot, and the RelPosHeading copy into sensor_gps.
Assisted-by: Claude:claude-opus-5-5
* docs(gps_compass): configure GNSS heading with SENS_GNSSn_HDG
The heading parameters were renamed and the heading now only comes from receivers that report their baseline, so the Trimble MB-Two and Femtomes pages no longer describe a heading setup. The antenna spacing is the recommended 30 cm rather than the 5 cm the code rejects below.
Assisted-by: Claude:claude-opus-5-5
* chore(drivers/gps): point PX4-GPSDrivers at the merged PX4/PX4-GPSDrivers#242
The submodule tracked the PR's branch commit; the squash-merged commit has the same tree.
Assisted-by: Claude:claude-opus-5-5
* docs(params): shorten verbose descriptions of commonly used parameters
Descriptions ship xz-compressed in ROMFS on boards without
CONSTRAINED_FLASH, and these carried restatements of the short
description, option lists already in the enum/bitmask labels, and
boilerplate. Every fact is kept; the text is 54% shorter.
Assisted-by: Claude:claude-opus-5-5
* Apply batched suggestions from code review
Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
---------
Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
The workflow triggers on pull requests that touch a board defconfig or
the USB ID tooling, and checks only the defconfigs the PR adds or
modifies. The file list comes from diffing the checked-out PR merge
commit against its base parent, so no API call or token is needed. A PR that changes the tooling itself is checked against every
board, so a checker change is proven on the real tree and not only by
its unit tests. A test keeps the tooling list and the workflow paths
filter in sync.
A manual run checks every board. There is no push or scheduled run: the
registry repo checks its own changes against PX4 main, and board
changes reach main only through PRs.
The mypy and flake8 step for the checker and runner lives in
python_checks.yml with the other Python tooling checks.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Boards that ship the Dronecode USB vendor ID 0x3643 must use a product
ID allocated in the Dronecode/usb-ids registry, and nothing in the tree
enforces that. A board reusing another board's PID, or picking an
unallocated one, becomes ambiguous to host tools that identify boards
by VID/PID.
The contract is the registry mapping: a defconfig under
boards/<vendor>/<board>/ using VID 0x3643 must use a registered PID
whose px4_board is exactly <vendor>/<board>. The USB vendor string is
deliberately not checked, since the registry does not govern it. Boards
on other vendor IDs are ignored.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
cond_timedwait() re-waited on a 10 ms wall-clock timeout until either a
signal or its virtual timeout arrived. A signal from the caller's own
signaler (px4_sem_post, or the unit test's broadcast) that lands while
the waiter is between that wall-clock timeout and re-acquiring the mutex
finds no waiter and is lost, so the waiter sleeps until its virtual
timeout. In lockstep_scheduler_test nothing else advances time and the
test hangs; in SITL a posted semaphore can report ETIMEDOUT late.
The loop is not needed: the caller holds the mutex until
pthread_cond_wait() releases it and set_absolute_time() takes that
mutex before broadcasting, so a timeout broadcast cannot be missed. The
original signal loss came from the MAX_WAKEUPS cap that the signal_next
list already removed. Go back to a single wait and keep the three-phase
signaling.
Refs #28862
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
The test starts the hrt worker without hrt_init(), so the callout lock
stays a default non-recursive mutex and the worker deadlocks on its own
nested lock in hrt_tim_isr() -> hrt_call_invoke() the first time a tick
fires. The selector's 10 ms backup reschedule normally keeps pushing the
tick out, so it only fires when Run() pauses under load or in the slower
Coverage build, where it hangs the test until the ctest timeout.
VehicleAirDataTest and VehicleMagnetometerTest already do this.
Refs #28862
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Every px4_add_git_submodule call ran check_submodules.sh twice, at
configure and build time, and with CI=true force-updated each submodule
every time, a 2017 workaround for an interactive prompt that is gone.
VS Code and CLion force-updated too, resetting submodule changes under
development.
The check now runs once per configure. A missing submodule is fetched;
one at another commit is kept as it is, with a warning locally and an
error in CI.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Some boards heartbeat from more than one component on the same sysid. An
FMUv6X-RT on v1.17 sends HEARTBEAT from compid 1 (PX4) and from compid 236
with autopilot=MAV_AUTOPILOT_INVALID. connect() accepted the first
heartbeat from anyone and left target_component at pymavlink's default of
0, so param and mission requests went out as broadcast, the other
component could answer, and param_stress mixed its PARAM_VALUE stream
(index, count, values) into the autopilot's download.
connect() now waits for a heartbeat whose autopilot is not
MAV_AUTOPILOT_INVALID and pins target_system/target_component to its
source. pymavlink only latches target_system once and never sets the
component, so the pin holds for the life of the connection. The
wait_heartbeat() helper shares that filter and, once pinned, only counts
the pinned component; wait_reconnect() goes through connect() and gets
the same behavior after a reboot.
PARAM_VALUE replies in px4bench.params, param_stress and link_forwarding
are filtered to the pinned component, and flight_mission uses the pinned
component for MAVFTP and the armed/disarmed heartbeat checks instead of a
hardcoded 1.
Diagnosed by @farhangnaderi in #27852.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
The 1.17 run in #27840 read 'SDLOG_UTC_OFFSET not found in downloaded
set' as the parameter not existing on that release. It does exist there;
the download itself was the anomaly (19 params total, where a booted
board reports hundreds, since the autopilot lists used params only).
Name the downloaded count and that semantics in the failure so the next
tiny param set is diagnosed as a degraded session, not a missing param.
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
log_transfer crashed with a raw ImportError on setups whose pymavlink
predates the mavftp module (#27840). mavftp first shipped in pymavlink
2.4.42, so raise the floor in pyproject.toml, Tools/setup/requirements.txt
and the README, and guard the import in px4bench.ftp so an old install
reports a clear upgrade hint instead of a traceback. Both mavftp
consumers now import it through the guard.
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Running the suite from main against a release/1.17 FMUv6X-RT board
(#27840) failed boot_health and flight_mission at 'shell open' while
reboot_loop passed the same check 10/10 in the same run. The failing
opens were the sessions immediately following a closed one, so the
single wake write was being consumed before the firmware nsh pipes were
reading and no amount of waiting could recover it. Re-send the wake
write every second until the deadline, strip stale BENCHOPEN wake lines
from subsequent command output, and replace the 5s hardcoded at every
call site with one px4bench.SHELL_OPEN_TIMEOUT (10s) so the budget is
tunable in one place.
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Velocity setpoints are not ignored on fixed-wing. When combined with a
position setpoint, FixedWingModeManager selects FW_POSCTRL_MODE_AUTO_PATH
and follows a path through the position tangent to the horizontal
velocity direction, with the acceleration component normal to the
velocity used as path curvature.
Velocity-only setpoints are not supported: offboard without a position
setpoint clears flag_control_position_enabled, so set_control_mode_current
falls through to FW_POSCTRL_MODE_OTHER and no new lateral/longitudinal
setpoints are published. Since MAV_FRAME_BODY_NED always discards the
position, it cannot be used for fixed-wing.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
BODY_NED position setpoints have never been supported: the receiver
rotates velocity and acceleration into the local frame and sets
position to NAN, and for fixed-wing (position setpoints only) the
frame cannot be used at all. The docs listed input combinations and
coordinate frames as independent lists, implying every combination
works in every frame, which misled users into filing #27617.
Also update stale code links still pointing at FlightTaskOffboard.cpp,
removed in the v1.12 offboard rework (#16739).
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
The replay csv generator drifted away from both the current message
definitions and the C++ loader that consumes its output.
The range finder reused the 'quality' column name of the optical flow, so
pandas merged the two columns and the distance was appended behind the
quality. SensorSimulator::setSingleReplaySample() reads the distance first,
so any log containing both sensors produced swapped range samples. Give the
range finder its own column names.
getGpsData() still read the removed alt/lon/lat fields of
vehicle_gps_position, so every GPS sample was silently dropped. Read
altitude_msl_m/latitude_deg/longitude_deg instead, emit them in the
altitude, latitude, longitude order the loader expects, and scale the
degrees by 1e7 to compensate for the 1e-7 rescale the loader still applies.
getVioData() read the removed x/y/z and vx/vy/vz fields, use position[] and
velocity[].
The bare except clauses hid all of this behind "X data not detected", so
print the exception too. Missing topics are still skipped, not fatal.
Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
`make -j clang-ci` configured px4_sitl_default-clang and
px4_sitl_default-clang-test at the same time, and both configures fetch
the same submodules. About 1 in 200 Static Analysis runs failed with
`index.lock: File exists` or a half-cloned libevents.
The workflow now runs the configure-only test dir first, which fetches
the submodules, then builds the clang dir, which finds them present.
clang-ci had no other user, so it is removed.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
CONFIG_BOARD_UAVCAN_PERIPHERALS made ROMFS clone the PX4 repository and
run a second full build of a CAN node on every build of the flight
controller, to carry that node's firmware in /etc/uavcan/fw/. Its only
user, px4_fmu-v5_uavcanv0periph (CUAV CAN GPS v1), disabled about 30
modules to make room, and was downloaded 40 times in five years, never
through QGroundControl. Releases now publish every CAN node's firmware
standalone as <target>.uavcan.bin, which the DroneCAN firmware server
installs from the SD card on any flight controller.
Remove the option, the ROMFS ExternalProject and the board variant, and
point the DroneCAN docs and the release notes at the SD card path. The
server still serves /etc/uavcan/fw/, which is now empty.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Each build job compiled its group one target at a time, and the serial
parts of every build (configure, the NuttX make, linking) left the 4-vCPU
runners about half idle. Two concurrent builds keep them around 80% busy
and cut total build-step time about 24% and runner cost about 17%.
Larger runners and more slots were measured and cost more than they save.
build_all_runner.py replaces build_all_runner.sh. NuttX compiles inside
its source tree, so each extra slot is a git worktree under .build_slots/.
All submodules are fetched once before building, each slot clones them
locally from the checkout, and builds run with GIT_SUBMODULES_ARE_EVIL=1
so CMake's per-configure submodule check does not sync and update them
concurrently on the shared .git/config. Targets that write the same
build/ directory (<board>_deb with <board>_default, the metadata targets
with px4_sitl_default) always share a slot, and the runner refuses a plan
that would split them. Every target is built even after a failure. Build
output streams with a [seconds|slot target] prefix, each target prints its
memory usage or, on failure, the excerpt from the first error, and a Build
Logs step prints each target's full log as one collapsed group. Slot build
directories are moved into build/, so packaging is unchanged.
The matrix now carries targets as a list plus a slot count from
build_all_config.yml; the comma-joined string existed only because
workflow expressions could not pass an array to a shell command, which
join() now does. The scan job runs new tests for the generated matrix and
the slot assignment.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
gencromfs writes its intermediate data to fixed file names in /tmp
(/tmp/gencromfs-000000, ...) because its mkstemp path is compiled out, so
two NuttX builds generating a ROMFS at the same moment overwrite each
other's temporary files and emit a corrupted nsh_romfsimg.c ("/*" within
comment, invalid suffix "x00"). Run gencromfs under flock where it is
available; it takes about a second, so the lock costs nothing.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
The comment reports a patch percentage and a project total that is mostly
SITL coverage carried forward from main, but not which lines are uncovered,
so it adds noise to every PR without telling the author what to test. The
informational patch and project statuses stay and link to the line-level
report.
Assisted-by: Claude:claude-opus-5-5
The pr and commit skills claimed every PR is squash-merged, which
contradicts CONTRIBUTING.md: merge strategy is case by case, with both
squash and rebase merges enabled. The wrong claim led to fix-up commits
being kept on branches on the assumption they would be squashed away.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
* ci(codecov): upload unit test coverage and limit SITL coverage to main
Coverage on main came from a single standard_vtol SITL flight, unit tests
were never uploaded, and the Coverage build never hit ccache because
ccache 4.9.1 (Ubuntu 24.04) rejects -fprofile-update=atomic. The flag only
keeps hit counts exact under thread races; line coverage is unaffected and
lcov already ignores the resulting negative counts.
Unit tests now build with coverage and upload as the unittests flag on
every PR and push to main. standard_vtol builds as Coverage on main only
and uploads as sitl-vtol, carried forward on PRs. codecov.yml sets the
default branch, informational project/patch statuses and ignores.
Per-build-type ccache keys stop the Coverage and RelWithDebInfo jobs from
overwriting each other's cache.
Fixes#28862
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
* ci(codecov): run unit test coverage as its own job
The Coverage build shifts EKF replay float output, so running it in the
Unit Tests job made the change indication step auto-commit new baselines
to the PR branch. Unit Tests goes back to the plain build and a separate
job produces the unittests upload. disable_search keeps the Codecov
uploader to the lcov report instead of running gcov over the build tree.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
---------
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Publishing px4-dev-ros2-gazebo fails because its SPDX attestation exceeds
BuildKit 0.32's 40 MiB limit. Pin the builder to v0.33.0 (80 MiB), matching
the fix already applied to build_deb_package.yml in 61907962c9.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ramon Roche <mrpollo@gmail.com>