Skip to content

Commit a4ab7f0

Browse files
authored
Merge pull request meshcore-dev#3137 from agessaman/feat/station-g3-fem-prefs
Station G3: Expose, persist, and apply FEM gain preferences
2 parents 1cb308b + e2aa7b9 commit a4ab7f0

16 files changed

Lines changed: 352 additions & 56 deletions

File tree

docs/cli_commands.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,22 @@ This document provides an overview of CLI commands that can be sent to MeshCore
291291

292292
---
293293

294+
#### View or change the LoRa FEM transmit-path gain state on supported boards
295+
**Usage:**
296+
- `get radio.fem.txgain`
297+
- `set radio.fem.txgain <state>`
298+
299+
**Parameters:**
300+
- `state`: `on`|`off`
301+
302+
**Notes:**
303+
- This controls a software-selectable external LoRa FEM transmit gain where the board supports it.
304+
- 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.
305+
- Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements.
306+
- 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.
307+
308+
---
309+
294310
### System
295311

296312
#### View or change this node's name

examples/companion_radio/MyMesh.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
885885
_prefs.tx_power_dbm = LORA_TX_POWER;
886886
_prefs.gps_enabled = 0; // GPS disabled by default
887887
_prefs.gps_interval = 0; // No automatic GPS updates by default
888+
_prefs.radio_fem_rxgain = 1;
889+
_prefs.radio_fem_txgain = 0;
888890
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
889891
_prefs.setRepeatEn(false);
890892
#if defined(USE_SX1262) || defined(USE_SX1268)
@@ -974,6 +976,8 @@ void MyMesh::begin(bool has_display) {
974976
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
975977
radio_driver.setTxPower(_prefs.tx_power_dbm);
976978
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
979+
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
980+
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
977981
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
978982
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
979983
}

examples/companion_radio/NodePrefs.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
3333
uint32_t gps_interval = 0; // GPS read interval in seconds
3434
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
3535
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
36+
uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain (LNA)
37+
uint8_t radio_fem_txgain = 0; // external LoRa FEM TX gain (low by default)
3638
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
3739
uint8_t path_hash_mode = 0; // which path mode to use when sending
3840
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
@@ -51,7 +53,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
5153
//def("cad", _parent->cad_enabled);
5254
//def("int_thr", _parent->interference_threshold);
5355
def("rxgain", _parent->rx_boosted_gain);
54-
def("fem_rxgain", _parent->rx_boosted_gain);
56+
def("fem_rxgain", _parent->radio_fem_rxgain);
57+
def("fem_txgain", _parent->radio_fem_txgain);
5558
def("tx", _parent->tx_power_dbm);
5659
def("af", _parent->airtime_factor);
5760
def("rxdelay", _parent->rx_delay_base);
@@ -135,4 +138,4 @@ class NodePrefs : public ConfigSerializer { // persisted to file
135138
// new accessor methods
136139
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
137140
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
138-
};
141+
};

examples/simple_repeater/MyMesh.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
913913
#endif
914914
#endif
915915
_prefs.radio_fem_rxgain = 1;
916+
_prefs.radio_fem_txgain = 0;
916917

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

966968
updateAdvertTimer();
967969
updateFloodAdvertTimer();

examples/simple_room_server/MyMesh.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
683683
#endif
684684
#endif
685685
_prefs.radio_fem_rxgain = 1;
686+
_prefs.radio_fem_txgain = 0;
686687

687688
next_post_idx = 0;
688689
next_client_idx = 0;
@@ -726,6 +727,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
726727
radio_driver.setTxPower(_prefs.tx_power_dbm);
727728
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
728729
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
730+
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
729731

730732
updateAdvertTimer();
731733
updateFloodAdvertTimer();

examples/simple_sensor/SensorMesh.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
735735
_prefs.gps_interval = 0;
736736
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
737737
_prefs.radio_fem_rxgain = 1;
738+
_prefs.radio_fem_txgain = 0;
738739

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

775777
updateAdvertTimer();
776778
updateFloodAdvertTimer();

src/MeshCore.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ class MainBoard {
6767
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
6868
virtual bool canControlLoRaFemLna() const { return false; }
6969
virtual bool isLoRaFemLnaEnabled() const { return false; }
70+
// Software-selectable external FEM transmit gain. This is not a PA power switch.
71+
virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; }
72+
virtual bool canControlLoRaFemPaGain() const { return false; }
73+
virtual bool isLoRaFemPaGainEnabled() const { return false; }
7074

7175
// Power management interface (boards with power management override these)
7276
virtual bool isExternalPowered() { return false; }

src/helpers/CommonCLI.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy
133133
// sanitise settings
134134
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
135135
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
136+
_prefs->radio_fem_txgain = constrain(_prefs->radio_fem_txgain, 0, 1); // boolean
136137
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
137138

138139
file.close();
@@ -562,6 +563,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
562563
} else {
563564
strcpy(reply, "Error: state must be on or off");
564565
}
566+
} else if (memcmp(config, "radio.fem.txgain ", 17) == 0) {
567+
if (!_board->canControlLoRaFemPaGain()) {
568+
strcpy(reply, "Error: unsupported");
569+
} else if (memcmp(&config[17], "on", 2) == 0) {
570+
if (_board->setLoRaFemPaGainEnabled(true)) {
571+
_prefs->radio_fem_txgain = 1;
572+
savePrefs();
573+
strcpy(reply, "OK - LoRa FEM TX gain on");
574+
} else {
575+
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
576+
}
577+
} else if (memcmp(&config[17], "off", 3) == 0) {
578+
if (_board->setLoRaFemPaGainEnabled(false)) {
579+
_prefs->radio_fem_txgain = 0;
580+
savePrefs();
581+
strcpy(reply, "OK - LoRa FEM TX gain off");
582+
} else {
583+
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
584+
}
585+
} else {
586+
strcpy(reply, "Error: state must be on or off");
587+
}
565588
} else if (memcmp(config, "radio ", 6) == 0) {
566589
strcpy(tmp, &config[6]);
567590
const char *parts[4];
@@ -827,6 +850,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
827850
} else {
828851
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
829852
}
853+
} else if (memcmp(config, "radio.fem.txgain", 16) == 0) {
854+
if (!_board->canControlLoRaFemPaGain()) {
855+
strcpy(reply, "Error: unsupported");
856+
} else {
857+
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
858+
}
830859
} else if (memcmp(config, "radio", 5) == 0) {
831860
char freq[16], bw[16];
832861
strcpy(freq, StrHelper::ftoa(_prefs->freq));

src/helpers/CommonCLI.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class NodePrefs : public ConfigSerializer {
6565
char owner_info[120];
6666
uint8_t rx_boosted_gain = 0; // power settings
6767
uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting
68+
uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain setting
6869
uint8_t path_hash_mode = 0; // which path mode to use when sending
6970
uint8_t loop_detect = 0;
7071
uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean)
@@ -83,7 +84,8 @@ class NodePrefs : public ConfigSerializer {
8384
def(radio_cad_key, _parent->cad_enabled);
8485
def("int_thr", _parent->interference_threshold);
8586
def("rxgain", _parent->rx_boosted_gain);
86-
def("fem_rxgain", _parent->rx_boosted_gain);
87+
def("fem_rxgain", _parent->radio_fem_rxgain);
88+
def("fem_txgain", _parent->radio_fem_txgain);
8789
def("tx", _parent->tx_power_dbm);
8890
def("af", _parent->airtime_factor);
8991
def("rxdelay", _parent->rx_delay_base);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <cstdio>
4+
#include <cstring>
5+
#include <string>
6+
7+
#include "../../examples/companion_radio/NodePrefs.h"
8+
9+
class ReplayStream : public Stream {
10+
const char* _text;
11+
int _pos = 0;
12+
int _len;
13+
14+
public:
15+
explicit ReplayStream(const char* text) : _text(text), _len(strlen(text)) { }
16+
17+
int available() override { return _len - _pos; }
18+
int read() override { return _pos < _len ? _text[_pos++] : -1; }
19+
int peek() override { return _pos < _len ? _text[_pos] : -1; }
20+
};
21+
22+
class CaptureStream : public Stream {
23+
std::string _text;
24+
25+
size_t emit(long long value) {
26+
char text[24];
27+
int length = snprintf(text, sizeof(text), "%lld", value);
28+
return write(reinterpret_cast<const uint8_t*>(text), length);
29+
}
30+
31+
public:
32+
size_t write(uint8_t value) override {
33+
_text.push_back(static_cast<char>(value));
34+
return 1;
35+
}
36+
37+
size_t write(const uint8_t* buffer, size_t size) override {
38+
_text.append(reinterpret_cast<const char*>(buffer), size);
39+
return size;
40+
}
41+
42+
size_t print(unsigned char value, int = DEC) override { return emit(value); }
43+
size_t print(int value, int = DEC) override { return emit(value); }
44+
size_t print(unsigned int value, int = DEC) override { return emit(value); }
45+
size_t print(long value, int = DEC) override { return emit(value); }
46+
size_t print(unsigned long value, int = DEC) override { return emit(value); }
47+
size_t print(long long value, int = DEC) override { return emit(value); }
48+
size_t print(unsigned long long value, int = DEC) override { return emit(value); }
49+
50+
const std::string& text() const { return _text; }
51+
};
52+
53+
TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {
54+
NodePrefs saved;
55+
saved.rx_boosted_gain = 0;
56+
saved.radio_fem_rxgain = 1;
57+
saved.radio_fem_txgain = 0;
58+
59+
CaptureStream output;
60+
ASSERT_TRUE(saved.saveSerial(output));
61+
EXPECT_NE(std::string::npos, output.text().find("rxgain:0"));
62+
EXPECT_NE(std::string::npos, output.text().find("fem_rxgain:1"));
63+
EXPECT_NE(std::string::npos, output.text().find("fem_txgain:0"));
64+
65+
ReplayStream input("{radio:{rxgain:1,fem_rxgain:0,fem_txgain:1}}");
66+
NodePrefs loaded;
67+
loaded.rx_boosted_gain = 0;
68+
loaded.radio_fem_rxgain = 1;
69+
loaded.radio_fem_txgain = 0;
70+
71+
ASSERT_TRUE(loaded.loadSerial(input));
72+
EXPECT_EQ(1, loaded.rx_boosted_gain);
73+
EXPECT_EQ(0, loaded.radio_fem_rxgain);
74+
EXPECT_EQ(1, loaded.radio_fem_txgain);
75+
}
76+
77+
int main(int argc, char** argv) {
78+
::testing::InitGoogleTest(&argc, argv);
79+
return RUN_ALL_TESTS();
80+
}

0 commit comments

Comments
 (0)