APEX — Adaptive Payload EXchange
Status: Draft | Scope: Wayfinding device class (traffic_type = 3)
The Wayfinding class covers Devices that produce navigational cues for the operator — "point me in this direction" updates the Host renders on its display, typically as a labeled arrow per target. The Host shows the direction and the description; the operator decides how to act.
The protocol is agnostic to the source of the cue. A wayfinding Device may compute its targets from anything it likes; only the resulting bearing and label appear on the wire. Each update is a small set of measurements about a target: a bearing the operator should look toward, optionally an elevation and a proximity value, and a short human-readable descriptor (e.g. "WAYPOINT#1", "Target #1", "01").
This class supports:
- Multiple simultaneous targets, each addressed by a small index (1..N).
- A short ASCII descriptor per target to give the operator context.
- Optional per-measurement fields (elevation, proximity) that a Device may include or omit; the Host parses only what the Device declared.
- Stream-and-forget operation — the Host does not poll, and there is no per-update acknowledgement.
This class does not support (out of scope for the current version; future class candidates):
- Bidirectional negotiation about which target the operator is following.
- Bearing values at sub-degree precision. Bearings are reported in whole degrees (§3.1); a future version may add a higher-precision mode.
This document covers both sides of the class — Device role and Host role. Discovery, framing, and capability exchange at the bus level are out of scope and are handled by Core. All frames in this class carry traffic_type = 3 in the APEX outer header.
The Device declares the shape of the updates it will produce — the number of target slots it uses, which optional measurement fields it includes, and the maximum descriptor length — via the CAPABILITY frame (§4.3). On ACCEPTED (§4.5), the Device streams TARGET_UPDATE frames (§4.6) at its own cadence. The Device decides when to send; the Host does not poll. A Device may add, refresh, or clear targets at any time while in ACTIVE.
The Host receives CAPABILITY, replies with CONFIG (§4.4) (acceptance is essentially unconditional in the current version; the CONFIG frame exists to surface a clear malformed-capability case), and consumes incoming TARGET_UPDATE frames. The Host presents the cues however the platform sees fit — typically an OSD arrow per target index with the descriptor as a label.
The Host MUST NOT send TARGET_UPDATE frames. Wayfinding is one-way Device → Host.
Bearings are reported relative to the airframe's current heading, in whole degrees, with 0° straight ahead and increasing clockwise. A bearing of 90° points to the operator's right, 180° aft, 270° to the left.
| Value | Meaning |
|---|---|
0–359 |
Valid bearing in degrees relative to heading. |
0xFFFF |
Clear — remove the target at the given index from the Host's display (§4.6). |
360–0xFFFE |
Reserved. A Device MUST NOT send these; a Host receiving one MUST treat the TARGET_UPDATE as malformed and drop it silently (APEX — Core §3.8). |
A Device that knows its targets in an absolute frame is responsible for converting to heading-relative before transmitting. The Host is not required to provide the heading.
A Device may include any of the following additional measurements in each TARGET_UPDATE. Which ones are included is declared once in the CAPABILITY frame (§4.3) as a bitmask, and every TARGET_UPDATE from that Device carries exactly the declared set, in the order below.
| Bit | Field | Width | Description |
|---|---|---|---|
0 |
ELEVATION |
i16 |
Elevation above horizontal, in whole degrees (-90..+90). Reserved bit pattern 0x8000 means unknown. |
1 |
PROXIMITY |
i16 |
A measure of how close the target is, in payload-defined units. The protocol does not assign units or interpret the value — the Device declares the unit and polarity (i.e. whether higher or lower means closer) once in the CAPABILITY frame (§4.3). Reserved bit pattern 0x8000 means unknown. |
2–7 |
(reserved) | — | MUST be 0. |
Bits 2–7 are reserved and a Device MUST NOT set them in the current version; a Host that receives a CAPABILITY with any reserved bit set replies REJECT_MALFORMED (§4.5).
A Device that includes no optional fields sets optional_fields = 0. A Device that includes elevation and proximity sets optional_fields = 0b00000011.
The class follows the APEX framing model (APEX — Core §3): every frame carries traffic_type = 3, is COBS-framed, and uses the outer header from APEX — Core §3.1.1, with the inner payload carrying class-specific content. All multi-byte fields are little-endian (APEX — Core §3.1).
class_msg_id |
Name | Direction | Meaning |
|---|---|---|---|
0 |
(reserved) | — | Reserved; never sent. |
1 |
CAPABILITY | Device → Host | Declares update shape (§4.3). |
2 |
CONFIG | Host → Device | Accepts or implicitly rejects the capability (§4.4). |
3 |
ACK | Device → Host | Acknowledges CONFIG (§4.5). |
4 |
TARGET_UPDATE | Device → Host | One target's current measurements (§4.6). |
- Core discovery completes for
device_class_req = 3(APEX — Core §3.3). Class traffic ontraffic_type = 3becomes valid. - Device immediately emits CAPABILITY (§4.3) — unprompted, exactly once.
- Host emits CONFIG (§4.4) accepting the declared shape.
- Device replies ACK (§4.5). On
ACCEPTED, the session enters ACTIVE. - Device streams TARGET_UPDATE frames (§4.6) at its own cadence for the remainder of the session.
| Offset | Field | Width | Description |
|---|---|---|---|
0 |
class_msg_id |
u8 |
1 (CAPABILITY). |
1 |
class_spec_version |
u8 |
Wayfinding-class spec revision the Device implements. 0 = the revision defined by this document. |
2 |
max_targets |
u8 |
Number of simultaneously addressable target slots. 1..16. The Host uses indices 1..max_targets in TARGET_UPDATE frames. |
3 |
optional_fields |
u8 |
Bitmask of optional measurement fields the Device includes in every TARGET_UPDATE (§3.2). |
4 |
descriptor_max_bytes |
u8 |
Maximum length, in bytes, of the descriptor field the Device will ever emit. 0..32. A value of 0 means the Device does not emit descriptors. |
5 |
proximity_polarity |
u8 |
Tells the Host which direction of change means the target is getting closer. 0 = LOWER_IS_CLOSER (e.g. meters, range — the value decreases as the operator approaches). 1 = HIGHER_IS_CLOSER (e.g. RSSI, signal strength — the value increases as the operator approaches). Present only when bit 1 of optional_fields is set (§3.2); omitted otherwise. |
6…13 |
proximity_unit |
char[8] |
ASCII unit label for the PROXIMITY field, NUL-padded to 8 bytes. The Host displays this string verbatim alongside the value (typical labels: "m", "km", "dBm", "RSSI"). Present only when bit 1 of optional_fields is set; omitted otherwise. The string is the last field in CAPABILITY so a future revision can promote it to a variable-length representation without disturbing earlier offsets. |
A max_targets of 0 or max_targets > 16 is malformed; the Host MUST reply REJECT_MALFORMED (§4.5). A descriptor_max_bytes > 32 is malformed for the same reason. A proximity_polarity value other than 0 or 1 is malformed.
A class_spec_version value the Host does not understand should be treated as 0 — the Host parses the fields it knows. Future revisions of this spec will extend CAPABILITY only by appending fields, never by repurposing existing ones.
| Offset | Field | Width | Description |
|---|---|---|---|
0 |
class_msg_id |
u8 |
2 (CONFIG). |
The CONFIG frame carries no body in the current version. Its purpose is to give the Host an explicit handshake step on which it may decide to reject the capability — see §4.5. A Host that accepts the capability sends CONFIG; a Host that finds it malformed sends nothing and lets the Device timeout to FAULT, or sends CONFIG and lets the Device's own validation produce REJECT_MALFORMED.
A future revision may add negotiation fields here (e.g. cadence caps, descriptor encoding selection).
| 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 entered ACTIVE; TARGET_UPDATE streaming begins. |
0x01 |
REJECT_MALFORMED |
The Device's own CAPABILITY was malformed (reserved bits set, max_targets out of range, etc.) or the Host's CONFIG was malformed. |
A rejecting ACK is terminal in the current version: the Device transitions to FAULT (§4.7) and the operator must intervene. A Host that detects a malformed CAPABILITY SHOULD surface the reject to the operator; that is the only signal a passive operator has.
| Offset | Field | Width | Description |
|---|---|---|---|
0 |
class_msg_id |
u8 |
4 (TARGET_UPDATE). |
1 |
target_index |
u8 |
Index of the target this update applies to. 1..max_targets. |
2 |
bearing |
u16 |
Heading-relative bearing in degrees (§3.1). 0..359 is a valid bearing; 0xFFFF clears the target. |
4… |
optional fields | variable | The optional measurement fields the Device declared in CAPABILITY (§3.2), in the bit order shown there: ELEVATION, then PROXIMITY. Only the declared fields are present. |
| (after) | descriptor_len |
u8 |
Number of bytes of descriptor that follow. 0..descriptor_max_bytes. Omitted entirely if descriptor_max_bytes == 0 in CAPABILITY. |
| (after) | descriptor |
descriptor_len × u8 |
ASCII descriptor for the target (no NUL terminator). May change between updates for the same target_index. |
A TARGET_UPDATE with target_index == 0 or target_index > max_targets is malformed; the Host MUST drop it silently. A TARGET_UPDATE with descriptor_len > descriptor_max_bytes is malformed; the Host MUST drop it silently.
Clearing a target. A Device clears target i by sending a TARGET_UPDATE with target_index = i and bearing = 0xFFFF. The optional fields and descriptor SHOULD be omitted (i.e. the frame ends after bearing) when clearing, but the Host MUST tolerate a clear frame whose declared optional fields and descriptor are present and ignore their values.
Refreshing a target. The Device sends TARGET_UPDATE for the same target_index as often as it has new data; each update replaces the previous one for that index.
The Device runs the following minimal state machine.
| Value | Name | Description |
|---|---|---|
0x01 |
WAITING_CONFIG | CAPABILITY has been sent; awaiting CONFIG. |
0x02 |
ACTIVE | CONFIG accepted; TARGET_UPDATE streaming. |
0xFF |
FAULT | CONFIG rejected, malformed traffic, or core-layer fault. Terminal in the current version. |
stateDiagram-v2
[*] --> WAITING_CONFIG: Class active (Core ACK_OK)
WAITING_CONFIG --> ACTIVE: CONFIG accepted, ACK ACCEPTED
WAITING_CONFIG --> FAULT: CONFIG REJECT or timeout
ACTIVE --> FAULT: Core watchdog or link loss
FAULT --> [*]
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) tells it everything else it needs.
- CAPABILITY. Emitted within 100 ms of the class becoming active.
- CONFIG. The Host SHOULD emit CONFIG within 500 ms of receiving CAPABILITY. A Device that has not received CONFIG within 5 s transitions to FAULT.
- ACK. The Device MUST emit ACK within 200 ms of receiving CONFIG.
- TARGET_UPDATE. Streaming begins immediately after the Device sends
ACCEPTED. Cadence is payload-defined — typical wayfinding payloads emit updates at 5–25 Hz per target. The Device's TARGET_UPDATE traffic satisfies the APEX — Core §3.5 1 Hz transmit floor; the Host's HOST_STATE broadcast satisfies the same on the Host side. A Device with currently no live targets MUST still send at least one frame per second — the implicit Core heartbeat is sufficient.
Bearings are whole degrees. This matches the on-display resolution of typical arrow renderers and is plenty for point me there cues. A payload whose internal bearing is more precise rounds before sending. A future revision of this class may add a higher-precision mode if real use cases demand it; the current version does not.
max_targets is capped at 16. Real wayfinding displays render only a handful of targets legibly; sixteen is a generous ceiling that bounds the worst-case CAPABILITY and TARGET_UPDATE frame size.
The Host cannot tell the Device which target the operator is currently following, nor ask the Device to refresh a specific target. The current version is strictly one-way (Device → Host) after the CONFIG handshake. A future revision may add a feedback channel for use cases where the payload could benefit from operator attention.
A wayfinding payload that streams up to 3 targets with a proximity measurement (reported in meters, lower-is-closer) and descriptors up to 7 bytes long.
max_targets = 3optional_fields = 0b00000010(PROXIMITY only)descriptor_max_bytes = 7proximity_polarity = 0(LOWER_IS_CLOSER)proximity_unit = "m"- Has completed Core discovery and been assigned
device_id = 0x01.
Frames below use the common notation defined in Core §3.1.5: each is shown as the bytes before CRC and COBS. PV = 00, TT = 03, ID = 01 throughout.
sequenceDiagram
participant H as Host
participant D as Device
Note over H,D: Core discovery complete, traffic_type 3 active
D->>H: Step 1 - CAPABILITY (3 targets, PROXIMITY in m, descr up to 7)
H->>D: Step 2 - CONFIG
D->>H: Step 3 - ACK (ACCEPTED)
Note over D: ACTIVE. Streaming begins.
D->>H: Step 4 - TARGET_UPDATE 1: 045 deg, 350 m, "WAYPT-1"
D->>H: Step 5 - TARGET_UPDATE 2: 270 deg, 120 m, "WAYPT-2"
D->>H: Step 6 - TARGET_UPDATE 1 cleared
Inner payload 14 bytes (LN = 0E).
00 03 01 0E 01 00 03 02 07 00 6D 00 00 00 00 00 00 00
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 0–3 | 00 03 01 0E |
outer header | LN=0E (14) |
| 4 | 01 |
class_msg_id |
1 (CAPABILITY) |
| 5 | 00 |
class_spec_version |
0 |
| 6 | 03 |
max_targets |
3 |
| 7 | 02 |
optional_fields |
0b00000010 (PROXIMITY) |
| 8 | 07 |
descriptor_max_bytes |
7 |
| 9 | 00 |
proximity_polarity |
0 (LOWER_IS_CLOSER) |
| 10–17 | 6D 00 00 00 00 00 00 00 |
proximity_unit |
"m" (NUL-padded to 8 bytes) |
Inner payload 1 byte (LN = 01).
00 03 01 01 02
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 4 | 02 |
class_msg_id |
2 (CONFIG) |
Inner payload 2 bytes (LN = 02).
00 03 01 02 03 00
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 4 | 03 |
class_msg_id |
3 (ACK) |
| 5 | 00 |
result |
ACCEPTED |
The Device transitions WAITING_CONFIG → ACTIVE and begins streaming.
Inner payload 14 bytes (LN = 0E).
00 03 01 0E 04 01 2D 00 5E 01 07 57 41 59 50 54 2D 31
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 4 | 04 |
class_msg_id |
4 (TARGET_UPDATE) |
| 5 | 01 |
target_index |
1 |
| 6–7 | 2D 00 |
bearing |
45° (little-endian) |
| 8–9 | 5E 01 |
PROXIMITY |
350 (little-endian, i16; in proximity_unit, i.e. meters) |
| 10 | 07 |
descriptor_len |
7 |
| 11–17 | 57 41 59 50 54 2D 31 |
descriptor |
"WAYPT-1" |
Inner payload 14 bytes (LN = 0E).
00 03 01 0E 04 02 0E 01 78 00 07 57 41 59 50 54 2D 32
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 4 | 04 |
class_msg_id |
4 (TARGET_UPDATE) |
| 5 | 02 |
target_index |
2 |
| 6–7 | 0E 01 |
bearing |
270° (little-endian) |
| 8–9 | 78 00 |
PROXIMITY |
120 (little-endian, i16; in proximity_unit, i.e. meters) |
| 10 | 07 |
descriptor_len |
7 |
| 11–17 | 57 41 59 50 54 2D 32 |
descriptor |
"WAYPT-2" |
The Device clears target index 1 by sending bearing = 0xFFFF; the optional fields and descriptor are omitted. Inner payload 4 bytes (LN = 04).
00 03 01 04 04 01 FF FF
| Byte(s) | Hex | Field | Value |
|---|---|---|---|
| 4 | 04 |
class_msg_id |
4 (TARGET_UPDATE) |
| 5 | 01 |
target_index |
1 |
| 6–7 | FF FF |
bearing |
0xFFFF (clear) |
The Host removes the arrow for target 1 from the display. Target 2 continues to be streamed.