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
16 changes: 16 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,22 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

#### View or change the LoRa FEM transmit-path gain state on supported boards
**Usage:**
- `get radio.fem.txgain`
- `set radio.fem.txgain <state>`

**Parameters:**
- `state`: `on`|`off`

**Notes:**
- This controls a software-selectable external LoRa FEM transmit gain where the board supports it.
- On Station G3, remove the PA PL1 jumper to allow software control. `on` selects PA PL1 high/short and `off` selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4.
- Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements.
- The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. `get` reports the configured state, which may lead the hardware until the node next transmits.

---

### System

#### View or change this node's name
Expand Down
4 changes: 4 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default
_prefs.radio_fem_rxgain = 1;
_prefs.radio_fem_txgain = 0;
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
_prefs.setRepeatEn(false);
#if defined(USE_SX1262) || defined(USE_SX1268)
Expand Down Expand Up @@ -974,6 +976,8 @@ void MyMesh::begin(bool has_display) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
Expand Down
7 changes: 5 additions & 2 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
uint32_t gps_interval = 0; // GPS read interval in seconds
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
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 _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 @@ -50,7 +52,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
//def("cad", _parent->cad_enabled);
//def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->radio_fem_rxgain);
def("fem_txgain", _parent->radio_fem_txgain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
Expand Down Expand Up @@ -133,4 +136,4 @@ class NodePrefs : public ConfigSerializer { // persisted to file
// new accessor methods
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
};
};
2 changes: 2 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#endif
#endif
_prefs.radio_fem_rxgain = 1;
_prefs.radio_fem_txgain = 0;

pending_discover_tag = 0;
pending_discover_until = 0;
Expand Down Expand Up @@ -962,6 +963,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#endif
#endif
_prefs.radio_fem_rxgain = 1;
_prefs.radio_fem_txgain = 0;

next_post_idx = 0;
next_client_idx = 0;
Expand Down Expand Up @@ -726,6 +727,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;
_prefs.radio_fem_txgain = 0;

