# 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