Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ static char ethernet_command[160];
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot

// How long loop() is willing to idle between iterations, for boards that
// implement MainBoard::idleUntilEvent().
//
// The bound is the shortest deadline not already delivered by the radio IRQ,
// and there are two. Dispatcher::getCADFailRetryDelay() is 200 ms, which 50 ms
// clears with 4x margin. The delayed-inbound queue is the tighter one: its
// delay is randomised per packet but floored at exactly 50 ms, because
// checkRecv() processes anything below that immediately rather than queueing
// it. So a queued inbound packet can be serviced up to one full iteration late.
//
// That is latency, not error. The delay being quantised is a randomised
// collision-spreading interval, and nodes do not wake in step with one another,
// so rounding it up adds jitter to a quantity that is already jitter -- it
// cannot bunch two nodes onto the same slot the way a synchronised delay would.
// Nothing else needs a faster iteration: RX-done and TX-done arrive on the IRQ.
// Boards with no implementation ignore this entirely and keep busy-looping.
#ifndef IDLE_MAX_WAIT_MS
#define IDLE_MAX_WAIT_MS 50
#endif

#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long userBtnDownAt = 0;
#define USER_BTN_HOLD_OFF_MILLIS 1500
Expand Down Expand Up @@ -213,4 +233,10 @@ void loop() {
// Small delay to prevent busy loop on platforms without power saving
delay(1);
}

// Idle instead of spinning between iterations. Default implementation is a
// no-op, so this is safe on every board; those that implement it block on
// the radio IRQ (and any other descriptor they will drain) until it fires or
// IDLE_MAX_WAIT_MS elapses.
board.idleUntilEvent(IDLE_MAX_WAIT_MS);
}
3 changes: 3 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ test_framework = googletest
build_flags = -std=c++17
-I src
-I test/mocks
-I variants/linux
test_build_src = yes
test_ignore = test_kiss_modem
build_src_filter =
Expand All @@ -227,6 +228,8 @@ build_src_filter =
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/DynamicConfigSerializer.cpp>
+<../variants/linux/LinuxEventLoop.cpp>
+<../variants/linux/LinuxRadioWait.cpp>
lib_deps =
google/googletest @ 1.17.0

Expand Down
24 changes: 24 additions & 0 deletions src/MeshCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ class MainBoard {
virtual void onBootComplete() { /* no op */ }
virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x)
virtual void sleep(uint32_t secs) { /* no op */ }

