This document describes the full communication protocol between the ESP32 modem (aircraft side) and the Web UI (ground side).
Communication is bidirectional:
- Uplink (telemetry): ESP32 publishes telemetry on
bulletgcss/telem/<callsign>; the UI subscribes. - Downlink (commands): UI publishes commands on
bulletgcss/cmd/<callsign>; the ESP32 subscribes.
Flight Controller ──MSPv2 (UART)──► ESP32 Modem ──MQTT (uplink)──► MQTT Broker ──MQTT──► Web UI
ESP32 Modem ◄─MQTT (downlink)─ MQTT Broker ◄─MQTT── Web UI
| Parameter | ESP32 → Broker | Browser → Broker |
|---|---|---|
| Protocol | MQTT 3.1.1 | MQTT 3.1.1 over WebSocket |
| Default port | 1883 (plaintext) | 8084 (WSS/TLS) |
| Default broker | broker.emqx.io |
broker.emqx.io |
| QoS | 0 (fire and forget) | 0 (subscribe) |
| Client ID | ESP32_<MAC> (chip MAC address) |
web_<random> |
QoS note: All messages use QoS 0 (fire-and-forget) by design. Cellular coverage is inherently intermittent — aircraft routinely fly beyond antenna range — and operators already expect gaps in telemetry. A dropped packet means a one-second stale display at most; the 10-second force-refresh cycle (see Standard Telemetry Message below) re-syncs all fields automatically when connectivity returns. The UI's stale-data indicator signals loss of connection; this is normal operating behaviour, not an error condition.
Topic format:
- Uplink (telemetry):
bulletgcss/telem/<callsign>— firmware publishes, UI subscribes. - Downlink (commands):
bulletgcss/cmd/<callsign>— UI publishes, firmware subscribes.
The callsign is read from the flight controller at startup via MSP and used as the topic suffix. Both topics are configured independently in Config.h (firmware) and in the browser settings (UI).
All messages are plain ASCII text. Each message is a comma-separated list of key:value pairs, always ending with a trailing comma:
key1:value1,key2:value2,key3:value3,
Values are always integers. Floating-point quantities are transmitted as scaled integers (see the field reference below for each field's scale factor).
There is no message envelope, no length prefix, and no message-level checksum. Each MQTT publish is one complete message.
There are six distinct message types, distinguished by their content.
Sent once, as the very first message after the ESP32 connects to the broker.
id:0,
This signals to any subscriber that a new session has begun.
Sent every 1000 ms (configurable via MESSAGE_SEND_INTERVAL in Config.h).
To reduce bandwidth, a field is only included in a message if its value has changed since the last message. However, to prevent stale data in the UI (e.g. after the UI reconnects mid-flight), all fields are forced into the message periodically, regardless of whether they changed.
Fields are divided into 10 groups (0–9). On each cycle, msgCounter % 10 determines which group is force-refreshed. This means every field is guaranteed to be re-sent at least once every 10 seconds.
| Group | Fields force-refreshed |
|---|---|
| 0 | ran, pan, hea, ggc, nvs, whd |
| 1 | asl, alt, gsp |
| 2 | vsp, hdr, hds |
| 3 | acv, bpv, bfp |
| 4 | cud, cad, rsi |
| 5 | gla, glo, gsc |
| 6 | ghp, css, 3df |
| 7 | hwh, arm, dls, mro, cmdrth, cmdalt, cmdcrs, cmdbep, cmdwp, cmdph, fmcrs, fmalt, fmwp, fmph |
| 8 | wpc, cwn, wpv |
| 9 | fs, trp, att |
The following fields are only sent when changed and are never force-refreshed here (they appear in the Low Priority Message instead):
hla, hlo, hal, ftm
The lseq field (last accepted command sequence number) is also included in the standard telemetry message only when its value changes — i.e. immediately after a command is accepted by the firmware. This allows connected UIs to update their sequence counters in near-real-time without waiting for the next low priority message.
Sent every 60 seconds (LOW_PRIORITY_MESSAGE_INTERVAL in Config.h), always including all fields regardless of change.
Contains slow-changing or static data: protocol version, home coordinates, cell count, callsign, flight times, and message frequency.
pv:1,bcc:4,cs:MyCallsign,hla:123456789,hlo:-456789012,hal:80000,ont:3600,flt:1200,ftm:9,mfr:1000,
Sent by the UI on the downlink topic (bulletgcss/cmd/<callsign>).
Ping:
cmd:ping,cid:ABC123,seq:42,sig:base64base64...==,
Flight controller command (RC mode toggle):
cmd:rth,cid:ABC123,seq:43,state:1,sig:base64base64...==,
| Field | Description |
|---|---|
cmd |
Command type — see table below |
cid |
Command ID — 6-character random alphanumeric string, unique per command |
seq |
Monotonically increasing sequence number (uint32, stored in localStorage). Used by the firmware to reject replayed commands. |
state |
(RC mode commands only) 1 = activate the mode, 0 = deactivate. Not included in the signed payload. |
heading |
(setheading only) Target heading in degrees (0–359). Not included in the signed payload. |
wp |
(jumpwp only) 0-based waypoint index to jump to. The UI sends displayed_wp_number - 1. Not included in the signed payload. |
alt |
(setalt only) Target altitude in centimetres relative to home. Not included in the signed payload. |
sig |
Ed25519 signature of the canonical payload string cmd:<cmd>,cid:<cid>,seq:<seq> — base64-encoded, 88 characters. Extra fields (state, heading, wp, alt) are never part of the signed string. |
The firmware verifies the signature against the stored commandPublicKey (32 bytes, configured in Config.h). Commands with an invalid signature, a missing sig field, a sequence number ≤ the last accepted sequence number, or sent while no public key is configured are silently dropped — no ack is sent. The last accepted sequence number is persisted to NVS so replay protection survives a firmware reboot.
The UI only sends commands if a private key is present in localStorage. See the Security panel in the UI sidebar.
Supported cmd values:
cmd |
Extra fields | Action |
|---|---|---|
ping |
— | No-op; used to verify the downlink channel is working |
rth |
state |
Return to Home — activates/deactivates BOXNAVRTH via RC channel override |
althold |
state |
Altitude Hold — activates/deactivates BOXNAVALTHOLD via RC channel override |
cruise |
state |
Cruise Mode — activates/deactivates BOXNAVCRUISE via RC channel override |
wp |
state |
WP Mission Mode — activates/deactivates BOXNAVWP via RC channel override |
beeper |
state |
Beeper — activates/deactivates BOXBEEPERON via RC channel override |
setheading |
heading |
Sets the Cruise/Course Hold heading target (degrees 0–359). Only effective when Cruise mode is active. Firmware converts to centidegrees and sends MSP2_INAV_SET_CRUISE_HEADING (0x2223). |
setalt |
alt |
Sets the altitude hold target (centimetres relative to takeoff point). Only effective when Altitude Hold is active. Requires INAV 10.0.0 or newer. Firmware sends MSP2_INAV_SET_ALT_TARGET (0x2215) with a 5-byte payload: uint8_t datum=0 (takeoff-relative) + int32_t altCm. |
jumpwp |
wp |
Jumps to a waypoint during an active WP mission (0-based index). Only effective when WP Mission mode is active. Firmware sends MSP2_INAV_SET_WP_INDEX (0x2221). |
setwp |
wpno, la, lo, al, ac, p1, p2, p3, f |
Uploads one waypoint to the firmware staging buffer. The firmware accumulates all waypoints and only forwards them to the FC once the last waypoint (f:165) is received and the full mission passes validation. See Mission Upload below. |
getmission |
— | Requests the firmware to publish the full mission currently stored on the aircraft. The firmware responds with one dlwp: message per waypoint followed by an ACK. See Mission Download below. |
Mission Upload (setwp):
Each waypoint is sent as a separate signed command. Extra fields (not included in the signed payload):
| Field | Description | Notes |
|---|---|---|
wpno |
Waypoint number (1-based) | 1..maxWaypoints |
la |
Latitude | Degrees × 10,000,000 |
lo |
Longitude | Degrees × 10,000,000 |
al |
Altitude | Centimetres relative to home |
ac |
Action code | 1=Waypoint, 3=Loiter, 4=RTH, 8=Land |
p1 |
Parameter 1 | Speed cm/s (Waypoint); loiter seconds (Loiter) |
p2 |
Parameter 2 | Speed cm/s (Loiter); unused otherwise |
p3 |
Parameter 3 | Bitfield (bit 0 = AMSL alt; 0 = relative to home) |
f |
Flag | 0 = normal; 165 (0xA5) = last waypoint |
The firmware buffers all incoming waypoints in a heap-allocated staging array. When the last waypoint (f:165) is received, it validates the complete mission (contiguous WP numbers, no gaps, WP Mission mode not active) before forwarding to the FC via sequential MSP_SET_WP calls. ACK is deferred until after validation. NACK reason codes: badfields, overflow, gap, busy, oom.
Mission Download (getmission):
The ACK for this command is deferred. Before sending the ACK, the firmware publishes one dlwp: message per waypoint (see below), then sends the ACK. If no mission is loaded, a NACK with reason:nomission is sent instead.
Sent by the firmware on the uplink topic in response to a verified, accepted command. Identified by the cmd: prefix, same as command messages.
cmd:ack,cid:ABC123,lseq:42,
| Field | Description |
|---|---|
cmd |
Value ack identifies this as an acknowledge |
cid |
Echoes back the cid from the received command |
lseq |
Last accepted sequence number (uint32). Allows all connected UIs to sync their sequence counters after a command is accepted. |
The UI detects the cmd: prefix, routes the message to parseCommandMessage() (not the standard telemetry parser), matches the cid against its pending command list, and marks the command as received. If no ack arrives within 10 subsequent telemetry messages, the command is marked lost.
The lseq value in the ACK is also used by other connected UI instances to synchronise their sequence counters — see the lseq field in the Low Priority Message reference.
Sent every 30 cycles (every ~30 seconds) when the aircraft has a waypoint mission loaded (waypointCount > 0).
One MQTT message is published per waypoint, including waypoint 0 (the home point). Waypoint messages are identified by starting with wpno:.
wpno:1,la:123456789,lo:-456789012,al:5000,ac:1,p1:100,
Optional fields (p1, p2, p3, f) are omitted when their value is 0.
Published by the firmware on the uplink topic in response to a getmission command. One message is sent per waypoint (excluding waypoint 0, the home/RTH position). Identified by the dlwp: prefix.
dlwp:1,la:123456789,lo:-456789012,al:5000,ac:1,p1:100,p2:0,p3:0,f:0,
dlwp:2,la:123456800,lo:-456789100,al:6000,ac:1,p1:0,p2:0,p3:0,f:165,
All fields are always present (unlike the periodic waypoint message where zero-valued optional fields are omitted). After all waypoints are published, the firmware sends a standard ACK for the getmission command.
The UI accumulates dlwp: messages in a buffer (missionDownloadBuffer) cleared before sending the getmission command. On ACK, the buffer is read, converted to the planner's plannedMission format, and loaded into the Mission Planner.
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
ran |
Roll angle | Decidegrees (÷10 → degrees) | data.rollAngle |
-1800 to 1800 |
pan |
Pitch angle | Decidegrees (÷10 → degrees) | data.pitchAngle |
-900 to 900 |
hea |
Heading (yaw) | Degrees | data.heading |
0 to 359 |
ggc |
GPS ground course | Degrees | data.gpsGroundCourse |
0 to 359 |
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
alt |
Relative altitude (barometric/estimated) | Centimeters | data.altitude |
-1000000 to 10000000 |
asl |
Altitude above sea level (GPS) | Meters | data.altitudeSeaLevel |
-500 to 9000 |
gsp |
Ground speed | cm/s | data.groundSpeed |
0 to 15000 |
vsp |
Vertical speed | cm/s | data.verticalSpeed |
-60000 to 60000 |
altrange covers 10 km below home to 100 km relative altitude.aslrange covers below-sea-level terrain to above the highest mountain.gspceiling of 15000 cm/s = 540 km/h, well above any fixed-wing UAV speed.vspallows ±2160 km/h vertical.
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
gla |
GPS latitude | Degrees × 10,000,000 | data.gpsLatitude |
-900000000 to 900000000 |
glo |
GPS longitude | Degrees × 10,000,000 | data.gpsLongitude |
-1800000000 to 1800000000 |
gsc |
GPS satellite count | Count | data.gpsSatCount |
0 to 50 |
ghp |
GPS HDOP | HDOP × 100 (÷100 → HDOP) | data.gpsHDOP |
0 to 9999 |
3df |
GPS 3D fix | 0 = no fix, 1 = 3D fix |
data.gps3DFix |
0 or 1 |
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
hdr |
Direction to home | Degrees | data.homeDirection |
0 to 359 |
hds |
Distance to home | Meters | data.homeDistance |
0 to 20000000 |
nvs |
Navigation state (INAV nav_state enum) | Integer | data.navState |
0 to 30 |
cwn |
Current active waypoint number | Count | data.currentWaypointNumber |
0 to 255 |
wpc |
Total waypoint count in mission | Count | data.waypointCount |
0 to 256 |
wpv |
Waypoint mission valid flag | 0 / 1 |
data.isWaypointMissionValid |
0 or 1 |
hdsceiling of 20000 km ≈ half the Earth's circumference.nvsceiling of 30 is intentionally generous — INAV's nav_state enum currently tops out around 20.
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
bpv |
Total battery voltage | Centivolts (÷100 → V) | data.batteryVoltage |
0 to 6000 |
acv |
Average cell voltage | Centivolts (÷100 → V) | data.battCellVoltage |
0 to 500 |
bfp |
Battery fuel (charge) percent | Percent | data.fuelPercent |
0 to 100 |
cud |
Current draw | Centiamps (÷100 → A) | data.currentDraw |
0 to 50000 |
cad |
Capacity drawn | mAh | data.capacityDraw |
0 to 100000 |
whd |
Energy drawn | mWh | data.mWhDraw |
0 to 1000000 |
trp |
Throttle percent | Percent | data.throttlePercent |
0 to 100 |
att |
Auto-throttle active | 0 / 1 |
data.isAutoThrottleActive |
0 or 1 |
bpvceiling of 6000 cV = 60 V covers up to 14S LiPo configurations.acvceiling of 500 cV = 5.0 V per cell covers HV LiPo (4.35 V) with margin.cudceiling of 50000 cA = 500 A accommodates very high-current setups.
| Key | Description | Values | JS field | Valid Range |
|---|---|---|---|---|
arm |
Aircraft armed | 0 / 1 |
data.uavIsArmed |
0 or 1 |
fs |
Failsafe active | 0 / 1 |
data.isFailsafeActive |
0 or 1 |
hwh |
Hardware healthy | 0 / 1 |
data.isHardwareHealthy |
0 or 1 |
dls |
Downlink status | 0 = not subscribed, 1 = subscribed ok |
data.downlinkStatus |
0 or 1 |
mro |
MSP RC Override mode active | 0 = not active, 1 = active |
data.mspRcOverride |
0 or 1 |
css |
Cellular/WiFi signal strength | 0–3 |
data.cellSignalStrength |
0 to 3 |
rsi |
RC link RSSI | Percent | data.rssiPercent |
0 to 100 |
mroindicates whether theMSP RC OVERRIDEflight mode is active on the flight controller. This mode must be active for the firmware's channel override commands to take effect. Without it, commands sent viaMSP_SET_RAW_RCwill be ignored by INAV. The UI shows a dedicated status icon for this field.
These fields report which RC channel overrides the firmware is currently holding active. A value of 1 means the firmware is actively commanding that mode via MSP_SET_RAW_RC; 0 means the channel is set to its safe-off value.
| Key | Description | JS field | Valid Range |
|---|---|---|---|
cmdrth |
RTH override active | data.cmdRth |
0 or 1 |
cmdalt |
Altitude Hold override active | data.cmdAltHold |
0 or 1 |
cmdcrs |
Cruise override active | data.cmdCruise |
0 or 1 |
cmdbep |
Beeper override active | data.cmdBeeper |
0 or 1 |
cmdwp |
WP Mission override active | data.cmdWp |
0 or 1 |
cmdph |
Position Hold override active | data.cmdPosHold |
0 or 1 |
These reflect what the firmware is doing, not whether INAV has activated the mode. A
1here withmro:0means the override is being sent but INAV is ignoring it (MSP RC Override mode not active on FC).
These fields report whether each flight mode is actually active on the flight controller, regardless of source (radio switch, Bullet GCSS command, or any other means). Derived from MSP_ACTIVEBOXES.
| Key | Description | JS field | Valid Range |
|---|---|---|---|
fmcrs |
Cruise / Course Hold mode active | data.fmCruise |
0 or 1 |
fmalt |
Altitude Hold mode active | data.fmAltHold |
0 or 1 |
fmwp |
WP Mission mode active | data.fmWp |
0 or 1 |
fmph |
Position Hold mode active | data.fmPosHold |
0 or 1 |
The UI uses these fields to gate the
setheading,setalt, andjumpwpcommands respectively — a command that has no effect when the relevant mode is inactive is not shown as available.fmwpadditionally gates the map waypoint-click jump feature: clicking a waypoint marker while WP Mission is active prompts the user to jump to that waypoint.
| Key | Description | JS field | Valid Range |
|---|---|---|---|
ftm |
Flight mode ID (see table below) | data.flightMode (resolved to name) |
1 to 11 |
Flight mode ID values:
| ID | Name | Description |
|---|---|---|
| 1 | MANUAL |
Manual / passthrough |
| 2 | RTH |
Return to home |
| 3 | A+PH |
Altitude hold + position hold |
| 4 | POS H |
Position hold only |
| 5 | 3CRS |
3D cruise with altitude hold |
| 6 | CRS |
Cruise |
| 7 | WP |
Waypoint mission active |
| 8 | ALT H |
Altitude hold + angle |
| 9 | ANGLE |
Angle (self-leveling) |
| 10 | HORIZON |
Horizon mode |
| 11 | ACRO |
Acro / rate mode |
| Key | Description | Unit / Scale | JS field | Valid Range |
|---|---|---|---|---|
pv |
Protocol version | Integer | data.protocolVersion |
1 to 999 |
bcc |
Battery cell count | Count | data.batteryCellCount |
1 to 12 |
cs |
Aircraft callsign | String (alphanumeric, _, -) |
data.callsign |
1–16 chars; pattern ^[A-Za-z0-9_-]+$ |
hla |
Home latitude | Degrees × 10,000,000 | data.homeLatitude |
-900000000 to 900000000 |
hlo |
Home longitude | Degrees × 10,000,000 | data.homeLongitude |
-1800000000 to 1800000000 |
hal |
Home altitude above sea level | Centimeters (÷100 → m) | data.homeAltitudeSL |
-50000 to 900000 |
ont |
On-time (time since power on) | Seconds | data.powerTime |
0 to 172800 |
flt |
Flight time (time since arm) | Seconds | data.flightTime |
0 to 86400 |
ftm |
Flight mode ID | See flight mode table | data.flightMode |
1 to 11 |
mfr |
Message frequency (send interval) | Milliseconds | pageSettings.messageInterval |
100 to 10000 |
fcver |
Flight controller firmware version | String "M.m.p" |
data.fcVersion; also sets data.extCmdsSupported |
Pattern ^\d+\.\d+\.\d+$ |
pk |
Command signing public key (Ed25519) | Base64 (44 chars) | data.firmwarePublicKey |
44-char base64 string |
lseq |
Last accepted command sequence number | uint32 | — (used to sync localStorage commandSeq) |
0 to 4294967295 |
pvwas introduced in protocol version 1. Firmware that predates this field sends nopvkey; the UI treats a missingpvas version 1 (same as the current protocol). The version is an integer incremented only on breaking changes (removed or reinterpreted fields). Adding new optional fields is not a breaking change and does not require a version bump.halrange covers Dead Sea (-430 m = -43000 cm) to above Everest (8849 m = 884900 cm), rounded to safe integers.ontceiling of 172800 s = 48 h.fltceiling of 86400 s = 24 h.mfrclamped to 100–10000 ms to prevent the UI from interpreting implausibly fast or slow rates.fcveris read from the FC once at startup viaMSP_FC_VERSIONand re-read on reconnect. The UI derives two capability flags from this value:
data.extCmdsSupported= 1 when FC version ≥ 9.0.2 — enablessetheading(MSP2_INAV_SET_CRUISE_HEADING) andjumpwp(MSP2_INAV_SET_WP_INDEX). The Set Course, Jump to WP rows and the map waypoint-click jump feature are hidden when this is 0.data.altCmdSupported= 1 when FC version ≥ 10.0.0 — enablessetalt(MSP2_INAV_SET_ALT_TARGET). The Set Altitude row is hidden when this is 0.pkis always sent, including when the key is all zeros (base64AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=), which means signing is not yet configured. The UI uses this field to verify that its stored public key matches the one flashed to the firmware. Public keys are safe to broadcast — they can only be used to verify signatures, not forge them.lseqis the last command sequence number accepted by the firmware (persisted to NVS). The UI only updates its localcommandSeqfrom this value if the firmware's public key (pk) matches the locally stored key — preventing a UI monitoring a different aircraft from accidentally adopting an unrelated sequence number.
Waypoint messages start with wpno: and are parsed separately from telemetry messages.
| Key | Description | Unit / Scale | Valid Range |
|---|---|---|---|
wpno |
Waypoint number (0 = home point) |
Count | 0 to 255 |
la |
Waypoint latitude | Degrees × 10,000,000 | -900000000 to 900000000 |
lo |
Waypoint longitude | Degrees × 10,000,000 | -1800000000 to 1800000000 |
al |
Waypoint altitude | Centimeters | 0 to 60000 |
ac |
Waypoint action (INAV action enum) | Integer | 1 to 8 |
p1 |
Parameter 1 (omitted if 0) | Integer | -32768 to 32767 |
p2 |
Parameter 2 (omitted if 0) | Integer | -32768 to 32767 |
p3 |
Parameter 3 (omitted if 0) | Integer | -32768 to 32767 |
f |
Flag (omitted if 0) | Integer | 0 to 255 |
alceiling of 60000 cm = 600 m is the typical INAV waypoint altitude limit.acvalues: 1=WAYPOINT, 2=POSHOLD_UNLIM, 3=POSHOLD_TIME, 4=RTH, 5=SET_POI, 6=JUMP, 7=SET_HEAD, 8=LAND.p1/p2/p3are signed 16-bit integers as defined by the MSPv2 waypoint payload.
Mission download messages start with dlwp: and are parsed separately from telemetry messages.
| Key | Description | Unit / Scale | Notes |
|---|---|---|---|
dlwp |
Waypoint number (1-based) | Count | WP 0 (home) is never included |
la |
Latitude | Degrees × 10,000,000 | Always present |
lo |
Longitude | Degrees × 10,000,000 | Always present |
al |
Altitude | Centimetres relative to home | Always present |
ac |
Action code | Integer | 1=Waypoint, 3=Loiter, 4=RTH, 8=Land |
p1 |
Parameter 1 | Integer | Speed cm/s (Waypoint); loiter seconds (Loiter) |
p2 |
Parameter 2 | Integer | Speed cm/s (Loiter); unused otherwise |
p3 |
Parameter 3 | Bitfield | Bit 0 = AMSL altitude flag |
f |
Flag | Integer | 0 = normal; 165 (0xA5) = last waypoint |
Unlike
wpno:messages, all fields are always present indlwp:messages regardless of their value.
When implementing input validation in the UI parser (CommScripts.js), apply the following rules consistently:
- Out-of-range numeric fields: silently discard the field (leave the previous value unchanged). Do not clamp — a clamped value could be misleading (e.g. showing 100 m altitude when the message contained 999999 m).
- Boolean / flag fields (0 or 1): discard the message field if the parsed value is anything other than 0 or 1.
- Enum fields (
nvs,ac,ftm): discard values outside the documented range. - String fields (
cs): reject the callsign if it does not match^[A-Za-z0-9_-]+$or exceeds 16 characters. Fall back to the previously displayed callsign. - GPS coordinates: both latitude and longitude must be within range simultaneously. If either is invalid, discard both (a position with only one valid coordinate is unusable and could plot to the wrong location).
parseInt/parseFloatfailures:NaNresults must be treated as invalid and discarded — never passed to the display logic.
| Message type | Interval |
|---|---|
| Standard telemetry | 1000 ms (default) |
| Low priority | 60 s |
| Waypoint mission | Every 30 telemetry cycles (~30 s) |
| MSP fetch cycle (FC task) | 160 ms per group; 6 groups → full refresh every ~960 ms |
| MSP_GET_RC + MSP_SET_RAW_RC | Every cycle (160 ms) — keeps RC override freshness within INAV's 200 ms window |
| Callsign + waypoints | Every 10 seconds (slow-poll) |
The ESP32 communicates with the flight controller using MSPv2 (MultiWii Serial Protocol v2) over UART at 115200 baud (Serial2: RX=GPIO19, TX=GPIO18).
The FC task runs every 160 ms (TASK_MSP_READ_MS). Telemetry messages are divided into 6 round-robin groups; one group is fetched per cycle, giving a full refresh every ~960 ms. MSP_RC and MSP_SET_RAW_RC run every cycle to keep INAV's RC override freshness timer alive (INAV drops overridden channels if no MSP_SET_RAW_RC arrives within 200 ms).
| MSP Message | Content | When |
|---|---|---|
MSP_RC (105) |
Current RC channel values (all channels) | Every cycle (160 ms) |
MSP_SET_RAW_RC (200) |
Override RC channels for active mode commands | Every cycle (160 ms) |
MSP_RAW_GPS |
GPS coordinates, speed, fix type, satellite count, HDOP | Group 0 |
MSP_COMP_GPS |
Distance and direction to home | Group 0 |
MSP_ATTITUDE |
Roll, pitch, yaw (heading) | Group 1 |
MSP_ALTITUDE |
Estimated altitude, vertical speed | Group 1 |
MSP_SENSOR_STATUS |
Hardware health flag | Group 2 |
MSP_ACTIVEBOXES |
Active flight mode bitmask | Group 2 |
MSP_WP_GETINFO |
Waypoint count and mission validity | Group 3 |
MSP_NAV_STATUS |
Nav state, active waypoint number | Group 3 |
MSP2_INAV_MISC2 |
On-time, flight time, throttle, auto-throttle | Group 4 |
MSP2_INAV_ANALOG |
Battery voltage, current, RSSI, fuel percent | Group 5 |
MSP_FC_VERSION (3) |
FC firmware version (major, minor, patch) | Once at startup |
MSP_BOXIDS (119) |
Flight mode permanent IDs → discovers MSP RC OVERRIDE box ID |
Once at startup |
MSP_MODE_RANGES (34) |
RC channel-to-mode mapping → discovers channel/PWM for each mode | Once at startup |
MSP2_COMMON_SETTING (0x1003) |
Reads msp_override_channels bitmask |
Once at startup (also to confirm write) |
MSP2_COMMON_SET_SETTING (0x1004) |
Writes msp_override_channels to enable needed channels |
Once at startup |
MSP_NAME |
Aircraft callsign | Once at startup; re-polled every 10 s |
MSP_WP |
Individual waypoint data | Every 10 s (slow-poll) |
MSP2_INAV_SET_WP_INDEX (0x2221) |
Jump to waypoint N (U8, 0-based) during an active WP mission | On jumpwp command |
MSP2_INAV_SET_ALT_TARGET (0x2215) |
Set altitude hold target (U8 datum=0 + I32 centimetres, takeoff-relative); INAV 10+ | On setalt command |
MSP2_INAV_SET_CRUISE_HEADING (0x2223) |
Set Cruise/Course Hold heading target (I32, centidegrees) | On setheading command |
Startup sequence: On each boot (or FC reconnect), the firmware probes for the FC every 2 seconds using MSP_NAME. Once the FC responds, the startup sequence runs: MSP_FC_VERSION → MSP_BOXIDS → MSP_MODE_RANGES → MSP2_COMMON_SETTING (read) / MSP2_COMMON_SET_SETTING (write) / MSP2_COMMON_SETTING (confirm). After this, per-cycle polling begins. If MSP communication is lost for more than 1 second, all startup flags reset and the probe sequence restarts.
MSPv2 frame format: $X< header, 1-byte flags, 2-byte message ID, 2-byte payload length, payload, 1-byte CRC8-DVB-S2 checksum.