Add staggered transmit-only channel text beacons

This commit is contained in:
mikecarper committed 2026-10-14 09:58:24 -07:00
1 parent 8049754de9
commit 009bfff383
35 files changed
+3389 -19

No files matched your search

+37 -2
View File
@@ -47,6 +47,9 @@ static uint32_t scaleCADDelayForQueue(uint32_t normal_delay, int ready_count,
void Dispatcher::setRadioAvailable(bool available) {
if (radio_available == available) return;
#if MESH_ENABLE_TX_BEACONS
if (!available) cancelBeaconRadioFrame();
#endif
radio_available = available;
#if MESH_ENABLE_MESHTEXT_CHAT
if (!available && hasForeignRadioFrame()) {
@@ -56,6 +59,9 @@ void Dispatcher::setRadioAvailable(bool available) {
#endif
radio_nonrx_timer_armed = false;
if (!dispatcher_started || !radio_available) return;
#if MESH_ENABLE_TX_BEACONS
if (serviceBeaconRadioFrame()) return;
#endif
const unsigned long now = _ms->getMillis();
radio_nonrx_start = now;
@@ -73,6 +79,10 @@ void Dispatcher::setRadioAvailable(bool available) {
}
void Dispatcher::begin() {
#if MESH_ENABLE_TX_BEACONS
cancelBeaconRadioFrame();
if (hasBeaconRadioFrame()) return; // restoration owns the peripheral
#endif
#if MESH_ENABLE_MESHTEXT_CHAT
if (hasForeignRadioFrame()) {
if (foreign_radio_sending && radio_available) _radio->onSendFinished();
@@ -282,6 +292,9 @@ bool Dispatcher::isForeignRadioCurrent() const {
bool Dispatcher::queueForeignRadioFrame(const uint8_t* bytes, size_t length,
uint8_t profile, uint32_t generation) {
#if MESH_ENABLE_TX_BEACONS
if (hasBeaconRadioFrame()) return false;
#endif
const auto* profiles = _radio->profiles();
if (!bytes || !length || length > sizeof(foreign_radio_bytes) || hasForeignRadioFrame()
|| !radio_available || !_radio->supportsMeshtext() || _radio->isCarrierWaveActive()
@@ -465,11 +478,26 @@ bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
const uint32_t now = _ms->getMillis();
bool found = false;
uint32_t shortest_delay = 0;
#if MESH_ENABLE_TX_BEACONS
if (radio_available && hasBeaconRadioFrame()) {
// TX completion and restoration need the fast lifecycle, not timed sleep.
delay_millis = 0;
return true;
}
if (radio_available && beacon_wake_enabled) {
const int32_t wait = int32_t(beacon_wake_at - now);
shortest_delay = wait > 0 ? uint32_t(wait) : 0;
found = true;
}
#endif
// Timed MCU sleep must not skip the adaptive maintenance/retry deadline.
// Preserve the legacy queue-only query when power saving is off.
if (radio_available && useAdaptiveAGC()) {
found = agc_reset.nextDelay(now, shortest_delay);
if (found && !agc_reset.recoveryMatches(_radio->getRecoveryCount())) {
uint32_t agc_delay;
const bool agc_found = agc_reset.nextDelay(now, agc_delay);
if (agc_found && (!found || agc_delay < shortest_delay)) shortest_delay = agc_delay;
found = found || agc_found;
if (agc_found && !agc_reset.recoveryMatches(_radio->getRecoveryCount())) {
// A Dispatcher-owned recovery can occur after configure() this loop.
// Poll once before sleeping so the base cadence is restored promptly.
delay_millis = 0;
@@ -535,6 +563,10 @@ void Dispatcher::loop() {
return;
}
#if MESH_ENABLE_TX_BEACONS
// Restore before any calibration/scan/watchdog can observe a foreign tuple.
if (serviceBeaconRadioFrame()) return;
#endif
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
_radio->setCADEnabled(getCADEnabled());
@@ -805,6 +837,9 @@ void Dispatcher::loop() {
}
#endif
}
#if MESH_ENABLE_TX_BEACONS
if (checkBeaconRadioSend()) { releaseDroppedOutbound(); return; }
#endif
#if MESH_ENABLE_MESHTEXT_CHAT
if (!checkForeignRadioSend()) checkSend();
#else
+33
View File
@@ -9,6 +9,7 @@
#include <helpers/MorseTx.h>
#include <helpers/MorseRx.h>
#include <helpers/AGCResetScheduler.h>
#include <helpers/BeaconConfig.h>
#ifndef RADIO_LIVENESS_SOFT_ONLY
#include <helpers/RadioLivenessTracker.h>
#endif
@@ -91,6 +92,15 @@ public:
virtual const RadioProfiles* profiles() const { return nullptr; }
virtual bool validateProfile(const RadioProfileParams&) const { return false; }
virtual bool supportsMeshtext() const { return false; }
#if MESH_ENABLE_TX_BEACONS
virtual RadioParamApplyResult prepareBeaconTransmit(const RadioProfileParams&, int8_t) {
return RadioParamApplyResult::FAILED;
}
virtual RadioParamApplyResult restoreBeaconReceive() { return RadioParamApplyResult::APPLIED; }
virtual bool isBeaconRadioOwned() const { return false; }
virtual bool isBeaconChannelBusy() { return true; }
virtual uint32_t getBeaconAirtime(const RadioProfileParams&, int) { return 0; }
#endif
virtual uint8_t receiveProfile() const { return 0; }
virtual uint32_t receiveProfileGeneration() const {
return profiles() ? profiles()->generation[receiveProfile()] : 0;
@@ -553,6 +563,17 @@ protected:
public:
void begin();
void loop();
#if MESH_ENABLE_TX_BEACONS
~Dispatcher();
bool queueBeaconRadioFrame(const uint8_t* bytes, size_t length,
const RadioProfileParams& params, int8_t power);
void cancelBeaconRadioFrame();
bool hasBeaconRadioFrame() const { return beacon_tx != nullptr; }
void setBeaconWakeDelay(uint32_t now, uint32_t delay, bool enabled) {
beacon_wake_at = now + delay;
beacon_wake_enabled = enabled;
}
#endif
Radio* getProfileRadio() { return _radio; }
bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); }
@@ -573,6 +594,9 @@ public:
uint16_t getErrFlags() const { return _err_flags; } // Get error flags
bool hasOutbound() const {
return outbound != NULL
#if MESH_ENABLE_TX_BEACONS
|| hasBeaconRadioFrame()
#endif
#if MESH_ENABLE_MESHTEXT_CHAT
|| hasForeignRadioFrame()
#endif
@@ -595,6 +619,15 @@ private:
uint32_t profile_cad_busy[2] = {};
uint8_t receive_context_profile = 0;
bool receive_context_active = false;
#if MESH_ENABLE_TX_BEACONS
struct BeaconTransmit;
BeaconTransmit* beacon_tx = nullptr;
uint32_t beacon_wake_at = 0, beacon_last_start = 0;
bool beacon_wake_enabled = false, beacon_has_started = false;
bool serviceBeaconRadioFrame();
bool checkBeaconRadioSend();
void accountBeaconAirtime();
#endif
#if MESH_PACKET_LOGGING
void logPacketLine(const char* direction, const Packet* packet, int len,
bool include_rx_metrics, float score,
+145
View File
@@ -0,0 +1,145 @@
#include "Dispatcher.h"
#if MESH_ENABLE_TX_BEACONS
#include <new>
namespace mesh {
struct Dispatcher::BeaconTransmit {
enum Phase : uint8_t { Pending, Sending, Restoring } phase = Pending;
RadioProfileParams radio;
uint8_t bytes[MAX_TRANS_UNIT];
uint16_t length = 0;
int8_t power = 0;
uint32_t retry_at = 0, prepare_expiry = 0, start = 0, expiry = 0;
};
Dispatcher::~Dispatcher() { delete beacon_tx; }
bool Dispatcher::queueBeaconRadioFrame(const uint8_t* bytes, size_t length,
const RadioProfileParams& params, int8_t power) {
if (!bytes || !length || length > MAX_TRANS_UNIT || beacon_tx || outbound
|| !radio_available || _radio->isCarrierWaveActive()
|| _radio->isBeaconRadioOwned()
|| _radio->isMorseTxActive() || _radio->isMorseRxActive()
|| !RadioProfiles::valid(params) || !_radio->validateProfile(params)) return false;
#if MESH_ENABLE_MESHTEXT_CHAT
if (hasForeignRadioFrame()) return false;
#endif
const uint32_t now = _ms->getMillis();
// Admit a due slot before checking the actual RF-start guard below. CAD
// and tuning made the preceding start slightly later than its scheduled
// hour; rejecting here would silently drop the next hourly slot.
auto* frame = new (std::nothrow) BeaconTransmit;
if (!frame) return false;
frame->radio = params;
frame->power = power;
frame->length = uint16_t(length);
memcpy(frame->bytes, bytes, length);
frame->retry_at = now;
frame->prepare_expiry = now + 30000UL;
beacon_tx = frame;
return true;
}
void Dispatcher::accountBeaconAirtime() {
const uint32_t elapsed = uint32_t(_ms->getMillis()) - beacon_tx->start;
total_air_time += elapsed;
updateTxBudget();
tx_budget_ms = elapsed >= tx_budget_ms ? 0 : tx_budget_ms - elapsed;
if (tx_budget_ms < 100) {
const float duty = 1.0f / (1.0f + getAirtimeBudgetFactor());
next_tx_time = futureMillis((unsigned long)((100 - tx_budget_ms) / duty));
} else next_tx_time = _ms->getMillis();
}
void Dispatcher::cancelBeaconRadioFrame() {
if (!beacon_tx) return;
if (!radio_available) {
// Suspension never performs SPI against an unavailable peripheral.
if (!_radio->isBeaconRadioOwned()) { delete beacon_tx; beacon_tx = nullptr; }
return;
}
if (beacon_tx->phase == BeaconTransmit::Sending) {
_radio->onSendFinished();
accountBeaconAirtime();
}
beacon_tx->phase = BeaconTransmit::Restoring;
serviceBeaconRadioFrame();
}
bool Dispatcher::serviceBeaconRadioFrame() {
if (!beacon_tx || beacon_tx->phase == BeaconTransmit::Pending) return false;
if (beacon_tx->phase == BeaconTransmit::Sending) {
const bool complete = _radio->isSendComplete();
if (!complete && int32_t(uint32_t(_ms->getMillis()) - beacon_tx->expiry) < 0) return true;
_radio->onSendFinished();
accountBeaconAirtime();
beacon_tx->phase = BeaconTransmit::Restoring;
if (!complete) {
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
_radio->recoverRadio(true);
}
}
// Never let normal TX, AGC, or RX metadata processing use the beacon tuple.
if (_radio->restoreBeaconReceive() != RadioParamApplyResult::APPLIED) return true;
delete beacon_tx;
beacon_tx = nullptr;
radio_nonrx_start = _ms->getMillis();
return false;
}
bool Dispatcher::checkBeaconRadioSend() {
if (!beacon_tx || outbound || beacon_tx->phase != BeaconTransmit::Pending) return false;
const uint32_t now = _ms->getMillis();
if (int32_t(now - beacon_tx->prepare_expiry) >= 0) {
cancelBeaconRadioFrame();
return beacon_tx != nullptr;
}
if (int32_t(now - beacon_tx->retry_at) < 0) return false;
// Beacons are background work, never ahead of due MeshCore traffic.
if (_mgr->getOutboundCount(now) || !restoreOutboundTxOverrides()
|| _radio->isCarrierWaveActive() || _radio->isMorseTxActive()
|| _radio->isMorseRxActive() || _radio->isCalibratingNoiseFloor()) {
beacon_tx->retry_at = now + 101;
return false;
}
updateTxBudget();
const uint32_t airtime = _radio->getBeaconAirtime(beacon_tx->radio, beacon_tx->length);
if (!airtime || airtime > UINT32_MAX / 1000) {
cancelBeaconRadioFrame();
return beacon_tx != nullptr;
}
if (tx_budget_ms < airtime / 2 || !millisHasNowPassed(next_tx_time)
|| (beacon_has_started && uint32_t(now - beacon_last_start) < 3600000UL)) {
beacon_tx->retry_at = now + 101;
return false;
}
const auto prepared = _radio->prepareBeaconTransmit(beacon_tx->radio, beacon_tx->power);
if (prepared == RadioParamApplyResult::BUSY) {
if (_radio->isBeaconRadioOwned()) {
cancelBeaconRadioFrame();
return beacon_tx != nullptr;
}
// Coprime retry avoids repeatedly probing one RXPS sleep phase.
beacon_tx->retry_at = now + 11;
return false;
}
if (prepared != RadioParamApplyResult::APPLIED || _radio->isBeaconChannelBusy()) {
cancelBeaconRadioFrame();
return beacon_tx != nullptr;
}
// Count attempts, not delivery: no automatic retry after an RF start.
beacon_has_started = true;
beacon_last_start = beacon_tx->start = _ms->getMillis();
if (!_radio->startSendRaw(beacon_tx->bytes, beacon_tx->length)) {
cancelBeaconRadioFrame();
return beacon_tx != nullptr;
}
beacon_tx->phase = BeaconTransmit::Sending;
beacon_tx->expiry = beacon_tx->start + airtime * 3 / 2 + 1000;
return true;
}
} // namespace mesh
#endif
+7
View File
@@ -618,6 +618,13 @@ public:
void begin();
void loop();
#if MESH_ENABLE_TX_BEACONS
bool validateTextBeacon(const BeaconSlot& slot) const;
bool queueTextBeacon(const BeaconSlot& slot, const char* node_name,
uint32_t mt_from, uint32_t mt_id);
uint32_t beaconNodeId() const;
#endif
LocalIdentity self_id;
RNG* getRNG() const { return _rng; }
+63
View File
@@ -0,0 +1,63 @@
#include <Mesh.h>
#if MESH_ENABLE_TX_BEACONS
#include <helpers/BeaconFrames.h>
#include <helpers/radiolib/RadioPowerLimits.h>
#include <string.h>
namespace mesh {
bool Mesh::validateTextBeacon(const BeaconSlot& slot) const {
if (!beacon::validText(slot.text) || slot.hops > 7
|| !RadioProfiles::valid(slot.radio)
|| !RadioProfiles::safePreamble(slot.radio, slot.radio.preamble ? slot.radio.preamble : 32)
|| slot.power < -9 || !isLoRaTxPowerValid(slot.power, slot.radio.freq)
|| !_radio->validateProfile(slot.radio)) return false;
if (slot.radio.protocol == RadioProfileProtocol::MeshCore) return true;
#if MESH_ENABLE_MESHTEXT_WIRE
return slot.radio.protocol == RadioProfileProtocol::Meshtext
&& _radio->supportsMeshtext()
&& mt::presetName(static_cast<mt::Preset>(slot.preset)) != nullptr;
#else
return false;
#endif
}
bool Mesh::queueTextBeacon(const BeaconSlot& slot, const char* node_name,
uint32_t mt_from, uint32_t mt_id) {
if (!validateTextBeacon(slot)) return false;
uint8_t raw[MAX_TRANS_UNIT];
if (slot.radio.protocol == RadioProfileProtocol::Meshtext) {
const size_t length = beacon::meshtextFrame(static_cast<mt::Preset>(slot.preset),
mt_from, mt_id, slot.hops, node_name, slot.text, raw);
return length && queueBeaconRadioFrame(raw, length, slot.radio, slot.power);
}
uint8_t plain[beacon::MaxPublicPayloadBytes];
const size_t length = beacon::publicPayload(getRTCClock()->getCurrentTimeUnique(),
node_name, slot.text, plain, sizeof(plain));
if (!length) return false;
GroupChannel channel{};
memcpy(channel.secret, beacon::PublicKey, sizeof(beacon::PublicKey));
Utils::sha256(channel.hash, sizeof(channel.hash), channel.secret, sizeof(beacon::PublicKey));
Packet* packet = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, plain, length);
if (!packet) return false;
// Serialize one unscoped Public flood. Do not enter sendFlood(), which can
// schedule reliable retries or fan the packet out across receive profiles.
packet->header = (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
packet->setPathHashSizeAndCount(1, 0);
const size_t wire_length = packet->writeTo(raw);
const bool queued = queueBeaconRadioFrame(raw, wire_length, slot.radio, slot.power);
if (queued) getTables()->markSent(packet);
releasePacket(packet);
return queued;
}
uint32_t Mesh::beaconNodeId() const {
return beacon::nodeId(self_id.pub_key);
}
} // namespace mesh
#endif // MESH_ENABLE_TX_BEACONS
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "BeaconFeatureConfig.h"
#include <RadioProfiles.h>
namespace mesh {
struct BeaconSlot {
static constexpr unsigned TextCapacity = 81;
RadioProfileParams radio;
char text[TextCapacity] = {};
int8_t power = 0;
uint8_t preset = 0; // Meshtext preset; ignored for MeshCore Public.
uint8_t hops = 0;
bool enabled = false;
};
} // namespace mesh
+12
View File
@@ -0,0 +1,12 @@
#pragma once
// Fixed STM32 images retain their existing features and filesystem boundary.
// Host tests explicitly opt in when exercising the beacon implementation.
#ifndef MESH_ENABLE_TX_BEACONS
#if !defined(STM32_PLATFORM) && (defined(ESP32_PLATFORM) \
|| defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM))
#define MESH_ENABLE_TX_BEACONS 1
#else
#define MESH_ENABLE_TX_BEACONS 0
#endif
#endif
+96
View File
@@ -0,0 +1,96 @@
#include "BeaconFrames.h"
#if MESH_ENABLE_TX_BEACONS
#include "UTF8Helpers.h"
#include <Utils.h>
#include <string.h>
namespace mesh { namespace beacon {
const uint8_t PublicKey[16] = {
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
};
bool validText(const char* text) {
if (!text) return false;
const size_t length = strnlen(text, MaxTextBytes + 1);
if (!length || length > MaxTextBytes
|| validUtf8PrefixLength(text, length) != length) return false;
for (size_t i = 0; i < length; ++i) {
const uint8_t byte = static_cast<uint8_t>(text[i]);
if (byte < 32 || byte == 127) return false;
}
return true;
}
size_t formatText(const char* node_name, const char* text, char* out, size_t capacity) {
if (!node_name || !out || !validText(text)) return 0;
const size_t name_length = strnlen(node_name, MaxNameBytes + 1);
const size_t text_length = strlen(text);
if (!name_length || name_length > MaxNameBytes
|| validUtf8PrefixLength(node_name, name_length) != name_length
|| capacity < name_length + 2 + text_length + 1) return 0;
// The name and message use the normal MeshCore visible sender convention.
// A colon in a configured name must not create a second sender delimiter.
for (size_t i = 0; i < name_length; ++i) out[i] = node_name[i] == ':' ? ';' : node_name[i];
out[name_length] = ':';
out[name_length + 1] = ' ';
memcpy(out + name_length + 2, text, text_length);
const size_t length = name_length + 2 + text_length;
out[length] = 0;
return length;
}
size_t publicPayload(uint32_t timestamp, const char* node_name, const char* text,
uint8_t* out, size_t capacity) {
if (!out || capacity < 6) return 0;
char formatted[MaxFormattedBytes + 1];
const size_t length = formatText(node_name, text, formatted, sizeof(formatted));
if (!length || capacity < 5 + length) return 0;
for (unsigned i = 0; i < 4; ++i) out[i] = static_cast<uint8_t>(timestamp >> (8 * i));
out[4] = 0; // TXT_TYPE_PLAIN, no ACK/response requested.
memcpy(out + 5, formatted, length);
return 5 + length;
}
size_t meshtextFrame(mt::Preset preset, uint32_t from, uint32_t id, uint8_t hops,
const char* node_name, const char* text,
uint8_t out[mt::MaxFrameSize]) {
#if MESH_ENABLE_MESHTEXT_WIRE
const char* channel = mt::presetName(preset);
if (!channel || !out) return 0;
char formatted[MaxFormattedBytes + 1];
const size_t length = formatText(node_name, text, formatted, sizeof(formatted));
return length ? mt::encodeText(from, id, hops, channel, mt::DefaultPsk,
sizeof(mt::DefaultPsk), formatted, length, out) : 0;
#else
(void)preset; (void)from; (void)id; (void)hops;
(void)node_name; (void)text; (void)out;
return 0;
#endif
}
uint32_t nodeId(const uint8_t public_key[32]) {
if (!public_key) return 0;
static const uint8_t domain[] = {'b', 'e', 'a', 'c', 'o', 'n'};
uint8_t digest[4], chat_digest[4];
Utils::sha256(digest, sizeof(digest), public_key, 32, domain, sizeof(domain));
Utils::sha256(chat_digest, sizeof(chat_digest), public_key, 32);
const auto number = [](const uint8_t bytes[4]) {
return uint32_t(bytes[0]) | (uint32_t(bytes[1]) << 8)
| (uint32_t(bytes[2]) << 16) | (uint32_t(bytes[3]) << 24);
};
uint32_t value = number(digest), chat = number(chat_digest);
if (value < 4 || value == UINT32_MAX) value = 0x0badf00d;
if (chat < 4 || chat == UINT32_MAX) chat = 0x0badf00d;
// Domain separation avoids systematic reuse. Also cover a truncated-hash
// collision with the default Companion identity without changing its ID.
if (value == chat) value = value == UINT32_MAX - 1 ? 4 : value + 1;
return value;
}
} } // namespace mesh::beacon
#endif // MESH_ENABLE_TX_BEACONS
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "BeaconConfig.h"
#include "MeshtextChat.h"
#include <stddef.h>
#include <stdint.h>
namespace mesh { namespace beacon {
static constexpr size_t MaxNameBytes = 31;
static constexpr size_t MaxTextBytes = BeaconSlot::TextCapacity - 1;
static constexpr size_t MaxFormattedBytes = MaxNameBytes + 2 + MaxTextBytes;
static constexpr size_t MaxPublicPayloadBytes = 5 + MaxFormattedBytes;
extern const uint8_t PublicKey[16];
// Inputs must be NUL terminated within their respective bounded capacities.
// Reject malformed/overlong text instead of cutting a UTF-8 scalar in half.
bool validText(const char* text);
size_t formatText(const char* node_name, const char* text, char* out, size_t capacity);
size_t publicPayload(uint32_t timestamp, const char* node_name, const char* text,
uint8_t* out, size_t capacity);
size_t meshtextFrame(mt::Preset preset, uint32_t from, uint32_t id, uint8_t hops,
const char* node_name, const char* text,
uint8_t out[mt::MaxFrameSize]);
// Separate the automatic beacon sender from normal Companion channel chat.
// One persistent monotonic packet-ID allocator must serve this sender.
uint32_t nodeId(const uint8_t public_key[32]);
} } // namespace mesh::beacon
+587
View File
@@ -0,0 +1,587 @@
#include "BeaconManager.h"
#if MESH_ENABLE_TX_BEACONS
#include <Mesh.h>
#include <helpers/FilePresence.h>
#include <helpers/MeshtextChat.h>
#include <Arduino.h>
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <new>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
namespace mesh {
namespace {
constexpr size_t HeaderSize = 8, RecordSize = 98;
constexpr size_t ConfigSize = HeaderSize + BeaconManager::SlotCount * RecordSize + 4;
constexpr size_t IdSize = 16;
const char* const ConfigPath = "/beacons";
const char* const ConfigBackup = "/beacons.bak";
const char* const ConfigTemp = "/beacons.tmp";
const char* const IdPath = "/beacon_ids";
const char* const IdBackup = "/beacon_ids.bak";
const char* const IdTemp = "/beacon_ids.tmp";
uint32_t crcUpdate(uint32_t crc, const uint8_t* bytes, size_t size) {
while (size--) {
crc ^= *bytes++;
for (unsigned i = 0; i < 8; ++i)
crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320U : 0);
}
return crc;
}
void put32(uint8_t* bytes, uint32_t value) {
for (unsigned i = 0; i < 4; ++i) bytes[i] = uint8_t(value >> (8 * i));
}
uint32_t get32(const uint8_t* bytes) {
return uint32_t(bytes[0]) | (uint32_t(bytes[1]) << 8)
| (uint32_t(bytes[2]) << 16) | (uint32_t(bytes[3]) << 24);
}
bool validInterval(uint32_t hours) {
return hours == 12 || hours == 24 || hours == 36 || hours == 48;
}
void encodeSlot(const BeaconSlot& slot, uint8_t bytes[RecordSize]) {
memset(bytes, 0, RecordSize);
if (!slot.enabled) return;
static_assert(sizeof(float) == 4, "beacon images require 32-bit floats");
uint32_t bits;
memcpy(&bits, &slot.radio.freq, 4); put32(bytes, bits);
memcpy(&bits, &slot.radio.bw, 4); put32(bytes + 4, bits);
bytes[8] = uint8_t(slot.radio.preamble);
bytes[9] = uint8_t(slot.radio.preamble >> 8);
bytes[10] = slot.radio.sf; bytes[11] = slot.radio.cr;
bytes[12] = uint8_t(slot.radio.protocol); bytes[13] = uint8_t(slot.power);
bytes[14] = slot.preset; bytes[15] = slot.hops; bytes[16] = 1;
memcpy(bytes + 17, slot.text, BeaconSlot::TextCapacity);
}
bool decodeSlot(const uint8_t bytes[RecordSize], BeaconSlot& slot) {
slot = {};
if (bytes[16] == 0) {
for (size_t i = 0; i < RecordSize; ++i) if (bytes[i]) return false;
return true;
}
if (bytes[16] != 1 || bytes[12] > uint8_t(RadioProfileProtocol::Meshtext)
|| bytes[15] > 7 || !memchr(bytes + 17, 0, BeaconSlot::TextCapacity)
|| !bytes[17]) return false;
uint32_t bits = get32(bytes); memcpy(&slot.radio.freq, &bits, 4);
bits = get32(bytes + 4); memcpy(&slot.radio.bw, &bits, 4);
slot.radio.preamble = uint16_t(bytes[8]) | (uint16_t(bytes[9]) << 8);
slot.radio.sf = bytes[10]; slot.radio.cr = bytes[11];
slot.radio.protocol = RadioProfileProtocol(bytes[12]);
slot.power = int8_t(bytes[13]); slot.preset = bytes[14]; slot.hops = bytes[15];
slot.enabled = true;
memcpy(slot.text, bytes + 17, BeaconSlot::TextCapacity);
// Reject noncanonical trailing data instead of carrying hidden text bytes.
for (size_t i = strlen(slot.text) + 1; i < BeaconSlot::TextCapacity; ++i)
if (slot.text[i]) return false;
for (const unsigned char* c = reinterpret_cast<const unsigned char*>(slot.text); *c; ++c)
if (*c < 32 || *c == 127) return false;
return isfinite(slot.radio.freq) && isfinite(slot.radio.bw)
&& (slot.radio.protocol != RadioProfileProtocol::Meshtext
|| (slot.preset <= uint8_t(mt::Preset::LongSlow) && slot.radio.preamble == 16));
}
const char* skipSpace(const char* text) {
while (*text == ' ' || *text == '\t') ++text;
return text;
}
bool token(const char*& cursor, char* output, size_t size) {
cursor = skipSpace(cursor);
const char* begin = cursor;
while (*cursor && *cursor != ' ' && *cursor != '\t') ++cursor;
const size_t count = size_t(cursor - begin);
if (!count || count >= size) return false;
memcpy(output, begin, count); output[count] = 0;
cursor = skipSpace(cursor);
return true;
}
bool unsignedValue(const char* text, uint32_t& value) {
if (!*text) return false;
value = 0;
for (; *text; ++text) {
if (*text < '0' || *text > '9' || value > (UINT32_MAX - unsigned(*text - '0')) / 10)
return false;
value = value * 10 + unsigned(*text - '0');
}
return true;
}
bool powerValue(const char* text, int8_t& value) {
bool negative = *text == '-';
if (negative) ++text;
uint32_t parsed;
if (!unsignedValue(text, parsed) || parsed > (negative ? 128U : 127U)) return false;
value = int8_t(negative ? -int(parsed) : int(parsed));
return true;
}
bool floatValue(const char* text, float& value) {
if (!*text || *text == ' ' || *text == '\t') return false;
errno = 0;
char* end = nullptr;
value = strtof(text, &end);
return end && !*end && errno != ERANGE && isfinite(value);
}
bool meshcoreTuple(char* text, BeaconSlot& slot) {
char* fields[6];
unsigned count = 0;
do {
if (count == 6 || !*text) return false;
fields[count++] = text;
char* comma = strchr(text, ',');
if (!comma) break;
*comma = 0; text = comma + 1;
} while (true);
uint32_t sf, cr, preamble = 32;
if ((count != 5 && count != 6) || !floatValue(fields[0], slot.radio.freq)
|| !floatValue(fields[1], slot.radio.bw) || !unsignedValue(fields[2], sf)
|| !unsignedValue(fields[3], cr) || sf > 255 || cr > 255
|| !powerValue(fields[4], slot.power)
|| (count == 6 && !unsignedValue(fields[5], preamble))
|| preamble < 8 || preamble > RadioProfiles::MaxPreamble) return false;
slot.radio.sf = uint8_t(sf); slot.radio.cr = uint8_t(cr);
slot.radio.preamble = uint16_t(preamble);
return true;
}
bool textValue(const char* text, char* output) {
const size_t size = strlen(text);
if (!size || size >= BeaconSlot::TextCapacity) return false;
for (size_t i = 0; i < size; ++i) {
const unsigned char c = static_cast<unsigned char>(text[i]);
if (c < 32 || c == 127) return false;
}
memcpy(output, text, size + 1);
return true;
}
File makeFile(FILESYSTEM* fs) {
#if defined(NRF52_PLATFORM)
return File(*fs);
#else
(void)fs;
return File();
#endif
}
bool openRead(FILESYSTEM* fs, const char* path, File& file) {
#if defined(NRF52_PLATFORM)
(void)fs;
return file.open(path, FILE_O_READ);
#elif defined(STM32_PLATFORM)
file = fs->open(path, FILE_O_READ);
#else
file = fs->open(path, "r");
#endif
return bool(file);
}
bool openWrite(FILESYSTEM* fs, const char* path, File& file) {
bool present = false;
if (!filePresence(fs, path, present) || (present && !fs->remove(path))) return false;
#if defined(NRF52_PLATFORM)
return file.open(path, FILE_O_WRITE);
#elif defined(STM32_PLATFORM)
file = fs->open(path, FILE_O_WRITE);
#else
file = fs->open(path, "w");
#endif
return bool(file);
}
} // namespace
BeaconManager::~BeaconManager() { delete[] slots_; }
bool BeaconManager::allocate() {
if (!slots_) slots_ = new (std::nothrow) BeaconSlot[SlotCount];
return slots_ != nullptr;
}
bool BeaconManager::anyEnabled() const {
if (!slots_) return false;
for (unsigned i = 0; i < SlotCount; ++i) if (slots_[i].enabled) return true;
return false;
}
BeaconManager::ReadResult BeaconManager::readConfig(
const char* path, BeaconSlot* output, uint8_t& hours, bool& initialized) {
bool present = false;
if (!fs_ || !filePresence(fs_, path, present)) return ReadResult::Unreadable;
if (!present) return ReadResult::Missing;
File file = makeFile(fs_);
if (!openRead(fs_, path, file)) return ReadResult::Unreadable;
uint8_t header[HeaderSize];
if (file.size() < HeaderSize || file.read(header, HeaderSize) != int(HeaderSize)) {
file.close(); return ReadResult::Unreadable;
}
if (memcmp(header, "BCN!", 4) != 0) { file.close(); return ReadResult::Invalid; }
if (header[4] != 1) { file.close(); return ReadResult::Future; }
if (file.size() != ConfigSize || header[5] != SlotCount || header[6] > 1
|| !validInterval(header[7])) { file.close(); return ReadResult::Invalid; }
uint32_t crc = crcUpdate(0xffffffffU, header, HeaderSize);
bool valid = true;
for (unsigned i = 0; i < SlotCount; ++i) {
uint8_t bytes[RecordSize];
if (file.read(bytes, sizeof(bytes)) != int(sizeof(bytes))) {
file.close(); return ReadResult::Unreadable;
}
crc = crcUpdate(crc, bytes, sizeof(bytes));
BeaconSlot slot;
if (!decodeSlot(bytes, slot) || (slot.enabled && (!mesh_ || !mesh_->validateTextBeacon(slot)))
|| (slot.enabled && slot.radio.protocol == RadioProfileProtocol::Meshtext && !header[6]))
valid = false;
if (output) output[i] = slot;
}
uint8_t trailer[4];
const bool read = file.read(trailer, 4) == 4;
file.close();
if (!read) return ReadResult::Unreadable;
if (!valid || get32(trailer) != crc) return ReadResult::Invalid;
hours = header[7]; initialized = header[6] != 0;
return ReadResult::Valid;
}
BeaconManager::ReadResult BeaconManager::readIds(const char* path, uint32_t& last) {
bool present = false;
if (!fs_ || !filePresence(fs_, path, present)) return ReadResult::Unreadable;
if (!present) return ReadResult::Missing;
File file = makeFile(fs_);
if (!openRead(fs_, path, file)) return ReadResult::Unreadable;
uint8_t bytes[IdSize];
if (file.size() != IdSize) { file.close(); return ReadResult::Invalid; }
const bool read = file.read(bytes, sizeof(bytes)) == int(sizeof(bytes));
file.close();
if (!read) return ReadResult::Unreadable;
if (memcmp(bytes, "BCI!", 4)) return ReadResult::Invalid;
if (bytes[4] != 1) return ReadResult::Future;
if (bytes[5] || bytes[6] || bytes[7]
|| get32(bytes + 12) != crcUpdate(0xffffffffU, bytes, 12)) return ReadResult::Invalid;
last = get32(bytes + 8);
return ReadResult::Valid;
}
bool BeaconManager::commit(const char* path, const char* backup,
const char* temporary, bool& hold) {
bool active_present = false, backup_present = false;
if (!filePresence(fs_, path, active_present) || !filePresence(fs_, backup, backup_present)) return false;
if (active_present) {
if (backup_present && !fs_->remove(backup)) return false;
if (!fs_->rename(path, backup)) return false;
backup_present = true;
}
if (!fs_->rename(temporary, path)) {
if (backup_present && !fs_->rename(backup, path)) hold = true;
return false;
}
// A leftover backup after a successful commit is harmless. Never choose it
// over a valid active image at boot.
if (backup_present) fs_->remove(backup);
return true;
}
bool BeaconManager::saveConfig(unsigned changed_slot, const BeaconSlot* changed,
uint8_t hours, bool initialized, bool clear_all) {
if (!fs_ || hold_ || !validInterval(hours)) return false;
File file = makeFile(fs_);
if (!openWrite(fs_, ConfigTemp, file)) return false;
uint8_t header[HeaderSize] = {'B', 'C', 'N', '!', 1, SlotCount,
uint8_t(initialized), hours};
uint32_t crc = crcUpdate(0xffffffffU, header, sizeof(header));
bool ok = file.write(header, sizeof(header)) == sizeof(header);
for (unsigned i = 0; i < SlotCount && ok; ++i) {
BeaconSlot empty;
const BeaconSlot& slot = clear_all ? empty
: (changed && changed_slot == i ? *changed : (slots_ ? slots_[i] : empty));
uint8_t bytes[RecordSize]; encodeSlot(slot, bytes);
crc = crcUpdate(crc, bytes, sizeof(bytes));
ok = file.write(bytes, sizeof(bytes)) == sizeof(bytes);
}
uint8_t trailer[4]; put32(trailer, crc);
if (ok) ok = file.write(trailer, sizeof(trailer)) == sizeof(trailer);
file.flush(); file.close();
uint8_t verified_hours = 0; bool verified_initialized = false;
if (!ok || readConfig(ConfigTemp, nullptr, verified_hours, verified_initialized) != ReadResult::Valid
|| verified_hours != hours || verified_initialized != initialized) return false;
return commit(ConfigPath, ConfigBackup, ConfigTemp, hold_);
}
bool BeaconManager::saveIds(uint32_t last) {
if (!fs_ || ids_hold_) return false;
File file = makeFile(fs_);
if (!openWrite(fs_, IdTemp, file)) { ids_hold_ = true; return false; }
uint8_t bytes[IdSize] = {'B', 'C', 'I', '!', 1, 0, 0, 0};
put32(bytes + 8, last); put32(bytes + 12, crcUpdate(0xffffffffU, bytes, 12));
bool ok = file.write(bytes, sizeof(bytes)) == sizeof(bytes);
file.flush(); file.close();
uint32_t verified = 0;
ok = ok && readIds(IdTemp, verified) == ReadResult::Valid && verified == last;
if (ok) ok = commit(IdPath, IdBackup, IdTemp, ids_hold_);
if (!ok) { ids_hold_ = true; return false; }
last_id_ = last; ids_known_ = true;
return true;
}
bool BeaconManager::initializeIds() {
if (ids_hold_) return false;
if (ids_known_) return last_id_ != UINT32_MAX;
if (ids_initialized_) { ids_hold_ = true; return false; }
if (!mesh_ || !mesh_->getRNG()) return false;
uint32_t seed = 0;
mesh_->getRNG()->random(reinterpret_cast<uint8_t*>(&seed), sizeof(seed));
// A deliberate filesystem wipe can preserve the node identity. Start at
// a random point rather than deterministically reusing its old CTR nonce.
// Ordinary reboots always continue the committed high-water mark.
seed = (seed & 0x7fffffffU) | 1U;
return saveIds(seed);
}
bool BeaconManager::nextId(uint32_t& id) {
if (!ids_initialized_ || !initializeIds() || last_id_ == UINT32_MAX) return false;
const uint32_t next = last_id_ + 1;
if (!saveIds(next)) return false;
id = next;
return true;
}
void BeaconManager::begin(FILESYSTEM* fs, Mesh* mesh, RTCClock* rtc, const char* name) {
(void)rtc; // A wall-clock correction must never compress beacon spacing.
fs_ = fs; mesh_ = mesh; name_ = name;
delete[] slots_; slots_ = nullptr;
elapsed_ = anchor_ = handled_hour_ = last_send_ = 0;
memset(slot_last_send_, 0, sizeof(slot_last_send_));
memset(slot_not_before_, 0, sizeof(slot_not_before_));
last_now_ = millis(); interval_hours_ = 24; last_id_ = 0;
hold_ = ids_hold_ = ids_known_ = ids_initialized_ = false;
if (!fs_ || !mesh_) { hold_ = true; return; }
uint8_t hours = 24; bool initialized = false;
ReadResult result = readConfig(ConfigPath, nullptr, hours, initialized);
if (result == ReadResult::Future || result == ReadResult::Unreadable) hold_ = true;
else if (result != ReadResult::Valid) {
const ReadResult backup = readConfig(ConfigBackup, nullptr, hours, initialized);
if (backup == ReadResult::Valid) {
if ((fs_->exists(ConfigPath) && !fs_->remove(ConfigPath))
|| !fs_->rename(ConfigBackup, ConfigPath)) hold_ = true;
result = ReadResult::Valid;
} else if (result != ReadResult::Missing || backup != ReadResult::Missing) hold_ = true;
}
if (!hold_ && result == ReadResult::Valid) {
if (!allocate() || readConfig(ConfigPath, slots_, hours, initialized) != ReadResult::Valid) {
hold_ = true;
} else { interval_hours_ = hours; ids_initialized_ = initialized; }
}
if (hold_) { delete[] slots_; slots_ = nullptr; }
uint32_t last = 0;
result = readIds(IdPath, last);
if (result == ReadResult::Future || result == ReadResult::Unreadable) ids_hold_ = true;
else if (result != ReadResult::Valid) {
const ReadResult backup = readIds(IdBackup, last);
if (backup == ReadResult::Valid) {
// Missing active metadata does not prove an interrupted rename. A
// stale backup can survive a cleanup failure after a newer ID was
// transmitted. Never rewind to it, even before the first MT setting.
ids_hold_ = true;
} else if (result != ReadResult::Missing || backup != ReadResult::Missing) ids_hold_ = true;
}
if (!ids_hold_ && result == ReadResult::Valid) { last_id_ = last; ids_known_ = true; }
if (ids_initialized_ && !ids_known_) ids_hold_ = true;
loop(last_now_);
}
void BeaconManager::suspend() {
hold_ = true;
if (mesh_) {
mesh_->setBeaconWakeDelay(millis(), 0, false);
mesh_->cancelBeaconRadioFrame();
}
}
void BeaconManager::loop(uint32_t now) {
elapsed_ += uint32_t(now - last_now_); last_now_ = now;
if (!mesh_) return;
if (hold_ || !anyEnabled()) { mesh_->setBeaconWakeDelay(now, 0, false); return; }
const uint64_t spacing = uint64_t(interval_hours_ / SlotCount) * HourMillis;
const uint64_t period = uint64_t(interval_hours_) * HourMillis;
const uint64_t offset = elapsed_ - anchor_;
const uint64_t hour = offset / spacing;
uint64_t delay = spacing - offset % spacing;
if (hour > 0 && handled_hour_ != hour) {
const unsigned index = unsigned((hour - 1) % SlotCount);
BeaconSlot& slot = slots_[index];
if (!slot.enabled) handled_hour_ = hour;
else {
uint64_t guard = last_send_ && elapsed_ - last_send_ < HourMillis
? HourMillis - (elapsed_ - last_send_) : 0;
if (slot_not_before_[index] > elapsed_ && slot_not_before_[index] - elapsed_ > guard)
guard = slot_not_before_[index] - elapsed_;
if (slot_last_send_[index] && elapsed_ - slot_last_send_[index] < period) {
const uint64_t slot_guard = period - (elapsed_ - slot_last_send_[index]);
if (slot_guard > guard) guard = slot_guard;
}
if (!guard) {
// One admission attempt per slot window; never retry a failed TX.
handled_hour_ = hour;
uint32_t id = 0;
if ((slot.radio.protocol != RadioProfileProtocol::Meshtext || nextId(id))
&& mesh_->queueTextBeacon(slot, name_ ? name_ : "", mesh_->beaconNodeId(), id)) {
last_send_ = elapsed_; slot_last_send_[index] = elapsed_;
}
} else if (guard < delay) delay = guard;
else handled_hour_ = hour; // the old window expires before it is safe
}
}
// Empty slots do not need hourly wakeups. Find the next configured window.
if (handled_hour_ == hour || !hour) {
for (unsigned next = 1; next <= SlotCount; ++next) {
const uint64_t target = hour + next;
if (slots_[unsigned((target - 1) % SlotCount)].enabled) {
delay = target * spacing - offset; break;
}
}
}
mesh_->setBeaconWakeDelay(now, uint32_t(delay), true);
}
void BeaconManager::formatSlot(unsigned index, char* reply, size_t capacity) const {
if (!slots_ || !slots_[index].enabled) {
snprintf(reply, capacity, "> beacon %u off", index + 1); return;
}
const BeaconSlot& slot = slots_[index];
const char* protocol = slot.radio.protocol == RadioProfileProtocol::Meshtext ? "meshtext" : "meshcore";
snprintf(reply, capacity, "> %u %s %.4f,%g,%u,%u,%d,%u %s", index + 1, protocol,
double(slot.radio.freq), double(slot.radio.bw), slot.radio.sf, slot.radio.cr,
int(slot.power), slot.radio.preamble, slot.text);
}
bool BeaconManager::isCommand(const char* command) {
if (!command) return false;
const char* cursor = command;
char verb[8], setting[24];
return token(cursor, verb, sizeof(verb)) && token(cursor, setting, sizeof(setting))
&& (!strcmp(verb, "get") || !strcmp(verb, "set") || !strcmp(verb, "delete") || !strcmp(verb, "del"))
&& (!strcmp(setting, "beacon") || !strcmp(setting, "beacon.interval"));
}
bool BeaconManager::handle(const char* command, char* reply, size_t capacity) {
if (!reply || !capacity || !isCommand(command)) return false;
if (hold_) { snprintf(reply, capacity, "Err - beacon settings unavailable; storage held"); return true; }
const bool interval_get = !strcmp(command, "get beacon.interval");
const bool interval_set = !strncmp(command, "set beacon.interval ", 20);
if (interval_get || interval_set) {
if (interval_get) snprintf(reply, capacity, "> %u hours", interval_hours_);
else {
uint32_t hours;
if (!unsignedValue(command + 20, hours) || !validInterval(hours))
snprintf(reply, capacity, "Err - beacon interval must be 12, 24, 36, or 48 hours");
else if (!saveConfig(SlotCount, nullptr, uint8_t(hours), ids_initialized_))
snprintf(reply, capacity, "Err - beacon settings not saved");
else {
if (mesh_) mesh_->cancelBeaconRadioFrame();
const uint32_t now = millis();
elapsed_ += uint32_t(now - last_now_); last_now_ = now;
interval_hours_ = uint8_t(hours); anchor_ = elapsed_; handled_hour_ = 0;
snprintf(reply, capacity, "OK - beacon interval %lu hours", (unsigned long)hours);
loop(millis());
}
}
return true;
}
const bool get = !strcmp(command, "get beacon") || !strncmp(command, "get beacon ", 11);
const bool set = !strncmp(command, "set beacon ", 11);
const bool del = !strncmp(command, "delete beacon ", 14) || !strncmp(command, "del beacon ", 11);
if (!get && !set && !del) {
snprintf(reply, capacity, "Err - use get/set beacon or beacon.interval, delete beacon"); return true;
}
const char* cursor = command + (get && !strcmp(command, "get beacon") ? 10
: !strncmp(command, "delete ", 7) ? 14 : 11);
cursor = skipSpace(cursor);
if (get && (!*cursor || !strcmp(cursor, "all"))) {
size_t used = size_t(snprintf(reply, capacity, "> beacons %uh:", interval_hours_));
for (unsigned i = 0; i < SlotCount && used < capacity; ++i) {
if (slots_ && slots_[i].enabled) {
const int count = snprintf(reply + used, capacity - used, " %u", i + 1);
if (count > 0) used += size_t(count);
}
}
return true;
}
if (del && !strcmp(cursor, "all")) {
if (!saveConfig(SlotCount, nullptr, interval_hours_, ids_initialized_, true))
snprintf(reply, capacity, "Err - beacon settings not saved");
else {
if (mesh_) mesh_->cancelBeaconRadioFrame();
if (slots_) for (unsigned i = 0; i < SlotCount; ++i) slots_[i] = {};
snprintf(reply, capacity, "OK - all beacons deleted"); loop(millis());
}
return true;
}
char number[16]; uint32_t index;
if (!token(cursor, number, sizeof(number)) || !unsignedValue(number, index)
|| !index || index > SlotCount) {
snprintf(reply, capacity, "Err - beacon slot must be 1..12"); return true;
}
--index;
if (get || del) {
if (*cursor) snprintf(reply, capacity, "Err - unexpected beacon arguments");
else if (get) formatSlot(index, reply, capacity);
else {
BeaconSlot empty;
if (!saveConfig(index, &empty, interval_hours_, ids_initialized_))
snprintf(reply, capacity, "Err - beacon settings not saved");
else {
if (mesh_) mesh_->cancelBeaconRadioFrame();
if (slots_) slots_[index] = {};
snprintf(reply, capacity, "OK - beacon %lu deleted", (unsigned long)index + 1);
loop(millis());
}
}
return true;
}
BeaconSlot candidate;
char protocol[16], argument[96];
bool ok = token(cursor, protocol, sizeof(protocol));
if (ok && !strcmp(protocol, "meshcore")) {
ok = token(cursor, argument, sizeof(argument)) && meshcoreTuple(argument, candidate);
} else if (ok && !strcmp(protocol, "meshtext")) {
#if MESH_ENABLE_MESHTEXT_WIRE
char region_text[12], preset_text[24], power_text[16];
mt::Region region = mt::Region::Unset; mt::Preset preset = mt::Preset::LongFast;
ok = token(cursor, region_text, sizeof(region_text))
&& token(cursor, preset_text, sizeof(preset_text)) && token(cursor, power_text, sizeof(power_text));
if (ok) {
for (char* c = region_text; *c; ++c) *c = char(tolower(static_cast<unsigned char>(*c)));
if (!strcmp(region_text, "us")) region = mt::Region::US;
else if (!strcmp(region_text, "eu868")) region = mt::Region::EU868;
else if (!strcmp(region_text, "eu433")) region = mt::Region::EU433;
else if (!strcmp(region_text, "anz")) region = mt::Region::ANZ;
ok = mt::parsePreset(preset_text, preset) && mt::modemConfig(region, preset, candidate.radio)
&& powerValue(power_text, candidate.power);
candidate.preset = uint8_t(preset);
candidate.radio.protocol = RadioProfileProtocol::Meshtext;
candidate.radio.preamble = 16;
}
#else
snprintf(reply, capacity, "Err - Meshtext TX is unavailable on this modem"); return true;
#endif
} else ok = false;
candidate.enabled = true;
if (!ok || !textValue(cursor, candidate.text) || !mesh_
|| !mesh_->validateTextBeacon(candidate)) {
snprintf(reply, capacity, "Err - invalid beacon radio, power, preset, or text (1..80 bytes)"); return true;
}
bool initialized = ids_initialized_;
if (candidate.radio.protocol == RadioProfileProtocol::Meshtext) {
if (!initializeIds()) {
snprintf(reply, capacity, "Err - Meshtext beacon ID storage unavailable"); return true;
}
initialized = true;
}
if (!allocate() || !saveConfig(index, &candidate, interval_hours_, initialized)) {
snprintf(reply, capacity, "Err - beacon settings not saved"); return true;
}
if (mesh_) mesh_->cancelBeaconRadioFrame();
const uint32_t now = millis();
elapsed_ += uint32_t(now - last_now_); last_now_ = now;
slots_[index] = candidate; ids_initialized_ = initialized;
slot_not_before_[index] = elapsed_ + HourMillis;
snprintf(reply, capacity, "OK - beacon %lu saved; %uh cycle", (unsigned long)index + 1, interval_hours_);
loop(millis());
return true;
}
} // namespace mesh
#endif
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <helpers/BeaconConfig.h>
#if MESH_ENABLE_TX_BEACONS
#include <helpers/IdentityStore.h>
namespace mesh {
class Mesh;
class RTCClock;
// Configuration and a monotonic, best-effort schedule for TX-only beacons.
// The radio/profile transaction itself belongs to Mesh and Dispatcher.
class BeaconManager {
public:
static constexpr unsigned SlotCount = 12;
static constexpr uint32_t HourMillis = 3600000;
BeaconManager() = default;
~BeaconManager();
BeaconManager(const BeaconManager&) = delete;
BeaconManager& operator=(const BeaconManager&) = delete;
void begin(FILESYSTEM* fs, Mesh* mesh, RTCClock* rtc, const char* node_name);
void suspend();
void loop(uint32_t now);
static bool isCommand(const char* command);
bool handle(const char* command, char* reply, size_t capacity);
private:
enum class ReadResult : uint8_t { Missing, Valid, Invalid, Unreadable, Future };
FILESYSTEM* fs_ = nullptr;
Mesh* mesh_ = nullptr;
const char* name_ = nullptr;
BeaconSlot* slots_ = nullptr;
uint64_t elapsed_ = 0, anchor_ = 0, handled_hour_ = 0;
uint64_t last_send_ = 0, slot_last_send_[SlotCount] = {}, slot_not_before_[SlotCount] = {};
uint32_t last_now_ = 0, last_id_ = 0;
uint8_t interval_hours_ = 24;
bool hold_ = false, ids_hold_ = false, ids_known_ = false;
bool ids_initialized_ = false;
bool allocate();
bool anyEnabled() const;
ReadResult readConfig(const char* path, BeaconSlot* output,
uint8_t& hours, bool& ids_initialized);
ReadResult readIds(const char* path, uint32_t& last);
bool saveConfig(unsigned changed_slot, const BeaconSlot* changed,
uint8_t hours, bool ids_initialized, bool clear_all = false);
bool saveIds(uint32_t last);
bool commit(const char* path, const char* backup, const char* temporary,
bool& hold);
bool initializeIds();
bool nextId(uint32_t& id);
void formatSlot(unsigned index, char* reply, size_t capacity) const;
};
} // namespace mesh
#endif
+9
View File
@@ -175,6 +175,9 @@ void CommonCLI::loop() {
if (!mesh::morseTimingActive(_callbacks->getProfileRadio())) {
_radio_profiles.loop();
loopManagement();
#if MESH_ENABLE_TX_BEACONS
loopBeacons();
#endif
}
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
_user_gpio.loop();
@@ -2950,6 +2953,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
#endif
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
if (handleManagementCommand(command, reply)) return;
#if MESH_ENABLE_TX_BEACONS
if (handleBeaconCommand(command, reply, 157)) return;
#endif
if (mesh::wireless::control().handle(command, reply, 160, millis(),
_callbacks->wirelessCommandSource(sender_timestamp))) return;
// Infrastructure callers can already have reflected a three-byte CLI
@@ -3249,6 +3255,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else if (memcmp(command, "del ", 4) == 0) {
handleDelCmd(command, reply);
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
#if MESH_ENABLE_TX_BEACONS
suspendBeacons();
#endif
bool s = _callbacks->formatFileSystem();
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
} else if (memcmp(command, "ver", 3) == 0) {
+8
View File
@@ -806,6 +806,7 @@ class LegacyUpgradeGate;
namespace mesh {
class ManagementReporter;
struct DataRouteState;
class BeaconManager;
}
class CommonCLI {
mesh::ManagementReporter* _management = nullptr;
@@ -857,6 +858,13 @@ class CommonCLI {
bool _common_save_succeeded = false;
uint32_t _prefs_save_failures = 0;
mesh::RadioProfileCLI _radio_profiles;
#if MESH_ENABLE_TX_BEACONS
mesh::BeaconManager* _beacons = nullptr;
void beginBeacons();
void loopBeacons();
void suspendBeacons();
bool handleBeaconCommand(const char* command, char* reply, size_t capacity);
#endif
mesh::MorseReplyHandoff _morse_reply;
#if MESH_ENABLE_FLEET_CONTROL
mesh::FleetChannel* _fleet_channel = nullptr; // allocate only when enrolled
+36
View File
@@ -0,0 +1,36 @@
#include "CommonCLI.h"
#if MESH_ENABLE_TX_BEACONS
#include "BeaconManager.h"
#include <new>
void CommonCLI::beginBeacons() {
if (!_management_fs || !_management_mesh || _beacons) return;
if (!_management_fs->exists("/beacons") && !_management_fs->exists("/beacons.bak")) return;
_beacons = new (std::nothrow) mesh::BeaconManager;
if (_beacons) _beacons->begin(_management_fs, _management_mesh, _rtc, _prefs->node_name);
}
void CommonCLI::loopBeacons() {
if (_beacons) _beacons->loop(millis());
}
void CommonCLI::suspendBeacons() {
if (_beacons) _beacons->suspend();
if (_management_mesh) _management_mesh->cancelBeaconRadioFrame();
}
bool CommonCLI::handleBeaconCommand(const char* command, char* reply, size_t capacity) {
if (!mesh::BeaconManager::isCommand(command)) return false;
if (!_management_mesh || !_management_fs) {
snprintf(reply, capacity, "Error: beacon storage unavailable");
return true;
}
if (!_beacons) {
_beacons = new (std::nothrow) mesh::BeaconManager;
if (_beacons) _beacons->begin(_management_fs, _management_mesh, _rtc, _prefs->node_name);
}
if (!_beacons) { snprintf(reply, capacity, "Error: beacon allocation failed"); return true; }
return _beacons->handle(command, reply, capacity);
}
#endif
+3
View File
@@ -244,6 +244,9 @@ static bool resolveDataScope(DataRouteState& route, TransportKey& scope,
void CommonCLI::beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs) {
_management_mesh = &mesh; _management_fs = fs;
#if MESH_ENABLE_TX_BEACONS
beginBeacons();
#endif
_management_last_ms = millis(); _management_uptime_ms = _management_last_ms;
#if MESH_ENABLE_FLEET_CONTROL
if (_callbacks->supportsFleetControl()
+1 -1
View File
@@ -1,6 +1,6 @@
#include "MeshtextChat.h"
#if defined(MESH_ENABLE_MESHTEXT_CHAT) && MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
#include <AES.h>
#include <string.h>
+16
View File
@@ -1,5 +1,7 @@
#pragma once
#include "BeaconFeatureConfig.h"
// Resolve the chip in every translation unit, including Dispatcher.cpp which
// includes this before Arduino.h. Otherwise different class layouts could be
// selected for the same build depending on include order.
@@ -24,3 +26,17 @@
#define MESH_ENABLE_MESHTEXT_CHAT 0
#endif
#endif
// Compile the bounded wire codec uniformly for beacon-enabled builds. Some
// variants discover their radio chip only when Arduino includes the variant
// header; using USE_SX1262 here would select inconsistent class layouts and
// omit codec definitions in translation units that do not include Arduino.
// Concrete driver capability still gates Meshtext TX independently. This
// does not enable Companion chat, RX handling, or automatic radio2 management.
#ifndef MESH_ENABLE_MESHTEXT_WIRE
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_TX_BEACONS
#define MESH_ENABLE_MESHTEXT_WIRE 1
#else
#define MESH_ENABLE_MESHTEXT_WIRE 0
#endif
#endif
+12 -2
View File
@@ -41,7 +41,8 @@ public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsCarrierWave() const override { return true; }
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE && (MESH_ENABLE_MESHTEXT_CHAT \
|| (MESH_ENABLE_TX_BEACONS && !defined(SENSECAP_INDICATOR_LORA)))
bool supportsMeshtext() const override { return true; }
#endif
@@ -70,8 +71,11 @@ protected:
return restoreSX126xPacketRamp(static_cast<CustomSX1262*>(_radio)) == RADIOLIB_ERR_NONE
&& RadioLibWrapper::restoreCarrierWaveModem();
}
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
// Compiling a shared codec must not change unqualified MC-only modem
// behavior or require its foreign sync-word SPI path.
if (!supportsMeshtext()) return protocol == mesh::RadioProfileProtocol::MeshCore;
auto* radio = static_cast<CustomSX1262*>(_radio);
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
// Stage shared packet invariants only in a proven owned RX hop; normal
@@ -106,6 +110,9 @@ protected:
public:
bool setCodingRate(uint8_t cr) override {
#if MESH_ENABLE_TX_BEACONS
if (isBeaconRadioOwned()) return false;
#endif
if (isCarrierWaveActive()) return false;
return ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE;
}
@@ -143,6 +150,9 @@ public:
}
bool setRxPowerSavingRfRxDisabled(bool disabled) override {
#if MESH_ENABLE_TX_BEACONS
if (isBeaconRadioOwned()) return false;
#endif
if (isCarrierWaveActive()) return false;
if (!supportsRxPowerSavingRfRxDisable()) return false;
+178 -9
View File
@@ -168,6 +168,9 @@ bool RadioLibWrapper::claimRadioInterrupt() {
}
void RadioLibWrapper::begin() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return;
#endif
syncProfileClock(_profiles.enabled());
#ifdef USE_CC310_HW_CRYPTO
// Initialize CryptoCell once from normal task context. The session helper
@@ -235,6 +238,9 @@ uint32_t RadioLibWrapper::getRngSeed() {
}
bool RadioLibWrapper::setTxPower(int8_t dbm) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
// A CW power-meter sweep may re-key the carrier. During Morse that would
// turn an inter-symbol gap into RF and corrupt the text, so keep the whole
// message exclusive (including cleanup retries).
@@ -276,6 +282,9 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
}
uint8_t RadioLibWrapper::beginReconfigure() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned && !_beacon_restoring) return 2;
#endif
if (_cw_active) return 2;
const uint8_t base_state = state;
// On SX126x/LR11xx duty-cycle RX, BUSY may remain asserted during the sleep
@@ -305,6 +314,9 @@ void RadioLibWrapper::endReconfigure(bool resume_rx) {
}
bool RadioLibWrapper::restoreAfterDeepInit() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) cacheBeaconRestoreParams();
#endif
if (!radioDeepInit()) return false;
_rx_ps_armed = false;
@@ -318,7 +330,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() {
restored = _radio->setPreambleLength(currentPreambleLength())
== RADIOLIB_ERR_NONE;
}
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
restored = restored && applyProtocol(_physical_protocol);
#endif
if (_dbm_valid) {
@@ -346,7 +358,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
const uint8_t previous_sf = _cur_sf;
const uint8_t previous_cr = _cur_cr;
const uint16_t previous_preamble = _physical_preamble;
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
const auto previous_protocol = _physical_protocol;
#endif
const uint8_t previous_profile = _active_profile;
@@ -363,7 +375,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
_physical_preamble = profilePreamble(0);
bool success = applyParams(freq, bw, sf, cr);
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
success = success && applyProtocol(requested.protocol);
if (success) _physical_protocol = requested.protocol;
#endif
@@ -397,7 +409,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
_profile_generation = previous_generation;
_profile_refresh_required = previous_refresh;
_physical_preamble = previous_preamble;
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
_physical_protocol = previous_protocol;
#endif
_rx_ps_enabled = previous_rxps;
@@ -408,7 +420,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
bool restored = had_previous_params
&& _radio->standby() == RADIOLIB_ERR_NONE
&& applyParams(previous_freq, previous_bw, previous_sf, previous_cr);
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
restored = restored && applyProtocol(previous_protocol);
#endif
@@ -448,6 +460,9 @@ bool RadioLibWrapper::validateProfile(const mesh::RadioProfileParams& p) const {
}
mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned && !_beacon_restoring) return mesh::RadioParamApplyResult::BUSY;
#endif
if (_cw_active) return mesh::RadioParamApplyResult::BUSY;
if (profile > 1 || (profile == 1 && !_profiles.enabled())) return mesh::RadioParamApplyResult::FAILED;
const auto& p = _profiles.params(profile);
@@ -469,12 +484,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
const bool old_refresh = _profile_refresh_required;
const bool old_params_valid = _params_valid;
const uint16_t old_preamble = _physical_preamble;
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
const auto old_protocol = _physical_protocol;
#endif
_physical_preamble = preamble;
bool applied = applyParams(p.freq, p.bw, p.sf, p.cr);
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
// Changing only the protocol still changes the sync word. Conversely a
// failed write must not publish the new generation or cached wire format.
if (applied && (_physical_protocol != p.protocol || _profile_refresh_required)) {
@@ -504,12 +519,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
_profile_generation = old_generation;
_profile_refresh_required = old_refresh;
_physical_preamble = old_preamble;
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
_physical_protocol = old_protocol;
#endif
bool restored = old_params_valid && _radio->standby() == RADIOLIB_ERR_NONE
&& applyParams(old_freq, old_bw, old_sf, old_cr);
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
restored = restored && applyProtocol(old_protocol);
#endif
if (!restored) restored = restoreAfterDeepInit();
@@ -533,11 +548,142 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
return applied ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
#if MESH_ENABLE_TX_BEACONS
uint32_t RadioLibWrapper::getBeaconAirtime(const mesh::RadioProfileParams& p, int length) {
if (length <= 0 || length > MAX_TRANS_UNIT || !validateProfile(p)) return 0;
const uint16_t preamble = p.protocol == mesh::RadioProfileProtocol::Meshtext
? 16 : (p.preamble ? p.preamble : 32);
if (!mesh::RadioProfiles::safePreamble(p, preamble)) return 0;
const double symbol = mesh::RadioProfiles::symbolUs(p);
const int de = symbol >= 16000 ? 1 : 0;
const double coded = ceil((8.0 * length - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16)
/ (4 * (p.sf - 2 * de)));
const double payload = 8 + (coded > 0 ? coded * p.cr : 0);
return static_cast<uint32_t>(ceil((preamble + (p.sf <= 6 ? 6.25 : 4.25) + payload)
* symbol / 1000.0));
}
void RadioLibWrapper::cacheBeaconRestoreParams() {
const uint8_t profile = _beacon_restore_profile == 1 && _profiles.enabled() ? 1 : 0;
const auto& p = _profiles.params(profile);
// These remain NORMAL cached settings even while the physical modem is
// briefly elsewhere. A hard recovery must never resurrect a beacon modem.
cacheParams(p.freq, p.bw, p.sf, p.cr);
_active_profile = profile;
_profile_generation = _profiles.generation[profile];
_physical_preamble = profilePreamble(profile);
#if MESH_ENABLE_MESHTEXT_WIRE
_physical_protocol = p.protocol;
#endif
_profile_refresh_required = true;
}
mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(
const mesh::RadioProfileParams& p, int8_t power) {
using Result = mesh::RadioParamApplyResult;
if (_beacon_owned || _cw_active) return Result::BUSY;
if (!_params_valid || !_dbm_valid || !getBeaconAirtime(p, MAX_TRANS_UNIT)) return Result::FAILED;
if (isCalibratingNoiseFloor()) return Result::BUSY;
beginProfileRetune(isInRecvMode() && !_rx_ps_armed && !_rx_ps_enabled);
const uint8_t resume = beginReconfigure();
if (resume > 1) {
endProfileRetune(true);
return Result::BUSY;
}
// Admission consumed no pending receive. From this point every error keeps
// an exclusive lease until the ordinary receiver has been restored.
_beacon_restore_profile = _active_profile;
_beacon_owned = true;
_beacon_restore_at = 0;
_beacon_cad_timeout = cadScanTimeoutMillis(p.sf, p.bw);
_physical_preamble = p.protocol == mesh::RadioProfileProtocol::Meshtext
? 16 : (p.preamble ? p.preamble : 32);
bool applied = _radio->standby() == RADIOLIB_ERR_NONE
&& applyParams(p.freq, p.bw, p.sf, p.cr);
#if MESH_ENABLE_MESHTEXT_WIRE
applied = applied && applyProtocol(p.protocol);
if (applied) _physical_protocol = p.protocol;
#endif
applied = applied && applyCachedTxPower(power) == RADIOLIB_ERR_NONE;
endProfileRetune(applied);
_rx_hold_continuous = false;
state = STATE_IDLE;
// Do not startReceive: this is a TX-only excursion, not a third RX profile.
return applied ? Result::APPLIED : Result::FAILED;
}
bool RadioLibWrapper::isBeaconChannelBusy() {
if (!_beacon_owned || _beacon_restoring || state == STATE_TX_WAIT || isChipBusy()) return true;
const int16_t result = performChannelScanWithTimeout(_beacon_cad_timeout);
// CAD IRQs are not packets. Stop CAD even on failure before TX/restore, and
// never compare this frequency's RSSI with either normal profile's floor.
const bool stopped = _radio->standby() == RADIOLIB_ERR_NONE;
const bool cleared = _radio->clearIrqFlags(UINT32_MAX) == RADIOLIB_ERR_NONE;
claimRadioInterrupt();
state = STATE_IDLE;
return result != RADIOLIB_CHANNEL_FREE || !stopped || !cleared;
}
mesh::RadioParamApplyResult RadioLibWrapper::restoreBeaconReceive() {
using Result = mesh::RadioParamApplyResult;
if (!_beacon_owned) return Result::APPLIED;
if (state == STATE_TX_WAIT) return Result::BUSY;
const uint32_t now = millis();
if (_beacon_restore_at && static_cast<int32_t>(now - _beacon_restore_at) < 0) return Result::BUSY;
_beacon_restoring = true;
cacheBeaconRestoreParams();
const auto& p = _profiles.params(_active_profile);
bool restored = validateProfile(p) && !isChipBusy()
&& _radio->standby() == RADIOLIB_ERR_NONE;
beginProfileRetune(false);
restored = restored && applyParams(p.freq, p.bw, p.sf, p.cr);
#if MESH_ENABLE_MESHTEXT_WIRE
restored = restored && applyProtocol(p.protocol);
#endif
restored = restored && applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
endProfileRetune(restored);
if (restored) {
// No RX ran on the beacon tuple, so any remaining event belongs to its
// CAD/TX lifecycle. Retire it before the normal receiver is armed.
restored = _radio->clearIrqFlags(UINT32_MAX) == RADIOLIB_ERR_NONE;
}
if (!restored) {
restored = restoreAfterDeepInit()
&& _radio->clearIrqFlags(UINT32_MAX) == RADIOLIB_ERR_NONE;
}
claimRadioInterrupt();
if (restored) {
state = STATE_IDLE;
startRecv();
restored = isInRecvMode();
if (!restored && restoreAfterDeepInit()) {
state = STATE_IDLE;
startRecv();
restored = isInRecvMode();
}
}
_beacon_restoring = false;
if (!restored) {
_beacon_restore_at = now + 100;
if (!_beacon_restore_at) _beacon_restore_at = 1;
return Result::BUSY;
}
_profile_refresh_required = false;
_beacon_restore_at = 0;
_beacon_owned = false;
return Result::APPLIED;
}
#endif
mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile) {
return prepareTransmitProfile(profile, false);
}
mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool reply_rx_override) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return mesh::RadioParamApplyResult::BUSY;
#endif
if (!_profiles.canTransmit(profile, reply_rx_override)) return mesh::RadioParamApplyResult::FAILED;
if (_profiles.enabled() && (isChipBusy() || isPacketPendingOrReceiving())) {
return mesh::RadioParamApplyResult::BUSY;
@@ -727,6 +873,9 @@ void RadioLibWrapper::idle() {
}
bool RadioLibWrapper::setTrackerSleep(bool sleeping) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
if (_cw_active || (state & 0x0f) == STATE_TX_WAIT) return false;
if (sleeping && isPacketPendingOrReceiving()) return false;
if (_radio->standby() != RADIOLIB_ERR_NONE) return false;
@@ -825,6 +974,9 @@ bool RadioLibWrapper::tryResetAGC() {
}
bool RadioLibWrapper::resetAGCIfIdle() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
if (_cw_active) return false;
// make sure we're not mid-receiving and mid-sending of packet!
if (isPacketPendingOrReceiving() || (state == STATE_TX_WAIT)) return false;
@@ -845,6 +997,11 @@ bool RadioLibWrapper::resetAGCIfIdle() {
}
bool RadioLibWrapper::recoverRadio(bool hard) {
#if MESH_ENABLE_TX_BEACONS
// A failed startSendRaw() may request recovery. Defer that to the owner's
// restore transaction so a reset never reapplies the beacon wire settings.
if (_beacon_owned && !_beacon_restoring) return false;
#endif
if (_cw_active) return false;
const uint8_t base_state = state;
if (hasPendingRadioInterrupt() || base_state == STATE_TX_WAIT) return false;
@@ -1475,6 +1632,9 @@ bool RadioLibWrapper::serviceCarrierWave() {
}
void RadioLibWrapper::loop() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return;
#endif
syncProfileClock(_profiles.enabled());
if (serviceCarrierWave()) return;
if (_profiles.enabled()) {
@@ -1618,6 +1778,9 @@ void RadioLibWrapper::loop() {
}
void RadioLibWrapper::startRecv() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned && !_beacon_restoring) return;
#endif
if (_cw_active) return;
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
@@ -1687,6 +1850,9 @@ bool RadioLibWrapper::isInRecvMode() const {
// RX PowerSaving
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
if (_cw_active) return false;
if (_profile_rxps_suspended) {
_profile_saved_rxps = enabled;
@@ -1711,6 +1877,9 @@ bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sl
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return 0;
#endif
if (_cw_active) return 0;
int len = 0;
#if defined(USE_LR2021)
+19 -1
View File
@@ -89,7 +89,7 @@ protected:
mesh::RadioProfiles _profiles;
uint8_t _active_profile = 0;
uint16_t _physical_preamble = 0;
#if MESH_ENABLE_MESHTEXT_CHAT
#if MESH_ENABLE_MESHTEXT_WIRE
mesh::RadioProfileProtocol _physical_protocol = mesh::RadioProfileProtocol::MeshCore;
// Wire settings are a transaction with the modulation tuple. Unsupported
// wrappers decline rather than claiming a profile was applied.
@@ -104,6 +104,16 @@ protected:
bool _profile_rxps_suspended = false;
bool _profile_saved_rxps = false;
bool _profile_refresh_required = false;
#if MESH_ENABLE_TX_BEACONS
// The normal cache is never overwritten by a transmit-only excursion.
// Retain ownership through failed TX and restoration retries.
bool _beacon_owned = false;
bool _beacon_restoring = false;
uint8_t _beacon_restore_profile = 0;
uint32_t _beacon_restore_at = 0;
uint32_t _beacon_cad_timeout = 0;
void cacheBeaconRestoreParams();
#endif
bool _cw_active = false;
bool _cw_stopping = false;
bool _cw_board_tx = false;
@@ -374,6 +384,14 @@ public:
uint32_t receiveProfileGeneration() const override { return _profile_generation; }
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override;
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile, bool reply_rx_override) override;
#if MESH_ENABLE_TX_BEACONS
mesh::RadioParamApplyResult prepareBeaconTransmit(const mesh::RadioProfileParams& params,
int8_t power) override;
mesh::RadioParamApplyResult restoreBeaconReceive() override;
bool isBeaconChannelBusy() override;
uint32_t getBeaconAirtime(const mesh::RadioProfileParams& params, int length) override;
bool isBeaconRadioOwned() const override { return _beacon_owned; }
#endif
mesh::RadioParamApplyResult tryRestoreCodingRate(uint8_t cr) override;
mesh::RadioParamApplyResult trySetPrimaryParams(const mesh::RadioProfileParams& params,
bool temporary, const uint32_t* timings = nullptr) override;