Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/graphics/SharedUIDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "meshtastic/config.pb.h"
#include "modules/AirplaneMode.h"
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include <OLEDDisplay.h>
Expand All @@ -16,6 +17,7 @@
namespace graphics
{


ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth)
{

Expand Down Expand Up @@ -121,10 +123,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}

// === Screen Title ===
const char *headerTitle = titleStr ? titleStr : "";
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
if (AirplaneMode::instance().isActive()) {
const int iconX = (screenW - icon_airplane_width) / 2;
const int iconY = y + (highlightHeight - icon_airplane_height) / 2;
display->drawXbm(iconX, iconY, icon_airplane_width, icon_airplane_height, icon_airplane);
} else {
const char *headerTitle = titleStr ? titleStr : "";
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
const int titleX = (screenW - titleWidth) / 2;
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
}
}
display->setTextAlignment(TEXT_ALIGN_LEFT);

Expand Down
41 changes: 38 additions & 3 deletions src/graphics/draw/MenuHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "mesh/MeshTypes.h"
#include "mesh/RadioLibInterface.h"
#include "modules/AdminModule.h"
#include "modules/AirplaneMode.h"
#include "modules/CannedMessageModule.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/KeyVerificationModule.h"
Expand Down Expand Up @@ -2177,6 +2178,31 @@ void menuHandler::shutdownMenu()
screen->showOverlayBanner(bannerOptions);
}

void menuHandler::airplaneModeMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions;
bool active = AirplaneMode::instance().isActive();
bannerOptions.message = active ? "Exit Airplane Mode? Device will reboot." : "Airplane Mode? All radios off. Device will reboot.";
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = active ? "Exit Airplane?" : "Airplane?";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner(AirplaneMode::instance().isActive() ? "Exiting airplane mode..."
: "Entering airplane mode...",
0));
AirplaneMode::instance().toggle();
} else {
menuQueue = PowerMenu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}

