mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-23 06:45:24 -04:00
* MeshPacketQueue: fix millis() rollover in the late-packet drop test replaceLowerPriorityPacket() read `backPacket->tx_after < now`, with `now` taken from millis() on the line above. tx_after is an absolute deadline, so that comparison inverts while the deadline sits on the far side of the 32-bit wrap: a queued late packet reads as not-yet-due for the rest of the wrap window, or every late packet reads as droppable at once. The same statement ordered two deadlines against each other with `backPacket->tx_after > p->tx_after`, which has the same problem. #11291 swept every site where millis() sits next to the comparison operator, and its CI guard matches that shape. Stashing the clock in a local first is the same bug written so the guard cannot see it. Both tests now subtract before comparing: the due test through Throttle::deadlinePassedAt(), and the ordering through the elapsed-since-now form already used in AdminModule's oldest-slot scan. The snapshot comes from Time::getMillis() so the deadlines and the test read one clock, per the convention deadlinePassedAt() documents. The `dt` the log line reports is now derived from the same elapsed value rather than recomputed. Behaviour is otherwise unchanged, save the boundary: deadlinePassedAt() is inclusive, so a deadline landing exactly on `now` reads as due rather than one millisecond early. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * RadioLibInterface: don't widen a uint32_t deadline delta into a 64-bit long TRANSMIT_DELAY_COMPLETED tested whether the front packet was still waiting with long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; if (delay_remaining > 0) ... The subtraction is uint32_t. Where long is 32-bit - every embedded target - an already-due deadline lands negative and the packet transmits, which is why this has never been visible on device. Where long is 64-bit (portduino, and the native test build) the same value zero-extends to ~4.29e9, reads as positive, and the packet is rescheduled 49.7 days out. It stays parked until some later notifyLater() with overwrite happens to reset the timer. That is not an edge case. notifyLater() schedules through setIntervalFromNow(), so the thread wakes at or after the deadline; being a millisecond past due is the ordinary path through this branch. Ask Throttle instead. deadlinePassedAt() is the unsigned half-range test, so there is no signed conversion to get wrong at any width, and the remaining delay handed to notifyLater() is computed from the same snapshot. On 32-bit the behaviour is identical, including at the boundary: a deadline equal to now transmitted before and still does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ExpressLRSFiveWay: convert the two remaining raw window checks to Throttle runOnce() dismissed the alert frame with `now > alertingSinceMs + 2000` and chose its poll rate with `now < keyDownStart + 20000`, both against a millis() snapshot in a local. Same rollover inversion as any other naive compare, and invisible to the millis-deadline-check guard because millis() is not adjacent to the operator. update() in the same file was already on Throttle. hasElapsed()/isWithinTimespanMs() with the stored event give the full ~49.7 day range and need no snapshot. Sentinels are unchanged in meaning: `alerting` is the armed flag for alertingSinceMs and is tested first, and keyDownStart == 0 reads as "recent" for the first 20s of uptime exactly as `now < 0 + 20000` did - a poll rate either way. The arm sites move to Time::getMillis() so the writes land on the clock Throttle reads, which also puts them within reach of Time::setTestMillis(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * GPSUpdateScheduling: record whether a search is running, don't infer it elapsedSearchMs() answered "am I searching?" by ordering two raw millis() stamps: searchStartedMs > searchEndedMs. Whichever stamp lands on the far side of the 32-bit wrap reads as the larger one, so the answer inverts once per wrap cycle, in both directions: - a search that started before the wrap and ended after it keeps reading as "searching". elapsedSearchMs() then grows without bound and searchedTooLong() aborts a search that is not running. - a search that started after the wrap, following one that ended before it, reads as "idle". elapsedSearchMs() returns 0, so an unproductive search is never aborted and the receiver stays powered until it locks. Both self-heal at the next informSearching(), which bounds the damage to one GPS cycle - but the ordering test cannot be made wrap-correct, because the two stamps carry no information about which wrap they belong to. It does not need to be. Whether a search is in progress is a fact the three inform*() calls already have in hand; the ordering was only ever standing in for it. Add the flag and set it there. elapsedSearchMs() keeps its unsigned subtraction, which was always the correct part. The file's clock reads move to Time::getMillis() so the suite can drive them across the wrap. Behaviour-preserving in production - Time::getMillis() is millis() unless a test injects a clock. test_gps_update_scheduling/ gains seven cases: the idle/searching/ended states, elapsed exactness across the wrap, both inversion directions above, and reset(). The two wrap cases fail on the old predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * MessageStore: date boot-relative messages in uptime seconds A message received before the wall clock is trustworthy is stamped boot-relative and healed by upgradeBootRelativeTimestamps() once the RTC arrives. Both the stamp and the "same boot?" test were millis() / 1000, which wraps every 49.7 days: a stamp taken before the wrap reads as newer than `bootNow` afterwards, so `m.timestamp <= bootNow` declines to heal it and the message shows "???" until it ages out. MessageRenderer's own copy of the test falls the same way and prints invalidTime. Neither produces a wrong time - the guard is what fails safe - but Time::getUptimeSecs() landed in #11291 for exactly this, and does not wrap for 136 years. Both sites take it, which makes the comparison exact rather than merely fail-safe. While here, the autosave tick had its own hand-rolled deadline helper - `reachedMs(now, target)` as `(int32_t)(now - target) >= 0`. Wrap-correct, but a competing idiom for what Throttle::isWithinTimespanMs() already answers, and the signed cast is the form #11291 replaced everywhere else. Deleted; the stamps read Time::getMillis() so the whole path is on one clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * WebServer: drop the hand-rolled millis() wrap branch getAdaptiveInterval() special-cased the wrap by hand: if (currentTime >= lastActivityTime) timeSinceActivity = currentTime - lastActivityTime; else timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; Those two expressions are the same number - unsigned subtraction already computes the difference modulo 2^32 - so this is not a bug, just eight lines reimplementing what Throttle does. It also reads like a site that has thought about the wrap and settled it, which makes it a bad example to copy. Two isWithinTimespanMs() calls against the stored activity stamp, matching ethApiServer's shape for the same adaptive-interval decision. The stamps move to Time::getMillis() so the writes and the reads share a clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * MeshPacketQueue: only order elapsed times once both deadlines have passed The late-packet eviction I rewrote compared how long ago each deadline passed: backElapsed < (uint32_t)(now - p->tx_after) That is only an ordering when both deadlines are in the past. An incoming packet whose tx_after is still in the future subtracts to a near-2^32 elapsed, which reads as the most overdue packet in the queue rather than the least - so a full queue would drop the overdue packet it was about to transmit in favour of one that is not ready yet. The comparison it replaced, `backPacket->tx_after > p->tx_after`, got this right away from the wrap; I lost it in the conversion. Classify before ordering: p->tx_after must be unset, or passed, before its elapsed time means anything. Two expired deadlines still order by which is further overdue, which is what the branch is for. Caught by CodeRabbit on #11483. test/test_meshpacket_queue/ pins the branch: the future-dated arrival that started this, both directions of the both-expired ordering, the undelayed arrival, and all of it again with the deadlines and `now` on opposite sides of the wrap. maxLen is 1 so the suite reaches the branch without dragging in CompareMeshPacketFunc and a NodeDB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ExpressLRSFiveWay: treat "no key pressed yet" as no activity keyDownStart is 0 until the first press of a boot, and the fast-poll window read that as a press at time zero: 100ms polling for the first 20s of uptime with no activity at all, re-triggering once per millis() wrap. The arithmetic this replaced (`now < keyDownStart + 20000`) did the same, so it is not a regression - but the sentinel is exactly what the conventions say to test before the elapsed comparison, and "has there been recent key activity" has an honest answer here. 250ms is the documented floor for not missing presses, so an idle node simply starts there and moves to 100ms on the first press. Also trims the wrap-cases comment in test_gps_update_scheduling to the two-line house limit. Both from CodeRabbit review on #11483. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
810 lines
29 KiB
C++
810 lines
29 KiB
C++
#include "RadioLibInterface.h"
|
|
#include "MeshTypes.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerMon.h"
|
|
#include "SPILock.h"
|
|
#include "Throttle.h"
|
|
#include "UptimeClock.h"
|
|
#include "configuration.h"
|
|
#include "error.h"
|
|
#include "main.h"
|
|
#include "mesh-pb-constants.h"
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
#include "modules/MeshBeaconModule.h"
|
|
#endif
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
|
|
#if ARCH_PORTDUINO
|
|
#include "PortduinoGlue.h"
|
|
#include "meshUtils.h"
|
|
#endif
|
|
|
|
void LockingArduinoHal::spiBeginTransaction()
|
|
{
|
|
spiLock->lock();
|
|
|
|
ArduinoHal::spiBeginTransaction();
|
|
}
|
|
|
|
void LockingArduinoHal::spiEndTransaction()
|
|
{
|
|
ArduinoHal::spiEndTransaction();
|
|
|
|
spiLock->unlock();
|
|
}
|
|
|
|
#if ARCH_PORTDUINO
|
|
void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
|
|
{
|
|
spi->transfer(out, in, len);
|
|
}
|
|
#endif
|
|
|
|
RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface)
|
|
: NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface)
|
|
{
|
|
instance = this;
|
|
|
|
// Initialize unused sample slots to a sane default; sample count controls averaging.
|
|
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
|
|
noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT;
|
|
}
|
|
|
|
#if defined(ARCH_STM32WL) && defined(USE_SX1262)
|
|
module.setCb_digitalWrite(stm32wl_emulate_digitalWrite);
|
|
module.setCb_digitalRead(stm32wl_emulate_digitalRead);
|
|
#endif
|
|
}
|
|
|
|
#ifdef ARCH_ESP32
|
|
// ESP32 doesn't use that flag
|
|
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
|
#else
|
|
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x)
|
|
#endif
|
|
|
|
void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause)
|
|
{
|
|
instance->disableInterrupt();
|
|
|
|
BaseType_t xHigherPriorityTaskWoken;
|
|
instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true);
|
|
|
|
/* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE.
|
|
The macro used to do this is dependent on the port and may be called
|
|
portEND_SWITCHING_ISR. */
|
|
YIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
|
}
|
|
|
|
void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0()
|
|
{
|
|
isrLevel0Common(ISR_RX);
|
|
}
|
|
|
|
void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0()
|
|
{
|
|
isrLevel0Common(ISR_TX);
|
|
}
|
|
|
|
/** Our ISR code currently needs this to find our active instance
|
|
*/
|
|
RadioLibInterface *RadioLibInterface::instance;
|
|
|
|
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
|
|
bool RadioLibInterface::canSendImmediately()
|
|
{
|
|
// We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one).
|
|
// To do otherwise would be doubly bad because not only would we drop the packet that was on the way in,
|
|
// we almost certainly guarantee no one outside will like the packet we are sending.
|
|
bool busyTx = sendingPacket != NULL;
|
|
bool busyRx = isReceiving && isActivelyReceiving();
|
|
|
|
if (busyTx || busyRx) {
|
|
if (busyTx) {
|
|
LOG_WARN("Can not send yet, busyTx");
|
|
}
|
|
// If we've been trying to send the same packet more than one minute and we haven't gotten a
|
|
// TX IRQ from the radio, the radio is probably broken.
|
|
if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) {
|
|
LOG_ERROR("Hardware Failure! busyTx >60s");
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED);
|
|
// reboot in 5 seconds when this condition occurs.
|
|
rebootAtMsec = lastTxStart + 65000;
|
|
}
|
|
if (busyRx) {
|
|
LOG_WARN("Can not send yet, busyRx");
|
|
}
|
|
return false;
|
|
} else
|
|
return true;
|
|
}
|
|
|
|
bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag)
|
|
{
|
|
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
|
|
// Handle false detections
|
|
if (detected) {
|
|
if (!activeReceiveStart) {
|
|
activeReceiveStart = millis();
|
|
} else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) {
|
|
if (!(irq & syncWordHeaderValidFlag)) {
|
|
// The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag
|
|
activeReceiveStart = 0;
|
|
LOG_TRACE("Ignore false preamble detection");
|
|
return false;
|
|
} else {
|
|
uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader));
|
|
if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) {
|
|
// We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag
|
|
activeReceiveStart = 0;
|
|
LOG_TRACE("Ignore false header detection");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return detected;
|
|
}
|
|
|
|
/// Send a packet (possibly by enquing in a private fifo). This routine will
|
|
/// later free() the packet to pool. This routine is not allowed to stall because it is called from
|
|
/// bluetooth comms code. If the txmit queue is empty it might return an error
|
|
ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p)
|
|
{
|
|
|
|
#ifndef DISABLE_WELCOME_UNSET
|
|
|
|
if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
if (disabled || !config.lora.tx_enabled) {
|
|
LOG_WARN("send - !config.lora.tx_enabled");
|
|
packetPool.release(p);
|
|
return ERRNO_DISABLED;
|
|
}
|
|
|
|
} else {
|
|
LOG_WARN("send - lora tx disabled: Region unset");
|
|
packetPool.release(p);
|
|
return ERRNO_DISABLED;
|
|
}
|
|
|
|
#else
|
|
|
|
if (disabled || !config.lora.tx_enabled) {
|
|
LOG_WARN("send - !config.lora.tx_enabled");
|
|
packetPool.release(p);
|
|
return ERRNO_DISABLED;
|
|
}
|
|
|
|
#endif
|
|
|
|
if (p->to == NODENUM_BROADCAST_NO_LORA) {
|
|
LOG_DEBUG("Drop no-LoRa pkt");
|
|
return ERRNO_SHOULD_RELEASE;
|
|
}
|
|
|
|
// Sometimes when testing it is useful to be able to never turn on the xmitter
|
|
#ifndef LORA_DISABLE_SENDING
|
|
printPacket("enqueue for send", p);
|
|
|
|
LOG_TRACE("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad);
|
|
bool dropped = false;
|
|
ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN;
|
|
|
|
if (dropped) {
|
|
txDrop++;
|
|
}
|
|
|
|
if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks
|
|
packetPool.release(p);
|
|
return res;
|
|
}
|
|
|
|
// set (random) transmit delay to let others reconfigure their radio,
|
|
// to avoid collisions and implement timing-based flooding
|
|
setTransmitDelay();
|
|
|
|
return res;
|
|
#else
|
|
packetPool.release(p);
|
|
return ERRNO_DISABLED;
|
|
#endif
|
|
}
|
|
|
|
meshtastic_QueueStatus RadioLibInterface::getQueueStatus()
|
|
{
|
|
meshtastic_QueueStatus qs;
|
|
|
|
qs.res = qs.mesh_packet_id = 0;
|
|
qs.free = txQueue.getFree();
|
|
qs.maxlen = txQueue.getMaxLen();
|
|
|
|
return qs;
|
|
}
|
|
|
|
bool RadioLibInterface::canSleep(bool deepSleep)
|
|
{
|
|
// A packet being actively transmitted has already left the TX queue (sendingPacket), so
|
|
// check it separately. It only vetoes deep sleep: light sleep keeps the radio powered and
|
|
// the TX finishes on its own, but deep sleep powers the radio down and would truncate the
|
|
// packet on air.
|
|
bool res = txQueue.empty() && !(deepSleep && isSending());
|
|
if (!res) { // only print debug messages if we are vetoing sleep
|
|
LOG_DEBUG("Radio wait to sleep, txEmpty=%d, txInFlight=%d", txQueue.empty(), isSending());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/** Allow other firmware components to ask whether we are currently sending a packet
|
|
Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx
|
|
*/
|
|
bool RadioLibInterface::isSending()
|
|
{
|
|
return sendingPacket != NULL;
|
|
}
|
|
|
|
/** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */
|
|
bool RadioLibInterface::cancelSending(NodeNum from, PacketId id)
|
|
{
|
|
auto p = txQueue.remove(from, id);
|
|
if (p)
|
|
packetPool.release(p); // free the packet we just removed
|
|
|
|
bool result = (p != NULL);
|
|
LOG_DEBUG("cancelSending id=0x%08x, removed=%d", id, result);
|
|
return result;
|
|
}
|
|
|
|
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
|
bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
|
|
{
|
|
return txQueue.find(from, id);
|
|
}
|
|
|
|
void RadioLibInterface::updateNoiseFloor()
|
|
{
|
|
// Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly.
|
|
if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) {
|
|
return;
|
|
}
|
|
|
|
uint32_t now = millis();
|
|
if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) {
|
|
return;
|
|
}
|
|
lastNoiseFloorUpdate = now;
|
|
|
|
int16_t rssi = getCurrentRSSI();
|
|
if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) {
|
|
LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi);
|
|
return;
|
|
}
|
|
|
|
noiseFloorSamples[currentSampleIndex] = (int32_t)rssi;
|
|
currentSampleIndex++;
|
|
|
|
if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) {
|
|
currentSampleIndex = 0;
|
|
isNoiseFloorBufferFull = true;
|
|
}
|
|
|
|
currentNoiseFloor = getAverageNoiseFloorInternal();
|
|
|
|
LOG_TRACE("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
|
|
}
|
|
|
|
uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const
|
|
{
|
|
return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex;
|
|
}
|
|
|
|
int32_t RadioLibInterface::getAverageNoiseFloorInternal() const
|
|
{
|
|
uint8_t sampleCount = getNoiseFloorSampleCountInternal();
|
|
|
|
if (sampleCount == 0) {
|
|
return NOISE_FLOOR_DEFAULT;
|
|
}
|
|
|
|
int32_t sum = 0;
|
|
for (uint8_t i = 0; i < sampleCount; i++) {
|
|
sum += noiseFloorSamples[i];
|
|
}
|
|
|
|
return sum / sampleCount;
|
|
}
|
|
|
|
int32_t RadioLibInterface::getAverageNoiseFloor()
|
|
{
|
|
return getAverageNoiseFloorInternal();
|
|
}
|
|
|
|
int32_t RadioLibInterface::getNoiseFloor()
|
|
{
|
|
return currentNoiseFloor;
|
|
}
|
|
|
|
bool RadioLibInterface::hasNoiseFloorSamples()
|
|
{
|
|
return getNoiseFloorSampleCountInternal() > 0;
|
|
}
|
|
|
|
uint8_t RadioLibInterface::getNoiseFloorSampleCount()
|
|
{
|
|
return getNoiseFloorSampleCountInternal();
|
|
}
|
|
|
|
void RadioLibInterface::resetNoiseFloor()
|
|
{
|
|
currentSampleIndex = 0;
|
|
isNoiseFloorBufferFull = false;
|
|
currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
|
LOG_INFO("Noise floor reset - rolling window will restart");
|
|
}
|
|
|
|
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
|
|
{
|
|
if (!buffer || length == 0 || !iface) {
|
|
return false;
|
|
}
|
|
|
|
// Older RadioLib versions only expose random(min, max), so fill the buffer byte-by-byte.
|
|
for (size_t i = 0; i < length; ++i) {
|
|
int32_t value = iface->random(0, 255);
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
buffer[i] = static_cast<uint8_t>(value & 0xFF);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** radio helper thread callback.
|
|
We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of
|
|
'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision.
|
|
The CW size is determined by setTransmitDelay() and depends either on the current channel utilization or SNR in case
|
|
of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is
|
|
currently active.
|
|
*/
|
|
// In software-IRQ-poll mode (LORA_DIO1_SOFTWARE_POLL) a 1ms poll tick is almost always pending, so
|
|
// TX timers must be allowed to overwrite the pending notification or TX scheduling starves. On all
|
|
// other targets keep the historical non-overwriting behavior.
|
|
#ifdef LORA_DIO1_SOFTWARE_POLL
|
|
static constexpr bool txTimerOverwrite = true;
|
|
#else
|
|
static constexpr bool txTimerOverwrite = false;
|
|
#endif
|
|
|
|
// cppcheck-suppress constParameterPointer ; a function pointer can't meaningfully point to const
|
|
bool RadioLibInterface::isIsrTxCallback(void (*callback)())
|
|
{
|
|
return callback == isrTxLevel0;
|
|
}
|
|
|
|
void RadioLibInterface::scheduleIrqPollTick()
|
|
{
|
|
// Never overwrite a pending notification (especially TRANSMIT_DELAY_COMPLETED),
|
|
// otherwise poll ticks would starve TX scheduling.
|
|
//
|
|
// There is a single notification slot, so while a TX is queued and the radio is busy receiving,
|
|
// the self-rescheduling TRANSMIT_DELAY_COMPLETED timer (which does overwrite, see txTimerOverwrite)
|
|
// can keep the slot and prevent a poll tick from being scheduled. In that window a completing
|
|
// RX/TX is not seen by the poll; RadioInterface's pollMissedIrqs() (~1s) is the backup that
|
|
// recovers it, so the effect is bounded added latency under heavy contention, not a lost event.
|
|
notifyLater(1, ISR_POLL_TICK, false);
|
|
}
|
|
|
|
void RadioLibInterface::deliverPendingIrqFromPoll(PendingISR cause)
|
|
{
|
|
disableInterrupt(); // stop polling; this is the poll-path equivalent of isrLevel0Common()
|
|
notify(cause, true);
|
|
}
|
|
|
|
void RadioLibInterface::onNotify(uint32_t notification)
|
|
{
|
|
|
|
switch (notification) {
|
|
case ISR_TX:
|
|
handleTransmitInterrupt(); // completeSending() already restored the radio to the home config
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
// Pre-switch the radio to the NEXT queued packet's beacon config (no-op for normal traffic).
|
|
// Not required for correctness - TRANSMIT_DELAY_COMPLETED would switch before CAD anyway - but
|
|
// doing it here lets the next beacon skip the switch-only delay cycle and, more importantly,
|
|
// keeps the post-TX listen window (and the CAD/LBT that follows) on the channel we're about to
|
|
// transmit on. Only engages when the next packet is itself a beacon - exactly when we want it.
|
|
MeshBeaconModule::reconfigureForBeaconTX(this, txQueue.getFront());
|
|
#endif
|
|
startReceive();
|
|
setTransmitDelay();
|
|
break;
|
|
case ISR_RX:
|
|
handleReceiveInterrupt();
|
|
startReceive();
|
|
setTransmitDelay();
|
|
break;
|
|
case ISR_POLL_TICK:
|
|
handleSoftwareLoraIrqPoll();
|
|
break;
|
|
case TRANSMIT_DELAY_COMPLETED:
|
|
|
|
// If we are not currently in receive mode, then restart the random delay (this can happen if the main thread
|
|
// has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode?
|
|
if (!txQueue.empty()) {
|
|
if (!canSendImmediately()) {
|
|
setTransmitDelay(); // currently Rx/Tx-ing: reset random delay
|
|
} else {
|
|
meshtastic_MeshPacket *txp = txQueue.getFront();
|
|
assert(txp);
|
|
const uint32_t now = Time::getMillis();
|
|
// Not `long remaining = tx_after - millis()`: that uint32_t subtraction widens to
|
|
// ~4.29e9 where long is 64-bit (portduino), rescheduling a due packet ~49.7 days out.
|
|
if (txp->tx_after && !Throttle::deadlinePassedAt(now, txp->tx_after)) {
|
|
// There's still some delay pending on this packet, so resume waiting for it to elapse
|
|
notifyLater(txp->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite);
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
} else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) {
|
|
// The beacon's target radio config is invalid (bad preset/region, or an
|
|
// unlicensed node keying up on a ham-only region). Drop the packet - never
|
|
// transmit it on the current (home) config - and move on to the next queued packet.
|
|
LOG_DEBUG("Beacon: invalid TX radio config, drop packet 0x%08x", txp->id);
|
|
meshtastic_MeshPacket *bad = txQueue.dequeue();
|
|
MeshBeaconModule::clearTargetRadioSettings(bad);
|
|
packetPool.release(bad);
|
|
setTransmitDelay();
|
|
} else if (MeshBeaconModule::reconfigureForBeaconTX(this, txp)) {
|
|
setTransmitDelay();
|
|
#endif
|
|
} else {
|
|
if (isChannelActive()) { // check if there is currently a LoRa packet on the channel
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
if (!MeshBeaconModule::hasTargetRadioSettings(txp))
|
|
#endif
|
|
{
|
|
startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again
|
|
}
|
|
setTransmitDelay();
|
|
} else {
|
|
// 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);
|
|
startSend(txp);
|
|
LOG_TRACE("%d packets in TX queue", txQueue.getMaxLen() - txQueue.getFree());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Do nothing, because the queue is empty
|
|
}
|
|
break;
|
|
default:
|
|
assert(0); // We expected to receive a valid notification from the ISR
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::setTransmitDelay()
|
|
{
|
|
meshtastic_MeshPacket *p = txQueue.getFront();
|
|
if (!p) {
|
|
return; // noop if there's nothing in the queue
|
|
}
|
|
|
|
// We want all sending/receiving to be done by our daemon thread.
|
|
// We use a delay here because this packet might have been sent in response to a packet we just received.
|
|
// So we want to make sure the other side has had a chance to reconfigure its radio.
|
|
|
|
if (p->tx_after) {
|
|
unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec();
|
|
unsigned long now = millis();
|
|
p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr));
|
|
notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite);
|
|
} else if (p->rx_snr == 0 && p->rx_rssi == 0) {
|
|
/* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally.
|
|
* This assumption is valid because of the offset generated by the radio to account for the noise
|
|
* floor.
|
|
*/
|
|
startTransmitTimer(true);
|
|
} else {
|
|
// If there is a SNR, start a timer scaled based on that SNR.
|
|
LOG_TRACE("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
|
|
startTransmitTimerRebroadcast(p);
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::startTransmitTimer(bool withDelay)
|
|
{
|
|
// If we have work to do and the timer wasn't already scheduled, schedule it now
|
|
if (!txQueue.empty()) {
|
|
uint32_t delay = !withDelay ? 1 : getTxDelayMsec();
|
|
notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p)
|
|
{
|
|
// If we have work to do and the timer wasn't already scheduled, schedule it now
|
|
if (!txQueue.empty()) {
|
|
uint32_t delay = getTxDelayMsecWeighted(p);
|
|
notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the packet is not already in the late rebroadcast window, move it there
|
|
*/
|
|
void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id)
|
|
{
|
|
// Look for non-late packets only, so we don't do this twice!
|
|
meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false);
|
|
if (p) {
|
|
p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr);
|
|
bool dropped = false;
|
|
if (txQueue.enqueue(p, &dropped)) {
|
|
LOG_TRACE("Move queued packet to late rebroadcast window %ums from now", (uint32_t)(p->tx_after - millis()));
|
|
} else {
|
|
packetPool.release(p);
|
|
}
|
|
if (dropped) {
|
|
txDrop++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version
|
|
* @return Whether a pending packet was removed
|
|
*/
|
|
bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt)
|
|
{
|
|
meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt);
|
|
if (p) {
|
|
LOG_DEBUG("Drop pending-TX packet 0x%08x, hop limit %d", p->id, p->hop_limit);
|
|
packetPool.release(p);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void RadioLibInterface::handleTransmitInterrupt()
|
|
{
|
|
// This can be null if we forced the device to enter standby mode. In that case
|
|
// ignore the transmit interrupt
|
|
if (sendingPacket)
|
|
completeSending();
|
|
powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now
|
|
}
|
|
|
|
void RadioLibInterface::completeSending()
|
|
{
|
|
// We are careful to clear sending packet before calling printPacket because
|
|
// that can take a long time
|
|
auto p = sendingPacket;
|
|
sendingPacket = NULL;
|
|
#ifdef LED_LORA
|
|
digitalWrite(LED_LORA, LED_STATE_OFF);
|
|
#endif
|
|
|
|
if (p) {
|
|
// Packet has been sent, count it toward our TX airtime utilization.
|
|
uint32_t xmitMsec = getPacketTime(p);
|
|
airTime->logAirtime(TX_LOG, xmitMsec);
|
|
|
|
txGood++;
|
|
if (!isFromUs(p))
|
|
txRelay++;
|
|
printPacket("Completed sending", p);
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
MeshBeaconModule::clearTargetRadioSettings(p);
|
|
MeshBeaconModule::reconfigureForBeaconTX(this, nullptr);
|
|
#endif
|
|
|
|
// We are done sending that packet, release it
|
|
packetPool.release(p);
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::handleReceiveInterrupt()
|
|
{
|
|
// when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race
|
|
// Condition?
|
|
if (!isReceiving) {
|
|
LOG_ERROR("handleReceiveInterrupt called while not in rx mode");
|
|
return;
|
|
}
|
|
|
|
isReceiving = false;
|
|
|
|
// read the number of actually received bytes
|
|
size_t length = iface->getPacketLength();
|
|
|
|
// Some drivers report this as a 16 bit value, so a bad readback can overrun radioBuffer in readData()
|
|
if (length > sizeof(radioBuffer)) {
|
|
LOG_ERROR("Ignore rx packet, bad length %u", (unsigned int)length);
|
|
rxBad++;
|
|
return;
|
|
}
|
|
|
|
uint32_t rxMsec = getPacketTime(length, true);
|
|
|
|
#ifndef DISABLE_WELCOME_UNSET
|
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
LOG_WARN("lora rx disabled: Region unset");
|
|
airTime->logAirtime(RX_ALL_LOG, rxMsec);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
int state = iface->readData((uint8_t *)&radioBuffer, length);
|
|
#if ARCH_PORTDUINO
|
|
if (portduino_config.logoutputlevel == level_trace) {
|
|
printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length);
|
|
}
|
|
#endif
|
|
if (state != RADIOLIB_ERR_NONE) {
|
|
// Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs.
|
|
LOG_ERROR("Ignore rx packet, error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i "
|
|
"nextHop=0x%x relay=0x%x)",
|
|
state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags,
|
|
iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node);
|
|
rxBad++;
|
|
|
|
airTime->logAirtime(RX_ALL_LOG, rxMsec);
|
|
|
|
} else {
|
|
// Skip the 4 headers that are at the beginning of the rxBuf
|
|
int32_t payloadLen = length - sizeof(PacketHeader);
|
|
|
|
// check for short packets
|
|
if (payloadLen < 0) {
|
|
LOG_WARN("Ignore received packet too short");
|
|
rxBad++;
|
|
airTime->logAirtime(RX_ALL_LOG, rxMsec);
|
|
} else {
|
|
rxGood++;
|
|
// altered packet with "from == 0" can do Remote Node Administration without permission
|
|
if (radioBuffer.header.from == 0) {
|
|
LOG_WARN("Ignore received packet without sender");
|
|
return;
|
|
}
|
|
|
|
// Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous).
|
|
// This allows the router and other apps on our node to sniff packets (usually routing) between other
|
|
// nodes.
|
|
meshtastic_MeshPacket *mp = packetPool.allocZeroed();
|
|
if (!mp) {
|
|
airTime->logAirtime(RX_LOG, rxMsec);
|
|
return;
|
|
}
|
|
|
|
// Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto
|
|
mp->from = radioBuffer.header.from;
|
|
mp->to = radioBuffer.header.to;
|
|
mp->id = radioBuffer.header.id;
|
|
mp->channel = radioBuffer.header.channel;
|
|
assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code
|
|
mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK;
|
|
mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT;
|
|
mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK);
|
|
mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK);
|
|
// If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3)
|
|
mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop;
|
|
mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node;
|
|
|
|
addReceiveMetadata(mp);
|
|
|
|
mp->which_payload_variant =
|
|
meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point
|
|
assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes));
|
|
memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen);
|
|
mp->encrypted.size = payloadLen;
|
|
|
|
printPacket("Lora RX", mp);
|
|
|
|
#ifdef LED_LORA
|
|
loraRxPacketObservable.notifyObservers(mp->from);
|
|
#endif
|
|
|
|
airTime->logAirtime(RX_LOG, rxMsec);
|
|
|
|
deliverToReceiver(mp);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::startReceive()
|
|
{
|
|
isReceiving = true;
|
|
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
|
|
}
|
|
|
|
void RadioLibInterface::pollMissedIrqs()
|
|
{
|
|
// RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges.
|
|
if (isReceiving) {
|
|
checkRxDoneIrqFlag();
|
|
}
|
|
if (sendingPacket) {
|
|
checkTxDoneIrqFlag();
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::resetAGC()
|
|
{
|
|
// Base implementation: no-op. Override in chip-specific subclasses.
|
|
}
|
|
|
|
void RadioLibInterface::checkRxDoneIrqFlag()
|
|
{
|
|
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
|
|
LOG_WARN("caught missed RX_DONE");
|
|
notify(ISR_RX, true);
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::checkTxDoneIrqFlag()
|
|
{
|
|
if (iface->checkIrq(RADIOLIB_IRQ_TX_DONE)) {
|
|
LOG_WARN("caught missed TX_DONE");
|
|
notify(ISR_TX, true);
|
|
}
|
|
}
|
|
|
|
void RadioLibInterface::configHardwareForSend()
|
|
{
|
|
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
|
|
}
|
|
|
|
void RadioLibInterface::setStandby()
|
|
{
|
|
// neither sending nor receiving
|
|
powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn);
|
|
powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn);
|
|
}
|
|
|
|
/** start an immediate transmit */
|
|
bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
|
{
|
|
/* NOTE: Minimize the actions before startTransmit() to keep the time between
|
|
channel scan and actual transmit as low as possible to avoid collisions. */
|
|
if (disabled || !config.lora.tx_enabled) {
|
|
LOG_WARN("Drop Tx packet: LoRa Tx disabled");
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
// This packet may have already triggered a beacon radio switch in TRANSMIT_DELAY_COMPLETED;
|
|
// since it never reaches completeSending() here, restore the radio so it isn't left on the
|
|
// beacon config (which would also break RX on the home channel).
|
|
MeshBeaconModule::clearTargetRadioSettings(txp);
|
|
MeshBeaconModule::reconfigureForBeaconTX(this, nullptr);
|
|
#endif
|
|
packetPool.release(txp);
|
|
return false;
|
|
} else {
|
|
configHardwareForSend(); // must be after setStandby
|
|
|
|
size_t numbytes = beginSending(txp);
|
|
|
|
int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes);
|
|
if (res != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("startTransmit failed, error=%d", res);
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG);
|
|
|
|
// This send failed, but make sure to 'complete' it properly
|
|
completeSending();
|
|
powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now
|
|
startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode)
|
|
} else {
|
|
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register
|
|
// bits
|
|
enableInterrupt(isrTxLevel0);
|
|
lastTxStart = millis();
|
|
printPacket("Started Tx", txp);
|
|
#ifdef LED_LORA
|
|
digitalWrite(LED_LORA, LED_STATE_ON);
|
|
#endif
|
|
}
|
|
|
|
return res == RADIOLIB_ERR_NONE;
|
|
}
|
|
}
|