diff --git a/docs/development/msp/README.md b/docs/development/msp/README.md
index 7377eef84bc..f35a79211b4 100644
--- a/docs/development/msp/README.md
+++ b/docs/development/msp/README.md
@@ -431,6 +431,8 @@ When the MSP JSON specification changes, bump `msp_messages.json` version:
[8305 - MSP2_INAV_EZ_TUNE_SET](#msp2_inav_ez_tune_set)
[8320 - MSP2_INAV_SELECT_MIXER_PROFILE](#msp2_inav_select_mixer_profile)
[8336 - MSP2_ADSB_VEHICLE_LIST](#msp2_adsb_vehicle_list)
+[8339 - MSP2_ADSB_VEHICLE](#msp2_adsb_vehicle)
+[8340 - MSP2_ADSB_VEHICLE_COUNT](#msp2_adsb_vehicle_count)
[8448 - MSP2_INAV_CUSTOM_OSD_ELEMENTS](#msp2_inav_custom_osd_elements)
[8449 - MSP2_INAV_CUSTOM_OSD_ELEMENT](#msp2_inav_custom_osd_element)
[8450 - MSP2_INAV_SET_CUSTOM_OSD_ELEMENTS](#msp2_inav_set_custom_osd_elements)
@@ -458,6 +460,7 @@ When the MSP JSON specification changes, bump `msp_messages.json` version:
[8743 - MSP2_INAV_ARM_DISARM](#msp2_inav_arm_disarm)
[8744 - MSP2_INAV_TIMESYNC](#msp2_inav_timesync)
[8752 - MSP2_INAV_SET_AUX_RC](#msp2_inav_set_aux_rc)
+[8753 - MSP2_INAV_WIND](#msp2_inav_wind)
[12288 - MSP2_BETAFLIGHT_BIND](#msp2_betaflight_bind)
[12289 - MSP2_RX_BIND](#msp2_rx_bind)
@@ -4402,6 +4405,42 @@ When the MSP JSON specification changes, bump `msp_messages.json` version:
**Notes:** Requires `USE_ADSB`. Only a subset of `adsbVehicle_t` is transmitted (callsign, core values, heading in whole degrees, TSLC, emitter type, TTL).
+## `MSP2_ADSB_VEHICLE (8339 / 0x2093)`
+**Description:** Retrieves a single tracked ADSB (Automatic Dependent Surveillance-Broadcast) vehicle by slot index. Intended for polling one slot at a time: query `MSP2_ADSB_VEHICLE_COUNT` for the iteration bound, then request indices `0 .. count-1`, skipping slots with `ttl == 0`, and identify each aircraft by its `icao`. See `adsbVehicle_t` / `adsbVehicleValues_t` in `io/adsb.h`.
+
+**Request Payload:**
+|Field|C Type|Size (Bytes)|Description|
+|---|---|---|---|
+| `index` | `uint8_t` | 1 | Slot index to read, `0 .. (MSP2_ADSB_VEHICLE_COUNT - 1)`. WARNING: this is an iteration cursor over fixed slots, NOT a stable identifier. The same index may return a different aircraft (or an empty slot) on a later poll. Always identify the aircraft by the `icao` field in the reply; never cache or correlate data by index. Returns an error result if the index is out of range. |
+
+**Reply Payload:**
+|Field|C Type|Size (Bytes)|Units|Description|
+|---|---|---|---|---|
+| `icao` | `uint32_t` | 4 | - | ICAO 24-bit address (`vehicleValues.icao`). This is the stable per-aircraft identifier; use it to correlate replies, not the request index. An empty slot reports `icao == 0` and `ttl == 0`. |
+| `lat` | `int32_t` | 4 | 1e-7 deg | Latitude (`vehicleValues.gps.lat`). |
+| `lon` | `int32_t` | 4 | 1e-7 deg | Longitude (`vehicleValues.gps.lon`). |
+| `alt` | `int32_t` | 4 | cm | Altitude above sea level (`vehicleValues.alt`). |
+| `heading` | `uint16_t` | 2 | 1e-2 deg | Course over ground at full resolution (`vehicleValues.heading`). Unlike `MSP2_ADSB_VEHICLE_LIST`, this is in centidegrees, not whole degrees. |
+| `horVelocity` | `uint16_t` | 2 | cm/s | Horizontal (ground) speed (`vehicleValues.horVelocity`). Not present in `MSP2_ADSB_VEHICLE_LIST`. |
+| `tslc` | `uint8_t` | 1 | s | Time since last communication (`vehicleValues.tslc`). |
+| `emitterType` | `uint8_t` | 1 | - | Emitter category (`vehicleValues.emitterType`). |
+| `ttl` | `uint8_t` | 1 | s | Remaining time-to-live for this slot (`adsbVehicle->ttl`). `ttl == 0` means the slot is empty/expired and its contents are stale; skip such entries. |
+| `callsign` | `char[ADSB_CALL_SIGN_MAX_LENGTH]` | 9 (ADSB_CALL_SIGN_MAX_LENGTH) | - | Fixed-length callsign (`vehicleValues.callsign`), padded with NULs if shorter. |
+
+**Notes:** Requires `USE_ADSB`. Reads a single ADSB vehicle slot by index. THE INDEX IS NOT A STABLE HANDLE: slots are reused, so a given index may hold a different aircraft (or be empty, `ttl == 0`) between polls. Correlate aircraft by the `icao` field in the reply, never by index. Compared with the bulk `MSP2_ADSB_VEHICLE_LIST`, this message adds horizontal velocity and reports heading at full (centidegree) resolution, and orders the callsign last. Returns an error result for an out-of-range index.
+
+## `MSP2_ADSB_VEHICLE_COUNT (8340 / 0x2094)`
+**Description:** Returns the number of ADSB vehicle slots available to iterate with `MSP2_ADSB_VEHICLE`.
+
+**Request Payload:** **None**
+
+**Reply Payload:**
+|Field|C Type|Size (Bytes)|Description|
+|---|---|---|---|
+| `count` | `uint8_t` | 1 | Number of vehicle slots to iterate (`MAX_ADSB_VEHICLES`). This is the slot capacity / iteration bound, not the number of currently active aircraft - some slots may be empty (`ttl == 0`). 0 if `USE_ADSB` is disabled. |
+
+**Notes:** Requires `USE_ADSB`. Returns the iteration bound for `MSP2_ADSB_VEHICLE`: request indices `0 .. count-1` and skip any slot whose `ttl == 0`.
+
## `MSP2_INAV_CUSTOM_OSD_ELEMENTS (8448 / 0x2100)`
**Description:** Retrieves counts related to custom OSD elements defined by the programming framework.
@@ -4825,6 +4864,20 @@ When the MSP JSON specification changes, bump `msp_messages.json` version:
**Notes:** CH1-CH12 (index 0-11) are protected and will return `MSP_RESULT_ERROR`. Payload size must be 2-49 bytes. Constraint: `startChannel + channelCount <= 32`. Values persist until overwritten; no timeout. Applied as a post-RX overlay in `calculateRxChannelsAndUpdateFailsafe()` after MSP RC Override but before failsafe. Does not require `USE_RX_MSP` or MSP-RC-OVERRIDE flight mode. Does not affect failsafe detection. When MSP is the primary RX provider, channels covered by `MSP_SET_RAW_RC` are automatically skipped. Channels in the `mspOverrideChannels` bitmask are skipped when MSP RC Override mode is active. Recommended to send with `MSP_FLAG_DONT_REPLY` (flags=0x01) to save bandwidth on telemetry passthrough links. 16-bit mode requires even number of data bytes and values are clamped to 750-2250us.
+## `MSP2_INAV_WIND (8753 / 0x2231)`
+**Description:** Retrieves the estimated horizontal wind speed and direction from the internal wind estimator.
+
+**Request Payload:** **None**
+
+**Reply Payload:**
+|Field|C Type|Size (Bytes)|Units|Description|
+|---|---|---|---|---|
+| `windSpeed` | `uint16_t` | 2 | cm/s | Estimated horizontal wind speed (`getEstimatedHorizontalWindSpeed()`). 0 if unavailable. |
+| `windAngle` | `uint16_t` | 2 | degrees | Estimated wind direction in degrees (0–359, 0 = North). Derived from centidegree value divided by 100. 0 if unavailable. |
+| `flags` | `uint8_t` | 1 | - | Validity flags. Bit 0: wind estimate valid (`isEstimatedWindSpeedValid()`). Remaining bits reserved. |
+
+**Notes:** Requires `USE_WIND_ESTIMATOR`; returns zeroes when wind estimation is not compiled in or not yet valid. Check bit 0 of `flags` before using speed/angle values.
+
## `MSP2_BETAFLIGHT_BIND (12288 / 0x3000)`
**Description:** Initiates the receiver binding procedure for supported serial protocols (CRSF, SRXL2).
diff --git a/docs/development/msp/gen_enum_md.py b/docs/development/msp/gen_enum_md.py
index 7583ba1bf39..03d292805d1 100644
--- a/docs/development/msp/gen_enum_md.py
+++ b/docs/development/msp/gen_enum_md.py
@@ -11,6 +11,9 @@
* If no assignment -> auto-increment.
- If auto-increment occurs inside an active preprocessor condition, wrap the number
in parentheses to indicate conditional numbering: e.g., 3, 4, #ifdef, (5), (6).
+- Mutually-exclusive branches (#ifdef X / #ifndef X siblings, or #else/#elif within
+ one #if family) restart numbering from the branch base, because only one branch's
+ members exist in any given build.
- Tracks nested #if/#ifdef/#ifndef/#elif/#else/#endif and shows Condition text.
- Handles multiline enumerators (split at the first top-level comma).
"""
@@ -81,24 +84,74 @@ def normalize_condition_text(text: str) -> str:
return t
class ConditionStack:
+ """Tracks preprocessor conditionals and the auto-increment base each branch
+ started from, so mutually-exclusive branches (#ifdef X / #ifndef X siblings,
+ or #else / #elif within one family) restart numbering from the shared base
+ instead of continuing through the other branch's members.
+ """
def __init__(self):
- self.stack: List[str] = []
- def push_ifdef(self, sym: str): self.stack.append(sym)
- def push_ifndef(self, sym: str): self.stack.append(f'!{sym}')
- def push_if(self, expr: str): self.stack.append(normalize_condition_text(expr))
- def elif_(self, expr: str):
- if self.stack: self.stack.pop()
- self.stack.append(normalize_condition_text(expr))
- def else_(self):
- if not self.stack: return
- top = self.stack.pop()
- if top.startswith('!'): self.stack.append(top[1:])
- elif top and all(ch.isalnum() or ch == '_' for ch in top): self.stack.append(f'!{top}')
- else: self.stack.append(f'NOT({top})')
+ # Open frames: {'text', 'base', 'sym', 'polarity'}
+ self.stack: List[dict] = []
+ # Most recently closed frame per nesting level, for sibling detection
+ self.closed: dict = {}
+
+ def push_ifdef(self, sym: str, base: Optional[int] = None) -> Optional[int]:
+ return self._push({'text': sym, 'base': base, 'sym': sym, 'polarity': True})
+
+ def push_ifndef(self, sym: str, base: Optional[int] = None) -> Optional[int]:
+ return self._push({'text': f'!{sym}', 'base': base, 'sym': sym, 'polarity': False})
+
+ def push_if(self, expr: str, base: Optional[int] = None) -> Optional[int]:
+ return self._push({'text': normalize_condition_text(expr), 'base': base, 'sym': None, 'polarity': None})
+
+ def _push(self, frame: dict) -> Optional[int]:
+ prev = self.closed.get(len(self.stack))
+ if prev and frame['sym'] is not None and prev['sym'] == frame['sym'] \
+ and prev['polarity'] != frame['polarity']:
+ # mutually-exclusive sibling: restart numbering from the sibling's base
+ frame['base'] = prev['base']
+ self.stack.append(frame)
+ return frame['base']
+
+ def elif_(self, expr: str) -> Optional[int]:
+ if not self.stack:
+ return None
+ base = self.stack[-1]['base']
+ self.stack[-1] = {'text': normalize_condition_text(expr), 'base': base, 'sym': None, 'polarity': None}
+ return base
+
+ def else_(self) -> Optional[int]:
+ if not self.stack:
+ return None
+ base = self.stack[-1]['base']
+ text = self.stack[-1]['text']
+ if text.startswith('!'):
+ text = text[1:]
+ elif text and all(ch.isalnum() or ch == '_' for ch in text):
+ text = f'!{text}'
+ else:
+ text = f'NOT({text})'
+ self.stack[-1] = {'text': text, 'base': base, 'sym': None, 'polarity': None}
+ return base
+
def endif(self):
- if self.stack: self.stack.pop()
+ if not self.stack:
+ return
+ top = self.stack.pop()
+ if top['sym'] is not None:
+ self.closed[len(self.stack)] = top
+ else:
+ self.closed.pop(len(self.stack), None)
+
+ def note_item(self):
+ """An enumerator was parsed at this nesting level, so a previously
+ closed sibling here is no longer immediately preceding and must not
+ be treated as the alternate of a later same-symbol block."""
+ self.closed.pop(len(self.stack), None)
+
def current(self) -> str:
- return " AND ".join(self.stack) if self.stack else ""
+ return " AND ".join(f['text'] for f in self.stack) if self.stack else ""
+
def has_active(self) -> bool:
return bool(self.stack)
@@ -164,12 +217,28 @@ def parse_files(paths: List[Path]) -> List[EnumDef]:
while idx < len(body_lines):
bl = body_lines[idx]
- # inner preproc
- if m := RE_IFDEF.match(bl): inner.push_ifdef(m.group(1)); idx += 1; continue
- if m := RE_IFNDEF.match(bl): inner.push_ifndef(m.group(1)); idx += 1; continue
- if m := RE_IF.match(bl): inner.push_if(m.group(1)); idx += 1; continue
- if m := RE_ELIF.match(bl): inner.elif_(m.group(1)); idx += 1; continue
- if RE_ELSE.match(bl): inner.else_(); idx += 1; continue
+ # inner preproc — reset counter when entering an
+ # exclusive alternate branch (returns new base)
+ if m := RE_IFDEF.match(bl):
+ new_base = inner.push_ifdef(m.group(1), current_numeric)
+ if new_base is not None: current_numeric = new_base
+ idx += 1; continue
+ if m := RE_IFNDEF.match(bl):
+ new_base = inner.push_ifndef(m.group(1), current_numeric)
+ if new_base is not None: current_numeric = new_base
+ idx += 1; continue
+ if m := RE_IF.match(bl):
+ new_base = inner.push_if(m.group(1), current_numeric)
+ if new_base is not None: current_numeric = new_base
+ idx += 1; continue
+ if m := RE_ELIF.match(bl):
+ new_base = inner.elif_(m.group(1))
+ if new_base is not None: current_numeric = new_base
+ idx += 1; continue
+ if RE_ELSE.match(bl):
+ new_base = inner.else_()
+ if new_base is not None: current_numeric = new_base
+ idx += 1; continue
if RE_ENDIF.match(bl): inner.endif(); idx += 1; continue
# accumulate one item across lines
@@ -232,6 +301,7 @@ def parse_files(paths: List[Path]) -> List[EnumDef]:
value_display = str(current_numeric)
enum.items.append(EnumItem(name=name, value_display=value_display, cond=cond_text))
+ inner.note_item()
idx += 1
enums.append(enum)
diff --git a/docs/development/msp/inav_enums.json b/docs/development/msp/inav_enums.json
index d6d2ba0f4c5..05b5eafedd3 100644
--- a/docs/development/msp/inav_enums.json
+++ b/docs/development/msp/inav_enums.json
@@ -649,7 +649,8 @@
"BOXGIMBALCENTER": "58",
"BOXGIMBALHTRK": "59",
"BOXAUTOSPEED": "60",
- "CHECKBOX_ITEM_COUNT": "61"
+ "BOXTERRAINAGLHOLD": "61",
+ "CHECKBOX_ITEM_COUNT": "62"
},
"busIndex_e": {
"_source": "inav/src/main/drivers/bus.h",
@@ -806,7 +807,8 @@
"CURRENT_SENSOR_SMARTPORT": "5",
"CURRENT_SENSOR_CRSF": "6",
"CURRENT_SENSOR_CAN": "7",
- "CURRENT_SENSOR_MAX": "CURRENT_SENSOR_CAN"
+ "CURRENT_SENSOR_INA226": "8",
+ "CURRENT_SENSOR_MAX": "CURRENT_SENSOR_INA226"
},
"devHardwareType_e": {
"_source": "inav/src/main/drivers/bus.h",
@@ -869,7 +871,8 @@
"DEVHW_UG2864": "56",
"DEVHW_SDCARD": "57",
"DEVHW_IRLOCK": "58",
- "DEVHW_PCF8574": "59"
+ "DEVHW_PCF8574": "59",
+ "DEVHW_INA226": "60"
},
"deviceFlags_e": {
"_source": "inav/src/main/drivers/bus.h",
@@ -944,6 +947,13 @@
"DJI_OSD_CN_ADJUSTEMNTS": "6",
"DJI_OSD_CN_MAX_ELEMENTS": "7"
},
+ "dronecanAsyncState_e": {
+ "_source": "inav/src/main/drivers/dronecan/dronecan.h",
+ "DRONECAN_ASYNC_IDLE": "0",
+ "DRONECAN_ASYNC_PENDING": "1",
+ "DRONECAN_ASYNC_READY": "2",
+ "DRONECAN_ASYNC_ERROR": "3"
+ },
"dronecanBitrate_e": {
"_source": "inav/src/main/drivers/dronecan/dronecan.h",
"DRONECAN_BITRATE_125KBPS": "0",
@@ -2442,7 +2452,7 @@
"USE_AUTO_TRANSITION"
],
"MIXERAT_PHASE_DONE": [
- "(5)",
+ "(3)",
"!USE_AUTO_TRANSITION"
]
},
@@ -3650,7 +3660,8 @@
"OWNER_PINIO": "32",
"OWNER_IRLOCK": "33",
"OWNER_DRONECAN": "34",
- "OWNER_TOTAL_COUNT": "35"
+ "OWNER_CURRENT_METER": "35",
+ "OWNER_TOTAL_COUNT": "36"
},
"resourceType_e": {
"_source": "inav/src/main/drivers/resource.h",
@@ -4355,7 +4366,8 @@
"VOLTAGE_SENSOR_SMARTPORT": "4",
"VOLTAGE_SENSOR_CRSF": "5",
"VOLTAGE_SENSOR_CAN": "6",
- "VOLTAGE_SENSOR_MAX": "VOLTAGE_SENSOR_CAN"
+ "VOLTAGE_SENSOR_INA226": "7",
+ "VOLTAGE_SENSOR_MAX": "VOLTAGE_SENSOR_INA226"
},
"vs600Band_e": {
"_source": "inav/src/main/io/smartport_master.h",
diff --git a/docs/development/msp/inav_enums_ref.md b/docs/development/msp/inav_enums_ref.md
index 86143b635a5..5f7116d71eb 100644
--- a/docs/development/msp/inav_enums_ref.md
+++ b/docs/development/msp/inav_enums_ref.md
@@ -73,6 +73,7 @@
- [displayTransactionOption_e](#enum-displaytransactionoption_e)
- [displayWidgetType_e](#enum-displaywidgettype_e)
- [DjiCraftNameElements_t](#enum-djicraftnameelements_t)
+- [dronecanAsyncState_e](#enum-dronecanasyncstate_e)
- [dronecanBitrate_e](#enum-dronecanbitrate_e)
- [dronecanState_e](#enum-dronecanstate_e)
- [dshotCommands_e](#enum-dshotcommands_e)
@@ -1198,7 +1199,8 @@
| `BOXGIMBALCENTER` | 58 | |
| `BOXGIMBALHTRK` | 59 | |
| `BOXAUTOSPEED` | 60 | |
-| `CHECKBOX_ITEM_COUNT` | 61 | |
+| `BOXTERRAINAGLHOLD` | 61 | |
+| `CHECKBOX_ITEM_COUNT` | 62 | |
---
## `busIndex_e`
@@ -1425,7 +1427,8 @@
| `CURRENT_SENSOR_SMARTPORT` | 5 | |
| `CURRENT_SENSOR_CRSF` | 6 | |
| `CURRENT_SENSOR_CAN` | 7 | |
-| `CURRENT_SENSOR_MAX` | CURRENT_SENSOR_CAN | |
+| `CURRENT_SENSOR_INA226` | 8 | |
+| `CURRENT_SENSOR_MAX` | CURRENT_SENSOR_INA226 | |
---
## `devHardwareType_e`
@@ -1494,6 +1497,7 @@
| `DEVHW_SDCARD` | 57 | |
| `DEVHW_IRLOCK` | 58 | |
| `DEVHW_PCF8574` | 59 | |
+| `DEVHW_INA226` | 60 | |
---
## `deviceFlags_e`
@@ -1613,6 +1617,18 @@
| `DJI_OSD_CN_ADJUSTEMNTS` | 6 | |
| `DJI_OSD_CN_MAX_ELEMENTS` | 7 | |
+---
+## `dronecanAsyncState_e`
+
+> Source: ../../../src/main/drivers/dronecan/dronecan.h
+
+| Enumerator | Value | Condition |
+|---|---:|---|
+| `DRONECAN_ASYNC_IDLE` | 0 | |
+| `DRONECAN_ASYNC_PENDING` | 1 | |
+| `DRONECAN_ASYNC_READY` | 2 | |
+| `DRONECAN_ASYNC_ERROR` | 3 | |
+
---
## `dronecanBitrate_e`
@@ -3593,7 +3609,7 @@
| `MIXERAT_PHASE_TRANSITIONING` | 2 | |
| `MIXERAT_PHASE_POST_SWITCH_FADE` | (3) | USE_AUTO_TRANSITION |
| `MIXERAT_PHASE_TAILSITTER_TO_MC_CAPTURE` | (4) | USE_AUTO_TRANSITION |
-| `MIXERAT_PHASE_DONE` | (5) | !USE_AUTO_TRANSITION |
+| `MIXERAT_PHASE_DONE` | (3) | !USE_AUTO_TRANSITION |
---
## `mixerProfileATWaitReason_e`
@@ -5247,7 +5263,8 @@
| `OWNER_PINIO` | 32 | |
| `OWNER_IRLOCK` | 33 | |
| `OWNER_DRONECAN` | 34 | |
-| `OWNER_TOTAL_COUNT` | 35 | |
+| `OWNER_CURRENT_METER` | 35 | |
+| `OWNER_TOTAL_COUNT` | 36 | |
---
## `resourceType_e`
@@ -6333,7 +6350,8 @@
| `VOLTAGE_SENSOR_SMARTPORT` | 4 | |
| `VOLTAGE_SENSOR_CRSF` | 5 | |
| `VOLTAGE_SENSOR_CAN` | 6 | |
-| `VOLTAGE_SENSOR_MAX` | VOLTAGE_SENSOR_CAN | |
+| `VOLTAGE_SENSOR_INA226` | 7 | |
+| `VOLTAGE_SENSOR_MAX` | VOLTAGE_SENSOR_INA226 | |
---
## `vs600Band_e`