From f6338430f8bc6a2f1b057b8e38891fad9f830d9f Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 7 Mar 2026 14:55:14 +0100 Subject: [PATCH 1/7] Add get/set dutycycle command We translate to af internally, it's easier to store and doesn't break stored prefs. Made get/set af command show deprecated, but it still works fine. --- docs/cli_commands.md | 17 ++++++++++++----- docs/terminal_chat_cli.md | 6 ++++-- src/helpers/CommonCLI.cpp | 28 ++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1d3430db..0e170a77 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -419,15 +419,22 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the airtime factor (duty cycle limit) +#### View or change the duty cycle limit **Usage:** -- `get af` -- `set af ` +- `get dutycycle` +- `set dutycycle ` **Parameters:** -- `value`: Airtime factor (0-9) +- `value`: Duty cycle percentage (10-100) -**Default:** `1.0` +**Default:** `50%` (equivalent to airtime factor 1.0) + +**Examples:** +- `set dutycycle 100` — no duty cycle limit +- `set dutycycle 50` — 50% duty cycle (default) +- `set dutycycle 10` — 10% duty cycle (strictest EU requirement) + +> **Deprecated:** `get af` / `set af` still work but are deprecated in favour of `dutycycle`. --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index f053e64d..b1a3af2a 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -28,9 +28,11 @@ set lon {longitude} Sets your advertisement map longitude. (decimal degrees) ``` -set af {air-time-factor} +set dutycycle {percent} ``` -Sets the transmit air-time-factor. +Sets the transmit duty cycle limit (10-100%). Example: `set dutycycle 10` for 10%. + +> **Deprecated:** `set af` still works but is deprecated in favour of `set dutycycle`. ``` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index f3cba406..963eb5d9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -283,8 +283,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); + if (memcmp(config, "dutycycle", 8) == 0) { + float dc = 100.0f / (_prefs->airtime_factor + 1.0f); + int dc_int = (int)dc; + int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); + sprintf(reply, "> %d.%d%%", dc_int, dc_frac); + } else if (memcmp(config, "af", 2) == 0) { + sprintf(reply, "> %s (deprecated, use 'get dutycycle')", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { @@ -436,10 +441,25 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "af ", 3) == 0) { + if (memcmp(config, "dutycycle ", 9) == 0) { + float dc = atof(&config[9]); + if (dc < 10 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 10-100"); + } else { + _prefs->airtime_factor = (100.0f / dc) - 1.0f; + savePrefs(); + float actual = 100.0f / (_prefs->airtime_factor + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + } + } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); - strcpy(reply, "OK"); + float actual = 100.0f / (_prefs->airtime_factor + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%% (deprecated, use 'set dutycycle')", a_int, a_frac); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); From 3c0d1865691abd7c4aeb329b71fd3047e2f5369b Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 11 Mar 2026 20:08:47 +0100 Subject: [PATCH 2/7] Fix memcp compare length off by one Co-authored-by: ViezeVingertjes --- src/helpers/CommonCLI.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 963eb5d9..b3eff17f 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -283,7 +283,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "dutycycle", 8) == 0) { + if (memcmp(config, "dutycycle", 9) == 0) { float dc = 100.0f / (_prefs->airtime_factor + 1.0f); int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); @@ -441,8 +441,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "dutycycle ", 9) == 0) { - float dc = atof(&config[9]); + if (memcmp(config, "dutycycle ", 10) == 0) { + float dc = atof(&config[10]); if (dc < 10 || dc > 100) { strcpy(reply, "ERROR: dutycycle must be 10-100"); } else { From 1f48d2b8698c3702baf2b548bc8e987bdfe20b17 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 18 Mar 2026 22:06:23 +0100 Subject: [PATCH 3/7] Address comments --- docs/cli_commands.md | 21 ++++++++++++++++++--- docs/terminal_chat_cli.md | 7 +++++-- src/helpers/CommonCLI.cpp | 11 ++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 0e170a77..f248e4f3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -425,16 +425,31 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set dutycycle ` **Parameters:** -- `value`: Duty cycle percentage (10-100) +- `value`: Duty cycle percentage (1-100) **Default:** `50%` (equivalent to airtime factor 1.0) **Examples:** - `set dutycycle 100` — no duty cycle limit - `set dutycycle 50` — 50% duty cycle (default) -- `set dutycycle 10` — 10% duty cycle (strictest EU requirement) +- `set dutycycle 10` — 10% duty cycle +- `set dutycycle 1` — 1% duty cycle (strictest EU requirement) -> **Deprecated:** `get af` / `set af` still work but are deprecated in favour of `dutycycle`. +> **Note:** Added in firmware v1.15.0 + +--- + +#### View or change the airtime factor (duty cycle limit) +> **Deprecated** as of firmware v1.15.0. Use [`get/set dutycycle`](#view-or-change-the-duty-cycle-limit) instead. + +**Usage:** +- `get af` +- `set af ` + +**Parameters:** +- `value`: Airtime factor (0-9) + +**Default:** `1.0` --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index b1a3af2a..c889da96 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -30,9 +30,12 @@ Sets your advertisement map longitude. (decimal degrees) ``` set dutycycle {percent} ``` -Sets the transmit duty cycle limit (10-100%). Example: `set dutycycle 10` for 10%. +Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. -> **Deprecated:** `set af` still works but is deprecated in favour of `set dutycycle`. +``` +set af {air-time-factor} +``` +Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead. ``` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b3eff17f..cf6d4712 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -289,7 +289,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); sprintf(reply, "> %d.%d%%", dc_int, dc_frac); } else if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s (deprecated, use 'get dutycycle')", StrHelper::ftoa(_prefs->airtime_factor)); + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { @@ -443,8 +443,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch const char* config = &command[4]; if (memcmp(config, "dutycycle ", 10) == 0) { float dc = atof(&config[10]); - if (dc < 10 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 10-100"); + if (dc < 1 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { _prefs->airtime_factor = (100.0f / dc) - 1.0f; savePrefs(); @@ -456,10 +456,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%% (deprecated, use 'set dutycycle')", a_int, a_frac); + strcpy(reply, "OK"); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); From 0aa0ec1f164bd659f752443f1267d9bb621b3fda Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 7 Mar 2026 14:55:14 +0100 Subject: [PATCH 4/7] Add get/set dutycycle command We translate to af internally, it's easier to store and doesn't break stored prefs. Made get/set af command show deprecated, but it still works fine. --- docs/cli_commands.md | 22 ++++++++++++---------- docs/terminal_chat_cli.md | 6 ++++-- src/helpers/CommonCLI.cpp | 28 ++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9769d713..7f1c93c7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -500,20 +500,22 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the airtime factor (duty cycle limit) +#### View or change the duty cycle limit **Usage:** -- `get af` -- `set af ` +- `get dutycycle` +- `set dutycycle ` **Parameters:** -- `value`: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: - - `af = 1` → ~50% duty - - `af = 2` → ~33% duty - - `af = 3` → ~25% duty - - `af = 9` → ~10% duty - Yyou are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation). +- `value`: Duty cycle percentage (10-100) -**Default:** `1.0` +**Default:** `50%` (equivalent to airtime factor 1.0) + +**Examples:** +- `set dutycycle 100` — no duty cycle limit +- `set dutycycle 50` — 50% duty cycle (default) +- `set dutycycle 10` — 10% duty cycle (strictest EU requirement) + +> **Deprecated:** `get af` / `set af` still work but are deprecated in favour of `dutycycle`. --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index f053e64d..b1a3af2a 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -28,9 +28,11 @@ set lon {longitude} Sets your advertisement map longitude. (decimal degrees) ``` -set af {air-time-factor} +set dutycycle {percent} ``` -Sets the transmit air-time-factor. +Sets the transmit duty cycle limit (10-100%). Example: `set dutycycle 10` for 10%. + +> **Deprecated:** `set af` still works but is deprecated in favour of `set dutycycle`. ``` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 8b097c29..b346e3ea 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -294,8 +294,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); + if (memcmp(config, "dutycycle", 8) == 0) { + float dc = 100.0f / (_prefs->airtime_factor + 1.0f); + int dc_int = (int)dc; + int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); + sprintf(reply, "> %d.%d%%", dc_int, dc_frac); + } else if (memcmp(config, "af", 2) == 0) { + sprintf(reply, "> %s (deprecated, use 'get dutycycle')", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { @@ -451,10 +456,25 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "af ", 3) == 0) { + if (memcmp(config, "dutycycle ", 9) == 0) { + float dc = atof(&config[9]); + if (dc < 10 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 10-100"); + } else { + _prefs->airtime_factor = (100.0f / dc) - 1.0f; + savePrefs(); + float actual = 100.0f / (_prefs->airtime_factor + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + } + } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); - strcpy(reply, "OK"); + float actual = 100.0f / (_prefs->airtime_factor + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%% (deprecated, use 'set dutycycle')", a_int, a_frac); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); From 741392889da728292c8aa3de3c449f742a059f86 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 11 Mar 2026 20:08:47 +0100 Subject: [PATCH 5/7] Fix memcp compare length off by one Co-authored-by: ViezeVingertjes --- src/helpers/CommonCLI.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b346e3ea..67a59b29 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -294,7 +294,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "dutycycle", 8) == 0) { + if (memcmp(config, "dutycycle", 9) == 0) { float dc = 100.0f / (_prefs->airtime_factor + 1.0f); int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); @@ -456,8 +456,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch */ } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; - if (memcmp(config, "dutycycle ", 9) == 0) { - float dc = atof(&config[9]); + if (memcmp(config, "dutycycle ", 10) == 0) { + float dc = atof(&config[10]); if (dc < 10 || dc > 100) { strcpy(reply, "ERROR: dutycycle must be 10-100"); } else { From 728b586c3aec2478ca7eb6652852aac94d77aed3 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 18 Mar 2026 22:06:23 +0100 Subject: [PATCH 6/7] Address comments --- docs/cli_commands.md | 21 ++++++++++++++++++--- docs/terminal_chat_cli.md | 7 +++++-- src/helpers/CommonCLI.cpp | 11 ++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 7f1c93c7..a6b42387 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -506,16 +506,31 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set dutycycle ` **Parameters:** -- `value`: Duty cycle percentage (10-100) +- `value`: Duty cycle percentage (1-100) **Default:** `50%` (equivalent to airtime factor 1.0) **Examples:** - `set dutycycle 100` — no duty cycle limit - `set dutycycle 50` — 50% duty cycle (default) -- `set dutycycle 10` — 10% duty cycle (strictest EU requirement) +- `set dutycycle 10` — 10% duty cycle +- `set dutycycle 1` — 1% duty cycle (strictest EU requirement) -> **Deprecated:** `get af` / `set af` still work but are deprecated in favour of `dutycycle`. +> **Note:** Added in firmware v1.15.0 + +--- + +#### View or change the airtime factor (duty cycle limit) +> **Deprecated** as of firmware v1.15.0. Use [`get/set dutycycle`](#view-or-change-the-duty-cycle-limit) instead. + +**Usage:** +- `get af` +- `set af ` + +**Parameters:** +- `value`: Airtime factor (0-9) + +**Default:** `1.0` --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index b1a3af2a..c889da96 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -30,9 +30,12 @@ Sets your advertisement map longitude. (decimal degrees) ``` set dutycycle {percent} ``` -Sets the transmit duty cycle limit (10-100%). Example: `set dutycycle 10` for 10%. +Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. -> **Deprecated:** `set af` still works but is deprecated in favour of `set dutycycle`. +``` +set af {air-time-factor} +``` +Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead. ``` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 67a59b29..47a2592b 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -300,7 +300,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); sprintf(reply, "> %d.%d%%", dc_int, dc_frac); } else if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s (deprecated, use 'get dutycycle')", StrHelper::ftoa(_prefs->airtime_factor)); + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { @@ -458,8 +458,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch const char* config = &command[4]; if (memcmp(config, "dutycycle ", 10) == 0) { float dc = atof(&config[10]); - if (dc < 10 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 10-100"); + if (dc < 1 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { _prefs->airtime_factor = (100.0f / dc) - 1.0f; savePrefs(); @@ -471,10 +471,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%% (deprecated, use 'set dutycycle')", a_int, a_frac); + strcpy(reply, "OK"); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); From fb08fc0b1ea5164b01cc600ec385b09d3f47994b Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Tue, 24 Mar 2026 03:08:18 +0100 Subject: [PATCH 7/7] restore docs --- docs/cli_commands.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f248e4f3..90c5ab08 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -447,7 +447,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set af ` **Parameters:** -- `value`: Airtime factor (0-9) +- `value`: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: + - `af = 1` → ~50% duty + - `af = 2` → ~33% duty + - `af = 3` → ~25% duty + - `af = 9` → ~10% duty + You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation). **Default:** `1.0`