void menuHandler::removeFavoriteMenu()
{

Expand Down Expand Up @@ -2377,9 +2403,9 @@ void menuHandler::screenOptionsMenu()
void menuHandler::powerMenu()
{

enum optionsNumbers { Back, Reboot, Shutdown, MUI };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
enum optionsNumbers { Back, Reboot, Shutdown, Airplane, MUI };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
int options = 1;

optionsArray[options] = "Reboot";
Expand All @@ -2388,6 +2414,9 @@ void menuHandler::powerMenu()
optionsArray[options] = "Shutdown";
optionsEnumArray[options++] = Shutdown;

optionsArray[options] = AirplaneMode::instance().isActive() ? "Exit Airplane" : "Airplane Mode";
optionsEnumArray[options++] = Airplane;

#if HAS_TFT
optionsArray[options] = "Switch to MUI";
optionsEnumArray[options++] = MUI;
Expand All @@ -2408,6 +2437,9 @@ void menuHandler::powerMenu()
} else if (selected == Shutdown) {
menuHandler::menuQueue = menuHandler::ShutdownMenu;
screen->runNow();
} else if (selected == Airplane) {
menuHandler::menuQueue = menuHandler::AirplaneModeMenu;
screen->runNow();
} else if (selected == MUI) {
menuHandler::menuQueue = menuHandler::MuiPicker;
screen->runNow();
Expand Down Expand Up @@ -2739,6 +2771,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case ShutdownMenu:
shutdownMenu();
break;
case AirplaneModeMenu:
airplaneModeMenu();
break;
case NodePickerMenu:
NodePicker();
break;
Expand Down
2 changes: 2 additions & 0 deletions src/graphics/draw/MenuHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class menuHandler
BrightnessPicker,
RebootMenu,
ShutdownMenu,
AirplaneModeMenu,
NodePickerMenu,
ManageNodeMenu,
RemoveFavorite,
Expand Down Expand Up @@ -95,6 +96,7 @@ class menuHandler
static void BrightnessPickerMenu();
static void rebootMenu();
static void shutdownMenu();
static void airplaneModeMenu();
static void NodePicker();
static void manageNodeMenu();
static void addFavoriteMenu();
Expand Down
6 changes: 6 additions & 0 deletions src/graphics/images.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010
// Lightning Bolt
const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100};

#define icon_airplane_width 8
#define icon_airplane_height 8
static const unsigned char icon_airplane[] PROGMEM = {
0b00011000, 0b00011000, 0b11111111, 0b11111111, 0b00011000, 0b00011000, 0b00111100, 0b00111100,
};

#define mail_width 10
#define mail_height 7
static const unsigned char mail[] PROGMEM = {
Expand Down
10 changes: 10 additions & 0 deletions src/mesh/wifi/WiFiAPClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#endif
#include <ESPmDNS.h>
#include <esp_wifi.h>
#ifdef MESHTASTIC_WIFI_BLE_COEX
#include <esp_coexist.h>
#endif
static void WiFiEvent(WiFiEvent_t event);
#elif defined(ARCH_RP2040)
#include <SimpleMDNS.h>
Expand Down Expand Up @@ -303,10 +306,17 @@ bool initWifi()
#ifdef ARCH_ESP32
WiFi.onEvent(WiFiEvent);
WiFi.setAutoReconnect(true);
#ifdef MESHTASTIC_WIFI_BLE_COEX
// Let WiFi release the radio between DTIM beacons so BLE gets coex slots.
WiFi.setSleep(true);
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);
#else
WiFi.setSleep(false);

// This is needed to improve performance.
esp_wifi_set_ps(WIFI_PS_NONE); // Disable radio power saving
#endif

WiFi.onEvent(
[](WiFiEvent_t event, WiFiEventInfo_t info) {
Expand Down
124 changes: 124 additions & 0 deletions src/modules/AirplaneMode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include "AirplaneMode.h"

#include "NodeDB.h"
#include "configuration.h"
#include "main.h"

#ifdef ARCH_ESP32
#include <Preferences.h>
#endif

// Shared with main-esp32.cpp's reboot counter namespace.
static constexpr const char *kNvsNs = "meshtastic";
static constexpr const char *kKeyActive = "apm_active";
static constexpr const char *kKeySaveWifi = "apm_save_wifi";
static constexpr const char *kKeySaveBt = "apm_save_bt";
static constexpr const char *kKeySaveLora = "apm_save_lora";

AirplaneMode &AirplaneMode::instance()
{
static AirplaneMode singleton;
return singleton;
}

#ifdef ARCH_ESP32
static bool readActiveFlag()
{
Preferences prefs;
if (!prefs.begin(kNvsNs, true)) {
LOG_ERROR("AirplaneMode: Preferences.begin(ro) failed; assuming inactive");
return false;
}
bool active = prefs.isKey(kKeyActive) ? prefs.getBool(kKeyActive, false) : false;
prefs.end();
return active;
}

static void writeActiveFlag(bool active)
{
Preferences prefs;
if (!prefs.begin(kNvsNs, false)) {
LOG_ERROR("AirplaneMode: Preferences.begin(rw) failed; active flag WILL NOT persist");
return;
}
prefs.putBool(kKeyActive, active);
prefs.end();
}

static void savePreAirplaneState(bool wifi, bool bt, bool lora)
{
Preferences prefs;
if (!prefs.begin(kNvsNs, false)) {
LOG_ERROR("AirplaneMode: Preferences.begin(rw) failed; saved radio state WILL NOT persist");
return;
}
prefs.putBool(kKeySaveWifi, wifi);
prefs.putBool(kKeySaveBt, bt);
prefs.putBool(kKeySaveLora, lora);
prefs.end();
}

static bool loadPreAirplaneState(bool &wifi, bool &bt, bool &lora)
{
Preferences prefs;
if (!prefs.begin(kNvsNs, true)) {
LOG_ERROR("AirplaneMode: Preferences.begin(ro) failed; cannot read saved radio state");
return false;
}
bool have = prefs.isKey(kKeySaveWifi) && prefs.isKey(kKeySaveBt) && prefs.isKey(kKeySaveLora);
if (have) {
wifi = prefs.getBool(kKeySaveWifi, true);
bt = prefs.getBool(kKeySaveBt, true);
lora = prefs.getBool(kKeySaveLora, true);
}
prefs.end();
return have;
}
#endif // ARCH_ESP32

bool AirplaneMode::isActive() const
{
#ifdef ARCH_ESP32
if (!cachedActiveLoaded) {
cachedActive = readActiveFlag();
cachedActiveLoaded = true;
}
return cachedActive;
#else
return false;
#endif
}

void AirplaneMode::toggle()
{
#ifdef ARCH_ESP32
bool turningOn = !isActive();
LOG_WARN("AirplaneMode: %s", turningOn ? "entering" : "exiting");

if (turningOn) {
savePreAirplaneState(config.network.wifi_enabled, config.bluetooth.enabled, config.lora.tx_enabled);
config.network.wifi_enabled = false;
config.bluetooth.enabled = false;
config.lora.tx_enabled = false;
writeActiveFlag(true);
cachedActive = true;
} else {
bool wifi = true, bt = true, lora = true;
if (!loadPreAirplaneState(wifi, bt, lora))
LOG_WARN("AirplaneMode: no saved pre-airplane state; defaulting all radios on");
config.network.wifi_enabled = wifi;
config.bluetooth.enabled = bt;
config.lora.tx_enabled = lora;
writeActiveFlag(false);
cachedActive = false;
}
cachedActiveLoaded = true;

if (nodeDB)
nodeDB->saveToDisk(SEGMENT_CONFIG);

rebootAtMsec = millis() + 2000;
#else
LOG_WARN("AirplaneMode: toggle unsupported on this platform (no NVS backend)");
#endif
}
31 changes: 31 additions & 0 deletions src/modules/AirplaneMode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include "configuration.h"

#include <Arduino.h>

// RF kill-switch. Disables LoRa TX, WiFi, and BLE by persisting config flags
// and rebooting. Exit restores the pre-airplane state of each radio.
//
// State of record is an explicit NVS flag (not derived from live config), so
// external config mutations (e.g. phone app re-enabling a radio mid-flight)
// don't confuse the enter/exit transition. Today only the ESP32 backend
// persists state; on other platforms toggle() is a no-op.
class AirplaneMode
{
public:
static AirplaneMode &instance();

// True when airplane mode is active per the persisted flag.
bool isActive() const;

// Toggle enter/exit. Persists config + state flag, schedules reboot.
void toggle();

private:
// Cached active flag to avoid a blocking NVS read per isActive() call
// (called every frame from drawCommonHeader). Populated lazily; kept in
// sync by toggle(). mutable so isActive() can remain const.
mutable bool cachedActive = false;
mutable bool cachedActiveLoaded = false;
};
6 changes: 5 additions & 1 deletion src/platform/esp32/main-esp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ void variant_shutdown() {}
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
void setBluetoothEnable(bool enable)
{
#ifdef USE_WS5500
// MESHTASTIC_WIFI_BLE_COEX: bypass the WiFi-XOR-BLE gate so both can run on the
// hardware coex arbiter.
#if defined(MESHTASTIC_WIFI_BLE_COEX)
if (config.bluetooth.enabled == true)
#elif defined(USE_WS5500)
if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false))
#elif HAS_WIFI
if (!isWifiAvailable() && config.bluetooth.enabled == true)
Expand Down
11 changes: 11 additions & 0 deletions variants/esp32s3/heltec_v4/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ build_flags =
-D I2C_SDA1=4
-D I2C_SCL1=3

; heltec-v4-coex: same hardware profile as heltec-v4 (OLED) with the
; MESHTASTIC_WIFI_BLE_COEX flag enabled. Inherits all pin defines and
; display flags from the heltec-v4 env; adds only the coex build flag
; and a distinguishing display name.
[env:heltec-v4-coex]
extends = env:heltec-v4
custom_meshtastic_display_name = Heltec V4 (WiFi+BLE coex)
build_flags =
${env:heltec-v4.build_flags}
-D MESHTASTIC_WIFI_BLE_COEX=1

[env:heltec-v4-tft]
custom_meshtastic_hw_model = 110
custom_meshtastic_hw_model_slug = HELTEC_V4
Expand Down
Loading