Add MAVLink device class (traffic_type = 6) - #2
Conversation
New device class spec for payloads that are MAVLink endpoints: version negotiation (MAVLink 1/2) at class setup, then a transparent bidirectional byte tunnel (MAVLINK_DATA). Not frame-aligned, so signed MAVLink 2 frames up to 280 bytes cross the 255-byte APEX payload cap without a fragmentation scheme. Registered as traffic_type 6 in the device class registry.
|
Hello! I've looked through this and so far, most things seem fine, but we need to account for updating the standard library as well. Thanks! |
|
Hello! I went through this with a finer toothed comb and found a few areas of improvement to make. See below and the suggested changes comments for my findings. Findings
Plus a §3 note pinning down that framing is the only thing this class negotiates (so scope doesn't creep to dialect/signing/routing). |
| The Host receives CAPABILITY, intersects the Device's supported versions | ||
| against its own, selects exactly one version, and replies with CONFIG | ||
| ([§4.4](#4-4--config-frame-host--device)). Once the Device's ACK confirms | ||
| acceptance, the Host feeds its endpoint's MAVLink byte stream into | ||
| MAVLINK_DATA frames toward the Device and reassembles the Device's stream from | ||
| inbound MAVLINK_DATA frames. |
There was a problem hiding this comment.
§2.2 Host — implicit-ACK + reject branch (#1, #3):
| The Host receives CAPABILITY, intersects the Device's supported versions | |
| against its own, selects exactly one version, and replies with CONFIG | |
| ([§4.4](#4-4--config-frame-host--device)). Once the Device's ACK confirms | |
| acceptance, the Host feeds its endpoint's MAVLink byte stream into | |
| MAVLINK_DATA frames toward the Device and reassembles the Device's stream from | |
| inbound MAVLINK_DATA frames. | |
| The Host receives CAPABILITY and intersects the Device's supported versions | |
| against its own. If the intersection is non-empty it selects exactly one | |
| version and replies with an accepting CONFIG; if it is empty (or the | |
| advertisement is malformed) it replies with a rejecting CONFIG | |
| ([§4.4](#4-4--config-frame-host--device)). The Host considers the session | |
| ACTIVE on the Device's ACK **or** the first MAVLINK_DATA frame from the Device | |
| — whichever arrives first ([§4.6](#4-6--mavlink_data-frame-bidirectional), | |
| [§4.8](#4-8--reporting-cadence-and-timing)) — after which it feeds its | |
| endpoint's MAVLink byte stream into MAVLINK_DATA frames toward the Device and | |
| reassembles the Device's stream from inbound MAVLINK_DATA frames. |
| <a id="3--negotiated-parameters" name="3--negotiated-parameters"></a> | ||
| ## 3. Negotiated Parameters | ||
|
|
||
| One parameter is negotiated at the start of every session. |
There was a problem hiding this comment.
§3 Negotiated Parameters — scope note (scope)
| One parameter is negotiated at the start of every session. | |
| One parameter is negotiated at the start of every session — by design, exactly | |
| one. | |
| Version negotiation is the single, deliberate exception to this class's opacity | |
| ([§1](#1--overview)). Its only purpose is to spare the endpoints from having to | |
| guess wire framing: rather than each MAVLink stack sniffing start bytes or | |
| falling back heuristically, APEX agrees on MAVLink 1 vs 2 framing up front so | |
| both parsers are configured deterministically from the first byte. That is the | |
| **only** endpoint concern this class takes on. Everything else — dialect, | |
| message set, microservices, `sysid` / `compid`, signing, retransmission, and | |
| rate — stays with the endpoints and rides transparently inside the tunnel | |
| ([§1](#1--overview), [§5.3](#5-3--no-mavlink-level-routing)). The class | |
| negotiates framing and nothing more; future revisions extend the tunnel, not | |
| the set of decisions APEX makes on the endpoints' behalf. |
| | --- | --- | --- | --- | | ||
| | `0` | *(reserved)* | — | Reserved; never sent. | | ||
| | `1` | **CAPABILITY** | Device → Host | Declares supported MAVLink versions ([§4.3](#4-3--capability-frame-device--host)). | | ||
| | `2` | **CONFIG** | Host → Device | Selects one version ([§4.4](#4-4--config-frame-host--device)). | |
There was a problem hiding this comment.
§4.1 sub-header — CONFIG row (#1)
| | `2` | **CONFIG** | Host → Device | Selects one version ([§4.4](#4-4--config-frame-host--device)). | | |
| | `2` | **CONFIG** | Host → Device | The Host's verdict on the advertisement: accept and select a version, or reject ([§4.4](#4-4--config-frame-host--device)). | |
| 1. Core discovery completes for `device_class_req = 6` ([APEX — Core §3.3](APEX_Core.md#3-3--startup-discovery-handshake)). Class traffic on `traffic_type = 6` becomes valid. | ||
| 2. Device immediately emits **CAPABILITY** ([§4.3](#4-3--capability-frame-device--host)) — unprompted, exactly once. | ||
| 3. Host emits **CONFIG** ([§4.4](#4-4--config-frame-host--device)) with the chosen MAVLink version. | ||
| 4. Device replies **ACK** ([§4.5](#4-5--ack-frame-device--host)). On `ACCEPTED`, the session enters ACTIVE. |
There was a problem hiding this comment.
§4.2 lifecycle — steps 3–4 (#1)
| 4. Device replies **ACK** ([§4.5](#4-5--ack-frame-device--host)). On `ACCEPTED`, the session enters ACTIVE. | |
| 3. Host evaluates the advertisement and emits **CONFIG** ([§4.4](#4-4--config-frame-host--device)): either `ACCEPTED` with the chosen MAVLink version, or a reject code if it cannot satisfy the advertisement. | |
| 4. On an accepting CONFIG the Device adopts the version and replies **ACK** ([§4.5](#4-5--ack-frame-device--host)); on `ACCEPTED` the session enters ACTIVE. On a rejecting CONFIG the Device sends no ACK and transitions to FAULT ([§4.7](#4-7--state-machine-device)). |
| | `3` | `intended_rate_hz` | `u8` | The Device's intended steady-state MAVLINK_DATA frame rate, in Hz. Informational; the Host MAY use it to size buffers but MUST NOT enforce it. `0` = unspecified / event-driven. | | ||
|
|
||
| `supported_versions == 0` is malformed; the Host MUST reject with | ||
| `REJECT_MALFORMED` ([§4.5](#4-5--ack-frame-device--host)). |
There was a problem hiding this comment.
§4.3 CAPABILITY — reject pointer (#1) — replace the two lines supported_versions == 0 is malformed; the Host MUST reject with / REJECT_MALFORMED ([§4.5]…).`:
| `REJECT_MALFORMED` ([§4.5](#4-5--ack-frame-device--host)). | |
| `supported_versions == 0` is malformed; the Host MUST reject the advertisement | |
| via CONFIG with `REJECT_MALFORMED` ([§4.4](#4-4--config-frame-host--device)). |
|
|
||
| The Host MUST send exactly one CONFIG in response to each CAPABILITY. If the | ||
| Device sees a second CONFIG with a different parameter, it MUST reply | ||
| `REJECT_MALFORMED` and remain in its current state. |
There was a problem hiding this comment.
§4.4 CONFIG — add result verdict + idempotent retransmit (#1, #3)
| `REJECT_MALFORMED` and remain in its current state. | |
| CONFIG is the Host's verdict on the Device's CAPABILITY advertisement — the | |
| accept/reject half of the class negotiation. It mirrors Core's `CONFIG_REPLY` | |
| ([APEX — Core §3.2.2](APEX_Core.md#3-2-2--config_reply-msg_id--2)): a single | |
| reply that either accepts and selects the parameter, or rejects with a reason. | |
| | Offset | Field | Width | Description | | |
| | --- | --- | --- | --- | | |
| | `0` | `class_msg_id` | `u8` | `2` (CONFIG). | | |
| | `1` | `result` | `u8` | The Host's verdict (see below). | | |
| | `2` | `selected_version` | `u8` | On `ACCEPTED`, exactly one value from [§3.1](#3-1--mavlink-version); it MUST be a value the Device declared as supported. On any reject, `0x00`. | | |
| **`result` values:** | |
| | Value | Name | Meaning | | |
| | --- | --- | --- | | |
| | `0x00` | `ACCEPTED` | The Host supports one of the advertised versions and has selected it in `selected_version`. | | |
| | `0x01` | `REJECT_NO_COMMON_VERSION` | The Host supports none of the versions in the Device's `supported_versions`. | | |
| | `0x02` | `REJECT_MALFORMED` | CAPABILITY could not be parsed (e.g. `supported_versions == 0`). | | |
| A Device that receives a `result` value not enumerated above MUST treat it as | |
| `REJECT_NO_COMMON_VERSION`. On any reject the Device transitions to FAULT | |
| without sending ACK ([§4.7](#4-7--state-machine-device)); the Host SHOULD | |
| surface the specific reject code to the operator, which is the only signal a | |
| passive operator has that setup failed. | |
| The Host sends one CONFIG per CAPABILITY, but MAY **retransmit that identical | |
| CONFIG** if it observes neither ACK nor MAVLINK_DATA within the ACK timeout | |
| ([§4.8](#4-8--reporting-cadence-and-timing)). A retransmitted CONFIG is | |
| idempotent: a Device that receives a CONFIG identical to one it has already | |
| accepted MUST re-send its ACK ([§4.5](#4-5--ack-frame-device--host)) and remain | |
| ACTIVE, so a lost ACK is recovered without disturbing the tunnel. A second | |
| CONFIG whose `result` or `selected_version` **differs** from the first is a | |
| protocol violation: the Device MUST reply `REJECT_MALFORMED` and remain in its | |
| current state. |
| A rejecting ACK is **terminal in v1**: the Device transitions to FAULT | ||
| ([§4.7](#4-7--state-machine-device)) and the operator must intervene. A Host | ||
| that cannot satisfy a Device's CAPABILITY SHOULD surface the specific reject | ||
| code to the operator; that's the only signal a passive operator has. |
There was a problem hiding this comment.
§4.5 ACK — clarify it's a confirmation; scope reject codes (#1)
| code to the operator; that's the only signal a passive operator has. | |
| ACK is the Device's confirmation of an **accepting** CONFIG — the Core | |
| `CONFIG_ACK` analog ([APEX — Core §3.2.6](APEX_Core.md#3-2-6--config_ack-msg_id--6)). | |
| The Device sends ACK only in response to a CONFIG whose `result` is `ACCEPTED`; | |
| a rejecting CONFIG is not acknowledged ([§4.4](#4-4--config-frame-host--device)). | |
| | Offset | Field | Width | Description | | |
| | --- | --- | --- | --- | | |
| | `0` | `class_msg_id` | `u8` | `3` (ACK). | | |
| | `1` | `result` | `u8` | Result code (see below). | | |
| **`result` values:** | |
| | Value | Name | Meaning | | |
| | --- | --- | --- | | |
| | `0x00` | `ACCEPTED` | The Device has adopted the selected MAVLink version; MAVLINK_DATA exchange may begin. | | |
| | `0x01` | `REJECT_VERSION` | The accepting CONFIG's `selected_version` is not a value the Device declared as supported — a Host error, since the Host MUST only select an advertised version ([§4.4](#4-4--config-frame-host--device)). | | |
| | `0x02` | `REJECT_MALFORMED` | The CONFIG frame could not be parsed. | | |
| A rejecting ACK is **terminal in v1**: the Device transitions to FAULT | |
| ([§4.7](#4-7--state-machine-device)) and the operator must intervene. The Host | |
| SHOULD surface the specific reject code to the operator; that's the only signal | |
| a passive operator has. |
| then discards the truncated message and resynchronizes on the next start byte. | ||
| The class layer does **not** retransmit; reliability for messages that need it | ||
| is a MAVLink-level concern (e.g. command/ack, param retry) handled by the | ||
| endpoints. |
There was a problem hiding this comment.
§4.6 MAVLINK_DATA — scope the "no retransmit" statement (#3, consistency)
| endpoints. | |
| The class layer does **not** retransmit dropped MAVLINK_DATA; reliability for | |
| tunnelled messages that need it is a MAVLink-level concern (e.g. command/ack, | |
| param retry) handled by the endpoints. (Setup is the one exception: the Host | |
| retransmits CONFIG to recover a lost ACK — [§4.4](#4-4--config-frame-host--device), | |
| [§4.8](#4-8--reporting-cadence-and-timing).) |
| endpoints. | ||
|
|
||
| MAVLINK_DATA frames are sent **unprompted** in both directions. Each side's | ||
| cadence is whatever its MAVLink stack produces; there is no per-frame ACK. |
There was a problem hiding this comment.
§4.6 MAVLINK_DATA — implicit-ACK on receive (#3)
| cadence is whatever its MAVLink stack produces; there is no per-frame ACK. | |
| MAVLINK_DATA frames are sent **unprompted** in both directions. Each side's | |
| cadence is whatever its MAVLink stack produces; there is no per-frame ACK. | |
| A MAVLINK_DATA frame from a Device the Host still considers **pre-ACTIVE** | |
| implicitly confirms acceptance: a Device emits MAVLINK_DATA only after it has | |
| accepted CONFIG and entered ACTIVE ([§4.7](#4-7--state-machine-device)), so the | |
| Host MUST transition that session to ACTIVE and reassemble the frame's bytes | |
| rather than discard them. This is what recovers a lost ACK — the tunnel comes | |
| up on the first data frame even if the ACK never arrived | |
| ([§4.8](#4-8--reporting-cadence-and-timing)). |
| The Host does not run an explicit state machine for this class beyond | ||
| "pre-CONFIG" / "ACTIVE" per device — the device's Core lifecycle status | ||
| ([APEX — Core §4](APEX_Core.md#4--device-lifecycle-status)) tells it | ||
| everything else it needs. |
There was a problem hiding this comment.
§4.7 state machine — link loss → re-discovery, scope FAULT (#2, #3)
| everything else it needs. | |
| | `0xFF` | **FAULT** | Class negotiation failed in a way retrying cannot fix — CONFIG rejected by the Host, or the Device rejected a malformed/impossible CONFIG ([§4.4](#4-4--config-frame-host--device), [§4.5](#4-5--ack-frame-device--host)). Terminal in v1; requires operator intervention. | | |
| ```mermaid | |
| stateDiagram-v2 | |
| [*] --> WAITING_CONFIG: Class active (Core ACK_OK) | |
| WAITING_CONFIG --> ACTIVE: CONFIG ACCEPTED, ACK ACCEPTED | |
| WAITING_CONFIG --> FAULT: CONFIG reject (Host) / ACK reject (Device) | |
| WAITING_CONFIG --> [*]: No CONFIG in 5 s / link loss → re-discovery | |
| ACTIVE --> ACTIVE: Duplicate identical CONFIG (re-ACK) | |
| ACTIVE --> [*]: Core watchdog / link loss → re-discovery | |
| FAULT --> [*] | |
| ``` | |
| **Transport loss is not a FAULT.** Loss of bus liveliness — a Core watchdog | |
| expiry or link loss ([APEX — Core §3.5](APEX_Core.md#3-5--heartbeat)) — is | |
| transient and recoverable, so the Device does **not** enter FAULT on it. | |
| Instead it leaves the class state machine, returns to its Core initial state, | |
| and restarts the discovery handshake | |
| ([APEX — Core §3.3](APEX_Core.md#3-3--startup-discovery-handshake)); a | |
| successful re-discovery brings it back to WAITING_CONFIG. The same applies if no | |
| CONFIG arrives within the 5 s setup window. FAULT is reserved for a negotiation | |
| outcome that re-discovery would only reproduce — an incompatible or malformed | |
| CONFIG — which genuinely needs a human. | |
| The Host does not run an explicit state machine for this class beyond | |
| "pre-CONFIG" / "ACTIVE" per device — the device's Core lifecycle status | |
| ([APEX — Core §4](APEX_Core.md#4--device-lifecycle-status)) tells it | |
| everything else it needs. It enters ACTIVE on the first ACK **or** MAVLINK_DATA | |
| from the device ([§2.2](#2-2--host), [§4.6](#4-6--mavlink_data-frame-bidirectional)), | |
| and retransmits the identical CONFIG if neither arrives in time | |
| ([§4.8](#4-8--reporting-cadence-and-timing)). |
| endpoint's MAVLink stack falls silent, the side MUST still meet the Core | ||
| transmit floor — a MAVLink HEARTBEAT (emitted by every conformant MAVLink | ||
| endpoint at ≥ 1 Hz) naturally satisfies this; the class layer adds no | ||
| keep-alive of its own. |
There was a problem hiding this comment.
§4.8 timing — host retransmit + heartbeat fallback (#2, #3, #4)
| keep-alive of its own. | |
| - **CONFIG.** The Host SHOULD emit CONFIG within **500 ms** of receiving | |
| CAPABILITY. A Device that has not received CONFIG within **5 s** returns to | |
| its initial state and restarts discovery ([§4.7](#4-7--state-machine-device)) | |
| rather than faulting — a lost CAPABILITY or CONFIG is recoverable. | |
| - **ACK.** The Device MUST emit ACK within **200 ms** of receiving an accepting | |
| CONFIG. A rejecting CONFIG is not acknowledged ([§4.5](#4-5--ack-frame-device--host)). | |
| - **CONFIG retransmit (Host).** The Host starts the session ACTIVE on the first | |
| ACK **or** MAVLINK_DATA it receives after sending an accepting CONFIG | |
| ([§2.2](#2-2--host), [§4.6](#4-6--mavlink_data-frame-bidirectional)). If it | |
| observes neither within **500 ms**, it retransmits the identical CONFIG | |
| ([§4.4](#4-4--config-frame-host--device)), for up to **3 attempts** total. If | |
| none elicits a response, the Host treats the device as unresponsive and | |
| applies the Core [§3.5](APEX_Core.md#3-5--heartbeat) recovery — reset via Pin | |
| 9 and re-discovery, or mark it FAULT at the Core lifecycle level | |
| ([APEX — Core §4](APEX_Core.md#4--device-lifecycle-status)) (this is the | |
| host-side lifecycle FAULT for a dead device, not the class-state FAULT of | |
| [§4.7](#4-7--state-machine-device)). All three attempts fall well inside the | |
| Device's 5 s setup window, so a Device never abandons setup while the Host is | |
| still retrying. | |
| - **MAVLINK_DATA.** Exchange begins immediately after the Device sends | |
| `ACCEPTED`. While MAVLink bytes are flowing, MAVLINK_DATA traffic in each | |
| direction satisfies the Core [§3.5](APEX_Core.md#3-5--heartbeat) 1 Hz transmit | |
| floor on that side — and in normal operation a conformant MAVLink endpoint | |
| emits a HEARTBEAT at ≥ 1 Hz, so the tunnel rarely goes quiet. If a side's | |
| MAVLink stream does fall silent, it MUST still meet the Core transmit floor by | |
| emitting the **Core implicit heartbeat** — an empty `CONFIG` frame | |
| (`traffic_type = 0`, `payload_length = 0`; | |
| [APEX — Core §3.5](APEX_Core.md#3-5--heartbeat)) — not by depending on the | |
| MAVLink application to produce a frame. Liveness is thus a property of the | |
| APEX layer, so a briefly quiet endpoint never trips the watchdog. |
| <a id="5-1--mavlink-frame-size" name="5-1--mavlink-frame-size"></a> | ||
| ### 5.1. MAVLink frame size | ||
|
|
||
| A MAVLink 2 frame can be up to 280 bytes (12 B header + 255 B payload + 2 B |
There was a problem hiding this comment.
§5.1 — header byte count (#5) — replace the line A MAVLink 2 frame can be up to 280 bytes (12 B header + 255 B payload + 2 B:
| A MAVLink 2 frame can be up to 280 bytes (12 B header + 255 B payload + 2 B | |
| A MAVLink 2 frame can be up to 280 bytes (10 B header + 255 B payload + 2 B |
| | Byte(s) | Hex | Field | Value | | ||
| | --- | --- | --- | --- | | ||
| | 4 | `02` | `class_msg_id` | `2` (CONFIG) | | ||
| | 5 | `01` | `selected_version` | `1` (MAVLINK2) | |
There was a problem hiding this comment.
§6.3 Step 2 — worked example bytes (#1)
| | 5 | `01` | `selected_version` | `1` (MAVLINK2) | | |
| Inner payload 3 bytes (`LN = 03`). | |
| ``` | |
| 00 06 01 03 02 00 01 | |
| ``` | |
| | Byte(s) | Hex | Field | Value | | |
| | --- | --- | --- | --- | | |
| | 4 | `02` | `class_msg_id` | `2` (CONFIG) | | |
| | 5 | `00` | `result` | `ACCEPTED` | | |
| | 6 | `01` | `selected_version` | `1` (MAVLINK2) | |
| A MAVLink 2 COMMAND_LONG (one full frame). Identical structure to Step 4; only | ||
| the direction and content differ. If the Host had a 280-byte signed frame to | ||
| send, it would split across two MAVLINK_DATA frames — the Device's MAVLink | ||
| parser reassembles it transparently ([§5.1](#5-1--mavlink-frame-size)). |
There was a problem hiding this comment.
§6.4 — new rejected-setup example (#1) — this is an insertion. Select the last two lines of §6.3 Step 5 (send, it would split across two MAVLINK_DATA frames … and the following parser reassembles it transparently ([§5.1](#5-1--mavlink-frame-size)).) and replace with the same two lines plus the new section (4-backtick wrapper — contains fences):
| parser reassembles it transparently ([§5.1](#5-1--mavlink-frame-size)). | |
| send, it would split across two MAVLINK_DATA frames — the Device's MAVLink | |
| parser reassembles it transparently ([§5.1](#5-1--mavlink-frame-size)). | |
| <a id="6-4--rejected-setup" name="6-4--rejected-setup"></a> | |
| ### 6.4. Rejected setup (no common version) | |
| If instead the Device advertised MAVLink 1 only (`supported_versions = 0x01`) | |
| while the Host speaks MAVLink 2 only, the Host answers the advertisement with a | |
| rejecting CONFIG and no tunnel opens. | |
| ```mermaid | |
| sequenceDiagram | |
| participant H as Host | |
| participant D as Device | |
| D->>H: CAPABILITY (MAVLink 1 only) | |
| H->>D: CONFIG (REJECT_NO_COMMON_VERSION) | |
| Note over H,D: Device → FAULT. No ACK. Operator intervenes. | |
| ``` | |
| Host CONFIG, inner payload 3 bytes (`LN = 03`): | |
| ``` | |
| 00 06 01 03 02 01 00 | |
| ``` | |
| | Byte(s) | Hex | Field | Value | | |
| | --- | --- | --- | --- | | |
| | 4 | `02` | `class_msg_id` | `2` (CONFIG) | | |
| | 5 | `01` | `result` | `REJECT_NO_COMMON_VERSION` | | |
| | 6 | `00` | `selected_version` | `0` (unused on reject) | |
This proposes a new device class for payloads that are MAVLink endpoints: a microcontroller on the payload side running its own MAVLink stack, with the flight controller as the APEX Host. APEX negotiates the MAVLink wire version (1 or 2) once during class setup, then becomes a transparent bidirectional byte tunnel (MAVLINK_DATA). The class is deliberately opaque to MAVLink content: no parsing, no sysid/compid rewriting, no routing. Those stay with the endpoints.
Because the tunnel is a byte stream rather than frame-aligned, signed MAVLink 2 frames up to 280 bytes cross the 255-byte APEX payload cap without a fragmentation scheme; the receiver's MAVLink parser recovers frame boundaries from the MAVLink framing itself. This is the main design difference from the frame-aligned Analog HMI class, which has to cap MAVLink 2 frames at 254 bytes (Analog HMI 5.1).