/**
* Idle until an event that needs servicing arrives -- the radio IRQ, or any
* other descriptor the platform knows the caller will drain this iteration --
* or until max_wait_ms elapses, whichever comes first.
*
* Returning early, or immediately, is ALWAYS correct: loop() re-checks all
* state on every iteration, so this is a pure "don't spin" hint and never a
* source of scheduling guarantees. Two obligations for implementers:
*
* - Do not lose an IRQ that is already asserted on entry. A level-latched
* line (SX1262 DIO1) that went high before the wait began may produce no
* further edge, so check the level first and return immediately if it is
* set. ESP32Board::sleep() does this via gpio_get_level().
* - Do not wait on a descriptor the caller will not drain, or the wait
* returns instantly forever and the loop spins anyway.
*
* Distinct from sleep(): this keeps peripherals live and is always safe to
* call, whereas sleep() is an opt-in deep sleep that may drop them.
*
* Default no-op: boards that don't implement it keep the historical
* busy-loop behaviour.
*/
virtual void idleUntilEvent(uint32_t max_wait_ms) { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
Expand Down
60 changes: 60 additions & 0 deletions src/helpers/radiolib/LinuxSX1262Wrapper.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
#pragma once

#include "LinuxSX1262.h"
#include "LinuxRadioWait.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"

class LinuxSX1262Wrapper : public RadioLibWrapper {
// How long performChannelScan() will wait for DIO1 before giving up on the
// line and reading the result over SPI. Set from the active SF/BW by
// setParams(); the initial value covers only the window before the first
// call, so it is seeded from the slowest scan a MeshCore preset can produce
// (SF12 at 62.5 kHz) rather than a hand-checked constant. CAD cannot actually
// run in that window -- _cad_enabled stays false until Dispatcher::loop()
// first pushes it -- so this is belt-and-braces rather than a live value.
uint32_t _cad_timeout_ms = cadTimeoutMillis(symbolMicros(12, 62.5f));

// _radio is held as the base mesh::Radio, so every use here needs the
// downcast. It is always a LinuxSX1262 -- the constructor takes one by
// reference -- so this is a naming convenience, not a checked conversion.
LinuxSX1262* r() const { return (LinuxSX1262 *)_radio; }

// Same for the board. waitForRadioIrq() is LinuxBoard's, not
// mesh::MainBoard's, and this reaches it through the member the wrapper was
// constructed with rather than through the `board` global LinuxSX1262.h
// declares. Those are the same object today; going through the member is what
// keeps them the same object if a second instance is ever constructed, and
// stops this file quietly depending on a global it does not own.
LinuxBoard* b() const { return (LinuxBoard *)_board; }

public:
LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }

Expand All @@ -22,6 +40,48 @@ class LinuxSX1262Wrapper : public RadioLibWrapper {
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
r()->setPreambleMillis(pm.preambleMillis);
r()->setMaxPayloadMillis(pm.payloadMillis);
_cad_timeout_ms = cadTimeoutMillis(symbolMicros(sf, bw));
}

// Hardware CAD without RadioLib's busy-wait.
//
// The base implementation calls scanChannel(), which spins on
// digitalRead(DIO1) until the line rises. On an MCU with nothing else to do
// that is merely wasteful; here it burns a core for the length of every scan,
// against an event loop built to sleep, and -- because it has no deadline --
// it turns a GPIO read that has started failing into an unbreakable hang.
// EventGPIOPin deliberately reads LOW on failure so a broken line degrades to
// "no packet" plus one logged error; inside an untimed spin that same failure
// would lock up the daemon.
//
// Splitting the scan into start / wait / read fixes both. Nothing is lost by
// blocking here: startChannelScan() puts the modem in standby first, so no
// packet can arrive during the scan and there is nothing for the loop to
// overlap with.
int16_t performChannelScan() override {
// Same configuration scanChannel() used: 4 symbols, exit to STDBY_RC, and
// DIO1 mapped to CAD_DONE | CAD_DETECTED. CAD_DONE being in that mask is
// what the wait below depends on -- the line rises however the scan
// resolves, so a free channel arrives as an edge and not as a timeout.
// startChannelScan() also clears the IRQ status, so DIO1 is low on entry.
int16_t state = r()->startChannelScan();
if (state != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("LinuxSX1262Wrapper: startChannelScan() failed (%d)", state);
return state; // isChannelActive() reads anything but CHANNEL_FREE as busy
}

if (!b()->waitForRadioIrq(_cad_timeout_ms)) {
// Logged every time rather than latched: the rate is bounded by transmit
// attempts, and a line that has stopped reporting should stay visible for
// as long as it is broken.
MESH_DEBUG_PRINTLN("LinuxSX1262Wrapper: CAD IRQ did not arrive within %ums", _cad_timeout_ms);
}

// Read the verdict whether or not DIO1 reported it. getChannelScanResult()
// goes over SPI to the modem's IRQ status register, which is authoritative
// and wholly independent of the GPIO -- so a dead line costs latency and a
// log line, never a wrong answer, and never a hang.
return r()->getChannelScanResult();
}

// Full SX126x receiver reset (warm sleep, recalibrate, re-image the configured
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/radiolib/RadioLibWrappers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {

PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) {
// based on RadioLib's calculateTimeOnAir()
uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10);
uint32_t tsym_us = symbolMicros(sf, bw);
uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd

// preamble + syncword + sfd + header
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/radiolib/RadioLibWrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class RadioLibWrapper : public mesh::Radio {
virtual float getCurrentRSSI() =0;
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; }
// LoRa symbol time in microseconds, for a spreading factor and a bandwidth in
// kHz. Every airtime and timeout derived from the modem's rate starts here.
static uint32_t symbolMicros(uint8_t sf, float bw) { return ((uint32_t)10000 << sf) / (bw * 10); }
void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); }
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols);
virtual int16_t performChannelScan();
Expand Down
Loading
Loading