Companion: say which profile raw and channel data arrived on

MeshCore Open TAK labels a MediaMesh photo or voice clip "Received via
MediaMesh", "via main mesh" or both. Wadamesh standalones tell the
app the profile in a frame byte stock firmware leaves reserved; a
Cascade companion left it unset, so the app showed nothing.

While radio2 is on (rx or rxtx):
- PUSH_CODE_RAW_DATA (0x84) byte 3: 0xA0 primary, 0xA1 radio2
  (stock 0xFF).
- RESP_CODE_CHANNEL_DATA_RECV (0x1B) byte 2: the same (stock 0).
With radio2 off, or for a packet from the UDP bridge, the stock values
stay, so existing clients see no change.

Test: test/test_companion_rx_via.py (in run-unit-tests); the two
harnesses that compile these handlers get a stub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i5tJJtj95MB4oUgncCUe5
This commit is contained in:
Claude committed 2026-10-24 17:31:53 -07:00
1 parent 06edf9aa8a
commit 0e756bf24a
7 files changed
+108 -4

No files matched your search

+1
View File
@@ -588,6 +588,7 @@ jobs:
python3 -B test/test_companion_prefs_transactions.py -v
python3 -B test/test_companion_primary_radio_persistence.py -v
python3 -B test/test_companion_response_bounds.py -v
python3 -B test/test_companion_rx_via.py -v
python3 -B test/test_companion_ack_delivery.py -v
python3 -B test/test_companion_lost_reply.py -v
python3 -B test/test_companion_delayed_reply_delivery.py -v
+3 -2
View File
@@ -715,7 +715,8 @@ Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarde
```
Byte 0: 0x1B (packet type)
Byte 1: SNR (signed int8, scaled x4 - divide by 4.0 to recover dB)
Bytes 2-3: Reserved (clients MUST ignore)
Byte 2: Receive profile while radio2 is on: 0xA0 primary, 0xA1 radio2; otherwise 0 (reserved)
Byte 3: Reserved (clients MUST ignore)
Byte 4: Channel index (0 through max_channels - 1)
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
Bytes 6-7: Data Type (uint16 little-endian)
@@ -1036,7 +1037,7 @@ Byte values are authoritative; names are aliases. When reading firmware source,
| `0x81` | `PUSH_CODE_PATH_UPDATED` | A contact path changed. |
| `0x82` | `PUSH_CODE_SEND_CONFIRMED` | A sent message was acknowledged. |
| `0x83` | `PUSH_CODE_MSG_WAITING` | One or more offline frames are waiting. |
| `0x84` | `PUSH_CODE_RAW_DATA` | Raw application data received. |
| `0x84` | `PUSH_CODE_RAW_DATA` | Raw application data received: `[0x84][SNR x4][RSSI][profile][payload]`. `profile` is 0xA0 (primary) or 0xA1 (radio2) while radio2 is on, otherwise 0xFF (reserved). |
| `0x85` / `0x86` | `PUSH_CODE_LOGIN_SUCCESS` / `PUSH_CODE_LOGIN_FAIL` | Server login result. |
| `0x87` | `PUSH_CODE_STATUS_RESPONSE` | Server status response. |
| `0x88` | `PUSH_CODE_LOG_RX_DATA` | Radio receive log data. |
+17 -2
View File
@@ -2358,7 +2358,7 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *
int i = 0;
out_frame[i++] = RESP_CODE_CHANNEL_DATA_RECV;
out_frame[i++] = (int8_t)(pkt->getSNR() * 4);
out_frame[i++] = 0; // reserved1
out_frame[i++] = rxViaTag(pkt, 0); // reserved1: the profile it arrived on, while radio2 is on
out_frame[i++] = 0; // reserved2
const int selected_channel = findChannelIdxForPacket(channel, pkt);
@@ -2707,6 +2707,21 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
}
}
// Which profile a received packet arrived on, for the app, in a frame byte that
// stock firmware leaves reserved: 0xA0 the primary, 0xA1 radio2. MeshCore Open
// TAK shows it under a MediaMesh photo or voice clip ("Received via MediaMesh",
// "via main mesh", or both); Wadamesh standalones send the same tags. While
// radio2 is off everything is the primary, so the stock value (`none`) stays,
// as it does for a packet that came in over the UDP bridge.
uint8_t MyMesh::rxViaTag(const mesh::Packet *pkt, uint8_t none) const {
const mesh::RadioProfiles *profiles = _radio ? _radio->profiles() : nullptr;
if (!pkt || !profiles || !profiles->enabled()) return none;
#if defined(WITH_UDP_BRIDGE)
if (pkt->udp_bridge_ingress) return none;
#endif
return pkt->radio_origin == 1 ? 0xA1 : 0xA0;
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > MAX_FRAME_SIZE) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -2716,7 +2731,7 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
out_frame[i++] = PUSH_CODE_RAW_DATA;
out_frame[i++] = (int8_t)(packet->getSNR() * 4);
out_frame[i++] = (int8_t)packet->getRSSI();
out_frame[i++] = 0xFF; // reserved (possibly path_len in future)
out_frame[i++] = rxViaTag(packet, 0xFF); // reserved: the profile it arrived on, while radio2 is on
memcpy(&out_frame[i], packet->payload, packet->payload_len);
i += packet->payload_len;
+3
View File
@@ -555,6 +555,9 @@ protected:
void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
void onControlDataRecv(mesh::Packet *packet) override;
void onRawDataRecv(mesh::Packet *packet) override;
// The profile a received packet arrived on, for a frame's reserved byte
// (see rxViaTag in MyMesh.cpp).
uint8_t rxViaTag(const mesh::Packet *pkt, uint8_t none) const;
void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override;
+1
View File
@@ -80,6 +80,7 @@ struct MyMesh {
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t);
void onControlDataRecv(mesh::Packet*);
void onRawDataRecv(mesh::Packet*);
uint8_t rxViaTag(const mesh::Packet*, uint8_t none) const { return none; }
void onTraceRecv(mesh::Packet*,uint32_t,uint32_t,uint8_t,const uint8_t*,const uint8_t*,uint8_t);
void clearBinaryTraceReply();
void checkGuards() const {
+82
View File
@@ -0,0 +1,82 @@
"""Companion receive-profile byte: raw and channel data frames say which
profile a packet arrived on (0xA0 primary, 0xA1 radio2) while radio2 is on,
and keep the stock reserved value otherwise or for UDP-bridge ingress."""
from pathlib import Path
import os
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <initializer_list>
#include <RadioProfiles.h>
namespace mesh {
struct Packet { uint8_t radio_origin = 0; bool udp_bridge_ingress = false; };
struct Radio {
RadioProfiles* p = nullptr;
const RadioProfiles* profiles() const { return p; }
};
}
struct MyMesh {
mesh::Radio* _radio = nullptr;
uint8_t rxViaTag(const mesh::Packet* pkt, uint8_t none) const;
};
@METHOD@
int main() {
mesh::RadioProfiles profiles;
mesh::Radio radio;
MyMesh mesh_;
mesh::Packet primary, second, bridged;
second.radio_origin = 1;
bridged.radio_origin = 1; bridged.udp_bridge_ingress = true;
// No radio, no profiles, radio2 off: the stock reserved values.
assert(mesh_.rxViaTag(&primary, 0xFF) == 0xFF);
mesh_._radio = &radio;
assert(mesh_.rxViaTag(&second, 0) == 0);
radio.p = &profiles;
profiles.secondary.mode = mesh::RadioProfileMode::Off;
assert(mesh_.rxViaTag(&second, 0xFF) == 0xFF);
assert(mesh_.rxViaTag(nullptr, 0) == 0);
// radio2 listening or sending: tagged by the profile it arrived on.
for (auto mode : {mesh::RadioProfileMode::Rx, mesh::RadioProfileMode::RxTx}) {
profiles.secondary.mode = mode;
assert(mesh_.rxViaTag(&primary, 0xFF) == 0xA0);
assert(mesh_.rxViaTag(&second, 0) == 0xA1);
assert(mesh_.rxViaTag(&bridged, 0) == 0); // came over the UDP bridge, not a radio
}
return 0;
}
'''
class CompanionRxViaTest(unittest.TestCase):
def test_rx_via_tag(self):
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text(encoding='utf-8')
method = extract_braced(source, 'uint8_t MyMesh::rxViaTag(')
self.assertIn('rxViaTag(pkt, 0)', source) # channel data, reserved1
self.assertIn('rxViaTag(packet, 0xFF)', source) # raw data, reserved
with tempfile.TemporaryDirectory(prefix='companion-rx-via-') as directory:
work = Path(directory)
cpp = work / 'test.cpp'
cpp.write_text(HARNESS.replace('@METHOD@', method), encoding='utf-8')
flags = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all'] if sys.platform.startswith('linux') else []
binary = work / 'test.exe'
compiled = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-DWITH_UDP_BRIDGE', *flags, f'-I{ROOT / "src"}', str(cpp), '-o', str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(compiled.returncode, 0, compiled.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stderr)
if __name__ == '__main__':
unittest.main()
+1
View File
@@ -92,6 +92,7 @@ struct MyMesh {
if(pending_route==route)pending_route=nullptr;
}
int findChannelIdxForPacket(const mesh::GroupChannel&,mesh::Packet*) const { return 0; }
uint8_t rxViaTag(const mesh::Packet*,uint8_t none) const { return none; }
int getOfflineQueueCapacity() const;
Frame& offlineQueueFrameAt(int);
bool addToOfflineQueue(const uint8_t*,int);