From a33b0bbdf24ed09022d7e38bb84e982b99a8cda5 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 5 Oct 2026 02:48:05 +0000 Subject: [PATCH] Radio: optional radio task, slot gate and measured TX lead Three compile-time switches, all off by default: - MESHTASTIC_RADIO_TASK (ESP32, nRF52): runs the RadioLib radio thread from its own FreeRTOS task, one priority above the loop and below the RX readout task. The task holds a recursive radio lock while the thread runs; loop-side entry points into the radio take it with RADIO_TASK_LOCK(). The static packet pool claims slots in a critical section. The task's log lines are queued and printed by the loop (MESHTASTIC_RADIO_TASK_LOG_LINES sets the depth; MESHTASTIC_RADIO_TASK_LOG_DIRECT writes them from the task instead), and no log line sits between the TX timer and SET_TX. - MESHTASTIC_TX_SLOT_GATE_MS=: a TX with a slot parity scans only in the first of a slot of its parity on the frame-end grid. The timer fires MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS (default 2) slots early and the radio thread waits out the rest; a timer further ahead is set again (MESHTASTIC_TX_SLOT_GATE_REARM=0 scans at once instead). - MESHTASTIC_TX_SLOT_LEAD (needs the gate): aims SET_TX MESHTASTIC_TX_SLOT_SET_TX_AT_MS (default 1) into the slot, starting the scan the measured scan-to-SET_TX time before it and firing the timer the measured wake latency plus MESHTASTIC_TX_SLOT_LEAD_GUARD_MS (default 1) before the scan. Works with the early TX stage on a CH341 host. Co-Authored-By: Jonathan Bennett Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 --- src/RedirectablePrint.cpp | 85 ++++++++++++- src/RedirectablePrint.h | 5 + src/SerialConsole.cpp | 2 +- src/concurrency/NotifiedWorkerThread.cpp | 9 +- src/concurrency/NotifiedWorkerThread.h | 9 ++ src/concurrency/OSThread.cpp | 25 +++- src/concurrency/OSThread.h | 7 + src/concurrency/RadioTask.cpp | 130 +++++++++++++++++++ src/concurrency/RadioTask.h | 55 ++++++++ src/main.cpp | 7 + src/mesh/FloodingRouter.cpp | 9 +- src/mesh/HardwareRNG.cpp | 1 + src/mesh/MemoryPool.h | 27 +++- src/mesh/NodeDB.cpp | 2 + src/mesh/RadioInterface.cpp | 39 ++++++ src/mesh/RadioInterface.h | 45 ++++++- src/mesh/RadioLibInterface.cpp | 155 ++++++++++++++++++++++- src/mesh/RadioLibInterface.h | 22 ++++ src/mesh/Router.cpp | 21 ++- src/mesh/Router.h | 6 + src/modules/MeshBeaconModule.cpp | 2 + 21 files changed, 644 insertions(+), 19 deletions(-) create mode 100644 src/concurrency/RadioTask.cpp create mode 100644 src/concurrency/RadioTask.h diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 392ca7bc56..5023434cb1 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -17,9 +17,59 @@ #include "platform/portduino/PortduinoGlue.h" #endif +#ifdef MESHTASTIC_RADIO_TASK +#include "concurrency/RadioTask.h" +#endif + #if HAS_NETWORKING extern meshtastic::Syslog syslog; #endif +#if defined(MESHTASTIC_RADIO_TASK) && !defined(MESHTASTIC_RADIO_TASK_LOG_DIRECT) +namespace +{ +// The radio task's lines, formatted where they were logged and printed by the loop. One writer (the radio task) and one +// reader (the loop), so the indexes need no lock. +struct RadioTaskLog { + const char *level; + const concurrency::OSThread *thread; + char text[240]; +}; +#ifndef MESHTASTIC_RADIO_TASK_LOG_LINES +#ifdef ARCH_ESP32 +#define MESHTASTIC_RADIO_TASK_LOG_LINES 64 // HWCDC prints a line in 4-6 ms, so bursts back up further +#else +#define MESHTASTIC_RADIO_TASK_LOG_LINES 16 +#endif +#endif +static_assert(MESHTASTIC_RADIO_TASK_LOG_LINES <= 255, "the queue's indexes are uint8_t"); +constexpr uint8_t radioTaskLogCount = MESHTASTIC_RADIO_TASK_LOG_LINES; +RadioTaskLog radioTaskLogs[radioTaskLogCount]; +volatile uint8_t radioTaskLogHead, radioTaskLogTail; +volatile uint32_t radioTaskLogsDropped; +} // namespace + +void RedirectablePrint::drainRadioTaskLogs() +{ + static uint32_t droppedReported; + const uint32_t dropped = radioTaskLogsDropped; + if (dropped != droppedReported) { + log(MESHTASTIC_LOG_LEVEL_WARN, "Radio task log full, %u lines dropped", (unsigned)(dropped - droppedReported)); + droppedReported = dropped; + } + while (radioTaskLogTail != radioTaskLogHead) { + __asm__ __volatile__("" ::: "memory"); // read the entry only after seeing the head that published it + const RadioTaskLog &line = radioTaskLogs[radioTaskLogTail]; + concurrency::setLoggingFor(line.thread); + log(line.level, "%s", line.text); + concurrency::clearLoggingFor(); + __asm__ __volatile__("" ::: "memory"); // and free its slot only after printing it + radioTaskLogTail = (uint8_t)((radioTaskLogTail + 1) % radioTaskLogCount); + } +} +#elif defined(MESHTASTIC_RADIO_TASK) +void RedirectablePrint::drainRadioTaskLogs() {} +#endif + void RedirectablePrint::rpInit() { #ifdef HAS_FREE_RTOS @@ -175,7 +225,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, printf("| ??:??:?? %u ", millis() / 1000); #endif } - auto thread = concurrency::OSThread::currentThread; + auto thread = concurrency::OSThread::current(); // the tag is printed by vprintf, which knows whether the formatted // message already carries one of its own @@ -216,7 +266,7 @@ void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, default: ll = 0; } - auto thread = concurrency::OSThread::currentThread; + auto thread = concurrency::OSThread::current(); if (thread) { syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); } else { @@ -239,7 +289,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_ isBleConnected = linuxBluetooth != nullptr && linuxBluetooth->isEnabled() && linuxBluetooth->isConnected(); #endif if (isBleConnected) { - auto thread = concurrency::OSThread::currentThread; + auto thread = concurrency::OSThread::current(); meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; logRecord.level = getLogLevel(logLevel); vsnprintf(logRecord.message, sizeof(logRecord.message), format, arg); @@ -326,6 +376,35 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { return; } +#if defined(MESHTASTIC_RADIO_TASK) && !defined(MESHTASTIC_RADIO_TASK_LOG_DIRECT) + // The radio task never writes the console itself (-DMESHTASTIC_RADIO_TASK_LOG_DIRECT: it does). A second task writing + // USB CDC can wedge the console, and the write would cost the radio its timing. The loop prints the line. + if (concurrency::inRadioTask()) { + const uint8_t next = (uint8_t)((radioTaskLogHead + 1) % radioTaskLogCount); + if (next == radioTaskLogTail) { + radioTaskLogsDropped = radioTaskLogsDropped + 1; + return; + } + RadioTaskLog &line = radioTaskLogs[radioTaskLogHead]; + line.level = logLevel; + line.thread = concurrency::OSThread::current(); + va_list arg; + va_start(arg, format); + vsnprintf(line.text, sizeof(line.text), format, arg); + va_end(arg); + __asm__ __volatile__("" ::: "memory"); // publish the entry before the head that points past it + radioTaskLogHead = next; + concurrency::mainDelay.interrupt(); // the loop prints it on its next pass + return; + } +#endif + + // append \n to format + size_t len = strlen(format); + auto newFormat = std::unique_ptr(new char[len + 2]); + strcpy(newFormat.get(), format); + newFormat[len] = '\n'; + newFormat[len + 1] = '\0'; // Append \n here rather than in every call site; past the early returns, so a filtered line allocates nothing size_t len = strlen(format); diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 216315464a..7f18a36211 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -47,6 +47,11 @@ class RedirectablePrint : public Print void hexDump(const char *logLevel, const unsigned char *buf, uint16_t len); +#ifdef MESHTASTIC_RADIO_TASK + /// From the loop: print the lines the radio task logged since the last call + void drainRadioTaskLogs(); +#endif + std::string mt_sprintf(const std::string fmt_str, ...); protected: diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 0cbb09870b..9d3b9662e4 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -346,7 +346,7 @@ void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_l if (usingProtobufs) { if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) { meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel); - auto thread = concurrency::OSThread::currentThread; + auto thread = concurrency::OSThread::current(); emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); } return; diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 8377b2e98a..b8fcfd295a 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -15,7 +15,7 @@ bool NotifiedWorkerThread::notify(uint32_t v, bool overwrite) bool r = notifyCommon(v, overwrite); if (r) - mainDelay.interrupt(); + wakeDelay->interrupt(); return r; } @@ -27,8 +27,9 @@ IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) { if (overwrite || notification == 0) { enabled = true; - setInterval(0); // Run ASAP - runASAP = true; + setInterval(0); // Run ASAP + if (wakeDelay == &mainDelay) // another task's controller is no reason for the loop to skip its sleep + runASAP = true; notification = v; if (debugNotification) { @@ -52,7 +53,7 @@ IRAM_ATTR bool NotifiedWorkerThread::notifyFromISR(BaseType_t *highPriWoken, uin { bool r = notifyCommon(v, overwrite); if (r) - mainDelay.interruptFromISR(highPriWoken); + wakeDelay->interruptFromISR(highPriWoken); return r; } diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index 6fa5777d6f..7e4283d1df 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -16,9 +16,18 @@ class NotifiedWorkerThread : public OSThread */ std::atomic notification{0}; + /// What a notification interrupts so the thread's controller runs it: mainDelay, unless a task runs the controller + InterruptableDelay *wakeDelay = &mainDelay; + public: NotifiedWorkerThread(const char *name) : OSThread(name) {} + /// For a thread run by a controller other than mainController, from a task waiting on wake + NotifiedWorkerThread(const char *name, ThreadController *controller, InterruptableDelay *wake) + : OSThread(name, 0, controller), wakeDelay(wake) + { + } + /** * Notify this thread so it can run */ diff --git a/src/concurrency/OSThread.cpp b/src/concurrency/OSThread.cpp index 058157f204..502098e33c 100644 --- a/src/concurrency/OSThread.cpp +++ b/src/concurrency/OSThread.cpp @@ -1,4 +1,5 @@ #include "OSThread.h" +#include "concurrency/RadioTask.h" #include "configuration.h" #include "memGet.h" #include @@ -16,6 +17,21 @@ bool OSThread::showRun = false; bool OSThread::showWaiting = false; const OSThread *OSThread::currentThread; +#ifdef MESHTASTIC_RADIO_TASK +const OSThread *OSThread::radioTaskThread; +#endif + +const OSThread *OSThread::current() +{ +#ifdef MESHTASTIC_RADIO_TASK + if (inRadioTask()) + return radioTaskThread; + const OSThread *loggedOn; + if (loggingFor(&loggedOn)) + return loggedOn; +#endif + return currentThread; +} ThreadController mainController, timerController; InterruptableDelay mainDelay; @@ -83,7 +99,12 @@ void OSThread::run() #ifdef DEBUG_HEAP auto heap = memGet.getFreeHeap(); #endif - currentThread = this; +#ifdef MESHTASTIC_RADIO_TASK + const OSThread *&running = controller == &radioController ? radioTaskThread : currentThread; +#else + const OSThread *&running = currentThread; +#endif + running = this; fromNowDuringRun = false; auto newDelay = runOnce(); #ifdef DEBUG_HEAP @@ -104,7 +125,7 @@ void OSThread::run() if (newDelay >= 0) setInterval(newDelay); - currentThread = NULL; + running = NULL; } int32_t OSThread::disable() diff --git a/src/concurrency/OSThread.h b/src/concurrency/OSThread.h index b93ade2ccd..ea5f493611 100644 --- a/src/concurrency/OSThread.h +++ b/src/concurrency/OSThread.h @@ -44,6 +44,13 @@ class OSThread : public Thread public: /// For debug printing only (might be null) static const OSThread *currentThread; +#ifdef MESHTASTIC_RADIO_TASK + /// currentThread for the radio task, so the two tasks' log lines keep their own thread names + static const OSThread *radioTaskThread; +#endif + + /// The thread running on the calling task, for debug printing only (might be null) + static const OSThread *current(); OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); diff --git a/src/concurrency/RadioTask.cpp b/src/concurrency/RadioTask.cpp new file mode 100644 index 0000000000..8d1aa21ca1 --- /dev/null +++ b/src/concurrency/RadioTask.cpp @@ -0,0 +1,130 @@ +#include "concurrency/RadioTask.h" + +#ifdef MESHTASTIC_RADIO_TASK + +#include "concurrency/OSThread.h" +#include "configuration.h" +#include + +#if !defined(HAS_FREE_RTOS) || defined(ARCH_PORTDUINO) || !(defined(ARCH_ESP32) || defined(ARCH_NRF52)) +#error "MESHTASTIC_RADIO_TASK runs the radio thread from a FreeRTOS task: ESP32 and nRF52 only" +#endif + +// ESP-IDF counts a task's stack in bytes, the other ports in words +#ifdef ARCH_ESP32 +#define RADIO_TASK_STACK 6144 +#else +#define RADIO_TASK_STACK 1536 +#endif + +namespace concurrency +{ + +ThreadController radioController; +InterruptableDelay radioDelay; + +namespace +{ +SemaphoreHandle_t radioMutex; +// Only the owner writes these while it holds the mutex, so the owner's own check needs no lock +TaskHandle_t volatile radioMutexOwner; +uint32_t radioMutexDepth; +TaskHandle_t radioTask; +// The line the loop is printing for the radio task (see RedirectablePrint::drainRadioTaskLogs) +const OSThread *loggingForThread; +TaskHandle_t volatile loggingForTask; + +void radioTaskMain(void *) +{ + for (;;) { + radioTaskLock(); + const long delayMsec = radioController.runOrDelay(); + radioTaskUnlock(); + radioDelay.delay(delayMsec); + } +} +} // namespace + +void radioTaskLock() +{ + const TaskHandle_t self = xTaskGetCurrentTaskHandle(); + if (radioMutexOwner == self) { + radioMutexDepth++; + return; + } + // Created on first use, which is on the loop during setup, before the task exists. A mutex, not a binary semaphore, + // so a lower task holding it is raised to the radio task's priority while the radio waits. + if (!radioMutex) + radioMutex = xSemaphoreCreateMutex(); + xSemaphoreTake(radioMutex, portMAX_DELAY); + radioMutexOwner = self; + radioMutexDepth = 1; +} + +void radioTaskUnlock() +{ + if (--radioMutexDepth) + return; + radioMutexOwner = nullptr; + xSemaphoreGive(radioMutex); +} + +bool inRadioTask() +{ + return radioTask && xTaskGetCurrentTaskHandle() == radioTask; +} + +void startRadioTask() +{ + static bool tried = false; + if (tried) + return; + tried = true; + radioController.ThreadName = "radioController"; + radioTaskLock(); // creates the mutex before a second task can race for it + radioTaskUnlock(); + // One above the loop, and below the RX readout task (loop + 2), so a received frame is read out first + UBaseType_t priority = uxTaskPriorityGet(nullptr) + 1; + if (priority > configMAX_PRIORITIES - 1) + priority = configMAX_PRIORITIES - 1; + TaskHandle_t task = nullptr; + int core = -1; // unpinned +#ifdef ARCH_ESP32 + // The loop's core, as the readout task: the radio preempts the loop as on one core + core = xPortGetCoreID(); + if (xTaskCreatePinnedToCore(radioTaskMain, "Radio", RADIO_TASK_STACK, nullptr, priority, &task, core) != pdPASS) + task = nullptr; +#else + if (xTaskCreate(radioTaskMain, "Radio", RADIO_TASK_STACK, nullptr, priority, &task) != pdPASS) + task = nullptr; +#endif + radioTask = task; + LOG_INFO("Radio task %s, priority %u (loop %u), core %d", task ? "started" : "not started", (unsigned)priority, + (unsigned)uxTaskPriorityGet(nullptr), core); + // Nothing else runs the radio thread: without the task the radio is dead + assert(task); +} + +void setLoggingFor(const OSThread *thread) +{ + loggingForThread = thread; + loggingForTask = xTaskGetCurrentTaskHandle(); +} + +void clearLoggingFor() +{ + loggingForTask = nullptr; +} + +bool loggingFor(const OSThread **thread) +{ + if (!loggingForTask || loggingForTask != xTaskGetCurrentTaskHandle()) + return false; + if (thread) + *thread = loggingForThread; + return true; +} + +} // namespace concurrency + +#endif diff --git a/src/concurrency/RadioTask.h b/src/concurrency/RadioTask.h new file mode 100644 index 0000000000..50a38743ee --- /dev/null +++ b/src/concurrency/RadioTask.h @@ -0,0 +1,55 @@ +#pragma once + +// -DMESHTASTIC_RADIO_TASK (ESP32 and nRF52) runs the RadioLib radio thread from its own FreeRTOS task, one priority above +// the main loop, so a main-loop hold can no longer delay a scan, a launch or the TX_DONE handling. The task holds the radio +// lock while the radio thread runs, and the loop takes it with RADIO_TASK_LOCK() wherever it reaches into the radio. +#ifdef MESHTASTIC_RADIO_TASK + +#include "ThreadController.h" +#include "concurrency/InterruptableDelay.h" + +namespace concurrency +{ + +/// Holds only the radio thread; run by the radio task as mainController is by loop() +extern ThreadController radioController; +/// What the radio thread's notifications interrupt, in place of mainDelay +extern InterruptableDelay radioDelay; + +/// Recursive: the radio thread can call back into an entry point the loop locks +void radioTaskLock(); +void radioTaskUnlock(); + +/// Once, from the loop: the task's priority is set from the caller's +void startRadioTask(); +bool inRadioTask(); + +class OSThread; +/// While the loop prints a line the radio task logged, the thread that logged it. Set and read on the printing task only; +/// false on any other. +void setLoggingFor(const OSThread *thread); +void clearLoggingFor(); +bool loggingFor(const OSThread **thread); + +class RadioTaskLockGuard +{ + public: + RadioTaskLockGuard() { radioTaskLock(); } + ~RadioTaskLockGuard() { radioTaskUnlock(); } + RadioTaskLockGuard(const RadioTaskLockGuard &) = delete; + RadioTaskLockGuard &operator=(const RadioTaskLockGuard &) = delete; +}; + +} // namespace concurrency + +#define RADIO_TASK_LOCK_NAME2(line) radioTaskLockGuard##line +#define RADIO_TASK_LOCK_NAME(line) RADIO_TASK_LOCK_NAME2(line) +#define RADIO_TASK_LOCK() concurrency::RadioTaskLockGuard RADIO_TASK_LOCK_NAME(__LINE__) + +#else + +#define RADIO_TASK_LOCK() \ + do { \ + } while (0) + +#endif diff --git a/src/main.cpp b/src/main.cpp index ecfeae58bd..6750cc2b39 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1499,6 +1499,7 @@ void loop() static uint32_t lastRadioMissedIrqPoll; if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) { lastRadioMissedIrqPoll = millis(); + RADIO_TASK_LOCK(); RadioLibInterface::instance->pollMissedIrqs(); } @@ -1507,6 +1508,7 @@ void loop() if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) { lastAgcReset = millis(); // Sample before resetAGC(): recalibrating the frontend biases an RSSI read taken right after it. + RADIO_TASK_LOCK(); RadioLibInterface::instance->updateNoiseFloor(); RadioLibInterface::instance->periodicRadioMaintenance(); } @@ -1575,6 +1577,11 @@ void loop() #endif #if !MESHTASTIC_EXCLUDE_WAYPOINT && ENABLE_WAYPOINT_PERSISTENCE waypointStoreAutosaveTick(); +#endif +#ifdef MESHTASTIC_RADIO_TASK + if (router) + router->wakeIfReceived(); + DEBUG_PORT.drainRadioTaskLogs(); #endif long delayMsec = mainController.runOrDelay(); diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 7048cd9187..4e2cf1b601 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -77,7 +77,12 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining - if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { + bool removed; + { + RADIO_TASK_LOCK(); + removed = iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold); + } + if (removed) { LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, p->hop_limit, dropThreshold); @@ -143,10 +148,12 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) txRelayCanceled++; } if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { + RADIO_TASK_LOCK(); iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && nodeDB->isFromOrToFavoritedNode(*p)) { + RADIO_TASK_LOCK(); iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } } diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index 451b8d8b74..55d7578183 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -73,6 +73,7 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length) size_t offset = 0; bool mixed = false; + RADIO_TASK_LOCK(); while (offset < length) { size_t toCopy = std::min(length - offset, chunkSize); diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index e1b84a4b43..508ca0b602 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -131,6 +131,21 @@ template class MemoryPool : public Allocator private: T pool[MaxSize]; bool used[MaxSize]; +#ifdef MESHTASTIC_RADIO_TASK + // The radio task allocates and releases packets beside the loop, so a slot's claim must be atomic. A critical section, + // not a mutex: it is a few dozen flag reads. +#ifdef ARCH_ESP32 + portMUX_TYPE usedMux = portMUX_INITIALIZER_UNLOCKED; + void enterUsed() { portENTER_CRITICAL(&usedMux); } + void exitUsed() { portEXIT_CRITICAL(&usedMux); } +#else + void enterUsed() { taskENTER_CRITICAL(); } + void exitUsed() { taskEXIT_CRITICAL(); } +#endif +#else + void enterUsed() {} + void exitUsed() {} +#endif public: explicit MemoryPool(const char *auditTag = nullptr) : Allocator(auditTag), pool{}, used{} @@ -151,8 +166,11 @@ template class MemoryPool : public Allocator // Find the index of this pointer in our pool int index = p - pool; if (index >= 0 && index < MaxSize) { - assert(used[index]); // Should be marked as used + enterUsed(); + const bool wasUsed = used[index]; used[index] = false; + exitUsed(); + assert(wasUsed); // Should be marked as used this->auditAdd(-(int32_t)sizeof(T)); LOG_HEAP("Released static pool item %d at 0x%x", index, p); } else { @@ -166,8 +184,11 @@ template class MemoryPool : public Allocator { // Find first free slot for (int i = 0; i < MaxSize; i++) { - if (!used[i]) { - used[i] = true; + enterUsed(); + const bool claimed = !used[i]; + used[i] = true; + exitUsed(); + if (claimed) { this->auditAdd((int32_t)sizeof(T)); LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); return &pool[i]; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ded4e67616..4d71779dca 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3041,6 +3041,8 @@ static concurrency::Lock g_reloadFromDiskMutex; bool NodeDB::reloadFromDisk() { concurrency::LockGuard guard(&g_reloadFromDiskMutex); + // With the radio in its own task, keep it parked for the whole swap, as the loop's cooperative threading does + RADIO_TASK_LOCK(); LOG_INFO("NodeDB: Reloading config from encrypted storage after unlock"); RadioInterface *rIface = router ? router->getRadioIface() : nullptr; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 912f90a0cb..c64077bf47 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -647,6 +647,7 @@ std::unique_ptr initLoRa() config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); + RADIO_TASK_LOCK(); if (rIf && !rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); if (screen) { @@ -878,6 +879,43 @@ uint32_t RadioInterface::getAnchoredSlotDelayMsec(uint32_t slots, meshtastic_Mes return delay; } +#ifdef MESHTASTIC_TX_SLOT_GATE_MS +uint32_t RadioInterface::ownSlotScanAt(meshtastic_MeshPacket_SlotParity parity, uint32_t drawnMs, int32_t backMs) +{ + const uint32_t now = Time::getMillis(); + const uint32_t slot = slotTimeMsec; + // The latest frame end's grid, which is the one the other nodes count from too + const uint32_t frameEnd = lastFrameEndMs.load(std::memory_order_relaxed); + if (!slot || !frameEnd) + return now; // no grid yet + const uint32_t wanted = parity == meshtastic_MeshPacket_SlotParity_SLOT_PARITY_ODD ? 1 : 0; +#ifdef MESHTASTIC_TX_SLOT_LEAD + // A slot qualifies if its scan time began at most GATE_MS ago, and the drawn one is the earliest: the timer fired + // early on purpose. + int32_t lower = (int32_t)(now - frameEnd) + backMs - MESHTASTIC_TX_SLOT_GATE_MS; + const int32_t drawnSince = (int32_t)(drawnMs - frameEnd); + if (drawnSince > lower) + lower = drawnSince; + if (lower < 0) + lower = 0; + uint32_t first = ((uint32_t)lower + slot - 1) / slot; + if ((first & 1) != wanted) + first++; + return frameEnd + first * slot - backMs; +#else + (void)backMs; + // Not before the drawn slot: the timer fired early on purpose + const uint32_t from = (int32_t)(drawnMs - now) > 0 ? drawnMs : now; + const uint32_t since = from - frameEnd; + uint32_t k = since / slot; + if ((k & 1) == wanted && since - k * slot <= MESHTASTIC_TX_SLOT_GATE_MS) + return from; + k += (k & 1) != wanted ? 1 : 2; // the next slot start of this parity + return frameEnd + k * slot; +#endif +} +#endif + /** The CW size to use when calculating SNR_based delays */ uint8_t RadioInterface::getCWsize(float snr) { @@ -1019,6 +1057,7 @@ bool RadioInterface::init() int RadioInterface::notifyDeepSleepCb(void *unused) { + RADIO_TASK_LOCK(); sleep(); return 0; } diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 56cc179570..e0511d3c0a 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -5,6 +5,7 @@ #include "Observer.h" #include "PointerQueue.h" #include "airtime.h" +#include "concurrency/RadioTask.h" #include "error.h" #include #include @@ -13,6 +14,36 @@ #include "LoRaFEMInterface.h" #endif +// -DMESHTASTIC_TX_SLOT_GATE_MS= scans a TX that has a slot parity only in the first of a slot of that parity, +// waiting on the radio thread for the next one if its handling came late. The backoff timer fires +// MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS (default 2) slots early so the wait, not the late handling, sets the start. A timer +// that fires further ahead than that is set again, or with -DMESHTASTIC_TX_SLOT_GATE_REARM=0 scans at once. +#ifdef MESHTASTIC_TX_SLOT_GATE_MS +#ifndef MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS +#define MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS 2 +#endif +#ifndef MESHTASTIC_TX_SLOT_GATE_REARM +#define MESHTASTIC_TX_SLOT_GATE_REARM 1 +#endif +// A scan target further out than this is not a slot the backoff drew: scan at once +#define SLOT_GATE_SANE_MS 10000 +#endif +// -DMESHTASTIC_TX_SLOT_LEAD replaces the gate's fixed early window with measured ones. SET_TX is aimed +// MESHTASTIC_TX_SLOT_SET_TX_AT_MS (default 1) into the slot; the gate starts the scan the measured scan-to-SET_TX time +// before that, and the timer fires the measured wake latency, plus MESHTASTIC_TX_SLOT_LEAD_GUARD_MS (default 1), before +// the scan. +#ifdef MESHTASTIC_TX_SLOT_LEAD +#ifndef MESHTASTIC_TX_SLOT_GATE_MS +#error "MESHTASTIC_TX_SLOT_LEAD sets the slot gate's lead: build with MESHTASTIC_TX_SLOT_GATE_MS" +#endif +#ifndef MESHTASTIC_TX_SLOT_SET_TX_AT_MS +#define MESHTASTIC_TX_SLOT_SET_TX_AT_MS 1 +#endif +#ifndef MESHTASTIC_TX_SLOT_LEAD_GUARD_MS +#define MESHTASTIC_TX_SLOT_LEAD_GUARD_MS 1 +#endif +#endif + // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; @@ -270,6 +301,13 @@ class RadioInterface [[nodiscard]] static uint32_t anchoredSlotDelayMsec(uint32_t sinceEndMs, uint32_t slotMsec, uint32_t pairsDrawn, meshtastic_MeshPacket_SlotParity parity); +#ifdef MESHTASTIC_TX_SLOT_GATE_MS + /** When to start the scan for a TX on `parity` whose backoff fell due at drawnMs: the earliest start of a slot of that + * parity, no earlier than drawnMs, that began at most MESHTASTIC_TX_SLOT_GATE_MS ago. Now if there is no grid yet. + * With MESHTASTIC_TX_SLOT_LEAD, backMs (scan-to-SET_TX less the aim) before such a slot, so SET_TX lands the aim in. */ + [[nodiscard]] uint32_t ownSlotScanAt(meshtastic_MeshPacket_SlotParity parity, uint32_t drawnMs, int32_t backMs); +#endif + /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } @@ -380,12 +418,17 @@ class RadioInterface /// Return 0 if sleep is okay. A non-NULL argument means the radio is about to be powered /// down (deep sleep / shutdown), see doPreflightSleep() - int preflightSleepCb(void *deepSleep = NULL) { return canSleep(deepSleep != NULL) ? 0 : 1; } + int preflightSleepCb(void *deepSleep = NULL) + { + RADIO_TASK_LOCK(); + return canSleep(deepSleep != NULL) ? 0 : 1; + } int notifyDeepSleepCb(void *unused = NULL); int reloadConfig(void *unused) { + RADIO_TASK_LOCK(); reconfigure(); return 0; } diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 4dab41a485..27788d25a2 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -41,7 +41,12 @@ void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface) +#ifdef MESHTASTIC_RADIO_TASK + : NotifiedWorkerThread("RadioIf", &concurrency::radioController, &concurrency::radioDelay), module(hal, cs, irq, rst, busy), + iface(_iface) +#else : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) +#endif { instance = this; @@ -576,9 +581,16 @@ void RadioLibInterface::onNotify(uint32_t notification) stageTxEarly(txQueue.getFront()); const uint32_t now = Time::getMillis(); if (!Throttle::deadlinePassedAt(now, dueMs)) { +#ifdef MESHTASTIC_TX_SLOT_LEAD + if (txTimerDueUs) + txTimerDueUs = micros() + (dueMs - now) * 1000; // the lead's wake sample is this timer's +#endif notifyLater(dueMs - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); break; } +#ifdef MESHTASTIC_TX_SLOT_LEAD + txTimerDueUs = 0; // the stage ran past the due time: not a wake sample +#endif } txDueMs.store(0, std::memory_order_relaxed); // the backoff is over; every path below that waits again re-stamps it @@ -608,27 +620,54 @@ void RadioLibInterface::onNotify(uint32_t notification) } else if (action == RadioTxHook::PRETX_DEFER) { setTransmitDelay(); // the radio config moved, so re-run the delay and scan on it } else { - // The scan and the transmit it decides are one sequence: a readout landing between them would - // clear the flags startTransmit() is about to set up, or on SX128x leave the chip in standby. - RadioSequence seq(this); // Listen-before-talk: a CAD preamble scan immediately before we key up. +#ifndef MESHTASTIC_RADIO_TASK LOG_DEBUG("CAD arm"); +#endif +#ifdef MESHTASTIC_TX_SLOT_GATE_MS + // After the log line, which on some boards costs most of a slot + if (!waitForTxSlot(txp)) + break; +#endif +#ifdef MESHTASTIC_TX_SLOT_LEAD + const uint32_t scanStartUs = micros(); +#endif + // The scan and the transmit it decides are one sequence: a readout landing between them would clear + // the flags startTransmit() is about to set up, or on SX128x leave the chip in standby. + RadioSequence seq(this); scanForTx = txp; const bool channelActive = isChannelActive(); scanForTx = nullptr; if (channelActive) { // currently traffic on the channel? +#ifdef MESHTASTIC_RADIO_TASK + // The radio task's lines wait on the loop's, so none sits between the timer and the launch + LOG_DEBUG("CAD arm"); +#endif LOG_DEBUG("CAD busy"); // Beacon target or not: reconfigureForBeaconTX() already left RX running on that // config, so skipping this only ever left the node deaf in standby. rearmReceive(); setTransmitDelay(); } else { +#ifndef MESHTASTIC_RADIO_TASK LOG_DEBUG("CAD free"); +#endif // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and // actual transmission as short as possible txp = txQueue.dequeue(); assert(txp); +#ifdef MESHTASTIC_TX_SLOT_LEAD + lastTxStartUs = 0; +#endif startSend(txp); +#ifdef MESHTASTIC_TX_SLOT_LEAD + if (lastTxStartUs) + noteLeadPath(lastTxStartUs - scanStartUs); +#endif +#ifdef MESHTASTIC_RADIO_TASK + LOG_DEBUG("CAD arm"); + LOG_DEBUG("CAD free"); +#endif LOG_TRACE("%d packets in TX queue", txQueue.getMaxLen() - txQueue.getFree()); } } @@ -703,6 +742,25 @@ void RadioLibInterface::scheduleTransmitDelayCompleted(uint32_t delay) const meshtastic_MeshPacket *p = txQueue.getFront(); const bool slotted = p && p->slot_parity != meshtastic_MeshPacket_SlotParity_SLOT_PARITY_UNSET; txDueMs.store(slotted ? Time::timerEndsAtMillis(delay) : 0, std::memory_order_relaxed); +#ifdef MESHTASTIC_TX_SLOT_GATE_MS + slotGateDueMs = Time::timerEndsAtMillis(delay); + if (slotted) { +#ifdef MESHTASTIC_TX_SLOT_LEAD + // The drawn slot starts at delay. Fire the measured wake latency and scan-to-SET_TX time (plus the guard) before + // SET_TX is due in it; the gate waits out what is left, so a late tail still lands in the slot. + const int32_t early = + (int32_t)(leadWakeMs() + leadPathMs()) + MESHTASTIC_TX_SLOT_LEAD_GUARD_MS - MESHTASTIC_TX_SLOT_SET_TX_AT_MS; +#else + // Fire early: the gate waits out the rest on the radio thread, so a late handling shortens the wait instead of + // pushing the scan into the next slot + const int32_t early = MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS * (int32_t)slotTimeMsec; +#endif + if (early < 0) + delay += (uint32_t)-early; + else + delay = delay > (uint32_t)early ? delay - early : 1; + } +#endif // Where the driver wants it, fire at once so the radio thread writes the payload while RX runs, then wait out the // rest of the backoff txStageDueMs = 0; @@ -710,9 +768,94 @@ void RadioLibInterface::scheduleTransmitDelayCompleted(uint32_t delay) txStageDueMs = Time::timerEndsAtMillis(delay); delay = 1; } +#ifdef MESHTASTIC_TX_SLOT_LEAD + txTimerDueUs = slotted ? micros() + delay * 1000 : 0; +#endif notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); } +#ifdef MESHTASTIC_TX_SLOT_GATE_MS +/// Hold this thread until untilMs: sleep while more than 2 ms remain, so other tasks run, then spin onto the slot start +static void waitUntilMs(uint32_t untilMs) +{ + for (int32_t left = (int32_t)(untilMs - Time::getMillis()); left > 0; left = (int32_t)(untilMs - Time::getMillis())) { + if (left > 2) + delay(1); + } +} + +bool RadioLibInterface::waitForTxSlot(meshtastic_MeshPacket *txp) +{ + if (txp->slot_parity == meshtastic_MeshPacket_SlotParity_SLOT_PARITY_UNSET) + return true; // not on the slot grid +#ifdef MESHTASTIC_TX_SLOT_LEAD + if (txTimerDueUs) { + // How late this handling starts after the timer fell due: the wake-up, the radio lock, and anything this thread + // ran first + noteLeadWake((int32_t)(micros() - txTimerDueUs)); + txTimerDueUs = 0; + } + const int32_t backMs = (int32_t)leadPathMs() - MESHTASTIC_TX_SLOT_SET_TX_AT_MS; + const int32_t earlyMs = (int32_t)leadWakeMs() + MESHTASTIC_TX_SLOT_LEAD_GUARD_MS; +#else + const int32_t backMs = 0; + const int32_t earlyMs = MESHTASTIC_TX_SLOT_GATE_EARLY_SLOTS * (int32_t)slotTimeMsec; +#endif + const uint32_t scanAt = ownSlotScanAt(txp->slot_parity, slotGateDueMs, backMs); + const int32_t ahead = (int32_t)(scanAt - Time::getMillis()); +#if MESHTASTIC_TX_SLOT_GATE_REARM + const int32_t saneMs = SLOT_GATE_SANE_MS; + if (ahead > earlyMs && ahead < saneMs) { + // A redraw cannot replace a pending TX timer (txTimerOverwrite), so this can be an older draw's timer. Come back + // the early window ahead of the slot instead of holding this thread; a notification already pending redraws. + txDueMs.store(slotGateDueMs, std::memory_order_relaxed); +#ifdef MESHTASTIC_TX_SLOT_LEAD + txTimerDueUs = micros() + (uint32_t)(ahead - earlyMs) * 1000; +#endif + notifyLater(ahead - earlyMs, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); + return false; + } +#else + const int32_t saneMs = earlyMs + 2 * (int32_t)slotTimeMsec + 1; // further out than this, scan at once +#endif + if (ahead <= 0 || ahead >= saneMs) + return true; + txDueMs.store(scanAt, std::memory_order_relaxed); // the router holds RX handling while this thread waits + waitUntilMs(scanAt); + txDueMs.store(0, std::memory_order_relaxed); + if (!canSendImmediately() || capturedFramePending()) { + // A frame arrived during the wait: take it first. Its handling redraws from its end, as the other nodes will. + setTransmitDelay(); + return false; + } + return true; +} + +#ifdef MESHTASTIC_TX_SLOT_LEAD +void RadioLibInterface::noteLeadWake(int32_t lateUs) +{ + // Rises to a late sample at once and falls back an eighth of the way per sample, so the timer covers the recent tail + // rather than the average. Capped at two slots: past that the gate's re-arm comes back for the slot anyway. + uint32_t sample = lateUs > 0 ? (uint32_t)lateUs : 0; + const uint32_t cap = 2 * slotTimeMsec * 1000; + if (sample > cap) + sample = cap; + if (sample >= leadWakeUs) + leadWakeUs = sample; + else + leadWakeUs -= (leadWakeUs - sample) / 8; +} + +void RadioLibInterface::noteLeadPath(uint32_t pathUs) +{ + // The scan and the launch take about the same time every TX, so an average; a sample over a slot is something else + if (pathUs > slotTimeMsec * 1000) + return; + leadPathUs = (uint32_t)((int32_t)leadPathUs + ((int32_t)pathUs - (int32_t)leadPathUs) / 8); +} +#endif +#endif + /** * If the packet is not already in the late rebroadcast window, move it there */ @@ -967,6 +1110,9 @@ void RadioLibInterface::startReceive() #ifdef MESHTASTIC_RX_READOUT_TASK if (!rxReadoutTaskTried) startRxReadoutTask(); +#endif +#ifdef MESHTASTIC_RADIO_TASK + concurrency::startRadioTask(); // once, from the loop during init, after the readout task: both take the loop's priority #endif isReceiving = true; // Drivers only reach here once the chip actually accepted the RX start, so the radio is alive again. @@ -1461,6 +1607,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) } else { // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register // bits +#ifdef MESHTASTIC_TX_SLOT_LEAD + lastTxStartUs = micros(); +#endif enableInterrupt(isrTxLevel0); // unset-sentinel-ok: busyTx/sendingPacket is the armed flag, so 0 is a legal stamp lastTxStart = Time::getMillis(); diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 0ce8865cb7..9d42fff1a0 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -440,6 +440,25 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Set across the getPacketTime() of a captured frame, so computePacketTime() takes that frame's header info * instead of reading the chip. Only ever set and cleared on the radio thread, inside handleReceiveInterrupt(). */ const CapturedRxInfo *rxCapturedHeader = nullptr; +#ifdef MESHTASTIC_TX_SLOT_GATE_MS + /** When the backoff of the TX at the front of the queue falls due, before the gate brings its timer forward */ + uint32_t slotGateDueMs = 0; + /** Hold a TX with a slot parity until its slot on the grid (see ownSlotScanAt()); false if it is not to be scanned now: + * its timer was set again, or a frame arrived during the wait */ + bool waitForTxSlot(meshtastic_MeshPacket *txp); +#ifdef MESHTASTIC_TX_SLOT_LEAD + /** How late the TX timer's handling starts after the timer falls due (a fast-rising, slowly falling envelope, so a late + * tail sets it), and the scan start to SET_TX (an average), in us */ + uint32_t leadWakeUs = 1000, leadPathUs = 6000; + /** micros() the TX timer was set to fall due, 0 when no sample is pending; micros() of the last SET_TX */ + uint32_t txTimerDueUs = 0, lastTxStartUs = 0; + void noteLeadWake(int32_t lateUs); + void noteLeadPath(uint32_t pathUs); + /** The estimates in whole ms, rounded up */ + uint32_t leadWakeMs() const { return (leadWakeUs + 999) / 1000; } + uint32_t leadPathMs() const { return (leadPathUs + 999) / 1000; } +#endif +#endif static void timerCallback(void *p1, uint32_t p2); @@ -694,6 +713,8 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * task notifies ISR_RX once the frame is out of the radio. */ bool rxDoneFromIsr(); bool rxReadoutActive() const { return rxReadoutTask != nullptr; } + /** Whether a frame the readout task captured is still waiting for this thread */ + bool capturedFramePending() const { return rxRingTail != rxRingHead; } /** Hand an RX_DONE found by a poll to the readout task; false if there is no task. Does not wait: the * radio-sequence lock, not a wait here, is what keeps the task out of the caller's RadioLib calls. */ bool wakeRxReadout(); @@ -738,6 +759,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified bool rxDoneFromIsr() { return false; } bool requestRearmFromIsr() { return false; } bool rxReadoutActive() const { return false; } + bool capturedFramePending() const { return false; } bool wakeRxReadout() { return false; } unsigned deliverCapturedFrames() { return 0; } #endif diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 9e36ed6dc8..8e6700b71e 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -435,16 +435,32 @@ void Router::setReceivedMessage() // LOG_DEBUG("set interval to ASAP"); setInterval(0); // Run ASAP, so we can figure out our correct sleep time runASAP = true; +#ifdef MESHTASTIC_RADIO_TASK + if (concurrency::inRadioTask()) + concurrency::mainDelay.interrupt(); // runASAP alone does not end a sleep the loop is already in +#endif } +#ifdef MESHTASTIC_RADIO_TASK +void Router::wakeIfReceived() +{ + if (rxHeldForTx) + return; // its own deadline wakes it + if (!fromRadioQueue.isEmpty()) + setInterval(0); +} +#endif + meshtastic_QueueStatus Router::getQueueStatus() { if (!iface) { meshtastic_QueueStatus qs; qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; return qs; - } else + } else { + RADIO_TASK_LOCK(); return iface->getQueueStatus(); + } } ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) @@ -645,12 +661,14 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) } assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) + RADIO_TASK_LOCK(); return iface->send(p); } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool Router::cancelSending(NodeNum from, PacketId id) { + RADIO_TASK_LOCK(); if (iface && iface->cancelSending(from, id)) { // We are not a relayer of this packet anymore removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); @@ -662,6 +680,7 @@ bool Router::cancelSending(NodeNum from, PacketId id) /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool Router::findInTxQueue(NodeNum from, PacketId id) { + RADIO_TASK_LOCK(); return iface->findInTxQueue(from, id); } diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 4507b653d3..388e9c3d09 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -120,6 +120,12 @@ class Router : protected concurrency::OSThread, protected PacketHistory */ void setReceivedMessage(); +#ifdef MESHTASTIC_RADIO_TASK + /// From loop(): run soon if a reception is queued. The radio task's enqueue can land while this thread runs, and the + /// run's own return then overwrites the wake-up the enqueue set. + void wakeIfReceived(); +#endif + /** * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for * freeing the packet diff --git a/src/modules/MeshBeaconModule.cpp b/src/modules/MeshBeaconModule.cpp index 34758de0ed..dc94c71ac2 100644 --- a/src/modules/MeshBeaconModule.cpp +++ b/src/modules/MeshBeaconModule.cpp @@ -558,6 +558,8 @@ void MeshBeaconBroadcastModule::sendBeacon() const meshtastic_ChannelSettings *chPtr = tgt.has_channel ? &tgt.channel : nullptr; const auto applyTarget = [&](meshtastic_MeshPacket *p) { + // The radio thread reads the target table, and sendBeaconPacket() swaps the primary channel around send() + RADIO_TASK_LOCK(); if (presetDiffers || legacySplit) setTargetRadioSettings(p, tgt.preset, tgt.slot, legacySplit, tgt.region, tgt.has_channel, chPtr); sendBeaconPacket(p, tgt.preset, tgt.has_channel, chPtr);