From b12c3fac49cb3056fd7f890bfd562a44761e9ad6 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 dc.gate threshold 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 + dmc-docs nl/en/de): 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. --- dmc-docs/custom-cli.de.md | 15 ++++++++----- dmc-docs/custom-cli.en.md | 13 +++++++---- dmc-docs/custom-cli.md | 15 ++++++++----- docs/cli_commands.md | 45 ++++++++++++++++++++++++++++----------- src/helpers/CommonCLI.cpp | 14 +++++++++--- 5 files changed, 73 insertions(+), 29 deletions(-) diff --git a/dmc-docs/custom-cli.de.md b/dmc-docs/custom-cli.de.md index 49a5e87872..1f6b3b655d 100644 --- a/dmc-docs/custom-cli.de.md +++ b/dmc-docs/custom-cli.de.md @@ -38,16 +38,21 @@ ACL-Kontaktpakete werden übersprungen. Der Zustand wird in `/filter_prefs` gesp ## Duty-Cycle-Region-Gating -`dc.gate.*`. Standardmäßig **aus**. Wirft Inter-Regions-Flood-Verkehr ab, wenn der eigene -TX-Duty-Cycle des Repeaters hoch ist. Transient: wird nie in die Regionskonfiguration -geschrieben. Läuft vor dem Packet-Filter, daher erscheinen diese Drops nicht in -`filter stats`. +`dc.gate.*`. Standardmäßig **aus**. Wirft Inter-Regions-Flood-Verkehr ab, wenn der +Repeater den größten Teil seines Duty-Cycle-Budgets verbraucht hat. Transient: wird nie in +die Regionskonfiguration geschrieben. Läuft vor dem Packet-Filter, daher erscheinen diese +Drops nicht in `filter stats`. -- `set dc.gate <0|1>` / `get dc.gate` +- `set dc.gate ` / `get dc.gate` (`1`/`0` funktionieren auch) - `set dc.gate.thresh <1-100>` / `get dc.gate.thresh` (Standard 70) - `set dc.gate.hyst <0-50>` / `get dc.gate.hyst` (Standard 10) - `get dc.gate.status` (z. B. `duty 74%, gate level 2/4`) +Schwelle, Hysterese und `duty` sind ein Prozentsatz des **Duty-Cycle-Budgets** +(`set dutycycle`), nicht der Uhrzeit. Mit `dutycycle 10` darf der Repeater 360 Sekunden +pro Stunde senden; `dc.gate.thresh 70` beginnt mit dem Gating, sobald 252 dieser Sekunden +verbraucht sind. Die Schwelle muss also nicht unter dem Duty-Cycle-Limit liegen. + ## Duty-Cycle DMC ergänzte `auto` und änderte die Vorgabe von 50% auf `auto`. diff --git a/dmc-docs/custom-cli.en.md b/dmc-docs/custom-cli.en.md index 90c53148d1..85d88dc638 100644 --- a/dmc-docs/custom-cli.en.md +++ b/dmc-docs/custom-cli.en.md @@ -37,15 +37,20 @@ packets bypass it. State is persisted to `/filter_prefs`. ## Duty-cycle region gating -`dc.gate.*`. **Off** by default. Sheds inter-region flood traffic when the repeater's own -TX duty cycle is high. Transient: never written to the region config. Runs before the -packet filter, so these drops do not appear in `filter stats`. +`dc.gate.*`. **Off** by default. Sheds inter-region flood traffic when the repeater has +used up most of its duty-cycle budget. Transient: never written to the region config. Runs +before the packet filter, so these drops do not appear in `filter stats`. -- `set dc.gate <0|1>` / `get dc.gate` +- `set dc.gate ` / `get dc.gate` (`1`/`0` work too) - `set dc.gate.thresh <1-100>` / `get dc.gate.thresh` (default 70) - `set dc.gate.hyst <0-50>` / `get dc.gate.hyst` (default 10) - `get dc.gate.status` (e.g. `duty 74%, gate level 2/4`) +The threshold, hysteresis and `duty` are a percentage of the **duty-cycle budget** +(`set dutycycle`), not of wall-clock time. With `dutycycle 10` the repeater may transmit +for 360 seconds per hour; `dc.gate.thresh 70` starts gating once 252 of those seconds are +used. The threshold does not need to be below the duty-cycle limit. + ## Duty cycle DMC added `auto` and changed the default from 50% to `auto`. diff --git a/dmc-docs/custom-cli.md b/dmc-docs/custom-cli.md index c6796e664c..617a6f259b 100644 --- a/dmc-docs/custom-cli.md +++ b/dmc-docs/custom-cli.md @@ -38,16 +38,21 @@ ACL-contactpakketten worden overgeslagen. De status wordt bewaard in `/filter_pr ## Duty-cycle region gating -`dc.gate.*`. Standaard **uit**. Werpt inter-regio-floodverkeer af wanneer de eigen -TX-duty-cycle van de repeater hoog is. Transient: wordt nooit naar de regioconfiguratie -weggeschreven. Draait vóór de packet filter, dus deze drops verschijnen niet in -`filter stats`. +`dc.gate.*`. Standaard **uit**. Werpt inter-regio-floodverkeer af wanneer de repeater +het grootste deel van zijn duty-cycle-budget heeft verbruikt. Transient: wordt nooit naar +de regioconfiguratie weggeschreven. Draait vóór de packet filter, dus deze drops +verschijnen niet in `filter stats`. -- `set dc.gate <0|1>` / `get dc.gate` +- `set dc.gate ` / `get dc.gate` (`1`/`0` werkt ook) - `set dc.gate.thresh <1-100>` / `get dc.gate.thresh` (standaard 70) - `set dc.gate.hyst <0-50>` / `get dc.gate.hyst` (standaard 10) - `get dc.gate.status` (bijv. `duty 74%, gate level 2/4`) +De drempel, de hysterese en `duty` zijn een percentage van het **duty-cycle-budget** +(`set dutycycle`), niet van de kloktijd. Met `dutycycle 10` mag de repeater 360 seconden +per uur zenden; `dc.gate.thresh 70` start de gating zodra 252 van die seconden zijn +verbruikt. De drempel hoeft dus niet onder de duty-cycle-limiet te liggen. + ## Duty cycle DMC voegde `auto` toe en veranderde de standaard van 50% naar `auto`. diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 57e00b4e37..0294a2d35c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -725,34 +725,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%). + +**What the percentage measures:** + +`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. + +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. + +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 5bd105b078..9e0d9fd957 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -488,9 +488,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)) {