Add WiFi, ESP-NOW and combined wireless CLI controls
This commit is contained in:
1 parent
975167938d
commit
e9a2326c4f
25 files changed
+1311
-61
No files matched your search
@@ -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: >-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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&¤t_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()&¤t_channel==6);
|
||||
bridgeEspNowChannel().store(0);current_channel=11;
|
||||
assert(!espNowChannelConstrained()&&stationChannelHint()==0&&stationScanChannel()==0);
|
||||
assert(restoreEspNowChannel()==ESP_OK&¤t_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()
|
||||
@@ -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,ðernet_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()
|
||||
Reference in new issue
Block a user