From f46177721472c0b3ba372c1e8e3ac83dbf2af8ca Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:43:29 +1000 Subject: [PATCH] t-echo lite screen: removed diag diagnostic prints, sorted compose mode with cardkb, fixed enter & esc handlers; increased e-ink offset for home screen centering; condensed footer text for all screens; datastore chunked saved guarded for esp32 ; still encountering memory problems with ble build even w 250 contacts and 10 chanel message history so trying standalone --- examples/companion_radio/DataStore.cpp | 158 ++++------ examples/companion_radio/DataStore.h | 11 +- examples/companion_radio/MyMesh.cpp | 12 + examples/companion_radio/main.cpp | 270 +++++++++++++++++- .../companion_radio/ui-new/ChannelScreen.h | 23 ++ .../ui-new/Channelpickerscreen.h | 5 + .../companion_radio/ui-new/Contactsscreen.h | 10 + .../companion_radio/ui-new/Settingsscreen.h | 20 +- examples/companion_radio/ui-new/UITask.cpp | 55 +++- src/helpers/ui/GxEPDDisplay.h | 19 +- variants/lilygo_techo_lite/platformio.ini | 51 +++- 11 files changed, 485 insertions(+), 149 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 93943df7..75bd22f7 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -9,8 +9,7 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock), #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - identity_store(fs, ""), - _saveFile(fs) + identity_store(fs, "") #elif defined(RP2040_PLATFORM) identity_store(fs, "/identity") #else @@ -22,8 +21,7 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra #if defined(EXTRAFS) || defined(QSPIFLASH) DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock), #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - identity_store(fs, ""), - _saveFile(fs) + identity_store(fs, "") #elif defined(RP2040_PLATFORM) identity_store(fs, "/identity") #else @@ -276,9 +274,6 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) { _prefs.large_font = 0; // default: tiny font } - if (file.read((uint8_t *)&_prefs.ui_font_style, sizeof(_prefs.ui_font_style)) != sizeof(_prefs.ui_font_style)) { - _prefs.ui_font_style = 0; // default: Classic (FreeSans) - } if (file.read((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)) != sizeof(_prefs.tx_fail_reset_threshold)) { _prefs.tx_fail_reset_threshold = 3; // default: 3 } @@ -286,20 +281,11 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no _prefs.rx_fail_reboot_threshold = 3; // default: 3 } - // v1.7+ Meck region scope fields — may not exist in older prefs files - if (file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)) != sizeof(_prefs.default_scope_name)) { - memset(_prefs.default_scope_name, 0, sizeof(_prefs.default_scope_name)); // default: unscoped - } - if (file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)) != sizeof(_prefs.default_scope_key)) { - memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key)); // default: null key - } - // Clamp to valid ranges if (_prefs.dark_mode > 1) _prefs.dark_mode = 0; if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0; if (_prefs.hint_shown > 1) _prefs.hint_shown = 0; if (_prefs.large_font > 1) _prefs.large_font = 0; - if (_prefs.ui_font_style >= 3) _prefs.ui_font_style = 0; if (_prefs.tx_fail_reset_threshold > 10) _prefs.tx_fail_reset_threshold = 3; if (_prefs.rx_fail_reboot_threshold > 10) _prefs.rx_fail_reboot_threshold = 3; // auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30) @@ -356,11 +342,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)); // 100 file.write((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)); // 101 file.write((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)); // 102 - file.write((uint8_t *)&_prefs.ui_font_style, sizeof(_prefs.ui_font_style)); // 103 - file.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 104 - file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 105 - file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 106 - file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 137 + file.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 103 + file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 104 file.close(); } @@ -447,10 +430,42 @@ void DataStore::loadContacts(DataStoreHost* host) { void DataStore::saveContacts(DataStoreHost* host) { FILESYSTEM* fs = _getContactsChannelsFS(); + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + // nRF52/STM32: direct write (no tmp+rename — InternalFS doesn't need atomic pattern) + File file = openWrite(fs, "/contacts3"); + if (file) { + uint32_t idx = 0; + ContactInfo c; + uint8_t unused = 0; + uint32_t recordsWritten = 0; + + while (host->getContactForSave(idx, c)) { + bool success = (file.write(c.id.pub_key, 32) == 32); + success = success && (file.write((uint8_t *)&c.name, 32) == 32); + success = success && (file.write(&c.type, 1) == 1); + success = success && (file.write(&c.flags, 1) == 1); + success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4); + success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (file.write(c.out_path, 64) == 64); + success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) break; + recordsWritten++; + idx++; + } + file.close(); + Serial.printf("DataStore: saved %d contacts\n", recordsWritten); + } +#else + // ESP32: atomic tmp+rename pattern (protects against SD card corruption on power loss) const char* finalPath = "/contacts3"; const char* tmpPath = "/contacts3.tmp"; - // --- Step 1: Write all contacts to a temporary file --- File file = openWrite(fs, tmpPath); if (!file) { Serial.println("DataStore: saveContacts FAILED — cannot open tmp file"); @@ -489,9 +504,8 @@ void DataStore::saveContacts(DataStoreHost* host) { file.close(); - // --- Step 2: Verify the write completed --- - // Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close) - size_t expectedBytes = recordsWritten * 152; // 152 bytes per contact record + // Verify the write completed + size_t expectedBytes = recordsWritten * 152; File verify = openRead(fs, tmpPath); size_t bytesWritten = verify ? verify.size() : 0; if (verify) verify.close(); @@ -499,23 +513,25 @@ void DataStore::saveContacts(DataStoreHost* host) { if (!writeOk || bytesWritten != expectedBytes) { Serial.printf("DataStore: saveContacts ABORTED — wrote %d bytes, expected %d (%d records)\n", (int)bytesWritten, (int)expectedBytes, recordsWritten); - fs->remove(tmpPath); // Clean up failed tmp file - return; // Original /contacts3 is untouched + fs->remove(tmpPath); + return; } - // --- Step 3: Replace original with verified temp file --- + // Replace original with verified temp file fs->remove(finalPath); if (fs->rename(tmpPath, finalPath)) { Serial.printf("DataStore: saved %d contacts (%d bytes)\n", recordsWritten, (int)bytesWritten); } else { - // Rename failed — tmp file still has the good data Serial.println("DataStore: rename failed, tmp file preserved"); } +#endif } // ========================================================================= // Chunked contact save — non-blocking across multiple loop iterations +// Only for ESP32 with SD card — nRF52 uses blocking saveContacts() above // ========================================================================= +#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM) bool DataStore::beginSaveContacts(DataStoreHost* host) { if (_saveInProgress) return false; // Already saving @@ -607,62 +623,14 @@ void DataStore::finishSaveContacts() { Serial.println("DataStore: rename failed, tmp file preserved"); } } +#endif // !NRF52_PLATFORM && !STM32_PLATFORM void DataStore::loadChannels(DataStoreHost* host) { FILESYSTEM* fs = _getContactsChannelsFS(); // Crash recovery (same pattern as contacts) - if (!fs->exists("/channels3") && fs->exists("/channels3.tmp")) { - Serial.println("DataStore: recovering channels3 from .tmp file"); - fs->rename("/channels3.tmp", "/channels3"); - } - if (fs->exists("/channels3.tmp")) { - fs->remove("/channels3.tmp"); - } - - // Try channels3 (new format with scope_name) first - if (fs->exists("/channels3")) { - File file = openRead(fs, "/channels3"); - if (file) { - bool full = false; - uint8_t channel_idx = 0; - while (!full) { - ChannelDetails ch; - memset(ch.scope_name, 0, sizeof(ch.scope_name)); - uint8_t unused[4]; - - bool success = (file.read(unused, 4) == 4); - success = success && (file.read((uint8_t *)ch.name, 32) == 32); - success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32); - success = success && (file.read((uint8_t *)ch.scope_name, 31) == 31); - - if (!success) break; // EOF - - // Sanitize scope_name — reject if it contains non-region characters - // (catches garbage from uninitialised memory in early channels3 files) - ch.scope_name[30] = '\0'; // force null-terminate - for (int s = 0; ch.scope_name[s]; s++) { - char sc = ch.scope_name[s]; - if (!((sc >= 'a' && sc <= 'z') || (sc >= '0' && sc <= '9') || sc == '-')) { - memset(ch.scope_name, 0, sizeof(ch.scope_name)); // invalid — clear - break; - } - } - - if (host->onChannelLoaded(channel_idx, ch)) { - channel_idx++; - } else { - full = true; - } - } - file.close(); - return; // channels3 loaded successfully - } - } - - // Fall back to channels2 (legacy format without scope_name) if (!fs->exists("/channels2") && fs->exists("/channels2.tmp")) { - Serial.println("DataStore: recovering channels2 from .tmp file"); + Serial.println("DataStore: recovering channels from .tmp file"); fs->rename("/channels2.tmp", "/channels2"); } if (fs->exists("/channels2.tmp")) { @@ -675,7 +643,6 @@ void DataStore::loadChannels(DataStoreHost* host) { uint8_t channel_idx = 0; while (!full) { ChannelDetails ch; - memset(ch.scope_name, 0, sizeof(ch.scope_name)); // default: no scope uint8_t unused[4]; bool success = (file.read(unused, 4) == 4); @@ -691,18 +658,13 @@ void DataStore::loadChannels(DataStoreHost* host) { } } file.close(); - - // Migrate: save as channels3 and remove channels2 - Serial.println("DataStore: migrating channels2 → channels3"); - saveChannels(host); - fs->remove("/channels2"); } } void DataStore::saveChannels(DataStoreHost* host) { FILESYSTEM* fs = _getContactsChannelsFS(); - const char* finalPath = "/channels3"; - const char* tmpPath = "/channels3.tmp"; + const char* finalPath = "/channels2"; + const char* tmpPath = "/channels2.tmp"; File file = openWrite(fs, tmpPath); if (!file) { @@ -720,7 +682,6 @@ void DataStore::saveChannels(DataStoreHost* host) { bool success = (file.write(unused, 4) == 4); success = success && (file.write((uint8_t *)ch.name, 32) == 32); success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32); - success = success && (file.write((uint8_t *)ch.scope_name, 31) == 31); if (!success) { writeOk = false; @@ -733,7 +694,7 @@ void DataStore::saveChannels(DataStoreHost* host) { file.close(); // Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close) - size_t expectedBytes = channel_idx * 99; // 4 + 32 + 32 + 31 = 99 bytes per channel + size_t expectedBytes = channel_idx * 68; // 4 + 32 + 32 = 68 bytes per channel File verify = openRead(fs, tmpPath); size_t bytesWritten = verify ? verify.size() : 0; if (verify) verify.close(); @@ -778,7 +739,7 @@ void DataStore::checkAdvBlobFile() { } void DataStore::migrateToSecondaryFS() { - // migrate old adv_blobs, contacts3 and channels3/channels2 files to secondary FS if they don't already exist + // migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist if (!_fsExtra->exists("/adv_blobs")) { if (_fs->exists("/adv_blobs")) { File oldAdvBlobs = openRead(_fs, "/adv_blobs"); @@ -817,14 +778,10 @@ void DataStore::migrateToSecondaryFS() { _fs->remove("/contacts3"); } } - if (!_fsExtra->exists("/channels3") && !_fsExtra->exists("/channels2")) { - // Migrate channels3 (preferred) or channels2 (legacy) to secondary FS - const char* srcName = _fs->exists("/channels3") ? "/channels3" - : _fs->exists("/channels2") ? "/channels2" - : nullptr; - if (srcName) { - File oldFile = openRead(_fs, srcName); - File newFile = openWrite(_fsExtra, srcName); + if (!_fsExtra->exists("/channels2")) { + if (_fs->exists("/channels2")) { + File oldFile = openRead(_fs, "/channels2"); + File newFile = openWrite(_fsExtra, "/channels2"); if (oldFile && newFile) { uint8_t buf[64]; @@ -835,7 +792,7 @@ void DataStore::migrateToSecondaryFS() { } if (oldFile) oldFile.close(); if (newFile) newFile.close(); - _fs->remove(srcName); + _fs->remove("/channels2"); } } // cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary @@ -878,9 +835,6 @@ void DataStore::migrateToSecondaryFS() { if (_fs->exists("/contacts3")) { _fs->remove("/contacts3"); } - if (_fs->exists("/channels3")) { - _fs->remove("/channels3"); - } if (_fs->exists("/channels2")) { _fs->remove("/channels2"); } diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index ecaca097..b5da9f16 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -24,13 +24,15 @@ class DataStore { void checkAdvBlobFile(); #endif - // Chunked save state +#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM) + // Chunked save state (ESP32 with SD card only) File _saveFile; DataStoreHost* _saveHost = nullptr; uint32_t _saveIdx = 0; uint32_t _saveRecordsWritten = 0; bool _saveInProgress = false; bool _saveWriteOk = true; +#endif public: DataStore(FILESYSTEM& fs, mesh::RTCClock& clock); @@ -45,14 +47,13 @@ public: void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); void loadContacts(DataStoreHost* host); void saveContacts(DataStoreHost* host); +#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM) // Chunked save — splits contact write across multiple loop iterations - // to prevent blocking the main loop for 500ms+ on large contact lists. - // Call beginSaveContacts(), then saveContactsChunk() each loop until it - // returns false (done), then finishSaveContacts() to verify and commit. bool beginSaveContacts(DataStoreHost* host); - bool saveContactsChunk(int batchSize = 20); // returns true if more to write + bool saveContactsChunk(int batchSize = 20); void finishSaveContacts(); bool isSaveInProgress() const { return _saveInProgress; } +#endif void loadChannels(DataStoreHost* host); void saveChannels(DataStoreHost* host); void migrateToSecondaryFS(); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 8cd1cd3c..3fed96af 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -3361,6 +3361,15 @@ void MyMesh::loop() { // is there are pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + // nRF52/STM32: blocking save (fast on internal flash, no chunking needed) + if (!_deferSaves) { + _store->saveContacts(this); + dirty_contacts_expiry = 0; + } else { + dirty_contacts_expiry = futureMillis(2000); + } +#else if (_deferSaves) { // Voice session receiving — push save forward to avoid SPI contention dirty_contacts_expiry = futureMillis(2000); @@ -3368,14 +3377,17 @@ void MyMesh::loop() { _store->beginSaveContacts(this); dirty_contacts_expiry = 0; } +#endif } +#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM) // Drive chunked contact save — write a batch each loop iteration if (_store->isSaveInProgress() && !_deferSaves) { if (!_store->saveContactsChunk(20)) { // 20 contacts per chunk (~3KB, ~30ms) _store->finishSaveContacts(); // Done or error — verify and commit } } +#endif // Discovery scan timeout if (_discoveryActive && millisHasNowPassed(_discoveryTimeout)) { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index c2ede9c1..9c4d5512 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -906,6 +906,21 @@ static CardKBKeyboard cardkb; static unsigned long lastCardKBProbe = 0; #define CARDKB_PROBE_INTERVAL_MS 5000 + + // CardKB compose mode state + static bool ckbComposeMode = false; + static char ckbComposeBuf[138]; // 137 bytes max + null + static int ckbComposePos = 0; + static uint8_t ckbComposeChIdx = 0; + static bool ckbComposeDM = false; + static int ckbComposeDMIdx = -1; + static char ckbComposeDMName[32]; + static unsigned long ckbLastKeystroke = 0; + static bool ckbComposeRefresh = false; + #define CKB_COMPOSE_DEBOUNCE 600 + + void drawCardKBCompose(); + void sendCardKBMessage(); #endif #endif @@ -2869,6 +2884,16 @@ void loop() { #ifdef HAS_4G_MODEM smsMode = ui_task.isOnSMSScreen(); #endif + #elif defined(MECK_CARDKB) + if (!ckbComposeMode) { + ui_task.loop(); + } else { + // Compose mode: debounced rendering + if (ckbComposeRefresh && (millis() - ckbLastKeystroke) >= CKB_COMPOSE_DEBOUNCE) { + drawCardKBCompose(); + ckbComposeRefresh = false; + } + } #else ui_task.loop(); #endif @@ -3177,7 +3202,42 @@ void loop() { char ckb = cardkb.readKey(); if (ckb != 0) { Serial.printf("[CardKB] key=0x%02X '%c'\n", (uint8_t)ckb, (ckb >= 32 && ckb < 127) ? ckb : '?'); - // Block input while locked (T5S3 only — T-Echo Lite has no lock screen yet) + + // --- CardKB compose mode: intercept ALL keys --- + if (ckbComposeMode) { + cpuPower.setBoost(); + ui_task.keepAlive(); + if (ckb == 0x1B) { + // ESC: cancel compose + ckbComposeMode = false; + ui_task.forceRefresh(); + } else if (ckb == '\r') { + // Enter: send message + if (ckbComposePos > 0) { + sendCardKBMessage(); + } else { + ckbComposeMode = false; + ui_task.forceRefresh(); + } + } else if (ckb == '\b') { + // Backspace: delete last character + if (ckbComposePos > 0) { + ckbComposeBuf[--ckbComposePos] = '\0'; + ckbComposeRefresh = true; + ckbLastKeystroke = millis(); + } + } else if (ckb >= 32 && ckb < 127) { + // Printable character + if (ckbComposePos < 137) { + ckbComposeBuf[ckbComposePos++] = ckb; + ckbComposeBuf[ckbComposePos] = '\0'; + ckbComposeRefresh = true; + ckbLastKeystroke = millis(); + } + } + // All keys consumed in compose mode — skip normal routing + } else { + // --- Normal (non-compose) key routing --- #if defined(LilyGo_T5S3_EPaper_Pro) if (!ui_task.isLocked()) { #else @@ -3255,23 +3315,21 @@ void loop() { #endif if (!handled) { - // ESC → back (same as 'q' on T-Deck Pro) for all non-notes screens - if (ckb == 0x1B) { - // Channel picker: ESC goes home + // ESC or Q → back navigation + if (ckb == 0x1B || ckb == 'q') { if (ui_task.isOnChannelPickerScreen()) { ui_task.gotoHomeScreen(); - // Channel screen: ESC goes to picker } else if (ui_task.isOnChannelScreen()) { ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); if (chScr && (chScr->isReplySelectMode() || chScr->isShowingPathOverlay())) { ui_task.injectKey('q'); // dismiss overlay/reply first } else if (chScr && chScr->isDMConversation()) { - ui_task.injectKey('q'); // DM conversation → inbox (handled internally) + ui_task.injectKey('q'); // DM conversation → inbox } else { ui_task.gotoChannelPickerScreen(); } } else { - ui_task.injectKey('q'); + ui_task.gotoHomeScreen(); // All other screens → home } } else if (ckb == '\r') { // Enter key — screen-specific compose or select @@ -3294,8 +3352,17 @@ void loop() { snprintf(label, sizeof(label), "DM: %s", dmName); #if defined(LilyGo_T5S3_EPaper_Pro) ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, j); + #elif defined(MECK_CARDKB) + ckbComposeMode = true; + ckbComposeBuf[0] = '\0'; + ckbComposePos = 0; + ckbComposeDM = true; + ckbComposeDMIdx = (int)j; + strncpy(ckbComposeDMName, dmName, sizeof(ckbComposeDMName) - 1); + ckbComposeRefresh = true; + ckbLastKeystroke = millis(); #else - ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler + ui_task.injectKey('\r'); #endif ui_task.clearDMUnread(j); break; @@ -3311,8 +3378,16 @@ void loop() { snprintf(label, sizeof(label), "To: %s", ch.name); #if defined(LilyGo_T5S3_EPaper_Pro) ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx); + #elif defined(MECK_CARDKB) + ckbComposeMode = true; + ckbComposeBuf[0] = '\0'; + ckbComposePos = 0; + ckbComposeDM = false; + ckbComposeChIdx = chIdx; + ckbComposeRefresh = true; + ckbLastKeystroke = millis(); #else - ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler + ui_task.injectKey('\r'); #endif } } @@ -3339,8 +3414,17 @@ void loop() { snprintf(label, sizeof(label), "DM: %s", dname); #if defined(LilyGo_T5S3_EPaper_Pro) ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx); + #elif defined(MECK_CARDKB) + ckbComposeMode = true; + ckbComposeBuf[0] = '\0'; + ckbComposePos = 0; + ckbComposeDM = true; + ckbComposeDMIdx = idx; + strncpy(ckbComposeDMName, dname, sizeof(ckbComposeDMName) - 1); + ckbComposeRefresh = true; + ckbLastKeystroke = millis(); #else - ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler + ui_task.injectKey('\r'); #endif } } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { @@ -3436,6 +3520,7 @@ void loop() { } } } + } // end compose mode else } } #endif @@ -5112,4 +5197,167 @@ void audio_eof_mp3(const char *info) { } #endif // !HAS_4G_MODEM -#endif // LilyGo_TDeck_Pro \ No newline at end of file +#endif // LilyGo_TDeck_Pro + +// ============================================================================ +// CARDKB COMPOSE FUNCTIONS (T-Echo Lite) +// ============================================================================ +#if defined(MECK_CARDKB) + +void drawCardKBCompose() { + #ifdef DISPLAY_CLASS + display.startFrame(); + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 0); + + // Header: "To: channel" or "DM: contact" + char headerBuf[40]; + if (ckbComposeDM) { + snprintf(headerBuf, sizeof(headerBuf), "DM: %s", ckbComposeDMName); + } else { + ChannelDetails channel; + if (the_mesh.getChannel(ckbComposeChIdx, channel)) { + snprintf(headerBuf, sizeof(headerBuf), "To: %s", channel.name); + } else { + snprintf(headerBuf, sizeof(headerBuf), "To: Channel %d", ckbComposeChIdx); + } + } + display.print(headerBuf); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, display.width(), 1); + + // Body: word-wrapped compose buffer + int y = 14; + int px = 0; + int lineW = display.width(); + char charStr[2] = {0, 0}; + char dblStr[3] = {0, 0, 0}; + bool atWordBoundary = true; + + display.setCursor(0, y); + display.setColor(DisplayDriver::LIGHT); + + for (int i = 0; i < ckbComposePos; i++) { + uint8_t b = (uint8_t)ckbComposeBuf[i]; + + // Word wrap: check if next word fits on this line + if (atWordBoundary && b != ' ' && px > 0) { + int wordW = 0; + for (int j = i; j < ckbComposePos; j++) { + uint8_t wb = (uint8_t)ckbComposeBuf[j]; + if (wb == ' ') break; + dblStr[0] = dblStr[1] = (char)wb; + charStr[0] = (char)wb; + wordW += display.getTextWidth(dblStr) - display.getTextWidth(charStr); + } + if (px + wordW > lineW) { + px = 0; + y += 12; + } + } + + if (b == ' ') { + charStr[0] = ' '; + dblStr[0] = dblStr[1] = ' '; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { + px = 0; + y += 12; + } else { + display.setCursor(px, y); + display.print(charStr); + px += adv; + } + atWordBoundary = true; + } else { + charStr[0] = (char)b; + dblStr[0] = dblStr[1] = (char)b; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { + px = 0; + y += 12; + } + display.setCursor(px, y); + display.print(charStr); + px += adv; + atWordBoundary = false; + } + } + + // Cursor + display.setCursor(px, y); + display.print("_"); + + // Footer status bar + int statusY = display.height() - 12; + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, statusY - 2, display.width(), 1); + display.setCursor(0, statusY); + display.setColor(DisplayDriver::YELLOW); + + char status[32]; + if (ckbComposePos == 0) { + display.print("Esc:Cancel"); + } else { + snprintf(status, sizeof(status), "Esc:X %d/137", ckbComposePos); + display.print(status); + } + const char* rt = "Ent:Send"; + display.setCursor(display.width() - display.getTextWidth(rt) - 2, statusY); + display.print(rt); + + display.endFrame(); + #endif +} + +void sendCardKBMessage() { + if (ckbComposePos == 0) return; + + cpuPower.setBoost(); + + if (ckbComposeDM) { + // Direct message + if (ckbComposeDMIdx >= 0) { + if (the_mesh.uiSendDirectMessage((uint32_t)ckbComposeDMIdx, ckbComposeBuf)) { + ui_task.addSentDM(ckbComposeDMName, the_mesh.getNodePrefs()->node_name, ckbComposeBuf); + ui_task.showAlert("DM sent!", 1500); + } else { + ui_task.showAlert("DM failed!", 1500); + } + } else { + ui_task.showAlert("No contact!", 1500); + } + } else { + // Channel message + ChannelDetails channel; + if (the_mesh.getChannel(ckbComposeChIdx, channel)) { + uint32_t timestamp = rtc_clock.getCurrentTime(); + int len = strlen(ckbComposeBuf); + + if (the_mesh.sendGroupMessage(timestamp, channel.channel, + the_mesh.getNodePrefs()->node_name, + ckbComposeBuf, len)) { + ui_task.addSentChannelMessage(ckbComposeChIdx, + the_mesh.getNodePrefs()->node_name, + ckbComposeBuf); + the_mesh.queueSentChannelMessage(ckbComposeChIdx, timestamp, + the_mesh.getNodePrefs()->node_name, + ckbComposeBuf); + ui_task.showAlert("Sent!", 1500); + } else { + ui_task.showAlert("Send failed!", 1500); + } + } else { + ui_task.showAlert("No channel!", 1500); + } + } + + ckbComposeMode = false; + ckbComposeBuf[0] = '\0'; + ckbComposePos = 0; + ui_task.forceRefresh(); +} + +#endif // MECK_CARDKB \ No newline at end of file diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 7015e833..e0f56fb9 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -732,6 +732,12 @@ public: const char* rtInbox = "Hold:Open"; display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY); display.print(rtInbox); +#elif defined(LILYGO_TECHO_LITE) + display.setCursor(0, footerY); + display.print("Q:Bk"); + const char* rtInbox = "Ent:Open"; + display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY); + display.print(rtInbox); #else display.setCursor(0, footerY); display.print("Q:Bck A/D:Ch"); @@ -968,6 +974,10 @@ public: display.print("Swipe: Switch channel"); display.setCursor(0, 40); display.print("Long press: Compose"); +#elif defined(LILYGO_TECHO_LITE) + display.print("Arrows: Switch channel"); + display.setCursor(0, 40); + display.print("Ent: Compose message"); #else display.print("A/D: Switch channel"); display.setCursor(0, 40); @@ -1400,6 +1410,19 @@ public: display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY); display.print(rtCh); } +#elif defined(LILYGO_TECHO_LITE) + // T-Echo Lite: minimal footer for narrow display + if (_viewChannelIdx == 0xFF) { + display.print("Q:Bk"); + const char* rightText = "Ent:Reply"; + display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); + display.print(rightText); + } else { + display.print("Q:Bk"); + const char* rightText = "Ent:New"; + display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); + display.print(rightText); + } #else // Left side: abbreviated controls if (_replySelectMode) { diff --git a/examples/companion_radio/ui-new/Channelpickerscreen.h b/examples/companion_radio/ui-new/Channelpickerscreen.h index 5d45699b..dfe7d7ca 100644 --- a/examples/companion_radio/ui-new/Channelpickerscreen.h +++ b/examples/companion_radio/ui-new/Channelpickerscreen.h @@ -341,6 +341,11 @@ public: const char* rt = "Boot:Back"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); +#elif defined(LILYGO_TECHO_LITE) + display.print("Q:Bk"); + const char* rt = "Ent:Open"; + display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + display.print(rt); #else display.print("W/S:Nav Q:Back"); const char* rt = "Ent:Open"; diff --git a/examples/companion_radio/ui-new/Contactsscreen.h b/examples/companion_radio/ui-new/Contactsscreen.h index fb1d7edd..34b7c9b9 100644 --- a/examples/companion_radio/ui-new/Contactsscreen.h +++ b/examples/companion_radio/ui-new/Contactsscreen.h @@ -466,6 +466,16 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); } +#elif defined(LILYGO_TECHO_LITE) + display.setCursor(0, footerY); + if (_selectMode) { + display.print("Q:Done"); + } else { + display.print("Q:Bk"); + const char* right = "Ent:Sel"; + display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); + display.print(right); + } #else display.setCursor(0, footerY); if (_selectMode) { diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 5ccd7844..5b949491 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -402,12 +402,16 @@ private: addRow(ROW_TX_POWER); addRow(ROW_UTC_OFFSET); addRow(ROW_MSG_NOTIFY); +#if HAS_GPS addRow(ROW_GPS_BAUD); +#endif addRow(ROW_PATH_HASH_SIZE); addRow(ROW_DEFAULT_SCOPE); addRow(ROW_DARK_MODE); +#if !defined(LILYGO_TECHO_LITE) addRow(ROW_LARGE_FONT); addRow(ROW_FONT_STYLE); +#endif #if defined(LilyGo_T5S3_EPaper_Pro) addRow(ROW_PORTRAIT_MODE); #endif @@ -2404,8 +2408,22 @@ public: } else { display.print("Editing..."); } -#else +#elif defined(LILYGO_TECHO_LITE) if (_editMode == EDIT_TEXT) { + display.print("Ent:Ok Q:Cancel"); + } else if (_editMode == EDIT_PICKER) { + display.print("A/D:Pick Ent:Ok"); + } else if (_editMode == EDIT_NUMBER) { + display.print("W/S:Adj Ent:Ok"); + } else if (_editMode == EDIT_CONFIRM) { + // overlay handles it + } else { + display.print("Q:Bk"); + const char* r = "Ent:Edit"; + display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); + display.print(r); + } +#else display.print("Type, Enter:Ok Q:Cancel"); #ifdef MECK_WIFI_COMPANION } else if (_editMode == EDIT_WIFI) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 994099ee..a0351bb3 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -179,6 +179,16 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, display.setCursor(textX, 0); display.print(battStr); display.setTextSize(1); // restore default text size +#elif defined(LILYGO_TECHO_LITE) + // T-Echo Lite: text-only battery (icon misaligns due to fillRect/setCursor offset mismatch at 2× scale) + char battStr[8]; + snprintf(battStr, sizeof(battStr), "%d%%", batteryPercentage); + uint16_t textWidth = display.getTextWidth(battStr); + int textX = display.width() - textWidth - 2; + if (outIconX) *outIconX = textX; + display.setCursor(textX, 0); // Same baseline as node name (HOME_HDR_Y) + display.print(battStr); + display.setTextSize(1); #else // T-Deck Pro: icon + percentage text (icon hidden in large font) int iconWidth = 16; @@ -322,6 +332,8 @@ public: // T5S3: FreeSans12pt ascenders need more room than built-in font. // Shift header elements down by 4 virtual units (~17px physical). #define HOME_HDR_Y 1 +#elif defined(LILYGO_TECHO_LITE) + #define HOME_HDR_Y 0 #else #define HOME_HDR_Y -3 #endif @@ -369,7 +381,9 @@ public: } } // curr page indicator -#if defined(LilyGo_T5S3_EPaper_Pro) +#if defined(LILYGO_TECHO_LITE) + int y = 13; // Below header +#elif defined(LilyGo_T5S3_EPaper_Pro) int y = 14; // Closer to header #else int y = 14; @@ -393,6 +407,8 @@ public: #else int y = 26; // Standalone: extra line below dots (no IP/Connected row) #endif +#elif defined(LILYGO_TECHO_LITE) + int y = 18; // Below page dots #else int y = 20; #endif @@ -400,7 +416,11 @@ public: display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getUnreadMsgCount()); display.drawTextCentered(display.width() / 2, y, tmp); +#if defined(LILYGO_TECHO_LITE) + y += 12; // Compact +#else y += 14; // Reduced from 18 +#endif #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) IPAddress ip = WiFi.localIP(); @@ -423,7 +443,11 @@ public: display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, y, tmp); +#if defined(LILYGO_TECHO_LITE) + y += 14; // Compact +#else y += 18; +#endif #endif } #endif @@ -480,6 +504,24 @@ public: } display.setTextSize(1); +#else + // Non-T5S3: keyboard shortcut menu +#if defined(LILYGO_TECHO_LITE) + // T-Echo Lite: compact centered menu (tiny font fits 117px virtual width) + display.setColor(DisplayDriver::LIGHT); + display.setTextSize(0); // 6×8 built-in font + y += 2; + display.drawTextCentered(display.width() / 2, y, "M:Msgs C:Contacts"); + y += 8; + display.drawTextCentered(display.width() / 2, y, "S:Set F:Discover"); + y += 8; + display.drawTextCentered(display.width() / 2, y, "H:Last Heard"); + y += 9; + if (y < display.height() - 14) { + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, y, "Arrows: cycle views"); + } + display.setTextSize(1); // restore #else // ----- T-Deck Pro: Keyboard shortcut text menu ----- display.setColor(DisplayDriver::LIGHT); @@ -491,12 +533,10 @@ public: y += 2; int col1, col2; if (_node_prefs->large_font) { - // 9pt font: measure widest left entry and place col2 just past it col1 = 2; int leftW = display.getTextWidth("[M] Messages"); col2 = col1 + leftW + 3; } else { - // Custom tiny (7pt): centered layout col1 = display.width() / 10; col2 = display.width() * 11 / 20; } @@ -578,6 +618,7 @@ public: (_node_prefs->large_font || display.getFontStyle() > 0) ? "A/D: cycle views" : "Press A/D to cycle home views"); } display.setTextSize(1); // restore +#endif // LILYGO_TECHO_LITE #endif } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -1299,14 +1340,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); msg_preview = new MsgPreviewScreen(this, &rtc_clock); channel_screen = new ChannelScreen(this, &rtc_clock); - Serial.printf("[DIAG] channel_screen=%p, unread=%d\n", channel_screen, - channel_screen ? ((ChannelScreen*)channel_screen)->getTotalUnread() : -999); ((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread); channel_picker_screen = new ChannelPickerScreen(this); - Serial.printf("[DIAG] channel_picker=%p\n", channel_picker_screen); ((ChannelPickerScreen*)channel_picker_screen)->setChannelScreen((ChannelScreen*)channel_screen); contacts_screen = new ContactsScreen(this, &rtc_clock); - Serial.printf("[DIAG] contacts=%p\n", contacts_screen); ((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread); #if !defined(LILYGO_TECHO_LITE) text_reader = new TextReaderScreen(this, node_prefs); @@ -1316,12 +1353,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no notes_screen = nullptr; #endif settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs); - Serial.printf("[DIAG] settings=%p\n", settings_screen); repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio path_editor = nullptr; // Lazy-initialized on first use from contacts screen discovery_screen = new DiscoveryScreen(this, &rtc_clock); last_heard_screen = new LastHeardScreen(&rtc_clock); - Serial.printf("[DIAG] discovery=%p, last_heard=%p\n", discovery_screen, last_heard_screen); #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) lock_screen = new LockScreen(this, &rtc_clock, node_prefs); #endif @@ -2014,7 +2049,7 @@ if (curr) curr->poll(); // Without this floor, changing readings (battery, uptime) trigger // back-to-back renders that cause continuous flashing. #ifdef EINK_FULL_REFRESH_ONLY - unsigned long minNext = millis() + 15000; // Full refresh: 15s floor + unsigned long minNext = millis() + 60000; // Full refresh: 60s idle (clock ticks per minute) #else unsigned long minNext = millis() + 800; // Partial refresh: 800ms floor #endif diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 451aa521..8eaf9318 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -19,13 +19,6 @@ #include #include -// Meck custom font styles (Noto Sans, Montserrat) — only available in -// companion radio builds which have -I examples/companion_radio/ui-new -#if __has_include("MeckFonts.h") - #include "MeckFonts.h" - #define HAS_MECK_FONTS 1 -#endif - // Inline CRC32 for frame change detection (replaces bakercp/CRC32 // to avoid naming collision with PNGdec's bundled CRC32.h) class FrameCRC32 { @@ -76,10 +69,18 @@ class GxEPDDisplay : public DisplayDriver { int last_display_crc_value = 0; public: +// Virtual canvas dimensions — default 128×128 (MeshCore standard). +// Override for displays where physical resolution / scale < 128. +#ifndef EINK_VIRTUAL_W + #define EINK_VIRTUAL_W 128 +#endif +#ifndef EINK_VIRTUAL_H + #define EINK_VIRTUAL_H 128 +#endif #if defined(EINK_DISPLAY_MODEL) - GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} + GxEPDDisplay() : DisplayDriver(EINK_VIRTUAL_W, EINK_VIRTUAL_H), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} #else - GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {} + GxEPDDisplay() : DisplayDriver(EINK_VIRTUAL_W, EINK_VIRTUAL_H), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {} #endif bool begin(); diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index bc4f7dbc..b1efb8f0 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -42,10 +42,12 @@ build_flags = ${nrf52_base.build_flags} -D EINK_DISPLAY_MODEL=GxEPD2_122_T61 -D EINK_SCALE_X=1.5f -D EINK_SCALE_Y=2.0f - -D EINK_X_OFFSET=0 - -D EINK_Y_OFFSET=10 + -D EINK_X_OFFSET=6 + -D EINK_Y_OFFSET=1 -D DISPLAY_ROTATION=4 -D EINK_FULL_REFRESH_ONLY=1 + -D EINK_VIRTUAL_W=117 + -D EINK_VIRTUAL_H=88 -D AUTO_OFF_MILLIS=0 build_src_filter = ${nrf52_base.build_src_filter} + @@ -79,9 +81,9 @@ build_flags = ${lilygo_techo_lite_meck.build_flags} -I src/helpers/ui -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 + -D MAX_CONTACTS=250 -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=50 + -D CHANNEL_MSG_HISTORY_SIZE=20 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=64 @@ -90,7 +92,7 @@ build_flags = -D UI_SENSORS_PAGE=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} + +<../examples/companion_radio/*.cpp> @@ -99,6 +101,33 @@ lib_deps = ${lilygo_techo_lite_meck.lib_deps} densaugeo/base64 @ ~1.4.0 +; --- Standalone Radio (no BLE, CardKB + display only) --- +; No companion app — device IS the terminal. +; USB serial for CLI configuration. +; Frees ~20-30KB BLE RAM → room for 500 contacts + larger message history. +[env:meck_techo_lite_standalone] +extends = lilygo_techo_lite_meck +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${lilygo_techo_lite_meck.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 + -D MAX_GROUP_CHANNELS=8 + -D CHANNEL_MSG_HISTORY_SIZE=150 + -D OFFLINE_QUEUE_SIZE=1 + -D MECK_CARDKB + -D UI_RECENT_LIST_SIZE=9 + -D UI_SENSORS_PAGE=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 +build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${lilygo_techo_lite_meck.lib_deps} + densaugeo/base64 @ ~1.4.0 + ; --- BLE Companion Radio (with GPS) --- ; Same as above + L76K GPS for location and time sync. ; Requires external GPS module connected to UART1 (TX=P0.29, RX=P1.10). @@ -113,15 +142,15 @@ build_flags = -D ENV_INCLUDE_GPS=1 -D GPS_BAUD_RATE=9600 -D PIN_GPS_EN=GPS_EN - -D MAX_CONTACTS=500 + -D MAX_CONTACTS=250 -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=50 + -D CHANNEL_MSG_HISTORY_SIZE=20 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=64 -D MECK_CARDKB -D UI_RECENT_LIST_SIZE=9 -D UI_SENSORS_PAGE=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} + +<../examples/companion_radio/*.cpp> @@ -227,12 +256,12 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${lilygo_techo_lite_meck_core.build_flags} - -D MAX_CONTACTS=500 + -D MAX_CONTACTS=250 -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=50 + -D CHANNEL_MSG_HISTORY_SIZE=20 -D BLE_PIN_CODE=234567 -D OFFLINE_QUEUE_SIZE=64 - -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter} + +<../examples/companion_radio/*.cpp>