Merge branch 'restore/upstream-duty-cycle' into restore/fem-rxgain

This commit is contained in:
agessaman committed 2026-07-09 17:54:45 -07:00
commit 1d52161332
5 files changed
+73 -3

No files matched your search

+14 -1
View File
@@ -35,10 +35,23 @@ The observer feature is kept out of upstream-tracked files through three mechani
Remaining integration points in upstream files:
- `examples/simple_repeater/MyMesh.{h,cpp}`, `examples/simple_room_server/MyMesh.{h,cpp}` -
bridge/alerter/SNMP wiring and packet-feed hooks, guarded by `#ifdef WITH_MQTT_BRIDGE`
bridge/alerter/SNMP wiring and packet-feed hooks, guarded by `#ifdef WITH_MQTT_BRIDGE`;
plus the `createObserverPacketManager()` call in each constructor (see below)
- `src/helpers/CommonCLI.{h,cpp}` - the three CLI hooks, `MQTTPrefs` load/save/migration
- `src/Dispatcher.{h,cpp}` - radio watchdog block, guarded by `#ifdef WITH_MQTT_BRIDGE`
### Capture vs. duty-cycle throttling
RX processing needs a free packet from the static pool before `logRx()` (and thus the
MQTT uplink) can run — `Dispatcher::checkRecv()` silently discards received data when
the pool is empty. Because the outbound queue holds pool packets with no expiry,
duty-cycle throttling can park the entire pool waiting on TX budget, capping capture at
the TX rate. Observer builds therefore use `RxReservePacketManager` (fork-owned,
`src/helpers/RxReservePacketManager.h`), which refuses to queue retransmissions once
the free pool drops below a reserve (a quarter of the pool) — the node sheds repeat
load it has no TX budget for, and capture continues at full rate. Non-observer builds
keep the upstream pool behavior.
### `/mqtt_prefs` file format
`/mqtt_prefs` is written with an 8-byte `MQTTPrefsHeader` (`magic`, `version`,
+12
View File
@@ -42,6 +42,18 @@ This touches core TX timing. Confirm on hardware:
verify `next_tx_time` spacing under load).
- The observer radio watchdog still recovers a stuck radio (`radio_watchdog_minutes`).
### 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). 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`):
retransmissions are shed once the free pool drops below a quarter of the pool, keeping
RX capture at full rate. 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:
+2 -1
View File
@@ -1,6 +1,7 @@
#include "MyMesh.h"
#include <algorithm>
#include <stdlib.h> // for qsort()
#include <helpers/RxReservePacketManager.h>
/* ------------------------------ Config -------------------------------- */
@@ -901,7 +902,7 @@ void MyMesh::sendNodeDiscoverReq() {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
: mesh::Mesh(radio, ms, rng, rtc, *createObserverPacketManager(32), tables),
region_map(key_store), temp_map(key_store),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
telemetry(MAX_PACKET_PAYLOAD - 4),
+2 -1
View File
@@ -1,4 +1,5 @@
#include "MyMesh.h"
#include <helpers/RxReservePacketManager.h>
#define REPLY_DELAY_MILLIS 1500
#define PUSH_NOTIFY_DELAY_MILLIS 2000
@@ -634,7 +635,7 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
: mesh::Mesh(radio, ms, rng, rtc, *createObserverPacketManager(32), tables),
region_map(key_store), temp_map(key_store),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
telemetry(MAX_PACKET_PAYLOAD - 4)
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <MeshCore.h>
#include <helpers/StaticPoolPacketManager.h>
// Fork-owned (not upstream-tracked). Observer builds capture every received packet
// to MQTT, but RX processing needs a free pool packet first — Dispatcher::checkRecv()
// discards the received bytes before logRx() when allocNew() fails. Under duty-cycle
// throttling the outbound queue can park the entire pool waiting on TX budget, which
// starves RX allocation and silently caps MQTT capture at the TX rate (each completed
// TX frees exactly one packet for exactly one more RX).
//
// This manager sheds *retransmissions* instead: once the free pool drops below the
// reserve, outbound packets are refused (freed straight back to the pool) so RX
// allocation — and therefore capture — continues at full rate. The node was already
// dropping traffic in that state; this chooses to drop repeats it has no TX budget
// for anyway, rather than capture.
class RxReservePacketManager : public StaticPoolPacketManager {
int _rx_reserve;
public:
RxReservePacketManager(int pool_size, int rx_reserve)
: StaticPoolPacketManager(pool_size), _rx_reserve(rx_reserve) {}
void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override {
if (getFreeCount() < _rx_reserve) {
MESH_DEBUG_PRINTLN("RxReservePacketManager: pool below RX reserve, shedding outbound");
free(packet);
return;
}
StaticPoolPacketManager::queueOutbound(packet, priority, scheduled_for);
}
};
// The packet manager for an app build: observer builds reserve a quarter of the pool
// for RX so MQTT capture survives duty-cycle throttling; non-observer builds keep the
// upstream pool behavior unchanged.
inline mesh::PacketManager* createObserverPacketManager(int pool_size) {
#ifdef WITH_MQTT_BRIDGE
return new RxReservePacketManager(pool_size, pool_size / 4);
#else
return new StaticPoolPacketManager(pool_size);
#endif
}