Fix firmware matrix build failures

This commit is contained in:
mikecarper
2026-07-30 01:18:21 -07:00
parent 07c968cb54
commit c19f248d97
14 changed files with 196 additions and 86 deletions
+1 -1
View File
@@ -402,7 +402,7 @@ static mesh::RadioParamApplyResult applyFixedRadioParams(float freq, float bw, u
#endif
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
+19 -13
View File
@@ -108,6 +108,12 @@
#define FLOOD_PACKET_FILTER_LOGIN_PROTECTED_HOPS 7
#define FLOOD_PACKET_FILTER_TXT_MSG_PROTECTED_HOPS 5
#define DEFAULT_FLOOD_CHANNEL_BLOCK_NAME "#wardriving"
static const char FLOOD_PACKET_FILTER_USAGE[] =
"Err - use: set flood.filter[.n] <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]";
static const char FLOOD_PACKET_FILTER_DUPLICATE[] =
"Err - duplicate filter option";
static const char FLOOD_CHANNEL_SCOPE_USAGE[] =
"Err - use: set flood.channel.scope[.n] <match> <region> [path=blacklist|path=bucket:1-6] [tx=slow]";
#ifndef DEFAULT_FLOOD_CHANNEL_BLOCK_HOPS
#define DEFAULT_FLOOD_CHANNEL_BLOCK_HOPS 4
#endif
@@ -1025,7 +1031,7 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
}
bool MyMesh::evaluateScopeRewriteTiming(const mesh::Packet* packet,
@@ -1091,7 +1097,7 @@ int MyMesh::calcRxDelayForPacket(const mesh::Packet* packet, float score,
float slow_base =
FloodFilterPolicy::slowScopeRxDelayBase(_prefs.rx_delay_base);
return (int)((pow(slow_base, 0.85f - score) - 1.0f) * air_time);
return (int)((powf(slow_base, 0.85f - score) - 1.0f) * air_time);
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
@@ -5047,7 +5053,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply) {
}
requested_slot = slot - 1;
if (*cursor != ' ') {
strcpy(reply, "Err - expected packet type");
strcpy(reply, "Err - packet type must be name, any, 0-15, or 0x00-0x0F");
return;
}
}
@@ -5075,7 +5081,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply) {
token = separator;
}
if (token_count == 0) {
strcpy(reply, "Err - use: set flood.filter[.n] <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]");
strcpy(reply, FLOOD_PACKET_FILTER_USAGE);
return;
}
@@ -5098,25 +5104,25 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply) {
for (int i = 1; i < token_count; i++) {
if (floodFilterAsciiEqual(tokens[i], "suspend=tempradio")) {
if (suspend_on_temp_radio) {
strcpy(reply, "Err - duplicate suspend=tempradio");
strcpy(reply, FLOOD_PACKET_FILTER_DUPLICATE);
return;
}
suspend_on_temp_radio = true;
} else if (floodFilterAsciiEqual(tokens[i], "path=blacklist")) {
if (match_blacklisted_path) {
strcpy(reply, "Err - duplicate path=blacklist");
strcpy(reply, FLOOD_PACKET_FILTER_DUPLICATE);
return;
}
match_blacklisted_path = true;
} else if (floodFilterAsciiEqual(tokens[i], "require=region")) {
if (scope_requires_region_match) {
strcpy(reply, "Err - duplicate require=region");
strcpy(reply, FLOOD_PACKET_FILTER_DUPLICATE);
return;
}
scope_requires_region_match = true;
} else if (floodFilterAsciiStartsWith(tokens[i], "tx=")) {
if (scope_timing_set) {
strcpy(reply, "Err - duplicate tx timing");
strcpy(reply, FLOOD_PACKET_FILTER_DUPLICATE);
return;
}
if (floodFilterAsciiEqual(tokens[i], "tx=slow")) {
@@ -5128,7 +5134,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply) {
scope_timing_set = true;
} else if (floodFilterAsciiStartsWith(tokens[i], "scope=")) {
if (scope_name[0] != 0) {
strcpy(reply, "Err - duplicate scope");
strcpy(reply, FLOOD_PACKET_FILTER_DUPLICATE);
return;
}
if (!normalizeFloodFilterScopeName(tokens[i] + strlen("scope="),
@@ -5139,7 +5145,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply) {
} else if (!hops_set && parseFloodFilterHopSpec(tokens[i], min_hops, max_hops)) {
hops_set = true;
} else {
strcpy(reply, "Err - use optional hops, path=blacklist, scope=<name>, require=region, tx=slow, and suspend=tempradio");
strcpy(reply, FLOOD_PACKET_FILTER_USAGE);
return;
}
}
@@ -5783,7 +5789,7 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) {
char region_text[32];
if (takeFloodModerationToken(cursor, channel_text, sizeof(channel_text)) != 1
|| takeFloodModerationToken(cursor, region_text, sizeof(region_text)) != 1) {
strcpy(reply, "Err - use: set flood.channel.scope[.n] <match> <region> [path=blacklist|path=bucket:1-6] [tx=slow]");
strcpy(reply, FLOOD_CHANNEL_SCOPE_USAGE);
return;
}
bool slow_timing = false;
@@ -5825,12 +5831,12 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) {
path_selector = (uint8_t)(
FloodFilterPolicy::SCOPE_PATH_BRIDGE_BUCKET_BASE + bucket - 1U);
} else {
strcpy(reply, "Err - scope option must be path=blacklist, path=bucket:1-6, or tx=fast|slow");
strcpy(reply, FLOOD_CHANNEL_SCOPE_USAGE);
return;
}
}
if (option_result < 0) {
strcpy(reply, "Err - use: set flood.channel.scope[.n] <match> <region> [path=blacklist|path=bucket:1-6] [tx=slow]");
strcpy(reply, FLOOD_CHANNEL_SCOPE_USAGE);
return;
}
+1 -1
View File
@@ -296,7 +296,7 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
}
const char *MyMesh::getLogDateTime() {
+1 -1
View File
@@ -354,7 +354,7 @@ bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
return (int) ((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
}
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
+28 -23
View File
@@ -91,7 +91,7 @@ void Dispatcher::restoreOutboundTxOverrides() {
}
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
return (int) ((powf(10.0f, 0.85f - score) - 1.0f) * air_time);
}
uint32_t Dispatcher::getCADFailRetryDelay() const {
@@ -101,6 +101,27 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
return 4000; // 4 seconds
}
#if MESH_PACKET_LOGGING
void Dispatcher::logPacketStart(const char* direction, const Packet* packet, int len) {
Serial.print(getLogDateTime());
Serial.printf(": %s, len=%d (type=%d, route=%s, payload_len=%d)",
direction, len, packet->getPayloadType(),
packet->isRouteDirect() ? "D" : "F", packet->payload_len);
}
void Dispatcher::logPacketEnd(const Packet* packet) {
const uint8_t type = packet->getPayloadType();
if (packet->payload_len >= 2
&& (type == PAYLOAD_TYPE_PATH || type == PAYLOAD_TYPE_REQ
|| type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_TXT_MSG)) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)packet->payload[1],
(uint32_t)packet->payload[0]);
} else {
Serial.write((uint8_t)'\n');
}
}
#endif
bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
const uint32_t now = _ms->getMillis();
bool found = false;
@@ -416,23 +437,15 @@ void Dispatcher::checkRecv() {
}
if (pkt) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
logPacketStart("RX", pkt, pkt->getRawLength());
Serial.printf(" SNR=%d RSSI=%d score=%d time=%d", (int)pkt->getSNR(),
(int)_radio->getLastRSSI(), (int)(score * 1000), air_time);
static uint8_t packet_hash[MAX_HASH_SIZE];
pkt->calculatePacketHash(packet_hash);
Serial.print(" hash=");
mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE);
if (pkt->payload_len >= 2
&& (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
Serial.printf("\n");
}
logPacketEnd(pkt);
#endif
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
@@ -594,16 +607,8 @@ void Dispatcher::checkSend() {
outbound_expiry = futureMillis(max_airtime);
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
if (outbound->payload_len >= 2
&& (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ
|| outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
} else {
Serial.printf("\n");
}
logPacketStart("TX", outbound, len);
logPacketEnd(outbound);
#endif
}
}
+5
View File
@@ -356,6 +356,11 @@ public:
unsigned long futureMillis(int millis_from_now) const;
private:
#if MESH_PACKET_LOGGING
void logPacketStart(const char* direction, const Packet* packet, int len)
__attribute__((noinline));
void logPacketEnd(const Packet* packet) __attribute__((noinline));
#endif
void checkRecv();
void checkSend();
};
+2 -2
View File
@@ -87,8 +87,8 @@ void AutoDiscoverRTCClock::setCurrentTime(uint32_t time) {
rtc_3231.adjust(DateTime(time));
} else if (rv3028_success) {
auto dt = DateTime(time);
uint8_t weekday = (dt.day() + (uint16_t)((2.6 * dt.month()) - 0.2) - (2 * (dt.year() / 100)) + dt.year() + (uint16_t)(dt.year() / 4) + (uint16_t)(dt.year() / 400)) % 7;
rtc_rv3028.setTime(dt.year(), dt.month(), weekday, dt.day(), dt.hour(), dt.minute(), dt.second());
rtc_rv3028.setTime(dt.year(), dt.month(), dt.dayOfTheWeek(), dt.day(),
dt.hour(), dt.minute(), dt.second());
} else if (rtc_8563_success) {
rtc_8563.adjust(DateTime(time));
} else if (rtc_8130_success) {
+12 -12
View File
@@ -108,27 +108,27 @@ bool RTC_RX8130CE::setTime(struct tm *t) {
}
void RTC_RX8130CE::adjust(DateTime dt) {
struct tm *atv;
time_t utime;
utime = (time_t)dt.unixtime();
atv = gmtime(&utime);
this->setTime(atv);
struct tm atv = {};
atv.tm_sec = dt.second();
atv.tm_min = dt.minute();
atv.tm_hour = dt.hour();
atv.tm_wday = dt.dayOfTheWeek();
atv.tm_mday = dt.day();
atv.tm_mon = dt.month() - 1;
atv.tm_year = dt.year() - 1900;
this->setTime(&atv);
}
DateTime RTC_RX8130CE::now() {
struct tm atv;
this->getTime(&atv);
return DateTime((uint32_t)mktime(&atv));
return DateTime(atv.tm_year + 1900, atv.tm_mon + 1, atv.tm_mday,
atv.tm_hour, atv.tm_min, atv.tm_sec);
}
uint32_t RTC_RX8130CE::unixtime() {
struct tm atv;
this->getTime(&atv);
return (uint32_t)mktime(&atv);
return now().unixtime();
}
bool RTC_RX8130CE::getTime(struct tm *t) {
+13 -5
View File
@@ -424,7 +424,9 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
if (strncmp(d, "stage ", 6) == 0) {
uint32_t sz = parse_u32(d + 6);
if (sz == 0 || sz > OTA_SERVE_BUF_SIZE) { sprintf(reply, "ERR size 1..%u", OTA_SERVE_BUF_SIZE); }
else { memset(c.serve_buf, 0xFF, sz); c.serve_expected = sz; c.serving = false;
else if (!c.ensureServeBuffer()) { strcpy(reply, "ERR stage OOM"); }
else { c.manager.clear_primary(); c.serving = false;
memset(c.serve_buf, 0xFF, sz); c.serve_expected = sz;
sprintf(reply, "OK stage %u bytes", (unsigned)sz); }
} else if (strncmp(d, "recv ", 5) == 0) {
@@ -435,7 +437,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
int blen = (int)strlen(hex) / 2;
uint8_t tmp[80];
if (blen <= 0 || blen > (int)sizeof(tmp) || !mesh::Utils::fromHex(tmp, blen, hex)) strcpy(reply, "ERR hex");
else if (off + blen > c.serve_expected) strcpy(reply, "ERR off>size (stage first)");
else if (!c.serve_buf || off + blen > c.serve_expected) strcpy(reply, "ERR off>size (stage first)");
else { memcpy(c.serve_buf + off, tmp, blen); sprintf(reply, "OK %d@%u", blen, (unsigned)off); }
} else if (strncmp(d, "serve self", 10) == 0) { // host our own running firmware, served from flash
@@ -447,6 +449,10 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
sprintf(reply, "OK serving self fw mid=%s (%u B, flash-backed) - peers can pull it", midhx, (unsigned)img);
} else strcpy(reply, "ERR serve self (no EndF / image too big / OOM)");
} else if (strncmp(d, "serve", 5) == 0) {
if (!c.serve_buf || c.serve_expected == 0) {
strcpy(reply, "ERR nothing staged");
return true;
}
c.serving = c.manager.serve(c.serve_buf, c.serve_expected);
if (!c.serving) { strcpy(reply, "ERR serve (bad .mota)"); return true; }
VerifyResult r = ota_verify(c.serve_buf, c.serve_expected, c.allow);
@@ -466,7 +472,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
} else if (strncmp(d, "verify", 6) == 0) {
const uint8_t* buf; uint32_t len;
if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); }
else { buf = c.serve_buf; len = c.serve_expected; }
else { buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0; }
if (len == 0 || !buf) { strcpy(reply, "ERR nothing to verify (flash-staged: applydelta verifies)"); return true; }
VerifyResult r = ota_verify(buf, len, c.allow);
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
@@ -486,7 +492,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
if (ota_apply_slot_info(&addr, &size)) sprintf(reply, "inactive slot addr=0x%X size=%u", (unsigned)addr, (unsigned)size);
else strcpy(reply, "ERR no A/B slot (apply unsupported on this build)");
} else if (strncmp(sub, "manifest", 8) == 0) {
if (ota_apply_set_manifest(c.serve_buf, c.serve_expected, c.allow, c.apply_st))
if (c.serve_buf && ota_apply_set_manifest(c.serve_buf, c.serve_expected, c.allow, c.apply_st))
sprintf(reply, "manifest ok img=%u sig=%d trust=%d", (unsigned)c.apply_st.image_size, c.apply_st.sig_ok, c.apply_st.trusted);
else strcpy(reply, "ERR manifest parse / not full-image / unsupported");
} else if (strncmp(sub, "verify", 6) == 0) {
@@ -501,7 +507,9 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
}
} else if (strncmp(d, "clear", 5) == 0) {
c.serve_expected = 0; c.serving = false; c.fetch_store.clear(); c.manager.reset_session();
c.manager.clear_primary();
c.serve_expected = 0; c.serving = false; c.releaseServeBuffer();
c.fetch_store.clear(); c.manager.reset_session();
strcpy(reply, "OK cleared");
} else {
+18
View File
@@ -1,6 +1,7 @@
#pragma once
#include <stdio.h> // snprintf (hw_id mismatch message)
#include <stdlib.h> // malloc/free (lazy ESP32 manual-stage buffer)
#include <string.h> // strncmp/strncpy (hw_id)
#include "OtaManager.h"
#include "OtaStore.h"
@@ -62,7 +63,24 @@ struct OtaContext {
OtaStoreRam<OTA_FETCH_BUF_SIZE> fetch_store;
#endif
SignerAllowlist allow;
#if defined(ESP32_PLATFORM)
// Manual `ota dev stage` is a diagnostic path. Reserving its full buffer in
// .bss prevents high-capacity classic ESP32 images from linking, even when
// the command is never used, so allocate it only while a manual stage exists.
uint8_t* serve_buf = nullptr;
bool ensureServeBuffer() {
if (!serve_buf) serve_buf = static_cast<uint8_t*>(malloc(OTA_SERVE_BUF_SIZE));
return serve_buf != nullptr;
}
void releaseServeBuffer() {
free(serve_buf);
serve_buf = nullptr;
}
#else
uint8_t serve_buf[OTA_SERVE_BUF_SIZE];
bool ensureServeBuffer() { return true; }
void releaseServeBuffer() {}
#endif
uint32_t serve_expected = 0; // size declared by `ota stage`
bool serving = false; // manager.serve() succeeded
// flash-backed self-serve: cached merkle leaves (heap, freed on re-serve) + assembled manifest of our
+54 -13
View File
@@ -10,6 +10,24 @@
namespace mesh {
namespace ota {
uint8_t* OtaManager::ensureSourceLeaves() {
#if defined(ESP32_PLATFORM)
if (!_src_leaves) {
_src_leaves = static_cast<uint8_t*>(malloc(OTA_PROOFGEN_SCRATCH));
}
#endif
return _src_leaves;
}
uint8_t* OtaManager::ensureScratch() {
#if defined(ESP32_PLATFORM)
if (!_scratch) {
_scratch = static_cast<uint8_t*>(malloc(OTA_PROOFGEN_SCRATCH));
}
#endif
return _scratch;
}
void OtaManager::begin(uint32_t my_target_id, OtaSend send, void* ctx) {
_target = my_target_id; _send = send; _ctx = ctx;
_fstate = IDLE; _have = 0; _fbc = 0;
@@ -24,10 +42,12 @@ void OtaManager::begin(uint32_t my_target_id, OtaSend send, void* ctx) {
// we advertise / answer OTA_QUERY with) is the lightweight _serve[] registry.
bool OtaManager::serve(const uint8_t* mota, uint32_t len) {
uint8_t* scratch = ensureScratch();
if (!scratch) return false;
if (!mota_parse(mota, len, _view0.m)) return false;
_view0.mfl = (uint16_t)(_view0.m.leaves - _view0.m.manifest_start); // contiguous container
_view0.read = nullptr; _view0.read_ctx = nullptr; // payload is contiguous _view0.m.payload
_view0.scratch = _scratch; _view0.scratch_sz = sizeof(_scratch); // <=1024 blocks (RAM .mota is small)
_view0.scratch = scratch; _view0.scratch_sz = OTA_PROOFGEN_SCRATCH; // <=1024 blocks (RAM .mota is small)
_view0.valid = true;
registerSelfEntry();
return true;
@@ -52,12 +72,19 @@ bool OtaManager::serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t
// (Re)build registry slot 0 from view0 (our own fw / RAM mota). Keeps any source entries in [1..].
void OtaManager::registerSelfEntry() {
if (!_view0.valid) return;
if (_n_serve > 0 && !_serve[0].is_self) {
if (_n_serve < OTA_MAX_SERVE) _n_serve++;
for (uint8_t i = _n_serve - 1; i > 0; i--) {
_serve[i] = _serve[i - 1];
}
} else if (_n_serve == 0) {
_n_serve = 1;
}
ServeEntry& e = _serve[0];
memcpy(e.mid, _view0.m.merkle_root, 4);
e.target_id = _view0.m.target_id; e.fw_version = _view0.m.fw_version;
e.codec_id = _view0.m.codec_id; e.flags = _view0.m.flags; e.have_count = _view0.m.block_count;
e.is_self = true; e.src = nullptr; e.src_idx = 0;
if (_n_serve == 0) _n_serve = 1;
}
bool OtaManager::add_source(MotaSource* src) {
@@ -68,9 +95,8 @@ bool OtaManager::add_source(MotaSource* src) {
}
void OtaManager::refresh_sources() {
uint8_t base = _view0.valid ? 1 : 0; // entry 0 stays our own fw
_n_serve = 0;
if (_view0.valid) registerSelfEntry();
_n_serve = base;
for (uint8_t s = 0; s < _n_src_obj; s++) {
MotaSource* src = _src_list[s];
if (!src) continue;
@@ -91,10 +117,20 @@ void OtaManager::refresh_sources() {
void OtaManager::clear_sources() {
_n_src_obj = 0; _srcv.valid = false;
_n_serve = _view0.valid ? 1 : 0;
_n_serve = 0;
if (_view0.valid) registerSelfEntry();
}
void OtaManager::clear_primary() {
_view0.valid = false;
if (_n_serve > 0 && _serve[0].is_self) {
for (uint8_t i = 1; i < _n_serve; i++) {
_serve[i - 1] = _serve[i];
}
_n_serve--;
}
}
int OtaManager::serveEntryIndex(const uint8_t* mid) const {
for (uint8_t i = 0; i < _n_serve; i++)
if (memcmp(_serve[i].mid, mid, 4) == 0) return i;
@@ -119,21 +155,24 @@ bool OtaManager::loadSource(const ServeEntry& e) {
if (!e.src) return false;
uint16_t mfl = (uint16_t)(d.leaves_off - 8);
if (mfl == 0 || mfl > sizeof(_src_manifest)) return false;
if (d.block_count == 0 || (uint64_t)d.block_count * 4 > sizeof(_src_leaves)) return false;
if (d.block_count == 0 || (uint64_t)d.block_count * 4 > OTA_PROOFGEN_SCRATCH) return false;
uint8_t* src_leaves = ensureSourceLeaves();
uint8_t* scratch = ensureScratch();
if (!src_leaves || !scratch) return false;
bool ok = e.src->read(e.src_idx, 8, _src_manifest, mfl);
if (!ok || !mota_parse_manifest(_src_manifest, mfl, _srcv.m)) return false;
if (memcmp(_srcv.m.merkle_root, d.mid, 4) != 0) return false; // descriptor/bytes disagree
if (_srcv.m.block_count != d.block_count) return false;
ok = e.src->read(e.src_idx, d.leaves_off, _src_leaves, d.block_count * 4);
ok = e.src->read(e.src_idx, d.leaves_off, src_leaves, d.block_count * 4);
if (!ok) return false;
_srcv.m.manifest_start = _src_manifest;
_srcv.m.leaves = _src_leaves;
_srcv.m.leaves = src_leaves;
_srcv.m.payload = nullptr;
_srcv.mfl = mfl;
_srcv_rdctx.src = e.src; _srcv_rdctx.idx = e.src_idx;
_srcv_rdctx.payload_off = d.payload_off;
_srcv.read = srcReadTramp; _srcv.read_ctx = &_srcv_rdctx;
_srcv.scratch = _scratch; _srcv.scratch_sz = sizeof(_scratch);
_srcv.scratch = scratch; _srcv.scratch_sz = OTA_PROOFGEN_SCRATCH;
memcpy(_srcv_mid, d.mid, 4);
_srcv.valid = true;
return true;
@@ -624,8 +663,9 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
OTA_DBG("OTA: RESUME have=%u/%u total=%u\n", (unsigned)_have, (unsigned)bc, (unsigned)total);
if (_have >= bc) { // already complete -> verify root + finalize
if (bc * 4 <= sizeof(_scratch) && _fetch->read(_floff, _scratch, bc * 4)) {
uint8_t root[4]; merkle_root(root, _scratch, bc);
uint8_t* scratch = bc * 4 <= OTA_PROOFGEN_SCRATCH ? ensureScratch() : nullptr;
if (scratch && _fetch->read(_floff, scratch, bc * 4)) {
uint8_t root[4]; merkle_root(root, scratch, bc);
_fstate = (memcmp(root, _froot, 4) == 0) ? COMPLETE : FAILED;
} else {
_fstate = COMPLETE;
@@ -701,8 +741,9 @@ void OtaManager::handleProof(const uint8_t* m, uint16_t n) {
if (_checkpoint_blocks && _have % _checkpoint_blocks == 0) _fetch->checkpoint();
if (_have < _fbc) { requestMissing(); return; } // next block
// all blocks present -> final root cross-check + finalize
if (_fbc * 4 <= sizeof(_scratch) && _fetch->read(_floff, _scratch, _fbc * 4)) {
uint8_t root[4]; merkle_root(root, _scratch, _fbc);
uint8_t* scratch = _fbc * 4 <= OTA_PROOFGEN_SCRATCH ? ensureScratch() : nullptr;
if (scratch && _fetch->read(_floff, scratch, _fbc * 4)) {
uint8_t root[4]; merkle_root(root, scratch, _fbc);
_fstate = (memcmp(root, _froot, 4) == 0) ? COMPLETE : FAILED;
} else {
_fstate = COMPLETE; // per-block proofs already guaranteed integrity vs the root
+13
View File
@@ -169,6 +169,9 @@ public:
void refresh_sources();
// Drop all external sources (keep serving our own fw).
void clear_sources();
// Stop serving the primary caller-owned image while preserving attached sources.
// Call this before releasing or overwriting the primary image's backing buffer.
void clear_primary();
uint8_t servedCount() const { return _n_serve; } // total mOTAs we offer (own fw + folder)
// Read-only view of one served entry (for `ota serve` listing): mid/target/fwver/codec/flags + is_self.
const ServeEntry* servedEntry(uint8_t i) const { return i < _n_serve ? &_serve[i] : nullptr; }
@@ -337,8 +340,15 @@ private:
MotaSource* _src_list[OTA_MAX_SOURCE_OBJ] = {nullptr};
uint8_t _n_src_obj = 0;
uint8_t _src_manifest[OTA_SRC_MANIFEST_MAX]; // manifest-minus-leaves of the loaded source mota
#if defined(ESP32_PLATFORM)
// Folder-source leaves and proof scratch are cold-path working storage. Keep
// them off classic ESP32 .bss and allocate them on first use.
uint8_t* _src_leaves = nullptr;
uint8_t* _scratch = nullptr;
#else
uint8_t _src_leaves[OTA_PROOFGEN_SCRATCH]; // leaves[] of the loaded source mota (<=1024 blocks)
uint8_t _scratch[OTA_PROOFGEN_SCRATCH]; // proof-gen / fetch root-check working buffer
#endif
// fetch
OtaStore* _fetch = nullptr;
@@ -386,6 +396,9 @@ private:
bool _diffing = false; // leaves are in; diffing the seed a batch per tick
uint32_t _diff_idx = 0; // next block index to diff against the seed
uint8_t* ensureSourceLeaves();
uint8_t* ensureScratch();
// discovery: heard sources (beacon senders) + the catalog assembled from their OTA_HAVE replies
struct Source { uint8_t seeder[4]; uint8_t digest[4]; uint8_t n_motas; uint32_t last_ms; bool have_catalog; };
Source _sources[OTA_MAX_SOURCES];
+7 -15
View File
@@ -704,25 +704,17 @@ float RadioLibWrapper::getLastSNR() const {
return _radio->getSNR();
}
// Approximate SNR threshold per SF for successful reception (based on Semtech datasheets)
static float snr_threshold[] = {
-7.5, // SF7 needs at least -7.5 dB SNR
-10, // SF8 needs at least -10 dB SNR
-12.5, // SF9 needs at least -12.5 dB SNR
-15, // SF10 needs at least -15 dB SNR
-17.5,// SF11 needs at least -17.5 dB SNR
-20 // SF12 needs at least -20 dB SNR
};
float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
if (sf < 7) return 0.0f;
if (snr < snr_threshold[sf - 7]) return 0.0f; // Below threshold, no chance of success
auto success_rate_based_on_snr = (snr - snr_threshold[sf - 7]) / 10.0;
auto collision_penalty = 1 - (packet_len / 256.0); // Assuming max packet of 256 bytes
// Semtech's approximate threshold drops 2.5 dB for each step from SF7.
const float snr_threshold = -7.5f - (sf - 7) * 2.5f;
if (snr < snr_threshold) return 0.0f;
return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty));
const float success_rate = (snr - snr_threshold) * 0.1f;
const float collision_penalty = 1.0f - packet_len / 256.0f;
const float score = success_rate * collision_penalty;
return score < 1.0f ? score : 1.0f;
}
PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) {
+22
View File
@@ -530,6 +530,28 @@ private:
};
}
TEST(OtaServe, ClearPrimaryInvalidatesCallerOwnedView) {
OtaManager manager;
manager.begin(0, nullptr, nullptr);
ASSERT_TRUE(manager.serve(SIM_MOTA, SIM_MOTA_LEN));
RamMotaSource folder;
folder.add(SIM_MOTA_1K, SIM_MOTA_1K_LEN);
ASSERT_TRUE(manager.add_source(&folder));
ASSERT_EQ(manager.servedCount(), 2);
manager.clear_primary();
ASSERT_EQ(manager.servedCount(), 1);
EXPECT_FALSE(manager.servedEntry(0)->is_self);
// A caller may release the container and later install a fresh primary view
// without dropping or overwriting attached folder sources.
EXPECT_TRUE(manager.serve(SIM_MOTA, SIM_MOTA_LEN));
ASSERT_EQ(manager.servedCount(), 2);
EXPECT_TRUE(manager.servedEntry(0)->is_self);
EXPECT_FALSE(manager.servedEntry(1)->is_self);
}
TEST(OtaTransfer, TwoManagersFullTransfer) {
g_q.clear();
OtaManager server, client;