Skip to content

Flight parameters for the real aircraft

These files are loaded onto the flight controller. sim/terrahawk.parm is SITL’s own file, with SITL’s output map, and must never be loaded onto the aircraft.

File Holds Owner
terrahawk-common.parm Everything that is the same for every airframe and flight controller: frame, speeds, assist, control, TECS, landing, returns, modes, failsafes, sensor positions th-avionics
board-pixhawk6c-mini.parm The Pixhawk 6C Mini’s orientation, output map, CAN drivers, serial ports and power monitor. Not the first-flight board since 2026-10-04; kept as the reference for our own FC th-avionics
board-luxf765.parm The first-flight board, the Lumenier LUX F765 NDAA (Ryan, 2026-10-04): one IMU, one CAN port (off for first flights), 12 outputs. Same output map as the NxtPX4v2 on 1 to 8 (lift motors on the 4-in-1, elevons 5 and 6, pusher 7); on the first build output 8 (the camera trigger) and S1 (the cargo door) are empty (Ryan, 2026-10-19), and the board file carries the camera’s rows as comments for when it flies; the I2C MS4525DO airspeed sensor; ELRS on the receiver port (SERIAL7). Needs a microSD card to boot. Mounted USB-C / microSD edge forward (th-blender 8ecb409); INS_POS is an estimate from a photo, and AHRS_ORIENTATION is set by the bench tilt in the file, because no source shows the board’s arrow th-avionics
board-nxtpx4v2.parm The HKUST NxtPX4v2, the first-flight board until the LUX F765 replaced it (2026-10-04); kept as a checked alternative with two IMUs: no CAN, so it flies the 4-in-1 only, with the 4-in-1 settings in this file (do not load lift-4in1.parm with it), an I2C MS4525DO airspeed sensor, the camera feedback and the video switch on its AUX pads, and no output for the cargo door. Its mount (arrow forward) and INS_POS are from th-blender’s plate, 31824ff; INS_POS_Z rests on an estimated grommet height th-avionics
lift-4in1.parm The lift motors on the TBS Lucid 4-in-1 (the 3-8S, AM32_TBS_8S_4IN1_F421) with the Pixhawk 6C Mini only: DShot with RPM on MAIN 1-4, the ESC mask on CAN bus 1 cleared, and KISS telemetry on SERIAL6. The NxtPX4v2 carries these settings in its own file, so the first build does not load it. Leave it out when DroneCAN pod ESCs are fitted th-avionics
pack-lipo-4500.parm Only when the Tattu 6S 4500 LiPo, the tuning pack, is fitted: its capacity, voltages and capacity triggers in place of the P50B’s. Load it last; load the common file again before the next flight on the P50B th-avionics
payload-tfmini.parm The optional downward TFmini-S on “Payload Cover (TFmini-S)”: loaded after the airframe file only with that cover; serial on the LUX’s TELEM1 pads (SERIAL1; the GPS2 pads are the M10-25Q’s since 2026-10-15), position from th-blender 0900ef8, takeoff and landing use. Without it the board file’s RNGFND1_TYPE 0 stands th-avionics
test-stall.parm The stall-test flight only (docs/stall-test.md): Q_ASSIST_SPEED 14. Load after the airframe file for that flight; load the common file again after it. An overlay, so the checklist gate leaves its names out
testbed-quad.parm The lift testbed quad (Ryan, 2026-10-10): an HGLRC MY10 10 inch frame with the aircraft’s lift motors, 3-blade props, Lucid, LUX and P50B, flying ArduPlane in Q modes with no wing. Loaded in place of airframe.parm, after the common and LUX files, so the Q_A_, Q_M_ and Q_P_ rows tuned on it are the aircraft’s rows; it overrides only Q_M_THST_HOVER’s start value (0.15 bare, 0.25 ballasted), which the hover learn replaces. It turns off the wing outputs, the airspeed sensor and Q-assist, makes every RTL a QRTL, allows arming only in Q modes, and adds a 40 m by 60 m fence. QAUTOTUNE needs our LUX build (hw/flight-controller, make lux-firmware). The rate gains it gives are a start for the aircraft’s own QAUTOTUNE, not its answer: the MY10’s arms and inertia are not the aircraft’s th-avionics; positions measured on the testbed
airframe.parm Values measured on the aircraft (one airframe, Ryan 2026-10-04): board level, airspeed ratio, elevon endpoints and trims, (written by calibration) compass offsets, and the compass’s current compensation (COMPASS_MOT_* from tools/fit_compass_mot.py) th-avionics; values measured on each airframe

Load order: common, then board, then airframe. With the Pixhawk 6C Mini and the 4-in-1, lift-4in1.parm goes after the board file. Some parameters appear only after the parameter that enables their group is written and the board restarts: the Q_ parameters after Q_ENABLE, the CAN_D1_/CAN_D2_ parameters after CAN_P1_DRIVER/CAN_P2_DRIVER, and the OSD, BATT2_ and relay groups after their type or function parameter. Mission Planner’s Load from file skips a name that the board does not have, so one pass is not sufficient.

  1. In Mission Planner, open Full Parameter List. Load and write the common file, then the board file (board-luxf765.parm for the first build), then the airframe file.

  2. Reboot the flight controller.

  3. Load and write the three files again, in the same order.

  4. Reboot the flight controller.

  5. Save the board’s parameters to a file (Save to file).

  6. Compare them with the files:

    Terminal window
    uv run --no-project python tools/check_params.py --loaded <saved.param> \
    params/terrahawk-common.parm params/board-luxf765.parm \
    params/airframe.parm
  7. If the check reports a name that is not on the board, do steps 3 to 6 again.

  8. If the check reports a different value, correct the board or the file. After a calibration, the airframe file gets the measured value.

Our own flight controller will get its own board file; the common file does not change. The airframe file’s elevon rows are named by output number, 5 and 6 for the LUX F765 and the NxtPX4v2: rename them if the new board puts the elevons elsewhere (check_params.py refuses an elevon row on an output that is not an elevon).

Pre-flight condition: the CG. Measured CG 267 to 270 mm aft of the nose datum with the flight pack fitted, aiming for 268; 270 is the hard aft edge (th-aeronautics, 2026-09-29, single-mesh CFD sweep in docs/cg.md, e66fc58). On Fuselage C the forward edge is 267 (Ryan, 2026-10-19, “trim row go”; docs/cg.md aa39a573): it is the stall test’s edge, because nothing flies forward of a tested CG, and the stall test is flown at or forward of 267. C keeps a static margin of about 7.4 % at 267 and 6.1 % at 270 in the soft band (th-aeronautics, coarse, ESTIMATE). The figures below are the earlier wing’s until docs/cg.md restates them for C. In cruise (alpha 4 to 8 degrees) the neutral point is 294.6 mm, so 270 has a 10.8 % margin. In a soft band, about 17.25 to 18.75 m/s at 2.733 kg (trimmed, level; jbsim-b3, 2026-10-03; 16.75 to 18.1 at 2.59 kg; about 17.6 to 19.2 m/s at the first build’s 2.84 to 2.87 kg by square-root mass scaling, an estimate), the pitch stiffness falls away: the CFD is exactly neutral from alpha 9.5 to 10 degrees, so a CG at 270 is locally neutral there, and 265 to 266 keeps about 2.0 % (conservative row, jbsim-b3 and th-aeronautics, 2026-09-30; it supersedes 2.6 % at 270 and 16 to 18 m/s). Normal flight stays above the band: TECS’s floor is AIRSPEED_STALL 20.0 m/s in straight flight, 1.25 m/s above the band’s top at 2.733 kg (0.65 m/s less margin than at 2.59 kg). At the first build’s flying mass the band’s top scales with the square root of the mass to about 19.1 m/s at 2.838 kg (keel B) and 19.2 m/s at 2.871 kg (keel A), so the margin is about 0.9 and 0.8 m/s (th-avionics estimate from th-aeronautics’ 2.85 kg planning mass, 2026-10-06). Do not lower AIRSPEED_STALL without jbsim-b3’s band table for the mass flown; back-transitions pass through it with the lift motors helping. The forward limit is 258 mm at 2.85 kg (the first build as it flies; th-aeronautics 4964a02 and docs/cg.md, 2026-10-06; it was 256 mm at 2.75 kg from 2026-10-01), from the elevon up travel that is left after the cruise trim (the most the autopilot can command is the trim plus MIXING_GAIN x the travel from trim to the end stop; th-aeronautics aefa9c5, 2026-10-01; it was 254 mm at 2.59 kg on the Tattu, and 261 mm with the trim assumed at neutral). So the aft side is the one to protect. Measure the CG with each pack and write it in the airframe file’s header. The sensor positions in the common file are taken from 270 mm; the 2 to 5 mm difference is negligible for them.

Check the files against ArduPlane’s own metadata before loading them:

