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 @@ -561,19 +561,42 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**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, including the US and ANZ channel plans, `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`, are newer than v1.17.1.

---

Expand All @@ -592,7 +615,11 @@ This document provides an overview of CLI commands that can be sent to MeshCore
- `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
8 changes: 6 additions & 2 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "MyMesh.h"

#include <helpers/DutyCycleLimits.h>

#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>

Expand Down Expand Up @@ -261,7 +263,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 @@ -1513,10 +1515,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
4 changes: 4 additions & 0 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file
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)
uint8_t dutycycle_auto = 1; // derive the duty cycle limit from freq (boolean)
uint8_t cad_enabled = 0;
char default_scope_name[31];
uint8_t default_scope_key[16];
Expand All @@ -61,6 +62,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file
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); currently hard-coded
//def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded
Expand All @@ -82,6 +84,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
uint8_t isDutyCycleAuto() const override { return _parent->dutycycle_auto; }
void setDutyCycleAuto(uint8_t on) override { _parent->dutycycle_auto = on; markDirty(); }
bool isCadEnabled() const override { return _parent->cad_enabled; }
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
uint8_t getIntThresh() const override { return 0; }
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 @@ -129,7 +130,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 @@ -2,6 +2,7 @@

#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/DutyCycleLimits.h>

#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.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
6 changes: 5 additions & 1 deletion 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 @@ -68,6 +69,7 @@ struct NodePrefs { // persisted to file
float freq;
int8_t tx_power_dbm;
uint8_t unused[3];
uint8_t dutycycle_auto;
};

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 @@ -289,6 +291,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 @@ -480,6 +483,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
4 changes: 3 additions & 1 deletion examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "SensorMesh.h"

#include <helpers/DutyCycleLimits.h>

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

#ifndef LORA_FREQ
Expand Down Expand Up @@ -298,7 +300,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 @@ -172,6 +172,7 @@ build_src_filter =
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/DynamicConfigSerializer.cpp>
+<../src/helpers/DutyCycleLimits.cpp>
lib_deps =
google/googletest @ 1.17.0

Expand Down
4 changes: 4 additions & 0 deletions src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 @@ -89,6 +90,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 All @@ -109,6 +111,8 @@ class NodePrefs : public ConfigSerializer {
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
uint8_t isDutyCycleAuto() const override { return _parent->dutycycle_auto; }
void setDutyCycleAuto(uint8_t on) override { _parent->dutycycle_auto = on; markDirty(); }
bool isCadEnabled() const override { return _parent->cad_enabled; }
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
uint8_t getIntThresh() const override { return _parent->interference_threshold; }
Expand Down
23 changes: 18 additions & 5 deletions src/helpers/CommonRadioPrefs.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "CommonRadioPrefs.h"
#include "DutyCycleLimits.h"
#include "TxtDataHelpers.h"
#include "Utils.h"
#include "target.h"
Expand Down Expand Up @@ -63,28 +64,40 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest
}

if (strcmp(command, "get af") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor()));
float af = getEffectiveAirtimeFactor(isDutyCycleAuto(), getAirtimeFactor(), getFreq());
sprintf(reply, "> %s", StrHelper::ftoa(af));
return true;
}
if (memcmp(command, "set af ", 7) == 0) {
setAirtimeFactor(atof(&command[7]));
setDutyCycleAuto(0); // an explicit factor takes over from the derived limit
strcpy(reply, "OK");
return true;
}

if (strcmp(command, "get dutycycle") == 0) {
float dc = 100.0f / (getAirtimeFactor() + 1.0f);
float af = getEffectiveAirtimeFactor(isDutyCycleAuto(), getAirtimeFactor(), getFreq());
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, isDutyCycleAuto() ? "auto" : "manual");
return true;
}
if (memcmp(command, "set dutycycle ", 14) == 0) {
if (strcmp(&command[14], "auto") == 0) {
setDutyCycleAuto(1);
float actual = getMaxDutyCyclePercent(getFreq());
int a_int = (int)actual;
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
sprintf(reply, "OK - auto, %d.%d%%", a_int, a_frac);
return true;
}
float dc = atof(&command[14]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
strcpy(reply, "ERROR: dutycycle must be 1-100, or auto");
} else {
setAirtimeFactor((100.0f / dc) - 1.0f);
setAirtimeFactor(dutyCycleToAirtimeFactor(dc));
setDutyCycleAuto(0);
float actual = 100.0f / (getAirtimeFactor() + 1.0f);
int a_int = (int)actual;
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/CommonRadioPrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore {
virtual float getAirtimeFactor() const = 0;
virtual void setAirtimeFactor(float af) = 0;

virtual uint8_t isDutyCycleAuto() const = 0;
virtual void setDutyCycleAuto(uint8_t on) = 0;

virtual bool isCadEnabled() const = 0;
virtual void setCadEnabled(bool en) = 0;

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