mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 20:23:53 +00:00
make CAD retry jittery, sync prefs load to companion-style
This commit is contained in:
@@ -216,7 +216,7 @@ loop():
|
||||
- Flood packets: compute RX delay based on score → defer or process immediately
|
||||
- Direct packets: process immediately
|
||||
4. checkSend(): Check outbound queue
|
||||
- CAD: if channel busy, retry with random backoff (120-480ms)
|
||||
- CAD: if channel busy, retry every 100-200ms (jittered) up to 4s total
|
||||
- Duty cycle: if exceeded, defer 5 seconds (admin packets exempt)
|
||||
- Final LBT check right before TX (closes timing gap)
|
||||
- Serialize and transmit
|
||||
@@ -313,10 +313,9 @@ Compile-time selection via `CONFIG_ZEPHCORE_RADIO_LR1110` in `RadioIncludes.h`.
|
||||
### 5.3 Noise Floor EMA
|
||||
|
||||
Algorithm in `triggerNoiseFloorCalibrate()`:
|
||||
- Random 0-500ms jitter to break phase-lock with interference
|
||||
- 4 RSSI samples per tick, take minimum
|
||||
- 8 RSSI samples per tick, take median (insertion-sort midpoint)
|
||||
- Threshold filter: reject samples ≥ floor + 14dB (after 8-tick warmup)
|
||||
- Periodic bypass: every 8th tick accepts unconditionally
|
||||
- Periodic bypass: every 16th tick accepts unconditionally
|
||||
- EMA: `floor += round_nearest((sample - floor) / 8)`, clamped to [-120, -50] dBm
|
||||
|
||||
### 5.4 LR1110 Driver Errata Workarounds
|
||||
@@ -405,7 +404,7 @@ Power: `powersaving on/off`
|
||||
- **Internal**: LittleFS on flash (`/lfs`), 256-byte cache for reduced flash I/O
|
||||
- **External**: Optional LittleFS on QSPI (`/ext`) with auto-migration
|
||||
- **BLE bonds**: File-based settings on LittleFS (`/lfs/settings/`) — all platforms (no NVS)
|
||||
- **Prefs**: 93-byte binary format, Arduino-compatible, field-by-field I/O
|
||||
- **Prefs**: 292-byte binary format, Arduino-compatible, field-by-field I/O (see §13)
|
||||
- **Contacts**: 152-byte records, stored on external flash if available
|
||||
- **Channels**: 68-byte records (4 pad + 32 name + 32 secret)
|
||||
- **Blobs**: Fixed-size records with LRU eviction by timestamp
|
||||
@@ -688,11 +687,10 @@ Dispatcher::checkRecv() → Mesh::onRecvPacket()
|
||||
main event loop (every 5s) → Dispatcher::maintenanceLoop()
|
||||
→ radio->triggerNoiseFloorCalibrate(threshold)
|
||||
→ guards: in RX? TX active? duty cycle? mid-receive?
|
||||
→ random 0-500ms jitter
|
||||
→ read 4 RSSI samples, take min
|
||||
→ read 8 RSSI samples, take median
|
||||
→ first sample: seed directly
|
||||
→ warmup (<8 ticks): accept unconditionally
|
||||
→ periodic bypass (every 8th): accept unconditionally
|
||||
→ periodic bypass (every 16th): accept unconditionally
|
||||
→ otherwise: reject if sample ≥ floor + 14dB
|
||||
→ EMA: floor += round((sample - floor) / 8)
|
||||
→ clamp [-120, -50] dBm
|
||||
|
||||
@@ -878,13 +878,12 @@ RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Mil
|
||||
}
|
||||
|
||||
void RepeaterMesh::begin(RepeaterDataStore* store) {
|
||||
mesh::Mesh::begin();
|
||||
_store = store;
|
||||
|
||||
/* Load persisted data.
|
||||
* NOTE: Identity is loaded in main_repeater.cpp before begin() is called,
|
||||
* so we skip loading it here (self_id should already be set). */
|
||||
_store->loadPrefs(_prefs);
|
||||
/* Prefs and identity are loaded by the caller (main_repeater.cpp) before
|
||||
* begin() — the radio reads freq/bw/sf/cr through _prefs during
|
||||
* Mesh::begin() → Dispatcher::begin() → Radio::begin(). */
|
||||
mesh::Mesh::begin();
|
||||
_contention.setBackoffMultiplier(_prefs.backoff_multiplier);
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
_power_ctrl.setSF(_prefs.sf);
|
||||
|
||||
+444
-440
@@ -1,440 +1,444 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Dispatcher implementation
|
||||
*/
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
#define PAYLOAD_TYPE_REQ 0x00
|
||||
#define PAYLOAD_TYPE_RESPONSE 0x01
|
||||
#define PAYLOAD_TYPE_TXT_MSG 0x02
|
||||
#define PAYLOAD_TYPE_PATH 0x08
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#define MAX_RX_DELAY_MILLIS 32000 /* upper bound for score-based RX delay */
|
||||
|
||||
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
||||
{
|
||||
outbound = nullptr;
|
||||
total_air_time = rx_air_time = 0;
|
||||
next_tx_time = 0;
|
||||
cad_busy_start = 0;
|
||||
next_agc_reset_time = 0;
|
||||
_err_flags = 0;
|
||||
_duty_cycle.init(0);
|
||||
radio_nonrx_start = 0;
|
||||
prev_isrecv_mode = true;
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_tx_queued_cb = nullptr;
|
||||
_tx_queued_user_data = nullptr;
|
||||
}
|
||||
|
||||
void Dispatcher::begin()
|
||||
{
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_err_flags = 0;
|
||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
_duty_cycle.init(getDutyCyclePercent());
|
||||
}
|
||||
|
||||
uint8_t Dispatcher::getDutyCyclePercent() const
|
||||
{
|
||||
return 10; /* EU 868 default: 10% duty cycle */
|
||||
}
|
||||
|
||||
bool Dispatcher::isAdminPacket(const Packet *pkt)
|
||||
{
|
||||
uint8_t t = pkt->getPayloadType();
|
||||
return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE ||
|
||||
t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL;
|
||||
}
|
||||
|
||||
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const
|
||||
{
|
||||
/* LUT: 10^(0.85 - i*0.1) - 1, i=0..10; replaces powf() (~1.9KB saved) */
|
||||
static const float lut[11] = {
|
||||
6.0793f, 4.6236f, 3.4674f, 2.5489f, 1.8184f, 1.2389f,
|
||||
0.7783f, 0.4125f, 0.1220f, -0.1089f, -0.2921f
|
||||
};
|
||||
if (score <= 0.0f) return (int)(lut[0] * (float)air_time);
|
||||
if (score >= 1.0f) return (int)(lut[10] * (float)air_time);
|
||||
float idx = score * 10.0f;
|
||||
int i = (int)idx;
|
||||
float frac = idx - (float)i;
|
||||
float val = lut[i] + frac * (lut[i + 1] - lut[i]);
|
||||
return (int)(val * (float)air_time);
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::getCADFailRetryDelay() const
|
||||
{
|
||||
return 200; /* ms between CAD retries; ~2 LoRa symbol periods at SF8/62.5k */
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::getCADFailMaxDuration() const
|
||||
{
|
||||
return 4000; /* ms; ~20 retry attempts before giving up */
|
||||
}
|
||||
|
||||
void Dispatcher::loop()
|
||||
{
|
||||
if (outbound) {
|
||||
if (_radio->isSendComplete()) {
|
||||
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
|
||||
total_air_time += t;
|
||||
_duty_cycle.recordTx(t, (uint32_t)_ms->getMillis());
|
||||
_radio->onSendFinished();
|
||||
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
if (outbound->isRouteFlood()) {
|
||||
n_sent_flood++;
|
||||
} else {
|
||||
n_sent_direct++;
|
||||
}
|
||||
releasePacket(outbound);
|
||||
outbound = nullptr;
|
||||
} else if (millisHasNowPassed(outbound_expiry)) {
|
||||
_radio->onSendFinished();
|
||||
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
releasePacket(outbound);
|
||||
outbound = nullptr;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||
}
|
||||
|
||||
{
|
||||
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
|
||||
if (pkt) {
|
||||
processRecvPacket(pkt);
|
||||
}
|
||||
}
|
||||
checkRecv();
|
||||
checkSend();
|
||||
}
|
||||
|
||||
void Dispatcher::maintenanceLoop()
|
||||
{
|
||||
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
|
||||
|
||||
/* RX mode watchdog: TX counts as "active" to avoid false triggers
|
||||
* when the 5s housekeeping timer misses brief RX windows. */
|
||||
bool is_active = _radio->isInRecvMode() || !_radio->isSendComplete();
|
||||
if (is_active != prev_isrecv_mode) {
|
||||
prev_isrecv_mode = is_active;
|
||||
if (!is_active) {
|
||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||
}
|
||||
}
|
||||
if (!is_active && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) { /* 8s stall threshold */
|
||||
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
|
||||
}
|
||||
|
||||
/* Periodic AGC recalibration */
|
||||
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
|
||||
_radio->resetAGC();
|
||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
pkt->header = raw[i++];
|
||||
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
|
||||
LOG_WRN("tryParsePacket: unsupported packet version");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pkt->hasTransportCodes()) {
|
||||
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
|
||||
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
|
||||
} else {
|
||||
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
|
||||
}
|
||||
|
||||
pkt->path_len = raw[i++];
|
||||
uint8_t path_mode = pkt->path_len >> 6;
|
||||
if (path_mode == 3) { /* reserved path mode */
|
||||
LOG_WRN("tryParsePacket: unsupported path mode: 3");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
|
||||
if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
|
||||
LOG_WRN("tryParsePacket: partial or corrupt packet, len=%d", len);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len;
|
||||
|
||||
pkt->payload_len = len - i;
|
||||
if (pkt->payload_len > (int)sizeof(pkt->payload)) {
|
||||
LOG_WRN("tryParsePacket: payload too big, payload_len=%d", (uint32_t)pkt->payload_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(pkt->payload, &raw[i], pkt->payload_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Dispatcher::checkRecv()
|
||||
{
|
||||
/* k_event is a bitfield — multiple ISR arrivals coalesce into one
|
||||
* wake, so drain the entire ring each time. */
|
||||
for (;;) {
|
||||
uint8_t raw[MAX_TRANS_UNIT + 1];
|
||||
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
|
||||
if (len <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len);
|
||||
|
||||
Packet *pkt = _mgr->allocNew();
|
||||
if (pkt == nullptr) {
|
||||
LOG_ERR("checkRecv: packet alloc failed");
|
||||
break;
|
||||
}
|
||||
|
||||
float score = 0.0f;
|
||||
uint32_t air_time = 0;
|
||||
|
||||
if (tryParsePacket(pkt, raw, len)) {
|
||||
pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f); /* x4 fixed-point SNR */
|
||||
score = _radio->packetScore(_radio->getLastSNR(), len);
|
||||
air_time = _radio->getEstAirtimeFor(len);
|
||||
rx_air_time += air_time;
|
||||
} else {
|
||||
_mgr->free(pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
|
||||
{
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
static char hash_hex[MAX_HASH_SIZE * 2 + 1];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE);
|
||||
|
||||
uint8_t ptype = pkt->getPayloadType();
|
||||
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
|
||||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
|
||||
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n",
|
||||
getLogDateTime(), pkt->getRawLength(), ptype,
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
|
||||
(int)(score * 1000), air_time, hash_hex,
|
||||
(uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n",
|
||||
getLogDateTime(), pkt->getRawLength(), ptype,
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
|
||||
(int)(score * 1000), air_time, hash_hex);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
logRx(pkt, pkt->getRawLength(), score);
|
||||
if (pkt->isRouteFlood()) {
|
||||
n_recv_flood++;
|
||||
processRecvPacket(pkt);
|
||||
} else {
|
||||
n_recv_direct++;
|
||||
processRecvPacket(pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::processRecvPacket(Packet *pkt)
|
||||
{
|
||||
DispatcherAction action = onRecvPacket(pkt);
|
||||
if (action == ACTION_RELEASE) {
|
||||
_mgr->free(pkt);
|
||||
} else if (action == ACTION_MANUAL_HOLD) {
|
||||
/* subclass holds packet */
|
||||
} else {
|
||||
uint8_t priority = (uint8_t)((action >> 24) - 1);
|
||||
uint32_t delay = action & 0xFFFFFF;
|
||||
_mgr->queueOutbound(pkt, priority, futureMillis((int)delay));
|
||||
if (_tx_queued_cb && delay > 0) {
|
||||
_tx_queued_cb(delay, _tx_queued_user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::checkSend()
|
||||
{
|
||||
uint32_t now = (uint32_t)_ms->getMillis();
|
||||
int count = _mgr->getOutboundCount(now);
|
||||
if (count == 0) {
|
||||
cad_busy_start = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_radio->isReceiving()) {
|
||||
/* Channel busy — enforce retry timer so we don't hammer the check */
|
||||
if (!millisHasNowPassed(next_tx_time)) {
|
||||
if (_tx_queued_cb) {
|
||||
uint32_t remaining = next_tx_time - now;
|
||||
_tx_queued_cb(remaining + 1, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cad_busy_start == 0) {
|
||||
cad_busy_start = now;
|
||||
}
|
||||
if (now - cad_busy_start > getCADFailMaxDuration()) {
|
||||
_err_flags |= ERR_EVENT_CAD_TIMEOUT;
|
||||
LOG_ERR("checkSend: CAD timeout exceeded");
|
||||
} else {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
next_tx_time = futureMillis((int)retry);
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry + 1, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
cad_busy_start = 0;
|
||||
|
||||
outbound = _mgr->getNextOutbound(now);
|
||||
if (outbound) {
|
||||
/* Duty cycle enforcement — exempt admin packets */
|
||||
if (!isAdminPacket(outbound) && _duty_cycle.isExceeded(now)) {
|
||||
LOG_WRN("checkSend: duty cycle exceeded (%u/%u ms), re-queuing type=%d",
|
||||
_duty_cycle.window_airtime_ms, _duty_cycle.budgetMs(),
|
||||
outbound->getPayloadType());
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis(5000));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(5000, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint8_t raw[MAX_TRANS_UNIT];
|
||||
int len = 0;
|
||||
raw[len++] = outbound->header;
|
||||
if (outbound->hasTransportCodes()) {
|
||||
memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
|
||||
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
|
||||
}
|
||||
raw[len++] = outbound->path_len;
|
||||
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
|
||||
|
||||
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
|
||||
LOG_ERR("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT);
|
||||
_mgr->free(outbound);
|
||||
outbound = nullptr;
|
||||
} else {
|
||||
memcpy(&raw[len], outbound->payload, outbound->payload_len);
|
||||
len += outbound->payload_len;
|
||||
|
||||
uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2;
|
||||
outbound_start = now;
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
|
||||
{
|
||||
uint8_t ptype = outbound->getPayloadType();
|
||||
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
|
||||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
|
||||
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n",
|
||||
getLogDateTime(), len, ptype,
|
||||
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len,
|
||||
(uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
|
||||
} else {
|
||||
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n",
|
||||
getLogDateTime(), len, ptype,
|
||||
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Final LBT check — close the gap between initial
|
||||
* isReceiving() and actual TX start (serialisation +
|
||||
* logging can take 1-5 ms). */
|
||||
if (_radio->isReceiving()) {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis((int)retry));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = _radio->startSendRaw(raw, len);
|
||||
if (!success) {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
LOG_ERR("checkSend: startSendRaw failed! re-queuing delay=%u", retry);
|
||||
logTxFail(outbound, outbound->getRawLength());
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis((int)retry));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry, _tx_queued_user_data);
|
||||
}
|
||||
} else {
|
||||
outbound_expiry = futureMillis((int)max_airtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Packet *Dispatcher::obtainNewPacket()
|
||||
{
|
||||
Packet *pkt = _mgr->allocNew();
|
||||
if (pkt == nullptr) {
|
||||
_err_flags |= ERR_EVENT_FULL;
|
||||
} else {
|
||||
pkt->payload_len = pkt->path_len = 0;
|
||||
pkt->_snr = 0;
|
||||
}
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void Dispatcher::releasePacket(Packet *packet)
|
||||
{
|
||||
_mgr->free(packet);
|
||||
}
|
||||
|
||||
void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis)
|
||||
{
|
||||
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
|
||||
LOG_ERR("sendPacket: rejected - path_len=%d or payload_len=%d invalid",
|
||||
packet->path_len, packet->payload_len);
|
||||
_mgr->free(packet);
|
||||
} else {
|
||||
_mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis));
|
||||
if (_tx_queued_cb && delay_millis > 0) {
|
||||
_tx_queued_cb(delay_millis, _tx_queued_user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const
|
||||
{
|
||||
return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0;
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::futureMillis(int millis_from_now) const
|
||||
{
|
||||
return (uint32_t)_ms->getMillis() + millis_from_now;
|
||||
}
|
||||
|
||||
} /* namespace mesh */
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Dispatcher implementation
|
||||
*/
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/random/random.h>
|
||||
LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
#define PAYLOAD_TYPE_REQ 0x00
|
||||
#define PAYLOAD_TYPE_RESPONSE 0x01
|
||||
#define PAYLOAD_TYPE_TXT_MSG 0x02
|
||||
#define PAYLOAD_TYPE_PATH 0x08
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#define MAX_RX_DELAY_MILLIS 32000 /* upper bound for score-based RX delay */
|
||||
|
||||
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
||||
{
|
||||
outbound = nullptr;
|
||||
total_air_time = rx_air_time = 0;
|
||||
next_tx_time = 0;
|
||||
cad_busy_start = 0;
|
||||
next_agc_reset_time = 0;
|
||||
_err_flags = 0;
|
||||
_duty_cycle.init(0);
|
||||
radio_nonrx_start = 0;
|
||||
prev_isrecv_mode = true;
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_tx_queued_cb = nullptr;
|
||||
_tx_queued_user_data = nullptr;
|
||||
}
|
||||
|
||||
void Dispatcher::begin()
|
||||
{
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_err_flags = 0;
|
||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
_duty_cycle.init(getDutyCyclePercent());
|
||||
}
|
||||
|
||||
uint8_t Dispatcher::getDutyCyclePercent() const
|
||||
{
|
||||
return 10; /* EU 868 default: 10% duty cycle */
|
||||
}
|
||||
|
||||
bool Dispatcher::isAdminPacket(const Packet *pkt)
|
||||
{
|
||||
uint8_t t = pkt->getPayloadType();
|
||||
return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE ||
|
||||
t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL;
|
||||
}
|
||||
|
||||
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const
|
||||
{
|
||||
/* LUT: 10^(0.85 - i*0.1) - 1, i=0..10; replaces powf() (~1.9KB saved) */
|
||||
static const float lut[11] = {
|
||||
6.0793f, 4.6236f, 3.4674f, 2.5489f, 1.8184f, 1.2389f,
|
||||
0.7783f, 0.4125f, 0.1220f, -0.1089f, -0.2921f
|
||||
};
|
||||
if (score <= 0.0f) return (int)(lut[0] * (float)air_time);
|
||||
if (score >= 1.0f) return (int)(lut[10] * (float)air_time);
|
||||
float idx = score * 10.0f;
|
||||
int i = (int)idx;
|
||||
float frac = idx - (float)i;
|
||||
float val = lut[i] + frac * (lut[i + 1] - lut[i]);
|
||||
return (int)(val * (float)air_time);
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::getCADFailRetryDelay() const
|
||||
{
|
||||
/* 100-200ms jittered retry: tighter than one SF8 flood airtime so we
|
||||
* sample multiple RX duty-cycle windows, and randomized so two nodes
|
||||
* contending on the same channel don't retry in lockstep. */
|
||||
return 100 + (sys_rand32_get() % 101);
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::getCADFailMaxDuration() const
|
||||
{
|
||||
return 4000; /* ms; ~20 retry attempts before giving up */
|
||||
}
|
||||
|
||||
void Dispatcher::loop()
|
||||
{
|
||||
if (outbound) {
|
||||
if (_radio->isSendComplete()) {
|
||||
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
|
||||
total_air_time += t;
|
||||
_duty_cycle.recordTx(t, (uint32_t)_ms->getMillis());
|
||||
_radio->onSendFinished();
|
||||
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
if (outbound->isRouteFlood()) {
|
||||
n_sent_flood++;
|
||||
} else {
|
||||
n_sent_direct++;
|
||||
}
|
||||
releasePacket(outbound);
|
||||
outbound = nullptr;
|
||||
} else if (millisHasNowPassed(outbound_expiry)) {
|
||||
_radio->onSendFinished();
|
||||
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
releasePacket(outbound);
|
||||
outbound = nullptr;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||
}
|
||||
|
||||
{
|
||||
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
|
||||
if (pkt) {
|
||||
processRecvPacket(pkt);
|
||||
}
|
||||
}
|
||||
checkRecv();
|
||||
checkSend();
|
||||
}
|
||||
|
||||
void Dispatcher::maintenanceLoop()
|
||||
{
|
||||
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
|
||||
|
||||
/* RX mode watchdog: TX counts as "active" to avoid false triggers
|
||||
* when the 5s housekeeping timer misses brief RX windows. */
|
||||
bool is_active = _radio->isInRecvMode() || !_radio->isSendComplete();
|
||||
if (is_active != prev_isrecv_mode) {
|
||||
prev_isrecv_mode = is_active;
|
||||
if (!is_active) {
|
||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||
}
|
||||
}
|
||||
if (!is_active && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) { /* 8s stall threshold */
|
||||
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
|
||||
}
|
||||
|
||||
/* Periodic AGC recalibration */
|
||||
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
|
||||
_radio->resetAGC();
|
||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
pkt->header = raw[i++];
|
||||
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
|
||||
LOG_WRN("tryParsePacket: unsupported packet version");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pkt->hasTransportCodes()) {
|
||||
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
|
||||
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
|
||||
} else {
|
||||
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
|
||||
}
|
||||
|
||||
pkt->path_len = raw[i++];
|
||||
uint8_t path_mode = pkt->path_len >> 6;
|
||||
if (path_mode == 3) { /* reserved path mode */
|
||||
LOG_WRN("tryParsePacket: unsupported path mode: 3");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
|
||||
if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
|
||||
LOG_WRN("tryParsePacket: partial or corrupt packet, len=%d", len);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len;
|
||||
|
||||
pkt->payload_len = len - i;
|
||||
if (pkt->payload_len > (int)sizeof(pkt->payload)) {
|
||||
LOG_WRN("tryParsePacket: payload too big, payload_len=%d", (uint32_t)pkt->payload_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(pkt->payload, &raw[i], pkt->payload_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Dispatcher::checkRecv()
|
||||
{
|
||||
/* k_event is a bitfield — multiple ISR arrivals coalesce into one
|
||||
* wake, so drain the entire ring each time. */
|
||||
for (;;) {
|
||||
uint8_t raw[MAX_TRANS_UNIT + 1];
|
||||
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
|
||||
if (len <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len);
|
||||
|
||||
Packet *pkt = _mgr->allocNew();
|
||||
if (pkt == nullptr) {
|
||||
LOG_ERR("checkRecv: packet alloc failed");
|
||||
break;
|
||||
}
|
||||
|
||||
float score = 0.0f;
|
||||
uint32_t air_time = 0;
|
||||
|
||||
if (tryParsePacket(pkt, raw, len)) {
|
||||
pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f); /* x4 fixed-point SNR */
|
||||
score = _radio->packetScore(_radio->getLastSNR(), len);
|
||||
air_time = _radio->getEstAirtimeFor(len);
|
||||
rx_air_time += air_time;
|
||||
} else {
|
||||
_mgr->free(pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
|
||||
{
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
static char hash_hex[MAX_HASH_SIZE * 2 + 1];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE);
|
||||
|
||||
uint8_t ptype = pkt->getPayloadType();
|
||||
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
|
||||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
|
||||
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n",
|
||||
getLogDateTime(), pkt->getRawLength(), ptype,
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
|
||||
(int)(score * 1000), air_time, hash_hex,
|
||||
(uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n",
|
||||
getLogDateTime(), pkt->getRawLength(), ptype,
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
|
||||
(int)(score * 1000), air_time, hash_hex);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
logRx(pkt, pkt->getRawLength(), score);
|
||||
if (pkt->isRouteFlood()) {
|
||||
n_recv_flood++;
|
||||
processRecvPacket(pkt);
|
||||
} else {
|
||||
n_recv_direct++;
|
||||
processRecvPacket(pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::processRecvPacket(Packet *pkt)
|
||||
{
|
||||
DispatcherAction action = onRecvPacket(pkt);
|
||||
if (action == ACTION_RELEASE) {
|
||||
_mgr->free(pkt);
|
||||
} else if (action == ACTION_MANUAL_HOLD) {
|
||||
/* subclass holds packet */
|
||||
} else {
|
||||
uint8_t priority = (uint8_t)((action >> 24) - 1);
|
||||
uint32_t delay = action & 0xFFFFFF;
|
||||
_mgr->queueOutbound(pkt, priority, futureMillis((int)delay));
|
||||
if (_tx_queued_cb && delay > 0) {
|
||||
_tx_queued_cb(delay, _tx_queued_user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::checkSend()
|
||||
{
|
||||
uint32_t now = (uint32_t)_ms->getMillis();
|
||||
int count = _mgr->getOutboundCount(now);
|
||||
if (count == 0) {
|
||||
cad_busy_start = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_radio->isReceiving()) {
|
||||
/* Channel busy — enforce retry timer so we don't hammer the check */
|
||||
if (!millisHasNowPassed(next_tx_time)) {
|
||||
if (_tx_queued_cb) {
|
||||
uint32_t remaining = next_tx_time - now;
|
||||
_tx_queued_cb(remaining + 1, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cad_busy_start == 0) {
|
||||
cad_busy_start = now;
|
||||
}
|
||||
if (now - cad_busy_start > getCADFailMaxDuration()) {
|
||||
_err_flags |= ERR_EVENT_CAD_TIMEOUT;
|
||||
LOG_ERR("checkSend: CAD timeout exceeded");
|
||||
} else {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
next_tx_time = futureMillis((int)retry);
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry + 1, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
cad_busy_start = 0;
|
||||
|
||||
outbound = _mgr->getNextOutbound(now);
|
||||
if (outbound) {
|
||||
/* Duty cycle enforcement — exempt admin packets */
|
||||
if (!isAdminPacket(outbound) && _duty_cycle.isExceeded(now)) {
|
||||
LOG_WRN("checkSend: duty cycle exceeded (%u/%u ms), re-queuing type=%d",
|
||||
_duty_cycle.window_airtime_ms, _duty_cycle.budgetMs(),
|
||||
outbound->getPayloadType());
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis(5000));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(5000, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint8_t raw[MAX_TRANS_UNIT];
|
||||
int len = 0;
|
||||
raw[len++] = outbound->header;
|
||||
if (outbound->hasTransportCodes()) {
|
||||
memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
|
||||
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
|
||||
}
|
||||
raw[len++] = outbound->path_len;
|
||||
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
|
||||
|
||||
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
|
||||
LOG_ERR("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT);
|
||||
_mgr->free(outbound);
|
||||
outbound = nullptr;
|
||||
} else {
|
||||
memcpy(&raw[len], outbound->payload, outbound->payload_len);
|
||||
len += outbound->payload_len;
|
||||
|
||||
uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2;
|
||||
outbound_start = now;
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
|
||||
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
|
||||
{
|
||||
uint8_t ptype = outbound->getPayloadType();
|
||||
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
|
||||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
|
||||
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n",
|
||||
getLogDateTime(), len, ptype,
|
||||
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len,
|
||||
(uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
|
||||
} else {
|
||||
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n",
|
||||
getLogDateTime(), len, ptype,
|
||||
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Final LBT check — close the gap between initial
|
||||
* isReceiving() and actual TX start (serialisation +
|
||||
* logging can take 1-5 ms). */
|
||||
if (_radio->isReceiving()) {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis((int)retry));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = _radio->startSendRaw(raw, len);
|
||||
if (!success) {
|
||||
uint32_t retry = getCADFailRetryDelay();
|
||||
LOG_ERR("checkSend: startSendRaw failed! re-queuing delay=%u", retry);
|
||||
logTxFail(outbound, outbound->getRawLength());
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis((int)retry));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(retry, _tx_queued_user_data);
|
||||
}
|
||||
} else {
|
||||
outbound_expiry = futureMillis((int)max_airtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Packet *Dispatcher::obtainNewPacket()
|
||||
{
|
||||
Packet *pkt = _mgr->allocNew();
|
||||
if (pkt == nullptr) {
|
||||
_err_flags |= ERR_EVENT_FULL;
|
||||
} else {
|
||||
pkt->payload_len = pkt->path_len = 0;
|
||||
pkt->_snr = 0;
|
||||
}
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void Dispatcher::releasePacket(Packet *packet)
|
||||
{
|
||||
_mgr->free(packet);
|
||||
}
|
||||
|
||||
void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis)
|
||||
{
|
||||
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
|
||||
LOG_ERR("sendPacket: rejected - path_len=%d or payload_len=%d invalid",
|
||||
packet->path_len, packet->payload_len);
|
||||
_mgr->free(packet);
|
||||
} else {
|
||||
_mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis));
|
||||
if (_tx_queued_cb && delay_millis > 0) {
|
||||
_tx_queued_cb(delay_millis, _tx_queued_user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const
|
||||
{
|
||||
return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0;
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::futureMillis(int millis_from_now) const
|
||||
{
|
||||
return (uint32_t)_ms->getMillis() + millis_from_now;
|
||||
}
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
@@ -276,22 +276,21 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time)
|
||||
#ifdef ZEPHCORE_LORA
|
||||
static mesh::ZephyrBoard zephyr_board;
|
||||
|
||||
/* Radio prefs — initialized with defaults in main(), updated after mesh.begin()
|
||||
* loads persisted prefs. Passed to radio adapter at static construction time. */
|
||||
static NodePrefs radio_prefs;
|
||||
/* Radio is constructed with no prefs pointer; main() binds it to
|
||||
* repeater_mesh._prefs via setPrefs() before repeater_mesh.begin(). */
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110)
|
||||
/* LR1110 via Zephyr LoRa driver */
|
||||
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
|
||||
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &radio_prefs);
|
||||
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board);
|
||||
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X)
|
||||
/* SX127x via Zephyr loramac-node driver */
|
||||
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
|
||||
static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board, &radio_prefs);
|
||||
static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board);
|
||||
#else
|
||||
/* SX126x via Zephyr LoRa driver */
|
||||
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
|
||||
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &radio_prefs);
|
||||
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board);
|
||||
#endif
|
||||
|
||||
static mesh::ZephyrMillisecondClock ms_clock;
|
||||
@@ -375,10 +374,6 @@ static void repeater_event_loop(void)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Initialize radio prefs with safe defaults before anything else */
|
||||
initNodePrefs(&radio_prefs);
|
||||
strcpy(radio_prefs.node_name, "Repeater");
|
||||
|
||||
#ifdef ZEPHCORE_LORA
|
||||
/* Clear any stale bootloader magic from previous sessions.
|
||||
* Prevents nRF52 boards from re-entering bootloader after reboot. */
|
||||
@@ -470,39 +465,18 @@ int main(void)
|
||||
self_identity.pub_key[4], self_identity.pub_key[5],
|
||||
self_identity.pub_key[6], self_identity.pub_key[7]);
|
||||
|
||||
/* Pre-load persisted prefs into radio_prefs BEFORE repeater_mesh.begin().
|
||||
*
|
||||
* Rationale: lora_radio was constructed at static-init time with a pointer
|
||||
* to radio_prefs (see line ~286). When repeater_mesh.begin() runs, it
|
||||
* calls Mesh::begin() -> Dispatcher::begin() -> _radio->begin(), which
|
||||
* reads freq/bw/sf/cr through that pointer to configure the hardware.
|
||||
*
|
||||
* Without this pre-load, the radio boots on the compile-time defaults
|
||||
* from initNodePrefs() (freq=869.618, EU ISM band) regardless of what the
|
||||
* user configured. RepeaterMesh::begin() then loads the persisted prefs
|
||||
* into its own _prefs member, so CLI/UI readback shows the correct saved
|
||||
* values — but the hardware is already configured on the stale defaults
|
||||
* and never gets reconfigured. The result: device appears operational on
|
||||
* the configured frequency but is physically tuned to 869.618 MHz, so
|
||||
* transmissions are not heard and no packets can be received.
|
||||
*
|
||||
* This went unnoticed in the EU because 869.618 happens to match the
|
||||
* default; US/CA users on 910.525 (and any other non-default freq) hit it.
|
||||
*
|
||||
* Mirrors the temp_prefs pattern in main_companion.cpp. The subsequent
|
||||
* setPrefs() rebind (after begin()) points the radio at the live prefs in
|
||||
* RepeaterMesh so CLI `set radio` changes take effect on reconfigure(). */
|
||||
data_store.loadPrefs(radio_prefs);
|
||||
|
||||
/* Start mesh with data store - this loads prefs, ACL, regions */
|
||||
repeater_mesh.begin(&data_store);
|
||||
|
||||
/* Rebind radio prefs pointer to the live prefs inside repeater_mesh.
|
||||
* radio_prefs above was a one-time copy for static init; from here on,
|
||||
* the radio must read from the same struct RepeaterMesh mutates so that
|
||||
* CLI-driven reconfigure() calls see current values. */
|
||||
/* Load persisted prefs and bind the radio to _prefs BEFORE begin() — the
|
||||
* radio reads freq/bw/sf/cr through this pointer during Mesh::begin() →
|
||||
* Dispatcher::begin() → Radio::begin(). Without this, the radio would
|
||||
* configure on NodePrefs defaults (869.618 MHz) regardless of saved
|
||||
* settings: CLI readback looked correct but the hardware stayed on EU.
|
||||
* Mirrors the temp_prefs pattern in main_companion.cpp. */
|
||||
data_store.loadPrefs(*repeater_mesh.getNodePrefs());
|
||||
lora_radio.setPrefs(repeater_mesh.getNodePrefs());
|
||||
|
||||
/* Start mesh with data store - loads ACL, regions */
|
||||
repeater_mesh.begin(&data_store);
|
||||
|
||||
/* Generate default node name from hardware device ID if not set */
|
||||
NodePrefs* prefs = repeater_mesh.getNodePrefs();
|
||||
if (strlen(prefs->node_name) == 0 || strcmp(prefs->node_name, "Repeater") == 0) {
|
||||
|
||||
Reference in New Issue
Block a user