Add staggered transmit-only channel text beacons
This commit is contained in:
1 parent
8049754de9
commit
009bfff383
35 files changed
+3389
-19
No files matched your search
+37
-2
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user