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
53 changes: 52 additions & 1 deletion docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,58 @@ This document provides an overview of CLI commands that can be sent to MeshCore
### Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors
**Usage:** `stats-radio`

**Serial Only:** Yes
**Serial Only:** No

Returns JSON with:
- `noise_floor`: current radio noise floor estimate in dBm
- `last_rssi`: RSSI from the most recent received packet
- `last_snr`: SNR from the most recent received packet
- `tx_air_secs`: accumulated transmit airtime in seconds
- `rx_air_secs`: accumulated receive airtime estimate in seconds

---

### Noise-floor calibration stats
**Usage:** `stats-noise`

**Serial Only:** No

Returns JSON with:
- `floor`: current radio noise floor estimate in dBm
- `accepted`: RSSI samples accepted into the current or most recent calibration batch
- `min` / `median` / `max`: accepted RSSI sample range in dBm. `floor` is estimated from the lower quartile of the accepted batch.
- `rejected_low`: RSSI samples rejected because they would cause a suspicious downward jump
- `rejected_high`: strong RSSI samples rejected because they look like channel activity rather than idle noise

---

### Noise-floor calibration settings
**Usage:** `get noise.sample.ms`

**Usage:** `set noise.sample.ms <milliseconds>`

**Usage:** `get noise.window.secs`

**Usage:** `set noise.window.secs <seconds>`

**Usage:** `get noise.clamp.low`

**Usage:** `set noise.clamp.low <dBm>`

**Usage:** `get noise.clamp.high`

**Usage:** `set noise.clamp.high <dBm>`

**Serial Only:** No

Controls the RSSI sampling cadence and maximum calibration attempt window used by noise-floor calibration.

- `noise.sample.ms`: minimum delay between instantaneous RSSI samples. Range: `50`-`5000` ms. Default: `50`.
- `noise.window.secs`: maximum age of a partial calibration batch before it is discarded. Range: `1`-`600` seconds. Default: `60`.
- `noise.clamp.low`: lowest RSSI value accepted into calibration after clamping. Range: `-150` to `-80` dBm. Default: `-125`.
- `noise.clamp.high`: RSSI level treated as channel activity rather than idle noise. Range: `-120` to `-40` dBm. Default: `-80`.

`noise.clamp.low` must be lower than `noise.clamp.high`.

---

Expand Down
12 changes: 12 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
_prefs.noise_sample_interval_ms = DEFAULT_NOISE_SAMPLE_INTERVAL_MS;
_prefs.noise_calib_window_secs = DEFAULT_NOISE_CALIB_WINDOW_SECS;
_prefs.noise_clamp_low_dbm = DEFAULT_NOISE_CLAMP_LOW_DBM;
_prefs.noise_clamp_high_dbm = DEFAULT_NOISE_CLAMP_HIGH_DBM;

// bridge defaults
_prefs.bridge_enabled = 1; // enabled
Expand Down Expand Up @@ -961,6 +965,10 @@ void MyMesh::begin(FILESYSTEM *fs) {

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setNoiseFloorCalibration(_prefs.noise_sample_interval_ms,
_prefs.noise_calib_window_secs * 1000U);
radio_driver.setNoiseFloorClamps(_prefs.noise_clamp_low_dbm,
_prefs.noise_clamp_high_dbm);

radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
Expand Down Expand Up @@ -1147,6 +1155,10 @@ void MyMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}

void MyMesh::formatNoiseFloorStatsReply(char *reply) {
StatsFormatHelper::formatNoiseFloorStats(reply, _radio);
}

void MyMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
Expand Down
7 changes: 7 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,17 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatNoiseFloorStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
void setNoiseFloorCalibration(uint16_t sample_interval_ms, uint16_t max_calib_window_secs) override {
_radio->setNoiseFloorCalibration(sample_interval_ms, ((uint32_t)max_calib_window_secs) * 1000U);
}
void setNoiseFloorClamps(int16_t low_bound, int16_t high_bound) override {
_radio->setNoiseFloorClamps(low_bound, high_bound);
}

mesh::LocalIdentity& getSelfId() override { return self_id; }

Expand Down
12 changes: 12 additions & 0 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
_prefs.noise_sample_interval_ms = DEFAULT_NOISE_SAMPLE_INTERVAL_MS;
_prefs.noise_calib_window_secs = DEFAULT_NOISE_CALIB_WINDOW_SECS;
_prefs.noise_clamp_low_dbm = DEFAULT_NOISE_CLAMP_LOW_DBM;
_prefs.noise_clamp_high_dbm = DEFAULT_NOISE_CLAMP_HIGH_DBM;
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
Expand Down Expand Up @@ -699,6 +703,10 @@ void MyMesh::begin(FILESYSTEM *fs) {

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setNoiseFloorCalibration(_prefs.noise_sample_interval_ms,
_prefs.noise_calib_window_secs * 1000U);
radio_driver.setNoiseFloorClamps(_prefs.noise_clamp_low_dbm,
_prefs.noise_clamp_high_dbm);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down Expand Up @@ -851,6 +859,10 @@ void MyMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}

void MyMesh::formatNoiseFloorStatsReply(char *reply) {
StatsFormatHelper::formatNoiseFloorStats(reply, _radio);
}

void MyMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
Expand Down
7 changes: 7 additions & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,17 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatNoiseFloorStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
void setNoiseFloorCalibration(uint16_t sample_interval_ms, uint16_t max_calib_window_secs) override {
_radio->setNoiseFloorCalibration(sample_interval_ms, ((uint32_t)max_calib_window_secs) * 1000U);
}
void setNoiseFloorClamps(int16_t low_bound, int16_t high_bound) override {
_radio->setNoiseFloorClamps(low_bound, high_bound);
}

mesh::LocalIdentity& getSelfId() override { return self_id; }

Expand Down
12 changes: 12 additions & 0 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.disable_fwd = true;
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
_prefs.noise_sample_interval_ms = DEFAULT_NOISE_SAMPLE_INTERVAL_MS;
_prefs.noise_calib_window_secs = DEFAULT_NOISE_CALIB_WINDOW_SECS;
_prefs.noise_clamp_low_dbm = DEFAULT_NOISE_CLAMP_LOW_DBM;
_prefs.noise_clamp_high_dbm = DEFAULT_NOISE_CLAMP_HIGH_DBM;

// GPS defaults
_prefs.gps_enabled = 0;
Expand Down Expand Up @@ -765,6 +769,10 @@ void SensorMesh::begin(FILESYSTEM* fs) {
}

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setNoiseFloorCalibration(_prefs.noise_sample_interval_ms,
_prefs.noise_calib_window_secs * 1000U);
radio_driver.setNoiseFloorClamps(_prefs.noise_clamp_low_dbm,
_prefs.noise_clamp_high_dbm);
radio_driver.setTxPower(_prefs.tx_power_dbm);

updateAdvertTimer();
Expand Down Expand Up @@ -853,6 +861,10 @@ void SensorMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}

void SensorMesh::formatNoiseFloorStatsReply(char *reply) {
StatsFormatHelper::formatNoiseFloorStats(reply, _radio);
}

void SensorMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
Expand Down
7 changes: 7 additions & 0 deletions examples/simple_sensor/SensorMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatNoiseFloorStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void setNoiseFloorCalibration(uint16_t sample_interval_ms, uint16_t max_calib_window_secs) override {
_radio->setNoiseFloorCalibration(sample_interval_ms, ((uint32_t)max_calib_window_secs) * 1000U);
}
void setNoiseFloorClamps(int16_t low_bound, int16_t high_bound) override {
_radio->setNoiseFloorClamps(low_bound, high_bound);
}
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
Expand Down
9 changes: 8 additions & 1 deletion src/Dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ void Dispatcher::updateTxBudget() {
}
}

void Dispatcher::scheduleNoiseFloorRefreshAfterRadioAnomaly() {
_radio->scheduleNoiseFloorCalibration(DEFAULT_NOISE_FLOOR_SETTLE_MS);
}

int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
}
Expand Down Expand Up @@ -117,6 +121,7 @@ void Dispatcher::loop() {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());

_radio->onSendFinished();
scheduleNoiseFloorRefreshAfterRadioAnomaly();
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);

releasePacket(outbound); // return to pool
Expand Down Expand Up @@ -294,6 +299,7 @@ void Dispatcher::checkSend() {
_err_flags |= ERR_EVENT_CAD_TIMEOUT;

MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): CAD busy max duration reached!", getLogDateTime());
scheduleNoiseFloorRefreshAfterRadioAnomaly();
// channel activity has gone on too long... (Radio might be in a bad state)
// force the pending transmit below...
} else {
Expand Down Expand Up @@ -329,6 +335,7 @@ void Dispatcher::checkSend() {
if (!success) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());

scheduleNoiseFloorRefreshAfterRadioAnomaly();
logTxFail(outbound, outbound->getRawLength());

releasePacket(outbound); // return to pool
Expand Down Expand Up @@ -386,4 +393,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}

}
}
26 changes: 26 additions & 0 deletions src/Dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ class MillisecondClock {
/**
* \brief Abstraction of this device's packet radio.
*/
struct NoiseFloorStats {
uint16_t accepted_count;
int16_t sample_min;
int16_t sample_median;
int16_t sample_max;
uint16_t rejected_low_bound_count;
uint16_t rejected_high_bound_count;
};

class Radio {
public:
virtual void begin() { }
Expand Down Expand Up @@ -62,6 +71,7 @@ class Radio {
virtual void loop() { }

virtual int getNoiseFloor() const { return 0; }
virtual NoiseFloorStats getNoiseFloorStats() const { return {0, 0, 0, 0, 0, 0}; }

virtual void triggerNoiseFloorCalibrate(int threshold) { }

Expand All @@ -76,6 +86,20 @@ class Radio {

virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }

virtual void setNoiseFloorCalibration(uint16_t sample_interval_ms, uint32_t max_calib_window_ms) {
(void)sample_interval_ms;
(void)max_calib_window_ms;
}

virtual void setNoiseFloorClamps(int16_t low_bound, int16_t high_bound) {
(void)low_bound;
(void)high_bound;
}

virtual void scheduleNoiseFloorCalibration(uint32_t settle_ms) {
(void)settle_ms;
}
};

/**
Expand Down Expand Up @@ -109,6 +133,7 @@ typedef uint32_t DispatcherAction;
#define ERR_EVENT_CAD_TIMEOUT (1 << 1)
#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2)

#define DEFAULT_NOISE_FLOOR_SETTLE_MS 500
/**
* \brief The low-level task that manages detecting incoming Packets, and the queueing
* and scheduling of outbound Packets.
Expand All @@ -129,6 +154,7 @@ class Dispatcher {

void processRecvPacket(Packet* pkt);
void updateTxBudget();
void scheduleNoiseFloorRefreshAfterRadioAnomaly();

protected:
PacketManager* _mgr;
Expand Down
Loading