Kaynağa Gözat

feat: Add support for PAYLOAD_TYPE_GRP_DATA

Docs changes are to reflect how it is currently in fw

This adds ability to send datagram data to everyone in channel
Janez T 5 ay önce
ebeveyn
işleme
9b84278607

+ 52 - 15
docs/companion_protocol.md

@@ -257,31 +257,56 @@ Bytes 34-49: Secret (16 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: 0x3E
+Byte 1: Data Type (`txt_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` (`TXT_TYPE_CUSTOM_BINARY`) is the custom-app binary type.
+- `0x00` (`TXT_TYPE_PLAIN`) is invalid for this command.
+- Values other than `0xFF` are reserved for official protocol extensions.
+
+**Response**: `PACKET_OK` (0x00) on success
 
 ---
 
-### 6. Get Message
+### 7. Get Message
 
 **Purpose**: Request the next queued message from the device.
 
@@ -304,7 +329,7 @@ Byte 0: 0x0A
 
 ---
 
-### 7. Get Battery and Storage
+### 8. Get Battery and Storage
 
 **Purpose**: Query device battery voltage and storage usage.
 
@@ -446,7 +471,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,9 +483,14 @@ 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`, use `txt_type == 0xFF`.
+
 **Parsing Pseudocode**:
 ```python
 def parse_channel_message(data):
@@ -477,11 +507,17 @@ def parse_channel_message(data):
     path_len = data[offset + 1]
     txt_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:]
+    if txt_type == 0:
+        message = payload.decode('utf-8')
+    else:
+        message = None
     
     return {
         'channel_idx': channel_idx,
+        'txt_type': txt_type,
         'timestamp': timestamp,
+        'payload': payload,
         'message': message,
         'snr': snr if packet_type == 0x11 else None
     }
@@ -489,7 +525,7 @@ def parse_channel_message(data):
 
 ### 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 +546,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         |
@@ -675,7 +711,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: Route Flag (0 = direct, 1 = flood)
@@ -737,7 +773,8 @@ BLE implementations enqueue and deliver one protocol frame per BLE write/notific
      - `DEVICE_QUERY` → `PACKET_DEVICE_INFO`
      - `GET_CHANNEL` → `PACKET_CHANNEL_INFO`
      - `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR`
-     - `SEND_CHANNEL_MESSAGE` → `PACKET_MSG_SENT`
+     - `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_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS`
      - `GET_BATTERY` → `PACKET_BATTERY`
 
@@ -809,7 +846,7 @@ command = build_channel_message(channel_index, message, timestamp)
 
 # 2. Send command
 send_command(rx_char, command)
-response = wait_for_response(PACKET_MSG_SENT)
+response = wait_for_response(PACKET_OK)
 ```
 
 ### Receiving Messages

+ 63 - 5
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
@@ -564,6 +565,41 @@ 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 txt_type,
+                               const uint8_t *data, size_t data_len) {
+  int i = 0;
+  if (app_target_ver >= 3) {
+    out_frame[i++] = RESP_CODE_CHANNEL_MSG_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_MSG_RECV;
+  }
+
+  uint8_t channel_idx = findChannelIdx(channel);
+  out_frame[i++] = channel_idx;
+  out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
+  out_frame[i++] = txt_type;
+  memcpy(&out_frame[i], &timestamp, 4);
+  i += 4;
+
+  size_t available = MAX_FRAME_SIZE - i;
+  if (data_len > 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,26 +1067,48 @@ 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 txt_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 char *text = (char *)&cmd_frame[i];
+    int text_len = (len > (size_t)i) ? (int)(len - i) : 0;
 
     if (txt_type != TXT_TYPE_PLAIN) {
       writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     } else {
       ChannelDetails channel;
-      bool success = getChannel(channel_idx, channel);
-      if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) {
+      if (!getChannel(channel_idx, channel)) {
+        writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
+      } else if (sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, text_len)) {
         writeOKFrame();
       } else {
-        writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
+        writeErrFrame(ERR_CODE_TABLE_FULL);
       }
     }
+  } else if (cmd_frame[0] == CMD_SEND_CHANNEL_DATA) { // send GroupChannel datagram
+    int i = 1;
+    uint8_t txt_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 (txt_type != TXT_TYPE_CUSTOM_BINARY) {
+      writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
+    } else if (sendGroupData(msg_timestamp, channel.channel, txt_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

+ 2 - 0
examples/companion_radio/MyMesh.h

@@ -137,6 +137,8 @@ protected:
                            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 txt_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;

+ 27 - 1
src/helpers/BaseChatMesh.cpp

@@ -353,8 +353,10 @@ 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) {
+  if (len < 5) return;
+
   uint8_t txt_type = data[4];
-  if (type == PAYLOAD_TYPE_GRP_TXT && len > 5 && (txt_type >> 2) == 0) {  // 0 = plain text msg
+  if (type == PAYLOAD_TYPE_GRP_TXT && (txt_type >> 2) == 0) {  // 0 = plain text msg
     uint32_t timestamp;
     memcpy(&timestamp, data, 4);
 
@@ -363,6 +365,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(&timestamp, data, 4);
+    onChannelDataRecv(channel, packet, timestamp, txt_type, &data[5], len - 5);
   }
 }
 
@@ -454,6 +460,26 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan
   return false;
 }
 
+bool BaseChatMesh::sendGroupData(uint32_t timestamp, mesh::GroupChannel& channel, uint8_t txt_type, const uint8_t* data, int data_len) {
+  if (data_len < 0) return false;
+  // createGroupDatagram() accepts at most (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE)
+  // plaintext bytes; subtract our 5-byte {timestamp, txt_type} header.
+  const int max_group_data_len = (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE) - 5;
+  if (data_len > max_group_data_len) data_len = max_group_data_len;
+
+  uint8_t temp[MAX_PACKET_PAYLOAD];
+  memcpy(temp, &timestamp, 4);
+  temp[4] = txt_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) {
+    sendFloodScoped(channel, pkt);
+    return true;
+  }
+  return false;
+}
+
 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

+ 3 - 0
src/helpers/BaseChatMesh.h

@@ -111,6 +111,8 @@ protected:
   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 txt_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 @@ public:
   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 txt_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);

+ 1 - 0
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 TXT_TYPE_CUSTOM_BINARY  0xFF // custom app binary payload (group/channel datagrams)
 
 class StrHelper {
 public: