mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-01 19:18:17 +00:00
Add runtime FEM control and fix full Companion boot
This commit is contained in:
@@ -87,8 +87,8 @@ Commands:
|
||||
build-full-esp32-firmwares: Build only feature-complete ESP32 MQTT profiles with up to 254 neighbors, LoRa OTA, and expanded dual-OTA partitions.
|
||||
build-full-esp32-logging-firmwares: Build only feature-complete ESP32 profiles with up to 254 neighbors, logging, MQTT disabled, LoRa OTA, and expanded dual-OTA partitions.
|
||||
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
|
||||
build-companion-firmwares: Build all companion firmwares for all build targets.
|
||||
build-full-companion-firmwares: Build all full Companion firmwares for supported ESP32 and nRF52 targets.
|
||||
build-companion-firmwares: Build canonical companion firmwares; legacy _femoff targets remain available as direct builds.
|
||||
build-full-companion-firmwares: Build canonical full Companion firmwares for supported ESP32 and nRF52 targets.
|
||||
build-repeater-firmwares: Build all repeater firmwares with 254 neighbors, except DRAM-limited targets that retain 50.
|
||||
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
|
||||
build-sensor-firmwares: Build all sensor firmwares for all build targets.
|
||||
@@ -474,12 +474,12 @@ prompt_for_build_mode() {
|
||||
"Build all firmwares"
|
||||
"Build all firmwares in 5 profiles (standard, logging, MQTT, full ESP32 MQTT, full ESP32 logging without MQTT)"
|
||||
"Build all repeater firmwares"
|
||||
"Build all companion firmwares"
|
||||
"Build canonical companion firmwares (FEM RX gain is runtime configurable)"
|
||||
"Build all chat room server firmwares"
|
||||
"Build all sensor firmwares"
|
||||
"Build only FULL ESP32 MQTT firmwares (all features, MQTT, and LoRa OTA)"
|
||||
"Build only FULL ESP32 logging firmwares (all features, logging, no MQTT, and LoRa OTA)"
|
||||
"Build all full Companion firmwares (all available transports and host-backed LoRa OTA)"
|
||||
"Build canonical full Companion firmwares (runtime FEM control and host-backed LoRa OTA)"
|
||||
)
|
||||
|
||||
echo "No command provided. Select a build action:"
|
||||
@@ -2695,8 +2695,23 @@ resolve_all_firmwares() {
|
||||
get_supported_pio_envs
|
||||
}
|
||||
|
||||
is_legacy_companion_femoff_target() {
|
||||
case "${1,,}" in
|
||||
*companion_radio_*_femoff)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_companion_firmwares() {
|
||||
get_pio_envs_for_variant_role companion
|
||||
local env_name
|
||||
|
||||
while IFS= read -r env_name; do
|
||||
if ! is_legacy_companion_femoff_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done < <(get_pio_envs_for_variant_role companion)
|
||||
}
|
||||
|
||||
resolve_full_companion_firmwares() {
|
||||
@@ -2705,6 +2720,9 @@ resolve_full_companion_firmwares() {
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if is_supported_build_env "$env_name" \
|
||||
&& is_companion_radio_full_target "$env_name"; then
|
||||
if is_legacy_companion_femoff_target "$env_name"; then
|
||||
continue
|
||||
fi
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -39,7 +39,7 @@ bash build.sh build-firmware RAK_4631_companion_radio_full \
|
||||
--firmware-version v1.17.0
|
||||
```
|
||||
|
||||
To build every available full Companion target, select the corresponding
|
||||
To build every canonical full Companion target, select the corresponding
|
||||
interactive menu item or run:
|
||||
|
||||
```bash
|
||||
@@ -47,6 +47,20 @@ bash build.sh build-full-companion-firmwares \
|
||||
--firmware-version v1.17.0
|
||||
```
|
||||
|
||||
Heltec `_femon` Companion firmware can switch the external FEM receive gain at
|
||||
runtime, so the Companion bulk-build commands omit the redundant legacy
|
||||
`_femoff` targets. Those targets remain available through an explicit
|
||||
`build-firmware` command for compatibility. In WebConfig, use the **FEM RX
|
||||
boost** switch. From the USB terminal, use:
|
||||
|
||||
```text
|
||||
get radio.fem.rxgain
|
||||
set radio.fem.rxgain off
|
||||
set radio.fem.rxgain on
|
||||
```
|
||||
|
||||
The selected state is applied immediately and retained after reboot.
|
||||
|
||||
Artifacts are written to `out/` by default.
|
||||
|
||||
On 4 MB ESP32 boards, the full target uses a single 3 MB application
|
||||
|
||||
@@ -84,6 +84,16 @@ set af {air-time-factor}
|
||||
```
|
||||
Sets the transmit air-time-factor. Deprecated - use `set dutycycle` instead.
|
||||
|
||||
```
|
||||
get radio.fem.rxgain
|
||||
set radio.fem.rxgain {on|off}
|
||||
```
|
||||
Shows or changes the external FEM receive-path LNA on supported Heltec boards.
|
||||
The change is applied immediately, saved across reboots, and recalibrates the
|
||||
radio noise floor. This is separate from the SX126x `radio.rxgain` setting.
|
||||
Boards whose FEM receive path cannot be controlled report the setting as
|
||||
unsupported.
|
||||
|
||||
|
||||
```
|
||||
time {epoch-secs}
|
||||
|
||||
@@ -2019,17 +2019,10 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
strcpy(reply, "Error: must be on or off");
|
||||
} else if (!board.canControlLoRaFemLna()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (!applyAndSaveFemRxGain(enabled)) {
|
||||
strcpy(reply, "Error: failed to apply FEM RX gain");
|
||||
} else {
|
||||
bool changed = board.isLoRaFemLnaEnabled() != enabled;
|
||||
if (!board.setLoRaFemLnaEnabled(enabled)) {
|
||||
strcpy(reply, "Error: failed to apply FEM RX gain");
|
||||
} else {
|
||||
if (changed) _radio->recalibrateNoiseFloor();
|
||||
_prefs.radio_fem_rxgain = enabled;
|
||||
_prefs.radio_fem_rxgain_override = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3215,12 +3208,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (!board.canControlLoRaFemLna()) {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
} else if (value <= 1) {
|
||||
bool changed = board.isLoRaFemLnaEnabled() != (value != 0);
|
||||
if (board.setLoRaFemLnaEnabled(value != 0)) {
|
||||
if (changed) _radio->recalibrateNoiseFloor();
|
||||
_prefs.radio_fem_rxgain = value;
|
||||
_prefs.radio_fem_rxgain_override = 1;
|
||||
savePrefs();
|
||||
if (applyAndSaveFemRxGain(value != 0)) {
|
||||
writeOKFrame();
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
@@ -3428,6 +3416,19 @@ void MyMesh::scheduleContactWriteAfterRelease(const ContactInfo& contact) {
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
|
||||
if (!board.canControlLoRaFemLna()) return false;
|
||||
|
||||
const bool changed = board.isLoRaFemLnaEnabled() != enabled;
|
||||
if (!board.setLoRaFemLnaEnabled(enabled)) return false;
|
||||
|
||||
if (changed) _radio->recalibrateNoiseFloor();
|
||||
_prefs.radio_fem_rxgain = enabled ? 1 : 0;
|
||||
_prefs.radio_fem_rxgain_override = 1;
|
||||
savePrefs();
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_USB_INTERFACE
|
||||
void MyMesh::enterTerminalMode() {
|
||||
_terminal_mode = true;
|
||||
@@ -4087,6 +4088,13 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
}
|
||||
} else if (strncmp(command, "import ", 7) == 0) {
|
||||
importTerminalCard(command + 7);
|
||||
} else if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!board.canControlLoRaFemLna()) {
|
||||
Serial.print(" ERROR: FEM RX gain control is unsupported on this board\r\n");
|
||||
} else {
|
||||
Serial.printf(" FEM RX gain: %s\r\n",
|
||||
board.isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
} else if (strncmp(command, "set ", 4) == 0) {
|
||||
const char* config = command + 4;
|
||||
if (strncmp(config, "af ", 3) == 0) {
|
||||
@@ -4113,6 +4121,20 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
_prefs.freq = constrain((float)atof(config + 5), 150.0f, 2500.0f);
|
||||
savePrefs();
|
||||
Serial.print(" OK - reboot to apply\r\n");
|
||||
} else if (strncmp(config, "radio.fem.rxgain", 16) == 0
|
||||
&& (config[16] == 0 || config[16] == ' '
|
||||
|| config[16] == '\t')) {
|
||||
const char* value = config + 16;
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
Serial.print(" ERROR: use set radio.fem.rxgain <on|off>\r\n");
|
||||
} else if (!board.canControlLoRaFemLna()) {
|
||||
Serial.print(" ERROR: FEM RX gain control is unsupported on this board\r\n");
|
||||
} else if (!applyAndSaveFemRxGain(strcmp(value, "on") == 0)) {
|
||||
Serial.print(" ERROR: failed to apply FEM RX gain\r\n");
|
||||
} else {
|
||||
Serial.printf(" OK - FEM RX gain %s\r\n", value);
|
||||
}
|
||||
} else {
|
||||
Serial.printf(" ERROR: unknown setting: %s\r\n", config);
|
||||
}
|
||||
@@ -4122,6 +4144,8 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
} else if (strcmp(command, "help") == 0) {
|
||||
Serial.print("Commands:\r\n");
|
||||
Serial.print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
|
||||
Serial.print(" get radio.fem.rxgain\r\n");
|
||||
Serial.print(" set radio.fem.rxgain <on|off>\r\n");
|
||||
Serial.print(" card\r\n");
|
||||
Serial.print(" import <meshcore://card>\r\n");
|
||||
Serial.print(" clock\r\n");
|
||||
|
||||
@@ -272,6 +272,7 @@ private:
|
||||
|
||||
void checkCLIRescueCmd();
|
||||
void checkSerialInterface();
|
||||
bool applyAndSaveFemRxGain(bool enabled);
|
||||
#ifdef ENABLE_USB_INTERFACE
|
||||
ContactInfo* getTerminalRecipient();
|
||||
void printTerminalPath(const ContactInfo& recipient);
|
||||
|
||||
@@ -590,10 +590,15 @@ void setup() {
|
||||
WIFI_DEBUG_PRINTLN("%s", web_reply);
|
||||
}
|
||||
#endif
|
||||
// Disable WiFi modem power-save after starting WebConfig: that startup may
|
||||
// restore a saved ESP-IDF WiFi power policy. Companion radios must keep
|
||||
// modem sleep disabled because its pauses can stall SX1262 SPI/DIO service.
|
||||
// ESP-IDF requires WiFi modem sleep when Bluetooth is active. Full ESP32
|
||||
// companions provide both transports, so use the lowest sleep policy there;
|
||||
// WiFi-only companions keep modem sleep disabled to avoid pauses in SX1262
|
||||
// SPI/DIO service.
|
||||
#if defined(BLE_PIN_CODE)
|
||||
WiFi.setSleep(true);
|
||||
#else
|
||||
WiFi.setSleep(false);
|
||||
#endif
|
||||
wifi_interface.begin(TCP_PORT);
|
||||
interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
|
||||
#ifdef ENABLE_OTA
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace wifi {
|
||||
|
||||
// MeshCore stores WiFi power save as: 0=min, 1=none, 2=max. ESP32 WiFi and
|
||||
// Bluetooth coexistence requires modem sleep, so "none" must fall back to the
|
||||
// minimum sleep policy whenever Bluetooth is active.
|
||||
inline uint8_t effectivePowerSave(uint8_t configured,
|
||||
bool bluetooth_active) {
|
||||
if (configured > 2) configured = 1;
|
||||
return bluetooth_active && configured == 1 ? 0 : configured;
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace mesh
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "../MQTTRuntimeBufferLifecycle.h"
|
||||
#include "../MQTTTopicRouter.h"
|
||||
#include "../TxtDataHelpers.h"
|
||||
#include "../WiFiPowerSave.h"
|
||||
#include <NTPClient.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <Timezone.h>
|
||||
@@ -2561,7 +2562,14 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
_wifi_reconnect_backoff_attempt = 0;
|
||||
#ifdef ESP_PLATFORM
|
||||
wifi_ps_type_t ps_mode;
|
||||
uint8_t ps_pref = _obs->wifi_power_save;
|
||||
uint8_t ps_pref = mesh::wifi::effectivePowerSave(
|
||||
_obs->wifi_power_save,
|
||||
#if defined(BLE_PIN_CODE) && defined(WIFI_SSID)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
);
|
||||
if (ps_pref == 1) {
|
||||
ps_mode = WIFI_PS_NONE;
|
||||
} else if (ps_pref == 2) {
|
||||
|
||||
@@ -26,6 +26,20 @@
|
||||
#include "WebConfigHtml.h"
|
||||
#include "helpers/CLICommandUtils.h"
|
||||
#include "helpers/WebConfigKeys.h"
|
||||
#include "helpers/WiFiPowerSave.h"
|
||||
|
||||
static bool bluetoothWiFiCoexistenceRequired() {
|
||||
#if defined(BLE_PIN_CODE) && defined(WIFI_SSID)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t effectiveWiFiPowerSave(uint8_t configured) {
|
||||
return mesh::wifi::effectivePowerSave(
|
||||
configured, bluetoothWiFiCoexistenceRequired());
|
||||
}
|
||||
|
||||
// Placeholder sent instead of stored secrets; POSTs carrying it are dropped
|
||||
// so an untouched password field never overwrites the stored value.
|
||||
@@ -106,6 +120,7 @@ WebConfigServer::WebConfigServer(Callbacks* callbacks, void* mqtt_prefs, bool ow
|
||||
loadStandaloneWiFi(_wifi_ssid, sizeof(_wifi_ssid),
|
||||
_wifi_password, sizeof(_wifi_password), &_wifi_power_save);
|
||||
}
|
||||
_wifi_power_save = effectiveWiFiPowerSave(_wifi_power_save);
|
||||
}
|
||||
|
||||
WebConfigServer::~WebConfigServer() {
|
||||
@@ -165,12 +180,13 @@ bool WebConfigServer::loadStandaloneWiFi(char* ssid, size_t ssid_len,
|
||||
ssid[ssid_len - 1] = 0;
|
||||
strncpy(password, stored_password.c_str(), password_len - 1);
|
||||
password[password_len - 1] = 0;
|
||||
if (power_save) *power_save = stored_ps <= 2 ? stored_ps : 1;
|
||||
if (power_save) *power_save = effectiveWiFiPowerSave(stored_ps);
|
||||
return stored_ssid.length() != 0;
|
||||
}
|
||||
|
||||
bool WebConfigServer::saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
uint8_t power_save) {
|
||||
power_save = effectiveWiFiPowerSave(power_save);
|
||||
if (!ssid || !ssid[0] || strlen(ssid) >= 32
|
||||
|| (password && strlen(password) >= 64) || power_save > 2) {
|
||||
return false;
|
||||
@@ -236,6 +252,11 @@ bool WebConfigServer::setStandaloneWiFiPowerSave(const char* value, char* reply,
|
||||
snprintf(reply, reply_len, "Error: power save must be none, min, or max");
|
||||
return false;
|
||||
}
|
||||
if (effectiveWiFiPowerSave(power_save) != power_save) {
|
||||
snprintf(reply, reply_len,
|
||||
"Error: power save none is unavailable while Bluetooth is active");
|
||||
return false;
|
||||
}
|
||||
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) {
|
||||
@@ -542,6 +563,7 @@ bool WebConfigServer::startAutoMode(char reply[]) {
|
||||
_setup_reconnect_deadline = 0;
|
||||
_wifi_reconnect_tracker.noteDisconnected(millis());
|
||||
WiFi.begin(_wifi_ssid, _wifi_password);
|
||||
_wifi_power_save = effectiveWiFiPowerSave(_wifi_power_save);
|
||||
wifi_ps_type_t ps_mode = _wifi_power_save == 1 ? WIFI_PS_NONE
|
||||
: _wifi_power_save == 2 ? WIFI_PS_MAX_MODEM
|
||||
: WIFI_PS_MIN_MODEM;
|
||||
@@ -917,7 +939,11 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
}
|
||||
} else if ((_mqtt_prefs == NULL || !_owns_wifi) && strcmp(e.key, "wifi.powersave") == 0) {
|
||||
if (strcmp(value, "min") == 0) _wifi_power_save = 0;
|
||||
else if (strcmp(value, "none") == 0) _wifi_power_save = 1;
|
||||
else if (strcmp(value, "none") == 0
|
||||
&& bluetoothWiFiCoexistenceRequired()) {
|
||||
strcpy(e.reply,
|
||||
"Error: power save none is unavailable while Bluetooth is active");
|
||||
} else if (strcmp(value, "none") == 0) _wifi_power_save = 1;
|
||||
else if (strcmp(value, "max") == 0) _wifi_power_save = 2;
|
||||
else strcpy(e.reply, "Error: must be none, min, or max");
|
||||
if (e.reply[0] == 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/WiFiPowerSave.h>
|
||||
|
||||
TEST(CLICommandUtils, NormalizesCapitalizedSetVerb) {
|
||||
char command[] = "Set altpath 600000,0d2784,F8DADA";
|
||||
@@ -291,6 +292,15 @@ TEST(CLICommandUtils, ValidatesStandaloneWiFiValues) {
|
||||
EXPECT_FALSE(mesh::cli::parseStandaloneWiFiPowerSave("off", power_save));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, EnforcesWiFiSleepForBluetoothCoexistence) {
|
||||
EXPECT_EQ(0, mesh::wifi::effectivePowerSave(0, true));
|
||||
EXPECT_EQ(0, mesh::wifi::effectivePowerSave(1, true));
|
||||
EXPECT_EQ(2, mesh::wifi::effectivePowerSave(2, true));
|
||||
EXPECT_EQ(1, mesh::wifi::effectivePowerSave(1, false));
|
||||
EXPECT_EQ(0, mesh::wifi::effectivePowerSave(99, true));
|
||||
EXPECT_EQ(1, mesh::wifi::effectivePowerSave(99, false));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, MatchesDiscoverNeighborsWithoutPrefixCollisions) {
|
||||
using mesh::cli::NoArgCommandMatch;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user