Merge branch 'transportcodes' into dev

This commit is contained in:
Scott Powell 2025-11-07 14:52:11 +11:00
commit 28b90c18cf
20 changed files with 821 additions and 22 deletions

View file

@ -50,6 +50,8 @@
#define CMD_SEND_BINARY_REQ 50
#define CMD_FACTORY_RESET 51
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define CMD_SET_FLOOD_SCOPE 54 // v8+
#define CMD_SEND_CONTROL_DATA 55 // v8+
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@ -99,6 +101,7 @@
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
#define PUSH_CODE_CONTROL_DATA 0x8E // v8+
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@ -378,6 +381,35 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
#endif
}
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
return false;
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
}
}
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
@ -596,6 +628,26 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
}
void MyMesh::onControlDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
return;
}
int i = 0;
out_frame[i++] = PUSH_CODE_CONTROL_DATA;
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
out_frame[i++] = packet->path_len;
memcpy(&out_frame[i], packet->payload, packet->payload_len);
i += packet->payload_len;
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
}
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@ -663,6 +715,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
sign_data = NULL;
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@ -1485,6 +1538,21 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
}
writeOKFrame();
} else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) {
auto resp = createControlData(&cmd_frame[1], len - 1);
if (resp) {
sendZeroHop(resp);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

View file

@ -5,7 +5,7 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 7
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "2 Oct 2025"
@ -68,6 +68,7 @@
#endif
#include <helpers/BaseChatMesh.h>
#include <helpers/TransportKeyStore.h>
/* -------------------------------------------------------------------------------------- */
@ -106,6 +107,10 @@ protected:
int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
@ -128,6 +133,7 @@ protected:
uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) override;
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;
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;
@ -191,6 +197,8 @@ private:
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
TransportKey send_scope;
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
uint8_t out_frame[MAX_FRAME_SIZE + 1];
CayenneLPP telemetry;

View file

@ -306,6 +306,10 @@ File MyMesh::openAppend(const char *fname) {
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false;
}
return true;
}
@ -404,6 +408,23 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
// just try to determine region for packet (apply later in allowPacketForward())
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
recv_pkt_region = NULL;
} else {
recv_pkt_region = &region_map.getWildcard();
}
} else {
recv_pkt_region = NULL;
}
// do normal processing
return false;
}
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
uint8_t *data, size_t len) {
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin
@ -412,7 +433,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
reply_len = 0; // unknown request type
}
if (reply_len == 0) return; // invalid request
@ -590,10 +618,42 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
return false;
}
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) {
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
memcpy(&tag, &packet->payload[i], 4); i += 4;
uint32_t since;
if (packet->payload_len >= i+4) { // optional since field
memcpy(&since, &packet->payload[i], 4); i += 4;
} else {
since = 0;
}
if ((filter & (1 << ADV_TYPE_REPEATER)) != 0 && _prefs.discovery_mod_timestamp >= since) {
uint8_t data[6 + PUB_KEY_SIZE];
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_REPEATER; // low 4-bits for node type
data[1] = packet->_snr; // let sender know the inbound SNR ( x 4)
memcpy(&data[2], &tag, 4); // include tag from request, for client to match to
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
auto resp = createControlData(data, sizeof(data));
if (resp) {
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
}
}
}
}
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120) // max 4 every 2 minutes
#if defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
#endif
@ -607,6 +667,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
dirty_contacts_expiry = 0;
set_radio_at = revert_radio_at = 0;
_logging = false;
region_load_active = false;
#if MAX_NEIGHBOURS
memset(neighbours, 0, sizeof(neighbours));
@ -652,8 +713,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
// TODO: key_store.begin();
region_map.load(_fs);
#if defined(WITH_BRIDGE)
if (_prefs.bridge_enabled) {
@ -829,8 +891,41 @@ void MyMesh::clearStats() {
}
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
while (*command == ' ')
command++; // skip leading spaces
if (region_load_active) {
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
region_map = temp_map; // copy over the temp instance as new current map
region_load_active = false;
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
} else {
char *np = command;
while (*np == ' ') np++; // skip indent
int indent = np - command;
char *ep = np;
while (RegionMap::is_name_char(*ep)) ep++;
if (*ep) { *ep++ = 0; } // set null terminator for end of name
while (*ep && *ep != 'F') ep++; // look for (optional) flags
if (indent > 0 && indent < 8 && strlen(np) > 0) {
auto parent = load_stack[indent - 1];
if (parent) {
auto old = region_map.findByName(np);
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists)
if (nw) {
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr
load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's
}
}
}
reply[0] = 0;
}
return;
}
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
@ -872,6 +967,88 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
Serial.printf("\n");
}
reply[0] = 0;
} else if (memcmp(command, "region", 6) == 0) {
reply[0] = 0;
const char* parts[4];
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
if (n == 1 && sender_timestamp == 0) {
region_map.exportTo(Serial);
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
memset(load_stack, 0, sizeof(load_stack));
load_stack[0] = &temp_map.getWildcard();
region_load_active = true;
} else if (n >= 2 && strcmp(parts[1], "save") == 0) {
_prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime(); // this node is now 'modified' (for discovery info)
savePrefs();
bool success = region_map.save(_fs);
strcpy(reply, success ? "OK" : "Err - save failed");
} else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags &= ~REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags |= REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "get") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
auto parent = region_map.findById(region->parent);
if (parent && parent->id != 0) {
sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
} else {
sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
}
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "home") == 0) {
auto home = region_map.findByNamePrefix(parts[2]);
if (home) {
region_map.setHomeRegion(home);
sprintf(reply, " home is now %s", home->name);
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n == 2 && strcmp(parts[1], "home") == 0) {
auto home = region_map.getHomeRegion();
sprintf(reply, " home is %s", home ? home->name : "*");
} else if (n >= 3 && strcmp(parts[1], "put") == 0) {
auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_map.getWildcard();
if (parent == NULL) {
strcpy(reply, "Err - unknown parent");
} else {
auto region = region_map.putRegion(parts[2], parent->id);
if (region == NULL) {
strcpy(reply, "Err - unable to put");
} else {
strcpy(reply, "OK");
}
}
} else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
auto region = region_map.findByName(parts[2]);
if (region) {
if (region_map.removeRegion(*region)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - not empty");
}
} else {
strcpy(reply, "Err - not found");
}
} else {
strcpy(reply, "Err - ??");
}
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}

View file

@ -32,6 +32,8 @@
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include "RateLimiter.h"
#ifdef WITH_BRIDGE
extern AbstractBridge* bridge;
@ -87,6 +89,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
RateLimiter discover_limiter;
bool region_load_active;
unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
@ -144,12 +152,15 @@ protected:
}
#endif
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);

View file

@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
class RateLimiter {
uint32_t _start_timestamp;
uint32_t _secs;
uint16_t _maximum, _count;
public:
RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
bool allow(uint32_t now) {
if (now < _start_timestamp + _secs) {
_count++;
if (_count > _maximum) return false; // deny
} else { // time window now expired
_start_timestamp = now;
_count = 1;
}
return true;
}
};

View file

@ -91,14 +91,16 @@ void loop() {
if (c != '\n') {
command[len++] = c;
command[len] = 0;
Serial.print(c);
}
Serial.print(c);
if (c == '\r') break;
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!

View file

@ -449,7 +449,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
reply_len = 0; // unknown request type
}
if (reply_len == 0) return; // invalid request