Skip to content

Clear to Launch

A pre-arm check that refuses to start the lift motors at the hangar while the pad’s people guard is set, optionally under an overhead obstruction if an upward rangefinder is fitted, and a landing hold that waits over the pad until that guard clears. In the baseline build the aircraft carries no sensor for either check: Ryan dropped the people radar and then the upward sonar on 2026-09-27, both for drag, after seeing their pods in Blender. All people detection is the hangar’s radar ring, the hangar’s approach corridor is kept clear by design, and away from the hangar the pilot is responsible for clearing the area and the sky above it before arming. The upward sonar stays on this page as a future option the script already supports. Requested by Ryan through the lidar session on 2026-09-27; the hangar side is th-hangar’s (agent-threads/hangar-people-guard/). This page is the aircraft side. Everything about ArduPilot below was read in the source at master 4c98c92, the commit Plane 4.7.1 was cut from. Nothing is ordered; Ryan approves purchases. The landing rangefinder itself is on landing-sensing.md; it is not in the first build (Ryan, 2026-10-05), so the rangefinder node and the “rangefinder under 0.2 m” contact test below are for a later build; the first build detects contact without it.

Need Plane 4.7 Where
A script that can refuse arming Yes: arming:get_aux_auth_id(), arming:set_aux_auth_passed(id), arming:set_aux_auth_failed(id, reason). The reason text reaches the GCS as the pre-arm message AP_Scripting bindings
A script that reads a MAVLink message from the dock Yes: mavlink:init, mavlink:register_rx_msgid(252), mavlink:receive_chan(); the raw header carries the sender’s system id, so the dock’s messages are told apart from the GCS’s. No decoder module exists for NAMED_VALUE_INT; it is three fields, decoded by hand bindings, modules/MAVLink/
A serial sensor on a CAN node read by the FC Yes, the DroneCAN serial tunnel: CAN_D1_UC_SER_EN, CAN_D1_UC_S1_NOD (node id), S1_IDX (which FC SERIALn becomes the virtual port), S1_BD, S1_PRO. Compiled on boards over 1 MB, so on the Lucid. AP_Periph has the node end in serial_tunnel.cpp AP_DroneCAN_serial.cpp, AP_DroneCAN.cpp
A script reading that port Yes: SERIALn_PROTOCOL 28 (scripting) on the tunnelled index, then serial:find_serial(0) bindings
Custom DroneCAN messages from a node into Lua Yes, DroneCAN_Handle with subscribe and check_message. Not needed if the tunnel is used bindings
An upward rangefinder over DroneCAN Yes, stock: a second rangefinder instance on the node, published with its I2C address as the sensor id; FC RNGFND2_TYPE 24, RNGFND2_ADDR = that address, RNGFND2_ORIENT 24 (up); read in Lua with rangefinder:distance_orient(24) AP_Periph/rangefinder.cpp line 82
A multi-zone time-of-flight driver No. AP_RangeFinder has the single-zone VL53L0X and VL53L1X only. An 8 × 8 zone sensor needs custom node code AP_RangeFinder/
A people radar driver No. Neither the LD2450 nor the LD2410 has a driver; the tunnel plus a Lua parser is the path

1. Overhead: no sensor in the baseline, an upward sonar later

Section titled “1. Overhead: no sensor in the baseline, an upward sonar later”

The baseline build has no upward sensor. Ryan removed the upward sonar on 2026-09-27 because its pod adds drag (th-lidar’s rough estimate: 3 to 5 percent of cruise drag). The pre-arm script does the overhead check only when an upward rangefinder is configured. Thus, to add the sonar later, buy the sensor and print the pod. The software does not change.

The future option is a MaxBotix MB7040 I2CXL-MaxSonar-WR, IP67, $89.95 (MaxBotix), in a pod on top of the rear fuselage at about X 0, Y 358. The pod’s mount was drawn for an earlier fuselage, so a pod on Fuselage C needs a new mount if it is ever fitted (not drawn). It uses the stock ArduPilot MaxBotix I2C driver (RNGFNDn_TYPE 2), which AP_Periph builds. We selected an ultrasonic sensor and not a multi-zone laser (ST VL53L5CX) because sunlight does not decrease its range and it has a stock driver.

To make the parts again, run tools/make_sonar_pod.py. It writes the pod, a reference model of the sensor and a Hatch Rear half with the sensor hole. That hatch is an earlier fuselage’s part, which Fuselage C does not use. If fitted, refuse to arm when anything is within 6 m straight up: RNGFND2_MAX 7, RNGFND2_MIN 0.2. The full design record (sensor sizes, pod fit, locknut, mass and CG shift) is in the git history of this page, at commit 3da5932 and before.

The plan carried a forward-looking presence radar (an HLK-LD2410C in a printed chin pod under the nose cone, at the maker’s 12.4 mm cover gap). Ryan looked at the pod and rejected the drag: th-lidar’s rough estimate was 7 to 8 percent of cruise drag for the box and 3 to 4 percent faired, and the pod sat just below the pitot. The radar, the pod (parts/Radar Chin Pod.stl and tools/make_radar_pod.py, removed in 30f404a and kept in git history at 747318d) and its lead are retired. The hangar’s ring of five LD2450 radars, which do report position, does all people detection, and it reaches the aircraft as the SITE_CLR value below. Away from the hangar there is no people check on the aircraft, and the pilot clears the area before arming.

For the record, the fit that preceded the decision: neither radar fits inside the nose cone at the maker’s cover gap, the LD2450 is the wrong sensor for a distance rule because it drops a still person past about 2 m, and the pod was the clean answer for the LD2410C alone. If a people sensor ever returns to the aircraft, that work is in the git history of this page and of th-main’s generators.

3. Transport: I2C on the node, nothing tunnelled

Section titled “3. Transport: I2C on the node, nothing tunnelled”

With the radar gone, both remaining sensors are I2C rangefinders with stock AP_Periph drivers, so the node publishes them as DroneCAN rangefinders and no serial tunnel is needed. The tunnel stays in the table above as a checked option for any future serial sensor; it is not configured.

4. The node: the planned rangefinder node, and a shortcut

Section titled “4. The node: the planned rangefinder node, and a shortcut”

The rangefinder node reserved on landing-sensing.md (TerraHawkRangeNode, DroneCAN id 22) now has a baseline job list of one I2C rangefinder, the TFmini-S, plus two CAN connectors for pass-through and stock AP_Periph with the rangefinder feature; the upward ultrasonic is an optional second I2C device on the same node. That is exactly the Matek CAN-G474 adapter node: 4 UARTs, 2 I2C, 2 CAN, 5.2 g, $21.16, running AP_Periph from the ArduPilot firmware server (RMRC). Ryan’s decision stands that the node is a board of ours; the recommendation here is to fly a CAN-G474 first as that node’s stand-in, since it needs no design work, and to draw the board only if the G474 falls short. It slots into the bus where the node was planned, in the starboard pocket beside the TFmini-S, and the FC configuration is the same either way.

Node-side parameters: RNGFND1_TYPE 25 (Benewake I2C) RNGFND1_ADDR 16, RNGFND2_TYPE 2 (MaxBotix I2C) RNGFND2_ADDR 112, RNGFND2_ORIENT 0 (the node does not know orientation; the FC sets it), FC side: RNGFND1_TYPE 24 RNGFND1_ADDR 16 ORIENT 25 (down, the TFmini-S), RNGFND2_TYPE 24 RNGFND2_ADDR 112 ORIENT 24 (up, the ultrasonic).

clear_to_launch.lua, on the FC, registered as an arming auxiliary authoriser. Every 200 ms it evaluates the conditions and sets passed or failed with a one-line reason, which the GCS shows as the pre-arm message.

Condition Source Rule
Nothing overhead (optional) rangefinder:distance_orient(24) Only if an upward rangefinder is configured: fail if a reading exists and is under 6 m. With none configured the check is skipped and never fails, so one script serves both builds
Site clear from the dock NAMED_VALUE_INT named SITE_CLR from the dock’s system id Required only when the aircraft is within 10 m of the pad location: fail unless a value equal to this aircraft’s SYSID_THISMAV arrived in the last 3 s (with two airframes the dock clears one aircraft at a time; 0 is not clear). Away from the pad the check is skipped, so a field takeoff is not held by a dock it cannot hear. With CTL_PAD_LAT 0 (no dock set up) it is always skipped: that is field mode, for a site mapped with no hangar
Elevons respond (only when TH_SRV_NODES is 1) servo_telem:get_telem(6) and (7) from the servo nodes Sweep each elevon to minimum, centre and maximum while disarmed, 400 ms per step; fail on no telemetry, a position more than 4° from the calibration, a stall current, a rail outside 4.8 to 6.2 V, or a sensor or jumper fault flag. With TH_SRV_NODES 0 (first flights, no nodes fitted) the check is skipped and the script says so at boot. The parameter decides, never the absence of telemetry: a dead node must fail, not look like a node that is not fitted
Sensors alive Rangefinder health, SITE_CLR age Fail if a sensor the check depends on has gone quiet for 2 s; a missing sensor never passes silently
Remote link fast enough CRSF channels 11 and 12 from the site node Only when channel 12 says the site node is the pilot: fail if the round trip on channel 11 is over 250 ms (see “Remote piloting” below)

The docked path for SITE_CLR. Pre-arm is the docked case, and the keel antennas may be shielded in the dock (see “RC lost while docked”), so hangar.md proposed CAN wires on the foot plates that put the dock on the aircraft’s DroneCAN bus. Decision (th-avionics, 2026-09-27): no CAN through the feet. The flight bus carries the airspeed sensor, the GPS, the compass and the rangefinder, and a wet or corroded pad pair on that bus is a stuck-dominant fault that stops all of them at once, in flight if the water stays. The 0.3 to 0.5 m stub would also force the whole bus to 500 kbit/s. Decided instead (accepted by th-hangar the same day), with no new hardware: the dock signals “clear” on the charge pads themselves. The charge node already reads the pad voltage as battery 3, and the pads carry 5 V during the probe. While the aircraft is docked and the site is clear, the dock sets the pad supply to 12 V instead; the pre-arm script reads battery 3 and treats 11 to 13 V as SITE_CLR 1 and anything else as 0. The node reports the pad voltage at 10 Hz, so a withdrawn “clear” reaches the FC within 200 ms. The pack can never reach the pads (the bridge and the charge switch block it), and the charge switch stays open at 12 V because the interlock only closes it on the ramp. The telemetry radio’s NAMED_VALUE_INT stays as the airborne path for the landing hold, which the pads cannot serve. The dock raises 12 V only at the lid’s open limit switch with the people guard clear, never before, so a closed lid can never pass pre-arm; a guard trip drops the pads to 0 V at once. The dock-side sequence and the level table are on hangar.md under “Site clear on the charge pads”.

