fix: stabilize primary ESP-NOW WiFi coexistence

This commit is contained in:
mikecarper committed 2026-08-28 21:01:17 -07:00
1 parent fefd1a9577
commit 11e70a5b32
11 files changed
+77 -21

No files matched your search

+8
View File
@@ -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
+1 -4
View File
@@ -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)
+13
View File
@@ -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);
+8 -4
View File
@@ -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;
}
+17 -2
View File
@@ -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(&current, &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
+4 -7
View File
@@ -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();