Add T-Beam WiFi button toggle

This commit is contained in:
mikecarper committed 2026-08-15 18:12:09 -07:00
1 parent 933697f65c
commit 7afa6b11a8
15 files changed
+263 -45

No files matched your search

+6
View File
@@ -181,6 +181,12 @@ service on ports 5001 and 5002. See the
[full Companion guide](./companion_radio_full.md) for its build, terminal mode,
and complete update-source workflow.
The LilyGo T-Beam 1W Full Companion maps a triple click of the physical button
labeled `17` to a persistent WiFi on/off toggle. Turning WiFi off closes the
WebConfig, Companion TCP, mOTA, and MQTT network services while USB, BLE, the
display, GPS, and LoRa remain available. The same triple click turns WiFi back
on even after reboot.
The companion loads runtime credentials saved in NVS. A non-placeholder
compile-time `WIFI_SSID`/`WIFI_PWD` can be used as a first-boot fallback, but
saved credentials take priority. With no credentials, its WebConfig portal
+7
View File
@@ -80,6 +80,13 @@ clock and keeps GPS awake. Full Companion transports remain available in both
states; WiFi modem sleep stays enabled when BLE is present because coexistence
requires it. The selected state is retained after reboot.
On the LilyGo T-Beam 1W Full Companion, triple-click the physical button
labeled `17` to turn the ESP32 WiFi radio and all WiFi services off or on. The
screen confirms `WiFi: OFF` or `WiFi: ON`, and the selected state is retained
after reboot. When WiFi is off, TCP ports 5000-5002, WebConfig, and MQTT are
stopped; USB, BLE, the display, GPS, and LoRa continue to operate. Triple-click
button 17 again to restore WiFi, including the saved station or setup-AP mode.
Artifacts are written to `out/` by default.
On 4 MB ESP32 boards, the full target uses a single 3 MB application
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#if defined(ESP32) && defined(WIFI_SSID)
// The requested state is persisted immediately. Network services transition
// from the main loop so a button callback never tears down an active server.
bool toggleCompanionWiFi();
bool isCompanionWiFiEnabled();
#endif
+6
View File
@@ -336,6 +336,10 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d
if (file.available() >= (int)sizeof(_prefs.powersaving_enabled)) {
file.read((uint8_t *)&_prefs.powersaving_enabled,
sizeof(_prefs.powersaving_enabled)); // 137
if (file.available() >= (int)sizeof(_prefs.wifi_enabled)) {
file.read((uint8_t *)&_prefs.wifi_enabled,
sizeof(_prefs.wifi_enabled)); // 138
}
}
}
@@ -401,6 +405,8 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
sizeof(_prefs.rx_ps_preamble)) == sizeof(_prefs.rx_ps_preamble); // 136
success = success && file.write((uint8_t *)&_prefs.powersaving_enabled,
sizeof(_prefs.powersaving_enabled)) == sizeof(_prefs.powersaving_enabled); // 137
success = success && file.write((uint8_t *)&_prefs.wifi_enabled,
sizeof(_prefs.wifi_enabled)) == sizeof(_prefs.wifi_enabled); // 138
#if defined(NRF52_PLATFORM)
success = file.commit(success);
+11
View File
@@ -1356,6 +1356,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
_prefs.powersaving_enabled = 0;
_prefs.wifi_enabled = 1;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
@@ -1439,6 +1440,7 @@ void MyMesh::begin(bool has_display) {
_prefs.radio_fem_txgain = constrain(_prefs.radio_fem_txgain, 0, 1);
_prefs.rx_powersaving_enabled = constrain(_prefs.rx_powersaving_enabled, 0, 1);
_prefs.powersaving_enabled = constrain(_prefs.powersaving_enabled, 0, 1);
_prefs.wifi_enabled = constrain(_prefs.wifi_enabled, 0, 1);
_prefs.rx_ps_level = constrain(_prefs.rx_ps_level, 0, 10);
if (_prefs.rx_ps_preamble != 16 && _prefs.rx_ps_preamble != 32) {
_prefs.rx_ps_preamble = 0;
@@ -1813,6 +1815,11 @@ void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password) {
}
}
}
void MyMesh::stopMQTT() {
if (_mqtt_started && _mqtt_bridge) _mqtt_bridge->end();
_mqtt_started = false;
}
#endif
#ifdef WITH_WEBCONFIG
@@ -1938,6 +1945,10 @@ void MyMesh::serviceWebConfig() {
if (_webconfig) _webconfig->tick(millis());
}
bool MyMesh::isWebConfigActiveOrStopping() const {
return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping());
}
bool MyMesh::isWebConfigSetupActive() const {
return _webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP
&& !_webconfig->isStopping();
+2
View File
@@ -121,6 +121,7 @@ public:
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
void serviceMQTT(const char* wifi_ssid, const char* wifi_password);
void stopMQTT();
bool isMQTTConfigured() const { return _mqtt_configured; }
#endif
@@ -128,6 +129,7 @@ public:
bool startWebConfig(bool force_ap, char* reply);
void stopWebConfig();
void serviceWebConfig();
bool isWebConfigActiveOrStopping() const;
bool isWebConfigSetupActive() const;
bool isWebConfigWiFiRecoveryActive() const;
+1
View File
@@ -45,6 +45,7 @@ struct CompanionNodePrefs { // persisted to file
uint8_t rx_ps_level; // 0=manual timings, 1..10=level-derived
uint8_t rx_ps_preamble; // 0=auto from SF, otherwise 16 or 32
uint8_t powersaving_enabled; // device CPU/GPS idle power saving
uint8_t wifi_enabled; // Companion WiFi radio and services
// Keep the upstream repeat API while retaining the existing binary prefs
// layout used by this branch.
+162 -43
View File
@@ -1,6 +1,7 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "MyMesh.h"
#include "CompanionWiFi.h"
#ifdef ESP32_PLATFORM
#include "esp_bt.h"
@@ -431,6 +432,27 @@ void halt() {
bool wifi_setup_recovery_mode = false;
static char configured_wifi_ssid[32];
static char configured_wifi_password[64];
static bool companion_wifi_has_credentials = false;
static bool companion_wifi_requested = true;
static bool companion_wifi_active = false;
static bool companion_wifi_disable_in_progress = false;
static bool companion_wifi_services_stopped = false;
bool isCompanionWiFiEnabled() {
return companion_wifi_requested;
}
bool toggleCompanionWiFi() {
companion_wifi_requested = !companion_wifi_requested;
the_mesh.getNodePrefs()->wifi_enabled = companion_wifi_requested ? 1 : 0;
the_mesh.savePrefs();
if (!companion_wifi_requested && companion_wifi_active) {
companion_wifi_disable_in_progress = true;
}
WIFI_DEBUG_PRINTLN("GPIO 17 triple click requested WiFi %s",
companion_wifi_requested ? "on" : "off");
return companion_wifi_requested;
}
#if defined(WITH_WEBCONFIG) && defined(DISPLAY_CLASS)
static DisplayDriver* companion_setup_display = nullptr;
@@ -492,6 +514,7 @@ void halt() {
configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0';
strncpy(configured_wifi_password, password ? password : "", sizeof(configured_wifi_password) - 1);
configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0';
companion_wifi_has_credentials = true;
return true;
}
#endif
@@ -502,6 +525,7 @@ void halt() {
`normalradio` here so a host can run an end-to-end mOTA source without occupying the binary port. */
#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA)
#include <helpers/ota/OtaCli.h> // mesh::ota::handle_ota_command(line, reply, board)
#include <helpers/esp32/WiFiOtaSeeder.h>
#ifndef OTA_CONSOLE_TCP_PORT
#define OTA_CONSOLE_TCP_PORT 5002
#endif
@@ -510,6 +534,18 @@ void halt() {
static char ota_console_line[128];
static uint8_t ota_console_len = 0;
static void ota_console_start() {
ota_console_server.begin();
WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc <ip> %d, type `ota ...`)",
OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT);
}
static void ota_console_stop() {
if (ota_console_client) ota_console_client.stop();
ota_console_server.end();
ota_console_len = 0;
}
static void ota_console_loop() {
if (!ota_console_client || !ota_console_client.connected()) {
WiFiClient c = ota_console_server.available();
@@ -543,6 +579,107 @@ void halt() {
}
#endif
#if defined(ESP32) && defined(WIFI_SSID)
static void resetCompanionWiFiRecoveryState() {
wifi_reconnect_tracker = WiFiReconnectPolicy::Tracker();
wifi_setup_attempted = false;
last_wifi_setup_attempt = 0;
wifi_setup_recovery_mode = false;
}
static void startCompanionWiFi() {
if (companion_wifi_active) return;
board.setInhibitSleep(true);
WiFi.setAutoReconnect(true);
resetCompanionWiFiRecoveryState();
if (companion_wifi_has_credentials) {
WiFi.mode(WIFI_STA);
wifi_reconnect_tracker.noteDisconnected(millis());
WiFi.begin(configured_wifi_ssid, configured_wifi_password);
}
#ifndef WITH_WEBCONFIG
else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP,
saveCompanionWiFi, nullptr)) {
WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal");
}
#endif
#ifdef WITH_WEBCONFIG
if (WebConfigServer::loadEnabled(true)) {
char web_reply[160];
the_mesh.startWebConfig(!companion_wifi_has_credentials, web_reply);
WIFI_DEBUG_PRINTLN("%s", web_reply);
}
#endif
#if defined(BLE_PIN_CODE)
WiFi.setSleep(true);
#else
WiFi.setSleep(false);
#endif
wifi_interface.begin(TCP_PORT);
wifi_interface.enable();
#ifdef ENABLE_OTA
ota_console_start();
#endif
companion_wifi_active = true;
companion_wifi_services_stopped = false;
WIFI_DEBUG_PRINTLN("WiFi enabled by GPIO 17 control");
}
static void stopCompanionWiFiServices() {
if (companion_wifi_services_stopped) return;
wifi_interface.end();
#ifdef ENABLE_OTA
ota_console_stop();
mesh::ota::WiFiOtaSeeder::stop();
#endif
#ifdef WITH_MQTT_BRIDGE
the_mesh.stopMQTT();
#endif
companion_wifi_services_stopped = true;
}
static bool finishStoppingCompanionWiFi() {
stopCompanionWiFiServices();
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigActiveOrStopping()) {
the_mesh.stopWebConfig();
return false;
}
#else
if (wifiSetupPortal().isActive()) {
wifiSetupPortal().stop();
return false;
}
if (wifiSetupPortal().isStopping()) return false;
#endif
WiFi.setAutoReconnect(false);
WiFi.disconnect(true, false);
WiFi.mode(WIFI_OFF);
board.setInhibitSleep(false);
resetCompanionWiFiRecoveryState();
companion_wifi_active = false;
WIFI_DEBUG_PRINTLN("WiFi radio disabled by GPIO 17 control");
return true;
}
static void serviceCompanionWiFiState() {
if (!companion_wifi_requested && companion_wifi_active) {
companion_wifi_disable_in_progress = true;
}
if (companion_wifi_disable_in_progress) {
if (!finishStoppingCompanionWiFi()) return;
companion_wifi_disable_in_progress = false;
}
if (companion_wifi_requested && !companion_wifi_active) {
startCompanionWiFi();
}
}
#endif
void setup() {
Serial.begin(115200);
board.begin();
@@ -644,64 +781,39 @@ void setup() {
// add wifi interface
#ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
WIFI_DEBUG_PRINTLN("WiFi disconnected; automatic recovery is active");
if (companion_wifi_requested) {
WIFI_DEBUG_PRINTLN("WiFi disconnected; automatic recovery is active");
}
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
WIFI_DEBUG_PRINTLN("connected! IP %s (companion app on :%d)",
WiFi.localIP().toString().c_str(), TCP_PORT);
}
});
bool have_wifi = WiFiSetupPortal::loadStoredCredentials(
companion_wifi_has_credentials = WiFiSetupPortal::loadStoredCredentials(
configured_wifi_ssid, sizeof(configured_wifi_ssid),
configured_wifi_password, sizeof(configured_wifi_password));
if (!have_wifi && !WiFiSetupPortal::isPlaceholderSSID(WIFI_SSID)) {
if (!companion_wifi_has_credentials
&& !WiFiSetupPortal::isPlaceholderSSID(WIFI_SSID)) {
strncpy(configured_wifi_ssid, WIFI_SSID, sizeof(configured_wifi_ssid) - 1);
configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0';
strncpy(configured_wifi_password, WIFI_PWD, sizeof(configured_wifi_password) - 1);
configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0';
have_wifi = true;
companion_wifi_has_credentials = true;
}
if (have_wifi) {
WiFi.mode(WIFI_STA);
wifi_reconnect_tracker.noteDisconnected(millis());
WiFi.begin(configured_wifi_ssid, configured_wifi_password);
}
#ifndef WITH_WEBCONFIG
else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal");
}
#endif
#ifdef WITH_WEBCONFIG
// WiFi companions expose the shared WebUI by default. With no stored
// credentials it opens the captive setup AP; otherwise it waits for the
// station connection and serves the same page on the LAN.
if (WebConfigServer::loadEnabled(true)) {
char web_reply[160];
the_mesh.startWebConfig(!have_wifi, web_reply);
WIFI_DEBUG_PRINTLN("%s", web_reply);
}
#endif
// ESP-IDF requires WiFi modem sleep when Bluetooth is active. Full ESP32
// companions provide both transports, so use the lowest sleep policy there;
// WiFi-only companions keep modem sleep disabled to avoid pauses in SX1262
// SPI/DIO service.
#if defined(BLE_PIN_CODE)
WiFi.setSleep(true);
#else
WiFi.setSleep(false);
#endif
wifi_interface.begin(TCP_PORT);
interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
#ifdef ENABLE_OTA
ota_console_server.begin(); // dedicated OTA text-console port (`nc <ip> 5002` -> `ota ...`)
WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc <ip> %d, type `ota ...`)", OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT);
#endif
companion_wifi_requested = the_mesh.getNodePrefs()->wifi_enabled != 0;
if (companion_wifi_requested) {
startCompanionWiFi();
} else {
WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_OFF);
board.setInhibitSleep(false);
WIFI_DEBUG_PRINTLN("WiFi remains off from the saved GPIO 17 setting");
}
#endif
// add usb interface
@@ -729,6 +841,9 @@ void setup() {
#endif
the_mesh.startInterface(interface_manager);
#if defined(ESP32) && defined(WIFI_SSID)
if (!companion_wifi_requested) wifi_interface.disable();
#endif
sensors.begin();
#if ENV_INCLUDE_GPS == 1
@@ -766,11 +881,12 @@ void loop() {
sensors.loop();
#ifdef DISPLAY_CLASS
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
if (the_mesh.isWebConfigSetupActive()
if (isCompanionWiFiEnabled() && (the_mesh.isWebConfigSetupActive()
#ifdef WITH_MQTT_BRIDGE
|| !the_mesh.isMQTTConfigured()
#endif
) {
)) {
ui_task.serviceWiFiToggleButton();
renderCompanionSetupDisplay();
} else {
ui_task.loop();
@@ -807,6 +923,8 @@ void loop() {
#ifdef WITH_WEBCONFIG
the_mesh.serviceWebConfig();
#endif
serviceCompanionWiFiState();
if (companion_wifi_requested && companion_wifi_active) {
#ifdef ENABLE_OTA
ota_console_loop(); // service the OTA text console (port 5002)
#endif
@@ -903,5 +1021,6 @@ void loop() {
#ifdef WITH_MQTT_BRIDGE
the_mesh.serviceMQTT(configured_wifi_ssid, configured_wifi_password);
#endif
}
#endif
}
+21 -2
View File
@@ -1,6 +1,7 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "../CompanionWiFi.h"
#include "target.h"
#ifdef WIFI_SSID
#include <WiFi.h>
@@ -234,8 +235,12 @@ public:
display.drawTextCentered(display.width() / 2, 22, tmp);
#ifdef WIFI_SSID
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
if (isCompanionWiFiEnabled()) {
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
} else {
strcpy(tmp, "WiFi: OFF");
}
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 54, tmp);
#endif
@@ -622,6 +627,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
setCurrScreen(splash);
}
void UITask::serviceWiFiToggleButton() {
#if defined(TBEAM_1W) && defined(WIFI_SSID) && defined(PIN_USER_BTN)
if (user_btn.check() == BUTTON_EVENT_TRIPLE_CLICK) {
handleTripleClick(KEY_SELECT);
}
#endif
}
void UITask::showAlert(const char* text, int duration_millis) {
strcpy(_alert, text);
_alert_expiry = millis() + duration_millis;
@@ -1000,7 +1013,13 @@ char UITask::handleDoubleClick(char c) {
char UITask::handleTripleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
checkDisplayOn(c);
#if defined(TBEAM_1W) && defined(WIFI_SSID)
const bool enabled = toggleCompanionWiFi();
showAlert(enabled ? "WiFi: ON" : "WiFi: OFF", 1200);
_next_refresh = 0;
#else
toggleBuzzer();
#endif
c = 0;
return c;
}
+1
View File
@@ -80,6 +80,7 @@ public:
curr = NULL;
}
void begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs);
void serviceWiFiToggleButton();
void gotoHomeScreen() { setCurrScreen(home); }
void showAlert(const char* text, int duration_millis);
+9
View File
@@ -321,6 +321,15 @@ bool WiFiSetupPortal::begin(const char* ap_name, SaveCallback save_callback, voi
return true;
}
void WiFiSetupPortal::stop() {
_active = false;
}
bool WiFiSetupPortal::isStopping() const {
const PortalImpl* impl = static_cast<const PortalImpl*>(_impl);
return impl && impl->task != nullptr;
}
void WiFiSetupPortal::configureRecovery(const char* ssid, const char* password,
uint32_t interval_ms,
uint32_t initial_delay_ms) {
+2
View File
@@ -14,6 +14,7 @@ public:
// Starts an open setup AP and captive HTTP form. The AP remains available
// after a failed association and shuts down shortly after a successful one.
bool begin(const char* ap_name, SaveCallback save_callback, void* context);
void stop();
// Retry interval is the steady-state cadence. initial_delay_ms defaults to
// that interval, but callers that opened the portal partway through an
// outage can preserve the original reconnect schedule with a shorter delay.
@@ -21,6 +22,7 @@ public:
uint32_t interval_ms, uint32_t initial_delay_ms = 0);
bool isActive() const { return _active; }
bool isStopping() const;
IPAddress apIP() const { return IPAddress(192, 168, 4, 1); }
// Companion WiFi builds keep their runtime credentials in NVS.
@@ -7,6 +7,15 @@ void SerialWifiInterface::begin(int port) {
server.begin(port);
}
void SerialWifiInterface::end() {
disable();
if (client) client.stop();
server.end();
deviceConnected = false;
resetReceivedFrameHeader();
clearBuffers();
}
// ---------- public methods
void SerialWifiInterface::enable() {
if (_isEnabled) return;
+1
View File
@@ -45,6 +45,7 @@ public:
}
void begin(int port);
void end();
// BaseSerialInterface methods
void enable() override;
@@ -65,6 +65,21 @@ TEST(CompanionNodePrefs, DevicePowerSavingIsIndependentFromRxps) {
EXPECT_EQ(1, prefs.powersaving_enabled);
}
TEST(CompanionNodePrefs, WiFiStateIsIndependentFromPowerSaving) {
CompanionNodePrefs prefs = {};
prefs.wifi_enabled = 1;
prefs.powersaving_enabled = 0;
EXPECT_EQ(1, prefs.wifi_enabled);
EXPECT_EQ(0, prefs.powersaving_enabled);
prefs.wifi_enabled = 0;
prefs.powersaving_enabled = 1;
EXPECT_EQ(0, prefs.wifi_enabled);
EXPECT_EQ(1, prefs.powersaving_enabled);
}
#if 0
// Re-enable test once we can SET fem_ values in companion
TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {