Add WiFi, ESP-NOW and combined wireless CLI controls

This commit is contained in:
mikecarper committed 2026-09-15 00:01:45 -07:00
1 parent 975167938d
commit e9a2326c4f
25 files changed
+1311 -61

No files matched your search

+5
View File
@@ -128,6 +128,11 @@ jobs:
- name: Verify Companion Bluetooth CLI connection protection
run: python3 -B test/test_companion_bluetooth_control.py -v
- name: Verify WiFi, ESP-NOW and combined wireless controls
run: |
python3 -B test/test_wireless_control.py -v
python3 -B test/test_espnow_radio_lifecycle.py -v
- name: Verify message reader buttons, touch targets, and footer layouts
working-directory: test
run: >-
+54
View File
@@ -1410,6 +1410,60 @@ Station G2/G3 targets default to `off`.
---
#### Turn WiFi, ESP-NOW, or all 2.4 GHz services on or off
```text
get wifi
set wifi off
set wifi on
get espnow
set espnow off
set espnow on
get 2.4ghz
set 2.4ghz off
set 2.4ghz on
set 2.4ghz on all
```
| Control | Effect |
| --- | --- |
| `wifi` | Infrastructure WiFi, setup AP, TCP/WebUI, and WiFi MQTT services. ESP-NOW remains available. |
| `espnow` | The ESP-NOW bridge or primary ESP-NOW mesh transport in this build. Infrastructure WiFi remains available. |
| `2.4ghz off` | Stop WiFi, Bluetooth, and ESP-NOW; remember which were enabled. LoRa and wired transports remain available. |
| `2.4ghz on` | Restore the states saved by the first `off`. Repeated `off` preserves that snapshot. With no previous `off`, keep the current states. |
| `2.4ghz on all` | Enable every service available in this build and boot. `2.4ghz on force` is an alias. |
These commands work in Companion, repeater, and room-server firmware where the
services are available. Changes last until reboot; saved WiFi credentials,
Bluetooth preferences, and bridge settings stay intact. `get 2.4ghz` lists each
service as `on`, `off`, or `unavailable`. An nRF52 Companion controls its Bluetooth
and reports WiFi/ESP-NOW as unavailable. Exclusive WiFi/Bluetooth builds retain
their boot transport restriction; `on all` cannot reclaim a released Bluetooth stack.
An `off` command requires a management connection that will survive it. Bluetooth
can provide the fallback for `wifi off`; USB, Ethernet, or a CLI request over LoRa
can provide it for `2.4ghz off`. WiFi association, a listening socket, and USB power
alone do not count. On USB hardware without an observable open-client session,
send the command through USB itself. To bypass the guard:
```text
set wifi off force
set espnow off force
set 2.4ghz off force
```
Shutdown starts after the reply has had time to drain (normally 250 ms, up to two
seconds for pending transport I/O). Network services stop before the shared WiFi
driver. A normal shutdown is cancelled if its fallback connection disappears
before it starts. The `get` commands report pending changes and startup/teardown
failures. After `2.4ghz off`, use `2.4ghz on` or `on all` before enabling an
individual service. The `all`/`force` suffix on **on** applies only to `2.4ghz`.
WiFi and ESP-NOW share one hardware channel. A running ESP-NOW bridge pins setup
and station connections to `bridge.channel`; starting the bridge alongside an
existing WiFi connection requires that same channel. A mismatch reports a failed
start and preserves the WiFi connection.
#### Turn Bluetooth on or off (Companion)
```text
+132
View File
@@ -0,0 +1,132 @@
#pragma once
// Included by the infrastructure main files after the mesh and transport objects.
#include <helpers/WirelessControl.h>
#if defined(ESP32) && (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || (defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW))
#include <WiFi.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#endif
class InfrastructureWirelessBackend : public mesh::wireless::Backend {
bool wifi_saved_ = false, resume_web_ = false, resume_mqtt_ = false;
public:
uint8_t available() const override {
uint8_t mask = 0;
#if defined(ESP32) && (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE))
mask |= mesh::wireless::WiFi;
#endif
#if defined(WITH_ESPNOW_BRIDGE) || (defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW)
mask |= mesh::wireless::EspNow;
#endif
return mask;
}
uint8_t enabled() const override {
uint8_t mask = 0;
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigActive()) mask |= mesh::wireless::WiFi;
#endif
#ifdef WITH_MQTT_BRIDGE
if (the_mesh.isBridgeRunning()) mask |= mesh::wireless::WiFi;
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (the_mesh.isBridgeRunning()) mask |= mesh::wireless::EspNow;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (radio_driver.isEnabled()) mask |= mesh::wireless::EspNow;
#endif
return mask;
}
uint8_t clients() const override {
uint8_t mask = 0;
#ifdef ETHERNET_ENABLED
if (ethernet_client.connected()) mask |= mesh::wireless::Independent;
#endif
#ifdef WITH_WEBCONFIG
if (the_mesh.hasWirelessNetworkClient()) mask |= mesh::wireless::WiFi;
#endif
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) \
|| (defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& (!defined(ARDUINO_USB_MODE) || ARDUINO_USB_MODE == 0))
if (board.isUsbDataConnected()) mask |= mesh::wireless::Independent;
#endif
return mask;
}
mesh::wireless::Result set(uint8_t service, bool on) override {
using mesh::wireless::Result;
#if defined(ESP32) && (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE))
if (service == mesh::wireless::WiFi) {
if (!on) {
if (!wifi_saved_) {
#ifdef WITH_WEBCONFIG
resume_web_ = the_mesh.isWebConfigActive();
#endif
#ifdef WITH_MQTT_BRIDGE
resume_mqtt_ = the_mesh.isBridgeRunning();
#endif
wifi_saved_ = true;
}
#ifdef WITH_MQTT_BRIDGE
if (!the_mesh.setBridgeState(false)) return Result::Failed;
#endif
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigActive()) {
char reply[160];
the_mesh.stopWebConfig(reply);
return Result::Pending;
}
#endif
WiFi.setAutoReconnect(false);
if (enabled() & mesh::wireless::EspNow) {
WiFi.disconnect(false, false);
WiFi.mode(WIFI_STA);
mesh::wifi::applyProtocolMask(WIFI_IF_STA);
mesh::wifi::restoreEspNowChannel();
} else
{
WiFi.disconnect(true, false);
WiFi.mode(WIFI_OFF);
}
return Result::Done;
}
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigStopping()) return Result::Pending;
if (resume_web_ || (!wifi_saved_ && !(enabled() & mesh::wireless::WiFi))
|| (wifi_saved_ && !resume_web_ && !resume_mqtt_)) {
char reply[160];
if (!the_mesh.startWebConfig(false, reply)) return Result::Failed;
}
#endif
#ifdef WITH_MQTT_BRIDGE
if (resume_mqtt_
#ifndef WITH_WEBCONFIG
|| !(enabled() & mesh::wireless::WiFi)
#endif
) {
if (!the_mesh.setBridgeState(true)) return Result::Failed;
}
#endif
wifi_saved_ = resume_web_ = resume_mqtt_ = false;
return (enabled() & mesh::wireless::WiFi) ? Result::Done : Result::Failed;
}
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (service == mesh::wireless::EspNow)
return the_mesh.setBridgeState(on) ? Result::Done : Result::Failed;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (service == mesh::wireless::EspNow) {
if (on) radio_driver.init();
else {
radio_driver.end();
if (!(enabled() & mesh::wireless::WiFi)) {
WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_OFF);
}
}
return radio_driver.isEnabled() == on ? Result::Done : Result::Failed;
}
#endif
return Result::Failed;
}
};
static InfrastructureWirelessBackend infrastructure_wireless;
@@ -0,0 +1,7 @@
#pragma once
#include <stddef.h>
#include <helpers/WirelessControl.h>
enum class CompanionWirelessSource { Other, Usb, Network, Framed };
bool handleCompanionWirelessCommand(const char* command, char* reply, size_t size,
CompanionWirelessSource source = CompanionWirelessSource::Other);
+10 -2
View File
@@ -1,6 +1,7 @@
#include <helpers/ui/DisplayPowerSettings.h>
#include "MyMesh.h"
#include "CompanionBluetooth.h"
#include "CompanionWireless.h"
#if defined(MESH_SOAK_DIAGNOSTICS)
#include "../../tools/hil/S3SoakDiagnostics.h"
#endif
@@ -2303,6 +2304,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
while (*command == ' ') command++;
if (handleCompanionBluetoothCommand(command, reply, reply_size)) return true;
if (handleCompanionWirelessCommand(command, reply, reply_size)) return true;
if (strcmp(command, "get powersaving") == 0 || strcmp(command, "powersaving") == 0) {
snprintf(reply, reply_size, "> %s", _prefs.powersaving_enabled ? "on" : "off");
@@ -5334,7 +5336,8 @@ void MyMesh::handleCmdFrame(size_t len) {
char command[MAX_FRAME_SIZE] = {0};
memcpy(command, &cmd_frame[1], command_len);
char reply[MAX_FRAME_SIZE] = {0};
if (!handleCompanionBluetoothCommand(command, reply, sizeof(reply),
if (!handleCompanionWirelessCommand(command, reply, sizeof(reply), CompanionWirelessSource::Framed)
&& !handleCompanionBluetoothCommand(command, reply, sizeof(reply),
CompanionBluetoothCommandSource::Framed)
&& !handleLocalControlCommand(command, reply, sizeof(reply))) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
@@ -7497,7 +7500,9 @@ void MyMesh::handleTerminalCommand(char* command) {
#endif
char local_reply[160];
if (handleCompanionBluetoothCommand(command, local_reply, sizeof(local_reply),
if (handleCompanionWirelessCommand(command, local_reply, sizeof(local_reply),
_terminal_mode ? CompanionWirelessSource::Usb : CompanionWirelessSource::Network)
|| handleCompanionBluetoothCommand(command, local_reply, sizeof(local_reply),
CompanionBluetoothCommandSource::Terminal)) {
terminalOutput().printf(" %s\r\n", local_reply);
return;
@@ -7990,6 +7995,9 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" get display.touch\r\n");
terminalOutput().print(" set display.touch <on|off> (this boot only)\r\n");
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
terminalOutput().print(" get wifi|espnow|2.4ghz\r\n");
terminalOutput().print(" set wifi|espnow on|off [force] (force only with off)\r\n");
terminalOutput().print(" set 2.4ghz on [all|force]|off [force] (this boot)\r\n");
terminalOutput().print(" get bluetooth.name\r\n");
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
#if defined(BLE_PIN_CODE)
+143 -2
View File
@@ -3,6 +3,7 @@
#include <helpers/BluetoothMac.h>
#include "MyMesh.h"
#include "CompanionBluetooth.h"
#include "CompanionWireless.h"
#include "CompanionWiFi.h"
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
@@ -1490,6 +1491,7 @@ void halt() {
#endif
void requestCompanionWiFiSetup() {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return;
companion_wifi_setup_stop_requested = false;
companion_wifi_setup_requested = true;
}
@@ -1500,6 +1502,7 @@ void halt() {
}
bool toggleCompanionWiFi() {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return false;
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE)
const CompanionTransportMode current = getCompanionTransportMode();
const CompanionTransportMode selected =
@@ -1798,7 +1801,8 @@ void halt() {
ota_console_client.print("> ");
#else
char reply[160]; reply[0] = 0;
if (!handleCompanionBluetoothCommand(ota_console_line, reply, sizeof(reply),
if (!handleCompanionWirelessCommand(ota_console_line, reply, sizeof(reply), CompanionWirelessSource::Network)
&& !handleCompanionBluetoothCommand(ota_console_line, reply, sizeof(reply),
CompanionBluetoothCommandSource::Terminal)
&& !the_mesh.handleLocalCommand(ota_console_line, reply)
&& !mesh::ota::handle_ota_command(ota_console_line, reply, board))
@@ -1853,6 +1857,7 @@ void halt() {
}
static void startCompanionWiFi() {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return;
if (companion_wifi_active) return;
board.setInhibitSleep(true);
@@ -2326,6 +2331,10 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
return true;
}
if (action == CompanionBluetoothCommand::On) {
if (!mesh::wireless::control().allowService(mesh::wireless::Bluetooth)) {
snprintf(reply, reply_size, "Error: 2.4ghz is off or changing; use set 2.4ghz on first");
return true;
}
companion_bluetooth_off_at = 0;
interface_manager.enableBluetooth();
snprintf(reply, reply_size, "%s", interface_manager.isBluetoothEnabled()
@@ -2375,7 +2384,137 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
}
#endif
class CompanionWirelessBackend : public mesh::wireless::Backend {
public:
uint8_t available() const override {
uint8_t mask = 0;
#if defined(ESP32) && defined(WIFI_SSID)
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE)
if (companionTransportWiFiActiveAtBoot())
#endif
mask |= mesh::wireless::WiFi;
#endif
#if defined(BLE_PIN_CODE)
if (companion_bluetooth_initialized) mask |= mesh::wireless::Bluetooth;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
mask |= mesh::wireless::EspNow;
#endif
return mask;
}
uint8_t enabled() const override {
uint8_t mask = 0;
#if defined(ESP32) && defined(WIFI_SSID)
if (companion_wifi_requested || companion_wifi_active) mask |= mesh::wireless::WiFi;
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigActiveOrStopping()) mask |= mesh::wireless::WiFi;
#endif
#endif
#if defined(BLE_PIN_CODE)
if (interface_manager.isBluetoothEnabled()) mask |= mesh::wireless::Bluetooth;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (radio_driver.isEnabled()) mask |= mesh::wireless::EspNow;
#endif
return mask;
}
uint8_t clients() const override {
using namespace mesh::wireless;
if (!interface_manager.isEnabled()) return 0;
uint8_t mask = 0;
if (interface_manager.isInterfaceConnected(InterfaceType::Bluetooth)) mask |= Bluetooth;
if (interface_manager.isInterfaceConnected(InterfaceType::WiFi)) mask |= mesh::wireless::WiFi;
if (interface_manager.isInterfaceConnected(InterfaceType::Ethernet)) mask |= Independent;
#if COMPANION_FEATURE_TEXT_TERMINAL && (COMPANION_FEATURE_NETWORK_TERMINAL || defined(WITH_WEBCONFIG))
if (the_mesh.isAnyNetworkTerminalMode()) mask |= mesh::wireless::WiFi;
#endif
#if defined(ENABLE_USB_INTERFACE) \
&& (defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) \
|| (defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& (!defined(ARDUINO_USB_MODE) || ARDUINO_USB_MODE == 0)))
#if COMPANION_FEATURE_USB_MOTA_SOURCE
if (!usb_mota_mode)
#endif
if (usb_serial_interface.isEnabled() && isUsbTerminalDataConnected()) mask |= Independent;
#endif
return mask;
}
bool replyBusy() const override {
return interface_manager.hasPendingIO();
}
mesh::wireless::Result set(uint8_t service, bool on) override {
using namespace mesh::wireless;
#if defined(BLE_PIN_CODE)
if (service == Bluetooth) {
companion_bluetooth_off_at = 0;
if (on) interface_manager.enableBluetooth();
else disableCompanionBluetoothForCli();
return interface_manager.isBluetoothEnabled() == on ? Result::Done : Result::Failed;
}
#endif
#if defined(ESP32) && defined(WIFI_SSID)
if (service == mesh::wireless::WiFi) {
companion_wifi_requested = on;
if (!on) {
companion_wifi_setup_requested = false;
if (!finishStoppingCompanionWiFi()) return Result::Pending;
companion_wifi_disable_in_progress = false;
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (!radio_driver.isEnabled()) ::WiFi.mode(WIFI_OFF);
#endif
} else {
// Finish a previously requested teardown before opening new sockets.
if (companion_wifi_disable_in_progress) {
if (!finishStoppingCompanionWiFi()) return Result::Pending;
companion_wifi_disable_in_progress = false;
}
startCompanionWiFi();
}
return companion_wifi_active == on ? Result::Done : Result::Failed;
}
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (service == EspNow) {
if (on) radio_driver.init();
else {
radio_driver.end();
if (!(enabled() & mesh::wireless::WiFi)) {
::WiFi.setAutoReconnect(false);
::WiFi.mode(WIFI_OFF);
}
}
return radio_driver.isEnabled() == on ? Result::Done : Result::Failed;
}
#endif
return Result::Failed;
}
};
static CompanionWirelessBackend companion_wireless;
bool handleCompanionWirelessCommand(const char* command, char* reply, size_t size,
CompanionWirelessSource source) {
uint8_t requester = 0;
if (source == CompanionWirelessSource::Usb) requester = mesh::wireless::Independent;
else if (source == CompanionWirelessSource::Network) requester = mesh::wireless::WiFi;
else if (source == CompanionWirelessSource::Framed) {
BaseSerialInterface* route = interface_manager.captureReplyRoute();
if (interface_manager.isReplyRouteAvailable(route)) {
#if defined(BLE_PIN_CODE)
if (route == &bluetooth_interface) requester = mesh::wireless::Bluetooth;
else
#endif
#if defined(ESP32) && defined(WIFI_SSID)
if (route == &wifi_interface) requester = mesh::wireless::WiFi;
else
#endif
requester = mesh::wireless::Independent;
}
}
return mesh::wireless::control().handle(command, reply, size, millis(), requester);
}
void setup() {
mesh::wireless::control().begin(companion_wireless);
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_PACKET_LOGGING
@@ -2776,6 +2915,7 @@ void setup() {
}
void loop() {
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
board.feedWatchdog();
#endif
@@ -2851,7 +2991,8 @@ void loop() {
// USB power alone (for example, a wall charger) does not inhibit sleep.
// Host sessions, live logging, and button activity still need service.
bool can_sleep = the_mesh.getNodePrefs()->powersaving_enabled
&& !the_mesh.hasPendingWork();
&& !the_mesh.hasPendingWork()
&& !mesh::wireless::control().pending();
#if defined(ESP32_PLATFORM) && MESH_USB_LOGGING_AVAILABLE
// The native-USB-only light-sleep path below bypasses ESP32Board::sleep.
can_sleep = can_sleep && !mesh::isUsbLoggingEnabled();
+7
View File
@@ -4019,6 +4019,12 @@ void MyMesh::checkBatteryAlert() {
}
void MyMesh::checkRxInactivityWatchdog() {
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (mesh::wireless::control().blocked(mesh::wireless::EspNow)) {
rx_inactivity_watchdog.reset();
return;
}
#endif
const uint32_t interval = radio_timing.watchdogMillis(_prefs.rx_watchdog_enabled);
if (rx_inactivity_watchdog.expired(millis(), getLastMeshCoreRecvMillis(), interval)) {
MESH_DEBUG_PRINTLN("RX watchdog: no MeshCore packet received in %lu hours, rebooting",
@@ -11172,6 +11178,7 @@ bool MyMesh::handleReplayResetCommand(ClientInfo* sender, const char* command,
void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command,
char *reply, int gpio_client_index,
uint8_t gpio_path_hash_size, bool usb_origin) {
_wireless_usb_command = usb_origin;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
_gpio_reply_tracker.beginCommand(gpio_client_index, gpio_path_hash_size,
sender == NULL ? NULL : sender->id.pub_key);
+11
View File
@@ -1077,6 +1077,15 @@ public:
void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command,
char* reply, int gpio_client_index = -1,
uint8_t gpio_path_hash_size = 1, bool usb_origin = false);
bool _wireless_usb_command = false;
uint8_t wirelessCommandSource(uint32_t sender_timestamp) const override {
if (sender_timestamp) return CommonCLICallbacks::wirelessCommandSource(sender_timestamp);
if (_wireless_usb_command) return mesh::wireless::Independent;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_command_output && _command_output != _web_terminal) return mesh::wireless::Independent;
#endif
return mesh::wireless::WiFi;
}
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
handleCommand(sender_timestamp, NULL, command, reply);
}
@@ -1398,6 +1407,8 @@ public:
#ifdef WITH_WEBCONFIG
bool startWebConfig(bool force_ap, char* reply) override;
bool isWebConfigStopping() const { return _webconfig && _webconfig->isStopping(); }
bool hasWirelessNetworkClient() const { return _web_terminal != nullptr; }
bool stopWebConfig(char* reply) override;
bool setWebUIEnabled(bool enabled, char* reply) override;
bool getWebUIStatus(char* reply) const override;
+5 -1
View File
@@ -61,6 +61,7 @@ SimpleMeshTables tables;
#endif
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
#include "../InfrastructureWireless.h"
void halt() {
while (1) ;
@@ -87,6 +88,7 @@ static unsigned long userBtnDownAt = 0;
#endif
void setup() {
mesh::wireless::control().begin(infrastructure_wireless);
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
@@ -351,6 +353,7 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
}
void loop() {
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
#endif
@@ -389,7 +392,8 @@ void loop() {
external_watchdog.loop();
#endif
bool can_power_save = the_mesh.getNodePrefs()->powersaving_enabled
&& !board.isUsbDataConnected();
&& !board.isUsbDataConnected()
&& !mesh::wireless::control().pending();
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
+16
View File
@@ -525,6 +525,20 @@ public:
void handleCommand(uint32_t sender_timestamp, char* command, char* reply,
int gpio_client_index = -1,
uint8_t gpio_path_hash_size = 1);
bool _wireless_usb_command = false;
void handleUsbCommand(char* command, char* reply) {
_wireless_usb_command = true;
handleCommand(0, command, reply);
_wireless_usb_command = false;
}
uint8_t wirelessCommandSource(uint32_t sender_timestamp) const override {
if (sender_timestamp) return CommonCLICallbacks::wirelessCommandSource(sender_timestamp);
if (_wireless_usb_command) return mesh::wireless::Independent;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_command_output && _command_output != _web_terminal) return mesh::wireless::Independent;
#endif
return mesh::wireless::WiFi;
}
void loop();
#if defined(WITH_BRIDGE)
@@ -672,6 +686,8 @@ public:
#ifdef WITH_WEBCONFIG
bool startWebConfig(bool force_ap, char* reply) override;
bool isWebConfigStopping() const { return _webconfig && _webconfig->isStopping(); }
bool hasWirelessNetworkClient() const { return _web_terminal != nullptr; }
bool stopWebConfig(char* reply) override;
bool setWebUIEnabled(bool enabled, char* reply) override;
bool getWebUIStatus(char* reply) const override;
+7 -3
View File
@@ -29,6 +29,7 @@
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
#include "../InfrastructureWireless.h"
void halt() {
while (1) ;
@@ -43,6 +44,7 @@ static char ethernet_command[MAX_POST_TEXT_LEN+1];
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
void setup() {
mesh::wireless::control().begin(infrastructure_wireless);
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
@@ -151,6 +153,7 @@ void setup() {
}
void loop() {
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
#endif
@@ -186,10 +189,10 @@ void loop() {
reply[0] = 0;
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleCommand(0, command, reply);
the_mesh.handleUsbCommand(command, reply);
}
#else
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleUsbCommand(command, reply);
#endif
if (reply[0]) {
console.printf(" -> %s\r\n", reply);
@@ -231,7 +234,8 @@ void loop() {
external_watchdog.loop();
#endif
bool can_power_save = the_mesh.getNodePrefs()->powersaving_enabled
&& !board.isUsbDataConnected();
&& !board.isUsbDataConnected()
&& !mesh::wireless::control().pending();
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
+2
View File
@@ -2464,6 +2464,8 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
mesh::cli::normalizeCommandVerb(command);
if (mesh::wireless::control().handle(command, reply, 160, millis(),
_callbacks->wirelessCommandSource(sender_timestamp))) return;
if (_radio_profiles.handle(command, reply)) return;
if (strncmp(command, "set tempradio ", 14) == 0) {
handleCommand(sender_timestamp, command + 4, reply);
+9
View File
@@ -14,6 +14,7 @@
#include <helpers/DynamicConfigSerializer.h>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/RadioProfileCLI.h>
#include <helpers/WirelessControl.h>
#ifndef DEFAULT_CAD_ENABLED
#define DEFAULT_CAD_ENABLED 0
@@ -607,6 +608,14 @@ public:
(void)reply;
return false;
};
virtual uint8_t wirelessCommandSource(uint32_t sender_timestamp) const {
if (!sender_timestamp) return 0;
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
return mesh::wireless::EspNow;
#else
return mesh::wireless::Independent; // Authenticated remote CLI over LoRa.
#endif
}
virtual bool getWiFiPassword(char* reply) const {
(void)reply;
return false;
+2
View File
@@ -2,6 +2,7 @@
#include "BaseSerialInterface.h"
#include "CompanionFrameQueue.h"
#include "WirelessControl.h"
#ifndef MAX_INTERFACES
// ble, usb, wifi, ethernet
@@ -93,6 +94,7 @@ public:
}
void enableBluetooth() {
if (mesh::wireless::control().blocked(mesh::wireless::Bluetooth)) return;
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
iface.instance->enable();
+2
View File
@@ -12,6 +12,7 @@
#include <helpers/CLICommandUtils.h>
#include <helpers/UsbLogging.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#include <helpers/WirelessControl.h>
#include <helpers/esp32/WiFiStationPolicy.h>
namespace {
@@ -319,6 +320,7 @@ static void portalTask(void* arg) {
WiFiSetupPortal::WiFiSetupPortal() : _active(false), _impl(nullptr) {}
bool WiFiSetupPortal::begin(const char* ap_name, SaveCallback save_callback, void* context) {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return false;
if (_active) return true;
PortalImpl* impl = static_cast<PortalImpl*>(_impl);
if (!impl) {
+169
View File
@@ -0,0 +1,169 @@
#pragma once
#include <atomic>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
namespace mesh {
namespace wireless {
enum Service : uint8_t { WiFi = 1, Bluetooth = 2, EspNow = 4, All = 7, Independent = 8 };
enum class Result { Done, Pending, Failed };
enum class Action { None, Get, On, OnAll, Off, ForceOff, Invalid };
struct Command { uint8_t scope; Action action; };
inline const char* skipSpace(const char* s) {
while (*s == ' ' || *s == '\t') ++s;
return s;
}
inline bool word(const char* s, const char* expected) {
const size_t n = strlen(expected);
return strncmp(s, expected, n) == 0 && *skipSpace(s + n) == 0;
}
inline Command parse(const char* s) {
if (!s) return {0, Action::None};
s = skipSpace(s);
const bool get = strncmp(s, "get ", 4) == 0;
if (!get && strncmp(s, "set ", 4) != 0) return {0, Action::None};
s = skipSpace(s + 4);
const char* key = strncmp(s, "2.4ghz", 6) == 0 ? "2.4ghz" : strncmp(s, "espnow", 6) == 0 ? "espnow" : "wifi";
const size_t n = strlen(key);
if (strncmp(s, key, n) || (s[n] && s[n] != ' ' && s[n] != '\t')) return {0, Action::None};
const uint8_t scope = key[0] == '2' ? All : key[0] == 'e' ? EspNow : WiFi;
s = skipSpace(s + n);
if (get) return {scope, *s ? Action::Invalid : Action::Get};
if (word(s, "on")) return {scope, Action::On};
if (word(s, "off")) return {scope, Action::Off};
if (!strncmp(s, "off", 3) && (s[3] == ' ' || s[3] == '\t')
&& word(skipSpace(s + 3), "force")) return {scope, Action::ForceOff};
if (scope == All && !strncmp(s, "on", 2) && (s[2] == ' ' || s[2] == '\t')) {
s = skipSpace(s + 2);
if (word(s, "all") || word(s, "force")) return {scope, Action::OnAll};
}
return {scope, Action::Invalid};
}
class Backend {
public:
virtual uint8_t available() const = 0; // Services usable in this boot.
virtual uint8_t enabled() const = 0;
virtual uint8_t clients() const = 0; // Open management sessions, not association/power.
virtual bool replyBusy() const { return false; }
virtual Result set(uint8_t service, bool on) = 0;
};
class Control {
Backend* backend_ = nullptr;
std::atomic<uint8_t> blocked_{0};
uint8_t scope_ = 0, target_ = 0, saved_ = 0;
bool pending_ = false, started_ = false, force_ = false, retained_requester_ = false;
bool saved_valid_ = false, master_off_ = false;
uint32_t due_ = 0;
const char* error_ = nullptr;
public:
void begin(Backend& backend) { backend_ = &backend; }
bool blocked(uint8_t service) const { return (blocked_.load() & service) != 0; }
bool pending() const { return pending_; }
bool masterOff() const { return master_off_; }
bool allowService(uint8_t service) {
if (master_off_ || pending_) return false;
blocked_.store(blocked_.load() & ~service);
return true;
}
uint8_t enabled() const { return backend_ ? backend_->enabled() : 0; }
bool handle(const char* text, char* reply, size_t size, uint32_t now, uint8_t requester) {
const auto command = parse(text);
if (command.action == Action::None || !reply || !size) return false;
const uint8_t available = backend_ ? backend_->available() : 0;
const char* name = command.scope == All ? "2.4ghz" : command.scope == EspNow ? "espnow" : "wifi";
if (command.action == Action::Invalid) {
snprintf(reply, size, "Error: use set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]");
return true;
}
const uint8_t current = enabled();
if (command.action == Action::Get) {
const auto state = [=](uint8_t bit) { return !(available & bit) ? "unavailable" : current & bit ? "on" : "off"; };
if (command.scope != All) snprintf(reply, size, "%s %s%s%s", name, state(command.scope), pending_ ? " (pending)" : "", error_ ? error_ : "");
else snprintf(reply, size, "wifi=%s, bluetooth=%s, espnow=%s%s%s", state(WiFi), state(Bluetooth), state(EspNow), pending_ ? " (pending)" : "", error_ ? error_ : "");
return true;
}
if (!(available & command.scope)) {
snprintf(reply, size, "Error: requested wireless service unavailable in this build/boot");
return true;
}
const bool on = command.action == Action::On || command.action == Action::OnAll;
if (pending_ && command.scope != All && (command.scope & available) != scope_) {
snprintf(reply, size, "Error: wireless change pending; get 2.4ghz for status");
return true;
}
if (command.scope != All && on && master_off_) {
snprintf(reply, size, "Error: 2.4ghz is off; use set 2.4ghz on first");
return true;
}
const uint8_t scope = command.scope & available;
const bool retained = (requester & ~scope) != 0;
const bool force = command.action == Action::ForceOff;
if (!on && (current & scope) && !force && !retained && !(backend_->clients() & ~scope)) {
snprintf(reply, size, "Error: no remaining management connection; use off force or USB/Ethernet");
return true;
}
if (command.scope == All && !on && !saved_valid_) {
saved_ = current;
saved_valid_ = true;
}
scope_ = scope;
target_ = !on ? 0 : command.scope == All && command.action != Action::OnAll
? (saved_valid_ ? saved_ : current) & available : scope;
force_ = force;
retained_requester_ = retained;
pending_ = true;
started_ = false;
error_ = nullptr;
due_ = now + 250;
// Keep the group latch independent of the set of services available on a board.
if (command.scope == All) master_off_ = !on;
snprintf(reply, size, "OK - %s %s requested (this boot); get %s for status",
name, on ? "on" : "off", name);
return true;
}
void service(uint32_t now) {
if (!pending_ || !backend_ || int32_t(now - due_) < 0) return;
if (!started_) {
if (!target_ && (enabled() & scope_) && !force_ && !retained_requester_ && !(backend_->clients() & ~scope_)) {
pending_ = false;
if (master_off_) { master_off_ = false; saved_valid_ = false; }
error_ = " (cancelled: connection lost)";
return;
}
if (backend_->replyBusy() && uint32_t(now - due_) < 1750) return;
blocked_.store((blocked_.load() & ~scope_) | (scope_ & ~target_));
started_ = true;
}
// Infrastructure services own tasks and sockets: stop them before ESP-NOW's
// WiFi driver. Restore ESP-NOW first so infrastructure WiFi can coexist.
const uint8_t order[] = {WiFi, EspNow, Bluetooth, EspNow, WiFi, Bluetooth};
for (unsigned i = 0; i < sizeof(order); ++i) {
const uint8_t bit = order[i];
const bool on = (target_ & bit) != 0;
if (!(scope_ & bit) || on != (i >= 3)) continue;
const auto result = backend_->set(bit, on);
if (result == Result::Pending && uint32_t(now - due_) < 30000) return;
if (result != Result::Done) {
pending_ = false;
error_ = " (failed: retry command; check startup/teardown)";
return;
}
}
pending_ = false;
if (!master_off_) saved_valid_ = false;
}
};
inline Control& control() { static Control instance; return instance; }
} // namespace wireless
} // namespace mesh
+34 -20
View File
@@ -1,9 +1,18 @@
#include "ESPNowBridge.h"
#include <helpers/WirelessControl.h>
#include <esp_wifi.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#ifdef WITH_ESPNOW_BRIDGE
static void stopBridgeWiFiIfUnused() {
if (!(mesh::wireless::control().enabled() & mesh::wireless::WiFi)) {
esp_wifi_stop();
esp_wifi_deinit();
}
}
// Static member to handle callbacks
ESPNowBridge *ESPNowBridge::_instance = nullptr;
@@ -46,6 +55,7 @@ ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTC
}
void ESPNowBridge::begin() {
if (mesh::wireless::control().blocked(mesh::wireless::EspNow)) return;
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
if (_initialized) return;
@@ -80,13 +90,21 @@ void ESPNowBridge::begin() {
// ESP-NOW only needs the ESP-IDF station interface. Avoid Arduino's WiFi
// facade here: it pulls DHCP, DNS, scanning, AP, and event plumbing that an
// ESP-NOW-only bridge never uses.
const bool shared_wifi = (mesh::wireless::control().enabled() & mesh::wireless::WiFi) != 0;
if (shared_wifi) {
uint8_t channel = 0;
wifi_second_chan_t secondary = WIFI_SECOND_CHAN_NONE;
if (esp_wifi_get_channel(&channel, &secondary) != ESP_OK || channel != _prefs->bridge_channel) {
BRIDGE_DEBUG_PRINTLN("ESP-NOW bridge channel must match the active WiFi channel\n");
return;
}
}
wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
if (esp_wifi_init(&wifi_config) != ESP_OK ||
if (!shared_wifi && (esp_wifi_init(&wifi_config) != ESP_OK ||
esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK ||
esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK ||
esp_wifi_start() != ESP_OK) {
esp_wifi_stop();
esp_wifi_deinit();
esp_wifi_start() != ESP_OK)) {
stopBridgeWiFiIfUnused();
return;
}
@@ -99,25 +117,22 @@ void ESPNowBridge::begin() {
| WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR;
if (esp_wifi_set_protocol(WIFI_IF_STA, protocols) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error enabling ESP-NOW LR protocol\n");
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
}
// Set Wi-Fi channel
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
if (!shared_wifi && esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
@@ -127,8 +142,7 @@ void ESPNowBridge::begin() {
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
@@ -145,8 +159,7 @@ void ESPNowBridge::begin() {
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
@@ -169,17 +182,18 @@ void ESPNowBridge::begin() {
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
esp_wifi_stop();
esp_wifi_deinit();
stopBridgeWiFiIfUnused();
return;
}
}
// Update bridge state
_initialized = true;
mesh::wifi::bridgeEspNowChannel().store(_prefs->bridge_channel);
}
void ESPNowBridge::end() {
if (!_initialized) return;
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
// Remove broadcast peer
@@ -197,9 +211,9 @@ void ESPNowBridge::end() {
BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
}
// Turn off the ESP-IDF WiFi interface.
esp_wifi_stop();
esp_wifi_deinit();
// Infrastructure WiFi may still own this shared radio.
mesh::wifi::bridgeEspNowChannel().store(0);
stopBridgeWiFiIfUnused();
portENTER_CRITICAL(&_rx_mux);
_rx_head = 0;
+4
View File
@@ -9,6 +9,7 @@
#include "../TxtDataHelpers.h"
#include "../UsbLogging.h"
#include "../WiFiPowerSave.h"
#include "../WirelessControl.h"
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
@@ -874,6 +875,9 @@ void MQTTBridge::releaseRuntimeBuffers() {
// begin()
// ---------------------------------------------------------------------------
void MQTTBridge::begin() {
#if defined(ESP32)
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return;
#endif
MQTT_DEBUG_PRINTLN("Initializing MQTT Bridge...");
// Idempotent start: a second begin() would re-run allocation and re-create
+62 -10
View File
@@ -5,6 +5,7 @@
#include <esp_wifi.h>
#include <helpers/ESPNowRawFragmentation.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#include <helpers/WirelessControl.h>
#if ESP_ARDUINO_VERSION_MAJOR >= 3
#include <esp_mac.h>
#endif
@@ -32,6 +33,7 @@ static volatile uint32_t rx_dropped = 0;
static uint32_t rx_dropped_reported = 0;
static portMUX_TYPE rx_mux = portMUX_INITIALIZER_UNLOCKED;
static mesh::espnow::ESPNowRawReassembler rx_reassembler;
static std::atomic<bool> accepting_packets{false};
// callback when data is sent
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
@@ -42,6 +44,7 @@ static void OnDataSent(const esp_now_send_info_t *info,
static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
(void)mac_addr;
#endif
if (!accepting_packets.load()) return;
last_send_status = status;
is_send_complete = true;
ESPNOW_DEBUG_PRINTLN("Send Status: %d", (int)status);
@@ -54,6 +57,7 @@ static void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data,
#else
static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
#endif
if (!accepting_packets.load()) return;
ESPNOW_DEBUG_PRINTLN("Recv: len = %d", len);
portENTER_CRITICAL(&rx_mux);
if (data != nullptr && len > 0 && len <= RX_FRAME_MAX
@@ -76,6 +80,7 @@ static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
}
void ESPNOWRadio::init() {
if (initialized_ || mesh::wireless::control().blocked(mesh::wireless::EspNow)) return;
portENTER_CRITICAL(&rx_mux);
rx_head = 0;
rx_tail = 0;
@@ -93,13 +98,15 @@ void ESPNOWRadio::init() {
// driver writes in RAM prevents a later conventional WiFi image from
// inheriting the proprietary protocol bit through NVS.
WiFi.persistent(false);
const wifi_mode_t existing_mode = WiFi.getMode();
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
// Arduino defaults station auto-reconnect to enabled. Disable it before the
// driver starts so credentials left by another image cannot pull the radio
// away from the configured mesh channel behind this policy's back.
WiFi.setAutoReconnect(false);
if (existing_mode == WIFI_OFF) WiFi.setAutoReconnect(false);
#endif
WiFi.mode(WIFI_STA);
// Restarting ESP-NOW must retain a live infrastructure setup AP.
if (!WiFi.mode((existing_mode & WIFI_AP) ? WIFI_AP_STA : WIFI_STA)) return;
#if defined(MESH_ESPNOW_RADIO) && MESH_ESPNOW_RADIO
// Full Companion also exposes ordinary WiFi. Preserve B/G/N for normal
// access points while retaining LR for ESP-NOW, and pin both transports to
@@ -128,6 +135,7 @@ void ESPNOWRadio::init() {
ESPNOW_DEBUG_PRINTLN("Error initializing ESP-NOW");
return;
}
initialized_ = true;
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
// Modem sleep is still required when BLE and infrastructure WiFi coexist,
@@ -137,15 +145,22 @@ void ESPNOWRadio::init() {
// WiFi/Bluetooth coexistence policy globally.
if (esp_wifi_force_wakeup_acquire() != ESP_OK) {
ESPNOW_DEBUG_PRINTLN("Error keeping primary ESP-NOW receive awake");
esp_now_deinit();
end();
return;
}
wake_held_ = true;
#endif
esp_wifi_set_max_tx_power(80); // should be 20dBm
if (!setTxPower(tx_power_dbm_)) {
end();
return;
}
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
if (esp_now_register_send_cb(OnDataSent) != ESP_OK
|| esp_now_register_recv_cb(OnDataRecv) != ESP_OK) {
end();
return;
}
// Register peer
memset(&peerInfo, 0, sizeof(peerInfo));
@@ -175,26 +190,58 @@ void ESPNOWRadio::init() {
rate_config.dcm = false;
if (esp_now_set_peer_rate_config(broadcastAddress, &rate_config) != ESP_OK) {
ESPNOW_DEBUG_PRINTLN("Error configuring ESP-NOW LR peer rate");
end();
return;
}
#else
if (esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_LORA_250K)
!= ESP_OK) {
ESPNOW_DEBUG_PRINTLN("Error configuring ESP-NOW LR interface rate");
end();
return;
}
#endif
#endif
accepting_packets.store(true);
ESPNOW_DEBUG_PRINTLN("init success");
} else {
// ESPNOW_DEBUG_PRINTLN("Failed to add peer");
end();
}
}
void ESPNOWRadio::end() {
if (send_active_) send_cancelled_ = true;
accepting_packets.store(false);
if (initialized_) {
esp_now_unregister_send_cb();
esp_now_unregister_recv_cb();
esp_now_deinit();
initialized_ = false;
}
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (wake_held_) {
esp_wifi_force_wakeup_release();
wake_held_ = false;
}
#endif
portENTER_CRITICAL(&rx_mux);
rx_head = rx_tail = rx_count = 0;
portEXIT_CRITICAL(&rx_mux);
rx_reassembler.reset();
tx_second_pending = false;
tx_second_length = 0;
// A cancelled transmission must time out, never become a false TX success.
is_send_complete = false;
}
uint32_t ESPNOWRadio::getRngSeed() {
return millis() + intID(); // TODO: where to get some entropy?
}
bool ESPNOWRadio::setTxPower(int8_t dbm) {
return esp_wifi_set_max_tx_power(dbm * 4) == ESP_OK;
if (esp_wifi_set_max_tx_power(dbm * 4) != ESP_OK) return false;
tx_power_dbm_ = dbm;
return true;
}
uint32_t ESPNOWRadio::intID() {
@@ -209,6 +256,7 @@ uint32_t ESPNOWRadio::intID() {
}
bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) {
if (!initialized_ || send_cancelled_) return false;
mesh::espnow::ESPNowRawFrames frames;
if (len < 0
|| !mesh::espnow::encodeEspNowRawFrames(
@@ -232,6 +280,7 @@ bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) {
esp_err_t result = esp_now_send(
broadcastAddress, frames.data[0], frames.lengths[0]);
if (result == ESP_OK) {
send_active_ = true;
n_sent++;
ESPNOW_DEBUG_PRINTLN("Send started: frames=%u raw_len=%d",
(unsigned)frames.count, len);
@@ -246,6 +295,7 @@ bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) {
}
bool ESPNOWRadio::isSendComplete() {
if (!initialized_ || send_cancelled_) return false;
if (!is_send_complete) return false;
if (last_send_status != ESP_NOW_SEND_SUCCESS) {
// A failed callback means the logical packet was not transmitted. Do not
@@ -279,6 +329,8 @@ bool ESPNOWRadio::isSendComplete() {
return is_send_complete;
}
void ESPNOWRadio::onSendFinished() {
send_active_ = false;
send_cancelled_ = false;
tx_second_pending = false;
tx_second_length = 0;
last_send_status = ESP_NOW_SEND_SUCCESS;
@@ -286,14 +338,14 @@ void ESPNOWRadio::onSendFinished() {
}
bool ESPNOWRadio::isInRecvMode() const {
return is_send_complete && !tx_second_pending;
return initialized_ && !send_cancelled_ && is_send_complete && !tx_second_pending;
}
float ESPNOWRadio::getLastRSSI() const { return 0; }
float ESPNOWRadio::getLastSNR() const { return 0; }
int ESPNOWRadio::recvRaw(uint8_t* bytes, int sz) {
if (bytes == nullptr || sz <= 0) return 0;
if (!initialized_ || bytes == nullptr || sz <= 0) return 0;
uint32_t dropped = 0;
portENTER_CRITICAL(&rx_mux);
+8 -1
View File
@@ -5,6 +5,11 @@
class ESPNOWRadio : public mesh::Radio {
protected:
uint32_t n_recv, n_sent, n_recv_errors;
bool initialized_ = false;
bool wake_held_ = false;
int8_t tx_power_dbm_ = 20;
bool send_active_ = false;
bool send_cancelled_ = false;
public:
ESPNOWRadio() { n_recv = n_sent = n_recv_errors = 0; }
@@ -24,9 +29,11 @@ public:
const uint32_t* rx_ps_timings = nullptr) {
return trySetParams(freq, bw, sf, cr, rx_ps_timings) == mesh::RadioParamApplyResult::APPLIED;
}
void powerOff() { /* no-op */ }
void powerOff() { end(); }
void init();
void end();
bool isEnabled() const { return initialized_; }
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
+20 -12
View File
@@ -1,4 +1,5 @@
#include "WebConfigServer.h"
#include <helpers/WirelessControl.h>
#ifdef WITH_WEBCONFIG
@@ -187,18 +188,17 @@ static uint8_t effectiveWiFiPowerSave(uint8_t configured) {
}
static void stopOwnedWiFiRadio() {
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
// ESP-NOW is this target's mesh radio. Stop only infrastructure-WiFi
// ownership; powering the driver down would also remove the mesh transport.
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, false);
WiFi.mode(WIFI_STA);
mesh::wifi::applyProtocolMask(WIFI_IF_STA);
mesh::wifi::restoreEspNowChannel();
#else
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
#endif
if (mesh::wifi::espNowChannelConstrained()) {
// Keep a primary ESP-NOW radio or running bridge on its mesh channel.
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, false);
WiFi.mode(WIFI_STA);
mesh::wifi::applyProtocolMask(WIFI_IF_STA);
mesh::wifi::restoreEspNowChannel();
} else {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
}
// Placeholder sent instead of stored secrets; POSTs carrying it are dropped
@@ -780,6 +780,10 @@ bool WebConfigServer::getSetupInfo(char* ssid, size_t ssid_len, char* ip, size_t
// ---------------------------------------------------------------------------
bool WebConfigServer::startSetupMode(char reply[]) {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) {
strcpy(reply, "Error: WiFi disabled; use set wifi on or set 2.4ghz on");
return false;
}
const bool promote_lan = _mode == MODE_LAN && !_owns_wifi && !_stopping;
if ((_mode != MODE_OFF && !promote_lan) || _stopping) {
strcpy(reply, "Err: webconfig busy");
@@ -892,6 +896,10 @@ bool WebConfigServer::startLanMode(char reply[]) {
}
bool WebConfigServer::startAutoMode(char reply[]) {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) {
strcpy(reply, "Error: WiFi disabled; use set wifi on or set 2.4ghz on");
return false;
}
if (_wifi_ssid[0] == 0) return startSetupMode(reply);
if (WiFi.status() == WL_CONNECTED) return startLanMode(reply);
if (_mode != MODE_OFF || _stopping) {
+18 -6
View File
@@ -4,6 +4,7 @@
#include <esp_err.h>
#include <esp_wifi.h>
#include <helpers/WiFiChannelPolicy.h>
#include <atomic>
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
#include <Preferences.h>
@@ -28,7 +29,7 @@ static constexpr bool kKeepEspNowRadioRunning = false;
static constexpr uint8_t kDefaultEspNowChannel = 1;
#endif
#if defined(MESH_ESPNOW_RADIO) && MESH_ESPNOW_RADIO
#if (defined(MESH_ESPNOW_RADIO) && MESH_ESPNOW_RADIO) || defined(WITH_ESPNOW_BRIDGE)
static constexpr uint8_t kProtocolMask = WIFI_PROTOCOL_11B
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR;
#elif defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
@@ -45,6 +46,16 @@ static constexpr uint8_t kProtocolMask = WIFI_PROTOCOL_11B
static constexpr uint8_t kAccessPointProtocolMask = WIFI_PROTOCOL_11B
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N;
// A bridge can start/stop independently of infrastructure WiFi. Its channel
// constraint exists only while it runs; primary ESP-NOW keeps its boot policy.
inline std::atomic<uint8_t>& bridgeEspNowChannel() {
static std::atomic<uint8_t> channel{0};
return channel;
}
inline bool espNowChannelConstrained() {
return kPrimaryEspNowRadio || bridgeEspNowChannel().load() != 0;
}
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
struct EspNowBootChannelState {
bool loaded;
@@ -96,7 +107,8 @@ inline uint8_t loadConfiguredEspNowChannel() {
return kDefaultEspNowChannel;
}
inline uint8_t activeEspNowChannel() {
return kDefaultEspNowChannel;
const uint8_t bridge_channel = bridgeEspNowChannel().load();
return bridge_channel ? bridge_channel : kDefaultEspNowChannel;
}
inline bool saveConfiguredEspNowChannel(uint8_t) {
return false;
@@ -116,21 +128,21 @@ inline esp_err_t applyAccessPointProtocolMask() {
// association on the mesh channel. Passing zero on ordinary targets preserves
// Arduino's normal all-channel scan behavior.
inline int32_t stationChannelHint() {
return kPrimaryEspNowRadio
return espNowChannelConstrained()
? static_cast<int32_t>(activeEspNowChannel()) : 0;
}
inline int accessPointChannel() {
return kPrimaryEspNowRadio
return espNowChannelConstrained()
? static_cast<int>(activeEspNowChannel()) : 1;
}
inline uint8_t stationScanChannel() {
return kPrimaryEspNowRadio ? activeEspNowChannel() : 0;
return espNowChannelConstrained() ? activeEspNowChannel() : 0;
}
inline esp_err_t restoreEspNowChannel() {
if (!kKeepEspNowRadioRunning) return ESP_OK;
if (!espNowChannelConstrained()) return ESP_OK;
const uint8_t target = activeEspNowChannel();
uint8_t current = 0;
wifi_second_chan_t secondary = WIFI_SECOND_CHAN_NONE;
+6 -4
View File
@@ -4,6 +4,7 @@
#include <string.h>
#include <WiFi.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#include <helpers/WirelessControl.h>
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
#include "../../../tools/hil/S3SoakWiFiKey.h"
#endif
@@ -17,18 +18,19 @@ namespace wifi {
// drag ESP-NOW (and a setup AP) with it. Existing application retry timers call
// beginStation() and therefore retain the fixed-channel/BSSID constraint.
inline void setStationAutoReconnect(bool enabled) {
WiFi.setAutoReconnect(enabled && !kPrimaryEspNowRadio);
WiFi.setAutoReconnect(enabled && !espNowChannelConstrained());
}
inline wl_status_t beginStation(const char* ssid, const char* password) {
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) return WL_CONNECT_FAILED;
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
password = mesh::hil::soakStationPassword(password);
#endif
if (!kPrimaryEspNowRadio) {
if (!espNowChannelConstrained()) {
return WiFi.begin(ssid, password);
}
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
#if (defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW) || defined(WITH_ESPNOW_BRIDGE)
if (!ssid || !ssid[0]) return WL_CONNECT_FAILED;
setStationAutoReconnect(false);
if (applyProtocolMask(WIFI_IF_STA) != ESP_OK
@@ -81,7 +83,7 @@ inline wl_status_t beginStation(const char* ssid, const char* password) {
// channel (for example after an AP channel-switch announcement). Callers then
// follow their existing disconnected/retry path.
inline bool enforceStationChannel() {
if (!kPrimaryEspNowRadio) return true;
if (!espNowChannelConstrained()) return true;
// beginStation(), disconnect handling, and setup-AP startup already restore
// the selected channel. Avoid polling the driver and attempting a channel
// write from every WebConfig and Companion loop while it is disconnected.
+279
View File
@@ -0,0 +1,279 @@
"""Execute production ESP-NOW driver startup/shutdown with fault-injected SDK APIs."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
STUBS = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <helpers/WirelessControl.h>
#include <helpers/ESPNowRawFragmentation.h>
#define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c))
#define ESPNOW_DEBUG_PRINTLN(...) ((void)0)
#define portENTER_CRITICAL(x) ((void)0)
#define portEXIT_CRITICAL(x) ((void)0)
#define portMUX_INITIALIZER_UNLOCKED 0
using portMUX_TYPE=int;
using esp_err_t=int;
constexpr int ESP_OK=0,WIFI_IF_STA=0,WIFI_PROTOCOL_LR=8,WIFI_PHY_MODE_LR=1,WIFI_PHY_RATE_LORA_250K=2;
enum wifi_mode_t {WIFI_OFF=0,WIFI_STA=1,WIFI_AP=2,WIFI_AP_STA=3};
enum esp_now_send_status_t {ESP_NOW_SEND_SUCCESS=0,ESP_NOW_SEND_FAIL=1};
struct esp_now_send_info_t {};
struct esp_now_recv_info_t {const uint8_t* src_addr;};
struct esp_now_peer_info_t {uint8_t peer_addr[6];int channel,ifidx;bool encrypt;};
struct esp_now_rate_config_t {int phymode,rate;bool ersu,dcm;};
int failure=0,init_calls=0,deinit_calls=0,wake_refs=0,send_calls=0,power=0;
bool sdk_active=false;uint32_t millis(){return 1234;}
struct FakeWiFi {
wifi_mode_t current=WIFI_OFF;bool autoreconnect=true;
void persistent(bool){}
wifi_mode_t getMode(){return current;}
void setAutoReconnect(bool value){autoreconnect=value;}
bool mode(wifi_mode_t value){if(failure==1)return false;current=value;return true;}
} WiFi;
namespace mesh {
enum class RadioParamApplyResult {APPLIED};
namespace wifi {
int applyProtocolMask(int){return failure==2?-1:0;}
int restoreEspNowChannel(){return failure==3?-1:0;}
}
}
int esp_wifi_set_protocol(int,int){return failure==2?-1:0;}
int esp_now_init(){++init_calls;if(failure==4)return -1;sdk_active=true;return 0;}
int esp_now_deinit(){assert(sdk_active);sdk_active=false;++deinit_calls;return 0;}
int esp_wifi_force_wakeup_acquire(){if(failure==5)return -1;++wake_refs;return 0;}
int esp_wifi_force_wakeup_release(){assert(wake_refs==1);--wake_refs;return 0;}
int esp_wifi_set_max_tx_power(int value){if(failure==6)return -1;power=value;return 0;}
template<typename T> int esp_now_register_send_cb(T){return failure==7?-1:0;}
template<typename T> int esp_now_register_recv_cb(T){return failure==8?-1:0;}
int esp_now_unregister_send_cb(){return 0;}
int esp_now_unregister_recv_cb(){return 0;}
int esp_now_add_peer(esp_now_peer_info_t* peer){assert(peer->channel==0);return failure==9?-1:0;}
int esp_now_set_peer_rate_config(const uint8_t*,esp_now_rate_config_t*){return failure==10?-1:0;}
int esp_wifi_config_espnow_rate(int,int){return failure==10?-1:0;}
int esp_now_send(const uint8_t*,const uint8_t*,size_t){assert(sdk_active);++send_calls;return 0;}
void esp_efuse_mac_get_default(uint8_t* mac){memset(mac,1,6);}
'''
CHECKS = r'''
void deliver(const uint8_t* bytes,int len){
static uint8_t mac[6]={1,2,3,4,5,6};
#if ESP_ARDUINO_VERSION_MAJOR >= 3
esp_now_recv_info_t info{mac};OnDataRecv(&info,bytes,len);
#else
OnDataRecv(mac,bytes,len);
#endif
}
int main(){
ESPNOWRadio radio;
assert(!radio.isEnabled());
for(int stage=1;stage<=10;++stage){
failure=stage;radio.init();assert(!radio.isEnabled());
assert(!sdk_active&&wake_refs==0);
assert(!radio.isInRecvMode()&&!radio.isSendComplete());
radio.end();assert(wake_refs==0); // failed startup and repeated off are safe
}
failure=0;WiFi.current=WIFI_OFF;radio.init();
assert(radio.isEnabled()&&sdk_active&&wake_refs==1&&!WiFi.autoreconnect);
const int started=init_calls;radio.init();assert(init_calls==started&&wake_refs==1);
assert(radio.setTxPower(13)&&power==52);
uint8_t raw[255]={0x15,1,2,3},out[255];
deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==4&&!memcmp(raw,out,4));
// Stop in the middle of a fragmented TX: do not report success or send fragment 2.
assert(radio.startSendRaw(raw,sizeof(raw))&&tx_second_pending);
OnDataSent(nullptr,ESP_NOW_SEND_SUCCESS);
radio.end();const int sent=send_calls;
assert(!radio.isSendComplete()&&!radio.startSendRaw(raw,4)&&send_calls==sent);
OnDataSent(nullptr,ESP_NOW_SEND_SUCCESS); // callback arriving while disabled
deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==0);
radio.end();assert(wake_refs==0&&!sdk_active);
// Starting alongside a setup AP preserves it and restores the configured TX power.
WiFi.current=WIFI_AP_STA;WiFi.autoreconnect=true;radio.init();
assert(WiFi.current==WIFI_AP_STA&&WiFi.autoreconnect&&power==52);
assert(!radio.isSendComplete()); // restarting before Dispatcher's timeout cannot fake TX success
radio.onSendFinished(); // Dispatcher acknowledges the cancelled operation
assert(radio.recvRaw(out,sizeof(out))==0&&radio.isInRecvMode());
// Incomplete RX fragments cannot cross a stop/start boundary.
mesh::espnow::ESPNowRawFrames frames;
assert(mesh::espnow::encodeEspNowRawFrames(raw,sizeof(raw),frames));
deliver(frames.data[0],frames.lengths[0]);assert(radio.recvRaw(out,sizeof(out))==0);
radio.end();radio.init();
deliver(frames.data[1],frames.lengths[1]);assert(radio.recvRaw(out,sizeof(out))==0);
// A fresh two-fragment packet is received normally after restarting.
deliver(frames.data[0],frames.lengths[0]);
assert(radio.recvRaw(out,sizeof(out))==255&&!memcmp(raw,out,255));
radio.end();assert(wake_refs==0&&!sdk_active);
}
'''
class EspNowLifecycleTests(unittest.TestCase):
def test_sdk_failures_stop_and_restart(self):
header=(ROOT/'src/helpers/esp32/ESPNOWRadio.h').read_text()
source=(ROOT/'src/helpers/esp32/ESPNOWRadio.cpp').read_text()
# Substitute only the abstract radio base and SDK includes; all driver
# state, callbacks, queues, fragmentation, and method bodies are production.
header=header[:header.index('#if ESPNOW_DEBUG_LOGGING')]
header=re.sub(r'^\s*#(?:include|pragma).*$', '', header, flags=re.MULTILINE)
header=header.replace(' : public mesh::Radio','').replace(' override','')
source=re.sub(r'^\s*#include.*$', '', source, flags=re.MULTILINE)
with tempfile.TemporaryDirectory() as tmp:
folder=Path(tmp)
(folder/'driver.cpp').write_text(STUBS+header+source+CHECKS)
for arduino,idf in [(2,40400),(3,50200),(3,50500)]:
with self.subTest(arduino=arduino,idf=idf):
binary=folder/f'driver-{idf}'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',
'-I',str(ROOT/'src'),'-DMESH_PRIMARY_ESPNOW=1','-DMESH_ESPNOW_RADIO=1',
f'-DESP_ARDUINO_VERSION_MAJOR={arduino}',f'-DESP_IDF_VERSION={idf}',
str(folder/'driver.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
def test_bridge_preserves_infrastructure_wifi(self):
source=(ROOT/'src/helpers/bridges/ESPNowBridge.cpp').read_text()
methods=method(source,'static void stopBridgeWiFiIfUnused(')+'\n'
methods+='\n'.join(method(source,f'void ESPNowBridge::{name}(').replace('ESPNowBridge::','Bridge::')
for name in ('begin','end'))
harness=STUBS.replace('assert(peer->channel==0)', 'assert(peer->channel==6)')+r'''
#include <helpers/bridges/ESPNowBridgeFormat.h>
#include <initializer_list>
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
#define ESP_NOW_ETH_ALEN 6
#define WIFI_INIT_CONFIG_DEFAULT() 0
using wifi_init_config_t=int;
using wifi_second_chan_t=int;
constexpr int WIFI_STORAGE_RAM=0,WIFI_MODE_STA=1,WIFI_SECOND_CHAN_NONE=0;
constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4;
int wifi_stops=0,wifi_starts=0;uint8_t wifi_channel=6;
int esp_wifi_init(wifi_init_config_t*){return 0;}
int esp_wifi_set_storage(int){return 0;}
int esp_wifi_set_mode(int){return 0;}
int esp_wifi_start(){++wifi_starts;return 0;}
int esp_wifi_stop(){++wifi_stops;return 0;}
int esp_wifi_deinit(){return 0;}
int esp_wifi_get_channel(uint8_t* channel,wifi_second_chan_t*){*channel=wifi_channel;return 0;}
int esp_wifi_set_channel(int channel,int){wifi_channel=channel;return 0;}
int esp_now_del_peer(const uint8_t*){return 0;}
namespace mesh {namespace wifi {
std::atomic<uint8_t>& bridgeEspNowChannel(){static std::atomic<uint8_t> channel{0};return channel;}
}}
struct Backend : mesh::wireless::Backend {
bool wifi=true;
uint8_t available()const override{return mesh::wireless::WiFi|mesh::wireless::EspNow;}
uint8_t enabled()const override{return wifi?mesh::wireless::WiFi:0;}
uint8_t clients()const override{return mesh::wireless::Independent;}
mesh::wireless::Result set(uint8_t,bool)override{return mesh::wireless::Result::Done;}
};
class Bridge {
public:
struct Prefs {int bridge_format=mesh::bridge::ESPNOW_FORMAT_RAW;uint8_t bridge_channel=6;} prefs;
Prefs* _prefs=&prefs;bool _initialized=false;
int _active_format=0,_rx_mux=0,_rx_head=0,_rx_tail=0,_rx_count=0,_rx_dropped=0,_rx_dropped_reported=0;
int _tx_mux=0,_tx_head=0,_tx_tail=0,_tx_count=0,_tx_waiting=0,_tx_callback_done=0;
int _tx_callback_status=0,_tx_dropped=0,_tx_dropped_reported=0;
mesh::espnow::ESPNowRawReassembler _raw_reassembler;
struct QueuedTransmit {bool started=false;int packet=0;};
static constexpr int TX_QUEUE_DEPTH=2;
QueuedTransmit _tx_queue[TX_QUEUE_DEPTH];
struct {void clear(const int*){}} _seen_packets;
static void recv_cb(){}static void send_cb(){}
void begin();void end();
};
'''+methods+r'''
int main(){
Backend backend;mesh::wireless::control().begin(backend);Bridge bridge;
// A channel conflict must leave the active WiFi connection untouched.
wifi_channel=3;bridge.begin();assert(!bridge._initialized&&init_calls==0&&wifi_stops==0);
wifi_channel=6;
for(int stage:{2,4,7,8,9,10}){
failure=stage;bridge.begin();assert(!bridge._initialized&&!sdk_active&&wifi_stops==0);
bridge.end();assert(wifi_stops==0);
}
failure=0;bridge.begin();assert(bridge._initialized&&sdk_active&&wifi_starts==0);
assert(mesh::wifi::bridgeEspNowChannel().load()==6);
bridge.end();assert(!sdk_active&&wifi_stops==0&&mesh::wifi::bridgeEspNowChannel().load()==0);
// When no infrastructure service owns WiFi, the bridge owns driver shutdown.
backend.wifi=false;bridge.begin();assert(bridge._initialized&&wifi_starts==1);
bridge.end();assert(!sdk_active&&wifi_stops==1);
bridge.end();assert(wifi_stops==1);
}
'''
with tempfile.TemporaryDirectory() as tmp:
folder=Path(tmp)
(folder/'bridge.cpp').write_text(harness)
binary=folder/'bridge'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-I',str(ROOT/'src'),
'-DESP_IDF_VERSION=50200',str(folder/'bridge.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
def test_bridge_channel_constraint_tracks_runtime_state(self):
with tempfile.TemporaryDirectory() as tmp:
folder=Path(tmp)
(folder/'esp_err.h').write_text('#pragma once\nusing esp_err_t=int;\nconstexpr int ESP_OK=0;\n')
(folder/'esp_wifi.h').write_text(r'''
#pragma once
#include <cstdint>
#include <esp_err.h>
using wifi_interface_t=int;using wifi_second_chan_t=int;
constexpr int WIFI_IF_STA=0,WIFI_IF_AP=1,WIFI_SECOND_CHAN_NONE=0;
constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4,WIFI_PROTOCOL_LR=8;
uint8_t current_channel=1,last_protocol=0;
int esp_wifi_set_protocol(int,uint8_t p){last_protocol=p;return 0;}
int esp_wifi_get_channel(uint8_t* p,int*){*p=current_channel;return 0;}
int esp_wifi_set_channel(uint8_t p,int){current_channel=p;return 0;}
''')
(folder/'channel.cpp').write_text(r'''
#include <helpers/esp32/WiFiRadioPolicy.h>
#include <helpers/esp32/WiFiStationPolicy.h>
#include <cassert>
int main(){using namespace mesh::wifi;
assert(!espNowChannelConstrained()&&stationChannelHint()==0&&stationScanChannel()==0);
bridgeEspNowChannel().store(6);
assert(espNowChannelConstrained()&&stationChannelHint()==6&&stationScanChannel()==6&&accessPointChannel()==6);
assert(restoreEspNowChannel()==ESP_OK&&current_channel==6);
applyProtocolMask(WIFI_IF_STA);assert(last_protocol==15);
setStationAutoReconnect(true);assert(!WiFi.auto_reconnect);
assert(beginStation("mesh","password")==WL_CONNECTED);
assert(WiFi.scan_channel==6&&WiFi.join_channel==6);
current_channel=11;assert(!enforceStationChannel()&&current_channel==6);
bridgeEspNowChannel().store(0);current_channel=11;
assert(!espNowChannelConstrained()&&stationChannelHint()==0&&stationScanChannel()==0);
assert(restoreEspNowChannel()==ESP_OK&&current_channel==11);
setStationAutoReconnect(true);assert(WiFi.auto_reconnect);
assert(beginStation("mesh","password")==WL_CONNECTED&&WiFi.join_channel==0);
}
''')
(folder/'WiFi.h').write_text(r'''
#pragma once
#include <esp_wifi.h>
#include <string>
enum wl_status_t {WL_IDLE_STATUS,WL_CONNECTED,WL_NO_SSID_AVAIL,WL_CONNECT_FAILED};
constexpr int WIFI_SCAN_RUNNING=-1;
struct {
bool auto_reconnect=true;uint8_t scan_channel=0;int join_channel=0;
wl_status_t state=WL_IDLE_STATUS;
void setAutoReconnect(bool value){auto_reconnect=value;}
wl_status_t begin(const char*,const char*,int channel=0,const uint8_t* =nullptr){
join_channel=channel;state=WL_CONNECTED;return state;
}
int scanComplete(){return -2;}void scanDelete(){}
int scanNetworks(bool,bool,bool,int,uint8_t channel,const char*,const uint8_t*){scan_channel=channel;return 2;}
int channel(int index=-1){return index<0?current_channel:index==0?1:6;}
std::string SSID(int){return "mesh";}int RSSI(int){return -50;}
const uint8_t* BSSID(int){static uint8_t mac[6]={1};return mac;}
wl_status_t status(){return state;}
void disconnect(bool,bool){state=WL_IDLE_STATUS;}
} WiFi;
''')
binary=folder/'channel'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-DWITH_ESPNOW_BRIDGE=1',
'-I',str(folder),'-I',str(ROOT/'src'),str(folder/'channel.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
if __name__=='__main__':
unittest.main()
+299
View File
@@ -0,0 +1,299 @@
"""Compile and execute the production wireless command/state controller."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <helpers/WirelessControl.h>
#include <cassert>
#include <vector>
using namespace mesh::wireless;
struct Fake : Backend {
uint8_t supported=All, active=All, sessions=0, wait=0, fail=0;
bool busy=false;
std::vector<int> calls;
uint8_t available()const override{return supported;}
uint8_t enabled()const override{return active;}
uint8_t clients()const override{return sessions;}
bool replyBusy()const override{return busy;}
Result set(uint8_t bit,bool on)override {
calls.push_back(on?bit:-bit);
if(wait==bit)return Result::Pending;
if(fail==bit)return Result::Failed;
if(on)active|=bit;else active&=~bit;
return Result::Done;
}
};
struct Fixture {
Fake b; Control c; char reply[160]={}; uint32_t now=100;
Fixture(){c.begin(b);}
void run(const char* text,uint8_t requester=Independent){assert(c.handle(text,reply,sizeof(reply),now,requester));}
void tick(unsigned ms=250){now+=ms;c.service(now);}
};
int main(int argc,char** argv) {
const int test=argc>1?atoi(argv[1]):0;
if(test==0){
for(const char* text:{"set wifi","set espnow on force","set wifi off force extra",
"set 2.4ghz on all force","set 2.4ghz on forced","get wifi junk","set 2.4ghz off all"})
assert(parse(text).action==Action::Invalid);
for(const char* text:{"set wifi.ssid x","get wifi.status","set espnow.channel 3","set bluetooth off","get 2.4ghzx"})
assert(parse(text).action==Action::None);
assert(parse(nullptr).action==Action::None);
assert(parse(" set espnow\t off\tforce \t").action==Action::ForceOff);
assert(parse("set 2.4ghz on all").action==Action::OnAll);
assert(parse("set 2.4ghz on force").action==Action::OnAll);
} else if(test==1){
for(unsigned mask=0;mask<=All;++mask){
Fixture f;f.b.active=mask;
f.run("set 2.4ghz on");f.tick();assert(f.b.active==mask); // on before off is idempotent
f.run("set 2.4ghz off");f.tick();assert(f.b.active==0);
f.run("set 2.4ghz off");f.tick(); // never overwrite the saved state
f.run("set wifi on");assert(strstr(f.reply,"Error:"));
assert(!f.c.allowService(Bluetooth));
f.run("set 2.4ghz on");f.tick();assert(f.b.active==mask);
assert(f.c.allowService(Bluetooth));
f.run("set 2.4ghz on all");f.tick();assert(f.b.active==All);
f.run("set 2.4ghz off");f.tick();
f.run("set 2.4ghz on force");f.tick();assert(f.b.active==All);
}
} else if(test==2){
Fixture f;f.run("set wifi off",WiFi);assert(strstr(f.reply,"Error:"));
f.b.sessions=Bluetooth;f.run("set wifi off",WiFi);assert(f.c.pending());
f.tick(249);assert(f.b.active==All);f.b.sessions=0;f.tick(1);
assert(f.b.active==All&&!f.c.pending());f.run("get wifi");assert(strstr(f.reply,"cancelled"));
f.b.sessions=Bluetooth;f.run("set wifi off",WiFi);f.tick();assert(f.b.active==(Bluetooth|EspNow));
f.run("set wifi on");f.tick();assert(f.b.active==All);
f.b.sessions=Bluetooth|WiFi;f.run("set 2.4ghz off",Bluetooth);assert(strstr(f.reply,"Error:"));
f.run("set espnow off",EspNow);f.tick();assert(f.b.active==(WiFi|Bluetooth));
} else if(test==3){
Fixture f;f.b.busy=true;f.run("set 2.4ghz off force",Bluetooth);
f.tick();assert(f.b.active==All);f.tick(1749);assert(f.b.active==All);
f.tick(1);assert(f.b.active==0);assert(f.c.blocked(All));
assert((f.b.calls==std::vector<int>{-WiFi,-EspNow,-Bluetooth}));
f.b.calls.clear();f.b.busy=false;f.run("set 2.4ghz on");f.tick();
assert((f.b.calls==std::vector<int>{EspNow,WiFi,Bluetooth}));
} else if(test==4){
Fixture f;f.b.wait=WiFi;f.run("set 2.4ghz off");f.tick();
assert(f.c.pending()&&f.b.calls.size()==1&&f.b.active==All);
f.run("set espnow off");assert(strstr(f.reply,"pending"));
f.b.wait=0;f.tick(1);assert(f.b.active==0&&!f.c.pending());
f.b.fail=EspNow;f.run("set 2.4ghz on");f.tick();
assert(f.b.active==0&&!f.c.pending());f.run("get 2.4ghz");assert(strstr(f.reply,"failed"));
f.b.fail=0;f.run("set 2.4ghz on");f.tick();assert(f.b.active==All); // saved state survives failure
} else if(test==5){
Fixture f;f.run("set 2.4ghz off");f.tick(249);
f.run("set 2.4ghz on");f.tick();assert(f.b.active==All&&!f.c.masterOff());
f.b.sessions=Independent;f.run("set 2.4ghz off",Bluetooth);f.b.sessions=0;f.tick();
assert(f.b.active==All&&!f.c.masterOff());
f.now=UINT32_MAX-249;f.run("set 2.4ghz off");f.tick(249);assert(f.b.active==All);
f.tick(1);assert(f.b.active==0);
} else if(test==6){
Fixture f;f.b.supported=Bluetooth;f.b.active=Bluetooth;
f.run("set wifi on");assert(strstr(f.reply,"unavailable"));
f.run("get 2.4ghz");assert(strstr(f.reply,"wifi=unavailable")&&strstr(f.reply,"bluetooth=on"));
f.run("set 2.4ghz off");f.tick();assert(f.b.active==0);
f.run("set 2.4ghz on all");f.tick();assert(f.b.active==Bluetooth);
f.b.supported=0;f.run("set 2.4ghz on");assert(strstr(f.reply,"unavailable"));
} else if(test==7){
Fixture f;f.b.wait=WiFi;f.run("set 2.4ghz off");f.tick(30250);
assert(!f.c.pending()&&f.b.active==All&&f.b.calls.size()==1);
f.run("get 2.4ghz");assert(strstr(f.reply,"failed"));
f.b.wait=0;f.run("set 2.4ghz off");f.tick();assert(f.b.active==0);
}
}
'''
class WirelessControlTests(unittest.TestCase):
def test_compiled_controller(self):
with tempfile.TemporaryDirectory() as tmp:
source = Path(tmp)/'wireless.cpp'
source.write_text('#include <cstdlib>\n'+HARNESS)
binary = Path(tmp)/'wireless'
subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++11', '-Wall', '-Wextra', '-Werror',
'-I', str(ROOT/'src'), str(source), '-o', str(binary)], check=True)
for scenario in range(8):
with self.subTest(scenario=scenario):
subprocess.run([str(binary), str(scenario)], check=True)
def test_actual_companion_transport_adapter(self):
from test_companion_bluetooth_control import HARNESS as BT_HARNESS
main = (ROOT/'examples/companion_radio/main.cpp').read_text()
prefix = BT_HARNESS.split('@METHODS@')[0].replace(
' bool isAnyNetworkTerminalMode()',
' bool isWebConfigActiveOrStopping(){return false;}\n bool isAnyNetworkTerminalMode()')
adapter = method(main, 'class CompanionWirelessBackend')+';\n'
adapter += method(main, 'static void disableCompanionBluetoothForCli(')+'\n'
adapter += method(main, 'bool handleCompanionWirelessCommand(')
source_text = prefix+r'''
#include "examples/companion_radio/CompanionWireless.h"
bool companion_wifi_requested=true,companion_wifi_active=true;
bool companion_wifi_setup_requested=false,companion_wifi_disable_in_progress=false;
bool stop_pending=false;
struct {int mode_value=0;void mode(int value){mode_value=value;}void setAutoReconnect(bool){}} WiFi;
#define WIFI_OFF 0
bool finishStoppingCompanionWiFi(){
if(stop_pending)return false;
companion_wifi_active=false;wifi_interface.disable();return true;
}
void startCompanionWiFi(){companion_wifi_active=true;wifi_interface.enable();}
struct {
bool active=true;
bool isEnabled(){return active;}
void end(){active=false;}
void init(){active=true;}
} radio_driver;
static void disableCompanionBluetoothForCli();
'''+adapter+r'''
int main(){
CompanionWirelessBackend backend;mesh::wireless::control().begin(backend);
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface);
interface_manager.addInterface(InterfaceType::WiFi,&wifi_interface);
interface_manager.addInterface(InterfaceType::Ethernet,&ethernet_interface);
interface_manager.enable();bluetooth_interface.connected=true;
assert(backend.available()==mesh::wireless::All);
assert(backend.enabled()==mesh::wireless::All);
// UART/HWCDC power cannot authorize cutting all wireless connections.
assert(!(backend.clients()&mesh::wireless::Independent));
char reply[160];
handleCompanionWirelessCommand("set 2.4ghz off",reply,sizeof(reply),CompanionWirelessSource::Network);
assert(strstr(reply,"Error:"));
// A stale framed BLE route must not disguise an actual USB ASCII request.
bluetooth_interface.frame=true;uint8_t frame[176];interface_manager.checkRecvFrame(frame);
handleCompanionWirelessCommand("set 2.4ghz off",reply,sizeof(reply),CompanionWirelessSource::Usb);
assert(strstr(reply,"requested"));stop_pending=true;now_ms+=250;
mesh::wireless::control().service(now_ms);
assert(radio_driver.active&&bluetooth_interface.enabled); // wait for WiFi tasks/sockets first
stop_pending=false;mesh::wireless::control().service(++now_ms);
assert(!radio_driver.active&&!bluetooth_interface.enabled&&!companion_wifi_active);
assert(!companion_wifi_requested);
handleCompanionWirelessCommand("set 2.4ghz on",reply,sizeof(reply),CompanionWirelessSource::Usb);
now_ms+=250;mesh::wireless::control().service(now_ms);
assert(radio_driver.active&&bluetooth_interface.enabled&&companion_wifi_active);
assert(companion_wifi_requested);
// WiFi's framed client must not be mistaken for independent USB.
wifi_interface.frame=true;wifi_interface.connected=true;bluetooth_interface.connected=false;
interface_manager.checkRecvFrame(frame);
handleCompanionWirelessCommand("set wifi off",reply,sizeof(reply),CompanionWirelessSource::Framed);
assert(strstr(reply,"Error:"));
// An actual framed USB request is proof even without DTR on a UART bridge.
usb_serial_interface.frame=true;usb_serial_interface.connected=true;
interface_manager.checkRecvFrame(frame);
handleCompanionWirelessCommand("set espnow off",reply,sizeof(reply),CompanionWirelessSource::Framed);
now_ms+=250;mesh::wireless::control().service(now_ms);
assert(!radio_driver.active&&companion_wifi_active&&bluetooth_interface.enabled);
}
'''
with tempfile.TemporaryDirectory() as tmp:
folder = Path(tmp)
(folder/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cstddef>\n#include <initializer_list>\n')
(folder/'adapter.cpp').write_text(source_text)
binary=folder/'adapter'
subprocess.run([os.environ.get('CXX','g++'), '-std=c++17',
'-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'),
'-DESP32=1','-DBLE_PIN_CODE=1','-DWIFI_SSID=""',
'-DWITH_WEBCONFIG=1','-DMESH_PRIMARY_ESPNOW=1',
'-DENABLE_USB_INTERFACE=1','-DCOMPANION_FEATURE_TEXT_TERMINAL=1',
'-DCOMPANION_FEATURE_NETWORK_TERMINAL=1',
str(folder/'adapter.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
def test_role_and_restart_wiring(self):
for role, backend in [('companion_radio','companion_wireless'),
('simple_repeater','infrastructure_wireless'),
('simple_room_server','infrastructure_wireless')]:
source = (ROOT/'examples'/role/'main.cpp').read_text()
self.assertIn(f'control().begin({backend})', source)
self.assertIn('control().service(millis())', source[source.index('\nvoid loop()'):])
self.assertIn('&& !mesh::wireless::control().pending()', source[source.index('\nvoid loop()'):])
gates = {
'src/helpers/bridges/MQTTBridge.cpp': 'WiFi',
'src/helpers/bridges/ESPNowBridge.cpp': 'EspNow',
'src/helpers/esp32/ESPNOWRadio.cpp': 'EspNow',
'src/helpers/esp32/WebConfigServer.cpp': 'WiFi',
'src/helpers/WiFiSetupPortal.cpp': 'WiFi',
'src/helpers/MultiSerialInterface.h': 'Bluetooth',
}
for name, bit in gates.items():
with self.subTest(file=name):
self.assertIn(f'control().blocked(mesh::wireless::{bit})', (ROOT/name).read_text())
def test_actual_infrastructure_transport_adapter(self):
source = r'''
#include <helpers/WirelessControl.h>
#include <cassert>
enum {WIFI_STA=1,WIFI_OFF=0,WIFI_IF_STA=0};
struct {bool usb=false;bool isUsbDataConnected(){return usb;}} board;
struct {
int mode_value=WIFI_STA;bool autoreconnect=true;
void setAutoReconnect(bool value){autoreconnect=value;}
void disconnect(bool,bool){}
void mode(int value){mode_value=value;}
} WiFi;
namespace mesh {namespace wifi {void applyProtocolMask(int){}void restoreEspNowChannel(){}}}
struct {
bool active=true;bool isEnabled(){return active;}
void init(){active=true;}void end(){active=false;}
} radio_driver;
struct {
bool web=true,stopping=false,bridge=true,network=false;
bool isWebConfigActive(){return web||stopping;}
bool isWebConfigStopping(){return stopping;}
bool hasWirelessNetworkClient(){return network;}
bool isBridgeRunning(){return bridge;}
bool setBridgeState(bool on){bridge=on;return true;}
bool stopWebConfig(char*){web=false;stopping=true;return true;}
bool startWebConfig(bool,char*){web=true;return true;}
} the_mesh;
#include "examples/InfrastructureWireless.h"
int main(){
InfrastructureWirelessBackend b;mesh::wireless::Control c;c.begin(b);
char reply[160];using namespace mesh::wireless;
#ifdef WITH_MQTT_BRIDGE
assert(b.available()==(mesh::wireless::WiFi|EspNow));
assert(b.enabled()==(mesh::wireless::WiFi|EspNow));
assert(b.clients()==0);the_mesh.network=true;assert(b.clients()==mesh::wireless::WiFi);
c.handle("set 2.4ghz off",reply,sizeof(reply),0,mesh::wireless::WiFi);assert(strstr(reply,"Error:"));
c.handle("set 2.4ghz off",reply,sizeof(reply),0,Independent);c.service(250);
assert(c.pending()&&!the_mesh.bridge&&the_mesh.stopping&&radio_driver.active);
the_mesh.stopping=false;c.service(251);
assert(!c.pending()&&!radio_driver.active&&::WiFi.mode_value==WIFI_OFF);
c.handle("set 2.4ghz on",reply,sizeof(reply),300,Independent);c.service(550);
assert(the_mesh.bridge&&the_mesh.web&&radio_driver.active);
// Individual WiFi off retains the ESP-NOW driver and stops both its own services.
c.handle("set wifi off",reply,sizeof(reply),600,Independent);c.service(850);
the_mesh.stopping=false;c.service(851);
assert(!the_mesh.bridge&&!the_mesh.web&&radio_driver.active&&::WiFi.mode_value==WIFI_STA);
c.handle("set wifi on",reply,sizeof(reply),900,Independent);c.service(1150);
assert(the_mesh.bridge&&the_mesh.web&&radio_driver.active);
c.handle("set espnow off",reply,sizeof(reply),1200,Independent);c.service(1450);
assert(!radio_driver.active&&the_mesh.web&&the_mesh.bridge&&::WiFi.mode_value==WIFI_STA);
#else
assert(b.available()==EspNow&&b.enabled()==EspNow);
c.handle("set espnow off",reply,sizeof(reply),0,Independent);c.service(250);assert(!the_mesh.bridge);
c.handle("set espnow on",reply,sizeof(reply),300,Independent);c.service(550);assert(the_mesh.bridge);
#endif
}
'''
with tempfile.TemporaryDirectory() as tmp:
folder=Path(tmp)
(folder/'helpers/esp32').mkdir(parents=True)
(folder/'WiFi.h').write_text('#pragma once\n')
(folder/'helpers/esp32/WiFiRadioPolicy.h').write_text('#pragma once\n')
(folder/'infrastructure.cpp').write_text(source)
for defines in [('WITH_MQTT_BRIDGE','WITH_WEBCONFIG','MESH_PRIMARY_ESPNOW'),('WITH_ESPNOW_BRIDGE',)]:
with self.subTest(features=defines):
binary=folder/'infrastructure'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-DESP32=1',
'-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'),
*['-D'+d+'=1' for d in defines],str(folder/'infrastructure.cpp'),
'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
if __name__ == '__main__':
unittest.main()