diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 9d45b59ef..4325ecd50 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -257,31 +257,60 @@ Bytes 34-65: Secret (32 bytes) --- -### 5. Send Channel Message +### 5. Send Channel Text Message -**Purpose**: Send a text message to a channel. +**Purpose**: Send a plain text message to a channel. **Command Format**: ``` Byte 0: 0x03 -Byte 1: 0x00 +Byte 1: Text Type Byte 2: Channel Index (0-7) Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds) -Bytes 7+: Message Text (UTF-8, variable length) +Bytes 7+: UTF-8 text bytes (variable length) ``` **Timestamp**: Unix timestamp in seconds (32-bit unsigned integer, little-endian) +**Text Type**: +- Must be `0x00` (`TXT_TYPE_PLAIN`) for this command. + **Example** (send "Hello" to channel 1 at timestamp 1234567890): ``` 03 00 01 D2 02 96 49 48 65 6C 6C 6F ``` -**Response**: `PACKET_MSG_SENT` (0x06) on success +**Response**: `PACKET_OK` (0x00) on success + +--- + +### 6. Send Channel Data Datagram + +**Purpose**: Send binary datagram data to a channel. + +**Command Format**: +``` +Byte 0: 0x3D +Byte 1: Data Type (`data_type`) +Byte 2: Channel Index (0-7) +Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds) +Bytes 7+: Binary payload bytes (variable length) +``` + +**Data Type / Transport Mapping**: +- `0xFF` (`DATA_TYPE_CUSTOM`) must be used for custom-protocol binary datagrams. +- `0x00` (`TXT_TYPE_PLAIN`) is invalid for this command. +- Values other than `0xFF` are reserved for official protocol extensions. + +**Limits**: +- Maximum payload length is `163` bytes (`MAX_GROUP_DATA_LENGTH`). +- Larger payloads are rejected with `PACKET_ERROR` / `ERR_CODE_ILLEGAL_ARG`. + +**Response**: `PACKET_OK` (0x00) on success --- -### 6. Get Message +### 7. Get Message **Purpose**: Request the next queued message from the device. @@ -297,6 +326,7 @@ Byte 0: 0x0A **Response**: - `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages +- `PACKET_CHANNEL_DATA_RECV` (0x1B) or `PACKET_CHANNEL_DATA_RECV_V3` (0x1C) for channel data - `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages - `PACKET_NO_MORE_MSGS` (0x0A) if no messages available @@ -304,7 +334,7 @@ Byte 0: 0x0A --- -### 7. Get Battery +### 8. Get Battery **Purpose**: Query device battery level. @@ -366,11 +396,15 @@ Messages are received via the RX characteristic (notifications). The device send - `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format - `PACKET_CHANNEL_MSG_RECV_V3` (0x11) - Version 3 with SNR -2. **Contact Messages**: +2. **Channel Data**: + - `PACKET_CHANNEL_DATA_RECV` (0x1B) - Standard format + - `PACKET_CHANNEL_DATA_RECV_V3` (0x1C) - Version 3 with SNR + +3. **Contact Messages**: - `PACKET_CONTACT_MSG_RECV` (0x07) - Standard format - `PACKET_CONTACT_MSG_RECV_V3` (0x10) - Version 3 with SNR -3. **Notifications**: +4. **Notifications**: - `PACKET_MESSAGES_WAITING` (0x83) - Indicates messages are queued ### Contact Message Format @@ -446,7 +480,7 @@ Byte 1: Channel Index (0-7) Byte 2: Path Length Byte 3: Text Type Bytes 4-7: Timestamp (32-bit little-endian) -Bytes 8+: Message Text (UTF-8) +Bytes 8+: Payload bytes ``` **V3 Format** (`PACKET_CHANNEL_MSG_RECV_V3`, 0x11): @@ -458,38 +492,74 @@ Byte 4: Channel Index (0-7) Byte 5: Path Length Byte 6: Text Type Bytes 7-10: Timestamp (32-bit little-endian) -Bytes 11+: Message Text (UTF-8) +Bytes 11+: Payload bytes +``` + +**Payload Meaning**: +- If `txt_type == 0x00`: payload is UTF-8 channel text. +- If `txt_type != 0x00`: payload is binary (for example image/voice fragments) and must be treated as raw bytes. + For custom app datagrams sent via `CMD_SEND_CHANNEL_DATA`, `data_type` must be `0xFF`. + +### Channel Data Format + +**Standard Format** (`PACKET_CHANNEL_DATA_RECV`, 0x1B): +``` +Byte 0: 0x1B (packet type) +Byte 1: Channel Index (0-7) +Byte 2: Path Length +Byte 3: Data Type +Bytes 4-7: Timestamp (32-bit little-endian) +Bytes 8+: Payload bytes +``` + +**V3 Format** (`PACKET_CHANNEL_DATA_RECV_V3`, 0x1C): +``` +Byte 0: 0x1C (packet type) +Byte 1: SNR (signed byte, multiplied by 4) +Bytes 2-3: Reserved +Byte 4: Channel Index (0-7) +Byte 5: Path Length +Byte 6: Data Type +Bytes 7-10: Timestamp (32-bit little-endian) +Bytes 11+: Payload bytes ``` **Parsing Pseudocode**: ```python -def parse_channel_message(data): +def parse_channel_frame(data): packet_type = data[0] offset = 1 # Check for V3 format - if packet_type == 0x11: # V3 + if packet_type in (0x11, 0x1C): # V3 snr_byte = data[offset] snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0) offset += 3 # Skip SNR + reserved channel_idx = data[offset] path_len = data[offset + 1] - txt_type = data[offset + 2] + item_type = data[offset + 2] timestamp = int.from_bytes(data[offset+3:offset+7], 'little') - message = data[offset+7:].decode('utf-8') + payload = data[offset+7:] + is_text = packet_type in (0x08, 0x11) + if is_text and item_type == 0: + message = payload.decode('utf-8') + else: + message = None return { 'channel_idx': channel_idx, + 'item_type': item_type, 'timestamp': timestamp, + 'payload': payload, 'message': message, - 'snr': snr if packet_type == 0x11 else None + 'snr': snr if packet_type in (0x11, 0x1C) else None } ``` ### Sending Messages -Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)). +Use `CMD_SEND_CHANNEL_TXT_MSG` for plain text, and `CMD_SEND_CHANNEL_DATA` for binary datagrams (see [Commands](#commands)). **Important**: - Messages are limited to 133 characters per MeshCore specification @@ -510,7 +580,7 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)). | 0x03 | PACKET_CONTACT | Contact information | | 0x04 | PACKET_CONTACT_END | End of contact list | | 0x05 | PACKET_SELF_INFO | Device self-information | -| 0x06 | PACKET_MSG_SENT | Message sent confirmation | +| 0x06 | PACKET_MSG_SENT | Direct message sent confirmation | | 0x07 | PACKET_CONTACT_MSG_RECV | Contact message (standard) | | 0x08 | PACKET_CHANNEL_MSG_RECV | Channel message (standard) | | 0x09 | PACKET_CURRENT_TIME | Current time response | @@ -520,6 +590,8 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)). | 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) | | 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) | | 0x12 | PACKET_CHANNEL_INFO | Channel information | +| 0x1B | PACKET_CHANNEL_DATA_RECV | Channel data (standard) | +| 0x1C | PACKET_CHANNEL_DATA_RECV_V3| Channel data (V3 with SNR) | | 0x80 | PACKET_ADVERTISEMENT | Advertisement packet | | 0x82 | PACKET_ACK | Acknowledgment | | 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification | @@ -677,7 +749,7 @@ def parse_self_info(data): return info ``` -**PACKET_MSG_SENT** (0x06): +**PACKET_MSG_SENT** (0x06, used by direct/contact send flows): ``` Byte 0: 0x06 Byte 1: Message Type @@ -796,8 +868,9 @@ def on_notification_received(data): - `DEVICE_QUERY` → `PACKET_DEVICE_INFO` - `GET_CHANNEL` → `PACKET_CHANNEL_INFO` - `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR` - - `SEND_CHANNEL_MESSAGE` → `PACKET_MSG_SENT` - - `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS` + - `CMD_SEND_CHANNEL_TXT_MSG` → `PACKET_OK` or `PACKET_ERROR` + - `CMD_SEND_CHANNEL_DATA` → `PACKET_OK` or `PACKET_ERROR` + - `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CHANNEL_DATA_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS` - `GET_BATTERY` → `PACKET_BATTERY` 4. **Timeout Handling**: @@ -873,7 +946,7 @@ command = build_channel_message(channel_index, message, timestamp) # 2. Send command send_command(tx_char, command) -response = wait_for_response(PACKET_MSG_SENT) +response = wait_for_response(PACKET_OK) ``` ### Receiving Messages @@ -882,8 +955,9 @@ response = wait_for_response(PACKET_MSG_SENT) def on_notification_received(data): packet_type = data[0] - if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3: - message = parse_channel_message(data) + if packet_type in (PACKET_CHANNEL_MSG_RECV, PACKET_CHANNEL_MSG_RECV_V3, + PACKET_CHANNEL_DATA_RECV, PACKET_CHANNEL_DATA_RECV_V3): + message = parse_channel_frame(data) handle_channel_message(message) elif packet_type == PACKET_MESSAGES_WAITING: # Poll for messages diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1f71a9bc6..490f34a13 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -58,6 +58,7 @@ #define CMD_GET_AUTOADD_CONFIG 59 #define CMD_GET_ALLOWED_REPEAT_FREQ 60 #define CMD_SET_PATH_HASH_MODE 61 +#define CMD_SEND_CHANNEL_DATA 62 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -91,6 +92,8 @@ #define RESP_CODE_STATS 24 // v8+, second byte is stats type #define RESP_CODE_AUTOADD_CONFIG 25 #define RESP_ALLOWED_REPEAT_FREQ 26 +#define RESP_CODE_CHANNEL_DATA_RECV 27 +#define RESP_CODE_CHANNEL_DATA_RECV_V3 28 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -204,7 +207,8 @@ 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; + 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_V3; } void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) { @@ -564,6 +568,44 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe #endif } +void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, uint8_t data_type, + const uint8_t *data, size_t data_len) { + int i = 0; + if (app_target_ver >= 3) { + out_frame[i++] = RESP_CODE_CHANNEL_DATA_RECV_V3; + out_frame[i++] = (int8_t)(pkt->getSNR() * 4); + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + } else { + out_frame[i++] = RESP_CODE_CHANNEL_DATA_RECV; + } + + uint8_t channel_idx = findChannelIdx(channel); + out_frame[i++] = channel_idx; + out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + out_frame[i++] = data_type; + memcpy(&out_frame[i], ×tamp, 4); + i += 4; + + size_t available = MAX_FRAME_SIZE - i; + if (data_len > available) { + MESH_DEBUG_PRINTLN("onChannelDataRecv(): payload_len=%d exceeds frame space=%d, truncating", (uint32_t)data_len, (uint32_t)available); + data_len = available; + } + int copy_len = (int)data_len; + if (copy_len > 0) { + memcpy(&out_frame[i], data, copy_len); + i += copy_len; + } + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrame(frame, 1); + } +} + uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { @@ -1031,7 +1073,7 @@ void MyMesh::handleCmdFrame(size_t len) { ? ERR_CODE_NOT_FOUND : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsuported TXT_TYPE_* } - } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg int i = 1; uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN uint8_t channel_idx = cmd_frame[i++]; @@ -1051,6 +1093,29 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } } + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_DATA) { // send GroupChannel datagram + int i = 1; + uint8_t data_type = cmd_frame[i++]; + uint8_t channel_idx = cmd_frame[i++]; + uint32_t msg_timestamp; + memcpy(&msg_timestamp, &cmd_frame[i], 4); + i += 4; + const uint8_t *payload = &cmd_frame[i]; + int payload_len = (len > (size_t)i) ? (int)(len - i) : 0; + + ChannelDetails channel; + if (!getChannel(channel_idx, channel)) { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } else if (data_type != DATA_TYPE_CUSTOM) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (sendGroupData(msg_timestamp, channel.channel, data_type, payload, payload_len)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list if (_iter_started) { writeErrFrame(ERR_CODE_BAD_STATE); // iterator is currently busy diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 4d77b5ab7..78ea6414e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -137,6 +137,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { const uint8_t *sender_prefix, const char *text) override; void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) override; + void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, uint8_t data_type, + const uint8_t *data, size_t data_len) override; uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) override; diff --git a/src/MeshCore.h b/src/MeshCore.h index 70cd0f067..3eb4f9354 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -17,6 +17,7 @@ #define PATH_HASH_SIZE 1 #define MAX_PACKET_PAYLOAD 184 +#define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 5) #define MAX_PATH_SIZE 64 #define MAX_TRANS_UNIT 255 @@ -100,4 +101,4 @@ class RTCClock { } }; -} \ No newline at end of file +} diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 33d7edbee..d8e089d54 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -353,8 +353,18 @@ int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel d #endif void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) { - uint8_t txt_type = data[4]; - if (type == PAYLOAD_TYPE_GRP_TXT && len > 5 && (txt_type >> 2) == 0) { // 0 = plain text msg + if (len < 5) { + MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping short group payload len=%d", (uint32_t)len); + return; + } + + uint8_t data_type = data[4]; + if (type == PAYLOAD_TYPE_GRP_TXT) { + if ((data_type >> 2) != 0) { + MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping unsupported group text type=%d", (uint32_t)data_type); + return; + } + uint32_t timestamp; memcpy(×tamp, data, 4); @@ -363,6 +373,10 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes // notify UI of this new message onChannelMessageRecv(channel, packet, timestamp, (const char *) &data[5]); // let UI know + } else if (type == PAYLOAD_TYPE_GRP_DATA) { + uint32_t timestamp; + memcpy(×tamp, data, 4); + onChannelDataRecv(channel, packet, timestamp, data_type, &data[5], len - 5); } } @@ -454,6 +468,30 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan return false; } +bool BaseChatMesh::sendGroupData(uint32_t timestamp, mesh::GroupChannel& channel, uint8_t data_type, const uint8_t* data, int data_len) { + if (data_len < 0) { + MESH_DEBUG_PRINTLN("sendGroupData: invalid negative data_len=%d", data_len); + return false; + } + if (data_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("sendGroupData: data_len=%d exceeds max=%d", data_len, MAX_GROUP_DATA_LENGTH); + return false; + } + + uint8_t temp[5 + MAX_GROUP_DATA_LENGTH]; + memcpy(temp, ×tamp, 4); + temp[4] = data_type; + if (data_len > 0) memcpy(&temp[5], data, data_len); + + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_DATA, channel, temp, 5 + data_len); + if (pkt == NULL) { + MESH_DEBUG_PRINTLN("sendGroupData: unable to create group datagram, data_len=%d", data_len); + return false; + } + sendFloodScoped(channel, pkt); + return true; +} + bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) { int plen = getBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, temp_buf); // retrieve last raw advert packet if (plen == 0) return false; // not found diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index ab90d581b..12fcb9571 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -111,6 +111,8 @@ class BaseChatMesh : public mesh::Mesh { virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; virtual void onSendTimeout() = 0; virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0; + virtual void onChannelDataRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, uint8_t data_type, + const uint8_t* data, size_t data_len) {} virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0; virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0; virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len); @@ -148,6 +150,7 @@ class BaseChatMesh : public mesh::Mesh { int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout); int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); + bool sendGroupData(uint32_t timestamp, mesh::GroupChannel& channel, uint8_t data_type, const uint8_t* data, int data_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout); int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout); diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index 6ab84d397..a853a64db 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -6,6 +6,7 @@ #define TXT_TYPE_PLAIN 0 // a plain text message #define TXT_TYPE_CLI_DATA 1 // a CLI command #define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender +#define DATA_TYPE_CUSTOM 0xFF // custom app binary payload (group/channel datagrams) class StrHelper { public: