Fix packet, delivery, clock and radio stability regressions

Reject truncated anonymous reply paths and advert metadata before copying,
while preserving timestamp-only empty-password logins. Limit room posting to
read-write/admin ACL roles, retain inbox frames until the requesting transport
accepts them, and lock raw LittleFS traversal against concurrent writes.

Separate ISR event generations from main-loop radio state. Confirm RX/TX IRQs,
detach concrete wrapper callbacks safely, and discard inconsistent LR2021 FIFO
snapshots before rearming. Preserve bridge RX frames through packet-pool
exhaustion and bound ESP-NOW retries and missing-callback recovery. Screen RTC
register layouts without losing DS1307 startup, and ignore poisoned future
contact timestamps during clock bootstrap.

Keep the nRF52 loop stack at 8 KiB while reducing the OTA listing call chain
from 7,832 to 6,056 static bytes. Retain all target names through lossless Full
Companion compression and the canonical size optimizer. Preserve constrained
STM32 filesystems and bind corrected ESP32 UART/OLED budgets to source size
assertions. Document the separately measured preexisting W12 package limits.

Adapted for this branch from meshcore-dev/MeshCore PRs #3521, #3340, #3362,
#2062, #3503, #3038, #3534, #3544 and #3512.

Validation: 1,829 native tests; sanitized parser, inbox, LittleFS, ACL, clock,
RTC, IRQ/FIFO and bridge fault tests; 332 additional Companion cases; real
RAK4631 Full and both repeater profiles, G2 Full/standard, V3 Full and both
constrained STM32 release recipes. Radio-family compile coverage continues
separately; W12 full/portable package guards remain enforced.
This commit is contained in:
mikecarper committed 2026-10-09 19:56:08 -07:00
1 parent f79a316da8
commit ddf625e9d0
60 files changed
+3647 -369

No files matched your search