The pad location and the guard radii are script parameters (CTL_PAD_LAT, CTL_PAD_LON, CTL_GUARD_M, CTL_OVHD_M) so the pilot can move the hangar or widen the guard without editing the script. A pilot override exists: an auxiliary switch that disables the authoriser, announced loudly, for the day the sensor is wrong and the pilot is sure.

landing_hold.lua, on the FC. When the aircraft is in a VTOL landing (AUTO’s VTOL land, QRTL or QLAND) within 10 m of the pad and SITE_CLR is not 1, it switches to QLOITER and holds. When SITE_CLR has been 1 for 5 s it resumes: QLAND if it was in QLAND, otherwise back to the mode it took over from. It also resumes a pad descent only when the Q43, the RTK receiver, has been RTK fixed (fix type 6, gps.GPS_OK_FIX_3D_RTK_FIXED) for 5 s. The script reads the Q43 by its instance (gps:status(0)), never “the primary”: with the M10 as GPS2 (2026-10-15) the primary can be the M10, which is L1 only and never RTK fixed, and a switch of the primary must never satisfy or skip this rule (th-hangar). A 3D fix is metres off and RTK float can be decimetres off, which is too much for the pad’s guides. Without RTK fixed it holds as for SITE_CLR 0, and at the hold limit it goes to the alternate point, where metres of error are acceptable. It also starts or resumes a pad descent only when the compass is healthy (compass:healthy(0), the M10’s QMC5883L, the only compass since 2026-10-15). The pad needs the heading within 10° of its axis. With the compass lost, EKF3 holds the yaw on the gyro alone in a hover: with EK3_SRC1_YAW 1 it fuses the GSF yaw estimate only as a one-off reset on bad innovations (AP_NavEKF3_MagFusion.cpp, ArduPilot 4c98c92), and the GSF needs horizontal acceleration to stay within its 15° gate. So with no healthy compass the script does not hold: it goes to the alternate point at once, because waiting cannot bring the compass back and the yaw only drifts. The dock reads the same loss in SYS_STATUS (th-hangar ebd597bc). In SITL (th-testpilot bbe5a02a, rows Y1-magfail and Y0: the only compass failed 20 s into a 10 m QLOITER, AHRS yaw against true yaw) the yaw error 60 s after the failure was 2.9 to 5.1°, and the maximum in about 120 s was 4.5 to 10.5°, the largest in calm air. The error grows faster with time, as a gyro bias does. There was no EKF yaw reset and the position drift stayed under 0.5 m. ESTIMATE: SITL’s gyro drift is not the ICM42688’s, so these numbers give the order of size, not the rate. The dock stops the RTCM corrections when its base’s own spoof check alerts (th-telemetry, hangar.md), so this rule is what keeps a spoofed or lost base from putting the aircraft on the pad on a 3D fix (th-hangar, th-avionics, 2026-10-15). It holds for at most 2 minutes, or until the pack reaches the reserve (battery:capacity_remaining_pct() below CTL_RESERVE_PCT, 36 %: critical’s 30 % plus the descent from the hold height, about 5 %, and the 6 s move, so the aircraft reaches the ground above critical), then moves to the alternate landing point and lands there with QLAND. The hold time that is left depends on the return: the hover takes about 33 A on Fuselage C, about 5.5 % of the pack a minute (th-avionics and th-aeronautics, 2026-10-22, ESTIMATE), so the full 2 minutes is about 11 % (S5 measured about 34 A, below). In th-testpilot’s S5 on Fuselage C (the low trigger 1.5 km out, the whole return into 8 m/s; 7fcfe781) the aircraft reached the hover over home at 41.9 % and landed at 34.7 % (cruise home 3.1 %, the hover and the descent 7.1 %). S5’s hover drew 714 mAh in 76 s, about 34 A, 0.094 % a second, and began with about 19 s of positioning over the pad, so a hold would start at about 40.1 %. The hold leaves when the whole-percent reading drops to 35, that is, under 36.0 %. So after that return the hold itself has about 45 s, and the hover from arrival to the hold’s exit is about 65 s, not 2 minutes (ESTIMATE from S5’s draw). The hold and the dock’s countdown both use the aircraft’s whole-percent reading (th-hangar 7320b63d). A farther corner leaves less. A lower BATT_LOW_MAH shortens the hold first: at 40 % the aircraft would reach CTL_RESERVE_PCT during the positioning, with no hold time. The alternate point is 12 m to starboard of the pad centre, abeam (site.alternate_point in parts/hangar/landing-window.json, th-hangar f259954; a drawing default until the site is chosen). The move stays in QLOITER: the script sets RC roll and pitch overrides from a position loop toward the point, at 2 m/s or less, as dock_yaw.lua does with the rudder. Do not use GUIDED for this move. Every GUIDED target goes through Plane::set_guided_WP, which clears auto_state.vtol_loiter and flies the leg as a fixed-wing aircraft until the loiter is reached (commands.cpp at 4c98c92). From a hover that can start a forward transition, and the move happens when the pack is low. Not yet flown in SITL. No radar covers the alternate point, so the dock cannot report it guarded; the operator sees it on the overview camera. Every mode change the script makes is announced, and the pilot’s stick or mode switch always wins: the script watches for a mode it did not set and stands down.

Script parameters (th-avionics, 2026-09-28). The flight script keeps every setting of its own in one CTL_ parameter table created with param:add_table (for example CTL_PAD_LAT, CTL_PAD_LON, CTL_PAD_HDG, CTL_FAR_M, CTL_AXIS_M, CTL_MUSH_SINK, CTL_LAND_SPD), never the shared SCR_USERn slots: in SITL two prototypes read the same SCR_USER1 and one test harness wrote it at arming, which changed another script’s limit without anyone seeing it.

Dock approach yaw. QLAND and QRTL do not remove a crab, so an arrival in a crosswind lands off the box axis (th-simulator, 2026-09-27: 85 to 88° off at 8 m/s across with stock QRTL). The fix th-simulator flew is a yaw onto the box axis, nose into the along-axis wind, at the start of QRTL’s descent, with the weathervane off for the landing; it passes every case from ahead or across to 5 m/s. How it is done on this aircraft, from the source at 4c98c92:

  • MAV_CMD_CONDITION_YAW is not implemented in ArduPlane, so a mission item cannot do it.
  • In QRTL and QLAND the yaw rate is the pilot’s rudder input plus the weathervane (QuadPlane::vtol_position_controller), and the rudder input is honoured in QRTL only while STICK_MIXING is not 0 (get_pilot_input_yaw_rate_cds). Keep STICK_MIXING at its default 1.
  • The landing script therefore commands the yaw through an RC override on the rudder channel (rc:get_channel(RCMAP_YAW):set_override(pwm)), refreshed at 10 Hz, exactly the stick input th-simulator used. When the aircraft is in QRTL or QLAND within 5 m of the pad and above 20 m, it targets the pad heading (CTL_PAD_HDG, nose toward the front wall), never its reciprocal: the keel A pad is asymmetric and charges only an aircraft landed that way round (th-hangar, 2026-09-28). It drives the heading error to under 2° with a proportional override (at most ±300 µs), then releases the override; with the weathervane off the heading then holds. It re-applies if the error grows past 5°. The same alignment runs during the landing hold, so a held aircraft is already on the axis when SITE_CLR arrives.
  • Q_WVANE_LAND 0 turns the weathervane off in landings only; Q_WVANE_GAIN 1 stays for hover.

Checked in SITL by th-simulator (2026-09-28, sim/scripts/dock_yaw.lua): the override yaws the aircraft in QRTL with a receiver (touchdown 0.1° off the axis from a 12.6° crab) and with no receiver pulses (8.6° off, inside 10°). It also masks the radio failsafe: an override counts as valid RC, so with no receiver and the script running the failsafe never tripped in 92 s. Rules that follow:

  • The override is 1500 ± (RC4_DZ + 4 µs per degree × error), at most ±150 µs beyond the dead zone. The rudder channel’s dead zone (RCn_DZ of the RCMAP_YAW channel, 30 µs by default) swallows a plain proportional command, and the heading then stalls at dead zone ÷ gain, about 7°; the script reads the dead zone at start-up (th-simulator, 2026-09-28: 1.2° at touchdown with it; 8 µs per degree limit-cycled).
  • The keel’s crossflow turns the aircraft off the axis after release across the wind (5 to 8° at 5 m/s, 7 to 10° at 8 m/s), so the script re-engages whenever the error passes 4°. Between two activations it leaves at least 1.5 s with no override, longer than RC_FS_TIMEOUT, so a real RC loss is still seen. Wind limits for a dock landing: 5 m/s across and 5 m/s along the box (the along-box limit was 8 m/s until th-simulator’s sheltered JSBSim sweep landed 0.38 m forward at 8 m/s with the front wall’s lee modelled, 2026-09-28; provisional until th-aeronautics’ CFD of the lee); above that the landing hold diverts. The dock judges these limits: it withholds the landing SITE_CLR while the highest 3 s gust of the last 2 minutes is over them, or while its wind reading is unknown (th-hangar, docs/hangar.md, “Wind at launch and landing”). The hold also compares the aircraft’s own wind estimate (ahrs:get_wind(), resolved into the box axis) with them before it starts the descent, as a coarse backup only. On Fuselage C that estimate depends on the heading: the side static ports read about 13 % low at cruise, and the EKF puts the error into the wind along the nose. In SITL, in an 8 m/s wind, it read 10.4 m/s with the nose downwind and 6.4 m/s with the nose into the wind (th-testpilot, thread 52278672). The approach flies into the wind, and the EKF does not learn wind in the hover, so at the hold the estimate reads low, on the side that lets a landing through. Do not make it the only check. Before launch the aircraft does not judge wind: its wind estimate is not observable on the pad. The dock’s anemometer does (th-hangar, 9a028ea), and a dock that refuses a launch for wind simply does not send SITE_CLR, so no new signal is needed. Wind from the lid end is limited to about 1 m/s along the axis (th-hangar and th-simulator, 2026-09-28). That limit is too fine for the aircraft’s estimate, which is noisy at that level and measured at altitude, so the dock judges it for landings too: it withholds the landing SITE_CLR while its anemometer reads more than 1 m/s from the lid end, and the aircraft holds and then diverts under the landing hold’s 2-minute rule. Gusts are limited the same way: the dock withholds SITE_CLR for launch and landing while the gust excess (the highest 3 s gust of wind speed in the last 2 minutes less the 2-minute mean speed; measured on the speed, not resolved into the box axes) is above a limit that depends on the wind’s direction, 0.9 cos²θ + 0.6 sin²θ m/s, where θ is the mean wind’s angle to the box axis (0.9 m/s along, 0.6 m/s across; the along limit was 1.2 until two landings cleared at 1.0 and 1.13 m/s missed sideways, th-hangar 2026-09-29) (th-hangar, from th-simulator’s Dryden runs, 2026-09-28; provisional). In SITL the aircraft knows its position to within 3 cm but cannot hold the ±60 mm landing window in gusts, and the box leaves only about ±0.2 m before a wingtip meets a wall. The limit’s value is th-avionics’ to set, from the hover gain trials and then from RTK hover tests on the real aircraft.
  • Override only during the VTOL descent within 5 m of the pad, at most 5 s per activation, and release it as soon as the error is under 2° (th-simulator’s alignment takes under 2 s). Never in the fixed-wing leg of QRTL.
  • Check the link before starting and report it: the script records whether RC frames were arriving and says so, because the autopilot cannot tell while the override runs.
  • Two cases still to fly before this is trusted: whether an override clears a radio failsafe that was already active (Plane can return to the previous mode when a short failsafe clears), and whether, after an override on an aircraft that never had a receiver, the radio failsafe rises when the override stops and then blocks the next arming on the pad until a reboot.

No low approach circle (th-avionics, 2026-09-28). ArduPlane’s VTOL landing from cruise normally circles down in fixed-wing flight before it transitions. In th-testpilot’s gusty SITL box that circle, at about 35° of bank and 21 m/s, crashed 4 of 8 flights. Q_OPTIONS bit 16 (DisableApproach) removes it: the aircraft flies to the landing point at its cruise height, transitions to hover there and descends vertically, which is also what the dock approach assumes. With it: 0 of 8 crashes, no assists, peak angle of attack 7.6°, and in 24 turn-backs the time on the lift motors fell from a median 32.5 s to 1.8 s (docs/agent-threads/test-battery/, a282add). The service value is Q_OPTIONS 200736: bit 5 (QRTL on RC loss in a Q mode), 12 (no speed assist on a synthetic airspeed, since 2026-10-19, docs/failsafes.md), 16 and 17, with bit 0 (Level Transition) off: with it on, ArduPlane caps the bank at 5° while the assist runs, so an aircraft the assist has caught cannot turn home, and switching it off cost nothing measurable in the transitions. An earlier SITL result, a caught aircraft unable to climb back out, came from SITL’s white-noise gust model and did not recur under Dryden gusts (th-simulator, 402e7c4). Decided by th-avionics and confirmed by Ryan on 2026-09-28. Bit 15 (throttle landing control) was removed on 2026-09-30: a throttle stick left at cruise turned a battery-failsafe landing into a climb (docs/failsafes.md). Where another page still shows the older 196640, 229408 or 163873, this value governs until that page is updated.

What bit 16 does to returns (th-simulator, 2026-09-28; QuadPlane::poscontrol_init_approach). With DisableApproach set, QRTL and any VTOL landing go straight to hover positioning at any distance, so a QRTL started far from home hovers all the way back: in SITL, returns from 0.5 to 2.6 km ran the pack to critical. Three rules keep bit 16 safe. Far returns are RTL, never QRTL: RTL flies home as a plane and Q_RTL_MODE 1 switches it to QRTL only near home, where bit 16 just removes the circle; the mode switch’s return slot is RTL, and the failsafe actions are RTL. A mission’s VTOL landing follows a waypoint within 100 m of the landing point, because the landing item hovers from the previous waypoint. QRTL is used only near the pad: the landing script’s AUTO-to-QRTL switch already acts only within 100 m. The RC-loss QRTL in a Q mode (bit 5) is the one far case left; Q modes are flown near the hangar, and the flight script’s far-return rule covers the rest: when an RC loss puts the aircraft in QRTL more than 300 m from home, the script switches to RTL once QRTL has climbed to within 2 m of Q_RTL_ALT (th-testpilot, sim/scripts/far_return.lua: 1.86 km out, landed 0.1 m off on 806 mAh against 2582 mAh hovering home).

Transitions start well above the assist floor. A forward transition sags about 10 to 12 m, and an aircraft the height assist has caught flies on at that height without following a climb demand (the far-return test held 28 m for the whole leg with the lift motors at 63 A). So every forward transition starts at least 20 m above Q_ASSIST_ALT: Q_RTL_ALT 50 and RTL_ALTITUDE 80 (th-avionics, 2026-09-28), and a mission’s VTOL takeoff climbs to at least 50 m before its first fixed-wing waypoint.

TECS weighting by height (th-avionics, 2026-09-28). An aircraft on the wing below Q_ASSIST_ALT with TECS_SPDWEIGHT 2 does not trade speed for height while the assist holds it up, so it can stay there on lift-motor power (th-simulator’s transition probe, calm air). The flight script therefore sets TECS_SPDWEIGHT 1 in fixed-wing flight below Q_ASSIST_ALT + 5 m and 2 above Q_ASSIST_ALT + 15 m, sets it at start-up, and never saves it, so the stored 2 stays the protected state. Checked in SITL by th-testpilot: the home-leg stall set kept its fix (worst 9.7°, no assists), a forced sag in AUTO climbed out in 24 s, and the far RC-loss return from 1.8 km landed on the pad on 641 mAh (806 without it, 2582 hovering home). Prototype: sim/scripts/spdweight_band.lua.

The last leg runs along the box, from the lid end (th-avionics, 2026-09-28). In th-simulator’s 4 m/s runs the only misses came from arrivals from the side (0.36 and 0.42 m sideways). So every dock landing arrives along the box axis from the lid end, flying toward the front wall, because the pad charges only an aircraft landed nose toward the front wall. With the dock sited for wind from the front, that is also into the wind; lid-end wind is a tailwind, which the dock limits to 1 m/s. A dock mission’s last waypoint sits on the axis 80 m from the pad on the lid side at Q_RTL_ALT, which also meets the 100 m rule for the VTOL landing that follows. For an RTL return the landing script does the same, in four stages (sim/scripts/axis_approach_brake.lua, v3.2.2, adopted 2026-10-01 at 17caf5b):

  1. Within 1 km of the pad it switches to GUIDED. The aircraft flies as an aeroplane at AIRSPEED_STALL + 1 m/s (the script reads the parameter: 21.0 m/s with 20.0, on the day and night builds since 2026-10-20), toward a target 400 m beyond the axis point. Every second the script aims the target again from the aircraft’s position, so the track goes through the point. A GUIDED aeroplane flies from its position and not along the first line, and it starts to turn onto a loiter circle before a target that it must reach (set_guided_WP, 4c98c92).
  2. When the distance ahead to the closest approach is 0.15 x the ground speed squared or less (40 m at least), it switches to QLOITER. QLOITER does the back transition and stops the aircraft before the point. QLOITER stops in about 0.12 x the ground speed squared, with the nose up and the lift motors at 1520 to 1580 µs.
  3. In QLOITER it moves onto the point at 2 m/s or less, with roll and pitch overrides.
  4. On the point it switches to QRTL, which flies the last 80 m along the axis.

In th-simulator’s wind set (calm, and wind from ahead, across, the quarter and behind at 5 and 8 m/s), the track of every arrival went through the point (a miss of 0 m). Every arrival stopped 3 to 25 m short of the point and landed on the pad, with 13.4 to 15.6 Wh from the GUIDED start to touchdown. Version 3.1 (a hover that starts at the point) overshot about 100 m in the same set. Do not use GUIDED to move from a hover, because it can start a forward transition.

The first version, sim/scripts/axis_approach.lua (218b170, adopted 2026-09-29), switched to QRTL within 30 m of the point, with WP_LOITER_RAD 25. Its SITL results: with wind from ahead at 0 to 5 m/s and across at 4 and 5 m/s, calm and gusty, every landing the dock would have cleared was captured (14 of 14), every miss was in a gust the dock withholds for, every touchdown was within 2.8° of the axis, and there was no crash and no lift on the wing. A later run with the adopted scripts found two landings cleared at 1.0 and 1.13 m/s of gust excess that missed sideways, which is why the along-box gust limit is now 0.9 m/s. A 1.05 m/s limit was considered on 2026-09-30 and not adopted. The rule, fixed before the result: every landing at 1.05 m/s or less must capture with 0.20 m or less sideways. In th-testpilot’s fixed-seed threshold sample a landing at 1.03 m/s came 0.226 m sideways, against a 0.24 m capture. The next step is the wider box, or real gust data from the mast.

Survey missions end with an RTL item (th-avionics, 2026-10-01). End every survey mission with MAV_CMD_NAV_RETURN_TO_LAUNCH, not with a VTOL landing item. With Q_OPTIONS bit 16, an AUTO VTOL landing goes straight to VTOL position control at the start of the item, with no approach and no airbrake stage (poscontrol_init_approach). In th-testpilot’s S3 (100 ha, 8 m/s wind) it started 399 m out at cruise with the wind behind, overshot by 300 m and hovered back for 70 s. With the RTL item, the axis approach brings the aircraft home through the hover at the axis point: 86 s on the lift motors against 115, and 12.6 Wh for the landing against 17.8 (test-battery thread, 671eb25). That result had no overshoot only on the earlier SITL pusher model. On the corrected model the hover at the point overshot about 100 m. With the adopted approach the aircraft stops 12 m short of the point, and the landing uses 12.8 Wh (th-testpilot, 2026-10-01).

