51179 Commits

Author SHA1 Message Date
Saibernard
0d2c7058b3 fix(tests): build and run the unit test suite on macOS (#28516)
* fix(tests): use snprintf in test_uart_send

The macOS SDK marks sprintf deprecated, and PX4 builds with -Werror
-Wfatal-errors, so this is a hard build failure on macOS before any
test binary is produced:

  test_uart_send.c:83:7: fatal error: 'sprintf' is deprecated

The return value is used as the write() length and snprintf() returns
the length it would have written rather than what fit, so clamp it to
the buffer.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(navigator): give publish_global_pos a float altitude parameter

The test helper declared its altitude parameter double, but it assigns
to vehicle_global_position_s.alt, which is float32, and all eight call
sites pass a float. Every call therefore promoted a float to double and
converted it straight back, which Apple Clang reports as
-Wdouble-promotion and -Werror turns into a build failure.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(tests): use a double NaN for double latitude and longitude fields

The NAN macro is a float, so using it for a double lat/lon field is an
implicit float to double promotion. Apple Clang reports it under
-Wdouble-promotion and -Werror makes it fatal, so these tests could not
be compiled on macOS. Same class of fix as 25596.

Only genuinely double-typed fields are changed: altitude and the other
float members deliberately keep the plain NAN.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(cmake): link the functional gtest stubs with -force_load on macOS

px4_add_functional_gtest() pulls in the daemon app-map stubs with GNU
ld's --whole-archive. Apple's ld64 has no such option and rejects the
link outright:

  ld: unknown options: --whole-archive --no-whole-archive

so every functional gtest failed to link on macOS. ld64 spells the same
thing -force_load <archive>.

test_stubs stays in the link library list on that path: a $<TARGET_FILE:>
inside a link flag does not make the executable depend on the archive, so
editing a stub would rebuild libtest_stubs.a without relinking the tests
that use it.

This affects the 40 px4_add_functional_gtest definitions across the tree.
Non-Apple platforms keep the existing flags unchanged.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(cdev): take the px4_poll deadline from the semaphore's clock

macOS has no pthread_condattr_setclock(), so px4_sem_init() leaves the
condition variable behind px4_sem_t on its default CLOCK_REALTIME, while
px4_poll() built its absolute deadline from CLOCK_MONOTONIC.

On Darwin CLOCK_MONOTONIC counts from boot and CLOCK_REALTIME is the
epoch, so the deadline lands decades in the past: measured on one
machine the two clocks were about 56 years apart. pthread_cond_timedwait()
therefore returned ETIMEDOUT immediately and every px4_poll() with a
positive timeout returned without waiting. The teardown pass then
overwrote the result, so callers just saw px4_poll() == 0.

Take the deadline from CLOCK_REALTIME on Darwin builds without the
lockstep scheduler, which is the clock the wait actually uses. Lockstep
builds provide their own consistent simulated time and are untouched, as
are all other platforms; px4_poll() is the only POSIX caller of
px4_sem_timedwait().

Fixes the uORB poll timeouts in functional-test_mission_route_cache and
sitl-uorb_tests on macOS.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* test(pure_pursuit): compare bearings across the +-pi branch cut

Two cases asserted an exact -M_PI_F. The bearing there is 180 degrees,
which wrap_pi()'s [-pi, pi) range can legitimately represent as either
end, and on macOS the arithmetic lands 2 ULP below +pi. PX4 also builds
with -fno-signed-zeros, so the sign of an atan2f() result at the cut is
not portable in the first place.

Compare the wrapped difference instead, which treats +pi and -pi as the
same bearing, and give it a tolerance that survives the subtraction.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(gps_blending): accumulate the blended timestamp without truncating

Each weighted timestamp was cast to uint64_t before being added, so
every contributing receiver's term lost its fractional microsecond and
the blended timestamp came out low by up to one microsecond per
receiver. The normalised weights are floats, so their rounding error
biased the result further: two receivers reporting an identical
timestamp did not blend back to that timestamp.

Accumulate in double, divide by the accumulated weight, and round once.
The truncation is platform independent; gps_blending_test caught it on
macOS only because the float weights round differently there.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

---------

Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
Co-authored-by: Farhang <46557204+farhangnaderi@users.noreply.github.com>
2026-09-08 22:25:45 -06:00
Alex Klimaj
d05e3a4a26 feat(boards/ark_fpv): add FMUID 1 LSM6DSV32X (#28512)
* feat(boards/ark_fpv): add FMUID 1 LSM6DSV32X

Rev 1 increments FMUID and replaces the IIM-42653 with an LSM6DSV32X
on the same SPI1 pads. Configure the 32X at ±32 g / ±4000 dps.

Assisted-by: Grok:grok-4.6
Signed-off-by: alexklimaj <alex@arkelectron.com>

* feat(drivers/lsm6dsv): run 32X at 7.68 kHz with PX4-owned filtering

Match IIM-42653-style raw data: HAODR 7680 Hz, disable accel LPF2,
10 MHz SPI (device max), and exact FS/32768 accel plus 140 mdps/LSB
gyro scales. Periodically re-check config registers like the TDK
drivers.

Assisted-by: Grok:grok-4.6
Signed-off-by: alexklimaj <alex@arkelectron.com>

* fix(drivers/lsm6dsv): keep the accel LPF2 at ODR/10

PX4 has no anti-alias stage of its own: the FIFO is decimated to the
publish rate with a plain mean and the software cutoffs run after that,
so with LPF2 off the accel is flat to ODR/2 (3840 Hz on the 32X) and
that content folds into the published stream. ODR/10 is the nearest
match to the 585 Hz AAF the Invensense drivers use, and to what the
rev 0 ARK FPV IIM-42653 flies with. The gyro is unchanged: its LPF2 is
fixed by ODR at 537 Hz.

* fix(drivers/lsm6dsv): validate complete register fields

Runtime checks must detect changes to zero-valued rate, range, and filter bits. Include register faults in the published IMU error counts so health reporting retains the fault history after recovery.

Assisted-by: Codex:gpt-6
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

---------

Signed-off-by: alexklimaj <alex@arkelectron.com>
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-08 22:02:09 -06:00
Jacob Dahl
c42695cfb0 refactor(mavlink): use tables for default stream configuration (#28631)
Repeated stream configuration calls duplicate argument setup in flash.
Store the defaults as constant data while preserving their order and
single-stream lookup behavior.

Extracted from PX4/PX4-Autopilot#27816.


Assisted-by: Codex

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Balduin <balduin@auterion.com>
2026-09-08 21:20:19 -06:00
Jacob Dahl
b8ba996bcf fix(mavlink): make packed parameter transfers reliable (#28630)
* fix(mavlink): make packed parameter transfers reliable

Recover pipelined uploads after packet loss without hiding failed parameter writes. Preserve correct defaults while values change during download, and avoid invalid float-to-integer conversions.

Assisted-by: Codex:GPT-6

* refactor(mavlink): allocate upload reorder blocks with new

malloc plus placement new was unlike the rest of the FTP server, which
uses nothrow new with a null check. Document why the reorder heap is
bounded only by the file length: mavftp retries a lost chunk after
sending every remaining one, the same bound ArduPilot's buffer has.
2026-09-08 21:20:10 -06:00
Jacob Dahl
6236d4a855 build(uxrce_dds_client): inherit firmware optimization flags (#28629)
The module's custom -O2 override defeats the default -Os on NuttX.
Use the selected build configuration to recover flash space.

Extracted from Balduin's optimization in PX4/PX4-Autopilot#27816.

Assisted-by: Codex

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Balduin <balduin@auterion.com>
2026-09-08 21:20:00 -06:00
Jacob Dahl
0658d87271 feat(uavcan): correct the bus clock rate from time sync and share the clock across drivers (#28572)
* feat(uavcan): correct the bus clock rate from time sync and share the clock across drivers

Stepping only the offset once a second leaves the bus clock a full second
of crystal drift behind between syncs, 8 us on an ARK X20 against an
ARKV6S where the sync measurement itself scatters by 1.5 us. Modelling
UTC as HRT plus an offset and a rate, and integrating half the rate error
each sync, brings the residual down to that noise. The same clock served
the stm32, stm32h7 and kinetis drivers as three copies; it now lives once.
`uavcannode status` reports the adjustment count, last step and rate.

* fix(uavcannode): guard HRT sync diagnostics by driver

SocketCAN uses a separate clock without the HRT sync status API, so the unconditional diagnostic broke NXP node builds.

Assisted-by: Codex
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

---------

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-08 20:43:37 -06:00
Jacob Dahl
ebc4d50a9e fix(mavlink): identify files in FTP open failure logs (#28627)
Failed FTP opens need the requested path and access mode to identify the file behind the error.

Assisted-by: Codex

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-08 20:25:55 -06:00
PX4BuildBot
7b9b22d080 docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <bot@px4.io>
2026-09-09 02:23:46 +00:00
Jacob Dahl
7cfa8d1d40 docs(gps): UART2 cannot update u-blox firmware (#28593)
u-blox does not support firmware upgrade on UART2. GPS_UBX_MODE 7 is a diagnostic port only.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-08 20:17:13 -06:00
Jacob Dahl
dcfce4343a feat(mavlink): serve and accept packed parameters as @PARAM/param.pck (#28435)
* feat(mavlink): serve packed parameters as @PARAM/param.pck

QGC already downloads ArduPilot parameters as a packed FTP file, which
is much more reliable on lossy SiK links than the PARAM_VALUE firehose.
Serve the same format from PX4 so a hash miss can use that path.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(mavlink): serialize the full message when resending a cached FTP reply

_last_reply caches only the header, PayloadHeader and four data bytes,
but the resend path handed that 19-byte array to the serializer as a
complete mavlink_file_transfer_protocol_t, which reads 254 bytes and
transmits whatever follows the cache. The decoded request is dead once
the resend decision is made, so expand the cache into it instead of
adding another message to the receiver thread's stack. Clear the unused
payload before caching so a resent NAK matches the original.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* test(mavsdk_tests): download parameters via MAVLink FTP

Fetch @PARAM/param.pck through MAVSDK's burst download on SIH and check
every entry against the PARAM_VALUE stream, so the packed format and the
FTP server's session, size and EOF handling stay covered by the SITL
tests that QGC and MAVSDK both rely on.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(mavlink): cache FTP Open ACKs whose size LSB is 5

_reply skipped the compact cache whenever data[0] was kErrNoSessionsAvailable. On a NAK that is the error code; on an Open ACK it is the file size LSB. A lost Open ACK for param.pck of that length was retried as a second Open and failed.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* feat(tools): download packed parameters over MAVFTP

Host-side check of @PARAM/param.pck against the PARAM_VALUE stream.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(mavlink): pad so a defaulted value does not straddle an FTP block

pack() kept the last 4 bytes of an entry in one chunk. With withdefaults those bytes are the default, so the value can still split. QGC always requests defaults; a hole-fill then retries one chunk with a live param_get().

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(mavlink): snapshot param.pck at open so a param_set cannot corrupt it

Pack the used set once into a heap buffer. Reads memcpy that snapshot, so a value change during the download cannot shift later entries or splice two generations across a retried FTP block.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(mavlink): freeze param.pck membership instead of snapshotting the file

Keep which parameters appear and whether each includes a default in bitsets (~1 KB). Pack values live. Layout cannot shift mid-download; a retried block cannot splice a number.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* style(mavlink): format MavlinkFtpParamTest.cpp

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* refactor(mavlink): keep FTP work buffers as members

@PARAM/param.pck is a connect-time path, not the rare path the lazy new[] was for. send() could free the buffers after 2 s of no request while a burst was still running. Fold them into the instance (495 B) and always idle-close the session at 30 s.

A new Open takes over the single session so a lost Terminate on a slow link does not NAK until that close.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* fix(tools): make mavftp_param.py reliable on a radio link

pymavlink applied FTP timings after ResetSessions, wrote param.pck?withdefaults=1 into cwd, and bumped the session id on Open retry (PX4 only ACKs session 0). Set radio timings at construction, pin session 0, retry a missed Open, and treat a complete decode as success.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

* feat(mavlink): accept packed parameter FTP uploads

CreateFile/Write/Terminate of @PARAM/param.pck applies the ArduPilot
packed stream so a GCS can bulk-load without a PARAM_SET round trip per
value. Unknown and read-only names are skipped; Terminate NAKs a
truncated file or the with-defaults magic.

* test(mavsdk_tests): upload MPC_XY_P via packed param FTP

Pin the SIH upload case to a known float instead of whatever
get_all_params() returns first.

---------

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-08 20:16:14 -06:00
Jacob Dahl
69e8bf05e3 feat(ark_x20-gps): capture the ZED-X20P timepulse for Fix2 timestamps (#28626)
The node's Fix2 publisher only fills the message timestamp while pps_capture is running, since that is what puts the send time and the receiver's gnss_timestamp on the same clock. Without it the flight controller's GNSS bridge cannot recover the navigation epoch and falls back to receive time. TIMEPULSE is on PB2, which has no timer alternate function on the F412, so the capture uses EXTI like can-rtk-gps.
2026-09-08 20:14:27 -06:00
Jacob Dahl
b8d5d2bc58 fix(optical_flow): normalize PixArt SQUAL onto the 0-255 quality contract (#28625)
sensor_optical_flow.quality is defined as 0 = worst, 255 = best, and EKF2 scales flow noise linearly with it, but the PAA3905 and PAW3902 drivers passed the raw SQUAL register through. Raw SQUAL is mode dependent (the datasheet noise floor is 0x19/0x46/0x55 for bright, low light and super low light), and the DroneCAN flow message carries no mode, so a consumer could not tell a solid bright-mode reading from one count above the super-low-light floor. Each mode's floor now maps to 0 and raw 255 stays 255; the false-motion discard, which also needs the shutter condition, shares the same constants.
2026-09-08 20:14:18 -06:00
Jacob Dahl
4af7b4e77e fix(uavcan): publish UNKNOWN node timestamps until time-synced and map them through the bus time base on the FC (#28176)
* fix(uavcannode): publish UNKNOWN until time-synced and fix RawIMU integration_interval units

The RawIMU and RangeSensor publishers computed the bus timestamp as
getUtcTime() - (hrt_now - sample_hrt). getUtcTime() is 0 until the time-sync
master disciplines the node's clock, so the unsigned subtraction underflowed and
the truncated uint56 field went out as ~7.2e16 us. Publish UNKNOWN until the
clock is disciplined, from one shared helper.

RawIMU.integration_interval is specified in seconds but was assigned
vehicle_imu.delta_angle_dt, which is in microseconds.

* fix(uavcan): map node timestamps through the bus time base in the sensor bridges

The accel, gyro and rangefinder bridges took msg.timestamp.usec verbatim as
timestamp_sample whenever it was non-zero, which assumes the bus shared time
base is HRT. It is not: the FC seeds its bus clock from HRT only after its own
init has run, so the base leads HRT by that duration, and the FC may also be a
slave to a lower node ID master. An undisciplined node, or one stamping a
foreign epoch, put the sample time arbitrarily far from HRT.

The CAN ISR stamps every received transfer in the same bus base the node used,
so the sample age is the difference between the two. Bound that age to a
plausible transport window and subtract it from the receive time, falling back
to the receive time otherwise, in one shared helper.

* fix(uavcan): stamp RelPosHeading and BatteryInfoAux in the bus base, map RelPosHeading on the FC

Both node publishers put node-local time into a bus-base field, and the
FC took the RelPosHeading stamp verbatim as HRT. Same defect as RawIMU
and RangeSensorMeasurement, same two helpers.

* fix(uavcan): read the bus clock and HRT together when mapping node timestamps

The ISR receive stamp is taken at the first frame of a transfer while HRT was
read in the callback, so the transfer and scheduling latency stayed in
timestamp_sample as jitter. Sampling both clocks at the same instant cancels
the bus-to-HRT offset exactly. The node-side helper takes the node for the same
reason.

The FC bus base leads HRT by the free-running timer phase at seeding, not by
the init duration; correct the comment.

* fix(uavcan): send true point samples in RawIMU and consume the integral on the FC

RawIMU.*_latest carried the window mean, delta / integration_interval,
under a field the DSDL defines as the latest point sample. The mean lags
the message timestamp by half the interval, 2.5 ms at IMU_INTEG_RATE 200,
and the FC published it as an instantaneous rate at the window end.

The node now fills *_latest from the sensor_gyro / sensor_accel instance
behind vehicle_imu, the same batch-end sample every PX4 IMU driver
produces, and keeps the integrals and timestamp as they were.

The FC bridges now consume the integral when one is present: divided by
its interval it is float32 rather than float16, anti-aliased against the
node's full-rate stream, and successive means spaced one interval apart
integrate back to the node's exact delta angle in VehicleIMU. It is
stamped at the window centroid. The point sample is used only when the
node reports no integral, as the DSDL specifies.
2026-09-08 20:14:07 -06:00
Jacob Dahl
b8023ac8f9 feat(invensense): board-selectable anti-alias bandwidth for CAN flow nodes (#28624)
* feat(invensense): board-selectable anti-alias bandwidth, 250 Hz on ARK flow nodes

The ICM-42688-P / IIM-4265x drivers hard-code the chip defaults (AAF 585 Hz, UI filter 1st order at ODR/2), which suit a flight controller decimating 8 kHz to a 1-2 kHz rate loop. A CAN flow node integrates the same stream to 1 kHz for the flow gyro compensation and 200 Hz for RawIMU, so everything between 500 Hz and the AAF knee aliases into what it sends. A flight on an ARK Flow MR showed the node pitch gyro at 1 rad/s RMS against 0.2 on the FC with an accel vibration metric 5x higher.

`-B <hz>` selects the closest anti-alias filter row from the datasheet table and a 3rd-order UI filter at the closest ODR/N; ARK Flow and Flow MR start their IMU with `-B 250`. EKF2_OF_DELAY drops from 20 ms to 7 ms, the transport delay measured for a DroneCAN flow frame after its integration window closes.

* fix(invensense): use size_t for the UI divisor loop index

clang-tidy bugprone-too-small-loop-variable: the index was uint8_t against a sizeof bound.

* refactor(invensense): replace the anti-alias table with three fixed presets

A 63-row table plus nearest-match search is more than the option needs. `-B` now takes exactly 126, 258 or 394 Hz, each row carrying its UI filter setting, and rejects anything else. ARK Flow and Flow MR use 258. The icm42688p line on ARK Flow had been missed.
2026-09-08 20:13:50 -06:00
Jacob Dahl
3f6fe0e40c refactor(uavcan): base the libuavcan clock on HRT instead of a dedicated timer (#28569)
* refactor(uavcan): base the libuavcan clock on HRT instead of a dedicated timer

The stm32, stm32h7 and kinetis drivers each ran a 16-bit hardware timer
with a software overflow extension, a second copy of what HRT provides,
and slewed UTC by patching that accumulator from the timer interrupt.
Every CAN board paid a general-purpose timer for it, and the bus time
base led HRT by the timer phase at seeding even when the FC is the sync
master.

Monotonic time is now hrt_absolute_time() and UTC is HRT plus an offset
that time sync moves. Each adjustment is applied directly; the rate PID
and its lock accessors had no callers, and at the sub-100 us errors sync
produces after convergence a 1 Hz offset correction bounds the error to
crystal drift over one second. The timer number plumbing and the
BOARD_UAVCAN_TIMER_OVERRIDE option go with it.

* refactor(uavcan): drop the vestigial clock::init and the timer enables it left behind

With the hardware timer gone, init() only set a flag, so the flag is set
where the singleton is constructed. The three boards that enabled their
override timer in defconfig alongside the override no longer need it.
2026-09-08 20:12:29 -06:00
Jacob Dahl
bb75224d6d feat(afbrs50): static rate/DFM profiles, pipeline hardening, range offset calibration (#28454)
* feat(afbrs50): static rate/DFM profiles, pipeline hardening, range offset calibration

The distance-based short/long range switching only re-evaluates after a
valid measurement, so once the target leaves the range the driver latches
short-range mode and the sensor stays blind; frames that fail evaluation
were never published, leaving consumers on the stale last value. Flight
characterization of the LV85D and LX85D showed frame rate is the dominant
range knob and DFM 4X beats the mode-default 8X on validity, spread and
wrong-window returns at every rate, so the switching is replaced by
SENS_AFBR_RATE / SENS_AFBR_DFM / SENS_AFBR_PROF with per-module defaults,
SENS_AFBR_MODE gains Auto (module default) with fallback when the API
rejects a mode, and the rate is clamped to the API's 5 Hz frame-time floor
that previously put CONFIGURE in a silent retry loop.

Invalid or quality-gated (SENS_AFBR_QMIN) frames now publish
max_distance + 1 with quality 0 so uavcannode emits TOO_FAR, and the max
distance is bounded by the configured unambiguous range.

Pipeline fixes: measurementReadyCallback dereferenced g_dev after its null
check, stop() deleted the object while a callback could still arrive, the
DRDY interrupt stayed bound to the destroyed handle, S2PI_Abort left the
bus BUSY forever, S2PI_Init leaked on restart, setRateAndDfm spun
unbounded, and a lost completion callback or wedged device stalled the
state machine for good. Non-OK result codes are counted by name for
'afbrs50 status'.

'afbrs50 cal' runs the vendor absolute range offset calibration on a
low-priority task (the sequence busy-waits and would starve the IWDG
feeder on wq:uavcan) and persists the offsets to SENS_AFBR_OFS_LO/HI.

* fix(afbrs50): evaluate every completed frame and guard CONFIGURE against stale completions

Argus_EvaluateData is what releases the API's raw data buffer, and the
API refuses new measurements and rejects configuration writes once two
buffers are held. An error-status callback skipped it, so two bad frames
wedged the driver into the stall recovery with nothing published.

The abort completion of that recovery, arriving from the SPI thread,
could overwrite CONFIGURE with TRIGGER and skip the reconfigure.

Also note the removed parameters in the 1.18 release notes.

* refactor(afbrs50): run the driver in its own task and the SPI transfers on the bus work queue

The transfer work item ran on wq:SPI0 (which does not exist on STM32
targets) purely for its near-top priority: before API 1.6.6 a DRDY
firing within ~60 us of the last SPI clock was lost unless the transfer
callback had already run. Since 1.6.6 ADS_SPI_Callback re-checks the
IRQ pin and recovers a DRDY that arrives before the callback, so the
deadline is gone and the transfer item can live on the work queue of
the bus the sensor actually sits on, at its conventional priority. The
blocking exchange stays on a work queue because the API requests
transfers from hrt interrupt context.

The state machine cannot share that thread: the API's configuration
calls spin in ADS_AwaitIdle until the transfer they queued completes.
It previously borrowed hp_default, whose 2800 byte stack
Argus_EvaluateData overflows and whose priority puts the driver's
blocking configuration waits ahead of dshot and pwm_out. It now runs as
a SCHED_PRIORITY_SLOW_DRIVER task woken by the completion callback
through a semaphore, so the range offset calibration no longer needs
its own task either: the driver drops to SCHED_PRIORITY_DEFAULT for the
duration of the sequence, below the wq:uavcan IWDG feeder, and a stop
request aborts it.

* fix(afbrs50): publish only NO_OBJECT as too far, reinit on stuck CONFIGURE, protect calibration

Every evaluation failure and quality-gated frame went out as max_distance + 1, which collision prevention clamps to max_distance and enters as free space regardless of signal_quality, so a sensor fault on a horizontal mount cleared a real obstacle. Only the device's own STATUS_ARGUS_NO_OBJECT is published that way now; errors and gated frames are counted and left to the consumers' stream timeouts.

CONFIGURE drains a raw buffer an abort may leave behind, since the API rejects configuration writes until it is evaluated, and falls back to Argus_ReinitMode after ten consecutive failures because a sticky error status never returns to IDLE on its own.

The vendor calibration sequence blocks longer than ModuleBase's 5 s stop deadline while holding pointers into the task stack, so 'afbrs50 stop' is refused while it runs and a stop clears a not-yet-started request. The sequence also rewrites the per-pixel offset tables, which cannot be persisted; they are restored afterwards so the sensor runs in the state the stored global offsets re-create at boot. The destructor now also stops the API's periodic timer, the last path that could reach the completion callback after ModuleBase has deleted the instance.

* fix(afbrs50): publish invalid frames with signal quality 0 instead of dropping them

Dropping errored and quality-gated frames left a receiver unable to tell a sensor returning invalid readings from one that fell off the bus. Every frame is published again with signal_quality 0 marking the invalid ones. The distance sent with it is chosen for collision prevention, which ignores quality: NO_OBJECT stays beyond max_distance (free space, TOO_FAR on DroneCAN), errors carry min_distance (discarded, UNDEFINED on DroneCAN), and a gated frame keeps its measured distance with the quality floored to 0.
2026-09-08 20:12:16 -06:00
Julian Oes
c1808fb460 fix(parameters): widen the flashfs working buffer size (#28622)
parameter_flashfs_alloc() assigns *buf_size + sizeof(flash_entry_header_t) to a
static uint16_t, so it can only ever address 65535 bytes.

Twenty-one boards give the parameter store a single 128 KiB sector, all of them
H7 flight controllers that keep parameters in flash because they have no SD
card. Half of that sector is unreachable by construction.

Were a request to exceed the range, the wrap would be silent. There is no
overflow: the caller uses the size written back through *buf_size rather than
the one it asked for, so the copy stays inside the allocation. The result would
be a truncated parameter blob written as if it were complete.

Use size_t, as the rest of the interface already does.

Reported by @lihnucs.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 17:02:26 -06:00
Julian Oes
cc976e150a feat(posix): separate the filesystem root from the storage directory (#28582)
On NuttX these are two things: / is the filesystem root, and /fs/microsd is the
SD card mounted under it. MAVLink FTP serves the root and confines writes to the
SD card, so the read-only ROMFS at /etc is visible but cannot be written.

On POSIX they were the same directory. PX4_ROOTFSDIR and PX4_STORAGEDIR both
resolved to CONFIG_BOARD_ROOT_PATH, which on SITL is ".", so the working
directory was simultaneously the FTP root and the only writable area. The
consequences were that the ROMFS symlink sat inside the FTP root, and that
_validatePathIsWritable() had nothing meaningful to check against and so was
compiled out on POSIX entirely, leaving no write restriction at all.

Give POSIX the same split. CONFIG_BOARD_ROOT_PATH keeps its meaning as the
storage directory, and a new CONFIG_BOARD_FS_ROOT_PATH names the root FTP
serves, defaulting to the storage path so every existing board is unchanged.
SITL sets the root to "." and storage to "./fs", which mirrors NuttX: logs,
parameters, dataman and eeprom move under ./fs, and etc/ stays in the root as
read-only data.

With storage distinct from the root, the write restriction now applies on every
platform rather than NuttX only, and no longer compares against a hardcoded
prefix length that was wrong for any board not using /fs/microsd.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 17:01:55 -06:00
Julian Oes
adb45a88b6 fix(commander): reject an out-of-range mode to replace on registration (#28619)
An external component registering a flight mode can ask to replace an internal
navigation state. The requested value was stored without a range check and later
used as a shift amount when building the selectable mode mask:

	can_set_nav_state_mask &= ~(1u << cur_mode.replaces_nav_state);

replace_internal_mode is a uint8 and NAVIGATION_STATE_MAX is 31, so anything
from 32 upwards shifts a uint32 by more than its width, which is undefined. In
practice the shift is taken modulo the width, so it clears the bit of an
unrelated mode and that mode stops being selectable.

The registration path already rejects a mode that another component has claimed,
with a message and reply.success = false. Reject an out-of-range one the same
way, so the request fails and the component is told, rather than corrupting the
mask. The status print at the top of the file already bounds the same field.

Reported by @lihnucs.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 16:24:45 -06:00
PX4BuildBot
5a0034b732 docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <bot@px4.io>
2026-09-08 16:13:11 +00:00
Jacob Dahl
2322c173aa fix(sensor_calibration): default external accel/gyro priority below internal (#28594)
External IMUs inherited the magnetometer's external-first default of 75,
so an uncalibrated CAN IMU (ARK Flow, X20) took over as primary gyro the
moment it appeared, ahead of the on-board FIFO IMUs, and a calibration
then saved that priority. A magnetometer away from the power electronics
is the better primary; an IMU behind a bus with its own clock, transport
latency and no heater is not. Default external accel and gyro to Low (25)
so they only win when explicitly prioritized.
2026-09-08 10:07:15 -06:00
Julian Oes
83c179163d fix(uavcan): stop the 21-element GNSS covariance case falling into the 36 case (#28604)
The Fix2 covariance handler decodes the upper-triangular 21-element form in
case 21, then falls through to case 36, which decodes the full 6x6 form and
overwrites everything case 21 just produced. Every sibling case breaks; this one
carried a /* FALLTHROUGH */ comment that reads as deliberate but cannot be.

Position covariance ends up read with the indices of a 6x6 matrix, which are in
range for a 21-element array but mean something else. Velocity covariance is
read from indices 21 to 35, which are out of range entirely, and since the
driver is built with UAVCAN_NO_ASSERTIONS the libuavcan dynamic array clamps
them to the last valid element instead of aborting. All nine velocity terms
become covariance[20].

So a CAN GPS that reports the 21-element form, which is a normal thing for it to
do, feeds EKF2 a wrong position covariance and a constant velocity covariance,
silently.

Reported by @lihnucs.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 10:06:23 -06:00
Julian Oes
bcfb9386d2 fix(mavlink): only handle TUNNEL messages addressed to this vehicle (#28597)
handle_message_tunnel() decoded target_system and target_component, copied both
into the uORB message and published, without ever comparing them. The consumers
do not look at them either: neither voxl_esc nor voxl2_io references either
field, so the ids travelled into uORB and were then ignored by everyone.

TUNNEL payloads are written straight to a device. On a link carrying more than
one vehicle, a message addressed to another vehicle was therefore written to
this one's ESC or IO UART.

Use the existing evaluate_target_ok(), as the command and command-ack paths do.
Broadcast still passes, since MAV_COMP_ID_ALL is zero.

Reported by @20210607.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 10:02:36 -06:00
Julian Oes
0084d9d347 fix(mavlink): authorize FTP writes when the file is opened (#28584)
The writable-path check sat in _workWrite(), which is both too late and against
the wrong path.

Too late, because CreateFile and OpenFileWO open with O_CREAT and O_TRUNC, so
the file is created or truncated during the open. No write ever has to arrive
for the effect to land, and _workOpen() checked only _validatePath().

The wrong path, because _workWrite() validated _work_buffer1, a scratch buffer
that every request writes its constructed path into. A directory listing
between the open and the write leaves it holding the listed path, so the check
compared something unrelated to the descriptor being written to.

Authorize in _workOpen() instead, where the descriptor is bound, and only when
the flags carry write intent. Every other destructive operation already checks
this way; the open path was the one that did not.

Reported by @20210607, @REYu6 and @mbanyamer.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-08 10:00:52 -06:00
abhijith
7df3dbcc73 feat(boards/agam-robotics): add Agam Robotics FMU-v6XRT board support (#28285)
* feat(boards/agam-robotics): add Agam Robotics FMU-v6XRT board support

---------

Signed-off-by: abhijithcv <abhijithcv1212@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Farhang <46557204+farhangnaderi@users.noreply.github.com>
Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
2026-09-08 10:47:44 -04:00
PX4BuildBot
fc5256cf55 docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <bot@px4.io>
2026-09-08 10:05:16 +00:00
Andrea Bernasconi
3677821fed fix(navigator): make RTL_TYPE dropdown options compact (#28528)
The option strings were too long, which made the value
dropdown overflow the window. Used short
names and kept the full explanations, unchanged, in the parameter long
description.
2026-09-08 11:45:45 +02:00
Thomas Stauber
662dd41db8 fix(simulation): correctly fill the device_id of the simulated sensor_gps message (#28592) 2026-09-08 11:34:44 +02:00
Mahima Yoga
b95ae09e36 fix(land_detector): keep FW landed while waiting for the takeoff status (#28551) 2026-09-08 10:23:26 +02:00
Ltxx
2c47a16f56 fix(simulation): restore Gazebo fallback targets (#28339)
* fix(simulation): restore Gazebo fallback targets

Move the airframe glob outside the dependency check and create one fallback target per Gazebo airframe when dependencies are missing.

Assisted-by: OpenAI Codex: GPT-5

---------

Co-authored-by: Farhang <46557204+farhangnaderi@users.noreply.github.com>
2026-09-07 15:59:53 -04:00
Ramon Roche
e3480a1e37 chore(agents): add Codex instructions and skills
Expose PX4 guidance and workflows to Codex while keeping root instructions concise and reusing tracked Claude workflows where possible.

Assisted-by: Copilot:gpt-6-astra
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-09-07 10:51:49 -07:00
Ramon Roche
51b9a86a7e docs(dev_setup): refresh macOS environment page to match macos.sh (#28295)
The macOS setup page has not been edited since 2026-04-20, while
Tools/setup/macos.sh changed five times after that. The page still
describes the px4-sim Homebrew formula, which has been a deprecated
no-op in PX4/homebrew-px4 since 2026-04-20, and tells users to add
simulation later with `brew install px4-sim`, which installs nothing.
On Homebrew 6.0+ it does not even load, because the page never mentions
the tap trust the script now performs.

Stop enumerating the script's packages and taps in the page; that list
is what went stale. Say what the script does in one sentence, point at
macos.sh as the source of truth, and document only the user-facing
behaviour: Gazebo, XQuartz and a JDK come with --sim-tools, that path
needs sudo, taps are trusted for Homebrew 6, and the way to add
simulation later is to re-run the script with --sim-tools. Drop the
Python prerequisite; the script installs Python and creates the venv
itself.

Fix the pages it links to that contradict it: the simulation overview
still marked Gazebo on Apple Silicon as unstable (pre-dates the
2026-04-20 fix), the supported-targets table claimed Gazebo Classic on
macOS with nothing installing it, the jMAVSim page said Java must be
installed manually on macOS, and the build troubleshooting page
recommended ulimit 300 and running mac_set_ulimit.sh as a script (which
cannot change the caller's shell limit).

Assisted-by: Claude:claude-fable-5

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
Co-authored-by: Farhang <46557204+farhangnaderi@users.noreply.github.com>
2026-09-07 13:14:34 -04:00
PX4BuildBot
2e20f66af7 build(macos): bump gz tap pin to 9403d12a474c 2026-09-07 08:57:05 -07:00
Jacob Dahl
91bb35c8b3 fix(nuttx): bump to the stm32h7 MDIO poll fix (#28591)
Every PHY register access on STM32H7 spent 5 ms in up_mdelay(), so the
boot-time autonegotiation link wait ran netinit at 44% CPU for 65 s with
the net lock held whenever no ethernet cable was attached (PX4/NuttX#414,
apache/nuttx#20067). Also picks up the two upstream stm32_mdio fixes for
the lower-half cast and the Clause 22 write register field.
2026-09-07 09:44:09 -06:00
PX4BuildBot
a9d4516ce9 docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <bot@px4.io>
2026-09-07 15:23:17 +00:00
Claudio Chies
e87918ea2b fix(failure_injection): keep failure names in sync with FailureInjection.msg (#28376)
unitName()/typeName() lacked the FAILURE_UNIT_DATALINK_*/FAILURE_UNIT_BUS_* units, so `failure can off` was announced as unit "unknown". FailureInjection.msg and VehicleCommand.msg also disagreed on FAILURE_TYPE_DRIFT (MAVLink FAILURE_TYPE 8).

Name every unit and type, add FAILURE_TYPE_DRIFT to both messages and the `failure` console command, and add a test that fails whenever a message constant has no name.
2026-09-07 09:16:38 -06:00
Julian Oes
0fb3847cd9 fix(mavlink): reject TUNNEL messages that overstate their payload length (#28573)
The TUNNEL message carries a fixed 128-byte payload alongside a uint8_t
payload_length, so a sender can advertise up to 255 bytes of data in a
buffer that can only ever hold 128. handle_message_tunnel() copied that
length into the uORB topic unchanged.

The consumers trust it: voxl_esc and voxl2_io both pass payload_length
straight to _uart_port.write() over the 128-byte payload, so a length
above 128 reads past the end of the array and transmits whatever follows
it out of the ESC or IO UART.

Drop such a message rather than clamping the length. These payloads are
forwarded verbatim to a device, where a truncated frame would corrupt the
device protocol rather than fail cleanly.

Assisted-by: Claude:claude-opus-5[1m]

Signed-off-by: Julian Oes <julian@oes.ch>
2026-09-07 13:29:21 +12:00
Alex Klimaj
1e015c73e3 feat(drivers/afbrs50): update Broadcom API to 1.6.6.1 (#28262)
* feat(drivers/afbrs50): update Broadcom API to 1.6.6.1

Vendor the latest AFBR-S50 API so the rangefinder driver tracks
Broadcom's 1.6.6.1 library and near-range DCA improvements.

Assisted-by: Grok:grok-4.6
Signed-off-by: alexklimaj <alex@arkelectron.com>

* fix(drivers/afbrs50): print full API version 1.6.6.1

Argus_GetAPIVersion() only encodes major.minor.bugfix, which hides
the vendored 1.6.6.1 patch level in the startup log.

Assisted-by: Grok:grok-4.6
Signed-off-by: alexklimaj <alex@arkelectron.com>

---------

Signed-off-by: alexklimaj <alex@arkelectron.com>
2026-09-06 13:19:38 -06:00
Jacob Dahl
4d99a38b5f docs(maintainers): promote Jonas Perolini to Navigator code owner
Navigator (missions, RTL, detect-and-avoid, takeoff/landing) has no Code Owner. Jonas has been doing that work as a Reviewer.

Assisted-by: Grok:grok-4.6
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-05 16:33:45 -07:00
Jacob Dahl
6dc89338a8 fix(commander): build mag calibration only with CONFIG_SENSORS_VEHICLE_MAGNETOMETER (#28555)
Boards that compile the magnetometer pipeline out still linked 8.6 KB of
calibration that could never run. holybro_kakutef7 is one of them and sits
at 100 % of flash on main. The calibration commands answer UNSUPPORTED
there.
2026-09-04 21:26:50 -06:00
Andreas
29120280ac fix(crsf_rc): avoid out-of-range packet type enum conversion (#28449)
An arbitrary CRSF packet type is an eight-bit wire value. Converting an unknown value to CRSF_PACKET_TYPE before descriptor lookup invokes undefined behavior under C++ enum sanitization. Keep the lookup  boundary as uint8_t; the descriptor table already stores its packet type in that representation.
2026-09-04 21:02:34 -06:00
Jacob Dahl
0294b6200f fix(paa3905): rate-limit challenging surface warning (#28553)
The Motion ChallengingSurface bit stays asserted for the whole time over a bad surface, so PX4_WARN floods the console at the run-loop rate.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-04 17:21:36 -06:00
Ramon Roche
6f93287fdd ci(macos): rewrite gz tap pin refresh in Ruby under brew ruby
The bash version spent most of its lines on plumbing: parsing a report
back out of a per-commit `brew ruby` child, awk and herestrings for set
logic, bash 3.2 workarounds, and a trap to restore the tap. The job
itself is small and lives inside Homebrew anyway, so run the whole thing
under Homebrew's Ruby and call its tap, formula and bottle APIs directly.

One process instead of one Homebrew startup per candidate commit, the
tap checkout restored by ensure, tarballs checked with Net::HTTP, and no
second language embedded in a string. Formulary.clear_cache and
Tap#clear_cache between checkouts keep each candidate's formulae fresh.
GITHUB_OUTPUT still arrives because brew forwards GITHUB_* whenever CI
is set. Same walk, same exemption for formulae unbottled at the pin,
same outputs; verified to pick the same commit as the bash version.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-09-04 13:26:30 -07:00
Ramon Roche
2d6329fab2 ci(macos): brew update before macos.sh so both legs see current core
The macOS runner images set HOMEBREW_NO_AUTO_UPDATE in their bashrc and
carry a homebrew-core snapshot from image build time. Today the macos-15
image (20260828) installs protobuf 36.0 while macos-26 (20260707) still
installs 35.1, so the two legs of this workflow cannot both be
compatible with any single gz tap pin. The pin is refreshed against tap
HEAD, which OSRF keeps consistent with current homebrew-core, so update
core before installing. Dev machines already do this through brew's
auto-update.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-09-04 13:26:30 -07:00
Ramon Roche
832f8ebac8 build(macos): bump gz tap pin to 43aee9cc6e02
homebrew-core moved to protobuf 36.0 and abseil 20260817.0, and the
macos-15 runner image (20260828) now ships that snapshot. The gz bottles
at the pinned commit c90f81ac were built against protobuf 35.1, so the
gz_bridge build fails on every run since today with

    gz/msgs/details/discovery.pb.h:17:2: fatal error: "Protobuf C++
    gencode is built with an incompatible version of"

on main as well as on this PR. 43aee9cc is tap HEAD, where OSRF has
rebuilt every gz-harmonic dependency for protobuf 36.0 and abseil
20260817.0 (the gz-msgs10 10.4.0_2 bottle's INSTALL_RECEIPT records
both). Tools/ci/refresh_gz_tap_pin.sh picked this commit.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-09-04 13:26:30 -07:00
Ramon Roche
045f249929 ci(macos): move gz tap pin refresh into a script that checks bottles
The weekly refresh workflow carried its logic inline, compared only the
tap SHA against the pin, and duplicated the --sim-tools package list.
`brew install --dry-run` never prints "from source", so its bottle check
could not fire.

Tools/ci/refresh_gz_tap_pin.sh now owns the job and the workflow calls
it. It reads the osrf/simulation formulae from macos.sh, walks the tap
from HEAD back to the pin, asks Homebrew at each commit whether every
formula in the runtime closure has a bottle for the host, HEAD-requests
each tarball, and rewrites the pin to the first commit that passes.
Formulae with no bottle at the current pin (the gz-harmonic
meta-formula, which OSRF never bottles) are not required.
peter-evans/create-pull-request opens or updates the bot PR from the
script's outputs, so an unmerged bump is refreshed instead of duplicated.

The job runs on macos-15, the oldest macOS in compile_macos.yml, because
Homebrew pours an older macOS bottle onto a newer one, not the reverse.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-09-04 13:26:30 -07:00
FARHANG
d840da70b6 build(macos): pin gz tap unconditionally, add weekly refresh
macos.sh only pinned osrf/simulation when --ci was passed, so dev
machines still hit OSRF's bottle-gap window and built Gazebo from
source. Apply it whenever --sim-tools runs.

The pin is a point-in-time SHA with nothing to advance it, so add a
weekly workflow that checks whether osrf/simulation HEAD is fully
bottled and opens a PR bumping gz-tap-pin.txt when it is.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: FARHANG <farhang.nba@gmail.com>
2026-09-04 13:26:30 -07:00
XiaoYang Chen
354823e2ef fix(load_mon): add return code at module stop path (#28544)
* fix uaf bug in load_mon module

* fix(load_mon): end the perf cycle before deleting the module

exit_and_cleanup() deletes this and frees _cycle_perf, so the elapsed counter has to be closed on the live object.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

---------

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-04 13:25:32 -06:00
PX4BuildBot
f8387d19a1 docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <bot@px4.io>
2026-09-04 15:28:01 +00:00
Hamish Willee
7e1298f8da docs(docs): SensorUwb.msg - update to UORB docs standard (#28522)
* docs(docs): SensorUwb.msg - update to docs standard

* docs(docs): Remove whitespace
2026-09-04 08:21:50 -07:00