+21 -13
View File
@@ -1,5 +1,7 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/UTF8Helpers.h>
#include <stdio.h>
#include <string.h>
uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) {
app_data[0] = _type;
@@ -39,11 +41,22 @@ bool AdvertDataParser::isValidName(const char *n) {
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
_name[0] = 0;
_lat = _lon = 0;
_flags = app_data[0];
_flags = 0;
_valid = false;
_extra1 = _extra2 = 0;
int i = 1;
if (app_data == nullptr || app_data_len == 0 || app_data_len > MAX_ADVERT_DATA_SIZE) return;
_flags = app_data[0];
const size_t prefix_len = 1
+ ((_flags & ADV_LATLON_MASK) ? sizeof(_lat) + sizeof(_lon) : 0)
+ ((_flags & ADV_FEAT1_MASK) ? sizeof(_extra1) : 0)
+ ((_flags & ADV_FEAT2_MASK) ? sizeof(_extra2) : 0);
// Validate the complete fixed prefix before copying any optional fields.
// This also preserves the defaults if a later field is truncated.
if (app_data_len < prefix_len) return;
size_t i = 1;
if (_flags & ADV_LATLON_MASK) {
memcpy(&_lat, &app_data[i], 4); i += 4;
memcpy(&_lon, &app_data[i], 4); i += 4;
@@ -55,17 +68,12 @@ bool AdvertDataParser::isValidName(const char *n) {
memcpy(&_extra2, &app_data[i], 2); i += 2;
}
if (app_data_len >= i) {
int nlen = 0;
if (_flags & ADV_NAME_MASK) {
nlen = app_data_len - i; // remainder of app_data
}
if (nlen > 0) {
memcpy(_name, &app_data[i], nlen);
_name[nlen] = 0; // set null terminator
}
_valid = true;
const size_t nlen = (_flags & ADV_NAME_MASK) ? app_data_len - i : 0;
if (nlen > 0) {
memcpy(_name, &app_data[i], nlen);
_name[nlen] = 0; // set null terminator
}
_valid = true;
}
#include <Arduino.h>
+101 -7
View File
@@ -26,14 +26,107 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
return (error == 0);
}
// An ACK only says that something answers at an RTC's address: an IMU at 0x68 or an EEPROM in
// 0x50-0x57 answers too, and would then be read as the clock and written on every time sync.
// So an RTC is adopted only if its time registers read like that chip's, per its datasheet:
// - bits documented as always 0 must read 0 (the PCF8563 documents none), and the seven
// registers must not all read 0xFF, as an erased EEPROM does;
// - seconds, minutes, date and month must be valid BCD in range, unless the chip's power-loss
// flag is set, or a DS1307's clock-halt bit is set: startup time may be undefined
// (not checked for the RX8130CE, see its entry below).
// The year is not checked, as MeshCore can write an out-of-range one from a bad epoch. This
// cannot catch every device: one whose bytes happen to fit is still adopted, as before.
struct RtcId {
uint8_t time_reg; // seconds register; the seven time registers follow it
uint8_t zero[7]; // bits that always read 0
uint8_t date_idx; // index of the date register within the seven
uint8_t flag_reg; // power-loss flag register and bit; flag_bit 0 skips the field check
uint8_t flag_bit;
uint8_t halt_bit; // seconds clock-halt flag; 0 for every profile except DS1307 at 0x68
};
#if !defined(DISABLE_DS3231_PROBE)
// DS3231 (Maxim 19-5170 Rev 10): Figure 1, p. 11; OSF in Status (0Fh) bit 7, p. 14. 00h bit 7
// is left out: a DS1307 at 0x68, which this code also drives, powers up with its clock-halt bit
// there set (Maxim DS1307 Rev 3/15, Table 2 and text, p. 8). Its other bits match.
// A halted DS1307 may have unset calendar fields; 0Fh is RAM on that chip, not OSF.
static const RtcId DS3231_ID =
{ 0x00, { 0x00, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }, 4, 0x0F, 0x80, 0x80 };
#endif
// RV3028 (RV-3028-C7 App Manual Rev 1.4): 3.2, p. 12; PORF in Status (0Eh) bit 0, 3.7, p. 22
static const RtcId RV3028_ID =
{ 0x00, { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }, 4, 0x0E, 0x01, 0 };
// PCF8563 (NXP Rev 11.1): unused bits are "not relevant", not 0, Table 4, p. 10; VL in
// VL_seconds (02h) bit 7, Table 8, p. 13, and set at power-up, Table 27, p. 24
static const RtcId PCF8563_ID = { 0x02, { 0 }, 3, 0x02, 0x80, 0 };
// RX8130CE (Epson ETM50E-10): read value always 0, 13.2.1, p. 22. No field check: its power-loss
// flag VLF (1Dh bit 1) cannot vouch for the fields, as RTC_RX8130CE::begin() clears it on every
// boot without setting the time.
static const RtcId RX8130CE_ID =
{ 0x10, { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }, 4, 0x1D, 0x00, 0 };
// Reads n registers from reg, with a repeated start as every one of these datasheets documents
static bool rtcRead(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t n) {
wire.beginTransmission(addr);
wire.write(reg);
if (wire.endTransmission(false) != 0) return false;
if (wire.requestFrom(addr, n) != n) return false;
for (uint8_t i = 0; i < n; i++) {
const int value = wire.read();
if (value < 0) return false;
buf[i] = uint8_t(value);
}
return true;
}
static bool bcdInRange(uint8_t v, uint8_t lo, uint8_t hi) {
if ((v & 0x0F) > 9 || (v >> 4) > 9) return false;
uint8_t d = (v >> 4) * 10 + (v & 0x0F);
return d >= lo && d <= hi;
}
// 1 if this read rules the device out, 0 if not, -1 if a read failed
static int rtcCheck(TwoWire& wire, uint8_t addr, const RtcId& id) {
uint8_t t[7];
if (!rtcRead(wire, addr, id.time_reg, t, 7)) return -1;
bool all_ff = true;
for (int i = 0; i < 7; i++) {
if (t[i] & id.zero[i]) return 1;
all_ff = all_ff && t[i] == 0xFF;
}
if (all_ff) return 1; // an erased EEPROM
if (t[0] & id.halt_bit) return 0; // DS1307 startup; never rely on its RAM at 0Fh
if (id.flag_bit == 0) return 0;
if (bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59)
&& bcdInRange(t[id.date_idx] & 0x3F, 1, 31) && bcdInRange(t[5] & 0x1F, 1, 12)) {
return 0;
}
uint8_t flag;
if (!rtcRead(wire, addr, id.flag_reg, &flag, 1)) return -1;
return (flag & id.flag_bit) ? 0 : 1; // power was lost, so the time is undefined
}
// Retry one failed/corrupted read, but do not initialize or write an unknown
// device when neither read identifies a compatible register layout.
static bool rtcLooksCompatible(TwoWire& wire, uint8_t addr, const RtcId& id) {
for (int attempt = 0; attempt < 2; ++attempt) {
if (rtcCheck(wire, addr, id) == 0) return true;
}
MESH_DEBUG_PRINTLN("RTC: skipping unrecognized or unreadable device at 0x%02X", addr);
return false;
}
void AutoDiscoverRTCClock::begin(TwoWire& wire) {
ds3231_success = rv3028_success = rtc_8563_success = rtc_8130_success = false;
#if !defined(DISABLE_DS3231_PROBE)
if (i2c_probe(wire, DS3231_ADDRESS)) {
if (i2c_probe(wire, DS3231_ADDRESS)
&& rtcLooksCompatible(wire, DS3231_ADDRESS, DS3231_ID)) {
ds3231_success = rtc_3231.begin(&wire);
}
#endif
if (i2c_probe(wire, RV3028_ADDRESS)) {
if (i2c_probe(wire, RV3028_ADDRESS)
&& rtcLooksCompatible(wire, RV3028_ADDRESS, RV3028_ID)) {
rtc_rv3028.initI2C(wire);
rtc_rv3028.writeToRegister(0x35, 0x00);
rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP
@@ -41,16 +134,17 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) {
rv3028_success = true;
}
if (i2c_probe(wire, PCF8563_ADDRESS)) {
if (i2c_probe(wire, PCF8563_ADDRESS)
&& rtcLooksCompatible(wire, PCF8563_ADDRESS, PCF8563_ID)) {
MESH_DEBUG_PRINTLN("PCF8563: Found");
rtc_8563_success = rtc_8563.begin(&wire);
}
if (i2c_probe(wire, RX8130CE_ADDRESS)) {
if (i2c_probe(wire, RX8130CE_ADDRESS)
&& rtcLooksCompatible(wire, RX8130CE_ADDRESS, RX8130CE_ID)) {
MESH_DEBUG_PRINTLN("RX8130CE: Found");
rtc_8130.begin(&wire);
rtc_8130_success = true;
MESH_DEBUG_PRINTLN("RX8130CE: Initialized");
rtc_8130_success = rtc_8130.begin(&wire);
if (rtc_8130_success) MESH_DEBUG_PRINTLN("RX8130CE: Initialized");
}
}
+25 -2
View File
@@ -127,16 +127,39 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, u
}
}
static uint32_t contactClockBootstrapLimit(uint32_t current_time) {
// A corrupt contact must not permanently move the clock into the far future.
// Use the build year while the RTC is unset, and a rolling bound when a
// long-lived installation already has a clock beyond that build horizon.
constexpr const char* date = __DATE__;
constexpr unsigned year = (date[7] - '0') * 1000 + (date[8] - '0') * 100
+ (date[9] - '0') * 10 + (date[10] - '0') + 4;
constexpr unsigned previous_year = year - 1;
// Gregorian leap days before this year, minus the 477 before 1970.
// Fold the build horizon at compile time instead of linking a calendar loop.
constexpr uint64_t days = 365ULL * (year - 1970)
+ previous_year / 4 - previous_year / 100 + previous_year / 400 - 477;
constexpr uint64_t build_limit = days * 86400ULL;
constexpr uint32_t build_bound = build_limit < UINT32_MAX
? uint32_t(build_limit) : UINT32_MAX;
constexpr uint32_t clock_window = 366UL * 86400UL;
const uint32_t clock_bound = current_time > UINT32_MAX - clock_window
? UINT32_MAX : current_time + clock_window;
return clock_bound > build_bound ? clock_bound : build_bound;
}
void BaseChatMesh::bootstrapRTCfromContacts() {
const uint32_t current_time = getRTCClock()->getCurrentTime();
const uint32_t max_time = contactClockBootstrapLimit(current_time);
uint32_t latest = 0;
for (int i = 0; i < num_contacts; i++) {
if (contacts[i].lastmod > latest) {
if (contacts[i].lastmod > latest && contacts[i].lastmod < max_time) {
latest = contacts[i].lastmod;
}
}
if (latest != 0 && latest != UINT32_MAX) {
const uint32_t contact_floor = latest + 1;
if (contact_floor > getRTCClock()->getCurrentTime()) {
if (contact_floor > current_time) {
getRTCClock()->setCurrentTime(contact_floor);
}
}
+73 -25
View File
@@ -77,6 +77,10 @@ void ESPNowBridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
if (_initialized) return;
if (_sdk_teardown_failed) {
end();
if (_sdk_teardown_failed) return;
}
_active_format = mesh::bridge::isValidEspNowFormat(_prefs->bridge_format)
? _prefs->bridge_format : mesh::bridge::ESPNOW_FORMAT_WRAPPED;
@@ -103,6 +107,7 @@ void ESPNowBridge::begin() {
_tx_dropped = 0;
portEXIT_CRITICAL(&_tx_mux);
_tx_dropped_reported = 0;
_tx_started_at = _tx_retry_at = 0;
if (!mesh::bridge::isValidEspNowBridgeChannel(
_prefs->bridge_channel, _active_format)) {
@@ -215,7 +220,7 @@ void ESPNowBridge::begin() {
}
void ESPNowBridge::end() {
if (!_initialized) return;
if (!_initialized && !_sdk_teardown_failed) return;
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
// Remove broadcast peer
@@ -229,7 +234,8 @@ void ESPNowBridge::end() {
esp_now_register_send_cb(nullptr);
// Deinitialize ESP-NOW
if (esp_now_deinit() != ESP_OK) {
_sdk_teardown_failed = esp_now_deinit() != ESP_OK;
if (_sdk_teardown_failed) {
BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
}
@@ -267,11 +273,17 @@ void ESPNowBridge::end() {
}
void ESPNowBridge::loop() {
if (!_initialized) return;
pumpTransmitQueue();
if (!_initialized) return;
uint8_t frame[MAX_ESPNOW_PACKET_SIZE];
uint8_t source[mesh::espnow::ESPNOW_RAW_SOURCE_MAC_SIZE];
for (uint8_t processed = 0; processed < RX_QUEUE_DEPTH; ++processed) {
// Reserve a mesh slot before removing a callback frame. Even a completing
// raw fragment must remain queued until the reconstructed packet can live.
mesh::Packet* packet = _mgr->allocNew();
if (packet == nullptr) break;
size_t frame_len = 0;
uint32_t dropped = 0;
@@ -295,8 +307,10 @@ void ESPNowBridge::loop() {
(unsigned long)dropped);
_rx_dropped_reported = dropped;
}
if (frame_len == 0) break;
processReceivedFrame(source, frame, frame_len);
if (frame_len == 0) { _mgr->free(packet); break; }
if (!processReceivedFrame(packet, source, frame, frame_len)) {
_mgr->free(packet);
}
}
}
@@ -321,6 +335,7 @@ bool ESPNowBridge::queueTransmitFrames(
queued_tx.packet = packet;
queued_tx.next_frame = 0;
queued_tx.started = false;
queued_tx.start_retries = 0;
_tx_head = static_cast<uint8_t>((_tx_head + 1) % TX_QUEUE_DEPTH);
_tx_count++;
queued = true;
@@ -339,6 +354,24 @@ void ESPNowBridge::pumpTransmitQueue() {
bool clear_unsent_seen = false;
mesh::Packet unsent_packet;
uint32_t dropped = 0;
const uint32_t now = millis();
bool timed_out = false;
portENTER_CRITICAL(&_tx_mux);
timed_out = _tx_waiting && !_tx_callback_done
&& static_cast<uint32_t>(now - _tx_started_at) >= TX_CALLBACK_TIMEOUT_MS;
portEXIT_CRITICAL(&_tx_mux);
if (timed_out) {
// A completion carries no frame identity. Continuing after timeout could
// mistake a late callback for the next fragment. Tear down that SDK session
// before accepting another frame; never transmit an orphan sibling.
BRIDGE_DEBUG_PRINTLN("TX callback timeout; restarting ESP-NOW\n");
end();
// Fail closed if the old session could still own an accepted frame. A
// later explicit begin retries teardown before accepting new traffic.
if (!_sdk_teardown_failed) begin();
return;
}
portENTER_CRITICAL(&_tx_mux);
dropped = _tx_dropped;
@@ -352,21 +385,27 @@ void ESPNowBridge::pumpTransmitQueue() {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (completed_status == ESP_NOW_SEND_SUCCESS) {
queued_tx.next_frame++;
queued_tx.start_retries = 0;
if (queued_tx.next_frame >= queued_tx.frames.count) {
_tx_tail = static_cast<uint8_t>(
(_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
}
} else if (queued_tx.start_retries < TX_MAX_START_RETRIES) {
// This completion is definite, unlike a missing callback. Retry the
// same frame within the shared start/callback budget; its sibling
// cannot be submitted until this frame succeeds.
++queued_tx.start_retries;
_tx_retry_at = now + TX_RETRY_DELAY_MS;
} else {
// esp_now_send() accepted at least one frame, matching the old bridge
// definition of "seen", but never submit any remaining fragments from
// a logical packet whose preceding frame failed.
_tx_tail = static_cast<uint8_t>((_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
}
}
}
if (!_tx_waiting && _tx_count > 0) {
if (!_tx_waiting && _tx_count > 0
&& (_tx_queue[_tx_tail].start_retries == 0
|| static_cast<int32_t>(now - _tx_retry_at) >= 0)) {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (queued_tx.next_frame < queued_tx.frames.count) {
frame_len = queued_tx.frames.lengths[queued_tx.next_frame];
@@ -379,6 +418,8 @@ void ESPNowBridge::pumpTransmitQueue() {
if (frame_len > 0) {
_tx_waiting = true;
_tx_callback_done = false;
_tx_started_at = now;
_tx_retry_at = 0;
}
}
portEXIT_CRITICAL(&_tx_mux);
@@ -404,12 +445,19 @@ void ESPNowBridge::pumpTransmitQueue() {
_tx_callback_done = false;
if (_tx_count > 0) {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (!queued_tx.started) {
unsent_packet = queued_tx.packet;
clear_unsent_seen = true;
if (result == ESP_ERR_ESPNOW_NO_MEM
&& queued_tx.start_retries < TX_MAX_START_RETRIES) {
++queued_tx.start_retries;
_tx_retry_at = now + TX_RETRY_DELAY_MS;
} else {
if (!queued_tx.started) {
unsent_packet = queued_tx.packet;
clear_unsent_seen = true;
}
_tx_tail = static_cast<uint8_t>((_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
_tx_retry_at = 0;
}
_tx_tail = static_cast<uint8_t>((_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
}
portEXIT_CRITICAL(&_tx_mux);
// Admission marks the packet to suppress duplicate queue entries. Restore
@@ -436,9 +484,7 @@ bool ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
return true;
}
void ESPNowBridge::receiveMeshPacket(const uint8_t *data, size_t len) {
mesh::Packet *pkt = _mgr->allocNew();
if (!pkt) return;
void ESPNowBridge::receiveMeshPacket(mesh::Packet* pkt, const uint8_t *data, size_t len) {
if (pkt->readFrom(data, len)) {
onPacketReceived(pkt);
@@ -481,10 +527,10 @@ void ESPNowBridge::queueReceivedFrame(const uint8_t *mac, const uint8_t *data,
portEXIT_CRITICAL(&_rx_mux);
}
void ESPNowBridge::processReceivedFrame(const uint8_t *mac,
bool ESPNowBridge::processReceivedFrame(mesh::Packet* packet_slot, const uint8_t *mac,
const uint8_t *data, size_t len) {
if (data == nullptr || len == 0 || len > MAX_ESPNOW_PACKET_SIZE) {
return;
return false;
}
if (_active_format == mesh::bridge::ESPNOW_FORMAT_RAW) {
@@ -500,28 +546,29 @@ void ESPNowBridge::processReceivedFrame(const uint8_t *mac,
(unsigned)packet_len,
result == mesh::espnow::ESPNowRawReassemblyResult::PACKET_COMPLETE
? " (reassembled)" : "");
receiveMeshPacket(packet, packet_len);
receiveMeshPacket(packet_slot, packet, packet_len);
return true;
} else if (result == mesh::espnow::ESPNowRawReassemblyResult::REJECTED
|| result ==
mesh::espnow::ESPNowRawReassemblyResult::OUTPUT_TOO_SMALL) {
BRIDGE_DEBUG_PRINTLN("RX raw fragment rejected, result=%u len=%u\n",
(unsigned)result, (unsigned)len);
}
return;
return false;
}
// Wrapped mode is strict: ignore packets too small to contain its header
// and checksum rather than trying to auto-detect raw MeshCore traffic.
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%u\n", (unsigned)len);
return;
return false;
}
// Check packet header magic
uint16_t received_magic = (data[0] << 8) | data[1];
if (received_magic != BRIDGE_PACKET_MAGIC) {
BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic);
return;
return false;
}
// Make a copy we can decrypt
@@ -530,7 +577,7 @@ void ESPNowBridge::processReceivedFrame(const uint8_t *mac,
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
// Try to decrypt (checksum + payload)
if (!xorCrypt(decrypted, encryptedDataLen)) return;
if (!xorCrypt(decrypted, encryptedDataLen)) return false;
// Validate checksum
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
@@ -539,11 +586,12 @@ void ESPNowBridge::processReceivedFrame(const uint8_t *mac,
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
// Failed to decrypt - likely from a different network
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum);
return;
return false;
}
BRIDGE_DEBUG_PRINTLN("RX, payload_len=%u\n", (unsigned)payloadLen);
receiveMeshPacket(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen);
receiveMeshPacket(packet_slot, decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen);
return true;
}
void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
+9 -3
View File
@@ -105,6 +105,7 @@ private:
mesh::Packet packet;
uint8_t next_frame;
bool started;
uint8_t start_retries;
};
QueuedTransmit _tx_queue[TX_QUEUE_DEPTH];
portMUX_TYPE _tx_mux = portMUX_INITIALIZER_UNLOCKED;
@@ -116,6 +117,11 @@ private:
volatile int _tx_callback_status;
volatile uint32_t _tx_dropped;
uint32_t _tx_dropped_reported;
uint32_t _tx_started_at = 0, _tx_retry_at = 0;
bool _sdk_teardown_failed = false;
static constexpr uint32_t TX_CALLBACK_TIMEOUT_MS = 1000;
static constexpr uint32_t TX_RETRY_DELAY_MS = 20;
static constexpr uint8_t TX_MAX_START_RETRIES = 3;
/** Raw-mode fragment assemblies, keyed by source MAC. */
mesh::espnow::ESPNowRawReassembler _raw_reassembler;
@@ -138,7 +144,7 @@ private:
bool xorCrypt(uint8_t *data, size_t len);
/** Parse and queue one serialized MeshCore packet. */
void receiveMeshPacket(const uint8_t *data, size_t len);
void receiveMeshPacket(mesh::Packet* packet, const uint8_t *data, size_t len);
/**
* ESP-NOW receive callback
@@ -151,8 +157,8 @@ private:
void queueReceivedFrame(const uint8_t *mac, const uint8_t *data, int len);
/** Parse a queued frame from loop(), never from the WiFi callback. */
void processReceivedFrame(const uint8_t *mac, const uint8_t *data,
size_t len);
bool processReceivedFrame(mesh::Packet* packet, const uint8_t *mac,
const uint8_t *data, size_t len);
/** Atomically append one logical packet's one or two transport frames. */
bool queueTransmitFrames(const mesh::espnow::ESPNowRawFrames& frames,
+35 -19
View File
@@ -8,7 +8,7 @@
#ifdef ESP32
// check_firmware_ram.py reserves this object plus the UART driver's runtime
// allocation even when a combined Full image starts with the UART disabled.
static_assert(sizeof(RS232Bridge) <= 4096, "Update the ESP32 UART heap budget");
static_assert(sizeof(RS232Bridge) <= 2304, "Update the ESP32 UART heap budget");
#endif
#if defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET)
// The combined RAK RAM policy reserves this object plus allocator metadata
@@ -24,6 +24,8 @@ RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
void RS232Bridge::begin() {
_initialized = false;
_rx_buffer_pos = 0;
_rx_packet_ready = false;
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
#if defined(ESP32)
if (!((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin)) return;
@@ -58,6 +60,20 @@ void RS232Bridge::end() {
// Update bridge state
_initialized = false;
_rx_buffer_pos = 0;
_rx_packet_ready = false;
}
void RS232Bridge::drainReceivedPackets() {
if (!_rx_packet_ready) return;
mesh::Packet* pkt = _mgr->allocNew();
if (!pkt) return;
const uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
const bool valid = pkt->readFrom(_rx_buffer + 4, len);
_rx_buffer_pos = 0;
_rx_packet_ready = false;
if (valid) onPacketReceived(pkt);
else _mgr->free(pkt);
}
void RS232Bridge::loop() {
@@ -66,8 +82,14 @@ void RS232Bridge::loop() {
return;
}
while (_serial->available()) {
uint8_t b = _serial->read();
drainReceivedPackets();
// Snapshot bounded work; a noisy UART must not starve mesh/radio service.
int remaining = _serial->available();
if (remaining > 512) remaining = 512;
while (remaining-- > 0 && !_rx_packet_ready) {
const int value = _serial->read();
if (value < 0) break;
const uint8_t b = static_cast<uint8_t>(value);
if (_rx_buffer_pos < 2) {
// Waiting for magic word
@@ -90,7 +112,7 @@ void RS232Bridge::loop() {
uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
// Validate length field
if (len > (MAX_TRANS_UNIT + 1)) {
if (len == 0 || len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("RX invalid length %d, resetting\n", len);
_rx_buffer_pos = 0; // Invalid length, reset
continue;
@@ -101,25 +123,17 @@ void RS232Bridge::loop() {
if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
BRIDGE_DEBUG_PRINTLN("RX, len=%d crc=0x%04x\n", len, received_checksum);
mesh::Packet *pkt = _mgr->allocNew();
if (pkt) {
if (pkt->readFrom(_rx_buffer + 4, len)) {
onPacketReceived(pkt);
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to parse packet\n");
_mgr->free(pkt);
}
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to allocate packet\n");
}
_rx_packet_ready = true;
drainReceivedPackets(); // continue this bounded batch when the pool is available
} else {
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04x\n", received_checksum);
}
_rx_buffer_pos = 0; // Reset for next packet
if (!_rx_packet_ready) _rx_buffer_pos = 0; // Keep a complete valid frame
}
}
}
}
drainReceivedPackets();
}
void RS232Bridge::sendPacket(mesh::Packet *packet) {
@@ -137,8 +151,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
if (!allowsPacket(packet)) return;
if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
const int expected = packet->getRawLength();
if (expected <= 0 || expected > MAX_TRANS_UNIT + 1) return;
uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
uint16_t len = packet->writeTo(buffer + 4);
@@ -160,7 +174,9 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
buffer[5 + len] = checksum & 0xFF; // Checksum low byte
// Send complete packet
_serial->write(buffer, len + SERIAL_OVERHEAD);
if (_serial->write(buffer, len + SERIAL_OVERHEAD) == static_cast<size_t>(len + SERIAL_OVERHEAD)) {
_seen_packets.markSeen(packet);
}
BRIDGE_DEBUG_PRINTLN("TX, len=%d crc=0x%04x\n", len, checksum);
}
+5
View File
@@ -142,6 +142,11 @@ private:
/** Buffer for building received packets */
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
// Retain the completed parser buffer while the shared pool is exhausted;
// unread bytes remain in the UART driver until this frame is delivered.
bool _rx_packet_ready = false;
void drainReceivedPackets();
/** Current position in the receive buffer */
uint16_t _rx_buffer_pos = 0;
};
+135 -117
View File
@@ -147,81 +147,10 @@ static bool is_cmd(const char* a, const char* names, const char** rest) {
// packet so it works as remote-admin over LoRa.
static bool handle_dev(const char* d, char* reply, OtaContext& c);
bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board) {
const auto* active = ota_context_if_active();
const float adaptive_pace = active ? active->manager.adaptivePacketSpeed() : OTA_SPEED_DEFAULT;
if (handleSpeedCommand(command, reply, 160, adaptive_pace)) return true;
const char* a = command + 3;
if (*a != 0 && *a != ' ') return false;
while (*a == ' ') a++;
if (!ota_acquire_context(reply, 160)) return true;
OtaContext& c = ota_ctx();
c.manager.set_clock(millis());
c.manager.set_speed(speedFactor());
#if defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE)
c.prepareTowerStorage();
#endif
const char* rest = a;
// ---- raw / internal primitives, tucked under `ota dev ...` ----
if (is_cmd(a, "dev", &rest)) {
return handle_dev(rest, reply, c);
}
// ---- help: list the commands in plain words (aliases in parentheses) ----
if (is_cmd(a, "help|?|h", &rest)) {
#if defined(OTA_SEEDER_ONLY)
snprintf(reply, 160,
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
"announce | folder | config. LoRa install is disabled.");
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | "
"bootloader | cancel | announce | self | folder | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
"cancel | announce | self | folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | storage | folder | config | key");
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"qspi | folder | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | qspi | "
"folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#if defined(OTA_TOWER_AUTO_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"storage | folder | cache | config | key");
#else
#if defined(OTA_SD_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"folder | cache | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | folder | "
"cache | config | key");
#endif
#endif
#else
snprintf(reply, 160,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | folder | "
"cache | config | key. `ota ls [page]`; folder [validate].");
#endif
// ---- inventory dashboard: running fw (self), the one fetch session, serving state ----
} else if (*a == 0 || is_cmd(a, "status|st", &rest)) {
// These explicit call boundaries keep unrelated control/staging buffers out of
// the target-name decoder call chain. This matters on the fixed 8 KiB nRF52
// loop stack, especially when listing is entered through a decrypted LoRa CLI.
static __attribute__((noinline)) bool handle_status(char* reply, OtaContext& c) {
#if defined(OTA_SEEDER_ONLY)
uint8_t dig[4]; c.manager.servedDigest(dig);
char dighx[9]; mesh::Utils::toHex(dighx, dig, 4);
@@ -304,49 +233,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
(unsigned)c.allow.count(), tenv ? tenv : "?");
#endif
#endif
return true;
}
// ---- admin OTA stats: crypto identities (our fw's content-id + body_hash), serving set, live fetch,
// policy - one dense line. The remote-admin CLI path is admin-gated, so this is admin-only over the
// mesh (send it from the app's repeater command screen, or the WiFi/serial OTA console).
} else if (is_cmd(a, "stats", &rest)) {
// our running fw's merkle content-id (mid) comes from the self serve entry; the EndF body_hash (fw
// identity, matched against a delta base) is separate - surface BOTH (only body_hash showed before).
const OtaManager::ServeEntry* self = nullptr;
for (uint8_t i = 0; i < c.manager.servedCount(); i++) {
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
if (e && e->is_self) { self = e; break; }
}
SelfFwInfo fi; bool sok = ota_self_firmware_for_display(fi);
char midhx[9], bodyhx[9], verbuf[20], dighx[9];
if (self) mesh::Utils::toHex(midhx, self->mid, 4); else strcpy(midhx, "?");
if (sok && fi.valid) mesh::Utils::toHex(bodyhx, fi.body_hash, 4); else strcpy(bodyhx, "?");
if (self) ver_str(verbuf, sizeof verbuf, self->fw_version); else strcpy(verbuf, "v?");
uint8_t dig[4]; c.manager.servedDigest(dig); mesh::Utils::toHex(dighx, dig, 4);
OtaManager::FetchState fs = c.manager.fetchState();
char fbuf[72];
if (fs == OtaManager::IDLE) {
strcpy(fbuf, "fetch idle");
} else {
char fmid[9]; mesh::Utils::toHex(fmid, c.manager.fetchManifestId(), 4);
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
if (fs == OtaManager::FAILED)
snprintf(fbuf, sizeof fbuf, "fetch failed:%s %u/%u id=%s",
fetch_error_word(c.manager.fetchError()), have, tot, fmid);
else
snprintf(fbuf, sizeof fbuf, "fetch %s %u/%u %u%% id=%s %us",
state_short(fs), have, tot, pct, fmid, age);
}
uint8_t af = c.manager.autofetch();
snprintf(reply, 160, "OTA | fw %s id=%s body=%s %ub %uK | serv %u dg=%s | %s | af=%s hops=%u",
verbuf, midhx, bodyhx, (unsigned)(self ? self->have_count : 0),
(unsigned)((sok ? fi.image_len : 0) / 1024), (unsigned)c.manager.servedCount(), dighx, fbuf,
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
(unsigned)c.manager.max_hops());
// ---- what's available around me (catalogued from beacons + OTA_HAVE), best/most-recent first ----
} else if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest)) {
static __attribute__((noinline)) bool handle_neighbors(
const char* rest, char* reply, OtaContext& c) {
// Kick a fresh round of catalog queries (async - rows arrive over the next seconds); render what we
// have now in plain words. The reply buffer is 160 B (serial / one LoRa packet for remote-admin), so
// the retained protocol catalog is rendered two rows at a time.
@@ -472,6 +363,133 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
shown++;
}
if (shown == 0) strcpy(reply, "No updates seen yet - re-run `ota ls` in a few seconds (just asked around).");
return true;
}
static __attribute__((noinline)) bool handle_control_command(
const char* a, char* reply, mesh::MainBoard& board, OtaContext& c);
bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board) {
const auto* active = ota_context_if_active();
const float adaptive_pace = active ? active->manager.adaptivePacketSpeed() : OTA_SPEED_DEFAULT;
if (handleSpeedCommand(command, reply, 160, adaptive_pace)) return true;
const char* a = command + 3;
if (*a != 0 && *a != ' ') return false;
while (*a == ' ') a++;
if (!ota_acquire_context(reply, 160)) return true;
OtaContext& c = ota_ctx();
c.manager.set_clock(millis());
c.manager.set_speed(speedFactor());
#if defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE)
c.prepareTowerStorage();
#endif
const char* rest = a;
if (*a == 0 || is_cmd(a, "status|st", &rest)) return handle_status(reply, c);
if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest))
return handle_neighbors(rest, reply, c);
return handle_control_command(a, reply, board, c);
}
static __attribute__((noinline)) bool handle_control_command(
const char* a, char* reply, mesh::MainBoard& board, OtaContext& c) {
const char* rest = a;
// ---- raw / internal primitives, tucked under `ota dev ...` ----
if (is_cmd(a, "dev", &rest)) {
return handle_dev(rest, reply, c);
}
// ---- help: list the commands in plain words (aliases in parentheses) ----
if (is_cmd(a, "help|?|h", &rest)) {
#if defined(OTA_SEEDER_ONLY)
snprintf(reply, 160,
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
"announce | folder | config. LoRa install is disabled.");
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | "
"bootloader | cancel | announce | self | folder | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
"cancel | announce | self | folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | storage | folder | config | key");
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"qspi | folder | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | qspi | "
"folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#if defined(OTA_TOWER_AUTO_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"storage | folder | cache | config | key");
#else
#if defined(OTA_SD_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | serve | "
"folder | cache | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | folder | "
"cache | config | key");
#endif
#endif
#else
snprintf(reply, 160,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | serve | folder | "
"cache | config | key. `ota ls [page]`; folder [validate].");
#endif
// ---- admin OTA stats: crypto identities (our fw's content-id + body_hash), serving set, live fetch,
// policy - one dense line. The remote-admin CLI path is admin-gated, so this is admin-only over the
// mesh (send it from the app's repeater command screen, or the WiFi/serial OTA console).
} else if (is_cmd(a, "stats", &rest)) {
// our running fw's merkle content-id (mid) comes from the self serve entry; the EndF body_hash (fw
// identity, matched against a delta base) is separate - surface BOTH (only body_hash showed before).
const OtaManager::ServeEntry* self = nullptr;
for (uint8_t i = 0; i < c.manager.servedCount(); i++) {
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
if (e && e->is_self) { self = e; break; }
}
SelfFwInfo fi; bool sok = ota_self_firmware_for_display(fi);
char midhx[9], bodyhx[9], verbuf[20], dighx[9];
if (self) mesh::Utils::toHex(midhx, self->mid, 4); else strcpy(midhx, "?");
if (sok && fi.valid) mesh::Utils::toHex(bodyhx, fi.body_hash, 4); else strcpy(bodyhx, "?");
if (self) ver_str(verbuf, sizeof verbuf, self->fw_version); else strcpy(verbuf, "v?");
uint8_t dig[4]; c.manager.servedDigest(dig); mesh::Utils::toHex(dighx, dig, 4);
OtaManager::FetchState fs = c.manager.fetchState();
char fbuf[72];
if (fs == OtaManager::IDLE) {
strcpy(fbuf, "fetch idle");
} else {
char fmid[9]; mesh::Utils::toHex(fmid, c.manager.fetchManifestId(), 4);
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
if (fs == OtaManager::FAILED)
snprintf(fbuf, sizeof fbuf, "fetch failed:%s %u/%u id=%s",
fetch_error_word(c.manager.fetchError()), have, tot, fmid);
else
snprintf(fbuf, sizeof fbuf, "fetch %s %u/%u %u%% id=%s %us",
state_short(fs), have, tot, pct, fmid, age);
}
uint8_t af = c.manager.autofetch();
snprintf(reply, 160, "OTA | fw %s id=%s body=%s %ub %uK | serv %u dg=%s | %s | af=%s hops=%u",
verbuf, midhx, bodyhx, (unsigned)(self ? self->have_count : 0),
(unsigned)((sok ? fi.image_len : 0) / 1024), (unsigned)c.manager.servedCount(), dighx, fbuf,
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
(unsigned)c.manager.max_hops());
// ---- start fetching a specific catalogued mOTA (by list index or manifest_id) ----
} else if (is_cmd(a, "pull|get|download", &rest)) {
@@ -10,6 +10,7 @@ protected:
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
public:
~CustomLLCC68Wrapper() { unregisterInterruptAction(); }
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void powerOff() { _radio->standby(); _radio->sleep(); }
@@ -13,6 +13,7 @@ class CustomLR1110Wrapper : public RadioLibWrapper {
DeepInitCallback _deep_init;
public:
~CustomLR1110Wrapper() { unregisterInterruptAction(); }
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board)
: RadioLibWrapper(radio, board), _deep_init(NULL) { }
bool supportsCarrierWave() const override { return true; }
@@ -6,6 +6,7 @@
class CustomLR1121Wrapper : public RadioLibWrapper {
public:
~CustomLR1121Wrapper() { unregisterInterruptAction(); }
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void powerOff() {
+23 -8
View File
@@ -156,6 +156,13 @@ class CustomLR2021 : public LR2021 {
}
int16_t startReceive() override {
// setRxPath requires standby, including retries after failed read cleanup.
int16_t status = standby();
if (status != RADIOLIB_ERR_NONE) return status;
// Failed cleanup can leave bytes behind; no new RX window may inherit
// the preceding FIFO even when the first discard attempt failed.
status = clearRxFifo();
if (status != RADIOLIB_ERR_NONE) return status;
// include the PREAMBLE_DETECTED irq bit in reported flags
return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
@@ -165,18 +172,10 @@ class CustomLR2021 : public LR2021 {
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED; // bit 5
bool header = irq & RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID; // bit 6
bool hdrErr = irq & RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR; // bit 9
uint32_t now = millis();
if (hdrErr) {
clearIrqFlags(RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID | RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
@@ -203,6 +202,22 @@ class CustomLR2021 : public LR2021 {
return false;
}
int16_t readData(uint8_t* data, size_t len) override {
uint16_t fifo_level = 0;
const int16_t status = getRxFifoLevel(&fifo_level);
if (status != RADIOLIB_ERR_NONE) return status;
const size_t expected = len != 0 ? len : getPacketLength();
if (fifo_level != expected || expected == 0) {
// RX_DONE with an empty FIFO, or overlapping continuous-RX packets,
// must not turn repeated padding or mixed bytes into a mesh packet.
// Consume the suspect FIFO so a subsequent clean frame can recover.
const int16_t cleared = clearRxFifo();
if (cleared != RADIOLIB_ERR_NONE) return cleared;
return RADIOLIB_ERR_PACKET_TOO_SHORT;
}
return LR2021::readData(data, len);
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
@@ -16,6 +16,7 @@
class CustomLR2021Wrapper : public RadioLibWrapper {
public:
~CustomLR2021Wrapper() { unregisterInterruptAction(); }
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void powerOff() { _radio->standby(); _radio->sleep(); }
@@ -11,6 +11,7 @@ protected:
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
public:
~CustomSTM32WLxWrapper() { unregisterInterruptAction(); }
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
protected:
@@ -35,6 +35,7 @@ protected:
}
public:
~CustomSX1262Wrapper() { unregisterInterruptAction(); }
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsCarrierWave() const override { return true; }
@@ -14,6 +14,7 @@ protected:
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
public:
~CustomSX1268Wrapper() { unregisterInterruptAction(); }
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void powerOff() { _radio->standby(); _radio->sleep(); }
@@ -9,6 +9,7 @@
class CustomSX1276Wrapper : public RadioLibWrapper {
public:
~CustomSX1276Wrapper() { unregisterInterruptAction(); }
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsCarrierWave() const override { return supportsRadioDeepInit(); }
@@ -0,0 +1,27 @@
#pragma once
#include <stdint.h>
// One radio ISR publishes; its owning loop consumes. Aligned word loads/stores
// are sufficient even on Cortex-M0, where an atomic exchange needs a lock. A
// mode transition never writes the producer's generation, so an IRQ arriving
// during claim remains pending. Hardware IRQ handlers do not reenter themselves.
class RadioInterruptEvents {
uint32_t _published = 0;
uint32_t _consumed = 0;
public:
__attribute__((always_inline)) inline void record() {
const uint32_t next = __atomic_load_n(&_published, __ATOMIC_RELAXED) + 1;
__atomic_store_n(&_published, next, __ATOMIC_RELEASE);
}
bool pending() const {
return __atomic_load_n(&_published, __ATOMIC_ACQUIRE) != _consumed;
}
bool claim() {
const uint32_t current = __atomic_load_n(&_published, __ATOMIC_ACQUIRE);
const bool ready = current != _consumed;
_consumed = current;
return ready;
}
};
+173 -43
View File
@@ -1,12 +1,12 @@
#define RADIOLIB_STATIC_ONLY 1
#include "RadioLibWrappers.h"
#include "RadioInterruptEvents.h"
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define STATE_TX_DONE 4
#define STATE_INT_READY 16
// Bounded noise-floor calibration in continuous RX, including RXPS refreshes.
// Requests are coalesced so retries cannot repeatedly force continuous RX.
@@ -19,7 +19,24 @@
// sample set every fifteen minutes instead.
#define NF_FAST_PROFILE_REFRESH_INTERVAL_MS (15UL * 60UL * 1000UL)
static volatile uint8_t state = STATE_IDLE;
// RadioLib callbacks carry no context. The internal STM32WLx radio needs one
// physical slot; other classes retain two, including external STM32 radios.
// Dual-profile scanning uses one slot. ISR storage has static lifetime, so a
// callback already in flight cannot access a destroyed/rebound wrapper.
#define RADIO_INTERRUPT_INTERNAL_CustomSTM32WLx 1
#define RADIO_INTERRUPT_INTERNAL_(radio) RADIO_INTERRUPT_INTERNAL_##radio
#define RADIO_INTERRUPT_INTERNAL(radio) RADIO_INTERRUPT_INTERNAL_(radio)
#if RADIO_INTERRUPT_INTERNAL(RADIO_CLASS)
#define RADIO_INTERRUPT_SLOT_COUNT 1
#else
#define RADIO_INTERRUPT_SLOT_COUNT 2
#endif
#undef RADIO_INTERRUPT_INTERNAL
#undef RADIO_INTERRUPT_INTERNAL_
#undef RADIO_INTERRUPT_INTERNAL_CustomSTM32WLx
static RadioInterruptEvents radio_interrupt_events[RADIO_INTERRUPT_SLOT_COUNT];
static RadioLibWrapper* radio_interrupt_owners[RADIO_INTERRUPT_SLOT_COUNT] = {};
// The Adafruit nRF52 core's micros() falls back to 1024 Hz RTOS ticks when
// DWT is disabled. A sub-millisecond retune then appears to take exactly
@@ -66,15 +83,76 @@ static uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool dual_profile
#endif
}
// this function is called when a complete packet
// is transmitted by the module
static
#if defined(ESP8266) || defined(ESP32)
ICACHE_RAM_ATTR
#endif
void setFlag(void) {
// we sent a packet, set the flag
state |= STATE_INT_READY;
void setFlag0() { radio_interrupt_events[0].record(); }
#if RADIO_INTERRUPT_SLOT_COUNT > 1
static
#if defined(ESP8266) || defined(ESP32)
ICACHE_RAM_ATTR
#endif
void setFlag1() { radio_interrupt_events[1].record(); }
#endif
bool RadioLibWrapper::registerInterruptAction() {
_loop_event_pending = false;
#if RADIO_INTERRUPT_SLOT_COUNT == 1
// There is only one integrated radio. Ownership is still checked so a
// second wrapper cannot steal its callback or consume its pending event.
if (radio_interrupt_owners[0] != nullptr && radio_interrupt_owners[0] != this) {
_interrupt_slot = 0xff;
return false;
}
radio_interrupt_owners[0] = this;
_interrupt_slot = 0;
_radio->clearPacketReceivedAction();
radio_interrupt_events[0].claim();
_radio->setPacketReceivedAction(setFlag0);
#else
if (_interrupt_slot >= RADIO_INTERRUPT_SLOT_COUNT || radio_interrupt_owners[_interrupt_slot] != this) {
_interrupt_slot = 0xff;
for (uint8_t i = 0; i < RADIO_INTERRUPT_SLOT_COUNT; ++i) {
if (radio_interrupt_owners[i] == nullptr) {
radio_interrupt_owners[i] = this;
_interrupt_slot = i;
break;
}
}
}
if (_interrupt_slot >= RADIO_INTERRUPT_SLOT_COUNT) return false;
_radio->clearPacketReceivedAction();
radio_interrupt_events[_interrupt_slot].claim();
_radio->setPacketReceivedAction(_interrupt_slot == 0 ? setFlag0 : setFlag1);
#endif
return true;
}
void RadioLibWrapper::unregisterInterruptAction() {
#if RADIO_INTERRUPT_SLOT_COUNT == 1
if (radio_interrupt_owners[0] == this) {
_radio->clearPacketReceivedAction();
radio_interrupt_owners[0] = nullptr;
}
#else
if (_interrupt_slot < RADIO_INTERRUPT_SLOT_COUNT && radio_interrupt_owners[_interrupt_slot] == this) {
_radio->clearPacketReceivedAction();
radio_interrupt_owners[_interrupt_slot] = nullptr;
}
#endif
_interrupt_slot = 0xff;
}
bool RadioLibWrapper::hasPendingRadioInterrupt() const {
return _loop_event_pending || (_interrupt_slot < RADIO_INTERRUPT_SLOT_COUNT && radio_interrupt_events[_interrupt_slot].pending());
}
bool RadioLibWrapper::claimRadioInterrupt() {
const bool startup = _loop_event_pending;
_loop_event_pending = false;
const bool interrupt = _interrupt_slot < RADIO_INTERRUPT_SLOT_COUNT && radio_interrupt_events[_interrupt_slot].claim();
return startup || interrupt;
}
void RadioLibWrapper::begin() {
@@ -84,7 +162,10 @@ void RadioLibWrapper::begin() {
// still initializes lazily if an earlier crypto operation runs first.
(void) mesh::initializeCC310Crypto();
#endif
_radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt
if (!registerInterruptAction()) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: no interrupt callback slot");
return;
}
_radio->setPreambleLength(currentPreambleLength()); // longer preamble for lower SF improves reliability
#if defined(LORA_FREQ) && defined(LORA_BW) && defined(LORA_SF)
if (!_params_valid) {
@@ -109,7 +190,7 @@ void RadioLibWrapper::begin() {
state = STATE_IDLE;
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
setFlag(); // LoRa packet is already received
_loop_event_pending = true; // Main-owned wake event; ISR remains sole publisher
}
_noise_floor = 0;
@@ -176,11 +257,11 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
uint8_t RadioLibWrapper::beginReconfigure() {
if (_cw_active) return 2;
const uint8_t base_state = state & ~STATE_INT_READY;
const uint8_t base_state = state;
// On SX126x/LR11xx duty-cycle RX, BUSY may remain asserted during the sleep
// side of the cycle. Do not issue an IRQ/preamble query over SPI then; due
// scheduled work will retry as soon as the next safe listen window opens.
if ((state & STATE_INT_READY) != 0 || base_state == STATE_TX_WAIT
if (hasPendingRadioInterrupt() || base_state == STATE_TX_WAIT
|| isChipBusy() || isReceivingPacket()) {
return 2;
}
@@ -191,6 +272,10 @@ uint8_t RadioLibWrapper::beginReconfigure() {
} else if (resume_rx) {
_radio->standby();
}
// An RX completion during standby still owns its FIFO and old profile.
// Defer reconfiguration until recvRaw consumes it, rather than rearming and
// clearing that completion as part of endReconfigure().
if (hasPendingRadioInterrupt()) return 2;
state = STATE_IDLE;
return resume_rx;
}
@@ -204,7 +289,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() {
_rx_ps_armed = false;
state = STATE_IDLE;
_radio->setPacketReceivedAction(setFlag);
if (!registerInterruptAction()) return false;
bool restored;
if (_params_valid) {
@@ -663,8 +748,8 @@ void RadioLibWrapper::resetAGC() {
bool RadioLibWrapper::recoverRadio(bool hard) {
if (_cw_active) return false;
const uint8_t base_state = state & ~STATE_INT_READY;
if ((state & STATE_INT_READY) != 0 || base_state == STATE_TX_WAIT) return false;
const uint8_t base_state = state;
if (hasPendingRadioInterrupt() || base_state == STATE_TX_WAIT) return false;
#ifdef RADIO_LIVENESS_SOFT_ONLY
// STM32WL integrates the radio into the MCU and has no independent reset.
@@ -709,7 +794,7 @@ void RadioLibWrapper::rxPsWatchdogCheck() {
// don't interfere mid-transmit or with a completed-but-unread packet
// (a pending DIO1 event is itself proof the radio is alive; recvRaw() will
// re-arm and re-base the watchdog)
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) {
if (hasPendingRadioInterrupt() || state == STATE_TX_WAIT) {
_wd_observe_until = 0;
return;
}
@@ -780,15 +865,11 @@ void RadioLibWrapper::rxPsWatchdogCheck() {
void RadioLibWrapper::requestRestartRecv() {
// An RX interrupt can arrive between the caller's idle check and this state
// transition. Preserve that flag so the completed packet is still consumed.
noInterrupts();
if ((state & ~STATE_INT_READY) != STATE_TX_WAIT) {
state &= STATE_INT_READY;
}
interrupts();
if (state != STATE_TX_WAIT) state = STATE_IDLE;
}
bool RadioLibWrapper::isPacketPendingOrReceiving() {
return (state & STATE_INT_READY) != 0 || isReceivingPacket();
return hasPendingRadioInterrupt() || isReceivingPacket();
}
void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) {
@@ -828,7 +909,7 @@ void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) {
// force a receive re-arm back into duty-cycle mode, but don't clobber a
// completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished()
// will re-arm right after those anyway; same guard style as setRxPowerSaving)
if ((state & ~STATE_INT_READY) != STATE_TX_WAIT
if (state != STATE_TX_WAIT
&& !isPacketPendingOrReceiving()) {
requestRestartRecv();
}
@@ -924,11 +1005,14 @@ bool RadioLibWrapper::stopCarrierWave() {
_cw_retry_at = static_cast<uint32_t>(millis()) + 100;
return false; // hold normal work until stopping the carrier is confirmed
}
if (!registerInterruptAction()) {
_cw_retry_at = static_cast<uint32_t>(millis()) + 100;
return false;
}
_cw_active = false;
_cw_stopping = false;
_rx_ps_armed = false;
state = STATE_IDLE;
_radio->setPacketReceivedAction(setFlag);
_profile_refresh_required = true;
const uint8_t restore = _profiles.enabled() ? _cw_restore_profile : 0;
const bool tuned = tuneProfile(restore) == mesh::RadioParamApplyResult::APPLIED;
@@ -1103,9 +1187,7 @@ void RadioLibWrapper::startRecv() {
// A very short frame may complete while startReceiveMode() returns.
_profile_visit_stamp = profileTimestamp(_profiles.enabled());
// Retain that RX interrupt instead of overwriting it with software state.
noInterrupts();
state = (state & STATE_INT_READY) | STATE_RX;
interrupts();
state = STATE_RX;
// RSSI is unsettled after every RX entry, including CAD/TX completion.
// Keep the partial block, but wait for the frontend before its next sample.
_nf_sample_from = millis() + NF_CALIB_SETTLE_MS;
@@ -1145,18 +1227,25 @@ void RadioLibWrapper::stopReceiveDutyCycle() {
}
bool RadioLibWrapper::isPacketReady() {
if (!_rx_ps_armed) return true; // continuous RX: DIO1 only fires for RxDone/TxDone here
// In duty-cycle RX the DIO1 interrupt also fires for RX timeout (false
// preamble detect) and header errors. GetRxBufferStatus still reports the
// *previous* packet's length then, so reading the buffer would re-deliver
// stale bytes as a ghost packet. Only read when the radio reports RxDone.
// (checkIrq errors are treated as ready, falling back to old behaviour.)
return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0;
// An unsupported probe retains the legacy event-only behavior.
const int16_t ready = _radio->checkIrq(RADIOLIB_IRQ_RX_DONE);
if (ready <= 0 && ready != RADIOLIB_ERR_UNSUPPORTED) {
// DIO can remain high after a transient SPI error, so no second edge will
// wake us. Retry the probe without treating the error as a valid packet.
_loop_event_pending = ++_irq_probe_failures < 3;
if (!_loop_event_pending) _irq_probe_failures = 0;
return false;
}
_irq_probe_failures = 0;
return ready > 0 || ready == RADIOLIB_ERR_UNSUPPORTED;
}
bool RadioLibWrapper::isInRecvMode() const {
return (state & ~STATE_INT_READY) == STATE_RX;
return state == STATE_RX;
}
// RX PowerSaving
@@ -1187,9 +1276,23 @@ bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sl
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
if (_cw_active) return 0;
int len = 0;
if (state & STATE_INT_READY) {
#if defined(USE_LR2021)
bool receiver_stopped = false;
#endif
if (claimRadioInterrupt()) {
last_radio_interrupt_millis = millis(); // ISR fired -> radio hardware is alive
if (isPacketReady()) {
#if defined(USE_LR2021)
// The LR2021 base reader clears FIFO/IRQs while otherwise staying in
// continuous RX. Freeze reception before taking length/status/FIFO
// snapshots so a new packet cannot be cleared halfway through the read.
if (_radio->standby() != RADIOLIB_ERR_NONE) {
n_recv_errors++;
state = STATE_IDLE;
return 0;
}
receiver_stopped = true;
#endif
if (_rx_ps_armed) {
// RxDone stops the active receive window, but the duty-cycle RTC can
// still be running. Stop it before reading and re-arming another mode.
@@ -1214,11 +1317,21 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
}
}
}
// Retry a transient IRQ-status probe before rearming/clearing hardware.
if (_loop_event_pending) return 0;
#if defined(USE_LR2021)
state = STATE_RX; // LR2021 stays in Rx after readData, calling startReceive while still in Rx throws -706 errors
#else
state = STATE_IDLE; // need another startReceive()
// An early FIFO/SPI failure must not leave RX_DONE asserted. Standby also
// covers spurious events for which no packet was read. No new hardware RX
// event can arrive during this acknowledgement; rearm happens afterwards.
if ((!receiver_stopped && _radio->standby() != RADIOLIB_ERR_NONE)
|| static_cast<LR2021*>(_radio)->clearRxFifo() != RADIOLIB_ERR_NONE
|| _radio->clearIrqFlags(RADIOLIB_LR2021_IRQ_RX_DONE) != RADIOLIB_ERR_NONE) {
n_recv_errors++;
state = STATE_IDLE;
return 0;
}
#endif
state = STATE_IDLE; // need another startReceive()
}
if (len > 0 && _rx_ps_enabled && !_rx_ps_continuous_fallback) {
@@ -1247,13 +1360,13 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
void RadioLibWrapper::finishReceiveProcessing() {
if (!_rx_hold_continuous) return;
if ((state & ~STATE_INT_READY) == STATE_TX_WAIT) {
if (state == STATE_TX_WAIT) {
_rx_hold_continuous = false;
return;
}
// A second packet may have completed while Dispatcher handled the first.
// Leave continuous RX intact until recvRaw() consumes that pending packet.
if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) {
if (hasPendingRadioInterrupt() || isReceivingPacket()) {
return;
}
@@ -1277,7 +1390,7 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
}
bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
if (_cw_active) return false;
if (_cw_active || hasPendingRadioInterrupt()) return false;
_rx_hold_continuous = false;
if (_rx_ps_armed) {
// stop the duty-cycle sequencer before SetTx, otherwise its next RTC
@@ -1285,6 +1398,11 @@ bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
stopReceiveDutyCycle();
}
_board->onBeforeTransmit();
if (hasPendingRadioInterrupt()) {
_board->onAfterTransmit();
return false;
}
_irq_probe_failures = 0;
int err = _radio->startTransmit((uint8_t *) bytes, len);
if (err == RADIOLIB_ERR_NONE) {
state = STATE_TX_WAIT;
@@ -1302,7 +1420,15 @@ bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
}
bool RadioLibWrapper::isSendComplete() {
if (state & STATE_INT_READY) {
if (state == STATE_TX_WAIT && claimRadioInterrupt()) {
const int16_t done = _radio->checkIrq(RADIOLIB_IRQ_TX_DONE);
if (done <= 0 && done != RADIOLIB_ERR_UNSUPPORTED) {
// getIrqFlags() may hide a failed SPI read as zero. Once DIO fired,
// keep probing until TX_DONE is observed or Dispatcher expires this TX.
_loop_event_pending = true;
return false;
}
_irq_probe_failures = 0;
state = STATE_IDLE;
n_sent++;
return true;
@@ -1312,6 +1438,10 @@ bool RadioLibWrapper::isSendComplete() {
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
// Receive has not been rearmed yet: these are completion/probe events for
// the retired TX. They must not block the Dispatcher's timeout recovery.
claimRadioInterrupt();
_irq_probe_failures = 0;
_board->onAfterTransmit();
state = STATE_IDLE;
}
@@ -1347,7 +1477,7 @@ int16_t RadioLibWrapper::performChannelScanWithTimeout(unsigned long timeout_ms)
bool RadioLibWrapper::isChannelActive() {
if (_cw_active) return true;
if (isPacketPendingOrReceiving() || (state & ~STATE_INT_READY) == STATE_TX_WAIT) return true;
if (isPacketPendingOrReceiving() || state == STATE_TX_WAIT) return true;
// int.thresh: RSSI-based interference detection (relative to noise floor).
// In RX duty-cycle mode only checked while the chip is in a listen window
// (during the sleep window the frontend is off and the read would stall).
@@ -1362,12 +1492,12 @@ bool RadioLibWrapper::isChannelActive() {
stopReceiveDutyCycle();
}
int16_t result = performChannelScan();
// scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY
// via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't
// CAD also publishes an IRQ event. Consume it before restarting RX so recvRaw() doesn't
// try to read a non-existent packet and count a spurious recv error.
claimRadioInterrupt();
state = STATE_IDLE;
startRecv();
if (result != RADIOLIB_CHANNEL_FREE || (state & ~STATE_INT_READY) != STATE_RX
if (result != RADIOLIB_CHANNEL_FREE || state != STATE_RX
|| isPacketPendingOrReceiving()) {
_board->n_cad_busy++;
return true;
+16 -1
View File
@@ -29,6 +29,15 @@ struct PacketMillis {
class RadioLibWrapper : public mesh::Radio {
protected:
// Mode is owned by this loop, not by the ISR or another wrapper.
uint8_t state = 0;
uint8_t _interrupt_slot = 0xff;
bool _loop_event_pending = false;
uint8_t _irq_probe_failures = 0;
bool registerInterruptAction();
void unregisterInterruptAction();
bool hasPendingRadioInterrupt() const;
bool claimRadioInterrupt();
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent, n_recv_errors;
@@ -217,7 +226,7 @@ protected:
void checkReceiveMode(uint32_t now);
virtual void doResetAGC();
public:
protected:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board)
: _radio(&radio), _board(&board), _noise_floor_valid(false), _nf_refresh_requested(true),
_rx_ps_enabled(false), _rx_ps_armed(false),
@@ -237,6 +246,12 @@ public:
_cad_scan_timeout_override_ms = 0;
}
// Concrete wrappers detach their callbacks before base destruction. Keep
// this destructor trivial so compact targets do not need a base vtable; a
// protected lifetime also rejects deletion through a borrowed base pointer.
~RadioLibWrapper() = default;
public:
void begin() override;
mesh::RadioParamApplyResult setCarrierWave(uint8_t profile, uint32_t duration_ms) override;
bool isCarrierWaveActive() const override { return _cw_active; }
+4 -4
View File
@@ -1,14 +1,14 @@
#include "SSD1306Display.h"
#if defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET)
#if defined(ESP32_PLATFORM) || (defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET))
// Adafruit_SSD1306::begin allocates WIDTH * ((HEIGHT + 7) / 8) once and
// reuses that framebuffer on later begin calls. The driver itself is embedded
// in the linked global display, not allocated on the heap. Even including
// that object again plus allocator overhead fits the combined 2 KiB bound.
// that object again plus allocator overhead fits the 2 KiB runtime bound.
static_assert(SSD1306Display::FRAMEBUFFER_BYTES == 1024,
"Update the combined RAK OLED framebuffer budget");
"Update the SSD1306 OLED framebuffer budget");
static_assert(sizeof(SSD1306Display) + SSD1306Display::FRAMEBUFFER_BYTES + 32 <= 2048,
"Update the combined RAK OLED runtime budget");
"Update the SSD1306 OLED runtime budget");
#endif
bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {