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:
7 files changed
+108
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user