Hierarchical region gating via TX duty cycle (simple_repeater)#2960
Hierarchical region gating via TX duty cycle (simple_repeater)#2960khagele wants to merge 2 commits into
Conversation
Autonomously shed inter-region traffic when a repeater's own TX duty cycle is high, protecting the local cluster during high-traffic events (issue meshcore-dev#2747). Opt-in, off by default. When TX duty cycle exceeds a configurable threshold, regions are gated from the outermost layer inward: wildcard '*' first, then the broadest named regions, always keeping the innermost cluster. Recovery re-enables regions inside-out once duty cycle drops below (threshold - hysteresis), with per-step random jitter to desync recovery across repeaters. Design notes: - Gating is transient: a new non-persisted rt_flags overlay on RegionEntry is OR'd with config flags in the forward path, so a 'region save' or reboot mid-event can never make a deny permanent. - The gate level is re-asserted every check interval, so region edits or a bulk 'region load' commit (which rebuild entries with cleared rt_flags) cannot desync the gate state; disabling the feature while gated lifts the gate immediately. - Dispatcher::getTxDutyCyclePercent() refreshes the airtime budget before reading, so recovery still fires when traffic (and TX) stops. Config (persisted, appended for upgrade compat): set dc.gate <0|1>, set dc.gate.thresh <1-100>, set dc.gate.hyst <0-50> get dc.gate[.thresh|.hyst], get dc.gate.status (live duty% + level) Adds a native gtest suite for the gating logic (test/test_region_gating) and extends the native test mocks (Arduino/Stream/SHA256) so RegionMap builds off-device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| rm.applyDutyGate(0); | ||
| EXPECT_FALSE(gated(rm, "*")); | ||
| EXPECT_FALSE(gated(rm, "eu")); |
There was a problem hiding this comment.
/nitpick: should this not expect all four regions to not be gated and would splitting this test up into multiple tests make it more readable, especially for later debugging if the test ever fails? If this is according to the current coding guidelines, no problem of course. However a basic rule is: a test should contain one expect. Same can be said about the tests below.
There was a problem hiding this comment.
Good point, thanks - addressed in 13935ba. The suite is now one-scenario-per-test: each gate level, each recovery transition, and each invariant is its own test, and the baseline/level tests assert the state of every region rather than a subset. I kept a handful of expects per test where they jointly define a single behaviour (e.g. "level 2 gates * and eu, and nothing else"), since a strict one-expect split would obscure that. Failure output still points at the exact region and level that broke.
The home region is an explicit operator statement of which cluster this repeater serves, which the innermost-depth heuristic alone can't express (the home region may not be at the deepest level of the hierarchy). Never gate it, in addition to the always-protected innermost layer. Repeaters without a home region set keep the pure depth-based behaviour. Also restructure the gating tests into one-scenario-per-test cases with complete per-region assertions (review feedback), and add coverage for the home region exemption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 13935ba, which adds a home region exemption to the gating, following a question raised in the Dutch community discussion: should the home region or the innermost layer have the highest priority? Previously gating protected only the deepest layer of the hierarchy, so a repeater with home set to One trade-off worth discussing: if an operator sets home to a broad region (e.g. The same commit also restructures the unit tests into one-scenario-per-test cases with complete per-region assertions, and adds coverage for the exemption. |
But then the behaviour reverts to the current default behaviour (right?), which I would think is not a problem; it has no active negative effects, only a lack of positive effects. Not getting a bonus because you configured your machines badly is acceptable - there are far worse things that can happen if you screw up other settings. Nice work! Hope to see this in stable release soon. |
|
Can also be tested here: https://mcimages.weebl.me/?commitId=13935ba |
|
At this point, wouldn't it be simpler to implement what @onnozweers mentioned in #2747 (comment) ? |
Implements the proposal in #2747: autonomous, per-repeater gating that sheds inter-region traffic when a repeater's own TX duty cycle is high, so the local cluster keeps working during high-traffic/disruption events without needing admin access.
This is a reference implementation of the proposal, opened to move the discussion to concrete code. Credit to @fizzyfuzzle, whose POC in the issue thread this tracks and generalizes, and to the NL/CZ operators (@LennertVA, @DreitTheDragon, @onnozweers, @titusn) whose feedback shaped the design.
How it works
Reuses the existing
RegionMapparent hierarchy andDispatcherairtime budget. When TX duty cycle exceeds a threshold, regions are gated from the outermost layer inward — wildcard*first, then the broadest named regions — always protecting the innermost cluster. Recovery re-enables regions inside-out once duty cycle falls belowthreshold − hysteresis, with per-step random jitter to desync recovery across repeaters. Multiple regions on the same hierarchy level gate together; their leaves stay protected.Design / safety
rt_flagsfield onRegionEntryis OR'd with configflagsin the forward path, so aregion saveor reboot mid-event can never make a deny permanent.region loadcommit (which rebuild entries with clearedrt_flags) cannot desync the gate; disabling the feature while gated lifts the gate immediately.Dispatcher::getTxDutyCyclePercent()refreshes the airtime budget before reading, so recovery still fires when traffic (and thus TX) stops.Config (opt-in, off by default; persisted with upgrade-safe append)
set dc.gate <0|1>·set dc.gate.thresh <1-100>(default 70) ·set dc.gate.hyst <0-50>(default 10) ·get dc.gate[.thresh|.hyst]·get dc.gate.status(live duty % + gate level).Gate state is intentionally not added to the
regiontree dump for now — that format is parsed and round-tripped by deployed apps, so surfacing per-region gate state in "Manage Regions" is left as an open question. Live state is available viaget dc.gate.status.Verification
Heltec_E290_repeaterfirmware builds & links.test/test_region_gating/) covering wildcard-first ordering, leaf protection, inside-out recovery, config/runtime separation, same-level regions, and edge cases; native mocks (Arduino.h/Stream.h/SHA256.h) extended soRegionMapbuilds off-device.Open questions / out of scope
getDutyCycleWindowMs(), mirroringgetAirtimeBudgetFactor()).simple_repeater;simple_room_serverwould be a trivial follow-up.🤖 Generated with Claude Code