Skip to content

DataX channels

Marco Ceccon edited this page Oct 12, 2026 · 2 revisions

DataX is the message layer inside an AirShield frame. It carries service traffic over numbered channels, and one frame can hold several messages back to back. This page covers the header, tags, opening frames and continuations, fragments, channel ids, the channel registry rules, status frames and the error codes on a channel.

Where DataX sits

Each AirShield frame carries plaintext that DataX splits into messages. Every message has a 4-byte fixed header, optional extension tags and a payload. The AirShield layer is described in AirShield-frames-and-ciphers.

Fixed header and extension tags

The fixed header is 4 bytes. The extension tags follow it, and each tag is 4 bytes.

Byte Bits Meaning
0 7 hasExtension. Set when at least one tag follows.
0 5 to 0 payload_len, high 6 bits
1 7 to 0 payload_len, low 8 bits
2 to 3 15 to 0 channelId, big-endian. Bit 15 is the originator bit.

payload_len is 14 bits, so the largest message is 16383 bytes. It counts the extension tags as well as the payload. The bytes on the wire for one message are 4 + payload_len. The application payload is payload_len - (header_len - 4), where header_len is 4 plus 4 for each tag.

Byte Bits Meaning
0 7 continuation. Set on every tag except the last.
0 6 to 0 Tag id
1 7 to 0 d1. Used only by tag 5 (priority).
2 to 3 15 to 0 Value, big-endian
Tag id Name Value Effect on parse
1 service id service number Sets the service. A value of 0 closes the channel.
2 message type type number Sets the message type.
3 error id status code Marks a status frame. The payload is empty.
4 message type, closed form type number Sets the message type and closes the channel.
5 QoS d1 priority, value experience tag Priority in d1. The meaning of the value is unconfirmed.

The phone writes tags in the order service, type, error, QoS. A message above 16383 bytes does not fit in one header.

Opening frame, continuations and the type cache

A channel's first message carries the service tag (tag 1) and the message type tag (tag 2). Later messages on the same channel may omit both. The glasses omit the type tag when the type has not changed since the last message on that channel.

A receiver reads a message with no tags as service 0 and type 0. It then takes the type from the last type seen on that channel. A status frame keeps its own type, which is absent.

Frame Tags present Parsed as
Opening frame of a service channel service, type Service and type set. The registry binds the channel.
Typed message on a known channel type Service 0. Registry lookup gives the service.
Continuation message none Type taken from the channel cache.
Status frame error Error id set. Payload empty.

Example. A status frame that acknowledges success on channel 0x0002:

80 04 00 02 03 00 10 00

Byte 0 0x80 sets hasExtension. Bytes 1 and 2 to 3 give payload_len 4 and channel 0x0002. The tag 03 00 10 00 is error id 0x1000, which is success.

A message with no tags, 17 bytes of payload, on channel 0x8002:

00 11 80 02

A sender should send a bare continuation when the type is unchanged. Tagging every chunk may starve the peer's decoder during an audio burst. Unconfirmed: the size of that effect. Airship chunks carry the type tag on every message, and the glasses accept them.

Fragments and reassembly across frames

A message can be longer than the rest of the AirShield frame that carries its header. The glasses do this. Airship transfers use 1024-byte messages, and some of them cross a frame boundary. The rest of the message comes at the start of the next frame as raw bytes with no header.

A receiver keeps one fragment slot. A fragment is always finished by the next frame before any other message is read, so one slot is enough.

The example below is a frame of 4096 bytes of plaintext. The message starts at offset 3084 with a 4-byte header that declares a 1024-byte payload.

Part of frame 1 Bytes Notes
Earlier messages 3084 Whole messages
Header 4 payload_len 1024, no tags
Head of the payload 1007 Data
Pad 1 0xC1

Frame 1 is 4096 bytes. The message owes 17 more payload bytes. Those 17 bytes start frame 2.

flowchart LR
  subgraph F1["Frame 1, 4096 bytes"]
    A["Earlier messages<br/>3084 bytes"] --> B["Header<br/>payload_len 1024"]
    B --> C["Head of payload<br/>1007 bytes"]
    C --> D["Pad<br/>1 byte 0xC1"]
  end
  subgraph F2["Frame 2"]
    E["Rest of payload<br/>17 bytes"] --> G["Next message"]
  end
  C -. "held in the fragment slot" .-> E
Loading

The diagram shows one 1024-byte message split across two AirShield frames. The tail of frame 1 is padding. Frame 2 starts with the 17 bytes that finish the message.

The walker works in this order.

  1. Read the header at the current offset. Compute wire_total = 4 + payload_len.
  2. If the rest of the frame holds wire_total bytes, take the message and move on.
  3. If not, strip the pad from the end of the frame and count what is left. If at least 4 bytes are left and fewer than wire_total, the message is a fragment. Hold the head of its payload and the number of bytes still owed.
  4. In the next frame, strip the pad, take the owed bytes from the front, and deliver the message when all bytes have arrived.
  5. Data is padded out to a 16-byte boundary. The pad is N copies of the byte N + 0xC0, with N from 1 to 16. All N bytes must match.

Trap. The pad must come off before a fragment's length is counted. A whole message does not care, because the walker stops before the pad. A fragment does. If the pad counts as data, the message comes out the right length with the wrong bytes at the end. The result is a file that fails its checksum.

A phone does not split an outbound message across frames. An outbound message above 16383 bytes cannot be sent.

Channel ids and the originator bit

A channel id is 16 bits. Bit 15 (0x8000) is the originator bit. The side that opens a channel sets it on every frame it sends on that channel. The other side answers on the same channel with the bit clear. The answer uses the mirror id, which is the same id with bit 15 cleared.

Channel id Meaning
0x8001 Link setup channel. RequestEncryption and EndLinkSetup use it.
0x0001 Mirror of 0x8001. The glasses' EnableEncryption and the EndLinkSetup acknowledgement use it.
0x8002 and up Service channels opened by the phone
0x0002 and up Replies on the phone's service channels

The phone allocates its service channels from a counter that starts at 2. Channel 0x8001 is taken by link setup. A counter that started at 1 would hand that id to the first service channel and break the link.

Each side allocates ids for its own channels. The glasses allocate theirs in the same range. Both sides can therefore use the same low number. The originator bit tells the two apart. A frame with bit 15 set is on a channel the peer opened. A frame with bit 15 clear is a reply on a channel this side opened.

The glasses can open a channel with the same low number as a phone channel, and both channels carry traffic. See the sequence below.

Channel registry rules

The registry answers one question: which service is this channel talking to, right now. A phone follows these rules.

  1. Keep two maps, both keyed on the low 15 bits of the channel id. local holds channels this side opened. peer holds channels the glasses opened.
  2. An inbound frame with bit 15 set goes to peer. An inbound frame with bit 15 clear goes to local.
  3. Allocate each new local id with bit 15 set, and bind it to its service at once.
  4. When a frame from the glasses names a service, record that service for the channel in peer. The glasses name the service on the first message of a channel only. Later frames read service 0 and need this lookup.
  5. For an inbound channel, return the bound service, or 0 when nothing has named one.
  6. A frame with isClosed removes the peer binding for its channel.

A plain counter works. It does not need to move above the channels the glasses use, because the originator bit keeps the two sides apart. Each connection starts again with empty maps and a counter at 2. Ids are not reused within one connection. Unconfirmed: whether the glasses expect a close frame for a channel the phone opened.

The phone keeps one channel per service for each connection. The glasses answer on every open channel of a service. One channel per request would multiply every reply.

The sequence below shows two channels with the same low number, one from each side.

sequenceDiagram
  participant Phone
  participant Glasses
  Phone->>Glasses: 0x8002 opens svc28 with type tag
  Glasses->>Phone: 0x0002 reply on the phone channel
  Glasses->>Phone: 0x8002 opens svc20 with type tag
  Phone->>Glasses: 0x0002 status success on the glasses channel
  Phone->>Glasses: 0x8002 message with no tags stays on svc28
  Glasses->>Phone: 0x0002 reply to the no-tag message
Loading

The sequence shows two channels that both use low number 2. The phone opened 0x8002 for svc28. The glasses opened their own 0x8002 for svc20. Each side keeps the two in separate maps, so the frames do not mix. The phone answers the glasses' channel on its mirror 0x0002.

Status frames

A status frame is a bare header with an error tag and no payload. It goes on the mirror of the channel it answers. Success is 0x1000. The example in the earlier section is the success frame on channel 0x0002.

Error codes on a channel

These codes arrive as the error tag of a status frame. The names are as used in Meta's DataX library.

Code Name Meaning
0x1000 SUCCESS The peer accepted the message. Trust results use the same value.
0xC001 service_not_found The service is not registered on this stream. On a link without a scope, every service except 30, 36 and 79 answers this.
0xC004 Bad or oversized data.
0xD001 unknown_type The service exists, but it does not handle that message type.
0xD004 The message type is wrong for a request, for example a response type.
0xD010 Bad request. The protobuf body failed to parse.
0xE001 write_would_block The peer could not take the write.

Where these codes appear on the wire:

  • The phone answers pushes from the glasses on services 30 and 79 with 0xC001.
  • 0xD001 does not mean one thing everywhere. On service 31, the settings service, the glasses return it for two cases. One is an unknown message type. The other is a type-2 body that fails to parse.
  • 0xD010 shows up on a Wi-Fi join. The glasses reject a JoinWifiDirectGroup message with 0xD010 when field 4 is missing, instead of answering with a status. See Wi-Fi-Direct.

Pushes from the glasses after link setup

After link setup, the glasses open channels of their own, for example svc20. Some pushes need a reply from the phone.

Push from the glasses Phone reply
svc36 type 4096 or 4097 on their own channel Success status, as trust. See Identity-and-trust.
svc30 (device-peer trust) 0xC001
svc79 (constellation auth) 0xC001. The phone sends no svc79 traffic of its own.
Any other service 0xC001 unless the phone handles the service. Unconfirmed: what the glasses do with an unanswered push.

The glasses also push a type 4096 message of about 551 bytes on their own channel after a change of owner. Its size suggests a certificate. Unconfirmed: its content. See Identity-and-trust.

After the trust exchange, the phone waits for the glasses' EndLinkSetup on channel 0x8001. It answers with success on 0x0001. The phone does not need a fixed settle delay after that. Unconfirmed: whether the glasses need one.

Reference code: Header.kt, DataXConnection.kt, ChannelRegistry.kt

Open questions

  • What the glasses do with a push that gets no answer is not confirmed.
  • Whether tagging the type on every message starves the glasses' decoder in an audio burst is not confirmed.
  • Whether the glasses need the phone's channel ids to skip the ids they use is not confirmed.
  • 0xD001 has more than one meaning, and its meaning depends on the service. How to tell them apart on a given channel is not known.
  • The content of the 551-byte type 4096 push from the glasses is unknown.
  • No full table of error codes per service is known.

Clone this wiki locally