paramguard
A CI check for PX4 and ArduPilot parameter files. It reads a parameter change the way the vehicle will, and blocks the pull request when the change turns off a failsafe, misspells a name, leaves the firmware's documented range, or steps a control gain too far.
A parameter file decides what a drone does when the radio link drops, the battery runs low or the fence is crossed. Teams review these files as text diffs, but the vehicle runs values. Delete a line and the vehicle falls back to the firmware default, which for PX4's link-loss failsafe is "do nothing". Misspell a name and the line sets nothing at all. Neither looks wrong in review; paramguard says why each one is.
Plays the six stages below in order, about 60 seconds. Scroll or press Esc to stop.
- Language
- Python 3.11 or newer, standard library only
- Tests
- 66 passing with pytest, 67 with ArduCopter's metadata downloaded
- Metadata
- PX4 v1.15.4's own, 1,784 parameters, bundled; ArduPilot's read from its XML or JSON
- Rules
- 28 built in for PX4, 17 for ArduCopter, each with a reason
- Runs as
- A CLI, and a composite GitHub Action
- License
- MIT; the bundled PX4 metadata is BSD 3-Clause
Real runs of paramguard 0.1.0 in examples/survey-quad, the repository's example vehicle, output verbatim. The cleanup pull request deletes 17 lines "already at their defaults"; the tuning one changes five values. Both files are part of the repository, not real flight logs.
1Read the file the way the vehicle's loader does
A parameter file can be a QGroundControl export, a PX4 startup script, or plain NAME VALUE and NAME,VALUE lines from ArduPilot and Mission Planner. paramguard reads all of them, mixed in one file if need be, and compares values, not text.
- Floats are compared as the float32 the autopilot stores, so
0.15and QGroundControl's0.15000000596046448are the same value. Line order never matters. - QGroundControl's type column is checked: an integer parameter written with a fraction is refused.
- Any line it can't read stops the run with the file and line number, and exit code 2. A file that is half read is worse than one that is refused.
- A name set twice is an error, because which value wins depends on the loader.
The same vehicle, written another way Illustrative input
vehicle.paramsQGroundControl export
respelled.paramsthree other formats, reordered
Files it refuses
examples/survey-quad/vehicle.params, first eight of its 24 parameters shown. The right file and the three refused ones were written for this page: the same 24 values in other formats and another order, a name set twice, a word for a value, and an integer with a fraction. paramguard's output and exit codes are real.2Compare what the vehicle will run, not the lines
Each parameter's effective value is the file's value, or the firmware default when the file doesn't set it. The defaults come from the firmware's own metadata. A change is any parameter whose effective value moved.
- Deleting a line is a change whenever the deleted value wasn't the default. That is the change a reviewer reads as harmless cleanup.
- Writing a default out explicitly changes nothing, so tidying a file in either direction is quiet.
- A file that is new on the branch is compared with the firmware defaults.
In PX4 v1.15.4 the default for NAV_DLL_ACT, the ground-station link-loss failsafe, is 0: disabled. PX4's simulated planes set it to 2 at start-up, which is why a value of 2 looks like a default to anyone who has used them.
The 17 lines the cleanup deleted
vehicle.params that cleanup.params doesn't have. Defaults and value names are read from the PX4 v1.15.4 metadata that ships with paramguard. The terminal is the head of paramguard's own report for the pair.3Check every changed line against the firmware's metadata
Every PX4 build generates a parameters.json with each parameter's type, range, enum values, bitmask bits and default. paramguard ships the v1.15.4 SITL build's copy, and --meta points at any other build's, or at ArduPilot's apm.pdef.xml.
unknown-param: the vehicle has no parameter by that name, so the line sets nothing. It suggests the name you meant.out-of-range,not-in-values,not-integerandunknown-bits: PX4 stores such values without complaint. Checked for this page on a fresh PX4 v1.15.4 SITL: it stored and read backMPC_Z_VEL_MAX_DN = 5against a maximum of 4,MC_ROLLRATE_P = -1andNAV_DLL_ACT = 4, and answeredMPC_XY_CRUSIEwith nothing but an error on its own console.- A problem already in the base version is reported as a note, not a block, so adopting paramguard doesn't fail on a vehicle's history.
Lines the metadata rejects
tuning.params. The rest come from a copy of the vehicle file edited for this page Illustrative input: NAV_DLL_ACT set to 4, COM_DL_LOSS_T to 10.5 and EKF2_GPS_CTRL to 31, against a base that already had MPC_TILTMAX_AIR = 95. Every message is paramguard's, verbatim.4Judge each change against rules that say why
A rule names parameters, a condition and a reason written for the reviewer. The built-in PX4 set covers failsafes, the geofence, circuit breakers, airframe and motor mapping, and tuning steps. Your own TOML rules add to it.
- Conditions:
forbid,allow_only,min,max,no_decrease,no_increase,max_step,no_sign_flip,any_change,no_new_bitsandbelow, on names or globs. - Rules are checked against the metadata before anything runs. A misspelt parameter, a glob that matches nothing, an unknown key or a missing reason is an error: a rule that silently never fires is the failure this tool exists to prevent.
- A rule is
blockorwarn. Only a block fails the check.
The tuning pull request, rule by rule
A team's own rule, and three ways to get one wrong Illustrative input
paramguard check vehicle.params tuning.params; the step bar is the rule's own arithmetic, 0.15 to 0.25 against a 50% limit. The rule file is the README's example, run against a copy of the vehicle file with MPC_XY_CRUISE at 10. The three broken copies of it were made for this page. Output and exit codes are real.AllowedAbove this line paramguard reads, resolves and checks, and any block fails the pull request. Below it, a block passes only with a committed allowance that names who accepted it and why.
5A named person accepts one exact value
Some blocked changes are right: a tethered vehicle doesn't need a link-loss failsafe, and a retune after a prop change does move gains a long way. An allowance in paramguard.allow.toml accepts one finding, and it must say who and why.
- It covers one rule, one parameter and one value, or
"removed". If a later change moves the gain again, the block comes back. - An allowance that no longer matches anything is reported, so a stale one can't be mistaken for one that still covers something.
- An allowed finding is still printed, with the name and reason, so the decision stays in git next to the parameters.
A roll-rate retune, three times Illustrative input
exit1
MC_ROLLRATE_P changed from 0.15 to 0.25, then to 0.3, and the allowance from the README. All made for this page; every report and exit code is paramguard's own.6Run it on every pull request
paramguard check --base origin/main vehicles/*.params compares each file with its version on the base branch, read with git show. As a GitHub Action it does the same against the pull request's base, and --format github puts each finding on the diff, on its line.
- Exit 0 when nothing blocks, 1 when something does, 2 when an input can't be read.
- Action inputs:
files,base,metafor your board's build,rulesandallow. - The repository runs its own action in CI, and flies the link-loss comparison in PX4 SITL on every push.
The tuning branch, as GitHub shows it
CI run 38611811458 on dfc14436 jobs, all passed
vehicle.params and whose branch replaces it with tuning.params. The annotations are the real --format github output of paramguard check --base main, drawn the way GitHub places them; each shows the finding, and the reason that follows it in the real annotation is left out here. Job names, results and durations are from GitHub's API for that run.Measured, not assumed
The cleanup's deleted line, flown. The repository's SITL script plays a ground station in PX4 v1.15.4 (Gazebo, in Docker): it loads the parameter file, takes off, flies 300 m north, holds for 5 s, then stops sending heartbeats. It keeps listening, so the telemetry after the link loss is still recorded.
Distance from home in metres, after the radio link is lost
vehicle.params, NAV_DLL_ACT 2After the cleanup, line deletedmake sitl's flight on this machine (2026-10-16), thinned to one point a second; the times are what the repository's analyze.py measured from it, and its --check passed. In the repository's own recorded run Return mode started at 17.4 s and the landing came at 161.5 s. PX4 waits COM_DL_LOSS_T (10 s) after the heartbeats stop, then COM_FAIL_ACT_T (5 s) before acting. Both runs used a separate Docker network, a change to sitl/run.sh made only for this machine.Typos it names correctly
The suite swaps two neighbouring letters in 300 random PX4 names and asks for a suggestion. In 5 of the 300 the swap gave back the same name.
The four misses are ambiguous: R1C1_MAX is one edit from RC1_MAX as well as RC11_MAX. The test asserts more than 97% and all within the top three.
The suite, start to end
pytest -q, including the Hypothesis property tests over random files drawn from the real PX4 metadata. Three runs in a row on this machine, Python 3.12.3:
66 passed and 1 skipped each time, close to the README's "about 10 s". The slowest test is the typo check, about 4 s; the Hypothesis tests, at 200 or 300 examples each, take most of the rest.
67 tests, one of them needs ArduPilot's file
Counted with pytest -q: 66 pass and one skips. The skipped test checks the built-in ArduCopter rules against ArduCopter 4.6.3's own metadata, which is GPL-licensed and not bundled; with it downloaded, as make ap-meta and CI do, all 67 pass.
Aimed at the ways a reviewer gets fooled
- Parser, metadata and rules: every line format, every format error with its line number, every rule-file mistake and rule condition. The built-in rules are checked against the bundled metadata.
- Both example pull requests end to end, plus allowances, cross-parameter
belowrules, notes for problems already in the base, and ArduPilot without defaults. - Properties, with Hypothesis: a file against itself changes nothing; line order never matters; writing out a default never changes the result; the link-loss rule blocks exactly when the value becomes 0; the range check agrees with the metadata; an allowance covers its own value and no other.
- The CLI: exit codes, JSON, GitHub annotations,
--baseagainst a real Git repository, allowance files, and quiet output when piped intohead.
CI runs ruff, mypy and the tests on Python 3.11, 3.12 and 3.13, checks the ArduCopter rules against 4.6.3's metadata, runs the action on the repository's own example, and flies the SITL comparison.
Deliberately missing
- A proof of safety
- It catches the kinds of change it has rules for. A file that passes can still fly badly; tuning needs flight testing, and the gain-step rule only stops big jumps between tests.
- Your vehicle's limits
- The built-in thresholds, 20 m return altitude, 3 m/s descent, a 50% gain step, are general multicopter sense. Put your own numbers in your own rule file.
- Your board's defaults
- The bundled metadata is PX4's SITL build. A flight-controller build adds driver parameters and airframe scripts change defaults, so point
--metaat your build'sparameters.json. - A whole new file
checkjudges changes, and a new file is compared with the firmware defaults, so a new vehicle file that never setsNAV_DLL_ACTpasses it.paramguard lintjudges a file on its own and blocks it.- The vehicle itself
- It checks files in a repository. A parameter changed from a ground station and never committed is invisible to it, and no real aircraft flew: the flight above is SITL.
Run it
git clone https://github.com/ahmed-hashim-pro/paramguard cd paramguard python3 -m venv .venv && . .venv/bin/activate pip install -e '.[dev]' pytest -q # 66 passed, 1 skipped cd examples/survey-quad paramguard check vehicle.params cleanup.params paramguard check vehicle.params tuning.params paramguard explain NAV_DLL_ACT GF_ACTON # In CI, against the base branch: paramguard check --base origin/main vehicles/*.params
Python 3.11 or newer, nothing else to install. For ArduPilot, download the release's apm.pdef.xml and pass it with --meta. make sitl needs Docker and flies both runs again.