mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-24 03:22:23 +00:00
fix: stabilize primary ESP-NOW WiFi coexistence
This commit is contained in:
1 parent
fefd1a9577
commit
11e70a5b32
11 files changed
+77
-21
No files matched your search
+3
-1
@@ -256,7 +256,9 @@ distinct from `bridge.channel`, which applies only to the separate ESP-NOW
|
||||
bridge feature. WiFi power saving does not let ESP-NOW and infrastructure WiFi
|
||||
operate on different channels. On an ESP32 Full build whose primary radio is
|
||||
ESP-NOW, `wifi.powersave max` is unavailable because a station using maximum
|
||||
modem sleep can miss ESP-NOW broadcasts; use `min` for coexistence.
|
||||
modem sleep can miss ESP-NOW broadcasts; use `min` for coexistence. The primary
|
||||
mesh transport holds the driver's RF wake reference continuously, so `min`
|
||||
does not suspend its ESP-NOW receiver.
|
||||
|
||||
ESP-NOW compatibility also depends on the bytes and PHY used above the common
|
||||
Espressif transport. The historical `*_repeater_bridge_espnow` firmware wraps
|
||||
|
||||
@@ -272,7 +272,10 @@ Companion whose primary mesh radio is ESP-NOW, `max` is also unavailable: a
|
||||
station using maximum modem sleep can miss ESP-NOW broadcasts, which the access
|
||||
point does not buffer for it. A previously saved `max` value is capped to and
|
||||
reported as `min`, and a new `max` selection is rejected. These
|
||||
WiFi/BLE/primary-ESP-NOW builds therefore use `min`.
|
||||
WiFi/BLE/primary-ESP-NOW builds therefore report `min`. The primary mesh radio
|
||||
also holds the ESP-IDF WiFi wake reference continuously so unsolicited ESP-NOW
|
||||
frames remain receivable; selecting `min` does not put that primary receiver to
|
||||
sleep.
|
||||
|
||||
#### View or change the primary ESP-NOW/WiFi channel
|
||||
|
||||
@@ -299,7 +302,8 @@ this node, plus the configured router's 2.4 GHz radio, must use the same fixed
|
||||
channel. WiFi power saving does not allow the transports to use different
|
||||
channels. On an ESP32 Full build with primary ESP-NOW, `wifi.powersave max` is
|
||||
unavailable because maximum modem sleep can miss ESP-NOW broadcasts; use `min`
|
||||
for coexistence.
|
||||
for coexistence. The firmware keeps the primary ESP-NOW receiver awake while
|
||||
still using the `min` WiFi/Bluetooth coexistence policy.
|
||||
|
||||
`espnow.channel` is the channel of the primary ESP-NOW mesh transport. It is
|
||||
separate from `bridge.channel`, which configures only an ESP-NOW bridge on a
|
||||
|
||||
@@ -70,7 +70,9 @@ backward-compatible default is the distinct wrapped format.
|
||||
WiFi power saving cannot put infrastructure WiFi and ESP-NOW on different
|
||||
channels. On these two primary-ESP-NOW Full targets, `max` is unavailable
|
||||
because maximum modem sleep can make the station miss ESP-NOW broadcasts; use
|
||||
`min` for WiFi/BLE/ESP-NOW coexistence.
|
||||
`min` for WiFi/BLE/ESP-NOW coexistence. The firmware also holds the ESP-IDF RF
|
||||
wake reference for the primary mesh radio, so the ESP-NOW receiver remains
|
||||
continuous even though the reported coexistence setting is `min`.
|
||||
|
||||
List the available targets:
|
||||
|
||||
|
||||
@@ -42,5 +42,13 @@ inline uint8_t validEspNowChannelOrDefault(uint8_t stored,
|
||||
return isValidEspNowChannel(stored) ? stored : fallback;
|
||||
}
|
||||
|
||||
// Rewriting an ESP32's current WiFi channel is not a harmless no-op. The
|
||||
// driver briefly reconfigures the shared RF path, which can interrupt SoftAP
|
||||
// beacons and ESP-NOW receive when a caller repeats the write from a fast main
|
||||
// loop. Only request a restore when the primary channel actually moved.
|
||||
inline bool espNowChannelRestoreRequired(uint8_t current, uint8_t target) {
|
||||
return current != target;
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace mesh
|
||||
@@ -334,10 +334,7 @@ bool WiFiSetupPortal::begin(const char* ap_name, SaveCallback save_callback, voi
|
||||
if (!WiFi.softAPConfig(SETUP_IP, SETUP_IP, SETUP_MASK)
|
||||
|| !WiFi.softAP(impl->ap_name, nullptr,
|
||||
mesh::wifi::accessPointChannel())
|
||||
|| esp_wifi_set_protocol(
|
||||
WIFI_IF_AP,
|
||||
mesh::wifi::kProtocolMask)
|
||||
!= ESP_OK
|
||||
|| mesh::wifi::applyAccessPointProtocolMask() != ESP_OK
|
||||
|| esp_wifi_set_protocol(
|
||||
WIFI_IF_STA,
|
||||
mesh::wifi::kProtocolMask)
|
||||
|
||||
@@ -129,6 +129,19 @@ void ESPNOWRadio::init() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
||||
// Modem sleep is still required when BLE and infrastructure WiFi coexist,
|
||||
// but unicast/broadcast ESP-NOW frames are not buffered by an access point.
|
||||
// Hold the driver's documented wake reference for the lifetime of this
|
||||
// primary mesh radio so receive stays continuous without disabling the
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_wifi_set_max_tx_power(80); // should be 20dBm
|
||||
|
||||
esp_now_register_send_cb(OnDataSent);
|
||||
|
||||
@@ -774,8 +774,8 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
// LR bit set. The driver then reports a healthy SoftAP, but ordinary
|
||||
// phones and laptops cannot discover its beacon. A WiFi companion must
|
||||
// advertise using the interoperable 2.4 GHz protocol set.
|
||||
const esp_err_t ap_protocol_result = esp_wifi_set_protocol(
|
||||
WIFI_IF_AP, mesh::wifi::kProtocolMask);
|
||||
const esp_err_t ap_protocol_result =
|
||||
mesh::wifi::applyAccessPointProtocolMask();
|
||||
const esp_err_t sta_protocol_result = esp_wifi_set_protocol(
|
||||
WIFI_IF_STA, mesh::wifi::kProtocolMask);
|
||||
if (ap_protocol_result == ESP_OK && sta_protocol_result == ESP_OK) break;
|
||||
@@ -812,7 +812,10 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
_initial_setup = _wifi_ssid[0] == 0 && node.admin_password[0] != 0;
|
||||
_setup_started_at = millis();
|
||||
_last_activity = _setup_started_at;
|
||||
WiFi.scanNetworks(true); // pre-populate the SSID picker
|
||||
// A primary ESP-NOW radio cannot leave its selected channel while scanning.
|
||||
// Ordinary WiFi targets retain the zero-channel all-band scan.
|
||||
WiFi.scanNetworks(true, false, false, 300,
|
||||
mesh::wifi::stationScanChannel());
|
||||
|
||||
sprintf(reply, "WebConfig AP started: join '%s' then open http://%s/", _ap_ssid, ip.toString().c_str());
|
||||
return true;
|
||||
@@ -2329,7 +2332,8 @@ void WebConfigServer::handleScan(AsyncWebServerRequest* req) {
|
||||
n = WIFI_SCAN_FAILED;
|
||||
}
|
||||
if (n == WIFI_SCAN_FAILED) {
|
||||
WiFi.scanNetworks(true);
|
||||
WiFi.scanNetworks(true, false, false, 300,
|
||||
mesh::wifi::stationScanChannel());
|
||||
req->send(200, "application/json", "{\"state\":\"scanning\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ static constexpr uint8_t kProtocolMask = WIFI_PROTOCOL_11B
|
||||
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N;
|
||||
#endif
|
||||
|
||||
// LR is private to ESP-NOW on the station interface. A setup SoftAP must keep
|
||||
// an ordinary b/g/n protocol bitmap so phones and laptops can discover it.
|
||||
static constexpr uint8_t kAccessPointProtocolMask = WIFI_PROTOCOL_11B
|
||||
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N;
|
||||
|
||||
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
||||
struct EspNowBootChannelState {
|
||||
bool loaded;
|
||||
@@ -98,6 +103,10 @@ inline esp_err_t applyProtocolMask(wifi_interface_t interface_id) {
|
||||
return esp_wifi_set_protocol(interface_id, kProtocolMask);
|
||||
}
|
||||
|
||||
inline esp_err_t applyAccessPointProtocolMask() {
|
||||
return esp_wifi_set_protocol(WIFI_IF_AP, kAccessPointProtocolMask);
|
||||
}
|
||||
|
||||
// An ESP-NOW radio and an associated station cannot occupy different channels.
|
||||
// Full Companion therefore keeps both its setup AP and any infrastructure-WiFi
|
||||
// association on the mesh channel. Passing zero on ordinary targets preserves
|
||||
@@ -118,12 +127,18 @@ inline uint8_t stationScanChannel() {
|
||||
|
||||
inline esp_err_t restoreEspNowChannel() {
|
||||
if (!kKeepEspNowRadioRunning) return ESP_OK;
|
||||
const uint8_t target = activeEspNowChannel();
|
||||
uint8_t current = 0;
|
||||
wifi_second_chan_t secondary = WIFI_SECOND_CHAN_NONE;
|
||||
if (esp_wifi_get_channel(¤t, &secondary) == ESP_OK
|
||||
&& !espNowChannelRestoreRequired(current, target)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
// Fail closed instead of silently moving to the build default. A failure can
|
||||
// be transient (scan/connect in progress), and changing the cached boot
|
||||
// channel here would split this node from peers which still use the selected
|
||||
// channel. Callers retry through their normal reconnect/tick paths.
|
||||
return esp_wifi_set_channel(
|
||||
activeEspNowChannel(), WIFI_SECOND_CHAN_NONE);
|
||||
return esp_wifi_set_channel(target, WIFI_SECOND_CHAN_NONE);
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
|
||||
@@ -76,13 +76,10 @@ inline wl_status_t beginStation(const char* ssid, const char* password) {
|
||||
// follow their existing disconnected/retry path.
|
||||
inline bool enforceStationChannel() {
|
||||
if (!kPrimaryEspNowRadio) return true;
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
// A disconnect and scan/association completion are asynchronous. Keep
|
||||
// reasserting the boot channel so a first ESP_ERR_WIFI_STATE cannot leave
|
||||
// the mesh parked on the router's former channel.
|
||||
restoreEspNowChannel();
|
||||
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.
|
||||
if (WiFi.status() != WL_CONNECTED) return true;
|
||||
if (WiFi.channel() == activeEspNowChannel()) return true;
|
||||
WiFi.disconnect(false, false);
|
||||
restoreEspNowChannel();
|
||||
|
||||
@@ -755,6 +755,13 @@ TEST(WiFiChannelPolicy, FallsBackOnlyWhenTheStoredChannelIsInvalid) {
|
||||
EXPECT_EQ(1, mesh::wifi::validEspNowChannelOrDefault(14, 1));
|
||||
}
|
||||
|
||||
TEST(WiFiChannelPolicy, RestoresOnlyWhenTheRadioActuallyMoved) {
|
||||
EXPECT_FALSE(mesh::wifi::espNowChannelRestoreRequired(1, 1));
|
||||
EXPECT_FALSE(mesh::wifi::espNowChannelRestoreRequired(6, 6));
|
||||
EXPECT_TRUE(mesh::wifi::espNowChannelRestoreRequired(1, 6));
|
||||
EXPECT_TRUE(mesh::wifi::espNowChannelRestoreRequired(13, 1));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, ObserverSettingsNeverStartADisabledBridge) {
|
||||
int calls = 0;
|
||||
EXPECT_TRUE(mesh::cli::restartBridgeIfEnabled(false, [&calls]() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define P_LORA_HF_PA_POWER 3
|
||||
#define ST7789_CS 16
|
||||
#define ST7789_RS 15
|
||||
#define USER_GPIO_RESERVED_PINS 40, 41
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <helpers/UserGpio.h>
|
||||
@@ -52,6 +53,12 @@ TEST(UserGpioPinPolicyTest, ReservesRadioPaAndSt7789ControlAliases) {
|
||||
EXPECT_FALSE(UserGpioPinPolicy::isFirmwareReserved(22));
|
||||
}
|
||||
|
||||
TEST(UserGpioPinPolicyTest, ReservesBoardSpecificDirectIoPins) {
|
||||
EXPECT_TRUE(UserGpioPinPolicy::isFirmwareReserved(40));
|
||||
EXPECT_TRUE(UserGpioPinPolicy::isFirmwareReserved(41));
|
||||
EXPECT_FALSE(UserGpioPinPolicy::isFirmwareReserved(39));
|
||||
}
|
||||
|
||||
TEST_F(UserGpioTest, ListsOnlyBoardApprovedPins) {
|
||||
UserGpio gpio(board);
|
||||
|
||||
|
||||
Reference in new issue
Block a user