Dock landings always end in QRTL. th-simulator’s GCS-failsafe flights (2026-09-28): in QRTL the script keeps steering with the telemetry link cut (landed 1.6° off the axis), but a mission that ends in its own NAV_VTOL_LAND touched down 58° off, because in an AUTO VTOL landing the yaw is the auto yaw plus the weathervane and the rudder input is not used (vtol_position_controller). Q_RTL_MODE switches to QRTL only from RTL mode (mode_rtl.cpp). A mission’s RTL item selects RTL mode (commands_logic.cpp), so it gets the QRTL hand-over; a mission’s own VTOL landing does not. So the landing script also watches AUTO: when the current mission item is a VTOL landing within the pad’s guard radius (mission:get_current_nav_id() is 85), it switches to QRTL (vehicle:set_mode), and QRTL then flies the same aligned approach. A mission can therefore end with a VTOL landing at the pad and still land on the axis, including after a GCS failsafe, which continues the mission. Flown in SITL by th-simulator (sim/scripts/dock_yaw.lua, 2026-09-28): telemetry cut in AUTO, the switch fired at the pad’s VTOL landing and the aircraft touched down 0.2° off the axis, against 58.3° without it. The flight script measures the distance to the pad location (CTL_PAD_LAT, CTL_PAD_LON), not to home, so a field landing away from the dock is never converted. The same flights confirmed the dock case as designed: with the site node’s hold values (throttle 1000, mode channel held) the aircraft armed first time, landed 1.4° off the axis and re-armed 3 s later without a reboot, and the held mode channel never changed the mode. One boot artefact: the aircraft comes up in QRTL on the pad, from a throttle failsafe that fires before the receiver starts and then clears. QRTL cannot be armed, so the dock’s launch sequence sets the launch mode explicitly before arming.

The script never acts on a stale SITE_CLR. A dock that stops talking looks the same as a dock that says not clear, which is the safe direction for landing and the reason the pre-arm check is gated on distance instead.

Ryan decided (2026-09-27, th-hangar’s agent-threads/site-node/) that a remote pilot flies through the hangar’s site node: a gamepad’s stick packets reach the node over the network, and the node writes CRSF to the ExpressLRS 2.4 GHz transmitter on the beacon post. Two consequences land on the aircraft.

Lost link. When the newest stick packet is older than 300 ms the node stops sending CRSF. An ExpressLRS transmitter with no CRSF input reports “no handset” and stops transmitting; the receiver then fails safe, and a CRSF receiver in failsafe stops sending channel frames rather than repeating stale sticks (ExpressLRS discussion, 3.4.0 release notes). ArduPilot sees the loss of frames within RC_FS_TIMEOUT (1 s) and runs its RC failsafe. The receiver’s failsafe mode must be “no pulses”, never “hold”. The actions, from ArduPlane/Parameters.cpp and events.cpp at 4c98c92:

Situation What happens Parameter
Hovering near the hangar in a Q mode (QSTABILIZE, QHOVER, QLOITER) QRTL: climb to Q_RTL_ALT (40 m), return to the pad, VTOL land there under the landing hold, which respects SITE_CLR. Without this option Plane would QLAND on the spot, on whatever is beneath it Q_OPTIONS bit 5 set
Flying by hand far from the hangar (FBWA, FBWB, MANUAL) CIRCLE after 1 s, then RTL at 5 s: fixed-wing return to home, which finishes with a VTOL landing because of Q_RTL_MODE 1. Home is the pad when the aircraft launched from the hangar and the field when it did not FS_SHORT_ACTN 0 (acts after RC_FS_TIMEOUT, 1 s; this ArduPlane has no FS_SHORT_TIMEOUT), FS_LONG_ACTN 0 (RTL from manual modes), FS_LONG_TIMEOUT 5
Flying a mission in AUTO or GUIDED No change: the mission continues, because a survey is meant to fly beyond the control link. The GCS failsafe over the telemetry radio link is a separate setting and stays as it is FS_SHORT_ACTN 0, FS_LONG_ACTN 0
In a VTOL takeoff or landing already Plane’s own rule: QLAND if in a VTOL takeoff, otherwise the landing continues built in

FS_LONG_ACTN 0 reads as “continue” but means RTL for the manual modes and continue for the automatic ones, which is exactly the split wanted. If a mission must abort on link loss for some reason, FS_LONG_ACTN 1 turns the third row into RTL as well.

Centring on the pad (Ryan, 2026-09-28). The dock centres the aircraft mechanically after it lands (th-hangar, c672f87), which accepts ±193 mm sideways, 274 mm forward and about 145 mm aft (th-hangar, 55b0eed; forward 274 mm on Fuselage C v2.4B, whose own airspeed tube beside the camera reaches further forward than the earlier fuselage’s pitot). Calm SITL landings miss by 56 to 155 mm, so no low touchdown gate is needed and the final descent stays at ArduPilot’s normal speed. The landing script holds nothing after touchdown: ArduPlane disarms by itself after a VTOL landing and never re-arms by itself, and a new arm needs the dock’s 12 V clear level, which the dock raises only after the bars are parked and the lid is open. The dock moves the bars only while the aircraft’s heartbeat reports disarmed.

Early disarm after contact (th-avionics, 2026-09-28). After touchdown ArduPlane waits for its land detector (throttle at its lower limit for 5 s and a steady height for 4 s) before it disarms. In th-simulator’s 8 m/s headwind landing the thrust was already zero 2 s after contact, but for the next 4.6 s the attitude controller kept its nose-down pitch target and drove the rear lift pair about 150 µs above the front pair, a nose-down moment of about 1.1 N·m that slows the rock onto the keels. The landing script therefore disarms 2.5 s after it sees contact (land final, throttle at its lower limit, rangefinder under 0.2 m and no vertical speed), which brought the disarm from 6.7 s to 3.8 s after contact in SITL. The yaw override ends for good at first contact as well.