Terminal window
# in a private ArduPilot checkout (never the SITL tree):
Tools/autotest/param_metadata/param_parse.py --vehicle ArduPlane --format json
uv run --no-project python tools/check_params.py <that apm.pdef.json> params/*.parm

It reports unknown names and values outside a documented range or list. A parameter that one of our Lua scripts adds (for example LCK_LANE_REQ) is not in ArduPlane’s metadata, so the check looks for it in sim/scripts/*.lua instead: the script’s prefix and the names it adds. A misspelled script parameter is still reported as unknown. A value deliberately outside a documented range carries a range-ok comment. Checked clean against ArduPlane 4c98c92 on 2026-09-28.

Provisional values and what replaces them:

Value Replaced by
AIRSPEED_STALL 20.0 The FBWA stall test’s measured 1 g stall, referred to the worst case in the window, then x 1.08 (docs/stall-test.md). Refer it to 265 mm, forward of anything flown: V x (1 + 0.0035 x (CG_test - 265)), because a forward CG stalls faster. Refer it to the heaviest flight mass in the window, on Fuselage C the night build at about 3.21 kg (th-aeronautics, 2026-10-19, ESTIMATE; it was 2.84 to 2.87 kg on the earlier first build): V x sqrt(m_max / m_test). One value covers every airframe and pack, so it must cover the worst one (th-aeronautics, 2026-09-29). Fuselage C (fenced wing, side ports at ARSPD_RATIO 2.3): the trimmed stall is about 17.2 m/s EAS at 3.21 kg, which the side ports read low near the stall (Cp about 0.39 to 0.40 at alpha 11 to 12), so it shows about 14.3 m/s; 20.0 is a margin setting about 1.4 times the indicated stall until the stall test, and holds about 22.6 m/s true in level flight (th-aeronautics, 2026-10-19, ESTIMATE: CFD CLmax pinned, coarse Cp at the stall alpha). Compare readings with readings: the test reads the indicated speed at the break, and that number sets it. 19.2 was 17.8 x 1.08. Do not change it on a SITL stall: the refit CFD model’s 18.2 m/s stall on th-simulator’s probe comes from the model’s construction, which pins the stall to the old stall analysis’s CL. It is not a CFD measurement. The CFD shows a soft stall with CL still rising past 14 degrees, and a trimmed stall near 16 m/s at 2.59 kg (th-aeronautics, an estimate). Until the flight stall test it is a margin setting, not the stall: 20.0 (2026-10-01, was 19.2) is 9 % over SITL’s 18.34 m/s wings-level stall at 2.733 kg and about 20 % over the CFD’s trimmed estimate of 16.6 (th-aeronautics). It is also the mass referral of 19.2 to 2.80 kg (19.96). At 2.87 kg the same referral gives 20.2 m/s; 20.0 stays, because it is a margin setting about 21 % over the CFD’s trimmed stall estimate of 16.5 m/s at 2.85 kg (th-aeronautics, 2026-10-06), and the stall test sets the real value. The reason is the survey turn: TECS uses this value as its speed floor in turns (AIRSPEED_STALL x the square root of the load factor, AP_TECS.cpp), 22.1 m/s at 35 degrees of bank, so at the 21 m/s survey the turns fly faster. In th-testpilot’s paired S2 sets at 21 m/s (5 m/s gusts) the turn’s AoA peak came down on every seed, to 9.9 to 11.0 degrees (0.3 to 1.6 lower), for about 1 Wh per 100 ha, 0.5 m of height p95 and a cross-track p95 of 7 to 9 m. ROLL_LIMIT_DEG 30 raised one seed, and NAVL1_PERIOD 25 did not soften the capture in gusts. At a 20 m/s survey, 20.0 only cost energy (th-simulator’s S1 and S2). Do not set it below 19.2 without a check from th-aeronautics; after the stall test it can come down if the real stall is lower. The CFD shows a soft pitch band, exactly neutral from alpha 9.5 to 10 degrees. At 2.59 kg it is about 16.75 to 18.1 m/s in trimmed level flight, and 17.25 to 18.75 at 2.733 kg (jbsim-b3, 2026-10-03) (conservative row, jbsim-b3 and th-aeronautics, 2026-09-30; the untrimmed CFD lift gives lower speeds, because the up-elevon for trim costs lift). A CG at 270 is locally neutral in the band, and 265 to 266 keeps about 2.0 %. The band’s top, 18.1, is just above AIRSPEED_MIN 18; a 19.2 floor trimmed at about 7.2 degrees, below the band
AIRSPEED_MIN 18, Q_ASSIST_SPEED 18 Do not lower them before the flight stall test. 18 m/s was at the top of the soft pitch band at 2.59 kg (16.75 to 18.1 m/s, trimmed and level; th-aeronautics, 2026-09-29), and at 2.733 kg it is inside it (17.25 to 18.75; jbsim-b3, 2026-10-03): the assist acts only once the aircraft is in the band. Normal flight does not go there, since TECS’s floor is AIRSPEED_STALL 20.0. After the stall test, with the referred stall V_s: Q_ASSIST_SPEED must be at least V_s + 1 m/s, and the S2 survey in 5 m/s Dryden gusts, re-flown in SITL with the stall set to V_s, must not drop below Q_ASSIST_SPEED. The first check is the safety rule: the assist must act before the stall. The second is about cost, not safety: if S2 drops below Q_ASSIST_SPEED, the lift motors come in briefly in the worst gusts, which uses energy and unfolds the lift propellers. S2’s low point comes from TECS and the gusts at the 20 m/s survey command, about 2 m/s under it on every model so far (18.0 to 18.6 m/s), not from the stall. So at 20 m/s the second check passes or fails by about 0.1 m/s (th-testpilot’s rehearsal, 2026-09-29). If it fails, raise the survey speed. Ryan set it to 21 m/s until the flight stall test (2026-10-01), on th-simulator’s S2 at five paired gust seeds on the CFD drag: at 20 m/s the angle of attack reached 12.1 degrees (SITL’s stall) on one seed and the assist at 18 never acted first. Q_ASSIST_SPEED 19 cost 8 to 33 % more energy, with 3 to 12 assists a flight, and still reached 12.1 degrees. At 21 m/s the angle stayed at 11.9 degrees or less on every seed, with no assist and about 1 Wh less for each 100 ha. So Q_ASSIST_SPEED stays 18. The speed trigger acts with no delay: Q_ASSIST_DELAY applies only to the height and angle triggers (VTOL_Assist.cpp, 4c98c92)
Q_P_NE_POS_P 1.0, Q_P_NE_VEL_P 1.4, Q_P_NE_VEL_I 0.7 The first real hovers. These came from SITL dock landings (84 of 84 captured against 53 with Plane’s defaults, th-testpilot 2026-09-30; the 53 on the folding pusher’s 142 mm aft limit, so the fixed APC’s 279 mm can only raise it). Check the hover log for oscillation before any dock landing; lower them if the position rings
Rate gains, Q_A_RAT_YAW_P AUTOTUNE and QAUTOTUNE on the real aircraft. Before that, check the feed-forward gains on the first roll and pitch doublets: they assume the CFD elevon authority (0.186 roll), and VSPAERO predicts 0.273
BATT_LOW_MAH 4500, BATT_CRT_MAH 3000, BATT_LOW_VOLT 21.6, BATT_CRT_VOLT 20.4 The battery-reserve flight on the P50B (th-testpilot S5). Critical at 30 %: the final 39 s QLand in 8 m/s keeps 3.0 V a cell under load from that charge (th-simulator 22343fb). Low at 45 %: critical plus the return home from the survey’s far point in 8 m/s, with the back-transition and 60 s of hover (th-aeronautics, 11.4 to 13.0 %). The voltages are at the same charge as the capacity triggers, 45 % and 30 % at rest, and are sag-compensated (BATT_FS_VOLTSRC 1); the capacity triggers act first
GPS1_POS_* A measured antenna phase centre, if Waveshare or a bench test gives one. It matters: in SITL a GPS offset the autopilot did not know moved 5 m/s crosswind dock landings 189 to 323 mm forward, against -65 to +33 mm with it matched (th-simulator, 6338e28). Check X against the saddle drawing on the aircraft
Everything in the airframe files Calibration and bench checks on that airframe

Not in these files yet: the geofence (proposed, not decided). The flight scripts’ own parameters (LCK_, CGS_, TH_) exist only after the scripts first run, as the first-flight card says.

params/sdcard/th-clr-list.txt is the checklist gate’s list (tools/checklist_code.py list writes it; check_params.py fails when it is stale). It names the parameters whose values a checklist code covers: every name in the common, first-flight board and airframe files, except the names an overlay file sets (payload-*, pack-*, test-*) and the names the aircraft rewrites itself (AHRS_TRIM_X, AHRS_TRIM_Y, Q_M_THST_HOVER). After any change to those files, run checklist_code.py list, commit the list, and clear the hard checklists again, because their codes change. The same command writes params/sdcard/th-clr-codes.json: the four checklist codes for the published files (hex, and the decimal TH_CLR_n values), which the tick page shows at sign-off. A board whose gated values differ from the files has other codes, so a published code arms only a board that matches the files. check_params.py fails when the file is missing or stale.

params/sdcard/message-intervals-chan1.txt goes in the root of the LUX’s microSD card, not in APM/. At boot ArduPlane reads it and sends EXTENDED_SYS_STATE (message 245) every 500 ms on MAVLink channel 1, which is the LoRa radio on SERIAL2. The site node reads the landed state from it to set its throttle hold (docs/hangar.md). The node also asks for this message after each link-up, so the file is a second source, not the only one.

The file takes only <message id> <interval ms> lines. Do not add a comment line: ArduPlane reads it as message 0, the heartbeat, at an interval of 0. The channel number counts the serial ports set to MAVLink, USB first. If a board file changes which ports use MAVLink, the number can change. tools/check_params.py checks the number and the line format.

The source files this page describes, as they are in the repo.

params/terrahawk-common.parm · 268 lines · open · download
# TerraHawk flight parameters: common to every airframe and flight controller.
# Load order: this file, then the board file (board-*.parm), then the airframe file (airframe.parm).
# th-avionics owns this file. Every value names its reason; "provisional" means it changes after a
# named flight test. SITL evidence is in docs/agent-threads/test-battery/ and sim/terrahawk.parm.
# --- Frame (kit ArduPilot guide, step 2) ---
Q_ENABLE 1
Q_FRAME_CLASS 1
Q_FRAME_TYPE 1
# Hover thrust, a THRUST fraction (what CTUN.ThO logs and Q_M_HOVER_LEARN 2 learns), not the ESC command:
# 0.244 at 2.75 kg on the XING2 2809 (th-aeronautics 154ab6b); about 0.254 on the EMAX ECO II 2807 1300KV that Ryan
# chose on 2026-10-04 (th-aeronautics, from an estimated winding resistance). 0.38 here before 2026-10-01 was a command.
# It rises as the pack sags. AP_Motors scales the demand by lift_max = v(1 - e) + e v^2 (v = V / 25.2,
# e = Q_M_THST_EXPO), so the hover needs 0.254 / lift_max: 0.274 at 23.9 V, 0.32 at 21.6 V, 0.34 at 20.4 V
# (Thrust_Linearization.cpp at 4c98c92; on the XING2, 0.263, 0.30 and 0.33, and SITL learned 0.264, 07af754).
# 0.25 stays the start value: Q_M_HOVER_LEARN 2 learns the real one on the first hovers. On EMAX's own ECO II 2807
# sheet (58 mOhm, th-aeronautics 2d2917e) the prediction is about 0.242 at 2.85 kg on the 2-blade, about 0.275 on
# the 3-blade (c49c8d9).
Q_M_THST_HOVER 0.25
# Explicit, not left to the default. The lift props are the Gemfan F8041 3-blade (Ryan, 2026-10-08), which on a fresh
# 25.2 V pack would draw about 64 A a motor at full throttle (62.5 to 65.7), past EMAX's 52 A peak and the Lucid's
# 50 A continuous. 0.82 holds 49 A in every case th-aeronautics modelled (central cap 0.84; T/W about 3.6 at 2.85 kg;
# c49c8d9, docs/propulsion.md "The 3-blade F8041"). It was 0.95 for the 2-blade. On the thrust stand: start at 0.82
# and raise it in steps of 0.01 only while a motor stays under 49 A; never above what the stand shows.
Q_M_SPIN_MAX 0.82 # range-ok value-ok: under the documented 0.9 on purpose (current cap); the code takes any value
# Thrust curve for the F8041 3-blade on the EMAX 2807 (th-aeronautics c49c8d9: fit 0.31 full, 0.34 nominal; it was
# 0.43 for the 2-blade). AN ESTIMATE: fitted again on the thrust stand's run, and checked against the first hover log.
Q_M_THST_EXPO 0.33
Q_M_HOVER_LEARN 2
# --- Speeds (docs/flight-parameters.md; schedule decided 2026-09-27) ---
# AIRSPEED_MIN 20 (th-avionics, 2026-10-18): the forward transition ends when EAS passes it (quadplane.cpp:1522), and
# Fuselage C's side static ports over-read at the transition's low and negative alpha. On the static-port sensor model
# (SIM_ARSPD_CP 1, table to -5 deg) on the C model (th-testpilot b9b5632f, 1d51b2d9) 18 ended 19 % of transitions under
# 18 m/s true and 20 none. It also raises the fixed-wing floor and the approach speeds, so TECS_LAND_ARSPD is filed
# below. lift_check.lua v1.6.5 keys its near-assist margin and its spin floor on Q_ASSIST_SPEED, not this.
AIRSPEED_MIN 20
# RTL flies at AIRSPEED_CRUISE. On a low pack the pusher still has margin: at 20.1 V (31 % charge under
# load) the propulsion model gives about 12 N at full throttle at 22 m/s and 9 N at 27 m/s, against 4 to 6 N
# of drag. SITL's pusher gives much less at speed on a low pack (8.8 N at 22 m/s, 3.9 N at 27 m/s), because
# its momentum model loses thrust as airspeed squared (th-simulator, 2026-10-01), so a SITL speed limit on a
# low pack is not the aircraft's. To raise AIRSPEED_CRUISE, check the pusher against propulsion.py or a thrust
# test, not SITL, and re-fly S5 for the return energy.
AIRSPEED_CRUISE 22
# The fixed-wing landing approach speed, pinned: at -1 it would follow AIRSPEED_MIN (halfway to AIRSPEED_CRUISE), and the
# belly-landing lane is sized from it (th-hangar). Lane stops at +45 m or less in a 5 m/s tailwind (th-testpilot 5f21a1f0).
TECS_LAND_ARSPD 20
# TECS's throttle feed-forward: the throttle that holds AIRSPEED_CRUISE level on a full pack. 65 since 2026-10-20:
# on Fuselage C (c24b-w78) 22 m/s indicated level in calm air on a fresh pack takes 64.6 % (th-simulator's
# film_flights.py cruise-trim, eaf7758e; it was 58 % on the earlier CFD drag, fadf08c). Unset it is 45 %. With the
# pitot in use TECS finds the steady value itself: 58 against 65 on C made no difference (th-testpilot, thread
# 7fcfe781: S1 80.6 to 80.8 Wh both, S5 return 1,029 to 1,037 mAh both, TR lowest 19.1 both, no assists). It
# matters most with the pitot disabled (ARSPD_USE 0), when TECS sets the throttle from this value and the height
# error: the lowest true airspeed on the wing went from 21.3 to 22.5 m/s (S3), 22.1 to 23.3 (LN-tail3) and 17.5 to
# 18.3 (F11 assist circle, 0.3 to 0.7 m/s over C's trimmed stall at 58), no assists in 12 flights an arm. It is also
# the pusher's floor in a forward transition (already 84 to 86 %), and the FBWA pitch-down below it costs 0.1 m/s
# of sink at mid stick.
TRIM_THROTTLE 65
AIRSPEED_MAX 30
# A margin setting until the FBWA stall test (docs/stall-test.md), which sets it to the measured 1 g stall,
# referred, x 1.08. 20.0 (2026-10-01, was 19.2 = 17.8 x 1.08) is 9 % over SITL's 18.34 m/s stall at 2.733 kg
# and about 20 % over the CFD's 16.6 (th-aeronautics). TECS's turn floor is this x sqrt(load factor), 22.1 m/s
# at 35 deg: at the 21 m/s survey the turns fly faster, and the AoA peak in th-testpilot's gusty S2 turns
# came down on every paired seed (9.9 to 11.0 deg) for about 1 Wh per 100 ha. params/README.md has the rest.
AIRSPEED_STALL 20.0
Q_TRANSITION_MS 5000
# --- Assist (guide step 6, raised from the guide's 14 because the stall is higher) ---
Q_ASSIST_SPEED 18
# Angle assist: the lift motors come in when the bank is past ROLL_LIMIT_DEG + 5 AND the attitude error
# exceeds this (VTOL_Assist.cpp at 4c98c92). ArduPilot's default 30, lower than the manual's 45 (assembly
# manual and beginner's guide, "catches an attitude upset before it becomes a spin"; 2026-10-01): the CFD shows a
# wing-drop tendency near 17 deg AoA that can match the full elevon roll authority (th-aeronautics), and in
# a 35 deg turn 45 would act only past 80 deg of bank, 30 past 65. Survey turns stay inside the 40 deg envelope.
Q_ASSIST_ANGLE 30
# Mission legs at least 10 m above this; every forward transition starts at least 20 m above it.
Q_ASSIST_ALT 30
# --- Fixed-wing control (docs/cg.md: 25 degree elevon travel, MIXING_GAIN 0.75) ---
# Provisional: rate gains are ArduPilot's quadplane defaults scaled for the larger authority, not
# tuned. The real aircraft needs AUTOTUNE (fixed wing) and QAUTOTUNE (hover) before survey work.
MIXING_GAIN 0.75
PTCH_RATE_P 0.132
PTCH_RATE_I 0.091
# PTCH_RATE_FF: scaled for the CFD elevons (th-testpilot 2026-09-29: no verdict changes, bank tracking better). It
# matches today's plant: the doublet's pitch ratio (0.535 deg/s per us of elevon) is the quasi-steady
# c_m_deltae / c_m_q, and the SITL inertia rescales moved both by the same factor, so the ratio and the matched
# feed-forward held (th-testpilot 3cc1417, th-aeronautics, 2026-10-05; the 0.76 of 99080b6 is withdrawn). The flight
# retune (the first-flight card's pitch feed-forward step; named, not numbered, as steps get renumbered): PTCH_RATE_FF = 0.801 x 0.535 / measured ratio (0.43 / measured).
PTCH_RATE_FF 0.801
RLL_RATE_P 0.060 # range-ok: below the documented 0.08 on purpose, scaled for 2.34x elevon authority
RLL_RATE_I 0.091
RLL_RATE_FF 0.311 # 0.236 / 0.758: roll authority from the up-side CFD elevon slope (th-aeronautics 1bcc0b1)
ROLL_LIMIT_DEG 35
# TECS: pitch capped near the stall angle; speed before height (home-leg stall fix, 2026-09-28).
# The flight script lowers the weighting to 1 below Q_ASSIST_ALT + 5 m and restores it above
# Q_ASSIST_ALT + 15 m, without saving it, so this stored value is the protected state. The cap is 12 deg
# absolute: the row is relative to PTCH_TRIM_DEG (below), so it reads 7 with the trim at 5.
TECS_PITCH_MAX 7
# Pitch trim (th-avionics, 2026-10-18; th-testpilot dd6577cd). Plane adds PTCH_TRIM_DEG to every fixed-wing pitch
# demand after the limits (Attitude.cpp:244), so FBWA's centre stick holds 5 deg, about level cruise on the C model
# (AUTO level cruise flies 5.3 deg at 23-24 m/s true, ESTIMATE). At 0 a hands-off FBWA transition held the nose near
# 1 deg through the lift ramp and lost up to 10.7 m (126 SITL transitions on c24b-w78); at 5, 1.0 m at most. Set it
# from the first flight log: the pitch in level cruise. The pitch limits are relative to it, so each row below
# states its absolute angle, the one flown before the trim. lift_check.lua subtracts it from its own limits (v1.6.6).
PTCH_TRIM_DEG 5
PTCH_LIM_MAX_DEG 15 # absolute 20, the default
PTCH_LIM_MIN_DEG -30 # absolute -25, the default
TECS_LAND_PMAX 5 # absolute 10, the default
LAND_PITCH_DEG -5 # absolute 0: the lane flare as flown (peaks 1.4 to 4.6 deg)
# Elevon trim is learned in flight on the first flights, then copied into the airframe file.
# th-simulator, 2026-09-28: a transition started 1.4 deg of elevon out of trim sagged 12 m deeper.
SERVO_AUTO_TRIM 1
TECS_SPDWEIGHT 2
# --- Hover and landing ---
Q_A_RAT_YAW_P 0.3
Q_WVANE_GAIN 1
Q_WVANE_LAND 0
Q_LAND_FINAL_ALT 3
Q_LAND_FINAL_SPD 0.3
# Pilot's fastest descent in QHOVER and QLOITER (full-down stick). ArduPlane's 0 uses Q_PILOT_SPD_UP, 2.5 m/s, too fast
# for a hand landing. 1.5 (th-testpilot, 2026-10-12, 30 m full-down, calm and 5 m/s gusts): touchdown 2.0 -> 1.0 m/s,
# peak sink 2.2 -> 1.2, roll 4.2 deg at most. The GPS-loss guard's QHOVER hands the landing to the pilot.
Q_PILOT_SPD_DN 1.5
# Horizontal position hold in VTOL, firmer than Plane's defaults (0.5, 0.7, 0.35; AC_PosControl.cpp), near
# Copter's. SITL dock landings, same 84 gusty seeds (th-testpilot, 2026-09-30): 84 of 84 captured against 53
# with the defaults (scored on the folding pusher's 142 mm aft limit; the fixed APC's 279 mm can only raise the
# 53, not the 84), no ringing (roll and pitch peaks unchanged). A faster final descent did nothing; the
# defaults let a gust push the aircraft aft faster than they pull it back. SITL STARTING VALUE: check the
# first real hover's log for oscillation before any dock landing (first-flight card).
Q_P_NE_POS_P 1.0
Q_P_NE_VEL_P 1.4
Q_P_NE_VEL_I 0.7
# Q_FWD_THR_USE stays 0 (ArduPlane's default, not set here). 1 holds a windy hover with the pusher instead of a
# nose-down tilt, but ArduPlane keeps the pusher running to contact, which pushed dock landings up to 0.34 m
# forward. With the RTL mission ending it bought only a flatter hold (11.6 against 19.7 deg) and 0.8 A, at the
# same energy (th-testpilot, 2026-10-01). Use 1 only together with sim/scripts/fwd_thr_cutoff.lua.
# Bits 5, 12, 16, 17: QRTL on RC loss in a Q mode, no approach circle, land reposition. Bit 0 (Level
# Transition) off. docs/clear-to-launch.md, "No low approach circle". Bit 15 (throttle landing control)
# is off since 2026-09-30: with it, a throttle stick left above 70 % (cruise) turned a battery-failsafe
# landing into a climb (quadplane.cpp landing_descent_rate_ms; th-testpilot F6: 61 to 640 m, then impact),
# and a QLAND entered with the stick high then held height at mid stick (F7). With 196640 the F6 flight
# landed 0.1 m from home with the stick at 82 %. To stop a descent, select QLOITER or QHOVER.
# Bit 12 (since 2026-10-19): no speed assist on a synthetic airspeed, only on a real sensor (VTOL_Assist.cpp:94).
# Without a sensor, or after ArduPilot disables a failed one, assisted flight sets fly_forward false, the EKF drops
# its wind states and the estimate falls back to DCM's (about 13 against 21 true): under Q_ASSIST_SPEED, so assist
# restarted every 5 s through cruise with the lift motors running (th-testpilot 83567e78). With bit 12: no loop and
# no height assist on three motors, and with the sensor nothing changes (cc6d5d92). Height and angle assist stay.
Q_OPTIONS 200736
# Returns: RTL flies home as a plane and hands over to QRTL near home. With Q_OPTIONS bit 16 a QRTL
# hovers home from any distance, so far returns are RTL and QRTL is used only near the pad.
Q_RTL_MODE 1
Q_RTL_ALT 50
RTL_ALTITUDE 80
STICK_MIXING 1
# --- Modes, channel 5: QSTABILIZE, QHOVER, QLOITER, AUTO, RTL, FBWA (slot 5 is RTL, never QRTL) ---
FLTMODE_CH 5
FLTMODE1 17
FLTMODE2 18
FLTMODE3 19
FLTMODE4 10
FLTMODE5 11
FLTMODE6 5
# Stick trims pinned: in a VTOL landing ArduPlane adds a velocity from off-trim sticks to the target.
RC1_TRIM 1500
RC2_TRIM 1500
RC4_TRIM 1500
# --- Failsafes (docs/clear-to-launch.md, remote piloting table) ---
# RC failsafe on; the aircraft always has an RC source (handset, or the site node's CRSF link).
THR_FAILSAFE 1
FS_SHORT_ACTN 0
# The short failsafe acts after RC_FS_TIMEOUT (default 1 s); this ArduPlane has no FS_SHORT_TIMEOUT.
RC_FS_TIMEOUT 1
# Backup RC over the LoRa link, way C (gps thread d8e1d6c; Ryan's form in docs/hangar.md): the operator's GCS sends
# RC_CHANNELS_OVERRIDE at 5 Hz as MAV_GCS_SYSID. Measured through the full-load link (th-mlrs-xiao run 4): 99.87 %
# arrived, worst gap 452 ms. Each overridden channel returns to ELRS this long after the last override: 1.5 s is
# three times the worst gap, and hands back to a live ELRS sooner than the 3 s default if the dock link drops in the
# middle of a takeover. With ELRS also lost, the RC failsafe starts RC_FS_TIMEOUT after the last override anyway.
# Never set RC_OPTIONS bit 14: it reads the receiver's frozen sticks after an ELRS loss and cancels the backup.
RC_OVERRIDE_TIME 1.5
FS_LONG_ACTN 0
FS_LONG_TIMEOUT 5
FS_GCS_ENABL 1
# Battery: the 6S2P Molicel P50B, 10 Ah, 216 Wh (Ryan, 2026-10-01). Low starts RTL, critical lands.
# The capacity triggers act first, with no delay. Critical at 3000 mAh left (30 %, Ryan, 2026-10-01): a 39 s
# QLand from 30 m holds 3.0 V a cell under load from 25 % in 5 m/s and 30 % in 8 m/s; 20 % sags to 17.6 V
# (th-simulator 22343fb; its 45 mOhm is a mid-charge figure, so 30 % keeps 0.5 V of margin). Low at 4500
# (45 %): critical plus the RTL home from the survey's far point in an 8 m/s headwind, with the back-
# transition and 60 s of hover (11.4 % at 1.5 km, 13.0 % at 2.5 km, th-aeronautics planning figures),
# rounded up to 5 %. The same 45 % also pays for the dock's landing hold: up to 2 minutes of hover at about 5.5 % a
# minute, until the hold leaves at CTL_RESERVE_PCT 36 % (clear-to-launch.md). On Fuselage C, S5 (1.5 km into 8 m/s,
# th-testpilot 7fcfe781) reaches the hover over home at 41.9 % and lands at 34.7 %; at its 34 A hover, after
# about 19 s of positioning, about 45 s of hold is left; at 40 % none (2026-10-22). Lower this only with the hold
# budgeted. The voltages are the backstop for a wrong mAh count, sag-compensated (BATT_FS_VOLTSRC
# 1), at the same charge: 21.6 V is 45 % and 20.4 V is 30 % at rest on the P50B table (th-simulator). A
# 27 A hover reads 1.2 V lower, so a raw trigger would fire early. Was 30 / 15 % until 2026-10-01.
# th-testpilot S5 checks it: the low trigger fires at the far point and the aircraft lands at home before
# critical. Revisit with real logs of the sag near 30 %. The Tattu: params/pack-lipo-4500.parm.
BATT_CAPACITY 10000
BATT_LOW_MAH 4500
BATT_CRT_MAH 3000
BATT_FS_VOLTSRC 1
BATT_LOW_VOLT 21.6
BATT_CRT_VOLT 20.4
BATT_FS_LOW_ACT 1
# Critical: QLand where it is (4). Loiter to QLand (6) was flown against it (th-testpilot F7 pair,
# 2026-09-30, trigger at 1.58 km and 67 m): 6 went to QLAND 0.1 s after entering, so both flew the
# same descent, 4.3 and 4.7 Wh, landing 259 and 260 m from the trigger. No benefit seen, so 4 stays.
BATT_FS_CRT_ACT 4
# Geofence (not enabled yet; FENCE_ENABLE stays at its default 0): FENCE_OPTIONS 0, set now so that it is
# already right on the day the fence is enabled. Plane's default (bit 0, disable mode
# change) refuses every mode change during fence recovery except a landing sequence, and a battery
# failsafe is not one, so the critical QLand is denied (system.cpp at 4c98c92; th-testpilot F9,
# 2026-09-30: "Mode change to QLand denied, in fence recovery").
# This row was only a comment until 2026-10-01; a fence turned on without it kept the lock, and in
# th-testpilot's F14 the guard's QLand was denied for 15 s while RTL climbed from 15 to 82 m at critical.
FENCE_OPTIONS 0
Q_M_BAT_VOLT_MAX 25.2
# Lowest voltage under load seen in SITL near empty is about 17.2 V (th-simulator): compensate down to 17.0.
Q_M_BAT_VOLT_MIN 17.0
# --- Sensors ---
ARSPD_USE 1
# ARSPD_OPTIONS 9 (bits 0 and 3; since 2026-10-19): ArduPilot disables a pitot that disagrees with GPS less the EKF wind
# (bit 0) or with the EKF (bit 3), and does NOT enable it again in flight (bit 1 off). With bit 1 the EKF can learn a wind
# that explains a stuck reading and the health check then re-enables the stuck pitot: in SITL that handed TECS the stuck
# value about 45 s after the disable, and the flight lost 8.8 to 11.2 m before the stuck guard caught it again
# (th-testpilot 4fa36c21, 184610e8). At 9 the worst was 3.1 m. The cost: a good pitot rejected after a sudden wind change
# stays off for the flight; the aircraft flies on synthetic airspeed (no speed assist, Q_OPTIONS bit 12) and the card
# says to circle and land. The GPS-loss fall-back re-enable (AP_Airspeed_Health.cpp:44-50) does not depend on bit 1.
ARSPD_OPTIONS 9
# Airspeed fault cover has two layers (th-simulator, 2026-10-01, steps from 25.7 to 37.3 m/s at a true 21): the
# EKF stops using a pitot that reads more than about 7 m/s high within 3.1 to 3.4 s (the AHRS then gives the
# synthetic airspeed), and arspd_stuck_guard.lua covers smaller errors, which the EKF accepts. Bit 2
# (DontDisableAirspeedUsingEKF) would remove the first layer: keep it clear. The default is 0; this row keeps it so.
# Bit 1 (DisableDCMFallbackVTOL) stays clear here too: set from boot it lets a quadplane arm before the EKF has a GPS
# origin (SITL armed at 10.7 s, origin at 19.4; th-avionics 2026-10-15). gps_loss_guard v2.4 sets it in flight for a
# GPS loss only, with param:set (never saved), and puts 0 back at the fix or the disarm.
AHRS_OPTIONS 0
# EKF source set 2 is the no-GPS set gps_loss_guard v2.4 selects for a GPS loss (a jammed GPS's short "locks" with wrong
# velocities otherwise reach the EKF). Its defaults already take no GPS position or velocity and the barometer for
# height; its yaw defaults to None, so the compass is set here. EK3_SRC_OPTIONS 0 (FuseAllVelocities off, the default)
# so set 2 really drops the GPS velocity. Neither row changes a flight that never selects set 2.
EK3_SRC2_YAW 1
EK3_SRC_OPTIONS 0
# Either line order at the nose joint reads correctly (the pitot lines disconnect with the nose).
ARSPD_TUBE_ORDR 2
# GPS: u-blox NEO-F9P (Waveshare Q43); L5 health taken from L1 while GPS L5 is pre-operational.
GPS_DRV_OPTIONS 32
GPS1_RATE_MS 100
# Sensor positions from the design CG point (0, 270, -5.6), body axes X fwd, Y right, Z down: INS_POS1 (board file)
# and every GPS_POS share this one origin (th-avionics, th-telemetry, th-blender W52 6ac8c58e).
# The Q43 on Fuselage C v2.4B: base centre (0, 300, 26.7) on the copper floor, dome 37.0 mm; phase centre at two-thirds
# of the dome, Z 51.4, an ESTIMATE within about 10 mm (th-telemetry 20261010T185835Z); Waveshare publishes no phase
# centre. SITL's SIM_GPS1_POS matches it (params_sync).
GPS1_POS_X -0.030
GPS1_POS_Y 0
GPS1_POS_Z -0.057
# The IMU offset from the same origin, INS_POS1, is in each board file: it belongs to the board and its mount
# (moved from this file 2026-10-04). Without it the IMU is the origin and GPS1_POS is off by the IMU's offset.
# --- Logging and scripting ---
# Raw IMU batch logging for the first flights' vibration spectrum (IMU filters and rev B island).
INS_LOG_BAT_MASK 1
SCR_ENABLE 1
params/board-luxf765.parm · 268 lines · open · download
# TerraHawk flight parameters: Lumenier LUX F765 NDAA, firmware target LumenierLUXF765-NDAA (in ArduPilot at 4c98c92).
# The first-flight board (Ryan, 2026-10-04), in place of the NxtPX4v2; our own FC follows. Load after
# terrahawk-common.parm, then the airframe file. Never load lift-4in1.parm with it (that file is the 6C's).
# Pinout: ArduPilot's hwdef/LumenierLUXF765-NDAA (hwdef.dat, README.md). STM32F765, 2 MB flash (Lua runs), ONE IMU
# (ICM42688), BMP280 barometer, no compass, 8 UARTs, 12 PWM outputs, one CAN port (GH 1.25 4-pin), 30.5 x 30.5 mm M3
# holes, 38 x 38 mm, 9 g. The board does not boot ArduPilot without a microSD card fitted (Lumenier's manual).
# The output map keeps the NxtPX4v2's 1 to 8, so the airframe files' elevon rows (SERVO5/6) are unchanged.
# Mounting (th-blender 8ecb409): flat, MCU and USB-C face up, the USB-C / microSD edge FORWARD so the card can be
# reached through the front hatch opening; GPS and HD VTX plugs to starboard, the ESC ports aft, CAN to port. No
# source shows the board's arrow, which is what AHRS_ORIENTATION refers to (the IMU's hwdef ROTATION_YAW_180 is
# relative to it). 0 is provisional: the bench tilt sets it. With 0 loaded, raise the nose about 30 degrees and read
# the GCS: pitch up = 0 (arrow forward); roll right = 2 (Yaw90, arrow to starboard); pitch down = 4 (Yaw180, arrow
# aft); roll left = 6 (Yaw270, arrow to port). Then do the accelerometer calibration again.
AHRS_ORIENTATION 0
# No safety button.
BRD_SAFETY_DEFLT 0
# One IMU, so one EKF3 lane. Set, not left to the default (3), so a board change cannot add a lane silently; SITL's
# SIM_IMU_COUNT follows the bit count (th-simulator).
EK3_IMU_MASK 1
# IMU offset from the design CG, kit (0, 270, -5.6), body axes (X forward, Y right, Z down). The ICM-42688-P at
# assembly (-2.30, 297.70, 16.10) on Fuselage C v2.4B (th-blender W55 4530460e: Z = the drawn Avionics Plate top 11.00 + standoff 3.00 + PCB 1.60 + 0.5; lux_imu_c() now reads Z from the drawn plate, where the old 18.00 used B's point lowered 2.25): 27.7 mm aft, 2.3 mm to starboard (assembly -X). ESTIMATE: no
# open CAD; the chip's place is scaled from a retail photo (+-1.5 mm), and Z rests on a 3 mm standoff, plate top
# to PCB underside, also estimated (each mm taller makes Z 0.001 more negative; measure on the bench). It does not depend on
# the arrow. One IMU: the LUX has no INS_POS2_* parameters at all (bench, 2026-10-15), so this file sets none.
INS_POS1_X -0.0277
INS_POS1_Y 0.0023
INS_POS1_Z -0.0217
# --- Outputs: M1-M4 (TIM2), M5-M8 (TIM4), S1-S2 (TIM3, with the LED pad 13), S3-S4 (TIM1). Every output in a group
# --- uses the same protocol, so no servo shares a group with a DShot motor.
# Lift motors 1-4 on the 4-in-1 through the ESC #1 port (TBS Lucid 60A 3-8S, AM32), DShot300, bidirectional DShot for RPM
# (the board's README: outputs 1-4). Check on the bench that all four RPM values arrive before the first hover.
# Output n drives the Lucid's ESC channel n (a straight cable); the functions put each channel on the motor its
# pads face. Fuselage C v2.4B (th-blender W36, 2026-10-14): the 4-in-1 stands on edge ahead of the Spar Box,
# component side aft, battery tabs to STARBOARD, so the board's x = 42 edge is the UPPER one. TBS manual p. 97
# (vendor/esc/NOTES.md frame): channels 1 and 2 on the x = 42 edge, 3 and 4 on the x = 0 edge, 1 and 3 at the tab
# end. So channels 1 and 3 (pad groups x1 y17, x0 y17) leave by the starboard channel mouth, 2 and 4 (x1 y38.5,
# x0 y38.5) by the port one. Front or rear is chosen beyond the mouth: the UPPER edge's triple goes to the FRONT pod.
# ArduPilot quad X: motor 1 front starboard, 2 rear port, 3 front port, 4 rear starboard. From the drawing, not the
# board itself: check the silkscreen, then the motor test (A front starboard, B rear starboard, C rear port, D front
# port) before props. Was 35, 34, 33, 36 for the Lucid flat in the belly (2026-10-05), which on edge would drive
# two motors from the wrong side.
SERVO1_FUNCTION 33 # ESC ch1, pads x1 y17, upper edge, starboard end: front starboard
SERVO2_FUNCTION 35 # ESC ch2, pads x1 y38.5, upper edge, port end: front port
SERVO3_FUNCTION 36 # ESC ch3, pads x0 y17, lower edge, starboard end: rear starboard
SERVO4_FUNCTION 34 # ESC ch4, pads x0 y38.5, lower edge, port end: rear port
Q_M_PWM_TYPE 5
SERVO_DSHOT_ESC 1
SERVO_BLH_BDMASK 15
SERVO_BLH_POLES 14
# M5-M8 (TIM4), all PWM at the servo rate, on the ESC #2 port's signal pins or its pads: elevons on 5 and 6
# (reversal and endpoints in the airframe file), pusher ESC on 7. Output 8 is empty on the first build (Ryan,
# 2026-10-19, th-main 54bfe073): the Survey3W does not fly first. Its rows are below as comments.
SERVO5_FUNCTION 77
SERVO6_FUNCTION 78
SERVO7_FUNCTION 70
SERVO8_FUNCTION 0
# Pusher ESC: DYS Aria 70A 3-6S AM32 (Ryan, 2026-10-04; cart row 5): 70 A continuous, 80 A burst, no BEC, PWM input
# on this output (its timer group stays PWM). Before it flies, set in the AM32 configurator
# (esc-configurator.com, through a USB linker on the signal wire or the flight controller's ESC pass-through):
# Brake on stop: off. The fixed APC 10x7E freewheels at zero throttle; SITL will fly its windmill curve.
# Low voltage cutoff: off. The battery failsafes in the common file are the only low-voltage action.
# Motor direction: so the pusher blows aft; motor poles: 14 (check the ECO II's count).
# Servo low and high thresholds: 1000 and 2000 us, to match SERVO7_MIN and SERVO7_MAX.
# Temperature limit: disabled. Above it AM32 cuts the duty to about a quarter at once (main.c), a pusher loss.
# Bench: the motor is still at 1000 us and starts just above it. AM32's defaults are brake off and cutoff off, but read
# them back in the configurator: do not trust the defaults.
SERVO7_MIN 1000
SERVO7_MAX 2000
SERVO7_TRIM 1000
# No camera on the first build: the trigger is off and the S2 pad (output 10) is unused. These rows stay here, gated,
# and not in an overlay: an overlay's names leave the checklist gate, and the empty outputs are worth gating.
CAM1_TYPE 0
SERVO10_FUNCTION 0
# When the Survey3W flies (Ryan's call; docs/survey3.md), replace the three rows above and SERVO8_FUNCTION with these,
# reboot (CAM1_TYPE), and clear the checklists again. The exposure feedback goes on the S2 pad, GPIO 59, as an input.
# SERVO8_FUNCTION 10
# CAM1_TYPE 1
# CAM1_SERVO_ON 2000
# CAM1_SERVO_OFF 1000 (idle: never 1500, which puts the camera into USB transfer mode)
# CAM1_DURATION 0.1
# CAM1_INTRVAL_MIN 1.6
# SERVO10_FUNCTION -1
# CAM1_FEEDBAK_PIN 59 (GPIO(59) is PB1, the S2 pad, PWM 10, in hwdef/LumenierLUXF765-NDAA)
# CAM1_FEEDBAK_POL 1
# S1 (output 9): reserved for the belly cargo-door release servo, which is NOT in the first build (th-main: the bay
# is closed by the Payload Cover (Survey3W); the door is a later build and Ryan will revise its actuation). So the
# gripper is off and S1 has no lead. When the door is built: SERVO9_FUNCTION 28, GRIP_ENABLE 1, GRIP_TYPE 1, and
# GRIP_GRAB, GRIP_RELEASE and GRIP_NEUTRAL from the bench (NEUTRAL equal to GRAB, so the door stays shut at boot),
# GRIP_AUTOCLOSE 0; close the port half (+X, no lip) by hand first, then the starboard half (-X, with the lap lip),
# then command GRAB (th-blender, 2026-10-04). The 6C file keeps the rows as they will be. cg_slider.lua (v1.7) then
# rebalances after a release from the same gripper output (CGS_GRIP_FN 28); the payload limits are in the 6C file.
# Group S1-S2 is servo-only (S2 is a GPIO input; the LED pad 13 shares TIM3 and stays unused).
SERVO9_FUNCTION 0
GRIP_ENABLE 0
# S3 (output 11, GPIO 60): the video switch's "ON" input. On at boot. Not the board's CAM_CTRL pin (GPIO 82, RELAY3's
# default), which is wired for an analog camera's OSD control.
SERVO11_FUNCTION -1
RELAY2_FUNCTION 1
RELAY2_PIN 60 # value-ok: GPIO(60) is PE9, the S3 pad (PWM 11), in hwdef/LumenierLUXF765-NDAA
RELAY2_DEFAULT 1
# S4 (output 12): the CG slider's continuous-rotation servo (Ryan, 2026-10-06), driven by the flight script
# (sim/scripts/cg_slider.lua) as Script1. SERVO12_TRIM is the servo's stop pulse, set on the bench: an expired script
# override returns the output to TRIM, so the servo stops if the script does. Harmless with no servo fitted.
SERVO12_FUNCTION 94
SERVO12_MIN 1000
SERVO12_MAX 2000
SERVO12_TRIM 1500
# --- Serial ports (SERIAL_ORDER OTG1 USART1 UART5 USART3 UART8 USART2 UART4 UART7 USART6) ---
# SERIAL1 (USART1, TELEM1): spare since the SiK went (Ryan, 2026-10-04).
SERIAL1_PROTOCOL -1
# SERIAL2 (UART5, TELEM2): the telemetry radio, an SX1262 LoRa module with MAVLink firmware (mLRS class; Ryan,
# 2026-10-04, th-telemetry), in place of the SiK pair. It carries the GCS link, RTCM (the one RTCM link) and SITE_CLR.
# mLRS v1.4 at 31 Hz: 2,000 B/s up, 2,562 B/s down at 915 MHz (th-telemetry, gps thread 20261004T130536Z). 115200 at
# both ends, TX and RX only (UART5 has no flow-control pins). The aircraft unit sends RADIO_STATUS ("Rx Snd RadioStat
# = ardu_1"), so ArduPilot slows its streams to what the link can carry.
# The radio (XIAO ESP32-S3) sends its ROM boot text on its TX at every power-up; MAVLink skips it, so a burst of bad
# bytes on SERIAL2 at power-up is that, not a fault (th-telemetry, 2026-10-05).
SERIAL2_PROTOCOL 2
SERIAL2_BAUD 115
# Stream rates for this link. MAVn_ counts MAVLink ports in serial order, USB (SERIAL0) first, so this port is MAV2_
# only while SERIAL1 is not MAVLink: give SERIAL1 MAVLink and these rows must become MAV3_ (GCS_Common.cpp, the
# backends are created by find_serial(MAVLink, i)). About 1,770 B/s, 69 % of the downlink, leaving about 790 B/s for
# GCS traffic; the uplink keeps 61 % after MSM4 RTCM, SITE_CLR and the wind (th-telemetry). Every stream is set, none
# left to its default. PARAMS 0 does not stop a parameter download: ArduPilot streams PARAM_VALUE at 10 Hz anyway when
# a GCS asks (set_ap_message_interval), limited by the link. Load parameters over USB on the bench.
MAV2_EXTRA1 4
MAV2_EXTRA2 4
MAV2_POSITION 2
MAV2_EXT_STAT 1 # INTERFACE: the dock reads SYS_STATUS (3D_MAG present and not healthy) and GPS2_RAW (no fix) from this group at 1 Hz (hangar.md 89dd3c56); tell th-hangar before changing it
MAV2_EXTRA3 1
MAV2_RC_CHAN 1
MAV2_RAW_SENS 0
MAV2_RAW_CTRL 0
MAV2_PARAMS 0
MAV2_ADSB 0
# Bit 2, Ignore Streamrate: a GCS cannot raise these rates when it connects.
MAV2_OPTIONS 4
# SERIAL3 (USART3, GPS port): Waveshare Q43 NEO-F9P. The GPS port also carries the board's only I2C (I2C3), so the
# Q43's compass rides its cable, and the MS4525DO shares it through the Y lead (parts list row 9f).
SERIAL3_PROTOCOL 5
SERIAL3_BAUD 230
GPS1_TYPE 2
# BACKUP ONLY, commented out: the Q43-compatible module with a Quectel LC29H (th-telemetry, gps thread d55a2ca), in case
# the Q43 cannot be bought. It speaks NMEA, so GPS1_TYPE 5. ArduPilot's NMEA driver (4c98c92) does not configure the
# receiver: set its output rate (10 Hz on an RTK 10 Hz variant), its messages (GGA, RMC, VTG at least) and its baud with
# Quectel's PAIR commands on the bench, saved in the module, and match SERIAL3_BAUD to it. RTCM injection works as with
# the Q43 (the backend writes it to the port). The driver reads no accuracy from Quectel (it parses GST nowhere; only
# other vendors' PHD, KSXT and AGRICA carry accuracy), so the EKF falls back to EK3_POSNE_M_NSE (0.5 m) and
# EK3_VELNE_M_NSE whatever the fix, takes no GPS vertical velocity (the baro carries it), and the GPS speed-accuracy
# checks do not run. RTK fixed and float still come through from GGA's quality field. That flies without EKF changes;
# measure the lag (driver default 0.2 s) on the bench and set GPS1_DELAY_MS. GPS_DRV_OPTIONS and GPS1_RATE_MS in the
# common file are u-blox settings and do nothing here. Its QMC5883P sits on the module's underside turned 180 deg in
# KiCad's frame (th-eda; axes checked against QST's datasheet figure 2, no 90 deg term), connector forward like the
# Q43's (th-blender, Fuselage C v3), so the chip reads (v.y, -v.x, v.z) of the body field. ArduPilot's driver first
# applies its own external default, ROTATION_ROLL_180 (HAL_COMPASS_QMC5883P_ORIENTATION_EXTERNAL, AP_Compass_QMC5883P.h),
# then COMPASS_ORIENT (AP_Compass_Backend::rotate_field), so the row is 10, ROTATION_ROLL_180_YAW_90 (th-telemetry
# caught it; a connector aft would be 14, ROLL_180_YAW_270). A bottom-mounted chip always needs a 180 deg roll or
# pitch term on top of the driver's; a pure yaw cannot fit it. The bench
# compass calibration checks and fixes it anyway (COMPASS_AUTO_ROT default 2).
# COMPASS_ORIENT 10
# GPS1_TYPE 5
# SERIAL3_BAUD 460
# Its phase centre is at the patch face, about 15 to 20 mm lower than the Q43 helix's (an estimate, th-telemetry): it
# needs its own GPS1_POS_Z here, overriding the common file's Q43 value (X and Y are the same seat). th-telemetry
# sends the value with the Fuselage C GPS_POS1 recompute; until then the row below is a placeholder.
# GPS1_POS_Z (pending)
# SERIAL4 (UART8, GPS2; the underside pads TX8, RX8, 5V, GND beside the CAN pads): the SEQURE M10-25Q (u-blox M10 class,
# L1, with a QMC5883L compass on I2C) as GPS2 (Ryan, 2026-10-15; th-telemetry docs/agent-threads/gps 3a4d0560, 20261015T211642Z).
# Its compass leads join the GPS port's I2C3, the board's only I2C, keep that branch short (the MS4525DO shares it).
# The Q43 with RTK stays primary: GPS_AUTO_SWITCH 4 uses GPS1 whenever it has a 3D fix or better, and the M10 only when
# the Q43 itself loses 3D (receiver, cable, antenna). Never 2 (Blend): it would mix a centimetre RTK solution with a
# metre-class L1 one. Not 1 (UseBest): when the Q43 loses only RTCM on a LoRa fade, UseBest weighs two 3D receivers by
# satellites and can hop between them, a position step each time. A switch to the M10 moves the position by up to a few
# metres; the landing hold still needs RTK fixed from the Q43 for the pad (clear-to-launch.md). Against jamming or
# spoofing GPS2 adds nothing: both are L1. On arrival, confirm the receiver (UBX-MON-VER) before trusting the rows.
SERIAL4_PROTOCOL 5
SERIAL4_BAUD 230
GPS2_TYPE 2
GPS2_RATE_MS 100
GPS_AUTO_SWITCH 4
GPS_PRIMARY 0
# The M10's antenna on the station Ryan picked, the old SEN0697 hanger holes (th-blender W52.1 a5bd7623): assembly X -3,
# Y 409. ArduPilot measures every GPS_POS and INS_POS from the same body-frame origin, here the design CG point
# (0, 270, -5.6) that INS_POS1 is set from: X = -(Y - 270), Y = -(X - 0), Z = -(Z + 5.6), in metres (assembly X port, Y aft,
# Z up; body X forward, Y right, Z down).
GPS2_POS_X -0.139
GPS2_POS_Y 0.003
GPS2_POS_Z -0.053
# SERIAL5 (USART2): the 4-in-1's telemetry on the ESC #1 port's TLM pin (voltage, current, temperature, RPM).
SERIAL5_PROTOCOL 16
SERIAL5_BAUD 115
# SERIAL6 (UART4, HD VTX port): MSP DisplayPort OSD for the Walksnail Avatar HD Moonlight (Ryan, 2026-10-06), on the
# port's TX4 and RX4. Do NOT power the VTX from the port's pin 1 (the board's 9 to 10 V, 2 A rail): the Moonlight
# draws about 1.7 A at 10 V (th-main), too near that rail's limit and the 2.0 A switch's. It takes 12 V from its own
# BEC (Matek BEC12S-PRO, cart TH-O-6) through the video switch. Not VBAT: a full pack is 25.2 V, the VTX's maximum,
# with no margin for regen spikes. Pin 6 is RX7, the receiver's line: leave it unconnected.
SERIAL6_PROTOCOL 42
SERIAL6_BAUD 115
OSD_TYPE 5
# The board's defaults.parm sets OSD_TYPE2 5, a second DisplayPort OSD; cleared so one OSD drives the goggles. The
# board's analog OSD chip (AT7456E) is not used.
OSD_TYPE2 0
# Fonts: ArduPilot's own (MSP_OPTIONS 0) with the ArduPilot font selected in the goggles. If the goggles show wrong
# symbols, set bit 2 (EnableBTFLFonts) and select the Betaflight font there instead. Never set bit 4
# (ForceVTXHighPower): it reports armed to the VTX at all times, so the Moonlight's Standby Mode (25 mW until it
# reads the armed bit in MSP_STATUS) would run at flight power on the pad (th-telemetry, th-aeronautics, 2026-10-06).
# No VTX_ rows: Standby Mode needs only the armed bit, which ArduPilot sends on this port.
MSP_OPTIONS 0
# SERIAL7 (UART7, receiver port: 5 V, GND, RX7, TX7): ELRS receiver, CRSF (the README: bidirectional RC here).
SERIAL7_PROTOCOL 23
# The CRSF driver sets its own 416 kBaud; 115 is the conventional value here.
SERIAL7_BAUD 115
# RC from CRSF only (bit 9). The default 1 also accepts RADIO_RC_CHANNELS from the LoRa link (MAVRadio, bit 16;
# compiled in on boards with more than 1 MB of flash). Without RC_OPTIONS bit 10 the first protocol to arrive is
# kept for the whole boot (AP_RCProtocol::should_search), so an mLRS receiver that sends RC channels before the
# ELRS link is up (transmitter off at power-up, the dock's mLRS transmitter on) would become the RC source and
# CRSF would be ignored until a reboot (th-telemetry, 2026-10-06).
RC_PROTOCOLS 512
# SERIAL8 (USART6, underside pads TX6/RX6): the CG slider's position sensor, a XIAO RP2040 reading an AS5600
# (hw/cg-slider), "$CGP" lines at 20 Hz, read by the flight script (scripting serial instance 0).
SERIAL8_PROTOCOL 28
SERIAL8_BAUD 115
# --- Airspeed: MS4525DO on I2C (Ryan keeps it for first flights, 2026-10-04), bus index 0 (I2C_ORDER I2C3) ---
ARSPD_BUS 0
ARSPD_TYPE 1
# --- Rangefinder: none on this board's base build. The TFmini-S is an optional payload: params/payload-tfmini.parm
# --- sets it when its cover is fitted. These rows clear it when that file is not loaded.
RNGFND1_TYPE 0
RNGFND_LANDING 0
# --- CAN: the port is there (GH 1.25: 5 V, GND, CAN_H, CAN_L) but nothing is on it for first flights. Off, so no
# --- driver waits for nodes. The charge node and a DroneCAN airspeed sensor can go on it later (one bus).
CAN_P1_DRIVER 0
# --- Power: the board's VBAT sensing (ESC #1 port pin 1, up to 8S; hwdef scale 10.1) and the PM02 V3's current signal
# --- on the ESC #1 port's CURRENT pin (pin 3), in place of the 4-in-1's own current pad; 36.36 A/V is the PM02 V3's
# --- scale. The PM02 measures the whole pack current, the pusher included.
BATT_MONITOR 4
BATT_AMP_PERVLT 36.36
# Cross-check only: the sum of the 4-in-1's reported current (its serial telemetry).
BATT2_MONITOR 9
# --- Scripting heap. The F765 (STM32F767, 512 KB RAM) is ArduPilot's memory class 500, whose default SCR_HEAP_SIZE is
# --- 100 KB (AP_Scripting.cpp). At 100 to 180 KB the first-flight set fails to load ("Lua: Insufficent memory loading"),
# --- and the aircraft cannot arm (th-testpilot heapprobe, 902b51ae; every SITL result before had SITL's 200 KB). In SITL
# --- the loaded set takes about 190 KB, plus the compile of the largest script still loading (axis_approach_brake about
# --- 35 KB). If the bench fails, the set gets smaller, not the margin (th-avionics, 2026-10-13). Reboot after setting it.
# --- MEASURED on the real LUX 2026-10-15 (ArduPlane 4c98c922, the eight scripts, electronics-notes "Scripting memory check
# --- on the LUX F765"): the scripts' peak Lua use is 188,027 bytes, less than SITL's (SITL is 64-bit, so every Lua
# --- pointer is twice the size). The heap fits; the board memory around it did not: at the defaults (LOG_FILE_BUFSIZE 80,
# --- terrain on) "EKF3 not enough memory" and "Terrain: Allocation failed" at boot, freemem about 1.7 KB. With the two
# --- rows below: all eight load texts, no memory text, freemem 24.6 KB lowest and 34.9 KB settled (pass: over 16 KB).
# --- The night build (payload-lights.parm: CAN on) takes about 20 KB more board memory, and at 240000 and a 32 KB log
# --- buffer EKF3 did not allocate (bench 2026-10-15, freemem 10.7 KB). So both builds use 225000 and 16 KB (Ryan,
# --- 2026-10-15: one setting, still checklist-gated): day 51.2 / 62.8 KB, Lua peak 193,382 B (31 KB headroom); night
# --- 26.5 / 37.9 KB, Lua peak 207,968 B (17 KB headroom). A new script needs this check again.
SCR_HEAP_SIZE 225000
# The log write buffer, KB: 16 in place of the default 80 (32 until 2026-10-15; the night build needs the room, see
# above). Watch the logs of the first flights for gaps (DSF Dp, dropped messages; first-flight card).
LOG_FILE_BUFSIZE 16
# The terrain database cache: no mission, failsafe or script of ours uses terrain data, and on this board its cache
# is what tips the memory over (at LOG_FILE_BUFSIZE 32: freemem 12.8 KB with it, 34.9 KB without). Turn it on only with
# a memory re-check.
TERRAIN_ENABLE 0
params/board-nxtpx4v2.parm · 131 lines · open · download
# TerraHawk flight parameters: HKUST NxtPX4v2 (MicoAir), firmware target NxtPX4v2 (in ArduPilot at 4c98c92).
# Was the first-flight board from 2026-10-04 until the Lumenier LUX F765 replaced it the same day (Ryan:
# board-luxf765.parm, which has one CAN bus). Kept as a checked alternative: two IMUs, no CAN.
# Load after terrahawk-common.parm, then the airframe file. Do NOT load lift-4in1.parm with this board: it is
# written for the 6C's IO co-processor, and its SERIAL6 is this board's RC input. This board has no CAN, so it
# can only fly the 4-in-1; the 4-in-1 settings are in this file.
# Pinout: ArduPilot's hwdef/NxtPX4v2 (hwdef.dat, README.md). What it cannot do: DroneCAN anything (airspeed,
# charge node, rangefinder node, pod ESCs), so no dock charging on this board.
# Mounting: face up, the silkscreen arrow (toward the 8-pin ESC connector's edge) forward, on th-blender's Avionics
# Plate (NxtPX4v2), 31824ff. Both IMUs' hwdef rotation is the board-frame correction, so nothing else is needed.
AHRS_ORIENTATION 0
# No safety button.
BRD_SAFETY_DEFLT 0
# Two IMUs (BMI088 x 2), two EKF3 lanes.
EK3_IMU_MASK 3
# IMU offset from the design CG, kit (0, 270, -5.6), in body axes (X forward, Y right, Z down); these replace the
# common file's 6C values. Both BMI088s (6.5 mm apart) at their midpoint, assembly (-7.90, 295.69, 21.61): 25.7 mm aft,
# 7.9 mm to starboard (assembly -X), from the open KiCad board placed on the plate (th-blender 31824ff). Z rests on
# an ESTIMATE of 5 mm for the board's soft-mount grommets, plate top to PCB underside: measure it on the bench. Each
# mm taller makes Z 0.001 more negative. One soft stage only: the kit's FC Mount dampeners stay rigid PETG.
INS_POS1_X -0.0257
INS_POS1_Y 0.0079
INS_POS1_Z -0.0272
INS_POS2_X -0.0257
INS_POS2_Y 0.0079
INS_POS2_Z -0.0272
# --- Outputs: 8, all on the H743. Timer groups 1-4 (TIM1), 5-6 (TIM2), 7-8 (TIM3); every output in a group
# --- uses the same protocol, so no servo shares a group with a DShot motor.
# Lift motors 1-4 on the 4-in-1 (TBS Lucid 60A 3-8S, AM32), DShot300, bidirectional DShot for RPM (the board's README:
# all channels support it). Check on the bench that all four RPM values arrive before the first hover.
# Output n drives the Lucid's ESC channel n (a straight cable); the functions put each channel on the motor its
# pads face. Fuselage C v2.4B (th-blender W36, 2026-10-14): the 4-in-1 stands on edge ahead of the Spar Box,
# component side aft, battery tabs to STARBOARD, so the board's x = 42 edge is the UPPER one. TBS manual p. 97
# (vendor/esc/NOTES.md frame): channels 1 and 2 on the x = 42 edge, 3 and 4 on the x = 0 edge, 1 and 3 at the tab
# end. So channels 1 and 3 (pad groups x1 y17, x0 y17) leave by the starboard channel mouth, 2 and 4 (x1 y38.5,
# x0 y38.5) by the port one. Front or rear is chosen beyond the mouth: the UPPER edge's triple goes to the FRONT pod.
# ArduPilot quad X: motor 1 front starboard, 2 rear port, 3 front port, 4 rear starboard. From the drawing, not the
# board itself: check the silkscreen, then the motor test (A front starboard, B rear starboard, C rear port, D front
# port) before props. Was 35, 34, 33, 36 for the Lucid flat in the belly (2026-10-05), which on edge would drive
# two motors from the wrong side.
SERVO1_FUNCTION 33 # ESC ch1, pads x1 y17, upper edge, starboard end: front starboard
SERVO2_FUNCTION 35 # ESC ch2, pads x1 y38.5, upper edge, port end: front port
SERVO3_FUNCTION 36 # ESC ch3, pads x0 y17, lower edge, starboard end: rear starboard
SERVO4_FUNCTION 34 # ESC ch4, pads x0 y38.5, lower edge, port end: rear port
Q_M_PWM_TYPE 5
SERVO_DSHOT_ESC 1
SERVO_BLH_BDMASK 15
SERVO_BLH_POLES 14
# Elevons on 5 and 6 (TIM2, servos only). Reversal and endpoints on the bench, kept in the airframe file.
SERVO5_FUNCTION 77
SERVO6_FUNCTION 78
# 7: pusher ESC (PWM); 8: Survey3W trigger (TIM3, both PWM at the servo rate).
SERVO7_FUNCTION 70
# Pusher ESC: DYS Aria 70A 3-6S AM32 (Ryan, 2026-10-04; cart row 5): 70 A continuous, 80 A burst, no BEC, PWM input
# on this output (its timer group stays PWM). Before it flies, set in the AM32 configurator
# (esc-configurator.com, through a USB linker on the signal wire or the flight controller's ESC pass-through):
# Brake on stop: off. The fixed APC 10x7E freewheels at zero throttle; SITL will fly its windmill curve.
# Low voltage cutoff: off. The battery failsafes in the common file are the only low-voltage action.
# Motor direction: so the pusher blows aft; motor poles: 14 (check the ECO II's count).
# Servo low and high thresholds: 1000 and 2000 us, to match SERVO7_MIN and SERVO7_MAX.
# Temperature limit: disabled. Above it AM32 cuts the duty to about a quarter at once (main.c), a pusher loss.
# Bench: the motor is still at 1000 us and starts just above it. AM32's defaults are brake off and cutoff off, but read
# them back in the configurator: do not trust the defaults.
SERVO7_MIN 1000
SERVO7_MAX 2000
SERVO7_TRIM 1000
SERVO8_FUNCTION 10
# Survey3W, as on the 6C (docs/survey3.md). The exposure feedback goes on the AUX2 pad, GPIO 82 (PC1).
CAM1_TYPE 1
CAM1_SERVO_ON 2000
# Idle pulse: never 1500, which puts the camera into USB transfer mode.
CAM1_SERVO_OFF 1000
CAM1_DURATION 0.1
CAM1_INTRVAL_MIN 1.6
CAM1_FEEDBAK_PIN 82 # value-ok: GPIO(82) is PC1, the AUX2 pad (both sources agree), in hwdef/NxtPX4v2
CAM1_FEEDBAK_POL 1
# The video switch's "ON" input on PA4, GPIO 81: the AUX3 pad, J12 pin 1 (the board's design nets and MicoAir's
# sheet; ArduPilot's README calls it AUX1, which is wrong; th-blender, 2026-10-04). J12 has no power pin. On at boot.
RELAY2_FUNCTION 1
RELAY2_PIN 81 # value-ok: GPIO(81) is PA4, the AUX3 pad, in hwdef/NxtPX4v2
RELAY2_DEFAULT 1
# No output is left for the cargo-door servo on this board.
# --- Serial ports (SERIAL_ORDER OTG1 USART2 UART4 USART1 USART3 UART7 UART5 UART8) ---
# SERIAL1 (UART2): SiK radio, MAVLink 2.
SERIAL1_PROTOCOL 2
SERIAL1_BAUD 57
# SERIAL2 (UART4, J5: 5 V, TX, RX): ELRS receiver, CRSF. Not the RC connector J7: it has UART5 TX and an SBUS pin
# that reaches RX5 through an inverter, so CRSF (non-inverted, both directions) would arrive inverted there.
# The board's README itself suggests UART2, 4 or 8 for a serial receiver.
SERIAL2_PROTOCOL 23
# The CRSF driver sets its own 416 kBaud; 115 is the conventional value here.
SERIAL2_BAUD 115
# RC from CRSF only (bit 9). The default 1 also accepts RADIO_RC_CHANNELS from the LoRa link (MAVRadio, bit 16;
# compiled in on boards with more than 1 MB of flash). Without RC_OPTIONS bit 10 the first protocol to arrive is
# kept for the whole boot (AP_RCProtocol::should_search), so an mLRS receiver that sends RC channels before the
# ELRS link is up (transmitter off at power-up, the dock's mLRS transmitter on) would become the RC source and
# CRSF would be ignored until a reboot (th-telemetry, 2026-10-06).
RC_PROTOCOLS 512
# SERIAL3 (UART1): Waveshare Q43 NEO-F9P on J3, which carries the UART, 5 V and the only external I2C (I2C4,
# pull-ups to 3.3 V), so the Q43's compass rides its own cable. The board has no internal compass.
SERIAL3_PROTOCOL 5
SERIAL3_BAUD 230
GPS1_TYPE 2
# SERIAL4 (UART3): the HD VTX connector, MSP DisplayPort OSD. Its pin 1 is 12 V.
SERIAL4_PROTOCOL 42
SERIAL4_BAUD 115
OSD_TYPE 5
# SERIAL5 (UART7): the 4-in-1's serial telemetry (voltage, current, temperature, RPM per channel).
SERIAL5_PROTOCOL 16
SERIAL5_BAUD 115
# SERIAL6 (UART5, J7): not used. The board's default puts RC here; it is switched off so only SERIAL2 decodes RC.
SERIAL6_PROTOCOL -1
# SERIAL7 (UART8): spare (a serial rangefinder later).
SERIAL7_PROTOCOL -1
# --- Airspeed: an I2C sensor sharing J3's I2C4 with the Q43's compass, through the Y lead, row 9f (bus index 1 in
# --- I2C_ORDER I2C1 I2C4) ---
ARSPD_BUS 1
# MS4525DO (I2C, address 0x28): the sensor Ryan has for first flights (chip marked "4525DO", 2026-10-04).
ARSPD_TYPE 1
# --- Power: the board's own pack-voltage sensing, and the PM02 V3's current signal on the ESC connector's
# --- current input. The hwdef's defaults (10.2, 20.4) suit MicoAir's ESC; the current scale is the PM02 V3's
# --- 36.36 A/V (as the Pixhawk6C hwdef). The PM02 measures the whole pack current, the pusher included.
BATT_MONITOR 4
BATT_AMP_PERVLT 36.36
# Cross-check only: the sum of the 4-in-1's reported current (its serial telemetry).
BATT2_MONITOR 9
params/board-pixhawk6c-mini.parm · 124 lines · open · download
# TerraHawk flight parameters: Holybro Pixhawk 6C Mini, Model A (firmware target Pixhawk6C).
# Load after terrahawk-common.parm. Wiring: docs/electronics-notes.md, "Output map trap".
# Not the first-flight board any more: the NxtPX4v2 flies first (board-nxtpx4v2.parm, Ryan 2026-10-04). Kept for a
# 6C if one is used, and as the reference for our own FC, whose hwdef derives from the Pixhawk6C.
# Mounted turned 180 degrees, arrow aft, ports toward the rear hatch.
AHRS_ORIENTATION 4
# The Q43 has no safety button; arming is by the GCS or the sticks.
BRD_SAFETY_DEFLT 0
# Two IMUs, two EKF3 lanes.
EK3_IMU_MASK 3
# IMU offset from the design CG, kit (0, 270, -5.6), body axes (X forward, Y right, Z down), so the EKF's outputs (and
# XKF1) are the CG's position, as GPS1_POS assumes. The 6C Mini's case centre, kit (0, 305.65, 23.90) (th-blender,
# 2026-10-03); Holybro gives no IMU position inside the case, so a placeholder within about 15 mm. Both IMUs. In the
# common file 2026-10-03 to 2026-10-04, then moved here.
INS_POS1_X -0.036
INS_POS1_Y 0
INS_POS1_Z -0.030
INS_POS2_X -0.036
INS_POS2_Y 0
INS_POS2_Z -0.030
# Bench, before the first power-up: on the DroneCAN page, set the internal compass's COMPASS_USE to
# 0 once the instances show, so only the Q43's compass is used (it sits over the PM02's current).
# --- Outputs. MAIN 1-8 via the IO MCU (groups 1-2, 3-4, 5-8), AUX 1-6 on the FMU (1-4, 5-6) ---
# Lift motors 1-4: functions on MAIN 1-4. This file sets them up for DroneCAN pod ESCs on CAN bus 1
# (pads unused). The first build flies the 4-in-1 on the MAIN pads: load lift-4in1.parm after this file.
SERVO1_FUNCTION 33
SERVO2_FUNCTION 34
SERVO3_FUNCTION 35
SERVO4_FUNCTION 36
Q_M_PWM_TYPE 0
Q_M_PWM_MIN 1000
Q_M_PWM_MAX 2000
SERVO5_FUNCTION 0
SERVO6_FUNCTION 0
# Elevons on MAIN 7 and 8 (group 5-8, no motor). Reversal and endpoints are set on the bench and kept
# in the airframe file.
SERVO7_FUNCTION 77
SERVO8_FUNCTION 78
# AUX 1 and 2: Survey3W trigger and exposure feedback (the first build carries it, Ryan 2026-09-28). Set as
# rows, not a comment: with no camera on AUX 1 and 2 they do nothing. AUX 2 is GPIO 51 on this board (the
# IO MCU drives MAIN; the FMU's PWM(1..8) are AUX 1..8 at GPIO 50..57, Pixhawk6C hwdef). CAM1_TYPE needs a
# reboot. Values and reasons: docs/survey3.md (MAPIR's trigger guide).
SERVO9_FUNCTION 10
SERVO10_FUNCTION -1
CAM1_TYPE 1
CAM1_SERVO_ON 2000
# Idle pulse: never 1500, which puts the camera into USB transfer mode.
CAM1_SERVO_OFF 1000
CAM1_DURATION 0.1
# Just above the camera's 1.5 s JPG interval. At the 21 m/s survey it gives 79 % forward overlap at 116 m
# (th-aeronautics); 1.5 only if th-main confirms the Survey3W sustains it.
CAM1_INTRVAL_MIN 1.6
CAM1_FEEDBAK_PIN 51
CAM1_FEEDBAK_POL 1
# AUX 3: belly cargo-door release servo (EMAX ES9051 at Y 360, th-blender 9b0c6a5; Ryan: door good for now, to be
# revised). ArduPilot's gripper: DO_GRIPPER in a mission, or an RC switch (RCn_OPTION 19). Group AUX 1-4 has no motor.
# At boot the gripper goes to GRIP_NEUTRAL (AP_Gripper_Servo::init_gripper); the default 1500 would half open the
# door, so NEUTRAL equals GRAB (closed). The bench sets GRAB and RELEASE for the 60 deg horn swing; these are
# placeholders. The servo only slides two end latches 5 mm; the door halves hang open after a drop, for the rest of
# the flight. So AUTOCLOSE is 0: closing the latches behind open halves would stop them being pushed shut (the
# fingers have no ramps). On the ground: close the port half (+X, no lip) by hand first, then the starboard half (-X, with the lap lip), then command GRAB
# (th-blender, 2026-10-04: the -X half's lip lies outside the +X half's edge, so a lip closed first blocks it).
# The drop (doors at Y 366 to 502, about Y 434) moves the CG forward. Bay payload limit (th-aeronautics fff6327,
# from th-testpilot's sweep cc74ad7): all-up 3.01 kg or less at the 21 m/s survey, so 100 g with the Survey3W fitted
# (about 165 g camera off, an estimate); after a release the CG must stay at 256 mm or aft with 8 deg of trim left at
# 18 m/s. A 100 g drop with the pack held leaves only 6.77 deg, so cg_slider.lua (v1.7) watches this gripper output
# (function 28 at GRIP_RELEASE) and moves the pack aft to put the CG back at the takeoff CG; set CGS_DROP_CG and
# CGS_AFT_MM from the balance. The open doors add about 60 % drag for the rest of the flight: drop at the end of a
# survey, not before its last line.
SERVO11_FUNCTION 28
GRIP_ENABLE 1
GRIP_TYPE 1
GRIP_GRAB 1100
GRIP_RELEASE 1600
GRIP_NEUTRAL 1100
GRIP_AUTOCLOSE 0 # range-ok: 0 disables autoclose ("0 to disable", AP_Gripper_Backend.cpp is_positive)
# AUX 4: the "ON" input of the video rail's MOSFET switch (the BEC has no enable). On at boot.
SERVO12_FUNCTION -1
RELAY2_FUNCTION 1
RELAY2_PIN 53
RELAY2_DEFAULT 1
# AUX 5: pusher ESC, alone in group AUX 5-6.
SERVO13_FUNCTION 70
SERVO14_FUNCTION 0
# --- CAN: two independent DroneCAN networks, never one driver on both ---
# Bus 1 (CAN1): the four DroneCAN lift ESCs, node ids 24-27, ESC index = motor number - 1. Empty while
# the 4-in-1 flies (lift-4in1.parm clears CAN_D1_UC_ESC_BM).
# Bus 2 (CAN2): airspeed (21), charge node (20), rangefinder node (22) when fitted.
CAN_P1_DRIVER 1
CAN_P2_DRIVER 2
CAN_D1_PROTOCOL 1
CAN_D2_PROTOCOL 1
CAN_D1_UC_NODE 10
CAN_D2_UC_NODE 10
CAN_D1_UC_ESC_BM 15
CAN_D1_UC_ESC_OF 0
# Airspeed: Holybro DLVR-L10D over DroneCAN on bus 2.
ARSPD_TYPE 8
# --- Serial ports (SERIAL_ORDER OTG1 UART7 UART5 USART1 UART8 ...) ---
# TELEM1: SiK radio, MAVLink 2.
SERIAL1_PROTOCOL 2
SERIAL1_BAUD 57
# TELEM2: ELRS receiver, CRSF.
SERIAL2_PROTOCOL 23
# The CRSF driver sets its own 416 kBaud; 115 is the conventional value here.
SERIAL2_BAUD 115
# GPS1: Waveshare Q43 NEO-F9P (u-blox), its compass on this port's I2C.
SERIAL3_PROTOCOL 5
SERIAL3_BAUD 230
GPS1_TYPE 2
# GPS2: DJI O4, MSP DisplayPort OSD.
SERIAL4_PROTOCOL 42
SERIAL4_BAUD 115
OSD_TYPE 5
# --- Power: PM02 V3 on the POWER port (analog voltage and current) ---
BATT_MONITOR 4
# Cross-check only, not used for failsafes: the sum of the lift ESCs' reported current (over bus 1, or
# the 4-in-1's serial telemetry).
BATT2_MONITOR 9
params/airframe.parm · 104 lines · open · download
# TerraHawk flight parameters: the airframe. Load last. Values measured on the aircraft: calibrations and the
# elevon linkage. One airframe (Ryan, 2026-10-04; airframe-A.parm and airframe-B.parm were one file each for two
# airframes). Every value below is a placeholder until measured; the procedure for each is on the right.
# Measured CG (267 to 270 mm aft of the nose datum on Fuselage C, aim 268; params/README.md):
# with the Tattu 4500: ___ mm with the P50B 6S2P: ___ mm date: ____
# Board level in the aircraft's FC cradle: accelerometer level calibration with the airframe level.
AHRS_TRIM_X 0
AHRS_TRIM_Y 0
# Airspeed ratio on Fuselage C v2.4B (th-aeronautics, 2026-10-18; coarse CFD, ESTIMATE; the tables are in
# aero/airspeed/static-cp-c-v24b.json, 6bbf590f). The flush side static ports
# (Y 40, Z -10, both sides, teed to the MS4525's static nipple) sit in a compression zone whose Cp climbs steeply
# with alpha: +0.08 at alpha 0, +0.22 at 3, +0.34 at 6.7, +0.39 at 9.5, +0.40 at 12.5, +0.39 at 14. So no single ratio
# reads true at both ends, and the low-alpha end is the dangerous one: a forward transition flies the wing at alpha 0.5
# to 1.5 deg until "Transition airspeed reached" (SITL 20261012T044346Z), and ArduPlane ends the airspeed wait when
# EAS passes AIRSPEED_MIN (quadplane.cpp:1522). Indicated 18 there is about true at 2.3, 16.7 to 17.1 m/s true at 2.6,
# and 15.8 to 16.2 at 2.9, the trimmed stall. Keep 2.3: about true at the transition end and low (slow-reading)
# everywhere else (13 % low at cruise, 17 % low near the stall), so the aircraft flies faster than it shows.
# SITL with a sensor that reads through this Cp(alpha) table (th-simulator SIM_ARSPD_CP, th-testpilot 5a04a88a, 47
# flights): at 2.3 the transition ends at 18.2 to 20.2 m/s true (2.9: 16.3 to 17.4). The table stops at alpha 0, and if
# Cp falls further below it the real end is a little earlier: at 2.3 by under 1 % of speed (alpha was -0.3 deg or more
# where indicated passed 18), more at 2.9. The wing stalls at the same true
# speed in every case, so the ratio moves the display, not the stall; 2.3 costs about 7.5 % more energy in the survey.
# AIRSPEED_MIN is 20 since 2026-10-18 (terrahawk-common.parm, th-simulator 7a2d664c): on the C model no transition then
# ends under 18 m/s true (th-testpilot 1d51b2d9); at 18, 19 % did.
# The total source since W104 (th-blender 888383ba, 2026-10-19): our own brass tube beside the camera, total only, an
# open bevelled mouth with a 30 deg internal chamber, 7 deg tip down; the static stays on the Y 40 ports. At its mouth
# the flow is within about +-17 deg of the tube axis through alpha 14 (-16.4 to +16.3 in pitch, about 16.7 with the yaw) and Cpt is 0.988 to 0.990 (th-aeronautics e68dc651),
# inside the mouth's +-23 deg acceptance, so total reads about 0.5 % low in airspeed; with the lift rotors on, the
# transition end moves about 0.5 % (9789b957). So 2.3 stands. (W93.2's kit tube, 60f05e70, is retired.)
# Recompute this row whenever the nose or the ports change.
ARSPD_RATIO 2.3
# Do NOT run the in-flight airspeed calibration on these ports: in cruise circles at 21 m/s (alpha about 6.5) it
# converges near 3.0, which ends the transition below the stall (above). A filed row, so the checklist code catches
# a 1 left on.
ARSPD_AUTOCAL 0
# Elevons: reversal from the bench check (guide step 4), endpoints for 25 degrees of travel
# (docs/cg.md). Trim: before the first transition, set the predicted cruise trim from docs/cg.md's
# 'up elevon to trim' at the aircraft's measured CG and 22 m/s, at 16 us per degree (400 us is 25
# degrees), toward up. A transition that starts out of trim sags much deeper (th-simulator,
# 2026-09-28). SERVO_AUTO_TRIM then refines it in flight; copy the learned values back here.
# On Fuselage C with our own wing the predicted level trim at 22 m/s indicated (24.3 true, ARSPD_RATIO 2.3) is about
# 4.8 deg trailing edge up at alpha 4.65, 3.034 kg and CG 268 (th-aeronautics 086d1330, aero/wing-own/c24b-level.json,
# ESTIMATE), so about 77 us toward up: cg.md's first-build table, the 22 m/s indicated row (4aeeb934), not the earlier
# wing's table under it (about 7.7 deg). Which
# way "up" moves the pulse is known only after the bench reversal check, so the rows stay 1500 until then.
# The output numbers belong to the board: 5 and 6 on the NxtPX4v2 (board-nxtpx4v2.parm, the first-flight board).
# On a board with the elevons on 7 and 8 (the 6C, our own FC), rename these rows; on the NxtPX4v2, 7 is the pusher
# and 8 the camera trigger. check_params.py refuses an elevon row on an output that is not an elevon.
SERVO5_REVERSED 0
SERVO5_MIN 1100
SERVO5_MAX 1900
SERVO5_TRIM 1500
SERVO6_REVERSED 0
SERVO6_MIN 1100
SERVO6_MAX 1900
SERVO6_TRIM 1500
# The flight compass is the QMC5883L in the SEQURE M10-25Q (GPS2, board file), the ONLY compass since 2026-10-15: the
# SEN0697 cannot be bought (Ryan, via th-blender). The Q43's QMC5883P stays off, because on v2.4B the PM02 sits on the
# beacon carrier about 32 mm from the Q43 (CLAUDE.md Fuselage row, th-telemetry's condition): DISBLMSK bit 20
# (QMC5883P) turns its driver off, so it can never be probed into compass 1. Bit 12 (QMC5883, the M10's chip) stays
# clear. The LUX F765 has no compass of its own. ArduPilot's QMC5883L driver already applies ROLL_180 for an external
# chip (AP_Compass_QMC5883L.h), the usual GPS-module build, so COMPASS_ORIENT starts at 0 for the module upright with
# its arrow forward, as th-blender's W52 hanger mounts it (6ac8c58e: upright, patch up, arrow forward; an arrow aft would be 4). The calibration may change it
# (COMPASS_AUTO_ROT 2, also the LUX's hwdef default); once first-flight step 11 confirms the signs by hand, the
# confirmed value replaces the 0 here.
COMPASS_DISBLMSK 1048576
COMPASS_ORIENT 0
# With one compass there is nothing to compare it against, so the EKF's GSF yaw estimator (yaw from GPS velocity and
# the IMU) stays on and may reset a bad magnetic yaw: run and use it on cores 1 and 2 (the defaults; filed because the
# sole-compass gate depends on them, th-telemetry M8, 20cf07d2). The one core of EK3_IMU_MASK 1 is core 1.
EK3_GSF_RUN_MASK 3
EK3_GSF_USE_MASK 3
COMPASS_AUTO_ROT 2
# Compass offsets: a full calibration on the aircraft once; after work near the compass, the fixed-yaw
# calibration as a check. The calibration writes COMPASS_OFS*, COMPASS_DIA*, COMPASS_ODI*.
# Current-based compensation for the battery feed, phase leads and the pusher's feed (the M10 at Y 390 to 420; th-telemetry's
# clearances: pusher motor and Aria 100 mm, pusher feed 60 mm, 20cf07d2; interference under 30 % is their M3). PLACEHOLDERS (0 = no compensation) until the tied-down bench
# run: after the compass calibration, props on and tied down, step the lift motors through hover current with
# LOG_DISARMED 1, then tools/fit_compass_mot.py on the log writes these rows. Over 30 % interference: re-route
# the leads instead (th-telemetry). ArduPlane has no compassmot command.
COMPASS_MOTCT 2
COMPASS_MOT_X 0
COMPASS_MOT_Y 0
COMPASS_MOT_Z 0
# th_prearm.lua adds TH_AIRFRAME (load the flight scripts and reboot before this file); set it to 1 here. It refuses
# to arm while it is 0, so a board that has not had this file loaded cannot fly on its placeholder calibrations.
TH_AIRFRAME 1
# th_prearm.lua v0.4: arming needs the mLRS telemetry radio seen on TELEM2 (RADIO_STATUS from 51/68 and its RSSI_A,
# RSSI_B and LQ values for 12 s, docs/failsafes.md). 0 only on a bench without the radio; SITL sets 0 in its harness.
TH_RAD_REQ 1
# lift_check.lua (load the flight scripts and reboot before this file): arming waits for a landing lane in the mission,
# a JUMP_TAG LCK_LANE_TAG then the approach and a fixed-wing NAV_LAND on the keels, into the usual wind, on ground fit
# for a belly landing. After a lift-motor loss it is the planned way down on the wing (th-avionics, 2026-10-12: kept at
# the script's default for every flight, the first hovers included; only the wingless testbed quad uses 0).
LCK_LANE_REQ 1
# FPV camera: the Walksnail lens points 15 deg below the airframe's axis (th-blender W104, 888383ba). ArduPilot's OSD
# artificial horizon has no camera-tilt offset (AP_OSD_Screen::draw_horizon; in fixed-wing flight it also subtracts
# PTCH_TRIM_DEG, Plane.cpp:1053), so with the trim at 5 its level line would sit about 10 deg off the real horizon in
# cruise and 15 in hover. So it is off (Ryan, 2026-10-20). Only OSD screen 1 is enabled by default; any other screen
# turned on later needs its HORIZON_EN at 0 too. The goggles draw no horizon of their own that we use, so nothing live
# depends on the tilt; the 15 deg goes into Gyroflow for the recordings (th-telemetry 086393e3).
OSD1_HORIZON_EN 0
params/lift-4in1.parm · 30 lines · open · download
# TerraHawk lift motors on a 4-in-1 ESC (TBS Lucid 60A 3-8S 4in1, AM32) instead of DroneCAN ESCs.
# Load after board-pixhawk6c-mini.parm and before the airframe file, only when the 4-in-1 is fitted.
# The 4-in-1 flies first (Ryan, 2026-09-29): the first build loads this file. The DroneCAN pod ESCs
# (S50, then our pod board) follow later; for them, leave this file out.
#
# The motors stay on MAIN 1-4 (SERVO1-4 = motors 1-4 in the board file). MAIN is driven by the IO
# co-processor. Pixhawk6C firmware carries a DShot build of the IO firmware, which drives IO PWM 1-4 on
# TIM2 and TIM4 with bidirectional DShot (hwdef iomcu-f103-dshot, ArduPlane 4c98c92). So the AUX
# outputs keep their jobs: Survey3 trigger and strobe, video switch, pusher.
# DShot on the IO co-processor (takes effect after a reboot).
BRD_IO_DSHOT 1
Q_M_PWM_TYPE 5
SERVO_DSHOT_ESC 1
# Bidirectional DShot RPM on MAIN 1-4, for the harmonic notch and the motor checks. Make sure on the
# bench that all four RPM values arrive before the first hover.
SERVO_BLH_BDMASK 15
SERVO_BLH_POLES 14
# No lift ESCs on CAN bus 1 while the 4-in-1 is fitted. The bus and its driver stay for the pod boards.
CAN_D1_UC_ESC_BM 0
# KISS serial telemetry (voltage, current, temperature per channel) on USART3, the FMU Debug SH-6
# connector, RX only. The Mini has no other free UART (th-eda). BATT2_MONITOR 9 in the board file sums
# this telemetry's current, so it needs no change. Leave the 4-in-1's analog current pad unconnected.
SERIAL6_PROTOCOL 16
SERIAL6_BAUD 115
# Set on the ESC with the AM32 configurator, not here: brake on stop (the folding props), motor
# direction per channel, and the temperature limit.
params/pack-lipo-4500.parm · 18 lines · open · download
# TerraHawk battery overlay for the Tattu 6S 4500 25C LiPo, the optional tuning pack (Ryan, 2026-10-01).
# The flight pack is the 6S2P Molicel P50B (10 Ah); terrahawk-common.parm carries its values. Load this file
# last, after the airframe file, only when the Tattu is fitted. Then load the common file again before the
# next flight on the P50B. The voltages are the manual's LiPo figures (docs/battery.md). On the Tattu the
# aircraft needs 109 g of nose ballast to keep the CG at 265 to 270 mm; with it, about 2.59 kg all-up
# (th-aeronautics, ballast at 20 mm). The P50B build is about 2.76 kg with about 55 g in the nose cups at
# Y 56 (cg.md, 5331f18). At Y 56 the Tattu needs more than 109 g, and the cups hold about 84 g of steel:
# confirm the Tattu's ballast with th-aeronautics before it flies.
BATT_CAPACITY 4500
Q_M_BAT_VOLT_MIN 19.8
BATT_LOW_VOLT 21.0
BATT_CRT_VOLT 20.4
# Capacity triggers at 50 % and 30 % (900 mAh apart). The return energy is mostly the hover: 60 s at about
# 28 A is about 470 mAh, a tenth of this pack, so 30 / 15 % (675 mAh apart) could not cover a return. Fly the
# Tattu only within 500 m of home. Its landing sag is not measured; 30 % follows the P50B (2026-10-01). The
# voltages above are the manual's LiPo figures and sit well below these charges: no LiPo table yet.
BATT_LOW_MAH 2250
BATT_CRT_MAH 1350
params/payload-tfmini.parm · 25 lines · open · download
# TerraHawk flight parameters: the TFmini-S payload. Load after the airframe file, ONLY when "Payload Cover (TFmini-S)"
# is fitted (th-blender 0900ef8, Ryan 2026-10-05); reboot after loading (RNGFND1_TYPE). Without this cover, the board
# file's RNGFND1_TYPE 0 stands. Loading this file with the cover off fails the pre-arm check (no rangefinder data),
# which is intended.
# Not loaded on Fuselage C's first flights: the ESC payload hatch closes the bay and the TFmini-S does not fly
# (Ryan, 2026-10-11). The position below is the old cover's; it is set again when the sensor returns.
# Benewake TFmini-S, downward, on the LUX's TELEM1 pads (USART1, SERIAL1), 115200 baud, serial. It was on the GPS2
# pads (UART8, SERIAL4) until the SEQURE M10-25Q took them as GPS2 (Ryan, 2026-10-15); TELEM1 is the spare UART
# since the SiK went (board file). Found by lux-765, 2026-10-19.
SERIAL1_PROTOCOL 9
SERIAL1_BAUD 115
RNGFND1_TYPE 20
RNGFND1_ORIENT 25
# TFmini-S: 0.1 m minimum; 7 m at 10 % reflectivity in sunlight (docs/landing-sensing.md).
RNGFND1_MIN 0.1
RNGFND1_MAX 7
# Position from the design CG, kit (0, 270, -5.6), body axes (X forward, Y right, Z down): the measuring point
# between the lens bosses at assembly (0, 375, -17.29) on the cover's centreline (th-blender 0900ef8, 3 mm aft in
# 4a5c07b; a snap-in PETG cradle with a TPU lens bezel).
RNGFND1_POS_X -0.105
RNGFND1_POS_Y 0
RNGFND1_POS_Z 0.0117
# Use it for takeoff and landing only (bit 1): the final descent starts by the measured height, not the barometer.
RNGFND_LANDING 2
params/payload-lights.parm · 44 lines · open · download
# TerraHawk flight parameters: the night lights (docs/lights.md). Load after the airframe file, ONLY when the lights
# node and its lighting battery are fitted; reboot after loading (CAN_P1_DRIVER). Without them, the board file's
# CAN_P1_DRIVER 0 stands. Loading this file with the node missing fails the pre-arm check (BATT3 unhealthy), which is
# intended. Also load sim/scripts/lights.lua onto the microSD card (APM/scripts).
# The LUX F765's only CAN port, on for this node alone (the first flights fly with CAN off).
# CAN on takes about 20 KB of the LUX's board memory: at a 240000 heap and a 32 KB log buffer EKF3 did not allocate
# (bench, 2026-10-15). The board file therefore sets SCR_HEAP_SIZE 225000 and LOG_FILE_BUFSIZE 16 for both builds
# (night measured 26.5 KB free lowest; docs/electronics-notes.md, the memory check). Do not set either one here: an
# overlay value removes the name from the checklist gate.
CAN_P1_DRIVER 1
CAN_D1_PROTOCOL 1
# SERVO13..16 (no pins on the LUX) carry lights.lua's four commands to the node as DroneCAN servo commands:
# bits 12 to 15. The node reads them as its RCIN13..16 (hw/lights-node defaults.parm).
CAN_D1_UC_SRV_BM 61440
CAN_D1_UC_SRV_RT 50
SERVO13_FUNCTION 95
SERVO14_FUNCTION 96
SERVO15_FUNCTION 97
SERVO16_FUNCTION 98
# The lighting battery, from the node's BatteryInfo (battery_id 3, the node's BATT_SERIAL_NUM). The capacity follows
# the pack Ryan picks (Roaring Top 1300 recommended); the node's charge estimate is used (DroneCAN state of charge).
BATT3_MONITOR 8
BATT3_SERIAL_NUM 3
BATT3_CAPACITY 1300
# No ArduPilot failsafe action on BATT3: lights.lua returns the aircraft at night only (LGT_LOW_PCT), and warns by day.
BATT3_FS_LOW_ACT 0
BATT3_FS_CRT_ACT 0
# No AIRSPEED_STALL row here since 2026-10-20: the night build flies the common file's 20.0. The 20.6 that was here
# (68002f2) came from Fuselage B's soft pitch band at 2.97 to 3.01 kg (jbsim-b3, 18.04 to 19.81 m/s). On Fuselage C,
# th-testpilot flew it against 20.0 on the night model c24b-w78-night (3.207 kg; first-flight set, lights scripts and
# this overlay, x3 each; ecca13b7, corrected fa3aba5a): no assist in any survey, peak true alpha in AUTO 7.3 to 8.8 deg
# at 20.0 (7.3 to 9.0 at 20.6), no time at or above 9.5 deg, the same bank, the lane and transitions unchanged, S1
# 3.0 % and S2 0.7 % less energy at 20.0. The C side ports read low near the stall (AspS/AspT 0.82 median, 0.71
# lowest in those surveys), so 20.0 indicated is far above C's trimmed stall at this mass (about 17.2 m/s EAS, which
# reads about 14.3; params/README.md). The stall test sets the real value for both builds.
# Pre-arm on the lighting battery's charge, by day: 7.8 V (3.9 V a cell, about 75 %, at the light load before
# arming). By day the lights are the two strobes and the node, about 4.5 W. At night lights.lua v1.2 raises this
# floor to LGT_ARM_V (8.2 V, about 95 %) while disarmed, without saving it: the longest night flight (about 43 min,
# the flight pack runs out first) takes about 43 % (4.1 Wh; the always-on rail is on the flight pack in flight), so
# from 95 % the pack is near 53 % when the flight pack triggers its return, over LGT_LOW_PCT 40 (th-aeronautics,
# 2026-10-11). The 7.8 here was once said to cover a night survey; it does not (th-aeronautics, 2026-10-10). The dock keeps
# the pack charged; a bench pack below the floor needs a charge first.
BATT3_ARM_VOLT 7.8
params/testbed-quad.parm · 95 lines · open · download
# TerraHawk flight parameters: the lift testbed quad (Ryan, 2026-10-10, through th-blender and th-testpilot). An
# HGLRC MY10 10 inch frame that carries TerraHawk's own lift system: the EMAX ECO II 2807 1300KV motors, the Gemfan
# F8041 3-blade props, the Lucid 4-in-1, the LUX F765 and the P50B 6S2P pack. It flies ArduPlane in Q modes with no
# wing, so every Q_A_, Q_M_ and Q_P_ value tuned on it is the aircraft's own row, name for name (th-testpilot).
# This file overrides only one of them: Q_M_THST_HOVER's start value, for the testbed's lighter weight.
#
# Load order: terrahawk-common.parm, board-luxf765.parm, then this file IN PLACE OF airframe.parm (never both). Reboot
# and load the three again (params/README.md). Load only sim/scripts/lift_check.lua onto the microSD card: its ground
# RPM check runs at arming, and its wing checks and its latch need flight on the wing, which this frame cannot do.
# Do not load cg_slider.lua, axis_approach_brake.lua or lights.lua.
#
# What carries to the aircraft and what does not. The motors, props, ESC, pack and flight controller are the
# aircraft's, so the thrust curve (Q_M_THST_EXPO, Q_M_SPIN_*), the ESC telemetry, the motor and prop vibration, the
# hover current, the battery sag and lift_check's RPM limits carry. The rate and angle gains (Q_A_RAT_*, Q_A_ANG_*)
# depend on the arm length and the inertia, which the MY10 does not share with the aircraft, so they are a starting
# point for the aircraft's own QAUTOTUNE, not its answer. Q_M_THST_HOVER is learned again on the aircraft.
#
# QAUTOTUNE: ArduPlane compiles it only for SITL (ArduPlane/qautotune.h, QAUTOTUNE_ENABLED = HAL_QUADPLANE_ENABLED
# && SITL). The LUX needs our own build with QAUTOTUNE_ENABLED 1 (hw/flight-controller, th-avionics); on stock
# firmware switch position 4 is refused and the tune is manual.
#
# Rate gains: the testbed starts on ArduPlane's quadplane defaults (Q_A_RAT_RLL/PIT_P and _I 0.25, _D 0.0036; the
# common file sets none). SITL gave no starting set to put here: its QAUTOTUNE failed on both frames from these
# gains and from half of them, in the D search or in levelling (th-testpilot TB6, 2026-10-10), and SITL's gyro
# vibration is a placeholder. So the first real hovers set the order:
# 1. QSTABILIZE at about 2 m. If roll or pitch oscillates, land, halve the P and D of that axis, and try again.
# 2. Hover 60 s in QHOVER for the hover learn and an FFT log (INS_LOG_BAT_MASK 1 is in the common file).
# 3. Set the harmonic notch from that log, and give the log to th-simulator for SITL's vibration model.
# 4. Then QAUTOTUNE, one axis at a time (Q_AUTOTUNE_AXES), in calm air, at 10 m or more.
# The wing outputs off: no elevons on 5 and 6, no pusher on 7, no camera trigger on 8, no script servo on 12.
# The lift motors stay on 1 to 4 (the board file's 35, 34, 33, 36). Wire each Lucid motor pad to the same corner
# as on the aircraft (M1/M2 port, M3/M4 starboard; docs/electronics-notes.md) and check the order and direction
# with Mission Planner's motor test before the first hover.
SERVO5_FUNCTION 0
SERVO6_FUNCTION 0
SERVO7_FUNCTION 0
SERVO8_FUNCTION 0
SERVO12_FUNCTION 0
CAM1_TYPE 0
# Hover thrust start value. Bare, the testbed hovers at about 385 g a motor (th-blender 6dc515c; about 0.31 raw
# throttle, a different scale). Q_M_THST_HOVER is linearized thrust, so it scales with the weight: the common file's
# 3-blade prediction for the aircraft, 0.275 at 712 g a motor, gives 0.275 x 385 / 712 = 0.15 (estimate; the
# hover learn sets the real one in the first hovers). Ballasted to the aircraft's 712 g a motor: set 0.25, the
# common file's start value. The CG is about 15 mm above the blade plane (the pack on the top plate), another
# reason the rate gains do not carry.
Q_M_THST_HOVER 0.15
# No airspeed sensor on the frame.
ARSPD_TYPE 0
ARSPD_USE 0
# No Q-assist: there is no wing to assist. -1 disables every assist (0 fails the pre-arm check; quadplane.cpp).
Q_ASSIST_SPEED -1 # range-ok: -1 disables every assist (the parameter's own description)
Q_ASSIST_ANGLE 0
Q_ASSIST_ALT 0
# Never a fixed-wing mode. Every RTL becomes a QRTL (3, QRTL Always; mode_rtl.cpp), so the battery and RC failsafes
# that command RTL return in hover. Q_OPTIONS keeps the common file's bits 5, 16 and 17 and adds bit 18: arm only
# in a Q mode or AUTO (196640 + 262144).
Q_RTL_MODE 3
Q_OPTIONS 458784
Q_RTL_ALT 20
# No fins: no weathervaning (ENABLE 0 overrides the common file's gain).
Q_WVANE_ENABLE 0
# The switch: QSTABILIZE, QHOVER, QLOITER, QAUTOTUNE (our build only), QRTL, QLAND. No FBWA, AUTO or RTL. QRTL is
# on 5, where the aircraft has RTL. QLAND is on 6, the position a switch at rest or a SITL channel 5 at 1800 selects
# at power-up: QRTL entered before home is set reads an unset (0, 0, 0) location. SITL stops on it (a SITL-only
# check in Location::get_alt_cm, th-simulator d52427e); on the LUX it goes on with a wrong altitude target, silently.
# QLAND does not use home. QRTL and QLAND both refuse arming (mode.h), and in the air home is always set.
FLTMODE1 17
FLTMODE2 18
FLTMODE3 19
FLTMODE4 22
FLTMODE5 21
FLTMODE6 20
# lift_check.lua: no lane landing on a frame with no wing, so arming must not wait for a lane mission (with 1 the
# arming check refuses, th-testpilot TB4). The ground RPM check at arming stays on.
LCK_LANE_REQ 0
# A cylinder fence for the tuning field: 40 m up, 60 m out, then QRTL (FENCE_ACTION 1 is RTL, which is a QRTL here).
FENCE_ENABLE 1
FENCE_TYPE 3
FENCE_ALT_MAX 40
FENCE_RADIUS 60
FENCE_ACTION 1
FENCE_MARGIN 5
# Sensor positions from the testbed's own CG (the pack fitted), measured on the frame; the aircraft's values in the
# common and board files do not apply. 0 until measured. GPS: the Q43 if it is moved over, or any u-blox on GPS1;
# set the compass orientation for its mount.
INS_POS1_X 0
INS_POS1_Y 0
INS_POS1_Z 0
INS_POS2_X 0
INS_POS2_Y 0
INS_POS2_Z 0
GPS1_POS_X 0
GPS1_POS_Y 0
GPS1_POS_Z 0
params/test-stall.parm · 10 lines · open · download
# TerraHawk flight parameters: the stall-test flight only (docs/stall-test.md). Load after the airframe file for
# that flight, and load params/terrahawk-common.parm again after it, so Q_ASSIST_SPEED returns to 18 or to the
# referred stall plus 1 m/s (the card's "After the test"). LCK_ENABLE 0 for the flight is the card's own step.
# An overlay: tools/checklist_code.py leaves its names out of the checklist gate, so the every-flight codes hold.
# Below the expected stall, so the speed assist does not hide the break (stall-test.md). 12, was 14: on Fuselage C
# the side ports read low near the stall, and th-aeronautics expects the wings-level break at about 14.0 indicated
# (16.8 EAS); in SITL every wings-level point ended on Q-assist at 14.3 indicated, alpha 12.1, before the break
# (th-testpilot, thread ee6d217f, 2026-10-19). 12 indicated is about 14.4 EAS, under the stall, so the assist stays
# the net after the break. LCK_ENABLE 0 is still the card's own step (an overlay row would ungate it on every flight).
Q_ASSIST_SPEED 12
params/sdcard/message-intervals-chan1.txt · 1 lines · open · download
245 500
params/sdcard/th-clr-list.txt · 181 lines · open · download
AHRS_OPTIONS
AHRS_ORIENTATION
AIRSPEED_CRUISE
AIRSPEED_MAX
AIRSPEED_MIN
AIRSPEED_STALL
ARSPD_AUTOCAL
ARSPD_BUS
ARSPD_OPTIONS
ARSPD_RATIO
ARSPD_TUBE_ORDR
ARSPD_TYPE
ARSPD_USE
BATT2_MONITOR
BATT_AMP_PERVLT
BATT_FS_CRT_ACT
BATT_FS_LOW_ACT
BATT_FS_VOLTSRC
BATT_MONITOR
BRD_SAFETY_DEFLT
CAM1_TYPE
COMPASS_AUTO_ROT
COMPASS_DISBLMSK
COMPASS_MOTCT
COMPASS_MOT_X
COMPASS_MOT_Y
COMPASS_MOT_Z
COMPASS_ORIENT
EK3_GSF_RUN_MASK
EK3_GSF_USE_MASK
EK3_IMU_MASK
EK3_SRC2_YAW
EK3_SRC_OPTIONS
FENCE_OPTIONS
FLTMODE1
FLTMODE2
FLTMODE3
FLTMODE4
FLTMODE5
FLTMODE6
FLTMODE_CH
FS_GCS_ENABL
FS_LONG_ACTN
FS_LONG_TIMEOUT
FS_SHORT_ACTN
GPS1_POS_X
GPS1_POS_Y
GPS1_POS_Z
GPS1_RATE_MS
GPS1_TYPE
GPS2_POS_X
GPS2_POS_Y
GPS2_POS_Z
GPS2_RATE_MS
GPS2_TYPE
GPS_AUTO_SWITCH
GPS_DRV_OPTIONS
GPS_PRIMARY
GRIP_ENABLE
INS_LOG_BAT_MASK
INS_POS1_X
INS_POS1_Y
INS_POS1_Z
LAND_PITCH_DEG
LCK_LANE_REQ
LOG_FILE_BUFSIZE
MAV2_ADSB
MAV2_EXTRA1
MAV2_EXTRA2
MAV2_EXTRA3
MAV2_EXT_STAT
MAV2_OPTIONS
MAV2_PARAMS
MAV2_POSITION
MAV2_RAW_CTRL
MAV2_RAW_SENS
MAV2_RC_CHAN
MIXING_GAIN
MSP_OPTIONS
OSD1_HORIZON_EN
OSD_TYPE
OSD_TYPE2
PTCH_LIM_MAX_DEG
PTCH_LIM_MIN_DEG
PTCH_RATE_FF
PTCH_RATE_I
PTCH_RATE_P
PTCH_TRIM_DEG
Q_ASSIST_ALT
Q_ASSIST_ANGLE
Q_A_RAT_YAW_P
Q_ENABLE
Q_FRAME_CLASS
Q_FRAME_TYPE
Q_LAND_FINAL_ALT
Q_LAND_FINAL_SPD
Q_M_BAT_VOLT_MAX
Q_M_HOVER_LEARN
Q_M_PWM_TYPE
Q_M_SPIN_MAX
Q_M_THST_EXPO
Q_OPTIONS
Q_PILOT_SPD_DN
Q_P_NE_POS_P
Q_P_NE_VEL_I
Q_P_NE_VEL_P
Q_RTL_ALT
Q_RTL_MODE
Q_TRANSITION_MS
Q_WVANE_GAIN
Q_WVANE_LAND
RC1_TRIM
RC2_TRIM
RC4_TRIM
RC_FS_TIMEOUT
RC_OVERRIDE_TIME
RC_PROTOCOLS
RELAY2_DEFAULT
RELAY2_FUNCTION
RELAY2_PIN
RLL_RATE_FF
RLL_RATE_I
RLL_RATE_P
ROLL_LIMIT_DEG
RTL_ALTITUDE
SCR_ENABLE
SCR_HEAP_SIZE
SERIAL2_BAUD
SERIAL2_PROTOCOL
SERIAL3_BAUD
SERIAL3_PROTOCOL
SERIAL4_BAUD
SERIAL4_PROTOCOL
SERIAL5_BAUD
SERIAL5_PROTOCOL
SERIAL6_BAUD
SERIAL6_PROTOCOL
SERIAL7_BAUD
SERIAL7_PROTOCOL
SERIAL8_BAUD
SERIAL8_PROTOCOL
SERVO10_FUNCTION
SERVO11_FUNCTION
SERVO12_FUNCTION
SERVO12_MAX
SERVO12_MIN
SERVO12_TRIM
SERVO1_FUNCTION
SERVO2_FUNCTION
SERVO3_FUNCTION
SERVO4_FUNCTION
SERVO5_FUNCTION
SERVO5_MAX
SERVO5_MIN
SERVO5_REVERSED
SERVO5_TRIM
SERVO6_FUNCTION
SERVO6_MAX
SERVO6_MIN
SERVO6_REVERSED
SERVO6_TRIM
SERVO7_FUNCTION
SERVO7_MAX
SERVO7_MIN
SERVO7_TRIM
SERVO8_FUNCTION
SERVO9_FUNCTION
SERVO_AUTO_TRIM
SERVO_BLH_BDMASK
SERVO_BLH_POLES
SERVO_DSHOT_ESC
STICK_MIXING
TECS_LAND_ARSPD
TECS_LAND_PMAX
TECS_PITCH_MAX
TECS_SPDWEIGHT
TERRAIN_ENABLE
THR_FAILSAFE
TH_AIRFRAME
TH_RAD_REQ
TRIM_THROTTLE
tools/check_params.py · 217 lines · open · download
"""Check parameter files against ArduPlane's own parameter metadata.
Usage: uv run --no-project python tools/check_params.py <apm.pdef.json> params/*.parm
Build the metadata in a private ArduPilot copy (never the SITL tree):
Tools/autotest/param_metadata/param_parse.py --vehicle ArduPlane --format json
Reports unknown names, values outside a documented range, and values not in a documented list.
Exits non-zero on any finding.
Second mode, after loading the aircraft: compare what the board holds with the files.
uv run --no-project python tools/check_params.py --loaded <saved.param> params/terrahawk-common.parm \
params/board-luxf765.parm params/airframe.parm
Save the board's parameters first (Mission Planner: Full Parameter List, Save to file). Give the
files in load order; a later file wins for a name that is in two files. Reports every name the board
does not have (load again after a reboot) and every value that differs.
"""
import json
import re
import sys
from pathlib import Path
def read_params(path):
"""name -> (value text, line) from a .parm file or a saved board dump (NAME,VALUE or NAME VALUE)."""
out = {}
for n, raw in enumerate(Path(path).read_text().splitlines(), 1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
parts = re.split(r"[,\s]+", line)
if len(parts) >= 2:
out[parts[0]] = (parts[1], n)
return out
if len(sys.argv) > 2 and sys.argv[1] == "--loaded":
board = read_params(sys.argv[2])
want = {}
for path in sys.argv[3:]:
for name, (value, n) in read_params(path).items():
want[name] = (value, f"{path}:{n}")
missing = differ = 0
for name, (value, where) in want.items():
if name not in board:
print(f"{where}: {name} not on the board (reboot, then load the files again)")
missing += 1
continue
a, b = float(value), float(board[name][0])
# the board stores float32, so 0.347 reads back as 0.34700000286
if abs(a - b) > max(1e-6, 1e-5 * abs(a)):
print(f"{where}: {name} is {board[name][0]} on the board, {value} in the file")
differ += 1
print(f"missing: {missing} different: {differ} checked: {len(want)}")
sys.exit(1 if missing or differ else 0)
meta = json.loads(Path(sys.argv[1]).read_text())
known = {}
for group in meta.values():
for name, info in group.items():
known[name] = info
def script_params():
"""Names our Lua scripts add (sim/scripts/*.lua): their PREFIX plus each name they add. ArduPlane's
metadata cannot know them, so a .parm row for one is checked against the script that declares it."""
names = {}
for lua in sorted((Path(__file__).resolve().parent.parent / "sim" / "scripts").glob("*.lua")):
text = lua.read_text()
m = re.search(r"PREFIX\s*=\s*(?:\d+\s*,\s*)?['\"]([A-Z0-9_]+)['\"]", text)
if not m:
continue
found = re.findall(r"\b(?:P|add)\(\s*\d+\s*,\s*['\"]([A-Z0-9_]+)['\"]", text) # P(i, 'NAME', d), add(i, "NAME", d)
t = re.search(r"local names\s*=\s*\{(.*?)\}", text, re.S) # local names = { 'A', 'B' }
if t:
found += re.findall(r"['\"]([A-Z0-9_]+)['\"]", t.group(1))
for suffix in found:
names[m.group(1) + suffix] = lua.name
return names
SCRIPT_PARAMS = script_params()
def expand(name):
"""Metadata uses templates like SERVOx_ or CAN_Dx_; try direct first, then digit-to-x. A script's own
parameter has no range or list to check."""
if name in known:
return known[name]
if name in SCRIPT_PARAMS:
return {}
for pattern in (re.sub(r"(?<=[A-Z_])\d+(?=_)", "x", name, count=1),
re.sub(r"\d+(?=_)", "x", name),
re.sub(r"\d", "x", name)):
if pattern in known:
return known[pattern]
return None
bad = 0
for path in sys.argv[2:]:
for n, line in enumerate(Path(path).read_text().splitlines(), 1):
range_ok = "range-ok" in line
value_ok = "value-ok" in line # a value the metadata's list omits on purpose (e.g. a board's own GPIO)
line = line.split("#", 1)[0].strip()
if not line:
continue
name, value = line.split()[:2]
info = expand(name)
if info is None:
print(f"{path}:{n}: unknown parameter {name}")
bad += 1
continue
v = float(value)
rng = info.get("Range")
if rng and not range_ok and not (float(rng["low"]) <= v <= float(rng["high"])):
print(f"{path}:{n}: {name} {value} outside {rng['low']}..{rng['high']}")
bad += 1
vals = info.get("Values")
if vals and not value_ok and not info.get("Bitmask") and str(int(v)) not in vals and value not in vals:
print(f"{path}:{n}: {name} {value} not a listed value")
bad += 1
# The airframe file sets elevon endpoints by output number, and the number belongs to the board: check them against
# the first-flight board named in sim/params_sync.py. On the NxtPX4v2 an old SERVO7/8 row would trim the pusher and
# the camera trigger, and leave the elevons on defaults (2026-10-04).
repo = Path(__file__).resolve().parent.parent
m = re.search(r'^FIRST_FLIGHT_BOARD\s*=\s*"([^"]+)"', (repo / "sim" / "params_sync.py").read_text(), re.M)
board_path = repo / "params" / m.group(1) if m else None
if board_path and board_path.exists():
funcs = {int(k[5:-9]): float(v) for k, (v, _) in read_params(board_path).items()
if re.fullmatch(r"SERVO\d+_FUNCTION", k)}
for path in sys.argv[2:]:
if not Path(path).name.startswith("airframe"):
continue
for name, (value, n) in read_params(path).items():
mm = re.fullmatch(r"SERVO(\d+)_(MIN|MAX|TRIM|REVERSED)", name)
if mm and funcs.get(int(mm.group(1))) not in (77.0, 78.0):
print(f"{path}:{n}: {name}: output {mm.group(1)} is not an elevon on {board_path.name} "
f"(function {funcs.get(int(mm.group(1)), 'unset')})")
bad += 1
# SD-card message interval files (params/sdcard/message-intervals-chan<N>.txt, read at boot from the card's root by
# GCS_MAVLINK::initialise_message_intervals_from_config_files). N is the MAVLink channel, counted over the serial ports
# whose protocol is MAVLink in port order with USB (SERIAL0, MAVLink2 by default) first, so a later SERIAL1 change
# renumbers it silently (th-hangar, 2026-10-12). The parser takes "<message id> <interval ms>" lines only: a comment line
# reads as message 0, HEARTBEAT, at interval 0.
LORA_SERIAL = 2 # the telemetry radio's port on the first-flight board (board file: SERIAL2, TELEM2)
if board_path and board_path.exists():
board = read_params(board_path)
mav_ports = [k for k in range(10)
if float(board.get(f"SERIAL{k}_PROTOCOL", ("2" if k == 0 else "-1", 0))[0]) in (1.0, 2.0)]
for f in sorted((repo / "params" / "sdcard").glob("message-intervals-chan*.txt")):
chan = int(re.search(r"chan(\d+)", f.name).group(1))
if LORA_SERIAL not in mav_ports or mav_ports.index(LORA_SERIAL) != chan:
print(f"{f}: channel {chan}, but SERIAL{LORA_SERIAL} is MAVLink channel "
f"{mav_ports.index(LORA_SERIAL) if LORA_SERIAL in mav_ports else 'none'} on {board_path.name} "
f"(MAVLink ports {mav_ports})")
bad += 1
for n, line in enumerate(f.read_text().splitlines(), 1):
if not re.fullmatch(r"\d+ \d+", line):
print(f"{f}:{n}: not '<message id> <interval ms>': {line!r}")
bad += 1
# The flight scripts' arming protection (AP_Scripting::arming_checks): SCR_LD_CHECKSUM/SCR_RUN_CHECKSUM refuse arming with
# a script missing, changed or dead, but the whole check returns "pass" when SCR_ENABLE is 0 or SCR_DEBUG_OPTS has bit 4
# (DISABLE_PRE_ARM) set, and the save-checksums bit 5 is easily set as 48 by mistake (th-hangar, 2026-10-12).
for path in sys.argv[2:]:
for name, (value, n) in read_params(path).items():
if name == "SCR_ENABLE" and float(value) != 1:
print(f"{path}:{n}: SCR_ENABLE {value}: with scripting off every scripting pre-arm, the checksums included, passes")
bad += 1
if name == "SCR_DEBUG_OPTS" and int(float(value)) & 16:
print(f"{path}:{n}: SCR_DEBUG_OPTS {value} has bit 4 (DISABLE_PRE_ARM): the script pre-arm and checksums are off")
bad += 1
# The checklist gate's list on the SD card must match the parameter files (tools/checklist_code.py): a stale list
# hashes the wrong names, so every code would be refused or, worse, an edited value would not be gated.
sys.path.insert(0, str(repo / "tools"))
import checklist_code # noqa: E402
if checklist_code.LIST_FILE.exists() and checklist_code.read_list() != checklist_code.gated_names():
print(f"{checklist_code.LIST_FILE.relative_to(repo)}: stale against the parameter files; run checklist_code.py list")
bad += 1
# The tick page's codes (th-clr-codes.json) must be the codes of the published files, or a pilot would be shown codes the
# aircraft refuses.
if checklist_code.CODES_FILE.exists():
import json # noqa: E402
if json.loads(checklist_code.CODES_FILE.read_text()) != checklist_code.codes_json():
print(f"{checklist_code.CODES_FILE.relative_to(repo)}: stale against the parameter files; run checklist_code.py list")
bad += 1
else:
print(f"{checklist_code.CODES_FILE.relative_to(repo)}: missing; run checklist_code.py list")
bad += 1
# The dock's interface texts (hangar.md cf59256d): a STATUSTEXT carries 50 characters, a longer text arrives in pieces,
# and the dock matches its texts (the 'gpsguard: GPS lost' prefix, 'tipguard: tipped over; disarmed', 'tipguard: armed')
# in the first piece only. So every text the GPS-loss and tip-over guards send, candidates included, must fit; one built
# with string.format cannot be measured here, so it is refused too. Other scripts' long texts are only noted.
STATUSTEXT_MAX = 50
# Known, with the fix that removes it; delete the entry when the fix is adopted, so the check counts it again.
KNOWN_LONG = {} # v2.4e's 61-character takeover text was the only entry; v2.4f's adoption removed it
send_re = re.compile(r"send_text\(\s*\d+\s*,\s*(?:'([^']*)'|\"([^\"]*)\"|(string\.format))")
for f in sorted((repo / "sim" / "scripts").glob("*.lua")):
interface = f.name.startswith(("gps_loss_guard", "tip_guard"))
for n, line in enumerate(f.read_text().splitlines(), 1):
if line.lstrip().startswith("--"):
continue
for m in send_re.finditer(line):
text = m.group(1) if m.group(1) is not None else m.group(2)
where = f"{f.relative_to(repo)}:{n}"
if text is None:
if interface:
print(f"{where}: a dock interface text built with string.format; its length cannot be checked")
bad += 1
elif len(text) > STATUSTEXT_MAX:
known = KNOWN_LONG.get((f.name, text))
if interface and known:
print(f"known: {where}: {len(text)} characters ({known}): {text!r}")
elif interface:
print(f"{where}: dock interface text is {len(text)} characters, over STATUSTEXT's {STATUSTEXT_MAX}: {text!r}")
bad += 1
else:
print(f"note: {where}: {len(text)} characters arrives in pieces: {text!r}")
print("findings:", bad)
sys.exit(1 if bad else 0)
tools/checklist_code.py · 156 lines · open · download
"""Checklist codes for th_prearm.lua's hard gate (th-avionics, 2026-10-13; docs/agent-threads/checklists/).
The one definition the aircraft's script, the test harness and the tick page share. A code says "this checklist was
cleared against these parameter values": when a gated value changes, the code no longer matches and arming is refused.
It is an interlock, not security; anyone with the list can compute a code.
The gated names: every parameter in the base files the first flight loads (the common file, the first-flight board
named in sim/params_sync.py, the airframe file), except any name an overlay file sets (params/payload-*.parm,
pack-*.parm, test-*.parm). Overlay values vary per flight by design (the stall test's Q_ASSIST_SPEED 14, the TFmini's
RNGFND1_TYPE), and the card that loads an overlay governs them. Also out: the names the aircraft rewrites itself
(CALIBRATED), which would make a code stale on their own. Names sorted; the list goes to the SD card.
The hash: FNV-1a 32-bit over each gated value as little-endian float32 bytes, in list order. ArduPilot stores
parameters as float32, so these are the bytes the aircraft holds, however a value is printed.
The code: the low 24 bits (6 hex digits) of FNV-1a 32-bit over the checklist id's ASCII bytes, then the hash's 4
little-endian bytes. 24 bits because TH_CLR_n is a parameter, and a float32 holds integers exactly only to 2^24.
th_prearm.lua computes the same from the values it reads.
uv run --no-project python tools/checklist_code.py list # writes th-clr-list.txt and th-clr-codes.json
uv run --no-project python tools/checklist_code.py codes # the gates' codes for the files
uv run --no-project python tools/checklist_code.py codes --values saved.param # for a board dump
From Python (the harness): checklist_code.codes(values) with values a dict of name to number read from the aircraft.
"""
import re
import struct
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PARAMS = REPO / "params"
LIST_FILE = PARAMS / "sdcard" / "th-clr-list.txt"
# the codes for the published files, for the tick page (th-docs-site fcf2bf69); a board whose gated values differ from the
# files gets other codes, so a published code can only arm a board that matches the files (it fails closed otherwise)
CODES_FILE = PARAMS / "sdcard" / "th-clr-codes.json"
OVERLAY_GLOBS = ("payload-*.parm", "pack-*.parm", "test-*.parm")
# rewritten on the aircraft, so the board differs from the file by design: AHRS_TRIM_X/Y by the level calibration
# (airframe.parm holds 0), Q_M_THST_HOVER learned and saved in flight (Q_M_HOVER_LEARN 2). COMPASS_MOT_X/Y/Z stay
# gated: tools/fit_compass_mot.py writes the fitted values into airframe.parm, so file and board agree.
CALIBRATED = {"AHRS_TRIM_X", "AHRS_TRIM_Y", "Q_M_THST_HOVER"}
# TH_CLR_n carries the code of gate n; the ids are the checklist definitions' ids (checklists/<id>.yaml)
GATES = {1: "before-power-up", 2: "bench-checks", 3: "failsafe-checks", 4: "night-build"}
FNV_OFFSET, FNV_PRIME = 2166136261, 16777619
def read_parm(path):
"""name -> value text from a .parm file or a saved board dump; a later row wins. Rows are NAME,VALUE or
NAME VALUE (our files, Mission Planner, MAVProxy) or QGroundControl's VEHICLE COMPONENT NAME VALUE TYPE."""
out = {}
for raw in Path(path).read_text().splitlines():
line = raw.split("#", 1)[0].strip()
parts = re.split(r"[,\s]+", line) if line else []
if len(parts) >= 4 and parts[0].isdigit() and parts[1].isdigit():
out[parts[2]] = parts[3]
elif len(parts) >= 2:
out[parts[0]] = parts[1]
return out
def base_files():
m = re.search(r'^FIRST_FLIGHT_BOARD\s*=\s*"([^"]+)"', (REPO / "sim" / "params_sync.py").read_text(), re.M)
if not m:
raise SystemExit("sim/params_sync.py names no FIRST_FLIGHT_BOARD")
return [PARAMS / "terrahawk-common.parm", PARAMS / m.group(1), PARAMS / "airframe.parm"]
def gated_names():
base = {}
for f in base_files():
base.update(read_parm(f))
overlay = set()
for pattern in OVERLAY_GLOBS:
for f in PARAMS.glob(pattern):
overlay |= set(read_parm(f))
return sorted(set(base) - overlay - CALIBRATED)
def file_values():
values = {}
for f in base_files():
values.update({k: float(v) for k, v in read_parm(f).items()})
return values
def fnv1a(data, h=FNV_OFFSET):
for b in data:
h = ((h ^ b) * FNV_PRIME) & 0xFFFFFFFF
return h
def params_hash(values, names):
missing = [n for n in names if n not in values]
if missing:
raise KeyError(f"no value for {len(missing)} gated parameter(s), first {missing[0]}")
h = FNV_OFFSET
for n in names:
h = fnv1a(struct.pack("<f", float(values[n])), h) # rounds to float32, as Lua does
return h
def code(checklist_id, values, names=None):
names = names if names is not None else read_list()
h = params_hash(values, names)
return f"{fnv1a(struct.pack('<I', h), fnv1a(checklist_id.encode('ascii'))) & 0xFFFFFF:06x}"
def codes(values, names=None):
names = names if names is not None else read_list()
return {n: code(cid, values, names) for n, cid in GATES.items()}
def read_list():
return [line.strip() for line in LIST_FILE.read_text().splitlines() if line.strip()]
def codes_json(names=None):
"""The th-clr-codes.json document for the published files: hex codes, and the decimal TH_CLR_n values to enter."""
names = names if names is not None else gated_names()
c = codes(file_values(), names)
return {"list_lines": len(names),
"codes": {GATES[n]: c[n] for n in sorted(GATES)},
"params": {f"TH_CLR_{n}": int(c[n], 16) for n in sorted(GATES)}}
def write_codes_json(names=None):
import json
CODES_FILE.write_text(json.dumps(codes_json(names), indent=2) + "\n")
def main(argv):
if not argv or argv[0] not in ("list", "codes"):
raise SystemExit(__doc__)
if argv[0] == "list":
names = gated_names()
LIST_FILE.write_text("\n".join(names) + "\n")
print(f"{LIST_FILE.relative_to(REPO)}: {len(names)} names")
write_codes_json(names)
print(f"{CODES_FILE.relative_to(REPO)}: codes for the published files")
return
names = read_list()
if names != gated_names():
raise SystemExit(f"{LIST_FILE.relative_to(REPO)} is stale: run 'checklist_code.py list'")
values = file_values()
if len(argv) >= 3 and argv[1] == "--values":
values = {k: float(v) for k, v in read_parm(argv[2]).items()}
missing = [n for n in names if n not in values]
if missing:
# a code from a partial dump could never match the aircraft, so give none
raise SystemExit(f"{argv[2] if len(argv) >= 3 else 'the files'}: no value for {len(missing)} of {len(names)} "
f"gated parameters (first {missing[0]}); not a full parameter download, or a format "
"this tool does not read")
for n, c in codes(values, names).items():
print(f"TH_CLR_{n} {int(c, 16)} # {c}, {GATES[n]}")
if __name__ == "__main__":
main(sys.argv[1:])

Source: params/README.md in the repo. Edit it there; this page is generated.

Comments on

    Comments are reviewed before they appear. Please leave out places, addresses and other personal details.

    A Supported Systems Flight

    This build log is written and kept by Supported Systems, a small studio that makes thoughtful, user-first technology. The airframe, the hangar and the simulation are ours.

    Visit supported.systems