diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 450ee6ff..1985ce58 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -73,6 +73,12 @@ #define CMD_SEND_CHANNEL_DATA 62 #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 // v1.15+ (device-wide default scope name+key) #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 // v1.15+ (query current default scope) +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) +#define CMD_RUN_CLI_COMMAND 66 // v14+ (upstream app command line) +// Combined BLE + WiFi companion (meck_max_ble_wifi): defined in main.cpp +extern bool meckCompanionCommand(const char* command, char* reply); +extern void meckCompanionPoll(); +#endif // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -108,6 +114,9 @@ #define RESP_ALLOWED_REPEAT_FREQ 26 #define RESP_CODE_CHANNEL_DATA_RECV 27 #define RESP_CODE_DEFAULT_FLOOD_SCOPE 28 // v1.15+ +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) +#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND +#endif #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -2934,6 +2943,31 @@ void MyMesh::handleCmdFrame(size_t len) { out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = _prefs.autoadd_config; _serial->writeFrame(out_frame, i); +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + } else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { + // App command line (upstream MeshCore, v14+). This build answers + // upstream's wifi.* commands; anything else gets "Unknown command", + // as upstream replies to a command it does not have. + char *text = (char *)&cmd_frame[1]; + text[len - 1] = 0; // ensure null + while (*text == ' ') text++; // skip leading spaces + char reply_buf[166]; + char *reply = reply_buf; + reply_buf[0] = 0; + if (strlen(text) > 4 && text[2] == '|') { // optional prefix: reflect it back + memcpy(reply, text, 3); + reply += 3; + *reply = 0; + text += 3; + } + if (!meckCompanionCommand(text, reply)) { + strcat(reply_buf, "Unknown command"); + } + out_frame[0] = RESP_CODE_CLI_REPLY; + int rlen = strlen(reply_buf); + memcpy(&out_frame[1], reply_buf, rlen); + _serial->writeFrame(out_frame, 1 + rlen); +#endif } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); @@ -4141,6 +4175,9 @@ void MyMesh::loop() { if (_ui) _ui->setHasConnection(false); #endif #endif +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + meckCompanionPoll(); // app's "set wifi.enabled": switch once its reply has gone +#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 20dfaa60..ebaf26cf 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -815,6 +815,20 @@ static uint32_t _atoi(const char* sp) { #ifndef TCP_PORT #define TCP_PORT 5000 #endif + #elif defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined BLE + WiFi companion (meck_max_ble_wifi test build): both + // connections built in, both off at boot, one on at a time. + #include + #include + #undef FRAME_QUEUE_SIZE // each connection header defines its own queue size + #include + #include "DualCompanionInterface.h" + SerialBLEInterface ble_companion; + SerialWifiInterface wifi_companion; + DualCompanionInterface serial_interface(&ble_companion, &wifi_companion); + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif #elif defined(MECK_WIFI_COMPANION) #include #include @@ -882,6 +896,235 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store /* END GLOBAL OBJECTS */ +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) +// --------------------------------------------------------------------------- +// Combined BLE + WiFi companion (meck_max_ble_wifi test build) +// Both connections are off at boot; turning one on turns the other off. +// Free memory is logged on serial ("MEM ...") at boot, after each switch and +// when the Web Reader starts, for the feasibility measurements. +// --------------------------------------------------------------------------- +static bool meckBtReleased = false; // Web Reader has freed Bluetooth's memory +static bool meckWifiServerStarted = false; // app server (TCP_PORT) started once +static int8_t meckPendingWifi = -1; // app's "set wifi.enabled": -1 none, 0 off, 1 on +static unsigned long meckPendingWifiAt = 0; + +static void meckLogMemory(const char* when) { + Serial.printf("MEM %s: heap free=%u largest=%u, PSRAM free=%u\n", when, + (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap(), + (unsigned)ESP.getFreePsram()); +} + +bool meckCompanionIsBLE() { + return serial_interface.getMode() == DualCompanionInterface::MODE_BLE; +} + +bool meckCompanionIsWiFi() { + return serial_interface.getMode() == DualCompanionInterface::MODE_WIFI; +} + +// Read /web/wifi.cfg (line 1 network name, line 2 password), as Settings +// writes it. Values are cut to upstream's sizes (32 and 63 characters). +static bool meckWifiCfgRead(char* ssid, size_t ssidLen, char* pass, size_t passLen) { + ssid[0] = 0; + pass[0] = 0; + if (!sdCardReady) return false; + if (!SD.exists("/web/wifi.cfg")) { + digitalWrite(SDCARD_CS, HIGH); + return false; + } + File f = SD.open("/web/wifi.cfg", FILE_READ); + if (!f) { + digitalWrite(SDCARD_CS, HIGH); + return false; + } + String s = f.readStringUntil('\n'); s.trim(); + String p = f.readStringUntil('\n'); p.trim(); + f.close(); + digitalWrite(SDCARD_CS, HIGH); + snprintf(ssid, ssidLen, "%s", s.c_str()); + snprintf(pass, passLen, "%s", p.c_str()); + return true; +} + +// Write /web/wifi.cfg the same way the Settings WiFi setup does. +static bool meckWifiCfgWrite(const char* ssid, const char* pass) { + if (!sdCardReady) return false; + bool ok = false; + if (SD.exists("/web") || SD.mkdir("/web")) { + File f = SD.open("/web/wifi.cfg", FILE_WRITE); + if (f) { + f.println(ssid); + f.println(pass); + f.close(); + ok = true; + } + } + digitalWrite(SDCARD_CS, HIGH); + return ok; +} + +static void meckWifiOff() { + wifi_companion.disable(); + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); +} + +void meckCompanionUseNone() { + if (meckCompanionIsBLE()) ble_companion.disable(); + if (meckCompanionIsWiFi()) meckWifiOff(); + serial_interface.setMode(DualCompanionInterface::MODE_NONE); + Serial.println("Companion: Bluetooth and WiFi off"); + meckLogMemory("both off"); +} + +// Bluetooth on, WiFi off. Fails once the Web Reader has freed Bluetooth's +// memory: as in the BLE build, Bluetooth then needs a reboot. +bool meckCompanionUseBLE() { + if (meckCompanionIsBLE()) return true; + if (meckBtReleased) { + Serial.println("Companion: Bluetooth unavailable until reboot (freed for the Web Reader)"); + return false; + } + if (meckCompanionIsWiFi()) meckWifiOff(); + serial_interface.setMode(DualCompanionInterface::MODE_BLE); + ble_companion.enable(); + Serial.println("Companion: Bluetooth on"); + meckLogMemory("Bluetooth on"); + return true; +} + +// WiFi on, Bluetooth off. connectSaved joins the network in /web/wifi.cfg, +// waiting up to 8 s as the Settings WiFi toggle does; WiFi setup passes +// false because it scans and joins by itself. +bool meckCompanionUseWiFi(bool connectSaved) { + if (meckCompanionIsWiFi()) return true; + if (meckCompanionIsBLE()) ble_companion.disable(); + serial_interface.setMode(DualCompanionInterface::MODE_NONE); + meckLogMemory("before WiFi on"); + if (!WiFi.mode(WIFI_STA)) { + Serial.println("Companion: WiFi STA init FAILED"); + WiFi.mode(WIFI_OFF); + meckLogMemory("WiFi init failed"); + return false; + } + if (!meckWifiServerStarted) { + wifi_companion.begin(TCP_PORT); + meckWifiServerStarted = true; + } + serial_interface.setMode(DualCompanionInterface::MODE_WIFI); + wifi_companion.enable(); + if (connectSaved) { + char ssid[33], pass[64]; + if (meckWifiCfgRead(ssid, sizeof(ssid), pass, sizeof(pass)) && ssid[0]) { + WiFi.begin(ssid, pass); + unsigned long timeout = millis() + 8000; + while (WiFi.status() != WL_CONNECTED && millis() < timeout) { + delay(100); + } + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("Companion: WiFi on, connected to %s, IP: %s\n", + ssid, WiFi.localIP().toString().c_str()); + } else { + Serial.println("Companion: WiFi on, but connection failed"); + } + } else { + Serial.println("Companion: WiFi on, no saved network (set one in Settings)"); + } + } else { + Serial.println("Companion: WiFi on"); + } + meckLogMemory("WiFi on"); + return true; +} + +// App command line (message 66): upstream MeshCore's wifi.* commands, with +// the network kept in /web/wifi.cfg as Settings keeps it. Replies are worded +// as upstream's. Returns false for any other command (the caller then +// replies "Unknown command", as upstream does). +bool meckCompanionCommand(const char* command, char* reply) { + char ssid[33], pass[64]; // upstream's wifi_ssid / wifi_pwd sizes + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + meckWifiCfgRead(ssid, sizeof(ssid), pass, sizeof(pass)); + snprintf(ssid, sizeof(ssid), "%s", &command[14]); + if (!meckWifiCfgWrite(ssid, pass)) { + strcpy(reply, "Error, can't save to SD card"); + return true; + } + sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + meckWifiCfgRead(ssid, sizeof(ssid), pass, sizeof(pass)); + snprintf(pass, sizeof(pass), "%s", &command[13]); + if (!meckWifiCfgWrite(ssid, pass)) { + strcpy(reply, "Error, can't save to SD card"); + return true; + } + strcpy(reply, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.pwd") == 0) { + meckWifiCfgRead(ssid, sizeof(ssid), pass, sizeof(pass)); + sprintf(reply, "> %s", pass); + return true; + } + if (strcmp(command, "set wifi.clear") == 0) { + if (!meckWifiCfgWrite("", "")) { + strcpy(reply, "Error, can't save to SD card"); + return true; + } + strcpy(reply, "> wifi config cleared (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { + meckWifiCfgRead(ssid, sizeof(ssid), pass, sizeof(pass)); + sprintf(reply, "> %s", ssid[0] ? ssid : "(not set)"); + return true; + } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + int en = atoi(&command[17]) ? 1 : 0; + // Both are off at boot in this build, so this switches WiFi now: one + // second after the reply, so the reply still reaches the app. WiFi on + // turns Bluetooth off. + meckPendingWifi = en; + meckPendingWifiAt = millis() + 1000; + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", en); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + sprintf(reply, "> %d", meckCompanionIsWiFi() ? 1 : 0); + return true; + } + if (strcmp(command, "get wifi.status") == 0) { + strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); + } else { + strcpy(reply, "> (not connected)"); + } + return true; + } + return false; +} + +// Called from MyMesh::loop(): runs the app's "set wifi.enabled" switch once +// its reply has had time to go out. +void meckCompanionPoll() { + if (meckPendingWifi < 0) return; + if ((long)(millis() - meckPendingWifiAt) < 0) return; + int8_t want = meckPendingWifi; + meckPendingWifi = -1; + if (want == 1 && !meckCompanionIsWiFi()) { + meckCompanionUseWiFi(true); + } else if (want == 0 && meckCompanionIsWiFi()) { + meckCompanionUseNone(); + } +} +#endif + // --------------------------------------------------------------------------- // Voice-over-LoRa: incoming raw packet handler (dz0ny VE3 protocol) // Registered with the_mesh.setVoiceHandler() when voice screen is created. @@ -2039,6 +2282,12 @@ void setup() { MESH_DEBUG_PRINTLN("setup() - WiFi mode (compile-time credentials)"); WiFi.begin(WIFI_SSID, WIFI_PWD); serial_interface.begin(TCP_PORT); +#elif defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined BLE + WiFi companion: both off at boot. Bluetooth is only + // prepared here (its controller starts on first use); WiFi and the app + // server start when the user turns WiFi on. + MESH_DEBUG_PRINTLN("setup() - combined BLE + WiFi companion, both off at boot"); + ble_companion.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); #elif defined(MECK_WIFI_COMPANION) { // WiFi companion: load credentials from SD at runtime. @@ -2366,6 +2615,9 @@ void setup() { #ifdef ESP32 Serial.printf("setup() complete - free heap: %d, largest block: %d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); +#endif +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + meckLogMemory("boot, both off"); #endif { void* p25 = malloc(25000); void* p10 = malloc(10000); void* p4 = malloc(4000); @@ -4554,6 +4806,32 @@ void handleKeyboardInput() { Serial.println("Opening web reader"); { static bool webReaderWifiReady = false; + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined build: with the WiFi companion on, WiFi is already up and + // Bluetooth is off, so nothing needs freeing. Otherwise do as the BLE + // build does, once per boot: switch the Bluetooth companion off, free + // Bluetooth's memory (it then needs a reboot) and start WiFi. + if (!meckCompanionIsWiFi() && !webReaderWifiReady) { + if (meckCompanionIsBLE()) meckCompanionUseNone(); + Serial.printf("WebReader: heap BEFORE BT release: free=%d, largest=%d\n", + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + btStop(); + delay(50); + esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); + delay(50); + meckBtReleased = true; + Serial.printf("WebReader: heap AFTER BT release: free=%d, largest=%d\n", + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + if (WiFi.mode(WIFI_STA)) { + Serial.println("WebReader: WiFi STA init OK"); + webReaderWifiReady = true; + } else { + Serial.println("WebReader: WiFi STA init FAILED"); + WiFi.mode(WIFI_OFF); + } + } + meckLogMemory("Web Reader start"); + #else #ifdef MECK_WIFI_COMPANION // WiFi companion: WiFi is already up from boot, no BLE to tear down webReaderWifiReady = true; @@ -4592,6 +4870,7 @@ void handleKeyboardInput() { Serial.printf("WebReader: heap after WiFi init: free=%d, largest=%d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); } + #endif } ui_task.gotoWebReader(); break; diff --git a/examples/companion_radio/ui-new/DualCompanionInterface.h b/examples/companion_radio/ui-new/DualCompanionInterface.h new file mode 100644 index 00000000..2951dfbb --- /dev/null +++ b/examples/companion_radio/ui-new/DualCompanionInterface.h @@ -0,0 +1,48 @@ +#pragma once + +// Combined BLE + WiFi companion (meck_max_ble_wifi test build). +// +// Holds the Bluetooth and the WiFi companion connections and passes every +// call to whichever one is selected. Only one is selected at a time; with +// neither selected it behaves as a switched-off connection. Selecting a +// connection and switching the radios on and off is done in main.cpp +// (meckCompanionUseBLE / meckCompanionUseWiFi / meckCompanionUseNone). + +#include + +class DualCompanionInterface : public BaseSerialInterface { +public: + enum Mode : uint8_t { MODE_NONE = 0, MODE_BLE = 1, MODE_WIFI = 2 }; + +private: + BaseSerialInterface* _ble; + BaseSerialInterface* _wifi; + Mode _mode; + + BaseSerialInterface* active() const { + if (_mode == MODE_BLE) return _ble; + if (_mode == MODE_WIFI) return _wifi; + return nullptr; + } + +public: + DualCompanionInterface(BaseSerialInterface* ble, BaseSerialInterface* wifi) + : _ble(ble), _wifi(wifi), _mode(MODE_NONE) { } + + Mode getMode() const { return _mode; } + void setMode(Mode m) { _mode = m; } + + // BaseSerialInterface methods, passed to the selected connection + void enable() override { if (active()) active()->enable(); } + void disable() override { if (active()) active()->disable(); } + bool isEnabled() const override { return active() ? active()->isEnabled() : false; } + bool isConnected() const override { return active() ? active()->isConnected() : false; } + bool isWriteBusy() const override { return active() ? active()->isWriteBusy() : false; } + bool hasPendingData() const override { return active() ? active()->hasPendingData() : false; } + size_t writeFrame(const uint8_t src[], size_t len) override { + return active() ? active()->writeFrame(src, len) : 0; + } + size_t checkRecvFrame(uint8_t dest[]) override { + return active() ? active()->checkRecvFrame(dest) : 0; + } +}; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 4f3bbda5..d3311e88 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -1048,7 +1048,16 @@ public: extern void otaResumeRadio(); otaResumeRadio(); // Try to restore STA WiFi from saved credentials - #ifdef MECK_WIFI_COMPANION + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + { + // Combined build: only if WiFi was the companion connection + extern bool meckCompanionIsWiFi(); + if (meckCompanionIsWiFi()) { + WiFi.mode(WIFI_STA); + wifiReconnectSaved(); + } + } + #elif defined(MECK_WIFI_COMPANION) WiFi.mode(WIFI_STA); wifiReconnectSaved(); #endif @@ -1474,7 +1483,16 @@ public: _editMode = EDIT_NONE; extern void otaResumeRadio(); otaResumeRadio(); - #ifdef MECK_WIFI_COMPANION + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + { + // Combined build: only if WiFi was the companion connection + extern bool meckCompanionIsWiFi(); + if (meckCompanionIsWiFi()) { + WiFi.mode(WIFI_STA); + wifiReconnectSaved(); + } + } + #elif defined(MECK_WIFI_COMPANION) WiFi.mode(WIFI_STA); wifiReconnectSaved(); #endif @@ -3529,11 +3547,33 @@ public: #ifdef MECK_WIFI_COMPANION case ROW_WIFI_SETUP: { // Launch WiFi scan → select → password → connect flow + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + { + // Combined build: WiFi setup makes WiFi the companion connection + // (Bluetooth goes off), as one connection is on at a time + extern bool meckCompanionUseWiFi(bool connectSaved); + meckCompanionUseWiFi(false); + } + #endif _editMode = EDIT_WIFI; performWifiScan(); break; } case ROW_WIFI_TOGGLE: + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + { + // Combined build: WiFi on turns Bluetooth off; WiFi off leaves + // neither on + extern bool meckCompanionIsWiFi(); + extern bool meckCompanionUseWiFi(bool connectSaved); + extern void meckCompanionUseNone(); + if (meckCompanionIsWiFi()) { + meckCompanionUseNone(); + } else { + meckCompanionUseWiFi(true); + } + } + #else if (WiFi.getMode() != WIFI_OFF) { // Turn WiFi OFF WiFi.disconnect(true); @@ -3567,6 +3607,7 @@ public: } Serial.println("Settings: WiFi radio ON"); } + #endif break; #endif #ifdef HAS_4G_MODEM diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 24173875..4dc30824 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -30,6 +30,12 @@ #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) #include #endif +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined BLE + WiFi companion (meck_max_ble_wifi): defined in main.cpp + extern bool meckCompanionIsBLE(); + extern bool meckCompanionUseBLE(); + extern void meckCompanionUseNone(); +#endif #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds @@ -147,7 +153,8 @@ class HomeScreen : public UIScreen { RADIO, #ifdef BLE_PIN_CODE BLUETOOTH, -#elif defined(MECK_WIFI_COMPANION) +#endif +#ifdef MECK_WIFI_COMPANION WIFI_STATUS, #endif ADVERT, @@ -697,7 +704,12 @@ public: } } #endif - #ifdef BLE_PIN_CODE + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined build: show the PIN only while Bluetooth is the connection + if (rightBuf[0] == 0 && meckCompanionIsBLE() && _task->isSerialEnabled() && the_mesh.getBLEPin() != 0) { + sprintf(rightBuf, "Pin:%d", the_mesh.getBLEPin()); + } + #elif defined(BLE_PIN_CODE) if (rightBuf[0] == 0 && _task->isSerialEnabled() && the_mesh.getBLEPin() != 0) { sprintf(rightBuf, "Pin:%d", the_mesh.getBLEPin()); } @@ -946,7 +958,30 @@ public: display.setCursor(0, 64); sprintf(tmp, "RX packets: %u", (unsigned)the_mesh.getRxPacketCount()); display.print(tmp); -#ifdef BLE_PIN_CODE +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + } else if (_page == HomePage::BLUETOOTH) { + // Combined build: Bluetooth counts as on only while it is the + // connection, not while WiFi is + bool btOn = meckCompanionIsBLE() && _task->isSerialEnabled(); + display.setColor(DisplayDriver::GREEN); + display.drawXbm((display.width() - 32) / 2, 18, + btOn ? bluetooth_on : bluetooth_off, + 32, 32); + if (btOn && _task->hasConnection()) { + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 53, "< Connected >"); + } else if (btOn && the_mesh.getBLEPin() != 0) { + display.setColor(DisplayDriver::RED); + display.setTextSize(2); + sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); + display.drawTextCentered(display.width() / 2, 53, tmp); + } + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL); + display.drawTextCentered(display.width() / 2, 78, "or press Enter key"); +#elif defined(BLE_PIN_CODE) } else if (_page == HomePage::BLUETOOTH) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, @@ -1356,7 +1391,18 @@ public: } return true; } -#ifdef BLE_PIN_CODE +#if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { + // Combined build: Bluetooth on turns WiFi off; Bluetooth off leaves + // neither on. Once the Web Reader has freed Bluetooth, it needs a reboot. + if (meckCompanionIsBLE()) { + meckCompanionUseNone(); + } else if (!meckCompanionUseBLE()) { + _task->showAlert("Reboot for Bluetooth", 1500); + } + return true; + } +#elif defined(BLE_PIN_CODE) if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (_task->isSerialEnabled()) { // toggle Bluetooth on/off _task->disableSerial(); diff --git a/examples/companion_radio/ui-new/WebReaderScreen.h b/examples/companion_radio/ui-new/WebReaderScreen.h index e42c2020..be07ff4f 100644 --- a/examples/companion_radio/ui-new/WebReaderScreen.h +++ b/examples/companion_radio/ui-new/WebReaderScreen.h @@ -5141,7 +5141,17 @@ public: if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; } _tlsHost = String(); - #ifdef MECK_WIFI_COMPANION + #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) + // Combined build: keep WiFi up only if it is the companion connection + extern bool meckCompanionIsWiFi(); + if (meckCompanionIsWiFi()) { + _wifiState = WIFI_CONNECTED; // WiFi is still up + } else { + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + _wifiState = WIFI_IDLE; + } + #elif defined(MECK_WIFI_COMPANION) // WiFi companion: keep WiFi alive for the companion TCP server. // Don't disconnect or change mode — just reset our internal state. _wifiState = WIFI_CONNECTED; // WiFi is still up diff --git a/variants/lilygo_tdeck_max/platformio.ini b/variants/lilygo_tdeck_max/platformio.ini index c2dec65d..ee2edf22 100644 --- a/variants/lilygo_tdeck_max/platformio.ini +++ b/variants/lilygo_tdeck_max/platformio.ini @@ -295,6 +295,40 @@ lib_deps = bitbank2/JPEGDEC bitbank2/PNGdec +; MAX combined BLE + WiFi companion: TEST BUILD. Both connections built in, +; both off at boot; turning one on turns the other off. Answers the app's +; command-line message (66) for upstream's wifi.* commands. Free memory is +; logged on serial ("MEM ...") at boot, after each switch and when the Web +; Reader starts. Flags are meck_max_ble's plus meck_max_wifi's. +[env:meck_max_ble_wifi] +extends = LilyGo_TDeck_Pro_Max +build_flags = + ${LilyGo_TDeck_Pro_Max.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=2000 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D MECK_BLE_TX_POWER=ESP_PWR_LVL_P15 + -D MECK_WIFI_COMPANION=1 + -D TCP_PORT=5000 + -D WIFI_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D MECK_WEB_READER=1 + -D MECK_OTA_UPDATE=1 + -D HAS_4G_MODEM=1 + -D FIRMWARE_VERSION='"Meck v1.14.MAX.BLE-WiFi"' +build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro_Max.lib_deps} + densaugeo/base64 @ ~1.4.0 + bitbank2/JPEGDEC + bitbank2/PNGdec + ; MAX standalone (no BLE/WiFi — maximum battery life, LoRa mesh only) ; Contacts in PSRAM (2000 capacity). OTA enabled (WiFi AP on demand). [env:meck_max_standalone]