#include "Dispatcher.h" #if MESH_ENABLE_TX_BEACONS #include 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_FOREIGN_RADIO 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 now = _ms->getMillis(); const uint32_t elapsed = now - beacon_tx->start; total_air_time += elapsed; // Match ordinary TX retirement without postponing adaptive maintenance. agc_reset.noteTransmit(now); 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 = now; } 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) < BEACON_START_GUARD_MS)) { 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