From f8637e14eb1c33ef8fc228ec61fd7a7ca3845d1e Mon Sep 17 00:00:00 2001 From: Elektr0Vodka <211697683+Elektr0Vodka@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:39:07 +0200 Subject: [PATCH] fix(cli): accept on/off for set dc.gate; clarify and dedupe dc.gate docs `set dc.gate` parsed its value with atoi(), so `set dc.gate on` evaluated to 0 and silently disabled region gating while replying OK. It now accepts on/off as well as 1/0 and rejects anything else with an error. Docs (cli_commands.md): dc.gate.thresh, dc.gate.hyst and the duty in `get dc.gate.status` are a percentage of the duty-cycle budget, not of wall-clock time. Worked example for dutycycle 10. Also removes the duplicated "Duty-cycle region gating" chapter left by an earlier merge, keeping the observer-only region_gate MQTT notes in the remaining copy. (cherry picked from commit b12c3fac) --- docs/cli_commands.md | 102 +++++++++++--------------------------- src/helpers/CommonCLI.cpp | 14 ++++-- 2 files changed, 40 insertions(+), 76 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 90dccabbb3..19d54e939a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -742,99 +742,55 @@ reports which of the two is active. #### Duty-cycle region gating -Autonomously sheds inter-region flood traffic when this repeater's own TX duty -cycle is high, protecting the local cluster during high-traffic or disruption +Autonomously sheds inter-region flood traffic when this repeater has used up most +of its duty-cycle budget, protecting the local cluster during high-traffic or disruption events — no admin access needed once configured. Opt-in and **off by default**. It reuses the existing [Region Management](#region-management-v110) hierarchy. -When the TX duty cycle rises above the threshold, regions are gated from the +When budget use rises above the threshold, regions are gated from the outermost layer inward — the wildcard `*` first, then the broadest named regions — always keeping the innermost cluster and the operator's home region open. As -the duty cycle recovers below `threshold − hysteresis`, regions re-open +budget use recovers below `threshold − hysteresis`, regions re-open inside-out, with a small random per-step delay so nearby repeaters don't all recover in lockstep. The gate is transient: it is never written to the region config, so a `region save` or a reboot mid-event can never make a deny permanent. **Usage:** -- `set dc.gate <0|1>` — enable (`1`) or disable (`0`) the feature +- `set dc.gate ` — enable or disable the feature (`1`/`0` work too) - `get dc.gate` — show whether it is on or off -- `set dc.gate.thresh <1-100>` — TX duty-cycle % above which gating starts +- `set dc.gate.thresh <1-100>` — % of the duty-cycle budget used above which gating starts - `get dc.gate.thresh` -- `set dc.gate.hyst <0-50>` — recovery margin %: regions re-open below `(threshold − hysteresis)` +- `set dc.gate.hyst <0-50>` — recovery margin in percentage points: regions re-open below `(threshold − hysteresis)` - `get dc.gate.hyst` -- `get dc.gate.status` — live TX duty cycle % and current gate level (`level/max`) +- `get dc.gate.status` — live % of the duty-cycle budget used, and current gate level (`level/max`) -**Defaults:** disabled; threshold `70`; hysteresis `10` (so recovery begins below 60%). +**Defaults:** off; threshold `70`; hysteresis `10` (so recovery begins below 60%). -**Examples:** -- `set dc.gate 1` — turn gating on -- `set dc.gate.thresh 80` — only start shedding above 80% duty cycle -- `set dc.gate.hyst 15` — re-open regions once duty cycle drops below 65% -- `get dc.gate.status` — e.g. `> duty 74%, gate level 2/4` - -**Notes:** -- The innermost (deepest) region layer and the configured home region are never gated. -- A repeater with no named regions (wildcard only) never gates — the wildcard *is* its local cluster. - -**How it interacts with the packet filter:** - -Region gating and the [packet filter](#packet-filter-repeater-only) are -complementary and run in a fixed order on each flood packet — region gating -first, the packet filter second: +**What the percentage measures:** -1. Region gating decides whether the packet's **region** may flood at all - (config deny **or** the transient duty-cycle gate). A gated region's packets - are dropped here. -2. Only packets that pass then reach the packet filter, which applies its - per-type hop/rate limits, soft cutoff and channel/source blocks. - -Because a region-gated packet is dropped *before* the filter sees it, the two -never double-count: region-gating drops do **not** appear in the `filter stats` -counters (watch the gate via `get dc.gate.status` instead). They also can't -conflict — both only ever *deny* forwarding, never re-enable it. +`dc.gate.thresh`, `dc.gate.hyst` and the `duty` in `get dc.gate.status` are a +percentage of this repeater's **duty-cycle budget** (set with +[`set dutycycle`](#view-or-change-the-duty-cycle-limit)), **not** a percentage of +wall-clock time. The budget is the airtime the duty-cycle limit allows per +hour, and it refills continuously. `duty 0%` means the full budget is +available; `duty 100%` means it is used up and the repeater has to hold back +transmissions until it refills. -They shed on different axes: region gating is coarse and load-adaptive -(*whose* traffic, driven by this repeater's own TX duty cycle), while the filter -is fine-grained and policy-driven (*what* traffic, by configured limits). Running -both is defense-in-depth and recommended; just note that both shed **flood** -traffic, so on a saturated repeater they stack — keep the filter's limits for -locally-relevant types generous if you rely on region gating as the first-line -congestion response. Region gating is off by default, so enabling it layers on -top of an existing filter configuration without disturbing it. Directed -(non-flood) traffic is never region-gated. - ---- - -#### Duty-cycle region gating - -Autonomously sheds inter-region flood traffic when this repeater's own TX duty -cycle is high, protecting the local cluster during high-traffic or disruption -events — no admin access needed once configured. Opt-in and **off by default**. - -It reuses the existing [Region Management](#region-management-v110) hierarchy. -When the TX duty cycle rises above the threshold, regions are gated from the -outermost layer inward — the wildcard `*` first, then the broadest named regions -— always keeping the innermost cluster and the operator's home region open. As -the duty cycle recovers below `threshold − hysteresis`, regions re-open -inside-out, with a small random per-step delay so nearby repeaters don't all -recover in lockstep. The gate is transient: it is never written to the region -config, so a `region save` or a reboot mid-event can never make a deny permanent. - -**Usage:** -- `set dc.gate <0|1>` — enable (`1`) or disable (`0`) the feature -- `get dc.gate` — show whether it is on or off -- `set dc.gate.thresh <1-100>` — TX duty-cycle % above which gating starts -- `get dc.gate.thresh` -- `set dc.gate.hyst <0-50>` — recovery margin %: regions re-open below `(threshold − hysteresis)` -- `get dc.gate.hyst` -- `get dc.gate.status` — live TX duty cycle % and current gate level (`level/max`) +For example, with `set dutycycle 10` the repeater may transmit for 360 seconds +per hour. With the default threshold of `70`, gating starts once 70% of that +budget is used (252 seconds, so less than 108 seconds left), and regions +start re-opening once usage drops below 60% (216 seconds). The threshold +therefore does **not** have to be set below the duty-cycle limit: `70` or `80` +is sensible with a 1%, 10% or any other limit. -**Defaults:** disabled; threshold `70`; hysteresis `10` (so recovery begins below 60%). +With no duty-cycle limit (`set dutycycle 100`, or `auto` outside the +863-870 MHz table) the budget is the whole hour, so the percentage then does +equal the share of wall-clock time spent transmitting. **Examples:** -- `set dc.gate 1` — turn gating on -- `set dc.gate.thresh 80` — only start shedding above 80% duty cycle -- `set dc.gate.hyst 15` — re-open regions once duty cycle drops below 65% +- `set dc.gate on` — turn gating on +- `set dc.gate.thresh 80` — only start shedding once 80% of the duty-cycle budget is used +- `set dc.gate.hyst 15` — re-open regions once usage drops below 65% of the budget - `get dc.gate.status` — e.g. `> duty 74%, gate level 2/4` **Notes:** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 99add90811..1ac390ffdb 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1585,9 +1585,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } } else if (memcmp(config, "dc.gate ", 8) == 0) { - _prefs->dc_gate_enabled = (atoi(&config[8]) != 0) ? 1 : 0; - savePrefs(); - strcpy(reply, "OK"); + // accept on/off as well as 1/0; anything else is rejected instead of silently disabling + const char* v = &config[8]; + bool on = strcmp(v, "on") == 0 || strcmp(v, "1") == 0; + bool off = strcmp(v, "off") == 0 || strcmp(v, "0") == 0; + if (on || off) { + _prefs->dc_gate_enabled = on ? 1 : 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "ERROR: dc.gate must be on/off (or 1/0)"); + } } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { int hours = _atoi(&config[22]); if ((hours > 0 && hours < 3) || (hours > 168)) {