Improve repeater reliability and build tooling
This commit is contained in:
1 parent
2b4aed4ef1
commit
da00a00436
31 files changed
+1079
-229
No files matched your search
+29
-14
@@ -29,6 +29,12 @@ void Dispatcher::begin() {
|
||||
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Use begin() as the watchdog baseline even if the radio never reports an
|
||||
// RX/IRQ timestamp. This lets a radio that is dead from startup recover.
|
||||
last_radio_active_ms = _ms->getMillis();
|
||||
last_watchdog_recovery = last_radio_active_ms;
|
||||
#endif
|
||||
}
|
||||
|
||||
float Dispatcher::getAirtimeBudgetFactor() const {
|
||||
@@ -107,20 +113,20 @@ void Dispatcher::loop() {
|
||||
{
|
||||
const uint32_t watchdog_ms = getRadioWatchdogMillis();
|
||||
if (watchdog_ms > 0) {
|
||||
unsigned long last_recv = _radio->getLastRecvMillis();
|
||||
unsigned long last_irq = _radio->getLastRadioInterruptMillis();
|
||||
unsigned long last_active = (last_recv > last_irq ? last_recv : last_irq);
|
||||
if (last_radio_active_ms > last_active) last_active = last_radio_active_ms;
|
||||
if (is_recv && last_active > 0) {
|
||||
unsigned long silent_ms = _ms->getMillis() - last_active;
|
||||
unsigned long since_recovery = _ms->getMillis() - last_watchdog_recovery;
|
||||
if (silent_ms > watchdog_ms && since_recovery > watchdog_ms) {
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: silent %lu ms, state=%d, recovering", silent_ms, _radio->getRadioState());
|
||||
_radio->idle();
|
||||
_radio->startRecv();
|
||||
last_watchdog_recovery = _ms->getMillis();
|
||||
}
|
||||
const unsigned long now = _ms->getMillis();
|
||||
unsigned long silent_ms = now - last_radio_active_ms;
|
||||
const unsigned long last_recv = _radio->getLastRecvMillis();
|
||||
const unsigned long last_irq = _radio->getLastRadioInterruptMillis();
|
||||
if (last_recv != 0 && now - last_recv < silent_ms) silent_ms = now - last_recv;
|
||||
if (last_irq != 0 && now - last_irq < silent_ms) silent_ms = now - last_irq;
|
||||
const unsigned long since_recovery = now - last_watchdog_recovery;
|
||||
if (is_recv && silent_ms >= watchdog_ms && since_recovery >= watchdog_ms) {
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: silent %lu ms, state=%d, recovering", silent_ms, _radio->getRadioState());
|
||||
_radio->idle();
|
||||
_radio->startRecv();
|
||||
last_watchdog_recovery = now;
|
||||
last_radio_active_ms = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +364,15 @@ void Dispatcher::checkSend() {
|
||||
|
||||
outbound = _mgr->getNextOutbound(_ms->getMillis());
|
||||
if (outbound) {
|
||||
if (!allowPacketTransmit(outbound)) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): packet no longer allowed, type=%u", getLogDateTime(),
|
||||
(uint32_t)outbound->getPayloadType());
|
||||
onSendFail(outbound);
|
||||
releasePacket(outbound);
|
||||
outbound = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
int len = 0;
|
||||
uint8_t raw[MAX_TRANS_UNIT];
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ protected:
|
||||
virtual uint32_t getCADFailRetryDelay() const;
|
||||
virtual uint32_t getCADFailMaxDuration() const;
|
||||
virtual uint8_t getDefaultTxCodingRate() const { return 0; }
|
||||
virtual bool allowPacketTransmit(const Packet* packet) const { return true; }
|
||||
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
|
||||
virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default
|
||||
virtual int getAGCResetInterval() const { return 0; } // disabled by default
|
||||
|
||||
+3
-1
@@ -11,6 +11,7 @@ Identity::Identity() {
|
||||
}
|
||||
|
||||
Identity::Identity(const char* pub_hex) {
|
||||
memset(pub_key, 0, sizeof(pub_key));
|
||||
Utils::fromHex(pub_key, PUB_KEY_SIZE, pub_hex);
|
||||
}
|
||||
|
||||
@@ -39,6 +40,7 @@ LocalIdentity::LocalIdentity() {
|
||||
memset(prv_key, 0, sizeof(prv_key));
|
||||
}
|
||||
LocalIdentity::LocalIdentity(const char* prv_hex, const char* pub_hex) : Identity(pub_hex) {
|
||||
memset(prv_key, 0, sizeof(prv_key));
|
||||
Utils::fromHex(prv_key, PRV_KEY_SIZE, prv_hex);
|
||||
}
|
||||
|
||||
@@ -140,4 +142,4 @@ void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_k
|
||||
ed25519_key_exchange(secret, other_pub_key, prv_key);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+60
-19
@@ -165,6 +165,7 @@ void Mesh::begin() {
|
||||
_flood_retries[i].retry_at = 0;
|
||||
_flood_retries[i].retry_delay = 0;
|
||||
_flood_retries[i].retry_attempts_sent = 0;
|
||||
memset(_flood_retries[i].retry_key, 0, sizeof(_flood_retries[i].retry_key));
|
||||
_flood_retries[i].priority = 0;
|
||||
_flood_retries[i].progress_marker = 0;
|
||||
_flood_retries[i].waiting_final_echo = false;
|
||||
@@ -221,6 +222,18 @@ void Mesh::loop() {
|
||||
if (_direct_retries[i].packet == getOutboundInFlight()) {
|
||||
continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event
|
||||
}
|
||||
uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0
|
||||
? 0
|
||||
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
|
||||
onDirectRetryEvent("dropped_queue_removed", NULL, elapsed_millis,
|
||||
_direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len,
|
||||
_direct_retries[i].payload_type);
|
||||
onDirectRetryEvent("failure", NULL, elapsed_millis,
|
||||
_direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len,
|
||||
_direct_retries[i].payload_type);
|
||||
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
}
|
||||
@@ -252,6 +265,13 @@ void Mesh::loop() {
|
||||
if (_flood_retries[i].packet == getOutboundInFlight()) {
|
||||
continue;
|
||||
}
|
||||
uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0
|
||||
? 0
|
||||
: (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at);
|
||||
onFloodRetryEvent("dropped_queue_removed", NULL, elapsed_millis,
|
||||
_flood_retries[i].retry_attempts_sent + 1);
|
||||
onFloodRetryEvent("failure", NULL, elapsed_millis,
|
||||
_flood_retries[i].retry_attempts_sent + 1);
|
||||
clearFloodRetrySlot(i);
|
||||
}
|
||||
}
|
||||
@@ -333,6 +353,17 @@ void Mesh::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Mesh::allowPacketTransmit(const Packet* packet) const {
|
||||
#if defined(ENABLE_OTA)
|
||||
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA) {
|
||||
return isTempRadioActive();
|
||||
}
|
||||
#else
|
||||
(void)packet;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Mesh::allowPacketForward(const mesh::Packet* packet) {
|
||||
return false; // by default, Transport NOT enabled
|
||||
}
|
||||
@@ -1250,6 +1281,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
}
|
||||
|
||||
void Mesh::clearFloodRetrySlot(int idx) {
|
||||
if (_flood_retries[idx].active) {
|
||||
onFloodRetrySlotReleased(_flood_retries[idx].retry_key);
|
||||
}
|
||||
if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) {
|
||||
releasePacket(_flood_retries[idx].packet);
|
||||
}
|
||||
@@ -1259,6 +1293,7 @@ void Mesh::clearFloodRetrySlot(int idx) {
|
||||
_flood_retries[idx].retry_at = 0;
|
||||
_flood_retries[idx].retry_delay = 0;
|
||||
_flood_retries[idx].retry_attempts_sent = 0;
|
||||
memset(_flood_retries[idx].retry_key, 0, sizeof(_flood_retries[idx].retry_key));
|
||||
_flood_retries[idx].priority = 0;
|
||||
_flood_retries[idx].progress_marker = 0;
|
||||
_flood_retries[idx].waiting_final_echo = false;
|
||||
@@ -1342,16 +1377,9 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
|
||||
max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX;
|
||||
}
|
||||
if (_flood_retries[i].retry_attempts_sent >= max_attempts) {
|
||||
Packet* final_wait = obtainNewPacket();
|
||||
if (final_wait == NULL) {
|
||||
onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
|
||||
onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
|
||||
clearFloodRetrySlot(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
*final_wait = *packet;
|
||||
_flood_retries[i].packet = final_wait;
|
||||
// Dispatcher releases the transmitted packet after this hook. Keep only
|
||||
// retry metadata during the final echo window so RX retains the pool slot.
|
||||
_flood_retries[i].packet = NULL;
|
||||
_flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay);
|
||||
_flood_retries[i].waiting_final_echo = true;
|
||||
_flood_retries[i].queued = false;
|
||||
@@ -1455,6 +1483,15 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t retry_key[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(retry_key);
|
||||
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
|
||||
if (_flood_retries[i].active
|
||||
&& memcmp(retry_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
|
||||
return; // the normal flood still sends, but only one retry sequence owns this logical packet
|
||||
}
|
||||
}
|
||||
|
||||
int slot_idx = -1;
|
||||
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
|
||||
if (!_flood_retries[i].active) {
|
||||
@@ -1473,7 +1510,7 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
|
||||
}
|
||||
|
||||
uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0);
|
||||
packet->calculatePacketHash(_flood_retries[slot_idx].retry_key);
|
||||
memcpy(_flood_retries[slot_idx].retry_key, retry_key, sizeof(retry_key));
|
||||
_flood_retries[slot_idx].packet = NULL;
|
||||
_flood_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
|
||||
_flood_retries[slot_idx].retry_started_at = 0;
|
||||
@@ -1755,14 +1792,16 @@ void Mesh::sendOtaFlood(Packet* packet, uint32_t delay_millis) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||
bool Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
|
||||
return;
|
||||
releasePacket(packet);
|
||||
return false;
|
||||
}
|
||||
if (path_hash_size == 0 || path_hash_size > 3) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
|
||||
return;
|
||||
releasePacket(packet);
|
||||
return false;
|
||||
}
|
||||
|
||||
packet->header &= ~PH_ROUTE_MASK;
|
||||
@@ -1780,17 +1819,19 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si
|
||||
pri = 1;
|
||||
}
|
||||
maybeScheduleFloodRetry(packet, pri);
|
||||
sendPacket(packet, pri, delay_millis);
|
||||
return sendPacket(packet, pri, delay_millis);
|
||||
}
|
||||
|
||||
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||
bool Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
|
||||
return;
|
||||
releasePacket(packet);
|
||||
return false;
|
||||
}
|
||||
if (path_hash_size == 0 || path_hash_size > 3) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
|
||||
return;
|
||||
releasePacket(packet);
|
||||
return false;
|
||||
}
|
||||
|
||||
packet->header &= ~PH_ROUTE_MASK;
|
||||
@@ -1810,7 +1851,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m
|
||||
pri = 1;
|
||||
}
|
||||
maybeScheduleFloodRetry(packet, pri);
|
||||
sendPacket(packet, pri, delay_millis);
|
||||
return sendPacket(packet, pri, delay_millis);
|
||||
}
|
||||
|
||||
void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) {
|
||||
|
||||
+9
-2
@@ -117,6 +117,7 @@ protected:
|
||||
DispatcherAction onRecvPacket(Packet* pkt) override;
|
||||
void onSendComplete(Packet* packet) override;
|
||||
void onSendFail(Packet* packet) override;
|
||||
bool allowPacketTransmit(const Packet* packet) const override;
|
||||
|
||||
virtual uint32_t getCADFailRetryDelay() const override;
|
||||
|
||||
@@ -215,9 +216,15 @@ protected:
|
||||
|
||||
/**
|
||||
* \brief Optional hook for logging flood-retry lifecycle events.
|
||||
* packet is null when the retained packet has already been released.
|
||||
*/
|
||||
virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
|
||||
|
||||
/**
|
||||
* \brief Called exactly once whenever an active flood-retry slot is released.
|
||||
*/
|
||||
virtual void onFloodRetrySlotReleased(const uint8_t* retry_key) { }
|
||||
|
||||
/**
|
||||
* \returns number of extra (Direct) ACK transmissions wanted.
|
||||
*/
|
||||
@@ -409,13 +416,13 @@ public:
|
||||
/**
|
||||
* \brief send a locally-generated Packet with flood routing
|
||||
*/
|
||||
void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
|
||||
bool sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
|
||||
|
||||
/**
|
||||
* \brief send a locally-generated Packet with flood routing
|
||||
* \param transport_codes array of 2 codes to attach to packet
|
||||
*/
|
||||
void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
|
||||
bool sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
|
||||
|
||||
/**
|
||||
* \brief send a locally-generated Packet with Direct routing
|
||||
|
||||
+3
-1
@@ -120,6 +120,7 @@ bool Utils::isHexChar(char c) {
|
||||
}
|
||||
|
||||
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
|
||||
if (dest == NULL || src_hex == NULL || dest_size < 0) return false;
|
||||
int len = strlen(src_hex);
|
||||
if (len != dest_size*2) return false; // incorrect length
|
||||
|
||||
@@ -127,6 +128,7 @@ bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
|
||||
while (dp - dest < dest_size) {
|
||||
char ch = *src_hex++;
|
||||
char cl = *src_hex++;
|
||||
if (!isHexChar(ch) || !isHexChar(cl)) return false;
|
||||
*dp++ = (hexVal(ch) << 4) | hexVal(cl);
|
||||
}
|
||||
return true;
|
||||
@@ -150,4 +152,4 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep
|
||||
return num;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -178,9 +178,9 @@ bool AlertReporter::sendChannel(const char* text) {
|
||||
uint16_t codes[2];
|
||||
codes[0] = scope.calcTransportCode(pkt);
|
||||
codes[1] = 0;
|
||||
_mesh->sendFlood(pkt, codes, 0, path_hash_size);
|
||||
if (!_mesh->sendFlood(pkt, codes, 0, path_hash_size)) return false;
|
||||
} else {
|
||||
_mesh->sendFlood(pkt, 0, path_hash_size);
|
||||
if (!_mesh->sendFlood(pkt, 0, path_hash_size)) return false;
|
||||
}
|
||||
ALERT_DEBUG_PRINTLN("sent: %s", text);
|
||||
return true;
|
||||
@@ -232,7 +232,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
|
||||
if (_wifi.state == OK) {
|
||||
if (wifi_down && down_ms >= thresh_ms &&
|
||||
(now_ms - _wifi.fired_at_ms) >= min_interval_ms) {
|
||||
(_wifi.fired_at_ms == 0 || (now_ms - _wifi.fired_at_ms) >= min_interval_ms)) {
|
||||
char age[16];
|
||||
formatAge(down_ms, age, sizeof(age));
|
||||
uint8_t reason = MQTTBridge::getLastWifiDisconnectReason();
|
||||
@@ -256,8 +256,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
formatAge(total, age, sizeof(age));
|
||||
char text[80];
|
||||
snprintf(text, sizeof(text), "WiFi recovered after %s", age);
|
||||
sendChannel(text);
|
||||
_wifi.state = OK;
|
||||
if (sendChannel(text)) _wifi.state = OK;
|
||||
}
|
||||
}
|
||||
} else if (_wifi.state == FIRING) {
|
||||
@@ -282,7 +281,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
|
||||
if (f.state == OK) {
|
||||
if (down && down_ms >= thresh_ms &&
|
||||
(now_ms - f.fired_at_ms) >= min_interval_ms) {
|
||||
(f.fired_at_ms == 0 || (now_ms - f.fired_at_ms) >= min_interval_ms)) {
|
||||
char age[16];
|
||||
formatAge(down_ms, age, sizeof(age));
|
||||
char text[100];
|
||||
@@ -303,8 +302,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
char text[100];
|
||||
snprintf(text, sizeof(text), "MQTT slot %d (%s) recovered after %s",
|
||||
i + 1, _bridge->getSlotPresetName(i), age);
|
||||
sendChannel(text);
|
||||
f.state = OK;
|
||||
if (sendChannel(text)) f.state = OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ bool ClientACL::clear() {
|
||||
}
|
||||
|
||||
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
|
||||
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return NULL;
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
|
||||
}
|
||||
@@ -106,7 +107,7 @@ ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
|
||||
|
||||
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
uint32_t min_time = 0xFFFFFFFF;
|
||||
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
|
||||
ClientInfo* oldest = NULL;
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (id.matches(clients[i].id)) return &clients[i]; // already known
|
||||
if ( (!clients[i].isAdmin() && !clients[i].isRegionMgr())
|
||||
@@ -120,6 +121,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
c = &clients[num_clients++];
|
||||
} else {
|
||||
if (oldest == NULL) return NULL; // every entry is protected
|
||||
c = oldest; // evict least active contact
|
||||
}
|
||||
memset(c, 0, sizeof(*c));
|
||||
@@ -131,6 +133,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
}
|
||||
|
||||
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
|
||||
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return false;
|
||||
ClientInfo* c;
|
||||
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
|
||||
c = getClient(pubkey, key_len);
|
||||
@@ -143,10 +146,11 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
|
||||
if (key_len != PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
|
||||
|
||||
mesh::Identity id(pubkey);
|
||||
c = putClient(id, 0);
|
||||
if (c == NULL) return false;
|
||||
|
||||
c->permissions = perms; // update their permissions
|
||||
self_id.calcSharedSecret(c->shared_secret, pubkey);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
|
||||
#include <RTClib.h>
|
||||
#include <Utils.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <nrf.h>
|
||||
@@ -1048,6 +1049,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->battery_alert_enabled = 0;
|
||||
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
||||
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
||||
memset(_prefs->battery_alert_region, 0, sizeof(_prefs->battery_alert_region));
|
||||
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
||||
_prefs->flood_channel_data_enabled = 1;
|
||||
_prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
@@ -1151,6 +1153,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
file.read((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level));
|
||||
file.read((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->battery_alert_region)) {
|
||||
file.read((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region));
|
||||
_prefs->battery_alert_region[sizeof(_prefs->battery_alert_region) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// sanitise bad pref values
|
||||
@@ -1369,7 +1375,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 816
|
||||
file.write((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 820
|
||||
file.write((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 821
|
||||
// next: 822
|
||||
file.write((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region)); // 822
|
||||
// next: 853
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -1422,12 +1429,15 @@ void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) {
|
||||
// hadn't appended a tail field) leaves the trailing fields at their
|
||||
// defaults, and a longer one (a future append) is truncated harmlessly.
|
||||
size_t payload_avail = file_size - sizeof(hdr);
|
||||
if (hdr.payload_len < payload_avail) payload_avail = hdr.payload_len;
|
||||
size_t to_read = payload_avail < sizeof(_mqtt_prefs) ? payload_avail : sizeof(_mqtt_prefs);
|
||||
size_t got = file.read((uint8_t *)&_mqtt_prefs, to_read);
|
||||
if (got != to_read) {
|
||||
setMQTTPrefsDefaults(&_mqtt_prefs);
|
||||
} else {
|
||||
has_observer_fields = true; // observer tail is part of the v1 payload
|
||||
const size_t observer_tail_end = offsetof(MQTTPrefs, alert_region)
|
||||
+ sizeof(_mqtt_prefs.alert_region);
|
||||
has_observer_fields = to_read >= observer_tail_end;
|
||||
}
|
||||
} else {
|
||||
// Unknown (newer) version: don't risk misreading a layout we don't know.
|
||||
@@ -1775,9 +1785,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
|
||||
const char* hex = &command[16];
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
|
||||
int pubkey_len = hex_len / 2;
|
||||
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
|
||||
size_t hex_len = strlen(hex);
|
||||
int pubkey_len = (int)(hex_len / 2);
|
||||
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
|
||||
&& mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
|
||||
_callbacks->removeNeighbor(pubkey, pubkey_len);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
@@ -2781,10 +2792,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
strcpy(reply, "Error: channel must be between 1-14");
|
||||
}
|
||||
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
|
||||
StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
|
||||
_callbacks->restartBridge();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
const char* secret = &config[14];
|
||||
if (secret[0] == 0 || strlen(secret) >= sizeof(_prefs->bridge_secret)) {
|
||||
sprintf(reply, "Error: secret must be 1-%u characters", (unsigned)(sizeof(_prefs->bridge_secret) - 1));
|
||||
} else {
|
||||
StrHelper::strncpy(_prefs->bridge_secret, secret, sizeof(_prefs->bridge_secret));
|
||||
_callbacks->restartBridge();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
#endif
|
||||
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
|
||||
_prefs->adc_multiplier = atof(&config[15]);
|
||||
|
||||
@@ -214,6 +214,7 @@ struct NodePrefs { // persisted to file
|
||||
uint32_t rx_ps_sleep_us;
|
||||
uint8_t rx_ps_level; // 0 = manual/explicit us timings; 1..10 = level-derived (auto-retunes on SF/BW change)
|
||||
uint8_t rx_ps_preamble; // 0 = auto (derive from SF); else 16 or 32 = explicit override for level calc
|
||||
char battery_alert_region[31]; // named scope for low-battery floods; empty = no alert scope
|
||||
};
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
+90
-25
@@ -79,36 +79,100 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
|
||||
|
||||
if (file) {
|
||||
uint8_t pad[128];
|
||||
|
||||
num_regions = 0; next_id = 1;
|
||||
default_id = home_id = 0;
|
||||
RegionEntry loaded[MAX_REGION_ENTRIES];
|
||||
uint16_t loaded_count = 0;
|
||||
uint16_t loaded_default = 0;
|
||||
uint16_t loaded_home = 0;
|
||||
uint16_t loaded_next = 1;
|
||||
uint8_t loaded_wildcard_flags = 0;
|
||||
|
||||
bool success = file.read(pad, 3) == 3; // reserved header
|
||||
success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
|
||||
success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
|
||||
success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
|
||||
success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
|
||||
success = success && file.read((uint8_t *) &loaded_default, sizeof(loaded_default)) == sizeof(loaded_default);
|
||||
success = success && file.read((uint8_t *) &loaded_home, sizeof(loaded_home)) == sizeof(loaded_home);
|
||||
success = success && file.read((uint8_t *) &loaded_wildcard_flags, sizeof(loaded_wildcard_flags)) == sizeof(loaded_wildcard_flags);
|
||||
success = success && file.read((uint8_t *) &loaded_next, sizeof(loaded_next)) == sizeof(loaded_next);
|
||||
|
||||
if (success) {
|
||||
while (num_regions < MAX_REGION_ENTRIES) {
|
||||
auto r = ®ions[num_regions];
|
||||
|
||||
success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
|
||||
success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
|
||||
success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
|
||||
success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
|
||||
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
|
||||
|
||||
if (!success) break; // EOF
|
||||
|
||||
if (r->id >= next_id) { // make sure next_id is valid
|
||||
next_id = r->id + 1;
|
||||
}
|
||||
num_regions++;
|
||||
while (success && file.available() > 0) {
|
||||
if (loaded_count >= MAX_REGION_ENTRIES) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
RegionEntry& r = loaded[loaded_count];
|
||||
memset(&r, 0, sizeof(r));
|
||||
success = file.read((uint8_t *) &r.id, sizeof(r.id)) == sizeof(r.id);
|
||||
success = success && file.read((uint8_t *) &r.parent, sizeof(r.parent)) == sizeof(r.parent);
|
||||
success = success && file.read((uint8_t *) r.name, sizeof(r.name)) == sizeof(r.name);
|
||||
success = success && file.read((uint8_t *) &r.flags, sizeof(r.flags)) == sizeof(r.flags);
|
||||
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
|
||||
if (!success) break;
|
||||
|
||||
const char* terminator = (const char*)memchr(r.name, 0, sizeof(r.name));
|
||||
if (r.id == 0 || r.id == 0xFFFF || terminator == NULL || r.name[0] == 0) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
for (const char* p = r.name; *p; p++) {
|
||||
if (!is_name_char((uint8_t)*p)) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) break;
|
||||
loaded_count++;
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
|
||||
// Validate IDs and parent links only after every record is present so forward
|
||||
// parent references are allowed. A bounded walk also rejects cycles.
|
||||
for (uint16_t i = 0; success && i < loaded_count; i++) {
|
||||
for (uint16_t j = i + 1; j < loaded_count; j++) {
|
||||
if (loaded[i].id == loaded[j].id) success = false;
|
||||
}
|
||||
uint16_t parent = loaded[i].parent;
|
||||
for (uint16_t depth = 0; success && parent != 0; depth++) {
|
||||
if (depth >= loaded_count || parent == loaded[i].id) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
const RegionEntry* found = NULL;
|
||||
for (uint16_t j = 0; j < loaded_count; j++) {
|
||||
if (loaded[j].id == parent) {
|
||||
found = &loaded[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == NULL) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
parent = found->parent;
|
||||
}
|
||||
}
|
||||
|
||||
bool default_found = loaded_default == 0;
|
||||
bool home_found = loaded_home == 0;
|
||||
uint16_t minimum_next = 1;
|
||||
for (uint16_t i = 0; success && i < loaded_count; i++) {
|
||||
if (loaded[i].id == loaded_default) default_found = true;
|
||||
if (loaded[i].id == loaded_home) home_found = true;
|
||||
if (loaded[i].id >= minimum_next) minimum_next = loaded[i].id + 1;
|
||||
}
|
||||
if (!default_found || !home_found) success = false;
|
||||
if (loaded_next == 0 || loaded_next == 0xFFFF || loaded_next < minimum_next) loaded_next = minimum_next;
|
||||
if (loaded_next == 0xFFFF && loaded_count < MAX_REGION_ENTRIES) success = false;
|
||||
|
||||
if (success) {
|
||||
memcpy(regions, loaded, loaded_count * sizeof(RegionEntry));
|
||||
if (loaded_count < MAX_REGION_ENTRIES) {
|
||||
memset(®ions[loaded_count], 0, (MAX_REGION_ENTRIES - loaded_count) * sizeof(RegionEntry));
|
||||
}
|
||||
num_regions = loaded_count;
|
||||
default_id = loaded_default;
|
||||
home_id = loaded_home;
|
||||
next_id = loaded_next;
|
||||
wildcard.flags = loaded_wildcard_flags;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
return false; // failed
|
||||
@@ -157,7 +221,8 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t
|
||||
|
||||
region->parent = parent_id; // re-parent / move this region in the hierarchy
|
||||
} else {
|
||||
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
|
||||
if (num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
|
||||
if (id == 0xFFFF || (id == 0 && (next_id == 0 || next_id == 0xFFFF))) return NULL;
|
||||
|
||||
region = ®ions[num_regions++]; // alloc new RegionEntry
|
||||
region->flags = REGION_DENY_FLOOD; // DENY by default
|
||||
|
||||
@@ -38,24 +38,34 @@ ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTC
|
||||
void ESPNowBridge::begin() {
|
||||
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
|
||||
|
||||
if (_initialized) return;
|
||||
|
||||
// Initialize WiFi in station mode
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
// Set Wi-Fi channel
|
||||
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize ESP-NOW
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
|
||||
WiFi.mode(WIFI_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register callbacks
|
||||
esp_now_register_recv_cb(recv_cb);
|
||||
esp_now_register_send_cb(send_cb);
|
||||
if (esp_now_register_recv_cb(recv_cb) != ESP_OK || esp_now_register_send_cb(send_cb) != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("Error registering ESP-NOW callbacks\n");
|
||||
esp_now_register_recv_cb(nullptr);
|
||||
esp_now_register_send_cb(nullptr);
|
||||
esp_now_deinit();
|
||||
WiFi.mode(WIFI_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add broadcast peer
|
||||
esp_now_peer_info_t peerInfo = {};
|
||||
@@ -66,6 +76,10 @@ void ESPNowBridge::begin() {
|
||||
|
||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
|
||||
esp_now_register_recv_cb(nullptr);
|
||||
esp_now_register_send_cb(nullptr);
|
||||
esp_now_deinit();
|
||||
WiFi.mode(WIFI_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,11 +116,17 @@ void ESPNowBridge::loop() {
|
||||
// Nothing to do here - ESP-NOW is callback based
|
||||
}
|
||||
|
||||
void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
|
||||
size_t keyLen = strlen(_prefs->bridge_secret);
|
||||
bool ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
|
||||
size_t keyLen = 0;
|
||||
while (keyLen < sizeof(_prefs->bridge_secret) && _prefs->bridge_secret[keyLen] != 0) keyLen++;
|
||||
if (keyLen == 0 || keyLen == sizeof(_prefs->bridge_secret)) {
|
||||
BRIDGE_DEBUG_PRINTLN("Invalid empty or unterminated bridge secret\n");
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
data[i] ^= _prefs->bridge_secret[i % keyLen];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
|
||||
@@ -135,7 +155,7 @@ void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int len)
|
||||
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
|
||||
|
||||
// Try to decrypt (checksum + payload)
|
||||
xorCrypt(decrypted, encryptedDataLen);
|
||||
if (!xorCrypt(decrypted, encryptedDataLen)) return;
|
||||
|
||||
// Validate checksum
|
||||
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
|
||||
@@ -178,14 +198,19 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
|
||||
|
||||
if (!_seen_packets.wasSeen(packet)) {
|
||||
_seen_packets.markSeen(packet);
|
||||
// Create a temporary buffer just for size calculation and reuse for actual writing
|
||||
// Check the serialized size before writing into the ESP-NOW-sized buffer.
|
||||
const int expectedMeshPacketLen = packet->getRawLength();
|
||||
if (expectedMeshPacketLen < 0 || (size_t)expectedMeshPacketLen > MAX_PAYLOAD_SIZE) {
|
||||
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", expectedMeshPacketLen,
|
||||
MAX_PAYLOAD_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
|
||||
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
|
||||
|
||||
// Check if packet fits within our maximum payload size
|
||||
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
|
||||
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen,
|
||||
MAX_PAYLOAD_SIZE);
|
||||
if (meshPacketLen != (uint16_t)expectedMeshPacketLen) {
|
||||
BRIDGE_DEBUG_PRINTLN("TX packet length mismatch (actual=%d, expected=%d)\n", meshPacketLen,
|
||||
expectedMeshPacketLen);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,7 +230,7 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
|
||||
buffer[3] = checksum & 0xFF; // Low byte
|
||||
|
||||
// Encrypt payload and checksum (not including magic header)
|
||||
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
|
||||
if (!xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE)) return;
|
||||
|
||||
// Total packet size: magic header + checksum + payload
|
||||
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
|
||||
|
||||
@@ -85,7 +85,7 @@ private:
|
||||
* @param data Pointer to data to encrypt/decrypt
|
||||
* @param len Length of data in bytes
|
||||
*/
|
||||
void xorCrypt(uint8_t *data, size_t len);
|
||||
bool xorCrypt(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* ESP-NOW receive callback
|
||||
|
||||
@@ -38,9 +38,15 @@ public:
|
||||
* These two functions do nothing for ESP-NOW, but are needed for the
|
||||
* Radio interface.
|
||||
*/
|
||||
virtual bool setRxBoostedGainMode(bool) { }
|
||||
virtual bool setRxBoostedGainMode(bool) { return false; }
|
||||
virtual bool getRxBoostedGainMode() const { return false; }
|
||||
|
||||
// ESP-NOW has no LoRa RX power-saving watchdog or noise-floor calibration.
|
||||
uint32_t getRxPsWatchdogSoftCount() const { return 0; }
|
||||
uint32_t getRxPsWatchdogHardCount() const { return 0; }
|
||||
bool isWatchdogObserving() const { return false; }
|
||||
bool isCalibratingNoiseFloor() const { return false; }
|
||||
|
||||
uint32_t intID();
|
||||
void setTxPower(uint8_t dbm);
|
||||
};
|
||||
|
||||
@@ -434,6 +434,7 @@ void OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate)
|
||||
memcpy(_fid, mid, 4);
|
||||
_fstate = WANT_MANIFEST;
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0; _mf_retries = 0; _loop_last_mfmask = 0; // fresh manifest reassembly
|
||||
memset(_mf_buf, 0, sizeof(_mf_buf));
|
||||
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4); gm.want_mask = 0xFFFF; // first ask: send all fragments
|
||||
uint8_t b[16];
|
||||
emit(b, encode_get_manifest(b, sizeof(b), gm), false);
|
||||
@@ -445,10 +446,21 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
if (_fstate != WANT_MANIFEST || memcmp(mm.manifest_id, _fid, 4) != 0) return;
|
||||
if (mm.frag_total == 0 || mm.frag_total > OTA_MF_MAXFRAG || mm.frag_idx >= mm.frag_total) return;
|
||||
|
||||
const uint8_t expected_total = (uint8_t)((MOTA_MFL + OTA_MF_FRAG - 1) / OTA_MF_FRAG);
|
||||
const uint16_t expected_len = mm.frag_idx + 1 == expected_total
|
||||
? (uint16_t)(MOTA_MFL - (uint32_t)mm.frag_idx * OTA_MF_FRAG)
|
||||
: (uint16_t)OTA_MF_FRAG;
|
||||
if (mm.frag_total != expected_total || mm.len != expected_len) return;
|
||||
|
||||
// reassemble the (possibly multi-fragment) manifest into _mf_buf; place fragment frag_idx at its offset
|
||||
uint32_t foff = (uint32_t)mm.frag_idx * OTA_MF_FRAG;
|
||||
if (foff + mm.len > sizeof(_mf_buf)) return;
|
||||
if (mm.frag_total != _mf_total) { _mf_total = mm.frag_total; _mf_mask = 0; _mf_len = 0; } // (re)start
|
||||
if (mm.frag_total != _mf_total) {
|
||||
_mf_total = mm.frag_total;
|
||||
_mf_mask = 0;
|
||||
_mf_len = 0;
|
||||
memset(_mf_buf, 0, sizeof(_mf_buf));
|
||||
}
|
||||
memcpy(_mf_buf + foff, mm.bytes, mm.len);
|
||||
_mf_mask |= (uint16_t)(1u << mm.frag_idx);
|
||||
if (mm.frag_idx == mm.frag_total - 1) _mf_len = foff + mm.len; // last fragment fixes the length
|
||||
@@ -461,6 +473,7 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
if (!codecOk(mf[56])) { _fstate = IDLE; return; } // codec we can't apply (lying/stale ADV) — abort
|
||||
uint32_t payload_size = rd_u32le(mf + 15);
|
||||
uint8_t bsl = mf[19];
|
||||
if (bsl >= 32) { _fstate = FAILED; return; }
|
||||
uint32_t bs = 1u << bsl;
|
||||
// a block must fit our reassembly buffer (and be non-empty) — reject an oversized block_size up front
|
||||
if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { _fstate = FAILED; return; }
|
||||
@@ -534,7 +547,9 @@ void OtaManager::handleLeaves(const uint8_t* m, uint16_t n) {
|
||||
uint8_t ftotal = (uint8_t)((leaves_len + OTA_LEAVES_FRAG - 1) / OTA_LEAVES_FRAG); if (ftotal == 0) ftotal = 1;
|
||||
if (lv.frag_total != ftotal || lv.frag_idx >= ftotal) return;
|
||||
uint32_t foff = (uint32_t)lv.frag_idx * OTA_LEAVES_FRAG;
|
||||
if (foff + lv.len > leaves_len) return; // slice out of range
|
||||
uint32_t expected_len = leaves_len - foff;
|
||||
if (expected_len > OTA_LEAVES_FRAG) expected_len = OTA_LEAVES_FRAG;
|
||||
if (lv.len != expected_len) return; // reject short/oversized slices
|
||||
if (lv.frag_total != _lv_total) { _lv_total = lv.frag_total; _lv_mask = 0; } // (re)start
|
||||
memcpy(_leaves_buf + foff, lv.bytes, lv.len);
|
||||
_lv_mask |= (uint16_t)(1u << lv.frag_idx);
|
||||
|
||||
@@ -203,9 +203,8 @@ void OtaStoreFlashEsp32::checkpoint() {
|
||||
bool OtaStoreFlashEsp32::reopen() {
|
||||
if (!acquire() || _psize < SEC) return false;
|
||||
uint8_t hb[8];
|
||||
// the meta/container is staged at the BOTTOM of the slot, so scan upward from there; cap the scan (a
|
||||
// miss just means "fetch fresh"). 128 sectors (512 KB) covers any full image's meta + typical deltas.
|
||||
uint32_t scanned = 0;
|
||||
// The meta/container is staged at the bottom of the slot. Large delta containers can begin well above
|
||||
// the final 512 KB, so scan the complete partition rather than silently losing resumability after reboot.
|
||||
for (uint32_t o = align_down(_psize - SEC, SEC); ; o -= SEC) {
|
||||
if (esp_partition_read(_part, o, hb, 8) == ESP_OK && memcmp(hb, MOTA_MAGIC, 4) == 0) {
|
||||
uint32_t total = rd_u32le(hb + 4);
|
||||
@@ -232,7 +231,7 @@ bool OtaStoreFlashEsp32::reopen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (o == 0 || ++scanned >= 128) break;
|
||||
if (o == 0) break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user