Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/companion_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,19 @@ Byte 0: 0x0A
- `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
- `PACKET_CHANNEL_DATA_RECV` (0x1B) for channel data datagrams
- `PACKET_CONTACT_MSG_SENT_V3` (0x1E) or `PACKET_CHANNEL_MSG_SENT_V3` (0x1F) for messages the device itself sent
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available

**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.

**Important**: a host must advance its polling loop — issue the next
`CMD_SYNC_NEXT_MESSAGE`, exactly as it would for a known type — on *any*
response to this command, including packet types it does not recognize. New
message-carrying packet types are added over time, and a host that only
advances on the types it knows will stall on the first unknown one: the queue
stops draining and the messages behind it stay undelivered until the next
`PACKET_MESSAGES_WAITING`. Only `PACKET_NO_MORE_MSGS` (0x0A) ends the loop.

---

### 8. Get Battery and Storage
Expand Down Expand Up @@ -619,6 +628,45 @@ def parse_channel_message(data):
}
```

### Device-Sent Messages

A message the device *sent* on its own (bot auto-reply, on-device keyboard
input, etc.) rather than via a host-issued `SEND_TXT_MESSAGE` /
`SEND_CHANNEL_MESSAGE`. Mainline firmware never emits these — they're for a
custom firmware that self-originates messages, delivered through the same
offline-queue / `PACKET_MESSAGES_WAITING` / `SYNC_NEXT_MESSAGE` pipe as any
other message.

Same byte layout as the RECV_V3 counterpart, except the pubkey field is the
*recipient* (not sender), and the RECV_V3 SNR byte is reserved (no SNR for a
local send). Requires `app_target_ver >= 3` — no pre-V3 shape exists.

Text Type is limited to `TXT_TYPE_PLAIN` (0) or `TXT_TYPE_CLI_DATA` (1).
`TXT_TYPE_SIGNED_PLAIN` is not emitted: it would need the 4-byte sender prefix
that RECV_V3 carries, which is meaningless when the device is the sender.

**Contact Message Sent** (`PACKET_CONTACT_MSG_SENT_V3`, 0x1E):
```
Byte 0: 0x1E (packet type)
Bytes 1-3: Reserved
Bytes 4-9: Recipient Public Key Prefix (6 bytes, hex)
Byte 10: Path Length (always 0xFF — meaningless for a local send)
Byte 11: Text Type
Bytes 12-15: Timestamp (32-bit little-endian)
Bytes 16+: Message Text (UTF-8)
```

**Channel Message Sent** (`PACKET_CHANNEL_MSG_SENT_V3`, 0x1F):
```
Byte 0: 0x1F (packet type)
Bytes 1-3: Reserved
Byte 4: Channel Index (0-7)
Byte 5: Path Length (always 0xFF — meaningless for a local send)
Byte 6: Text Type (always 0 / TXT_TYPE_PLAIN — a channel carries no CLI-data/signed-plain concept)
Bytes 7-10: Timestamp (32-bit little-endian)
Bytes 11+: Message Text (UTF-8)
```

### Sending Messages

Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
Expand Down Expand Up @@ -662,6 +710,8 @@ Byte values are authoritative; names are aliases. When reading firmware source,
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
| 0x12 | PACKET_CHANNEL_INFO | Channel information |
| 0x1B | PACKET_CHANNEL_DATA_RECV | Channel data datagram |
| 0x1E | PACKET_CONTACT_MSG_SENT_V3 | Contact message the device itself sent (see [Device-Sent Messages](#device-sent-messages)) |
| 0x1F | PACKET_CHANNEL_MSG_SENT_V3 | Channel message the device itself sent (see [Device-Sent Messages](#device-sent-messages)) |
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
| 0x82 | PACKET_ACK | Acknowledgment |
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
Expand Down
66 changes: 65 additions & 1 deletion examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
#define RESP_CODE_CHANNEL_DATA_RECV 27
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND
#define RESP_CODE_CONTACT_MSG_SENT_V3 30 // a reply to CMD_SYNC_NEXT_MESSAGE -- device-originated msg, see queueSentMessage()
#define RESP_CODE_CHANNEL_MSG_SENT_V3 31 // a reply to CMD_SYNC_NEXT_MESSAGE -- device-originated msg, see queueSentChannelMessage()

#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)

Expand Down Expand Up @@ -222,7 +224,7 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, co

bool MyMesh::Frame::isChannelMsg() const {
return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3 ||
buf[0] == RESP_CODE_CHANNEL_DATA_RECV;
buf[0] == RESP_CODE_CHANNEL_DATA_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_SENT_V3;
}

void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
Expand Down Expand Up @@ -532,6 +534,68 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
#endif
}

// Tells the connected app this device sent `text` to `to` on its own (bot
// reply, on-device keyboard, etc.) -- mainline never calls this. No pre-V3
// fallback, so a non-V3 app just doesn't get it.
void MyMesh::queueSentMessage(const ContactInfo &to, uint8_t txt_type, uint32_t timestamp, const char *text) {
if (app_target_ver < 3) return;
if (txt_type == TXT_TYPE_SIGNED_PLAIN) return; // has no sender prefix field, and we ARE the sender

int i = 0;
out_frame[i++] = RESP_CODE_CONTACT_MSG_SENT_V3;
out_frame[i++] = 0; // reserved1 (RECV_V3's SNR byte)
out_frame[i++] = 0; // reserved2
out_frame[i++] = 0; // reserved3
memcpy(&out_frame[i], to.id.pub_key, 6); // recipient, not sender
i += 6;
out_frame[i++] = 0xFF; // path_len n/a
out_frame[i++] = txt_type;
memcpy(&out_frame[i], &timestamp, 4);
i += 4;
int tlen = strlen(text); // TODO: UTF-8 ??
if (i + tlen > MAX_FRAME_SIZE) {
tlen = MAX_FRAME_SIZE - i;
}
memcpy(&out_frame[i], text, tlen);
i += tlen;
addToOfflineQueue(out_frame, i);

if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
}
}

// Channel counterpart to queueSentMessage().
void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char *text) {
if (app_target_ver < 3) return;

int i = 0;
out_frame[i++] = RESP_CODE_CHANNEL_MSG_SENT_V3;
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
out_frame[i++] = 0; // reserved3
out_frame[i++] = channel_idx;
out_frame[i++] = 0xFF; // path_len n/a
out_frame[i++] = TXT_TYPE_PLAIN;
memcpy(&out_frame[i], &timestamp, 4);
i += 4;
int tlen = strlen(text); // TODO: UTF-8 ??
if (i + tlen > MAX_FRAME_SIZE) {
tlen = MAX_FRAME_SIZE - i;
}
memcpy(&out_frame[i], text, tlen);
i += tlen;
addToOfflineQueue(out_frame, i);

if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
}
}

bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
Expand Down
5 changes: 5 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
_prefs.clearDirty();
}

// For a custom firmware's own self-originated sends (bot replies, on-device
// keyboard, etc.) -- mainline doesn't call these. See MyMesh.cpp.
void queueSentMessage(const ContactInfo &to, uint8_t txt_type, uint32_t timestamp, const char *text);
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char *text);

#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
Expand Down