memset(default_scope.key, 0, sizeof(default_scope.key));
}
Expand Down Expand Up @@ -771,6 +772,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
4 changes: 4 additions & 0 deletions src/MeshCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class MainBoard {
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
// Software-selectable external FEM transmit gain. This is not a PA power switch.
virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemPaGain() const { return false; }
virtual bool isLoRaFemPaGainEnabled() const { return false; }

// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
Expand Down
29 changes: 29 additions & 0 deletions src/helpers/CommonCLI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy
// sanitise settings
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
_prefs->radio_fem_txgain = constrain(_prefs->radio_fem_txgain, 0, 1); // boolean
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean

file.close();
Expand Down Expand Up @@ -562,6 +563,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio.fem.txgain ", 17) == 0) {
if (!_board->canControlLoRaFemPaGain()) {
strcpy(reply, "Error: unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemPaGainEnabled(true)) {
_prefs->radio_fem_txgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemPaGainEnabled(false)) {
_prefs->radio_fem_txgain = 0;
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char *parts[4];
Expand Down Expand Up @@ -827,6 +850,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio.fem.txgain", 16) == 0) {
if (!_board->canControlLoRaFemPaGain()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
Expand Down
4 changes: 3 additions & 1 deletion src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class NodePrefs : public ConfigSerializer {
char owner_info[120];
uint8_t rx_boosted_gain = 0; // power settings
uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting
uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain setting
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)
Expand All @@ -82,7 +83,8 @@ class NodePrefs : public ConfigSerializer {
def("cad", _parent->cad_enabled);
def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->radio_fem_rxgain);
def("fem_txgain", _parent->radio_fem_txgain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
Expand Down
80 changes: 80 additions & 0 deletions test/test_companion_node_prefs/test_companion_node_prefs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#include <gtest/gtest.h>

#include <cstdio>
#include <cstring>
#include <string>

#include "../../examples/companion_radio/NodePrefs.h"

class ReplayStream : public Stream {
const char* _text;
int _pos = 0;
int _len;

public:
explicit ReplayStream(const char* text) : _text(text), _len(strlen(text)) { }

int available() override { return _len - _pos; }
int read() override { return _pos < _len ? _text[_pos++] : -1; }
int peek() override { return _pos < _len ? _text[_pos] : -1; }
};

class CaptureStream : public Stream {
std::string _text;

size_t emit(long long value) {
char text[24];
int length = snprintf(text, sizeof(text), "%lld", value);
return write(reinterpret_cast<const uint8_t*>(text), length);
}

public:
size_t write(uint8_t value) override {
_text.push_back(static_cast<char>(value));
return 1;
}

size_t write(const uint8_t* buffer, size_t size) override {
_text.append(reinterpret_cast<const char*>(buffer), size);
return size;
}

size_t print(unsigned char value, int = DEC) override { return emit(value); }
size_t print(int value, int = DEC) override { return emit(value); }
size_t print(unsigned int value, int = DEC) override { return emit(value); }
size_t print(long value, int = DEC) override { return emit(value); }
size_t print(unsigned long value, int = DEC) override { return emit(value); }
size_t print(long long value, int = DEC) override { return emit(value); }
size_t print(unsigned long long value, int = DEC) override { return emit(value); }

const std::string& text() const { return _text; }
};

TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {
NodePrefs saved;
saved.rx_boosted_gain = 0;
saved.radio_fem_rxgain = 1;
saved.radio_fem_txgain = 0;

CaptureStream output;
ASSERT_TRUE(saved.saveSerial(output));
EXPECT_NE(std::string::npos, output.text().find("rxgain:0"));
EXPECT_NE(std::string::npos, output.text().find("fem_rxgain:1"));
EXPECT_NE(std::string::npos, output.text().find("fem_txgain:0"));

ReplayStream input("{radio:{rxgain:1,fem_rxgain:0,fem_txgain:1}}");
NodePrefs loaded;
loaded.rx_boosted_gain = 0;
loaded.radio_fem_rxgain = 1;
loaded.radio_fem_txgain = 0;

ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_EQ(1, loaded.rx_boosted_gain);
EXPECT_EQ(0, loaded.radio_fem_rxgain);
EXPECT_EQ(1, loaded.radio_fem_txgain);
}

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
65 changes: 54 additions & 11 deletions test/test_config_serializer/test_config_serializer.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#include <gtest/gtest.h>
#include "helpers/ConfigSerializer.h"

class NativeFileSystem {
public:
void mkdir(const char*) { }
};
#define FILESYSTEM NativeFileSystem
#include "helpers/CommonCLI.h"
#undef FILESYSTEM

#define TEST_INT_S "56"
#define TEST_INT 56
#define TEST_FLOAT_S "-6.123"
Expand All @@ -21,6 +29,19 @@ class MockInputStream : public Stream {
class MockPrintStream : public Stream {
int len = 0;
uint8_t _buf[1024];

size_t printSigned(long long value) {
char text[24];
snprintf(text, sizeof(text), "%lld", value);
return Print::print(text);
}

size_t printUnsigned(unsigned long long value) {
char text[24];
snprintf(text, sizeof(text), "%llu", value);
return Print::print(text);
}

public:
size_t write(uint8_t b) override {
if (len < sizeof(_buf)) {
Expand All @@ -30,17 +51,17 @@ class MockPrintStream : public Stream {
return 0;
}

size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
size_t print(double v, int p = 2) override {
if (p == 6) return Print::print(TEST_DOUBLE_S);
if (p == 4) return Print::print(TEST_FLOAT_S);
return 0;
size_t print(unsigned char v, int r) override { return printUnsigned(v); }
size_t print(int v, int r) override { return printSigned(v); }
size_t print(unsigned int v, int r) override { return printUnsigned(v); }
size_t print(long v, int r) override { return printSigned(v); }
size_t print(unsigned long v, int r) override { return printUnsigned(v); }
size_t print(long long v, int r) override { return printSigned(v); }
size_t print(unsigned long long v, int r) override { return printUnsigned(v); }
size_t print(double v, int p = 2) override {
char text[32];
snprintf(text, sizeof(text), "%.*f", p, v);
return Print::print(text);
}

int getLength() const { return len; }
Expand Down Expand Up @@ -171,6 +192,28 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
EXPECT_TRUE(match);
}

TEST(NodePrefs, FemGainSettingsRoundTrip) {
NodePrefs saved;
saved.radio_fem_rxgain = 0;
saved.radio_fem_txgain = 1;

MockPrintStream output;
ASSERT_TRUE(saved.saveSerial(output));

std::string serialised(reinterpret_cast<const char*>(output.getBytes()), output.getLength());
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));

MockInputStream input(serialised.c_str());
NodePrefs loaded;
loaded.radio_fem_rxgain = 1;
loaded.radio_fem_txgain = 0;

ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_EQ(0, loaded.radio_fem_rxgain);
EXPECT_EQ(1, loaded.radio_fem_txgain);
}


// ── main ───────────────────────────────────────────────────────

Expand Down
Loading