Stick trims are pinned (th-avionics, 2026-09-28). In a VTOL landing ArduPlane adds a velocity from the roll and pitch sticks to the landing target (Q_OPTIONS bit 17), so an input resting a few µs off trim walks the target and moves the touchdown. RC1_TRIM, RC2_TRIM and RC4_TRIM are therefore exactly 1500 in the common parameter set, the site node’s hold values are exactly 1500 (CRSF’s centre), and the node reads the three trims at link-up and refuses to be the pilot source if one differs. The landing script warns, and logs, when an RC input is off trim during a dock descent; it does not fail the landing, because a person may reposition on purpose.

Hold values and link recovery (checked in the source at 4c98c92). The site node’s hold values use throttle 1000 µs, above THR_FS_VALUE 950 (its default, unchanged); a CRSF loss is detected by the frame timeout, so the value only guards against a wrong hold. A link that returns after an RC failsafe with the same mode-channel value does not change the mode: Plane acts on the mode channel only when its debounced position changes (RC_Channel::read_mode_switch) and ignores RC older than 100 ms. A short failsafe restores the earlier mode only when the failsafe itself changed the mode (rc_failsafe_short_off_event, reason RADIO_FAILSAFE), which on this aircraft happens only in the manual modes; a long failsafe never restores. So an aircraft in QRTL stays in QRTL when the link returns.

RC lost while docked. The keel antennas (th-telemetry, 2026-09-27) sit inside the dock’s 250 mm aluminium walls when the aircraft is on the pad, so the RC link can be shielded exactly where the aircraft arms. An earlier version of this paragraph said an aircraft that has never received RC arms from the GCS. That was wrong, and th-simulator’s SITL flights (2026-09-28, docs/agent-threads/hangar-aero/) show it: with THR_FAILSAFE 1, an aircraft with no RC link cannot arm at all (“Waiting for RC”, or “Radio failsafe on” with RC_PROTOCOLS 0), and a reboot does not change that. With THR_FAILSAFE 0 and FS_GCS_ENABL 1 it arms from the GCS, but then a lost RC link in a handset flight triggers nothing, so that profile is rejected.

The rule instead: the aircraft always has an RC source when it flies. For a handset flight it is the handset. For a launch from the dock it is the site node’s CRSF link (see “Remote piloting”), which runs whenever the dock operates the aircraft, whether or not a person is piloting, and THR_FAILSAFE stays 1 in every configuration. The trough RSSI test in th-telemetry’s plan checks that the link reaches the keels on the pad; th-telemetry’s budget says it should, with about 80 dB to spare at 2 m. A handset switched on near the hangar and carried out of range still makes the aircraft unarmable until its link returns, so keep it off for dock launches or in range.

Latency at the pre-arm check. The remote page refuses to arm above 250 ms round trip or with SITE_CLR 0. The aircraft should see the latency too, as defence in depth, and the natural carrier is not MAVLink but the CRSF stream itself, whose latency is the thing being measured and which dies when the link dies. The node sets two channels: channel 12 at 2000 µs while the site node is the pilot source, and channel 11 at 1000 µs plus the round trip in milliseconds (988 to 2012 µs covers 0 to 1000 ms). The pre-arm script reads them with rc:get_pwm(): if channel 12 is above 1700, it fails the check unless channel 11 minus 1000 is at or under 250, with the reason “remote link 3xx ms”. A handset pilot never sets channel 12 and is not gated. No NAMED_VALUE_FLOAT is needed, and nothing depends on a MAVLink path from the node to the aircraft.

Channels 11 and 12 are otherwise unused on this aircraft (the map uses 1 to 4 for sticks, 5 for mode, and a few switches). Both are recorded in the flight-parameters page when the script lands.

All of this runs in SITL first. The upward rangefinder is RNGFND2_TYPE 100 with RNGFND2_ORIENT 24. The dock is a second MAVLink endpoint sending NAMED_VALUE_INT SITE_CLR at 2 Hz; MAVProxy can do that from a script, and if th-hangar’s suggestion of an ArduPilot Rover board as the dock controller is taken, the dock end is Lua too and a second SITL instance plays the dock. That is the argument for the Rover controller that matters to this side.

  • Ryan: approve the Matek CAN-G474 ($21.16) as the node stand-in. Optional, future: the MB7040 Compact ($89.95) with a 3/4-inch NPS nylon locknut and a hole in the top shell.
  • th-main: the sonar pod on the rear hatch, kept as a future option. Its parts are removed from parts/; tools/make_sonar_pod.py makes them again.
  • Ryan: the people radar is dropped from the aircraft (2026-09-27); the chin pod is retired.
  • Ryan: the sonar is optional and out of the baseline (2026-09-27); its hole in the top shell waits with it.
  • th-avionics: clear_to_launch.lua and landing_hold.lua; SITL first with a stub dock.
  • th-eda, when the parts are firm: the TFmini-S lead to the node in the harness table; the sonar lead only if the option is taken; no board work unless the G474 falls short.
  • th-hangar: keep SITE_CLR as NAMED_VALUE_INT at 2 Hz with the dock’s own system id; the aircraft filters on that id.

Source: docs/clear-to-launch.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