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`