Skip to content

Fly TerraHawk in SITL

SITL runs the real ArduPlane firmware against a physics model on a computer. The default SITL aircraft is a generic 4.5 kg quadplane with ailerons and a rudder. This directory replaces it with TerraHawk. Since 2026-10-18 the flown model is terrahawk-c24b-w78.json: Fuselage C v2.4B with our own wing, the wing fence and keel B, 3.034 kg at a CG of 268 mm on th-blender’s W78 mass rows (docs/sitl-model.md, Fuselage C). The earlier flight model, terrahawk-keel-notips.json, is the Fuselage B build on keel B, the decided landing gear: 2.85 kg with fuselage B, nose revision C, the Walksnail Moonlight, the Survey3W camera and the Gemfan F8041 3-blade lift props, with the pack at its drawn place and no ballast (CG at 269.5 mm). Keel A, retired from the print beds on 2026-10-11, is --model terrahawk-keela-notips.json (2.89 kg, CG at 267.2 mm with no ballast). That model has keel A’s mass, inertia and gear drag, but keel B’s crossflow terms, so it does not show a difference in side force between the two keels. With the night lights and their lighting battery (the Roaring Top 1300), use --model terrahawk-keel-notips-lights.json (2.99 kg) or --model terrahawk-keela-notips-lights.json (3.02 kg). In these two models the slider moves the pack to set the CG of 268 mm; on keel B it reaches only 268.2 mm at the forward stop. They do not include the drag of the strobe domes or the power of the lights, which comes from the lighting battery. It has elevons only, and the lift motors in the positions from tools/blender_scenes.py. It has the 6S2P Molicel P50B pack (10 Ah, with the data sheet discharge curve) and the CFD wing aerodynamics from aero/. The parameters come from the QuadPlane setup guide, so you practise the configuration that you will fly.

Use SITL to learn procedures, not stick feel. Practise QHOVER, QLOITER, transitions, VTOL missions, Q_assist and the failsafes before the airframe is at risk. For stick feel, use RealFlight with your own transmitter.

The container gives the same SITL session on a virtual desktop. You can fly it in a browser, and screenshots of it are repeatable.

The SITL desktop: satellite map of the CMAC field with TerraHawk on the ground, the MAVProxy console reporting pre-arm good, and the command prompt

  1. Copy .env.example to .env, then set SITL_USER and SITL_PASSWORD_HASH.

  2. Start the container:

    Terminal window
    cd sim
    make up
  3. Open https://sitl.terrahawk.l.warehack.ing and log in.

  4. Type the commands from Fly it in the MAVProxy window, at the bottom right.

The first make up builds ArduPilot in the image. This takes about 10 minutes. The container uses the files in the repo, so make restart starts a new session with your model and parameter changes. make shot NAME=transition saves the desktop to artifacts/screenshots/<date>/transition.png. A VNC client on this computer can connect to localhost:5904. A GCS on this computer can listen on UDP 14551. The firmware output goes to run/vnc/arduplane.log.

You do not need ArduPilot’s install-prereqs-ubuntu.sh for SITL. GCC and a Python environment with MAVProxy are sufficient. Clone ArduPilot next to this repo, not in it:

Terminal window
cd ~/claude/drone
git clone --recurse-submodules --shallow-submodules --depth 50 https://github.com/ArduPilot/ardupilot
uv venv --python /usr/bin/python3 --system-site-packages sitl-venv
VIRTUAL_ENV=$PWD/sitl-venv uv pip install empy==3.3.4 pexpect future pymavlink MAVProxy opencv-python-headless lxml numpy
VIRTUAL_ENV=$PWD/sitl-venv uv pip install --no-deps "mcardupilot>=2026.10.3"
VIRTUAL_ENV=$PWD/sitl-venv uv pip install "pydantic-settings>=2.15.0"
cd terrahawk/sim && make build

The venv uses --system-site-packages on purpose. The MAVProxy console and map need wxPython, and PyPI has no Linux wheels for it. The Arch package python-wxpython supplies it for the system Python, and the venv uses that package. The version of empy is 3.3.4 because the ArduPilot MAVLink generator does not work with empy 4. The build takes about one minute on this computer.

The Makefile looks for ArduPilot at ../../ardupilot and for the venv at ../../sitl-venv. To change these paths, use make fly ARDUPILOT=... VENV=....

