Add direct retry controls
This commit is contained in:
1 parent
7d8bb1d319
commit
765ec1447b
12 files changed
+649
-416
No files matched your search
+45
-87
@@ -35,6 +35,7 @@ void Mesh::begin() {
|
||||
_direct_retries[i].retry_at = 0;
|
||||
_direct_retries[i].retry_delay = 0;
|
||||
_direct_retries[i].retry_attempts_sent = 0;
|
||||
_direct_retries[i].next_hop_hash_len = 0;
|
||||
_direct_retries[i].priority = 0;
|
||||
_direct_retries[i].progress_marker = 0;
|
||||
_direct_retries[i].expect_path_growth = false;
|
||||
@@ -63,6 +64,7 @@ void Mesh::loop() {
|
||||
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
|
||||
onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
|
||||
onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
|
||||
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
continue;
|
||||
}
|
||||
@@ -183,12 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
}
|
||||
}
|
||||
|
||||
if (canDecodeDirectPayloadForSelf(pkt)) {
|
||||
// Some path sources include the final node hash, and some packets are
|
||||
// heard before all planned hops are consumed. Only stop forwarding once
|
||||
// this node proves it can decrypt the payload.
|
||||
removePathPrefix(pkt, pkt->getPathHashCount());
|
||||
} else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
|
||||
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
|
||||
if (allowPacketForward(pkt)) {
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
|
||||
return forwardMultipartDirect(pkt);
|
||||
@@ -479,13 +476,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
|
||||
|
||||
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
if (!packet->isMarkedDoNotRetransmit()) {
|
||||
uint32_t crc;
|
||||
memcpy(&crc, packet->payload, 4);
|
||||
|
||||
uint8_t extra = getExtraAckTransmitCount();
|
||||
while (extra > 0) {
|
||||
delay_millis += getDirectRetransmitDelay(packet) + 300;
|
||||
auto a1 = createMultiAck(crc, extra);
|
||||
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
|
||||
if (a1) {
|
||||
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
|
||||
a1->header &= ~PH_ROUTE_MASK;
|
||||
@@ -496,7 +490,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
extra--;
|
||||
}
|
||||
|
||||
auto a2 = createAck(crc);
|
||||
auto a2 = createAck(packet->payload, packet->payload_len);
|
||||
if (a2) {
|
||||
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
|
||||
a2->header &= ~PH_ROUTE_MASK;
|
||||
@@ -508,9 +502,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
}
|
||||
|
||||
void Mesh::clearDirectRetrySlot(int idx) {
|
||||
if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) {
|
||||
releasePacket(_direct_retries[idx].packet);
|
||||
}
|
||||
_direct_retries[idx].packet = NULL;
|
||||
_direct_retries[idx].trigger_packet = NULL;
|
||||
_direct_retries[idx].retry_started_at = 0;
|
||||
@@ -518,6 +509,8 @@ void Mesh::clearDirectRetrySlot(int idx) {
|
||||
_direct_retries[idx].retry_at = 0;
|
||||
_direct_retries[idx].retry_delay = 0;
|
||||
_direct_retries[idx].retry_attempts_sent = 0;
|
||||
memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash));
|
||||
_direct_retries[idx].next_hop_hash_len = 0;
|
||||
_direct_retries[idx].priority = 0;
|
||||
_direct_retries[idx].progress_marker = 0;
|
||||
_direct_retries[idx].expect_path_growth = false;
|
||||
@@ -558,6 +551,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
|
||||
}
|
||||
|
||||
int8_t echo_snr_x4 = packet->_snr;
|
||||
onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4);
|
||||
if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) {
|
||||
if (_direct_retries[i].packet != NULL) {
|
||||
// Success quality comes from the received downstream echo, not the original upstream RX.
|
||||
@@ -620,16 +614,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX;
|
||||
}
|
||||
if (_direct_retries[i].retry_attempts_sent >= max_attempts) {
|
||||
Packet* final_wait = obtainNewPacket();
|
||||
if (final_wait == NULL) {
|
||||
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
|
||||
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
|
||||
clearDirectRetrySlot(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
*final_wait = *packet;
|
||||
_direct_retries[i].packet = final_wait;
|
||||
// Dispatcher releases the retry packet after this hook. Keep only retry metadata
|
||||
// for the final echo window so pool exhaustion cannot force a premature failure.
|
||||
_direct_retries[i].packet = NULL;
|
||||
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
|
||||
_direct_retries[i].waiting_final_echo = true;
|
||||
_direct_retries[i].queued = false;
|
||||
@@ -783,62 +770,6 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h
|
||||
}
|
||||
}
|
||||
|
||||
bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) {
|
||||
if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (packet->getPayloadType()) {
|
||||
case PAYLOAD_TYPE_PATH:
|
||||
case PAYLOAD_TYPE_REQ:
|
||||
case PAYLOAD_TYPE_RESPONSE:
|
||||
case PAYLOAD_TYPE_TXT_MSG: {
|
||||
if (packet->payload_len < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
uint8_t dest_hash = packet->payload[i++];
|
||||
uint8_t src_hash = packet->payload[i++];
|
||||
if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int num = searchPeersByHash(&src_hash);
|
||||
for (int j = 0; j < num; j++) {
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
getPeerSharedSecret(secret, j);
|
||||
|
||||
uint8_t data[MAX_PACKET_PAYLOAD];
|
||||
if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case PAYLOAD_TYPE_ANON_REQ: {
|
||||
int i = 0;
|
||||
uint8_t dest_hash = packet->payload[i++];
|
||||
if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Identity sender(&packet->payload[i]);
|
||||
i += PUB_KEY_SIZE;
|
||||
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
self_id.calcSharedSecret(secret, sender);
|
||||
|
||||
uint8_t data[MAX_PACKET_PAYLOAD];
|
||||
return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
const uint8_t* next_hop_hash;
|
||||
uint8_t next_hop_hash_len;
|
||||
@@ -849,6 +780,18 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t retry_key[MAX_HASH_SIZE];
|
||||
calculateDirectRetryKey(packet, retry_key);
|
||||
|
||||
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
|
||||
if (_direct_retries[i].active
|
||||
&& memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0
|
||||
&& _direct_retries[i].progress_marker == progress_marker
|
||||
&& _direct_retries[i].expect_path_growth == expect_path_growth) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int slot_idx = -1;
|
||||
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
|
||||
if (!_direct_retries[i].active) {
|
||||
@@ -864,7 +807,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
|
||||
// Only store retry metadata here; allocate the retry packet after the initial TX really completes.
|
||||
uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0);
|
||||
calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key);
|
||||
memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE);
|
||||
_direct_retries[slot_idx].packet = NULL;
|
||||
_direct_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
|
||||
_direct_retries[slot_idx].retry_started_at = 0;
|
||||
@@ -872,6 +815,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
_direct_retries[slot_idx].retry_at = 0;
|
||||
_direct_retries[slot_idx].retry_delay = retry_delay;
|
||||
_direct_retries[slot_idx].retry_attempts_sent = 0;
|
||||
memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash));
|
||||
memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len);
|
||||
_direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len;
|
||||
_direct_retries[slot_idx].priority = priority;
|
||||
_direct_retries[slot_idx].progress_marker = progress_marker;
|
||||
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
|
||||
@@ -1036,7 +982,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) {
|
||||
if (ack_len > sizeof(Packet::payload)) return NULL;
|
||||
|
||||
Packet* packet = obtainNewPacket();
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime());
|
||||
@@ -1044,13 +992,19 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
}
|
||||
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
|
||||
|
||||
memcpy(packet->payload, &ack_crc, 4);
|
||||
packet->payload_len = 4;
|
||||
memcpy(packet->payload, ack_hash, ack_len);
|
||||
packet->payload_len = ack_len;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
return createAck((const uint8_t*)&ack_crc, 4);
|
||||
}
|
||||
|
||||
Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) {
|
||||
if (ack_len + 1 > sizeof(Packet::payload)) return NULL;
|
||||
|
||||
Packet* packet = obtainNewPacket();
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
|
||||
@@ -1059,12 +1013,16 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
|
||||
|
||||
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
|
||||
memcpy(&packet->payload[1], &ack_crc, 4);
|
||||
packet->payload_len = 5;
|
||||
memcpy(&packet->payload[1], ack_hash, ack_len);
|
||||
packet->payload_len = ack_len + 1;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
return createMultiAck((const uint8_t*)&ack_crc, 4, remaining);
|
||||
}
|
||||
|
||||
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
|
||||
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
|
||||
|
||||
|
||||
+18
-1
@@ -29,6 +29,10 @@ public:
|
||||
* and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets.
|
||||
*/
|
||||
class Mesh : public Dispatcher {
|
||||
RNG* _rng;
|
||||
RTCClock* _rtc;
|
||||
MeshTables* _tables;
|
||||
|
||||
struct DirectRetryEntry {
|
||||
Packet* packet;
|
||||
Packet* trigger_packet;
|
||||
@@ -38,6 +42,8 @@ class Mesh : public Dispatcher {
|
||||
uint32_t retry_delay;
|
||||
uint8_t retry_attempts_sent;
|
||||
uint8_t retry_key[MAX_HASH_SIZE];
|
||||
uint8_t next_hop_hash[MAX_HASH_SIZE];
|
||||
uint8_t next_hop_hash_len;
|
||||
uint8_t priority;
|
||||
uint8_t progress_marker;
|
||||
bool expect_path_growth;
|
||||
@@ -58,7 +64,6 @@ class Mesh : public Dispatcher {
|
||||
void clearPendingDirectRetryOnSendFail(const Packet* packet);
|
||||
bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len,
|
||||
uint8_t& progress_marker, bool& expect_path_growth) const;
|
||||
bool canDecodeDirectPayloadForSelf(const Packet* packet);
|
||||
void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority);
|
||||
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
DispatcherAction forwardMultipartDirect(Packet* pkt);
|
||||
@@ -133,6 +138,16 @@ protected:
|
||||
*/
|
||||
virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
|
||||
|
||||
/**
|
||||
* \brief Optional hook for link-quality feedback when all direct-retry attempts fail.
|
||||
*/
|
||||
virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { }
|
||||
|
||||
/**
|
||||
* \brief Optional hook for link-quality feedback when a direct-retry echo is heard.
|
||||
*/
|
||||
virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { }
|
||||
|
||||
/**
|
||||
* \brief Optional hook to set local-only transmit options on a retry packet before it is queued.
|
||||
*/
|
||||
@@ -253,7 +268,9 @@ public:
|
||||
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
|
||||
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
|
||||
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
|
||||
Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len);
|
||||
Packet* createAck(uint32_t ack_crc);
|
||||
Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining);
|
||||
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
|
||||
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "ClientACL.h"
|
||||
|
||||
static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1;
|
||||
|
||||
static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
_fs->remove(filename);
|
||||
@@ -30,7 +28,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
uint8_t unused[2];
|
||||
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.alt_path_len = OUT_PATH_UNKNOWN;
|
||||
|
||||
bool success = (file.read(pub_key, 32) == 32);
|
||||
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
|
||||
@@ -39,10 +36,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.read(c.out_path, 64) == 64);
|
||||
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
|
||||
if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) {
|
||||
success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1);
|
||||
success = success && (file.read(c.alt_path, 64) == 64);
|
||||
}
|
||||
|
||||
if (!success) break; // EOF
|
||||
|
||||
@@ -64,8 +57,7 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
|
||||
File file = openWrite(_fs, "/s_contacts");
|
||||
if (file) {
|
||||
uint8_t unused[2];
|
||||
unused[0] = CONTACT_RECORD_VERSION_ALT_PATH;
|
||||
unused[1] = 0;
|
||||
memset(unused, 0, sizeof(unused));
|
||||
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
auto c = &clients[i];
|
||||
@@ -78,8 +70,6 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
|
||||
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
|
||||
success = success && (file.write(c->out_path, 64) == 64);
|
||||
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1);
|
||||
success = success && (file.write(c->alt_path, 64) == 64);
|
||||
|
||||
if (!success) break; // write failed
|
||||
}
|
||||
@@ -125,7 +115,6 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
c->permissions = init_perms;
|
||||
c->id = id;
|
||||
c->out_path_len = OUT_PATH_UNKNOWN;
|
||||
c->alt_path_len = OUT_PATH_UNKNOWN;
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,13 @@
|
||||
#define PERM_ACL_READ_WRITE 2
|
||||
#define PERM_ACL_ADMIN 3
|
||||
|
||||
#define OUT_PATH_FORCE_FLOOD 0xFE
|
||||
#define OUT_PATH_UNKNOWN 0xFF
|
||||
#define OUT_PATH_UNKNOWN 0xFF
|
||||
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint8_t permissions;
|
||||
uint8_t out_path_len;
|
||||
uint8_t out_path[MAX_PATH_SIZE];
|
||||
uint8_t alt_path_len;
|
||||
uint8_t alt_path[MAX_PATH_SIZE];
|
||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
uint32_t last_timestamp; // by THEIR clock (transient)
|
||||
uint32_t last_activity; // by OUR clock (transient)
|
||||
|
||||
+275
-2
@@ -27,6 +27,113 @@ static bool isValidName(const char *n) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool looksNumeric(const char* s) {
|
||||
if (s == NULL) return false;
|
||||
while (*s == ' ') s++;
|
||||
if (*s == '-' || *s == '+') s++;
|
||||
bool saw_digit = false;
|
||||
while (*s) {
|
||||
if (*s >= '0' && *s <= '9') {
|
||||
saw_digit = true;
|
||||
} else if (*s != '.') {
|
||||
break;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
return saw_digit;
|
||||
}
|
||||
|
||||
static int16_t parseSnrDbX4(const char* s) {
|
||||
float db = atof(s);
|
||||
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
|
||||
}
|
||||
|
||||
static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) {
|
||||
int16_t v = snr_x4;
|
||||
const char* sign = "";
|
||||
if (v < 0) {
|
||||
sign = "-";
|
||||
v = -v;
|
||||
}
|
||||
snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25);
|
||||
}
|
||||
|
||||
static const char* retryPresetName(uint8_t preset) {
|
||||
switch (preset) {
|
||||
case RETRY_PRESET_INFRA: return "infra";
|
||||
case RETRY_PRESET_ROOFTOP: return "rooftop";
|
||||
case RETRY_PRESET_MOBILE: return "mobile";
|
||||
default: return "custom";
|
||||
}
|
||||
}
|
||||
|
||||
static void markDirectRetryPrefsValid(NodePrefs* prefs) {
|
||||
prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
|
||||
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
|
||||
}
|
||||
|
||||
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
prefs->retry_preset = preset;
|
||||
if (preset == RETRY_PRESET_INFRA) {
|
||||
prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT;
|
||||
prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS;
|
||||
prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS;
|
||||
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4;
|
||||
} else if (preset == RETRY_PRESET_MOBILE) {
|
||||
prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT;
|
||||
prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS;
|
||||
prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS;
|
||||
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4;
|
||||
} else {
|
||||
prefs->retry_preset = RETRY_PRESET_ROOFTOP;
|
||||
prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT;
|
||||
prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
|
||||
prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
|
||||
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
|
||||
}
|
||||
markDirectRetryPrefsValid(prefs);
|
||||
}
|
||||
|
||||
static void setDefaultDirectRetryPrefs(NodePrefs* prefs) {
|
||||
applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP);
|
||||
prefs->direct_retry_cr_enabled = 1;
|
||||
prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT;
|
||||
prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT;
|
||||
prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
|
||||
prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
|
||||
markDirectRetryPrefsValid(prefs);
|
||||
}
|
||||
|
||||
static bool directRetryPrefsValid(const NodePrefs* prefs) {
|
||||
return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0
|
||||
&& prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1;
|
||||
}
|
||||
|
||||
static bool parseRetryPreset(const char* s, uint8_t& preset) {
|
||||
if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) {
|
||||
preset = RETRY_PRESET_INFRA;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) {
|
||||
preset = RETRY_PRESET_ROOFTOP;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) {
|
||||
preset = RETRY_PRESET_MOBILE;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) {
|
||||
size_t hex_len = strlen(text);
|
||||
if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) {
|
||||
return false;
|
||||
}
|
||||
prefix_len = hex_len / 2;
|
||||
return mesh::Utils::fromHex(prefix, prefix_len, text);
|
||||
}
|
||||
|
||||
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
if (fs->exists("/com_prefs")) {
|
||||
loadPrefsInt(fs, "/com_prefs"); // new filename
|
||||
@@ -93,7 +200,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
|
||||
file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
|
||||
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
|
||||
// next: 295
|
||||
file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
|
||||
file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
|
||||
file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
|
||||
file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
|
||||
file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
|
||||
file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
|
||||
// next: 310
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
|
||||
@@ -125,6 +243,17 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
|
||||
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
|
||||
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
|
||||
if (!directRetryPrefsValid(_prefs)) {
|
||||
setDefaultDirectRetryPrefs(_prefs);
|
||||
}
|
||||
if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) {
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
}
|
||||
_prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15);
|
||||
_prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000);
|
||||
_prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000);
|
||||
_prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160);
|
||||
_prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1);
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -190,7 +319,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
|
||||
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
|
||||
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
|
||||
// next: 295
|
||||
markDirectRetryPrefsValid(_prefs);
|
||||
file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
|
||||
file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
|
||||
file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
|
||||
file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
|
||||
file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
|
||||
file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
|
||||
// next: 310
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -301,6 +442,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else if (memcmp(command, "clear stats", 11) == 0) {
|
||||
_callbacks->clearStats();
|
||||
strcpy(reply, "(OK - stats reset)");
|
||||
} else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) {
|
||||
_callbacks->clearRecentRepeaters();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "get ", 4) == 0) {
|
||||
handleGetCmd(sender_timestamp, command, reply);
|
||||
} else if (memcmp(command, "set ", 4) == 0) {
|
||||
@@ -680,6 +824,104 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} else if (memcmp(config, "retry.preset ", 13) == 0) {
|
||||
uint8_t preset;
|
||||
if (parseRetryPreset(&config[13], preset)) {
|
||||
applyDirectRetryPreset(_prefs, preset);
|
||||
savePrefs();
|
||||
sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset));
|
||||
} else {
|
||||
strcpy(reply, "Error, must be infra, rooftop, or mobile");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.margin ", 20) == 0) {
|
||||
if (!looksNumeric(&config[20])) {
|
||||
strcpy(reply, "Error, must be 0-40 dB");
|
||||
} else {
|
||||
int16_t margin_x4 = parseSnrDbX4(&config[20]);
|
||||
if (margin_x4 >= 0 && margin_x4 <= 160) {
|
||||
_prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-40 dB");
|
||||
}
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.count ", 19) == 0) {
|
||||
int attempts = _atoi(&config[19]);
|
||||
if (attempts >= 1 && attempts <= 15) {
|
||||
_prefs->direct_retry_attempts = (uint8_t)attempts;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 1-15");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.base ", 18) == 0) {
|
||||
int base_ms = _atoi(&config[18]);
|
||||
if (base_ms >= 10 && base_ms <= 5000) {
|
||||
_prefs->direct_retry_base_ms = (uint16_t)base_ms;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 10-5000 ms");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.step ", 18) == 0) {
|
||||
int step_ms = _atoi(&config[18]);
|
||||
if (step_ms >= 0 && step_ms <= 5000) {
|
||||
_prefs->direct_retry_step_ms = (uint16_t)step_ms;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-5000 ms");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.cr ", 16) == 0) {
|
||||
if (memcmp(&config[16], "off", 3) == 0) {
|
||||
_prefs->direct_retry_cr_enabled = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(tmp, &config[16]);
|
||||
const char *parts[4];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ',');
|
||||
if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) {
|
||||
int16_t cr4 = parseSnrDbX4(parts[0]);
|
||||
int16_t cr5 = parseSnrDbX4(parts[1]);
|
||||
int16_t cr7 = parseSnrDbX4(parts[2]);
|
||||
int16_t cr8 = parseSnrDbX4(parts[3]);
|
||||
if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) {
|
||||
_prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4;
|
||||
_prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5;
|
||||
_prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7;
|
||||
_prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8;
|
||||
_prefs->direct_retry_cr_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off");
|
||||
}
|
||||
}
|
||||
} else if (memcmp(config, "recent.repeater ", 16) == 0) {
|
||||
strcpy(tmp, &config[16]);
|
||||
const char *parts[2];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
|
||||
uint8_t prefix[MAX_HASH_SIZE];
|
||||
uint8_t prefix_len = 0;
|
||||
int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0;
|
||||
if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) {
|
||||
strcpy(reply, "Error, use: set recent.repeater <hex> <snr_db>");
|
||||
} else if (snr_x4 < -128 || snr_x4 > 127) {
|
||||
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
|
||||
} else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, table rejected prefix");
|
||||
}
|
||||
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
||||
config += 11;
|
||||
char *dp = _prefs->owner_info;
|
||||
@@ -862,6 +1104,37 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
|
||||
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
|
||||
} else if (memcmp(config, "retry.preset", 12) == 0) {
|
||||
sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset));
|
||||
} else if (memcmp(config, "direct.retry.margin", 19) == 0) {
|
||||
char margin[12];
|
||||
formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4);
|
||||
sprintf(reply, "> %s", margin);
|
||||
} else if (memcmp(config, "direct.retry.count", 18) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts);
|
||||
} else if (memcmp(config, "direct.retry.base", 17) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
|
||||
} else if (memcmp(config, "direct.retry.step", 17) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms);
|
||||
} else if (memcmp(config, "direct.retry.cr", 15) == 0) {
|
||||
if (!_prefs->direct_retry_cr_enabled) {
|
||||
strcpy(reply, "> off");
|
||||
} else {
|
||||
char cr4[12], cr5[12], cr7[12], cr8[12];
|
||||
formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4);
|
||||
formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4);
|
||||
formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4);
|
||||
formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4);
|
||||
sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8);
|
||||
}
|
||||
} else if (memcmp(config, "recent.repeater", 15) == 0) {
|
||||
int page = 1;
|
||||
const char* cursor = &config[15];
|
||||
while (*cursor == ' ') cursor++;
|
||||
if (memcmp(cursor, "page ", 5) == 0) cursor += 5;
|
||||
if (*cursor) page = _atoi(cursor);
|
||||
if (page < 1) page = 1;
|
||||
_callbacks->formatRecentRepeatersReply(reply, page);
|
||||
} else if (memcmp(config, "owner.info", 10) == 0) {
|
||||
auto start = reply;
|
||||
*reply++ = '>';
|
||||
|
||||
@@ -19,6 +19,34 @@
|
||||
#define LOOP_DETECT_MODERATE 2
|
||||
#define LOOP_DETECT_STRICT 3
|
||||
|
||||
#define RETRY_PRESET_INFRA 0
|
||||
#define RETRY_PRESET_ROOFTOP 1
|
||||
#define RETRY_PRESET_MOBILE 2
|
||||
#define RETRY_PRESET_CUSTOM 0xFF
|
||||
|
||||
#define DIRECT_RETRY_INFRA_BASE_MS 275
|
||||
#define DIRECT_RETRY_INFRA_COUNT 4
|
||||
#define DIRECT_RETRY_INFRA_STEP_MS 150
|
||||
#define DIRECT_RETRY_INFRA_MARGIN_X4 60
|
||||
|
||||
#define DIRECT_RETRY_ROOFTOP_BASE_MS 175
|
||||
#define DIRECT_RETRY_ROOFTOP_COUNT 15
|
||||
#define DIRECT_RETRY_ROOFTOP_STEP_MS 100
|
||||
#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20
|
||||
|
||||
#define DIRECT_RETRY_MOBILE_BASE_MS 175
|
||||
#define DIRECT_RETRY_MOBILE_COUNT 15
|
||||
#define DIRECT_RETRY_MOBILE_STEP_MS 50
|
||||
#define DIRECT_RETRY_MOBILE_MARGIN_X4 0
|
||||
|
||||
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
|
||||
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
|
||||
#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10
|
||||
#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10
|
||||
|
||||
#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1
|
||||
#define DIRECT_RETRY_PREFS_MAGIC_1 0x52
|
||||
|
||||
struct NodePrefs { // persisted to file
|
||||
float airtime_factor;
|
||||
char node_name[32];
|
||||
@@ -65,6 +93,17 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t path_hash_mode; // which path mode to use when sending
|
||||
uint8_t loop_detect;
|
||||
uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean)
|
||||
uint8_t retry_preset;
|
||||
uint8_t direct_retry_attempts;
|
||||
uint16_t direct_retry_base_ms;
|
||||
uint16_t direct_retry_step_ms;
|
||||
uint16_t direct_retry_snr_margin_x4;
|
||||
int8_t direct_retry_cr4_snr_x4;
|
||||
int8_t direct_retry_cr5_snr_x4;
|
||||
int8_t direct_retry_cr7_snr_x4;
|
||||
int8_t direct_retry_cr8_snr_x4;
|
||||
uint8_t direct_retry_cr_enabled;
|
||||
uint8_t direct_retry_prefs_magic[2];
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
@@ -88,6 +127,18 @@ public:
|
||||
virtual void formatStatsReply(char *reply) = 0;
|
||||
virtual void formatRadioStatsReply(char *reply) = 0;
|
||||
virtual void formatPacketStatsReply(char *reply) = 0;
|
||||
virtual void formatRecentRepeatersReply(char *reply, int page) {
|
||||
(void)page;
|
||||
if (reply != NULL) reply[0] = 0;
|
||||
}
|
||||
virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
|
||||
(void)prefix;
|
||||
(void)prefix_len;
|
||||
(void)snr_x4;
|
||||
return false;
|
||||
}
|
||||
virtual void clearRecentRepeaters() {
|
||||
}
|
||||
virtual mesh::LocalIdentity& getSelfId() = 0;
|
||||
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
|
||||
virtual void clearStats() = 0;
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include <FS.h>
|
||||
#endif
|
||||
|
||||
#define MAX_PACKET_HASHES 128
|
||||
#define MAX_PACKET_ACKS 64
|
||||
#define MAX_PACKET_HASHES (128+32)
|
||||
#ifndef MAX_RECENT_REPEATERS
|
||||
// Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=<n>.
|
||||
#if defined(ESP32) || defined(ESP32_PLATFORM)
|
||||
@@ -39,8 +38,6 @@ public:
|
||||
private:
|
||||
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
|
||||
int _next_idx;
|
||||
uint32_t _acks[MAX_PACKET_ACKS];
|
||||
int _next_ack_idx;
|
||||
uint32_t _direct_dups, _flood_dups;
|
||||
RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS];
|
||||
int _next_recent_repeater_idx;
|
||||
@@ -48,20 +45,6 @@ private:
|
||||
RecentRepeaterAllowFn _recent_repeater_allow_fn;
|
||||
void* _recent_repeater_allow_ctx;
|
||||
|
||||
bool hasSeenAck(uint32_t ack) const {
|
||||
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
|
||||
if (ack == _acks[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void storeAck(uint32_t ack) {
|
||||
_acks[_next_ack_idx] = ack;
|
||||
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS;
|
||||
}
|
||||
|
||||
bool hasSeenHash(const uint8_t* hash) const {
|
||||
const uint8_t* sp = _hashes;
|
||||
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
|
||||
@@ -148,8 +131,6 @@ public:
|
||||
SimpleMeshTables() {
|
||||
memset(_hashes, 0, sizeof(_hashes));
|
||||
_next_idx = 0;
|
||||
memset(_acks, 0, sizeof(_acks));
|
||||
_next_ack_idx = 0;
|
||||
_direct_dups = _flood_dups = 0;
|
||||
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
|
||||
_next_recent_repeater_idx = 0;
|
||||
@@ -162,8 +143,6 @@ public:
|
||||
void restoreFrom(File f) {
|
||||
f.read(_hashes, sizeof(_hashes));
|
||||
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
|
||||
f.read((uint8_t *) &_acks[0], sizeof(_acks));
|
||||
f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
|
||||
// Recent repeater entries are intentionally not restored across boots.
|
||||
// This avoids struct-layout migration issues and keeps stale path quality
|
||||
// stats from persisting indefinitely.
|
||||
@@ -173,31 +152,10 @@ public:
|
||||
void saveTo(File f) {
|
||||
f.write(_hashes, sizeof(_hashes));
|
||||
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
|
||||
f.write((const uint8_t *) &_acks[0], sizeof(_acks));
|
||||
f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool hasSeen(const mesh::Packet* packet) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
recordRecentRepeater(packet);
|
||||
|
||||
uint32_t ack;
|
||||
memcpy(&ack, packet->payload, 4);
|
||||
|
||||
if (hasSeenAck(ack)) {
|
||||
if (packet->isRouteDirect()) {
|
||||
_direct_dups++; // keep some stats
|
||||
} else {
|
||||
_flood_dups++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
storeAck(ack);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
|
||||
@@ -217,15 +175,6 @@ public:
|
||||
|
||||
void markSent(const mesh::Packet* packet) override {
|
||||
// Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves.
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
uint32_t ack;
|
||||
memcpy(&ack, packet->payload, 4);
|
||||
if (!hasSeenAck(ack)) {
|
||||
storeAck(ack);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
if (!hasSeenHash(hash)) {
|
||||
@@ -234,25 +183,14 @@ public:
|
||||
}
|
||||
|
||||
void clear(const mesh::Packet* packet) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
uint32_t ack;
|
||||
memcpy(&ack, packet->payload, 4);
|
||||
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
|
||||
if (ack == _acks[i]) {
|
||||
_acks[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
|
||||
uint8_t* sp = _hashes;
|
||||
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
|
||||
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
|
||||
memset(sp, 0, MAX_HASH_SIZE);
|
||||
break;
|
||||
}
|
||||
uint8_t* sp = _hashes;
|
||||
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
|
||||
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
|
||||
memset(sp, 0, MAX_HASH_SIZE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,17 +220,13 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry.
|
||||
// Keep exact prefixes distinct so a 1-byte path prefix does not collapse
|
||||
// independent 2/3-byte repeaters that share the same first byte.
|
||||
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
|
||||
RecentRepeaterInfo& existing = _recent_repeaters[i];
|
||||
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
|
||||
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (prefix_len > existing.prefix_len) {
|
||||
memset(existing.prefix, 0, sizeof(existing.prefix));
|
||||
memcpy(existing.prefix, prefix, prefix_len);
|
||||
existing.prefix_len = prefix_len;
|
||||
}
|
||||
if (snr_locked) {
|
||||
existing.snr_x4 = snr_x4;
|
||||
existing.snr_locked = 1;
|
||||
@@ -352,14 +286,9 @@ public:
|
||||
|
||||
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
|
||||
RecentRepeaterInfo& existing = _recent_repeaters[i];
|
||||
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
|
||||
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (prefix_len > existing.prefix_len) {
|
||||
memset(existing.prefix, 0, sizeof(existing.prefix));
|
||||
memcpy(existing.prefix, prefix, prefix_len);
|
||||
existing.prefix_len = prefix_len;
|
||||
}
|
||||
if (!existing.snr_locked) {
|
||||
int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4;
|
||||
if (lowered < -128) {
|
||||
@@ -432,18 +361,25 @@ public:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match.
|
||||
// Prefer exact matches. If none exists, fall back to the newest longest
|
||||
// overlapping prefix so coarse learned prefixes can still inform CR.
|
||||
const RecentRepeaterInfo* best = NULL;
|
||||
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
|
||||
int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS;
|
||||
const RecentRepeaterInfo* info = &_recent_repeaters[idx];
|
||||
if (info->prefix_len == 0) {
|
||||
continue;
|
||||
}
|
||||
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
|
||||
if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) {
|
||||
return info;
|
||||
}
|
||||
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
|
||||
if (best == NULL || info->prefix_len > best->prefix_len) {
|
||||
best = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
return best;
|
||||
}
|
||||
void clearRecentRepeaters() {
|
||||
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
|
||||
|
||||
Reference in new issue
Block a user