5.7 KiB
Restoring accidentally-reverted upstream features
Historical recovery record, reconciled 2026-08-18. Phase 1 and Phase 2 described below have been implemented in the current tree. This file explains why the restoration occurred; use the CLI and build documentation for current behavior rather than treating the old “remaining” text as an active plan.
Background
On 2026-03-20, commit 22eb9b87 - Revert "Merge remote-tracking branch 'origin/dev' into mqtt-bridge-implementation" - reverted an entire upstream merge to escape a bad merge state, deleting 860 lines across 66 files. That was not intentional feature removal; it wholesale dropped a batch of upstream progress. When upstream was later re-merged, some collateral came back (MicroNMEA claim()/release(), the GAT562 board) but several upstream features were never reconciled and remained missing.
This branch restores them. They are pure upstream code the fork accidentally dropped, so restoring re-aligns the fork with upstream and reduces the future merge-conflict surface rather than adding to it.
Phase 1 - duty-cycle enforcement (done on this branch)
Restored the token-bucket duty-cycle enforcement and its cluster:
src/Dispatcher.{h,cpp}- restored upstream'supdateTxBudget(),tx_budget_ms,duty_cycle_window_ms,getRemainingTxBudget(),getDutyCycleWindowMs(), and the windowedcheckSend()/loop()budget logic. The fork's MQTT radio watchdog was re-applied on top as pure additions (behind#ifdef WITH_MQTT_BRIDGE).src/helpers/StaticPoolPacketManager.{h,cpp}- restoredgetOutboundTotal()and the0xFFFFFFFFcount-all sentinel incountBefore().src/helpers/StatsFormatHelper.h- restored thegetOutboundTotal()call; kept the fork'sformatRadioDiagtemplate.
Net effect: these files now diverge from upstream by watchdog additions only (0 deletions) instead of rewriting upstream's TX logic.
Why this is safe (no reinterpretation of stored settings)
getAirtimeBudgetFactor() changed meaning between fork and upstream, but the resulting
duty cycle is the same formula:
- Fork:
next_tx = t * factor-> duty ~1/(1+factor) - Upstream:
duty = 1/(1+factor)enforced over a rolling window
So a device's stored airtime_factor (a NodePrefs field, unchanged) keeps its meaning.
The only behavioral difference is upstream enforces it over a 1-hour window (allowing
short bursts) instead of rigid per-packet spacing - a strict improvement, and the
mechanism that keeps EU 868 MHz nodes under the legally-mandated duty cycle.
Must be validated on-device before merge
This touches core TX timing. Confirm on hardware - all device-confirmed 2026-07-09/10 on a Heltec V4.2 (busy live mesh + off-frequency bench):
- [OK] Normal traffic still flows (repeater forwards, observer uplinks).
- [OK] Sustained TX is throttled to the configured duty cycle: at
set dutycycle 1on a busy mesh, TX pinned to ~1% while MQTT capture continued at full rate (after theRxReservePacketManagerfixes below). Remote admin remains usable under throttle with the priority-shed + stale-expiry policy. - [OK] CAD enabled under load: TX, capture, and CLI all normal.
- [OK] The observer radio watchdog fires and recovers non-destructively: with
radio.watchdog 1on a silent frequency,err_flagsbit 8 set, radio remained in RX state through repeated recovery cycles, and packets were still received after. Note: firings are invisible on release builds (no MESH_DEBUG) - checkstats-radio-diagerr_flags; the watchdog arms only after first radio activity (last_active > 0), so a radio wedged from boot is deliberately not covered.
At the time of that review, the remaining undraft recommendation was a passive
soak at the fleet-default airtime_factor (duty convergence, flat heap, and
continued adverts). That sentence is retained as historical validation scope,
not as the current branch's merge state.
Known interaction: throttling starves MQTT capture (mitigated)
Found during the on-device duty-cycle load test (set dutycycle 1): queued
retransmissions hold static-pool packets with no expiry, so heavy throttling parks the
whole pool in the send queue. Dispatcher::checkRecv() then drops received packets
before logRx() runs, capping MQTT capture at the TX rate (each TX frees one packet
for one RX). Parked repeats also absorb every budget refill, starving the node's own
CLI responses - device-confirmed: a 1%-duty node on a busy mesh became
un-administrable within ~2 minutes. This is inherent upstream behavior - the old
fork's next_tx spacing had the same steady-state drain - but it defeats the
observer's purpose. Mitigated on observer builds by RxReservePacketManager
(src/helpers/RxReservePacketManager.h): priority-aware shedding below a pool
reserve (own responses/ACKs stay queueable) plus 30 s expiry of stale queued
outbound. See MQTT_INTERNALS.md "Capture vs. duty-cycle throttling".
Phase 2 - CAD and FEM RX gain (completed)
The current tree contains both restored capabilities:
cad_enabledis present in common preferences, load/save paths, and theget/set cadCLI. Repeater, room-server, sensor, and Companion integrations feed it toRadioLibWrapper::setCADEnabled(). Target-default builds default CAD off; the Cascade profile suppliesDEFAULT_CAD_ENABLED=1.radio_fem_rxgainis persisted and exposed asget/set radio.fem.rxgain. Supported boards implementsetLoRaFemLnaEnabled(); unsupported boards reject the operation rather than claiming a state change. Companion protocol v13 also exposes FEM RX-gain get and set commands.
See CLI commands and the command availability matrix for current syntax and build/hardware limits.