Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,19 +603,42 @@ Find more information in [packet_filter_reference.md](packet_filter_reference.md
**Usage:**
- `get dutycycle`
- `set dutycycle <value>`
- `set dutycycle auto`

**Parameters:**
- `value`: Duty cycle percentage (1-100)
- `value`: Duty cycle percentage (1-100), or `auto` to follow the regulatory limit of the configured frequency

**Default:** `50%` (equivalent to airtime factor 1.0)
**Default:** `auto`

In `auto` the limit comes from the sub-band the node is tuned to, and it is
re-derived as soon as `set freq` or `set radio` changes that frequency. The
table only covers the 863-870 MHz SRD band of ETSI EN 300 220-2; on any other
frequency `auto` means no limit.

| Sub-band (MHz) | Limit |
| --- | --- |
| 863.0 - 865.0 | 0.1% |
| 865.0 - 868.0 | 1% |
| 868.0 - 868.6 | 1% |
| 868.7 - 869.2 | 0.1% |
| 869.4 - 869.65 | 10% |
| 869.7 - 870.0 | 1% |
| gaps between the rows above | 0.1% |

A frequency exactly on a boundary takes the lower of the two sub-bands.

Setting an explicit percentage, or setting `af`, turns `auto` off and that
value stays in force until `set dutycycle auto` restores it. `get dutycycle`
reports which of the two is active.

**Examples:**
- `set dutycycle auto` — follow the sub-band of the configured frequency (default)
- `set dutycycle 100` — no duty cycle limit
- `set dutycycle 50` — 50% duty cycle (default)
- `set dutycycle 50` — 50% duty cycle
- `set dutycycle 10` — 10% duty cycle
- `set dutycycle 1` — 1% duty cycle (strictest EU requirement)
- `set dutycycle 1` — 1% duty cycle

> **Note:** Added in firmware v1.15.0
> **Note:** Added in firmware v1.15.0. `auto`, and the default changing from 50% to `auto`, added in the DMC fork.

---

Expand All @@ -634,7 +657,11 @@ Find more information in [packet_filter_reference.md](packet_filter_reference.md
- `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`
Setting `af` turns off `set dutycycle auto`. `get af` reports the factor
that is actually in force, so while `dutycycle` is on `auto` it reports the
one derived from the frequency rather than the stored value.

**Default:** `1.0`, used only while `dutycycle` is not on `auto`

---

Expand Down
7 changes: 5 additions & 2 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "MyMesh.h"
#include <helpers/DutyCycleLimits.h>

#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
Expand Down Expand Up @@ -255,7 +256,7 @@ int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
}

float MyMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
return getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq);
}

int MyMesh::getInterferenceThreshold() const {
Expand Down Expand Up @@ -1431,10 +1432,12 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4;
_prefs.rx_delay_base = ((float)rx) / 1000.0f;
_prefs.airtime_factor = ((float)af) / 1000.0f;
_prefs.dutycycle_auto = 0; // an explicit airtime factor takes over from the derived limit
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) {
uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000;
uint32_t rx = _prefs.rx_delay_base * 1000;
uint32_t af = getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq) * 1000;
int i = 0;
out_frame[i++] = RESP_CODE_TUNING_PARAMS;
memcpy(&out_frame[i], &rx, 4); i += 4;
Expand Down
2 changes: 2 additions & 0 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain (LNA)
uint8_t radio_fem_txgain = 0; // external LoRa FEM TX gain (low by default)
uint8_t dutycycle_auto = 1; // derive the duty cycle limit from freq (boolean)
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
uint8_t path_hash_mode = 0; // which path mode to use when sending
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
Expand All @@ -61,6 +62,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file
#endif
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("dc_auto", _parent->dutycycle_auto);
def("rxdelay", _parent->rx_delay_base);
//def("f_txdelay", _parent->tx_delay_factor); currently hard-coded
//def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <Mesh.h>
#include <RTClib.h>
#include <target.h>
#include <helpers/DutyCycleLimits.h>

#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
Expand Down Expand Up @@ -135,7 +136,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {

protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
return getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq);
}

bool allowPacketForward(const mesh::Packet* packet) override;
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/DutyCycleLimits.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
Expand Down Expand Up @@ -130,7 +131,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {

protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
return getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq);
}

void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
Expand Down
8 changes: 6 additions & 2 deletions examples/simple_secure_chat/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/DutyCycleLimits.h>
#include <RTClib.h>
#include <target.h>

Expand Down Expand Up @@ -67,7 +68,8 @@ struct NodePrefs { // persisted to file
double node_lat, node_lon;
float freq;
int8_t tx_power_dbm;
uint8_t unused[3];
uint8_t dutycycle_auto;
uint8_t unused[2];
};

class MyMesh : public BaseChatMesh, ContactVisitor {
Expand Down Expand Up @@ -191,7 +193,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {

protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
return getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq);
}

int calcRxDelay(float score, uint32_t air_time) const override {
Expand Down Expand Up @@ -285,6 +287,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.dutycycle_auto = 1;

command[0] = 0;
curr_recipient = NULL;
Expand Down Expand Up @@ -476,6 +479,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
_prefs.airtime_factor = atof(&config[3]);
_prefs.dutycycle_auto = 0;
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "name ", 5) == 0) {
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "SensorMesh.h"
#include <helpers/DutyCycleLimits.h>

/* ------------------------------ Config -------------------------------- */

Expand Down Expand Up @@ -298,7 +299,7 @@ void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const ch
}

float SensorMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
return getEffectiveAirtimeFactor(_prefs.dutycycle_auto, _prefs.airtime_factor, _prefs.freq);
}

bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
Expand Down
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ build_src_filter =
+<../src/Utils.cpp>
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/DutyCycleLimits.cpp>
lib_deps =
google/googletest @ 1.17.0

Expand Down
34 changes: 24 additions & 10 deletions src/helpers/CommonCLI.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "CommonCLI.h"
#include "DutyCycleLimits.h"
#include "TxtDataHelpers.h"
#include "AdvertDataHelpers.h"
#include "TxtDataHelpers.h"
Expand Down Expand Up @@ -448,19 +449,30 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (memcmp(config, "dutycycle ", 10) == 0) {
float dc = atof(&config[10]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
if (memcmp(&config[10], "auto", 4) == 0) {
_prefs->dutycycle_auto = 1;
savePrefs();
float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
float actual = getMaxDutyCyclePercent(_prefs->freq);
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);
sprintf(reply, "OK - auto, %d.%d%%", a_int, a_frac);
} else {
float dc = atof(&config[10]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100, or auto");
} else {
_prefs->airtime_factor = dutyCycleToAirtimeFactor(dc);
_prefs->dutycycle_auto = 0;
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]);
_prefs->dutycycle_auto = 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "int.thresh ", 11) == 0) {
Expand Down Expand Up @@ -807,12 +819,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (memcmp(config, "dutycycle", 9) == 0) {
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
float af = getEffectiveAirtimeFactor(_prefs->dutycycle_auto, _prefs->airtime_factor, _prefs->freq);
float dc = 100.0f / (af + 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);
sprintf(reply, "> %d.%d%% (%s)", dc_int, dc_frac, _prefs->dutycycle_auto ? "auto" : "manual");
} else if (memcmp(config, "af", 2) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
float af = getEffectiveAirtimeFactor(_prefs->dutycycle_auto, _prefs->airtime_factor, _prefs->freq);
sprintf(reply, "> %s", StrHelper::ftoa(af));
} else if (memcmp(config, "int.thresh", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "cad", 3) == 0) {
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class NodePrefs : public ConfigSerializer {
uint8_t path_hash_mode = 0; // which path mode to use when sending
uint8_t loop_detect = 0;
uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean)
uint8_t dutycycle_auto = 1; // derive the duty cycle limit from freq (boolean)
uint8_t extra_sf[4];

private:
Expand All @@ -87,6 +88,7 @@ class NodePrefs : public ConfigSerializer {
def("fem_txgain", _parent->radio_fem_txgain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("dc_auto", _parent->dutycycle_auto);
def("rxdelay", _parent->rx_delay_base);
def("f_txdelay", _parent->tx_delay_factor);
def("d_txdelay", _parent->direct_tx_delay_factor);
Expand Down
49 changes: 49 additions & 0 deletions src/helpers/DutyCycleLimits.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "DutyCycleLimits.h"

#define SRD_BAND_START_MHZ 863.0f
#define SRD_BAND_END_MHZ 870.0f

// Applied inside the SRD band to any frequency that no sub-band below covers.
// Those gaps are the alarm and social alarm allocations, which carry their own
// restrictions, so the tightest limit of the band is used rather than none.
#define SRD_BAND_FALLBACK 0.1f

struct SubBand {
float start_mhz;
float end_mhz;
float max_duty_cycle;
};

// ETSI EN 300 220-2, the sub-bands MeshCore presets are tuned to. A frequency
// that sits exactly on a boundary matches the first entry it falls in, which
// is the lower and therefore more restrictive of the two.
static const SubBand SUB_BANDS[] = {
{ 863.0f, 865.0f, 0.1f },
{ 865.0f, 868.0f, 1.0f },
{ 868.0f, 868.6f, 1.0f },
{ 868.7f, 869.2f, 0.1f },
{ 869.4f, 869.65f, 10.0f },
{ 869.7f, 870.0f, 1.0f },
};

float getMaxDutyCyclePercent(float freq_mhz) {
if (freq_mhz < SRD_BAND_START_MHZ || freq_mhz > SRD_BAND_END_MHZ) {
return DUTY_CYCLE_UNLIMITED;
}
for (int i = 0; i < (int)(sizeof(SUB_BANDS) / sizeof(SUB_BANDS[0])); i++) {
if (freq_mhz >= SUB_BANDS[i].start_mhz && freq_mhz <= SUB_BANDS[i].end_mhz) {
return SUB_BANDS[i].max_duty_cycle;
}
}
return SRD_BAND_FALLBACK;
}

float dutyCycleToAirtimeFactor(float percent) {
return (100.0f / percent) - 1.0f;
}

float getEffectiveAirtimeFactor(uint8_t dutycycle_auto, float airtime_factor, float freq_mhz) {
if (!dutycycle_auto) return airtime_factor;

return dutyCycleToAirtimeFactor(getMaxDutyCyclePercent(freq_mhz));
}
23 changes: 23 additions & 0 deletions src/helpers/DutyCycleLimits.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <stdint.h>

// Regulatory duty cycle limits, derived from the frequency a node is tuned to.
//
// Only the 863 to 870 MHz SRD band is constrained here, using the sub-band
// table of ETSI EN 300 220-2. Every other frequency is reported as
// DUTY_CYCLE_UNLIMITED, so US, ANZ and any other region keep the behaviour
// they had before this table existed.

#define DUTY_CYCLE_UNLIMITED 100.0f

// Highest duty cycle (percent) allowed on freq_mhz.
float getMaxDutyCyclePercent(float freq_mhz);

// Duty cycle percentage to the airtime budget factor Dispatcher works with,
// where duty_cycle = 1 / (1 + factor).
float dutyCycleToAirtimeFactor(float percent);

// Airtime budget factor a node should use: derived from its frequency while
// dutycycle_auto is set, otherwise the factor its operator configured.
float getEffectiveAirtimeFactor(uint8_t dutycycle_auto, float airtime_factor, float freq_mhz);
Loading