diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1e4bf99a..f097df0a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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) { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ce87b089..10d4dff4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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] [hops] [path=blacklist] [scope=] [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] [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] [hops] [path=blacklist] [scope=] [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=, 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] [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] [path=blacklist|path=bucket:1-6] [tx=slow]"); + strcpy(reply, FLOOD_CHANNEL_SCOPE_USAGE); return; } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index f8ba69a9..ad8ec736 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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() { diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 88c47de1..eac25a1c 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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) { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index edc42538..b6c1adac 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -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 } } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index b46c93f5..3101f22c 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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(); }; diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index af08fddb..53e58dd4 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -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) { diff --git a/src/helpers/RTC_RX8130CE.cpp b/src/helpers/RTC_RX8130CE.cpp index 8aa2b1ff..c0361194 100644 --- a/src/helpers/RTC_RX8130CE.cpp +++ b/src/helpers/RTC_RX8130CE.cpp @@ -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) { diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 55a6f942..d8ed90c7 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -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 { diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index b786c2fe..4b724b44 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -1,6 +1,7 @@ #pragma once #include // snprintf (hw_id mismatch message) +#include // malloc/free (lazy ESP32 manual-stage buffer) #include // strncmp/strncpy (hw_id) #include "OtaManager.h" #include "OtaStore.h" @@ -62,7 +63,24 @@ struct OtaContext { OtaStoreRam 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(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 diff --git a/src/helpers/ota/OtaManager.cpp b/src/helpers/ota/OtaManager.cpp index a1843a18..ad10bcf4 100644 --- a/src/helpers/ota/OtaManager.cpp +++ b/src/helpers/ota/OtaManager.cpp @@ -10,6 +10,24 @@ namespace mesh { namespace ota { +uint8_t* OtaManager::ensureSourceLeaves() { +#if defined(ESP32_PLATFORM) + if (!_src_leaves) { + _src_leaves = static_cast(malloc(OTA_PROOFGEN_SCRATCH)); + } +#endif + return _src_leaves; +} + +uint8_t* OtaManager::ensureScratch() { +#if defined(ESP32_PLATFORM) + if (!_scratch) { + _scratch = static_cast(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 diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h index 7d4a966f..8028af0f 100644 --- a/src/helpers/ota/OtaManager.h +++ b/src/helpers/ota/OtaManager.h @@ -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]; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 3c8a105c..476f298b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -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) { diff --git a/test/test_ota/test_ota_core.cpp b/test/test_ota/test_ota_core.cpp index 9f2d08ac..0358d0ce 100644 --- a/test/test_ota/test_ota_core.cpp +++ b/test/test_ota/test_ota_core.cpp @@ -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;