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.

View on GitHub The flight test

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
paramguard check vehicle.params cleanup.params cleanup.params (against vehicle.params) 1 effective change: NAV_DLL_ACT 2 (Return mode) -> 0 (Disabled) (line removed: firmware default) BLOCK gcs-loss-failsafe-off cleanup.params

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.15 and QGroundControl's 0.15000000596046448 are 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

      The left file is the repository's 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

        Rows are every line in 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-integer and unknown-bits: PX4 stores such values without complaint. Checked for this page on a fresh PX4 v1.15.4 SITL: it stored and read back MPC_Z_VEL_MAX_DN = 5 against a maximum of 4, MC_ROLLRATE_P = -1 and NAV_DLL_ACT = 4, and answered MPC_XY_CRUSIE with 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

          BLOCKFails the checknoteAlready in the base version
          The first two rows are from the repository's 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_bits and below, 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 block or warn. 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

            
                          
            The three findings are from 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

            The vehicle file with 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, meta for your board's build, rules and allow.
            • 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

              A scratch Git repository whose main branch holds 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 deleted
              Logged telemetry from two runs of make 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.

              Same flight, two endings

              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.

              First guess291 / 295
              In the top 3295 / 295

              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:

              9.4 s9.2 s8.6 s

              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 below rules, 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, --base against a real Git repository, allowance files, and quiet output when piped into head.

                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 --meta at your build's parameters.json.
                A whole new file
                check judges changes, and a new file is compared with the firmware defaults, so a new vehicle file that never sets NAV_DLL_ACT passes it. paramguard lint judges 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.