Files
HaloKeymind/RESTORE_UPSTREAM_NOTES.md
T
agessaman 258ca46c0b docs(radio): record on-device validation results for duty-cycle restore
Duty-cycle throttling, CAD-under-load, capture decoupling, remote admin under
throttle, and radio-watchdog fire/recover all device-confirmed on a Heltec
V4.2 (busy mesh + off-frequency bench). Notes the watchdog observability
gotchas found during testing: silent on release builds (check stats-radio-diag
err_flags bit 8) and armed only after first radio activity.
2026-07-09 19:33:39 -07:00

6.0 KiB

Restoring accidentally-reverted upstream features

Background

On 2026-03-20, commit 22eb9b87Revert "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's updateTxBudget(), tx_budget_ms, duty_cycle_window_ms, getRemainingTxBudget(), getDutyCycleWindowMs(), and the windowed checkSend()/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} — restored getOutboundTotal() and the 0xFFFFFFFF count-all sentinel in countBefore().
  • src/helpers/StatsFormatHelper.h — restored the getOutboundTotal() call; kept the fork's formatRadioDiag template.

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):

  • Normal traffic still flows (repeater forwards, observer uplinks).
  • Sustained TX is throttled to the configured duty cycle: at set dutycycle 1 on a busy mesh, TX pinned to ~1% while MQTT capture continued at full rate (after the RxReservePacketManager fixes below). Remote admin remains usable under throttle with the priority-shed + stale-expiry policy.
  • CAD enabled under load: TX, capture, and CLI all normal.
  • The observer radio watchdog fires and recovers non-destructively: with radio.watchdog 1 on a silent frequency, err_flags bit 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) — check stats-radio-diag err_flags; the watchdog arms only after first radio activity (last_active > 0), so a radio wedged from boot is deliberately not covered.

Remaining before undraft: passive soak at fleet-default airtime_factor (duty convergence, flat heap, adverts still advertised).

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 (NOT done; needs care + device testing)

Still missing at HEAD, also dropped by 22eb9b87, still present upstream:

  • cad_enabled — hardware Channel Activity Detection (listen-before-talk) before TX. The Dispatcher/Radio interface (setCADEnabled/getCADEnabled) is restored by Phase 1, so CAD currently stays off by default (unchanged behavior). To make it configurable again, restore:
    • NodePrefs.cad_enabled field, its CLI get/set, and its persistence in CommonCLI.cpp (loadPrefsInt/savePrefs). Offset care: this branch's /com_prefs layout is carefully managed — add cad_enabled following the same append-and-size-guard pattern used for rx_boosted_gain/flood_max_*, and add a host-side round-trip test (see scratchpad/migtest).
    • The MyMesh::getCADEnabled() override returning _prefs.cad_enabled.
    • RadioLibWrappers::setCADEnabled() override so the hardware CAD is actually driven (the fork's wrapper currently doesn't override it).
  • radio_fem_rxgain — LoRa front-end-module RX gain. Restore NodePrefs.radio_fem_rxgain
    • CLI + persistence (same offset care), plus the per-board FEM wiring reverted across ~20 variants/*/target.cpp and the heltec_tracker_v2/LoRaFEMControl.{cpp,h} files. This is board-specific and only affects FEM-equipped hardware.

Phase 2 is lower urgency than duty-cycle enforcement (CAD/FEM are capability gaps, not a compliance regression) and is best done as its own change with per-board hardware testing.