The harness scripts use the mcardupilot library to start and stop SITL and to make the Dryden gusts. Install mcardupilot 2026.10.3 or later from PyPI (https://pypi.org/project/mcardupilot/, documentation at https://mcardupilot.warehack.ing). Install it without its dependencies, as shown above. Its MCP server dependencies are not necessary for the harness. If you have access to its repository (private, in the warehack.ing organisation on git.supported.systems), you can install an editable clone instead: uv pip install --no-deps -e <clone>. If the import of mcardupilot.turbulence fails, the installed version is older than 2026.10.3. Run the scripts with ../../sitl-venv/bin/python. To use uv run instead, add --with mcardupilot.

To build the SITL binary in a container, use make sitl-build. build-spec.json gives the inputs: the ArduPilot commit, the patches and the build image, which is pinned by its digest. The build uses a new clone of the commit, not the ArduPilot tree. It builds the binary two times and stops if the two binaries are not identical. It writes the build record to builds/<build_id>.json. Commit that file. The harness scripts fly this build by default: they use the build whose record matches build-spec.json and the patch files as they are now. If you change a patch, run make sitl-build again before you fly. Until you do, the scripts stop with an error, because no build contains the change. To fly an in-tree build (for example while you develop a patch with make build), set ARDUPILOT=../../ardupilot. make fly always uses the tree, because MAVProxy and sim_vehicle.py are in the tree. MCP sessions (the mcardupilot server in .mcp.json) fly ~/.local/share/mcardupilot/builds/terrahawk-current. make sitl-build moves that link to each new build after the build reproduces. To open a TerraHawk session with one argument, use open_sitl(preset="<repo>/sim/presets/terrahawk.json"). The preset gives the flight model, the parameter files, the harness overlay and the boot mode. make presets writes it again from the models, and make params stops with an error if a model changed and the preset did not. Each battery result and each flight_check report records the build_id and the sha256 of the binary that flew.

Each SITL instance has a lease. A lease is a lock file in ~/.local/share/mcardupilot/leases. Two runs cannot use the same instance at the same time. Also, a run does not start on an instance whose ports are in use. To see which runs hold instances, use ../../sitl-venv/bin/python -m mcardupilot.leases status. Instance 0 is for autotest.py. Do not use instances 14 and 15, because the desktop containers use their VNC ports.

Terminal window
cd sim
make fly

Three windows open:

  • The terminal becomes the MAVProxy prompt. You type commands here. The prompt shows the current flight mode.
  • The console shows the mode, arm state, pack, GPS and airspeed at the top. Below them, it shows messages from the FC.
  • The map shows the aircraft at the CMAC field near Canberra, the default SITL home. It shows the track and the mission.

Wait approximately 30 seconds until the console shows EKF3 IMU0 is using GPS. Then arm the aircraft.

Type all the commands below at the MAVProxy prompt. SITL has no transmitter, so rc <channel> <pwm> replaces the sticks. Channel 1 is roll, 2 is pitch, 3 is throttle and 4 is yaw. The value 1500 is centre. The throttle starts at 1000.

First flight: let the autopilot fly a mission

Section titled “First flight: let the autopilot fly a mission”

The aircraft takes off vertically to 40 m and does a transition. It flies a 400 m box at 60 m, comes back, and does a transition to hover. Then it lands. The flight takes a little more than three minutes. The console messages show the events that the manual describes, and SITL is the safest place to learn them.

AUTO mission at the transition: the aircraft climbing out of its VTOL takeoff toward waypoint 2 at 18 m/s and 22 A, with Transition airspeed reached in the console

AUTO mission in cruise: the 400 m box on the map with the flown track, the console at 24 m/s and 9.8 A, and the three commands that started it

AUTO mission landing: the console lists VTOL approach, airbrake, position1, position2 and Land descend started as the aircraft descends over home

  1. Load the mission, set AUTO and arm:

    wp load ../../missions/cmac-box.waypoints
    mode auto
    arm throttle
  2. Monitor the console. Look for Transition airspeed reached, then Transition done.

  3. Look for VTOL airbrake, Land descend started and Land complete at the end.

In CRUISE, the FC holds the height and the heading. The throttle sets the airspeed: 1500 gives the middle of AIRSPEED_MIN and AIRSPEED_MAX, approximately 25 m/s. The roll stick changes the heading. Thus CRUISE is the correct mode for your first transitions. QRTL climbs or descends to Q_RTL_ALT, 40 m, flies back in fixed-wing mode and slows down with the lift motors. Then it lands vertically and disarms. terrahawk.parm limits the bank to 45 degrees; with ArduPilot’s 65 the first turn of a QRTL from 50 m put the aircraft into the ground in SITL.

QHOVER at 51 m after the climb with rc 3 1700 then rc 3 1500, drawing 16.5 A

CRUISE after a transition from QHOVER: the track curves into a 25 degree banked turn at 22 m/s, with the commands that flew it at the prompt

After mode qrtl: the console shows the VTOL approach and SIM Hit ground at 0.30 m/s, then Land complete and Throttle disarmed

  1. Type these commands, one at a time:

    mode qhover
    arm throttle
    rc 3 1700 # climb, about 1 m/s
    rc 3 1500 # hold height once you are around 50 m
    mode cruise # transition: pusher runs up, lift motors stop after 18 m/s plus 5 s
    rc 1 1650 # bank right; rc 1 1500 levels the wings and holds the new heading
    mode qrtl # fly home, convert to hover and land
  2. After mode qrtl, do not give more commands. The aircraft lands and disarms.

Since 2026-10-18 the common file sets PTCH_TRIM_DEG 5. On the Fuselage C model a hands-off FBWA transition then loses 1.0 m at most, against up to 10.7 m at 0 (th-testpilot dd6577cd, 126 transitions). The first-flight card sets the procedure for the aircraft. The earlier results below explain why the trim matters.

CAUTION: With PTCH_TRIM_DEG 0, FBWA transitions lose height fast when the lift motors stop.

FBWA holds the wings level and holds a pitch attitude. It does not hold the height. At 18 m/s, TerraHawk must fly at approximately 8 degrees angle of attack. With the sticks at centre, the aircraft descends. These are the SITL results for a transition in FBWA from 55 m, with the sticks at centre, measured on the earlier model with the earlier 20 degree, MIXING_GAIN 0.5 configuration:

Throttle PTCH_TRIM_DEG Result
rc 3 1500 0 The transition does not complete. The aircraft stays at 15.4 m/s on the lift motors.
rc 3 1700 or rc 3 1850 0 The transition completes. Then the aircraft descends into the ground in 5 s.
rc 3 1700 5 The aircraft descends into the ground.
rc 3 1850 5 The aircraft loses 40 m, then climbs at 25 m/s.

To practise FBWA transitions, keep PTCH_TRIM_DEG at the file’s 5, set rc 3 1850, and start at 100 m or higher.

Other useful commands:

  • mode shows a list of the modes.
  • disarm stops the motors. It operates only on the ground.
  • param show Q_ASSIST* shows parameters. param set NAME VALUE changes a parameter.
  • wp list shows the loaded mission.

SITL keeps your parameter changes in run/fly/eeprom.bin, or run/vnc/eeprom.bin in the container. To go back to the file values, use make wipe. Mission Planner or QGroundControl can connect at the same time on UDP 14550.

make stock runs the generic ArduPilot quadplane with the same controls. Use it to find if a problem comes from TerraHawk or from ArduPilot.

Terminal window
make check

First, make check runs make params. This compares terrahawk.parm with the aircraft file ../params/terrahawk-common.parm. If a parameter is in the two files with different values, the check stops. Change the value in the two files. If the difference is necessary, add the parameter and the reason to ALLOWED in params_sync.py. The harnesses, make fly and the container load ../params/terrahawk-common.parm first and terrahawk.parm on top. Thus SITL flies the aircraft’s failsafe rows and its other common rows. The check lists the rows that come only from the common file. The check also makes sure that SIM_GPS1_POS_* is equal to the aircraft’s GPS1_POS_*. If SITL’s GPS is not at the antenna, crosswind dock landings move up to 0.3 m.

This flies one mission without a display, at ten times real time, on the flight model terrahawk-c24b-w78.json. To fly a different model, use --model (for example --model terrahawk-keel-notips.json, the earlier flight model). The mission has a VTOL takeoff to 40 m, a transition, a 400 m box at 60 m, a transition back and a VTOL landing. For each phase, the output shows the result and the model prediction. A repeat usually gives the same numbers to the last digit shown, but a flight is not exactly repeatable: SITL receives the commands at times that change with the load on the computer. Thus a change to tools/sitl_model.py, terrahawk.parm or the aero data shows as a clear difference only if it is larger than that spread. For a small change, fly three times or more on each side and compare the sets (mcardupilot’s batch_compare). On the earlier flight model terrahawk-keel-notips.json, with the F8041 3-blade lift props (models 107bdf8, Q_M_SPIN_MAX 0.82 and Q_M_THST_EXPO 0.33 from 9391676: keel B, 2.85 kg, CG at 269.5 mm with no ballast, elevons trimmed to 1378 us, the APC 10x7E pusher; SITL build 58fe10ee1a0c), the results are:

Phase Result
Hover learned Q_M_THST_HOVER 0.309; 31.5 A in the VTOL climb, 24.5 A in the VTOL landing
Transition 18 m/s after 4.9 s, complete at 9.9 s
Cruise 22.3 m/s average at 60% throttle and 6.7 A
Level cruise, 22 m/s 6.1 deg AoA and 8.0 deg of up elevon, against a predicted 5.5 and 7.5
VTOL landing touchdown at 0.3 m/s, disarmed 152 s after the VTOL landing item starts at 60 m

Set these parameters at the MAVProxy prompt with param set, or in the parameter list of the GCS. make wipe resets them.

Climb to 100 m in QHOVER. Set rc 3 1850 (PTCH_TRIM_DEG is 5 in the file), then mode fbwa and wait for Transition done. Set rc 3 1000 and hold the nose up with rc 2 1650. The aircraft slows from 26 m/s. Near 17 to 18 m/s the wing stalls: the up elevon that trims it costs lift, so the trimmed stall is 16.7 to 17.8 m/s in the model (stall blend 100 and 50), not the 13.4 m/s of the clean wing. stall_probe.py measures it. The nose drops to approximately 45 degrees down.

Q_assist drill: after the FBWA transition and rc 3 1000 the console counts the height down from 106 m, then Alt assist below 25 m and the lift motors start

In SITL the angle assist (Q_ASSIST_ANGLE 45 when this was flown; 30 since 2026-10-01, so it now starts earlier) then starts the lift motors, the speed assist follows at 18 m/s, and the aircraft hovers after a loss of approximately 60 m. terrahawk.parm also sets Q_ASSIST_ALT 25, which is not in the guide: if the descent gets below 25 m the lift motors start on height. To see the aircraft without the assists, set Q_ASSIST_ANGLE 0, Q_ASSIST_SPEED 0 and Q_ASSIST_ALT 0, climb to 100 m and do the test again. The model page explains the stall.

Set SIM_WIND_SPD 8 and SIM_WIND_DIR 270. For gusts, also set SIM_WIND_TURB 2. Hover in QLOITER and monitor the tilt. Do one transition downwind and one upwind. Compare the time for each.

QLOITER hover at 31 m in an 8 m/s crosswind with gusts: 3 degrees of pitch and 1 degree of roll, 46% throttle

In SITL, a hover in this wind needs 0 to 7 degrees of pitch and 1 to 2 degrees of roll. To see the correct hover attitude, the container needs a make up after 2026-09-26, because the patch changed.

In a hover, set SIM_ENGINE_FAIL 16 and SIM_ENGINE_MUL 0. Bit 4 is servo 5, the front-right lift motor. A quad cannot hold yaw on three motors. At once, set mode cruise and rc 3 1900. The pusher accelerates the aircraft and the wing lifts it. To restore the lift motor, set SIM_ENGINE_MUL 1.

Lift motor failure at 150 m: mode cruise and rc 3 1900 typed at once, transition done, and the aircraft climbing through 102 m at 27 m/s on the wing

In SITL, this reaction one second after the failure lost 50 m from 150 m, and got down to 16 m from 70 m. From 40 m the aircraft hit the ground before the transition could start. QLAND on three motors is a crash. Thus, hover at 70 m or higher when you practise this, and react at once.

Fly a long QLOITER until the pack is low. The pack is the P50B, and the failsafe uses the voltage with the sag removed (BATT_FS_VOLTSRC 1) or the charge used, whichever comes first. At 4500 mAh remaining or 21.6 V (45%), the failsafe starts RTL. At 3000 mAh remaining or 20.4 V (30%), it starts QLAND. The 30% critical level keeps a QLAND from 30 m in an 8 m/s wind at or above 3.0 V a cell under load (thread test-battery, 2026-10-01). To make the drill shorter, set SIM_SPEEDUP 10, and set it back to 1 after the landing.

Pack failsafe: Battery 1 is low at 20.92 V after 3168 mAh, RTL, a transition in place and a vertical landing at flight time 11

With the earlier Tattu 6S 4500 model and its 21.0 V threshold, a QLOITER hover at 30 m reached the failsafe after 11 minutes, with 3168 mAh used. RTL then did a transition in place and landed the aircraft vertically. This has not been measured again with the P50B.

lua_charge_test.py full|short|cell flies a short hover, lands, and charges on the pad through tools/charge_interlock.lua. scripts/fake_charge_node.lua is a test-only stand-in for the charge node. It feeds battery instances 2 and 3 (BATT2_MONITOR and BATT3_MONITOR 29), and the test sets the cell voltages and the charge current with the TCN_ parameters. The landing does not reset the autopilot’s battery count (disarm(reset_count=False)), so only the interlock’s full-charge reset can bring it back. BATT_LOW_MAH is 9800 in this test only, so a relaunch without the reset is refused. In full, the cells taper at 4.16 V and 0.3 A for 40 s: the count resets to 0 and the relaunch arms. In short (20 s) and cell (one cell at 4.10 V), the count stays and the relaunch is refused. Never load the fake node or the BATT2/3_MONITOR 29 rows on the aircraft.

terrahawk.parm models a downward rangefinder, the Benewake TFmini-S from Landing and takeoff sensing, with SITL’s own sensor: RNGFND1_TYPE 100, RNGFND1_MAX 7 and RNGFND_LANDING 2, which makes VTOL landings use it. To land without it, set RNGFND_LANDING 0. To remove the sensor, set RNGFND1_TYPE 0 and type reboot.

QRTL landing with the rangefinder: the console shows Rangefinder engaged at 13.18m on the approach and Land final started at 4 m

The same QRTL landing with RNGFND_LANDING 0: Land final started at 2 m, and the RNG indicator in the console header is grey

  1. Climb to 30 m in QHOVER and set mode qland, with RNGFND_LANDING 2 and then with 0.
  2. Climb to 50 m, mode cruise, then mode qrtl, with 2 and then with 0.
  3. Compare the console. With the sensor, look for Rangefinder engaged at about 7 m on the approach.

In SITL the sensor changes only the last metres. The approach and the touchdown speed are the same. These results are for a 13.5 m sensor, the TF02-Pro first chosen. With the sensor, the slow-down to the 0.3 m/s final descent starts about 2 m higher (10 to 6 m at 1.1 m/s, not 1.3; 6 to 3 m at 0.4 m/s, not 0.6), the final descent starts at about 4 m instead of 2.5 m, and touchdown comes 4 s later, at 0.29 m/s against 0.30. The SITL barometer does not drift, so the true-height reference the sensor gives a real aircraft does not show here. The landing detector is not affected: Land complete came 6 s after touchdown in every flight, with the sensor, without it, and with no sensor configured. With the TFmini-S range of 7 m, now in terrahawk.parm, the sensor engages at 6.8 m and the QLAND profile is the same as without it: 1.3 m/s from 10 to 6 m, 0.65 from 6 to 3 m, 0.3 to touchdown.

make hangar flies the landing and takeoff matrix for the hangar pad without the desktop: 0, 3, 5 and 8 m/s from ahead, across and behind, calm and gusty, with the box disturbances from docs/agent-threads/hangar-aero/ switched by true height. Each landing is scored against the pad window and the touchdown limits in that JSON; each takeoff by its drift before it clears the wall tops. Results go to run/hangar/*.csv. One instance flies the takeoffs and one the landings, about 12 minutes at 4x.

In SITL (2026-09-27, the original airframe model), every landing from ahead or across passed up to 5 m/s, gusty included, within 100 mm of the pad. At 8 m/s from ahead the aircraft touches down 10 to 12 degrees nose down, at the stilt sole limit, and at 0.4 to 0.6 m/s. Wind from behind is not a landing direction: the aircraft lands 0.3 to 0.9 m aft at 3 to 5 m/s and the hover runs away downwind at 8 m/s, because the controller pitches the wing into the reversed flow. Land nose into the wind. All takeoffs cleared the walls with less than 35 mm of drift, then drifted downwind 0.6 to 7 m in the next 3 m of climb. To fly one cell, or a gusty cell three times: python hangar_envelope.py landing --only 5ms-side --repeat 3.

Since 2026-10-08 each landing is scored against th-hangar’s capture window in parts/hangar/landing-window.json, read at each flight. dx_mm and dy_mm are the offset from the aim point (x port, y aft). in_capture_fixed_prop uses the aft limit of the fixed APC pusher and sets the result; in_capture_folding uses the folding pusher’s and is the conservative check. The rule is for the feet centre (the window’s feet_centre_y_kit, 276 mm, from the gear definition); the harness measures the CG and moves dy aft by that less the model’s flown CG.

--box-wind sitl moves the box effects into SITL (sim/patches/sitl-box.patch: SIM_BOX_ENA 1 for the height gates, 2 for the CFD table, SIM_BOX_YAW, SIM_BOX_SHLT, and a box.json the harness writes into the run directory). SITL evaluates them on the physics step, so a flight through the box repeats exactly, and the gusts are SITL’s Dryden (sim/patches/sitl-dryden.patch). The box is fixed at home, and the harness locks home there before each case. The default, --box-wind host, keeps the writes from the harness. The tracked build does not carry these two patches yet; on it the harness stops and names the patch. With the CFD table (--pad-wind cfd) the host model needs --param SIM_WIND_TC=0.1. SITL filters the wind’s speed and direction separately, so at the default 0.8 s the reversal over the pad swings through a crosswind and reaches the aircraft late. The SITL box needs no such setting.

--gusts dryden-sitl, the default since 2026-10-09 (hangar_envelope and dock_approach; --gusts dryden keeps the host process), runs the same MIL-F-8785C gusts inside SITL (SIM_DRYDEN_*), sized and aligned by the free stream, seeded per case (the run’s --gust-seed plus the case count, in sitl_gust_seed), with no parameter writes per tick. The dock’s anemometer is then its own Dryden process at 2.6 m, seeded from the case’s seed plus 1000 on a fixed 20 Hz grid of sim time, as the host model’s is; gust_source says which. The *_decision columns judge the 2 minutes before the decision to descend, which is when the dock decides; the columns without the suffix keep the 2 minutes before touchdown, as flown before 2026-10-08. A window under 115 s gives no verdict.

Ryan’s lift testbed is a 10 in quad (HGLRC MY10) with TerraHawk’s lift parts: the EMAX 2807s, the F8041 3-blade props, the LUX F765, the Lucid and the P50B pack. make testbed writes two SITL models of it from th-blender’s checks (parts/review/testbed-my10-checks.json) and the flown model’s lift block: testbed-quad.json as built (1.54 kg) and testbed-quad-ballast.json at the flown model’s mass, so it hovers at the aircraft’s load per motor. The lift block is rescaled by the mass ratio (hoverThrOut with it, refCurrent with its 1.5 power), which keeps SITL’s motor constants unchanged. All wing, fin and pusher terms are zero. Pass --model testbed or --model testbed-ballast to the harness, or model="testbed" to the battery. It then flies on the aircraft’s parameter files: params/terrahawk-common.parm, params/board-luxf765.parm and params/testbed-quad.parm, then testbed-sim.parm, in place of terrahawk.parm. testbed-sim.parm has only SITL wiring and simulator rows, so the Q_A, Q_M and Q_P rows the testbed flies are the aircraft’s. It moves the lift motors to outputs 5 to 8, puts the plane throttle on output 3 and sets the current scale to SITL’s 17. SITL’s forward motor reads an unassigned output 3 as full reverse thrust: the testbed then leaned 30 deg and drew double current. The harness boots a testbed on mode switch position 1 (QSTABILIZE), whatever the other positions hold: SITL’s channel 5 rests on position 6, which was QRTL until th-avionics’ 0a903e6 (now QLAND), and a QRTL before home stops SITL. Hovering at 5.7 m in QLOITER, the bare testbed drew 10.6 A (th-aeronautics predicts 10.0 to 11.7 A); the ballasted one drew 28.0 A and learned a hover throttle of 0.286, as TerraHawk’s model does (28.0 A, 0.284). make check checks that SITL’s GPS and IMU sit where testbed-quad.parm puts them, and that the two model files match the generator. The models are estimates: the frame is traced from photos to +-5 %, the inertia is a lumped-box estimate to +-25 %, and the drag area is a guess.

dock_approach.py --align flies the approach the hangar page specifies: takeoff from the pad, cruise out, QRTL home, and at the start of the descent (30 m) a yaw onto the box axis, nose into the along-axis wind, held to touchdown. The rudder stick stands in for the yaw command a dock script would send. Q_WVANE_LAND 0 keeps the weathervane out of the landing. Without --align it flies stock QRTL for comparison. --hold 0.8,5 pauses the descent at 0.8 m, --inflight NAME=VALUE sets a parameter after the takeoff (GPS drift or noise), and the results go to run/dock/. Channel 3 stays at 1500 µs the whole flight unless --throttle-hold says otherwise. --throttle-hold 1000 holds 1000 µs from the AUTO outbound (or the return; with --launch auto, from the arm) to touchdown, as a site node would that kept its ground value in the air. --throttle-hold node flies th-hangar’s node rule (hangar.md 101719b) from EXTENDED_SYS_STATE: 1000 µs while disarmed or ON_GROUND, 1500 µs in every other armed state, including no message for 2 s. ArduPlane streams that message only on request, so the harness asks for it at the node’s 2 Hz (--node-ess-hz), as the node must after every aircraft boot. --throttle-hold node-no-ess is the node that never hears it, so it goes to 1500 at the arm. With a stick launch the rule takes over at 50 m. --launch auto is the dock’s launch: the mission loaded, AUTO set and seen in the heartbeat, then the arm, with a NAV_VTOL_TAKEOFF to 50 m. The harness reads the pad before arming, because the takeoff starts with the arm. The options add throttle_hold, node_rc3 (each change: time, µs, landed state) and the takeoff columns. On the calm case from ahead (2026-10-11, the battery’s flags, the adopted Lua, build 03e25c9367e6, one flight each): the default and the stick-launch node land 3 cm apart and both pass. With --launch auto, node, node-no-ess and a fixed 1000 µs take off the same way: lift-off 0.9 s after the arm, 49 m at 20.4 s, 0.10 m drift, a peak climb of 2.88 m/s. node and node-no-ess both pass the landing. At 2 Hz the switch back to 1000 µs at ON_GROUND comes before the disarm, calm and in 5 m/s gusts from ahead (both pass). At 1000 µs in the air the axis brake’s QLOITER hold sinks about 2 m/s from 50 m and touches down 72 to 73 m from the pad, with no descent to the pad.

In SITL (2026-09-27, keel model) the aligned approach passes every calm and gusty case from ahead and across up to 5 m/s, arriving along or across the box, aligned within 2 s at 28 m. Stock QRTL lands 85 to 88 degrees off the axis in a crosswind. With the wind over the parked lid, every landing touches down 0.4 to 1.4 m toward the lid (the shelter step), and a pause at 0.8 m fixes only half of them. With GPS noise of 1 m the aircraft lands 2 to 3 m off; with 2 cm (RTK) half of the calm landings are 130 to 145 mm off, because the position controller’s own scatter is about 100 mm. docs/images/sitl/dock-approach-touchdowns.png shows the touchdowns against the window.

landing_height.py --offset <m> [--source gps] flies one VTOL landing on the EKF’s height alone: it hovers at 30 m, ramps both simulated barometers by the offset over 20 s (SIM_BARO_DRIFT), and lands with QLAND, optionally after switching the EKF to source set 2 with GPS height. It appends the EKF error, the true height where the slow final descent starts, SITL’s touchdown speed and the time to disarm to run/landheight/results.csv. The first build has no rangefinder, so terrahawk.parm has it off; pass --param RNGFND1_TYPE=100 --param RNGFND_LANDING=2 for the TFmini-S payload build.

lateral_tests.py <model>.json flies two tests on one of the model files in sim/ (terrahawk.json, -stilts, -stilts-notips, -keel): a 5 m/s crosswind switched on in one step in FBWA at about 18 m/s, with roll and yaw logged for 10 s, and a QLAND from CRUISE in an 8 m/s crosswind, with heading and drift logged to touchdown. A third test, --only spiral, banks the aircraft and lets go for 20 s, in FBWA and then in MANUAL. lateral_plot.py [model ...] [--suffix name] then draws the chosen models on docs/images/sitl/lateral-gust-step.png, lateral-decrab.png and lateral-spiral.png and prints the settling times. Each test starts a fresh SITL, because a QLAND kilometres from home hung at 0.2 m without a landing report.

In SITL (2026-09-27) all four variants weathervane into the gust by 12 to 14 degrees with no oscillation on the stock loops; the original airframe model is the slowest and rolls the most (1.3 degrees against 0.4 to 0.7), and no variant needed a parameter change. QLAND from cruise keeps the crab heading to touchdown: ArduPlane does not decrab, so a crosswind arrival at the hangar must yaw the aircraft onto the box axis before the descent.

In each flight phase, set SIM_RC_FAIL 1. Monitor what the aircraft does.

Radio failsafe in CRUISE: RC Short Failsafe switches to Circle, RC Long Failsafe switches to RTL, and the track turns for home

In SITL, in CRUISE, the FC switched to CIRCLE after the short failsafe and to RTL after the long failsafe. Then it flew home and landed vertically. Set SIM_RC_FAIL 0 to restore the radio.

SITL’s airspeed sensor normally reads the true dynamic pressure. The real aircraft’s static source does not sit at free-stream pressure: th-aeronautics’ CFD puts Fuselage C’s side ports at a pressure coefficient Cp that rises from 0.08 at 0 degrees of angle of attack to 0.40 at 12.5 degrees, so the sensor reads q(1 - Cp). SIM_ARSPD_CP (the sim/patches/sitl-arspdcp.patch patch, in the tracked build since 2026-10-19, bd782d740932) applies that: 1 the side ports alone, 2 the tube static, 3 the 50/50 blend, from static_cp.json, which make static-cp writes from aero/airspeed/static-cp-c-v24b.json and make params checks. The harness places the file in every run directory. Alpha is the true body angle of the air-relative velocity, gusts included, and Cp is held at the table’s end values outside it. The SACP log has alpha, Cp and the pitot speed before and after. Since 2026-10-19 (d854b569) sim/terrahawk.parm sets SIM_ARSPD_CP 1, SIM_ARSPD_CPR 1 and the aircraft’s ARSPD_RATIO 2.3, so every flight flies the modelled sensor; set SIM_ARSPD_CP 0 and SIM_ARSPD_CPR 0 for the ideal one. A SITL started from these files outside the harness or a preset needs static_cp.json in its run directory (run_files), or it stops at boot. The autopilot’s ARSPD_RATIO is then the variable under test. SIM_ARSPD_CPR 1 (the tracked build bd782d740932) adds the lift rotors’ downwash at the same ports, from the table’s rotor block, which static_cp.py writes from aero/airspeed/rotor-static-c-v24b.json; it needs SIM_ARSPD_CP set, and SITL stops at boot if the selected table has no rotor block. On the calm dock case (2026-10-18), the true alpha through the transition’s acceleration was -1.1 to -0.4 degrees. That flight used the first table (6bbf590f, 0 to 14 degrees), so Cp held at 0.079 there; since 95607e0e the tables reach -5 degrees (a transition crossed -4.7), with the -5 value held below it. In cruise at ARSPD_RATIO 2.0 the sensor read 24.6 m/s against 28.7 true.

CAUTION: Do not load terrahawk.parm onto the FC. The SITL outputs are different from the real outputs, so the motors and servos will not match.

The outputs are different. The SITL plane model reads the elevons on outputs 1 and 2, the pusher on 3, and the lift motors on 5 to 8. On the real TerraHawk, S1 to S4 are the lift motors, S5 is the pusher, and S6 and S7 are the elevons. Refer to step 3 of the setup guide. The top of terrahawk.parm also gives this caution.

Q_M_PWM_TYPE is 0 in SITL and 5 (DShot300) on the aircraft. The rate gains in terrahawk.parm are ArduPilot’s defaults scaled for the sim’s control authority. Do not put them on the aircraft. Do an AUTOTUNE flight after the elevon and MIXING_GAIN change. SITL models the pusher as one of its own rotor motors, pointed forward, from patches/sitl-terrahawk.patch. Its static thrust, outflow speed and current are derived for a V3115 with the APC 10x7E from the propulsion page, not bench data. For the source of the numbers in terrahawk.json, and for the values that are still estimates, refer to docs/sitl-model.md.

From the kit’s makers, Nomadium Robotics, in their Pathfinder series:


Source: sim/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