diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 90173d89..01cd65d1 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -202,7 +202,10 @@ // Returns number of contacts exported, or -1 on error. // ----------------------------------------------------------------------- int exportContactsToSD() { - if (!sdCardReady) return -1; + if (!sdCardReady) { + Serial.println("Export: SD card not ready"); + return -1; + } // Ensure in-memory contacts are flushed to SPIFFS first the_mesh.saveContacts(); @@ -210,40 +213,53 @@ if (!SD.exists("/meshcore")) SD.mkdir("/meshcore"); // 1) Binary backup: SPIFFS /contacts3 → SD /meshcore/contacts.bin - if (!SPIFFS.exists("/contacts3")) return -1; - if (!copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin")) return -1; + // Non-fatal — text export reads from memory and doesn't need this. + if (SPIFFS.exists("/contacts3")) { + if (copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin")) { + Serial.println("Export: binary backup OK"); + } else { + Serial.println("Export: binary copy to SD failed (continuing with text export)"); + } + } else { + Serial.println("Export: /contacts3 not found on SPIFFS (skipping binary backup)"); + } // 2) Human-readable listing for inspection on a computer + // Reads from in-memory contact table — always works if SD is writable. int count = 0; File txt = SD.open("/meshcore/contacts_export.txt", "w", true); - if (txt) { - txt.printf("Meck Contacts Export (%d total)\n", (int)the_mesh.getNumContacts()); - txt.printf("========================================\n"); - txt.printf("%-5s %-30s %s\n", "Type", "Name", "PubKey (prefix)"); - txt.printf("----------------------------------------\n"); - - ContactInfo c; - for (uint32_t i = 0; i < (uint32_t)the_mesh.getNumContacts(); i++) { - if (the_mesh.getContactByIdx(i, c)) { - const char* typeStr = "???"; - switch (c.type) { - case ADV_TYPE_CHAT: typeStr = "Chat"; break; - case ADV_TYPE_REPEATER: typeStr = "Rptr"; break; - case ADV_TYPE_ROOM: typeStr = "Room"; break; - } - // First 8 bytes of pub key as hex identifier - char hexBuf[20]; - mesh::Utils::toHex(hexBuf, c.id.pub_key, 8); - txt.printf("%-5s %-30s %s\n", typeStr, c.name, hexBuf); - count++; - } - } - - txt.printf("========================================\n"); - txt.printf("Total: %d contacts\n", count); - txt.close(); + if (!txt) { + Serial.println("Export: failed to open contacts_export.txt for writing"); + digitalWrite(SDCARD_CS, HIGH); + return -1; } + txt.printf("Meck Contacts Export (%d total)\n", (int)the_mesh.getNumContacts()); + txt.printf("========================================\n"); + txt.printf("%-5s %-30s %s\n", "Type", "Name", "PubKey (prefix)"); + txt.printf("----------------------------------------\n"); + + ContactInfo c; + for (uint32_t i = 0; i < (uint32_t)the_mesh.getNumContacts(); i++) { + if (the_mesh.getContactByIdx(i, c)) { + const char* typeStr = "???"; + switch (c.type) { + case ADV_TYPE_CHAT: typeStr = "Chat"; break; + case ADV_TYPE_REPEATER: typeStr = "Rptr"; break; + case ADV_TYPE_ROOM: typeStr = "Room"; break; + } + // First 8 bytes of pub key as hex identifier + char hexBuf[20]; + mesh::Utils::toHex(hexBuf, c.id.pub_key, 8); + txt.printf("%-5s %-30s %s\n", typeStr, c.name, hexBuf); + count++; + } + } + + txt.printf("========================================\n"); + txt.printf("Total: %d contacts\n", count); + txt.close(); + digitalWrite(SDCARD_CS, HIGH); Serial.printf("Contacts exported to SD: %d contacts\n", count); return count; @@ -1981,7 +1997,7 @@ void handleKeyboardInput() { snprintf(alertBuf, sizeof(alertBuf), "Exported %d to SD", exported); ui_task.showAlert(alertBuf, 2000); } else { - ui_task.showAlert("Export failed (no SD?)", 2000); + ui_task.showAlert("Export failed (check serial)", 2000); } } break; diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 1f5dfe9f..076c2ed2 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -20,6 +20,25 @@ class UITask; class MyMesh; extern MyMesh the_mesh; +// --------------------------------------------------------------------------- +// Auto-add config bitmask (mirrored from MyMesh.cpp for UI access) +// --------------------------------------------------------------------------- +#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full +#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) +#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) +#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) +#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) + +// All type bits combined (excludes overwrite flag) +#define AUTO_ADD_ALL_TYPES (AUTO_ADD_CHAT | AUTO_ADD_REPEATER | \ + AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR) + +// Contact mode indices for picker +#define CONTACT_MODE_AUTO_ALL 0 // Add all contacts automatically +#define CONTACT_MODE_CUSTOM 1 // Per-type toggles +#define CONTACT_MODE_MANUAL 2 // No auto-add, companion app only +#define CONTACT_MODE_COUNT 3 + // --------------------------------------------------------------------------- // Radio presets // --------------------------------------------------------------------------- @@ -67,11 +86,19 @@ enum SettingsRowType : uint8_t { ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle #ifdef MECK_WIFI_COMPANION ROW_WIFI_SETUP, // WiFi SSID/password configuration + ROW_WIFI_TOGGLE, // WiFi radio on/off toggle #endif #ifdef HAS_4G_MODEM ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only) // ROW_RINGTONE, // Incoming call ringtone toggle (4G builds only) #endif + ROW_CONTACT_HEADER, // "--- Contacts ---" separator + ROW_CONTACT_MODE, // Contact auto-add mode picker (Auto All / Custom / Manual) + ROW_AUTOADD_CHAT, // Toggle: auto-add Chat clients + ROW_AUTOADD_REPEATER,// Toggle: auto-add Repeaters + ROW_AUTOADD_ROOM, // Toggle: auto-add Room Servers + ROW_AUTOADD_SENSOR, // Toggle: auto-add Sensors + ROW_AUTOADD_OVERWRITE, // Toggle: overwrite oldest non-favourite when full ROW_CH_HEADER, // "--- Channels ---" separator ROW_CHANNEL, // A channel entry (dynamic, index stored separately) ROW_ADD_CHANNEL, // "+ Add Hashtag Channel" @@ -91,7 +118,7 @@ enum SettingsRowType : uint8_t { enum EditMode : uint8_t { EDIT_NONE, // Just browsing EDIT_TEXT, // Typing into a text buffer (name, channel name) - EDIT_PICKER, // A/D cycles options (radio preset) + EDIT_PICKER, // A/D cycles options (radio preset, contact mode) EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC) EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio) #ifdef MECK_WIFI_COMPANION @@ -99,15 +126,15 @@ enum EditMode : uint8_t { #endif }; -// Max rows in the settings list +// Max rows in the settings list (increased for contact sub-toggles + WiFi) #if defined(HAS_4G_MODEM) && defined(MECK_WIFI_COMPANION) -#define SETTINGS_MAX_ROWS 48 // Extra rows for IMEI, Carrier, APN, WiFi +#define SETTINGS_MAX_ROWS 56 // Extra rows for IMEI, Carrier, APN, contacts, WiFi #elif defined(HAS_4G_MODEM) -#define SETTINGS_MAX_ROWS 46 // Extra rows for IMEI, Carrier, APN +#define SETTINGS_MAX_ROWS 54 // Extra rows for IMEI, Carrier, APN + contacts #elif defined(MECK_WIFI_COMPANION) -#define SETTINGS_MAX_ROWS 42 // Extra row for WiFi +#define SETTINGS_MAX_ROWS 50 // Extra rows for contacts + WiFi #else -#define SETTINGS_MAX_ROWS 40 +#define SETTINGS_MAX_ROWS 48 // Contacts section #endif #define SETTINGS_TEXT_BUF 33 // 32 chars + null @@ -133,7 +160,7 @@ private: EditMode _editMode; char _editBuf[SETTINGS_TEXT_BUF]; int _editPos; - int _editPickerIdx; // for preset picker + int _editPickerIdx; // for preset picker / contact mode picker float _editFloat; // for freq/BW editing int _editInt; // for SF/CR/TX/UTC editing int _confirmAction; // 0=none, 1=delete channel, 2=apply radio @@ -167,6 +194,57 @@ private: unsigned long _wifiFormLastChar; // For brief password reveal #endif + // --------------------------------------------------------------------------- + // Contact mode helpers + // --------------------------------------------------------------------------- + + // Determine current contact mode from prefs + int getContactMode() const { + if ((_prefs->manual_add_contacts & 1) == 0) { + return CONTACT_MODE_AUTO_ALL; + } + // manual_add_contacts bit 0 is set — check if any type bits are enabled + if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) != 0) { + return CONTACT_MODE_CUSTOM; + } + return CONTACT_MODE_MANUAL; + } + + // Get display label for a contact mode + static const char* contactModeLabel(int mode) { + switch (mode) { + case CONTACT_MODE_AUTO_ALL: return "Auto All"; + case CONTACT_MODE_CUSTOM: return "Custom"; + case CONTACT_MODE_MANUAL: return "Manual Only"; + default: return "?"; + } + } + + // Apply a contact mode selection from picker + void applyContactMode(int mode) { + switch (mode) { + case CONTACT_MODE_AUTO_ALL: + _prefs->manual_add_contacts &= ~1; // clear bit 0 → auto all + break; + case CONTACT_MODE_CUSTOM: + _prefs->manual_add_contacts |= 1; // set bit 0 → selective + // If no type bits are set, default to all types enabled + if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) == 0) { + _prefs->autoadd_config |= AUTO_ADD_ALL_TYPES; + } + break; + case CONTACT_MODE_MANUAL: + _prefs->manual_add_contacts |= 1; // set bit 0 → selective + _prefs->autoadd_config &= ~AUTO_ADD_ALL_TYPES; // clear all type bits + // Note: keeps AUTO_ADD_OVERWRITE_OLDEST bit unchanged + break; + } + the_mesh.savePrefs(); + rebuildRows(); // show/hide sub-toggles + Serial.printf("Settings: Contact mode = %s (manual=%d, autoadd=0x%02X)\n", + contactModeLabel(mode), _prefs->manual_add_contacts, _prefs->autoadd_config); + } + // --------------------------------------------------------------------------- // Row table management // --------------------------------------------------------------------------- @@ -185,11 +263,27 @@ private: addRow(ROW_MSG_NOTIFY); #ifdef MECK_WIFI_COMPANION addRow(ROW_WIFI_SETUP); + addRow(ROW_WIFI_TOGGLE); #endif #ifdef HAS_4G_MODEM addRow(ROW_MODEM_TOGGLE); // addRow(ROW_RINGTONE); #endif + + // --- Contacts section --- + addRow(ROW_CONTACT_HEADER); + addRow(ROW_CONTACT_MODE); + + // Show per-type sub-toggles only in Custom mode + if (getContactMode() == CONTACT_MODE_CUSTOM) { + addRow(ROW_AUTOADD_CHAT); + addRow(ROW_AUTOADD_REPEATER); + addRow(ROW_AUTOADD_ROOM); + addRow(ROW_AUTOADD_SENSOR); + addRow(ROW_AUTOADD_OVERWRITE); + } + + // --- Channels section --- addRow(ROW_CH_HEADER); // Enumerate current channels @@ -230,7 +324,7 @@ private: bool isSelectable(int idx) const { if (idx < 0 || idx >= _numRows) return false; SettingsRowType t = _rows[idx].type; - return t != ROW_CH_HEADER && t != ROW_INFO_HEADER + return t != ROW_CH_HEADER && t != ROW_INFO_HEADER && t != ROW_CONTACT_HEADER #ifdef HAS_4G_MODEM && t != ROW_IMEI && t != ROW_OPERATOR_INFO #endif @@ -565,6 +659,11 @@ public: } display.print(tmp); break; + case ROW_WIFI_TOGGLE: + snprintf(tmp, sizeof(tmp), "WiFi Radio: %s", + (WiFi.getMode() != WIFI_OFF) ? "ON" : "OFF"); + display.print(tmp); + break; #endif #ifdef HAS_4G_MODEM @@ -581,6 +680,54 @@ public: // break; #endif + // --- Contacts section --- + case ROW_CONTACT_HEADER: + display.setColor(DisplayDriver::YELLOW); + display.print("--- Contacts ---"); + break; + + case ROW_CONTACT_MODE: + if (editing && _editMode == EDIT_PICKER) { + snprintf(tmp, sizeof(tmp), "< Add Mode: %s >", + contactModeLabel(_editPickerIdx)); + } else { + snprintf(tmp, sizeof(tmp), "Add Mode: %s", + contactModeLabel(getContactMode())); + } + display.print(tmp); + break; + + case ROW_AUTOADD_CHAT: + snprintf(tmp, sizeof(tmp), " Chat: %s", + (_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF"); + display.print(tmp); + break; + + case ROW_AUTOADD_REPEATER: + snprintf(tmp, sizeof(tmp), " Repeater: %s", + (_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF"); + display.print(tmp); + break; + + case ROW_AUTOADD_ROOM: + snprintf(tmp, sizeof(tmp), " Room Server: %s", + (_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF"); + display.print(tmp); + break; + + case ROW_AUTOADD_SENSOR: + snprintf(tmp, sizeof(tmp), " Sensor: %s", + (_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF"); + display.print(tmp); + break; + + case ROW_AUTOADD_OVERWRITE: + snprintf(tmp, sizeof(tmp), " Overwrite Oldest: %s", + (_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF"); + display.print(tmp); + break; + + // --- Channels section --- case ROW_CH_HEADER: display.setColor(DisplayDriver::YELLOW); display.print("--- Channels ---"); @@ -1011,62 +1158,81 @@ public: return true; // consume all keys in text edit } - // --- Picker mode (radio preset) --- + // --- Picker mode (radio preset or contact mode) --- if (_editMode == EDIT_PICKER) { + SettingsRowType type = _rows[_cursor].type; + if (c == 'a' || c == 'A') { - _editPickerIdx--; - if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1; + if (type == ROW_CONTACT_MODE) { + _editPickerIdx--; + if (_editPickerIdx < 0) _editPickerIdx = CONTACT_MODE_COUNT - 1; + } else { + // Radio preset + _editPickerIdx--; + if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1; + } return true; } if (c == 'd' || c == 'D') { - _editPickerIdx++; - if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0; + if (type == ROW_CONTACT_MODE) { + _editPickerIdx++; + if (_editPickerIdx >= CONTACT_MODE_COUNT) _editPickerIdx = 0; + } else { + // Radio preset + _editPickerIdx++; + if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0; + } return true; } if (c == '\r' || c == 13) { - // Apply preset - if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { - const RadioPreset& p = RADIO_PRESETS[_editPickerIdx]; - _prefs->freq = p.freq; - _prefs->bw = p.bw; - _prefs->sf = p.sf; - _prefs->cr = p.cr; - _prefs->tx_power_dbm = p.tx_power; - _radioChanged = true; - } - _editMode = EDIT_NONE; - if (_onboarding) { - applyRadioParams(); - #ifdef MECK_WIFI_COMPANION - // Move to WiFi setup before finishing onboarding - for (int r = 0; r < _numRows; r++) { - if (_rows[r].type == ROW_WIFI_SETUP) { - _cursor = r; - break; - } + if (type == ROW_CONTACT_MODE) { + applyContactMode(_editPickerIdx); + _editMode = EDIT_NONE; + } else { + // Apply radio preset + if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { + const RadioPreset& p = RADIO_PRESETS[_editPickerIdx]; + _prefs->freq = p.freq; + _prefs->bw = p.bw; + _prefs->sf = p.sf; + _prefs->cr = p.cr; + _prefs->tx_power_dbm = p.tx_power; + _radioChanged = true; } - // Auto-launch the WiFi scan - _editMode = EDIT_WIFI; - _wifiPhase = WIFI_PHASE_SCANNING; - _wifiSSIDCount = 0; - _wifiSSIDSelected = 0; - WiFi.mode(WIFI_STA); - int n = WiFi.scanNetworks(); - if (n > 0) { - _wifiSSIDCount = min(n, 10); - for (int si = 0; si < _wifiSSIDCount; si++) { - _wifiSSIDs[si] = WiFi.SSID(si); + _editMode = EDIT_NONE; + if (_onboarding) { + applyRadioParams(); + #ifdef MECK_WIFI_COMPANION + // Move to WiFi setup before finishing onboarding + for (int r = 0; r < _numRows; r++) { + if (_rows[r].type == ROW_WIFI_SETUP) { + _cursor = r; + break; + } } - _wifiPhase = WIFI_PHASE_SELECT; - } else { - _editMode = EDIT_NONE; - _wifiPhase = WIFI_PHASE_IDLE; - _onboarding = false; // Finish even without WiFi + // Auto-launch the WiFi scan + _editMode = EDIT_WIFI; + _wifiPhase = WIFI_PHASE_SCANNING; + _wifiSSIDCount = 0; + _wifiSSIDSelected = 0; + WiFi.mode(WIFI_STA); + int n = WiFi.scanNetworks(); + if (n > 0) { + _wifiSSIDCount = min(n, 10); + for (int si = 0; si < _wifiSSIDCount; si++) { + _wifiSSIDs[si] = WiFi.SSID(si); + } + _wifiPhase = WIFI_PHASE_SELECT; + } else { + _editMode = EDIT_NONE; + _wifiPhase = WIFI_PHASE_IDLE; + _onboarding = false; // Finish even without WiFi + } + WiFi.scanDelete(); + #else + _onboarding = false; + #endif } - WiFi.scanDelete(); - #else - _onboarding = false; - #endif } return true; } @@ -1234,6 +1400,41 @@ public: WiFi.scanDelete(); break; } + case ROW_WIFI_TOGGLE: + if (WiFi.getMode() != WIFI_OFF) { + // Turn WiFi OFF + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + Serial.println("Settings: WiFi radio OFF"); + } else { + // Turn WiFi ON — reconnect using saved credentials + WiFi.mode(WIFI_STA); + if (SD.exists("/web/wifi.cfg")) { + File f = SD.open("/web/wifi.cfg", FILE_READ); + if (f) { + String ssid = f.readStringUntil('\n'); ssid.trim(); + String pass = f.readStringUntil('\n'); pass.trim(); + f.close(); + digitalWrite(SDCARD_CS, HIGH); + if (ssid.length() > 0) { + WiFi.begin(ssid.c_str(), pass.c_str()); + unsigned long timeout = millis() + 8000; + while (WiFi.status() != WL_CONNECTED && millis() < timeout) { + delay(100); + } + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("Settings: WiFi ON, connected to %s\n", ssid.c_str()); + } else { + Serial.println("Settings: WiFi ON, but connection failed"); + } + } + } else { + digitalWrite(SDCARD_CS, HIGH); + } + } + Serial.println("Settings: WiFi radio ON"); + } + break; #endif #ifdef HAS_4G_MODEM case ROW_MODEM_TOGGLE: @@ -1261,6 +1462,44 @@ public: break; } #endif + + // --- Contact mode picker --- + case ROW_CONTACT_MODE: + startEditPicker(getContactMode()); + break; + + // --- Contact sub-toggles (flip bit and save) --- + case ROW_AUTOADD_CHAT: + _prefs->autoadd_config ^= AUTO_ADD_CHAT; + the_mesh.savePrefs(); + Serial.printf("Settings: Auto-add Chat = %s\n", + (_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF"); + break; + case ROW_AUTOADD_REPEATER: + _prefs->autoadd_config ^= AUTO_ADD_REPEATER; + the_mesh.savePrefs(); + Serial.printf("Settings: Auto-add Repeater = %s\n", + (_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF"); + break; + case ROW_AUTOADD_ROOM: + _prefs->autoadd_config ^= AUTO_ADD_ROOM_SERVER; + the_mesh.savePrefs(); + Serial.printf("Settings: Auto-add Room = %s\n", + (_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF"); + break; + case ROW_AUTOADD_SENSOR: + _prefs->autoadd_config ^= AUTO_ADD_SENSOR; + the_mesh.savePrefs(); + Serial.printf("Settings: Auto-add Sensor = %s\n", + (_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF"); + break; + case ROW_AUTOADD_OVERWRITE: + _prefs->autoadd_config ^= AUTO_ADD_OVERWRITE_OLDEST; + the_mesh.savePrefs(); + Serial.printf("Settings: Overwrite oldest = %s\n", + (_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF"); + break; + case ROW_ADD_CHANNEL: startEditText(""); break; diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index f996b390..900cbbcb 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -233,8 +233,10 @@ lib_deps = ${LilyGo_TDeck_Pro.lib_deps} densaugeo/base64 @ ~1.4.0 -; 4G standalone (audio-player hardware, no BLE/WiFi — maximum battery life) -; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design. +; 4G standalone (4G modem hardware, no BLE — maximum battery + cellular features) +; No BLE_PIN_CODE: BLE never initializes, saving ~30KB heap + radio power. +; MECK_WEB_READER enabled: works better without BLE (no teardown dance needed, +; more free heap from boot). WiFi-first with cellular PPP fallback (future). ; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB. [env:meck_4g_standalone] extends = LilyGo_TDeck_Pro @@ -245,6 +247,7 @@ build_flags = -D MAX_GROUP_CHANNELS=20 -D OFFLINE_QUEUE_SIZE=256 -D HAS_4G_MODEM=1 + -D MECK_WEB_READER=1 -D FIRMWARE_VERSION='"Meck v0.9.8.4G.SA"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} +