diff --git a/boards/lilygo_twatch_s3.json b/boards/lilygo_twatch_s3.json deleted file mode 100644 index 6eb2b165..00000000 --- a/boards/lilygo_twatch_s3.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "esp32s3_out.ld", - "partitions": "default_16MB.csv", - "memory_type": "qio_opi" - }, - "core": "esp32", - "extra_flags": [ - "-DARDUINO_USB_MODE=1", - "-DARDUINO_RUNNING_CORE=1", - "-DARDUINO_EVENT_RUNNING_CORE=1" - ], - "f_cpu": "240000000L", - "f_flash": "80000000L", - "flash_mode": "qio", - "psram_type": "opi", - "hwids": [ - [ - "0x303A", - "0x1001" - ] - ], - "mcu": "esp32s3", - "variant": "esp32s3" - }, - "connectivity": [ - "wifi", - "bluetooth" - ], - "debug": { - "default_tool": "esp-builtin", - "onboard_tools": [ - "esp-builtin" - ], - "openocd_target": "esp32s3.cfg" - }, - "frameworks": [ - "arduino", - "espidf" - ], - "name": "LilyGo T-Watch S3 (16M Flash 8M PSRAM)", - "upload": { - "flash_size": "16MB", - "maximum_ram_size": 327680, - "maximum_size": 16777216, - "require_upload_port": true, - "use_1200bps_touch": true, - "wait_for_upload_port": true, - "speed": 921600 - }, - "url": "https://www.lilygo.cc/products/t-watch-s3", - "vendor": "LilyGo" -} diff --git a/boards/lilygo_twatch_s3_plus.json b/boards/lilygo_twatch_s3_plus.json deleted file mode 100644 index f758afc3..00000000 --- a/boards/lilygo_twatch_s3_plus.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "esp32s3_out.ld", - "partitions": "default_16MB.csv", - "memory_type": "qio_opi" - }, - "core": "esp32", - "extra_flags": [ - "-DARDUINO_USB_MODE=1", - "-DARDUINO_RUNNING_CORE=1", - "-DARDUINO_EVENT_RUNNING_CORE=1" - ], - "f_cpu": "240000000L", - "f_flash": "80000000L", - "flash_mode": "qio", - "psram_type": "opi", - "hwids": [["0x303A", "0x1001"]], - "mcu": "esp32s3", - "variant": "esp32s3" - }, - "connectivity": ["wifi", "bluetooth"], - "debug": { - "default_tool": "esp-builtin", - "onboard_tools": ["esp-builtin"], - "openocd_target": "esp32s3.cfg" - }, - "frameworks": ["arduino", "espidf"], - "name": "LilyGo T-Watch S3 Plus (16M Flash 8M PSRAM)", - "upload": { - "flash_size": "16MB", - "maximum_ram_size": 327680, - "maximum_size": 16777216, - "require_upload_port": true, - "use_1200bps_touch": true, - "wait_for_upload_port": true, - "speed": 921600 - }, - "url": "https://www.lilygo.cc/products/t-watch-s3-plus", - "vendor": "LilyGo" -} \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b9f82388..0db8b575 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -12,7 +12,7 @@ #include "ModemManager.h" // Serial CLI modem commands #endif -#if defined(LilyGo_TDeck_Pro_Max) || defined(LILYGO_TWATCH_S3) +#if defined(LilyGo_TDeck_Pro_Max) #include "DRV2605Haptic.h" // inline haptic driver for the 'buzz' CLI test command #endif @@ -1427,20 +1427,8 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i // auto-discovery overwrite it. Skip the base class call entirely — ACKs // embedded in path responses will still be delivered via separate ACK packets. if (contact.flags & CONTACT_FLAG_CUSTOM_PATH) { -#if defined(MECK_TWATCH) - // Watch: keep the manually set path (don't let discovery overwrite it), but - // STILL deliver an embedded login/response reply riding on this path packet - // -- otherwise logins to custom-path contacts silently time out. Gated to - // the watch so other builds' admin flow is unchanged. - MESH_DEBUG_PRINTLN("onContactPathRecv: custom-path %s, keeping path, delivering response (type=%d len=%d)", contact.name, extra_type, extra_len); - if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) { - onContactResponse(contact, extra, extra_len); - } - return false; -#else MESH_DEBUG_PRINTLN("onContactPathRecv: skipping path update for custom-path contact %s", contact.name); return false; -#endif } return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); } @@ -2561,10 +2549,6 @@ void MyMesh::handleCmdFrame(size_t len) { if (strcmp(sp, "gps") == 0) { _prefs.gps_enabled = (np[0] == '1') ? 1 : 0; savePrefs(); - #if defined(MECK_TWATCH) - // Apply the BLDO1 GPS rail live so the toggle takes effect now, no reboot - if (_prefs.gps_enabled) board.gpsPowerOn(); else board.gpsPowerOff(); - #endif } else if (strcmp(sp, "gps_interval") == 0) { uint32_t interval_seconds = atoi(np); _prefs.gps_interval = constrain(interval_seconds, 0, 86400); @@ -3748,7 +3732,7 @@ void MyMesh::checkCLIRescueCmd() { } } -#if defined(LilyGo_TDeck_Pro_Max) || defined(LILYGO_TWATCH_S3) +#if defined(LilyGo_TDeck_Pro_Max) else if (strncmp(cli_command, "buzz", 4) == 0 && (cli_command[4] == '\0' || cli_command[4] == ' ')) { // Fire a DRV2605 library-1 effect to confirm the motor works. diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 50c95009..39b6eff4 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -109,7 +109,7 @@ struct NodePrefs { // persisted to file // upward → fillRect must start above baseline to cover ascenders. // T5S3: always 0 (both sizes use baseline fonts with highlight at y). inline int smallHighlightOff() const { -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) return 0; #else // Custom 7pt fonts at textSize 0 use GFXfont (baseline rendering), not built-in. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4e2be810..f74ec996 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -709,31 +709,13 @@ // ============================================================================= // Define MECK_TOUCH_ENABLED for any platform with touch support -#if defined(LilyGo_T5S3_EPaper_Pro) || (defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN)) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) || (defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN)) #define MECK_TOUCH_ENABLED 1 #endif // --- T-Watch S3 Plus: screen headers for the touch UI --- // The watch needs the same concrete screen types as the gesture machine casts // to, but none of the T5S3/T-Deck hardware baggage (GT911, SD, TCA8418 keyboard). -#if defined(MECK_TWATCH) - #include "TextReaderScreen.h" - #include "NotesScreen.h" - #include "ContactsScreen.h" - #include "ChannelScreen.h" - #include "ChannelPickerScreen.h" - #include "MeckExport.h" - #include "MeckImport.h" - #include "SettingsScreen.h" - #include "RepeaterAdminScreen.h" - #include "DiscoveryScreen.h" - #include "LastHeardScreen.h" - #include "PathEditorScreen.h" - #include "Tracescreen.h" - #include "GamesMenuScreen.h" - #include "SnakeScreen.h" - #include "MinesweeperScreen.h" -#endif // --- T5S3: GT911 capacitive touch driver --- #if defined(LilyGo_T5S3_EPaper_Pro) @@ -901,8 +883,6 @@ #define TOUCH_LONG_PRESS_MS 750 #if defined(LilyGo_T5S3_EPaper_Pro) #define TOUCH_SWIPE_THRESHOLD 60 // T5S3: 960×540 — 60px ≈ 6% of width - #elif defined(MECK_TWATCH) - #define TOUCH_SWIPE_THRESHOLD 16 // Watch: getTouch() returns 0..119 (240px/UI_ZOOM); 16 is ~13% of width #else #define TOUCH_SWIPE_THRESHOLD 30 // T-Deck Pro: 240×320 — 30px ≈ 12.5% of width #endif @@ -939,18 +919,6 @@ } #elif defined(LilyGo_TDeck_Pro) return touchInput.getPoint(*outX, *outY); - #elif defined(MECK_TWATCH) - { - // FT6336U is read through the LovyanGFX panel backing the display. - // display.getTouch() returns coordinates already divided by UI_ZOOM. - int tx, ty; - if (display.getTouch(&tx, &ty)) { - *outX = (int16_t)tx; - *outY = (int16_t)ty; - return true; - } - return false; - } #else return false; #endif @@ -964,11 +932,6 @@ #elif defined(LilyGo_TDeck_Pro) float sx = (float)EINK_WIDTH / 128.0f; // 240/128 = 1.875 float sy = (float)EINK_HEIGHT / 128.0f; // 320/128 = 2.5 - #elif defined(MECK_TWATCH) - // display.getTouch() already divides by UI_ZOOM, so px/py span the - // 240/UI_ZOOM logical canvas (0..119 at UI_ZOOM=2). Scale to 128 virtual. - float sx = (240.0f / UI_ZOOM) / 128.0f; - float sy = (240.0f / UI_ZOOM) / 128.0f; #endif vx = (int)(px / sx); #if defined(LilyGo_TDeck_Pro) @@ -1058,9 +1021,6 @@ static uint32_t _atoi(const char* sp) { DataStore store(LittleFS, rtc_clock); #elif defined(ESP32) #include - #if defined(MECK_TWATCH) - #include // watch: dedicated 'maps' tiles partition - #endif DataStore store(SPIFFS, rtc_clock); #endif @@ -1125,13 +1085,7 @@ static uint32_t _atoi(const char* sp) { /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - #if defined(MECK_TWATCH) - #include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs - #include "WatchChannelConfigScreen.h" // After UITask.h -- needs NodePrefs, the_mesh - #endif - #if defined(MECK_TWATCH) && HAS_GPS - #include "WatchMapScreen.h" // After BLE -- PNGdec headers conflict with BLE if included earlier - #elif HAS_GPS && !defined(LILYGO_TECHO_CARD) + #if HAS_GPS && !defined(LILYGO_TECHO_CARD) #include "MapScreen.h" // After BLE -- PNGdec headers conflict with BLE if included earlier #endif UITask ui_task(&board, &serial_interface); @@ -1348,15 +1302,7 @@ static void lastHeardToggleContact() { // Snake screen: tap = Enter (start / restart) if (ui_task.isOnSnakeScreen()) { -#if defined(MECK_TWATCH) - // Watch: tap starts/restarts, or steers by tapping the up/down/left/right - // zone relative to the board centre while playing. - SnakeScreen* sk = (SnakeScreen*)ui_task.getSnakeScreen(); - if (sk) sk->handleTap(x, y); - return 0; -#else return '\r'; -#endif } // Minesweeper screen: tap = Enter (reveal cell / start / restart) @@ -1364,49 +1310,6 @@ static void lastHeardToggleContact() { return '\r'; } -#if defined(MECK_TWATCH) && HAS_GPS - // Map screen: footer +/- buttons zoom; a tap in the map area recenters on - // GPS. (A top-strip tap is already handled above -> home.) - if (ui_task.isOnMapScreen()) { - WatchMapScreen* wms = (WatchMapScreen*)ui_task.getMapScreen(); - if (wms) wms->handleTap(x, y); - return 0; - } -#endif -#if defined(MECK_TWATCH) - // Notes screen: list rows, view scroll zones and the footer are all tap - // targets, in the same logical coords as the alarm screen. - if (ui_task.isOnWatchNotesScreen()) { - WatchNotesScreen* wn = (WatchNotesScreen*)ui_task.getWatchNotesScreen(); - if (wn) wn->handleTap(x, y); - return 0; - } - // Channel config screen: list rows, detail action rows and the footer are - // all tap targets, in the same logical coords as the alarm/notes screens. - if (ui_task.isOnWatchChannelConfigScreen()) { - WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)ui_task.getWatchChannelConfigScreen(); - if (wc) wc->handleTap(x, y); - return 0; - } -#endif -#ifdef TWATCH_COMPOSE_ENABLED - // TWatch channel screen: touch is handled entirely in the screen's own - // poll() (ticker select, hop-overlay dismiss, compose bar). Do not - // synthesise keys here -- the hop-path overlay must open only on the - // physical button (PWR on the S3 / boot on the Plus), whose KEY_ENTER / - // KEY_NEXT arrive via a separate inject path. - if (ui_task.isOnTWatchChannelScreen()) { - return 0; - } -#endif -#if defined(MECK_TWATCH) - // Watch: tiles open on long-press and pages change on swipe, so a plain tap - // on any home page does nothing -- skip the T5S3 tile hit-test below (wrong - // geometry for the 2-column watch grid) and the left/right page cycling. - if (ui_task.isOnHomeScreen()) { - return 0; - } -#endif // Home screen FIRST page: tile taps (virtual coordinate hit test) if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) { const int tileW = 40, tileH = 22, gapX = 1, gapY = 1; @@ -1576,81 +1479,6 @@ static void lastHeardToggleContact() { if (ui_task.isOnContactsScreen()) { ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); if (cs) { -#if defined(MECK_TWATCH) - if (cs->isActionOverlayOpen()) { - int btn = cs->actionOverlayButtonAtVY(vy); - if (btn == 0) { - // Open -- route by contact type (mirrors the pre-overlay long-press open) - cs->closeActionOverlay(); - int idx = cs->getSelectedContactIdx(); - uint8_t ctype = cs->getSelectedContactType(); - if (idx >= 0 && ctype == ADV_TYPE_CHAT) { - if (ui_task.hasDMUnread(idx)) { - char cname[32]; - cs->getSelectedContactName(cname, sizeof(cname)); - ui_task.clearDMUnread(idx); - ui_task.gotoDMConversation(cname); - return 0; - } - ui_task.openTWatchKeyboard(1, idx); // TWKB_DM - return 0; - } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { - ui_task.gotoRepeaterAdmin(idx); - return 0; - } else if (idx >= 0 && ctype == ADV_TYPE_ROOM) { - ui_task.gotoRepeaterAdmin(idx); - return 0; - } else if (idx >= 0 && ui_task.hasDMUnread(idx)) { - char cname[32]; - cs->getSelectedContactName(cname, sizeof(cname)); - ui_task.clearDMUnread(idx); - ui_task.gotoDMConversation(cname); - return 0; - } - ui_task.forceRefresh(); - return 0; - } else if (btn == 1) { - // Favourite -- flip flags bit 0 on the selected contact and persist - int idx = cs->getSelectedContactIdx(); - if (idx >= 0) { - ContactInfo tmp; - if (the_mesh.getContactByIdx(idx, tmp)) { - ContactInfo* cp = the_mesh.lookupContactByPubKey(tmp.id.pub_key, PUB_KEY_SIZE); - if (cp) { - cp->flags ^= 0x01; - the_mesh.saveContacts(); - ui_task.showAlert((cp->flags & 0x01) ? "Favourited" : "Unfavourited", 1500); - } - } - } - cs->closeActionOverlay(); - cs->invalidateCache(); - ui_task.forceRefresh(); - return 0; - } else if (btn == 2) { - // Delete -- remove the selected contact and persist - int idx = cs->getSelectedContactIdx(); - if (idx >= 0) { - ContactInfo c; - if (the_mesh.getContactByIdx(idx, c)) { - if (the_mesh.removeContact(c)) { - the_mesh.saveContacts(); - ui_task.showAlert("Contact deleted", 1500); - } - } - } - cs->closeActionOverlay(); - cs->invalidateCache(); - ui_task.forceRefresh(); - return 0; - } else { - // Tap outside the buttons -- dismiss - cs->closeActionOverlay(); - ui_task.forceRefresh(); - return 0; - } - } -#endif int result = cs->selectRowAtVY(vy); if (result == 1) { ui_task.forceRefresh(); @@ -1682,27 +1510,6 @@ static void lastHeardToggleContact() { return 0; } -#if defined(MECK_TWATCH) - // Trace screen (watch): tap to select row, tap same to activate. Mode row - // toggles 1/2-byte; Type Path opens the keyboard (handleInput -> wantsKeyboard). - // Guarded to the watch: selectRowAtVY uses the watch's 128-space coords, and - // T5S3/T-Deck keep their existing boot-button/keyboard trace handling. - if (ui_task.isOnTraceScreen()) { - TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); - if (ts) { - int result = ts->selectRowAtVY(vy); - if (result == 1) { - ui_task.forceRefresh(); - return 0; - } - if (result == 2) { - if (ts->isOnModeRow()) return 'd'; // toggle 1-byte / 2-byte - return KEY_ENTER; // Type Path / Add / Remove / Run / Exit / hop - } - } - return 0; - } -#endif // Discovery screen: tap to select, tap same to add if (ui_task.isOnDiscoveryScreen()) { @@ -1738,16 +1545,6 @@ static void lastHeardToggleContact() { // Settings screen: tap to select row, tap same row to activate if (ui_task.isOnSettingsScreen()) { SettingsScreen* ss = (SettingsScreen*)ui_task.getSettingsScreen(); -#if defined(MECK_TWATCH) - // Watch: while editing UTC offset, path hash size, or TX power, tap the - // upper half to increment and the lower half to decrement. Long-press saves. - if (ss && ss->isEditing() && - (ss->getCurrentRowType() == ROW_UTC_OFFSET || - ss->getCurrentRowType() == ROW_PATH_HASH_SIZE || - ss->getCurrentRowType() == ROW_TX_POWER)) { - return (vy < 64) ? 'w' : 's'; - } -#endif if (ss && !ss->isEditing()) { int result = ss->selectRowAtVY(vy); if (result == 1) { @@ -1765,27 +1562,6 @@ static void lastHeardToggleContact() { if (ui_task.isOnSMSScreen()) return 0; #endif -#if defined(MECK_TWATCH) - // Repeater admin (watch): tap selects a menu row (tap again = activate); - // param entry opens the keyboard for the value. - if (ui_task.isOnRepeaterAdmin()) { - RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen(); - if (admin) { - RepeaterAdminScreen::AdminState astate = admin->getState(); - if (astate == RepeaterAdminScreen::STATE_CATEGORY_MENU || - astate == RepeaterAdminScreen::STATE_COMMAND_MENU) { - int result = admin->selectRowAtVY(vy); - if (result == 1) { ui_task.forceRefresh(); return 0; } - if (result == 2) return KEY_ENTER; // tapped current row -- activate - return 0; - } - if (astate == RepeaterAdminScreen::STATE_PARAM_ENTRY) { - ui_task.openTWatchKeyboard(3, 0); // tap opens keyboard for the value - return 0; - } - } - } -#endif // All other screens: tap = select return KEY_ENTER; @@ -1800,14 +1576,6 @@ static void lastHeardToggleContact() { if (ui_task.isOnSMSScreen()) return 0; #endif -#if defined(MECK_TWATCH) && HAS_GPS - // Map screen: swipe pans the viewport (logical-pixel delta) - if (ui_task.isOnMapScreen()) { - WatchMapScreen* wms = (WatchMapScreen*)ui_task.getMapScreen(); - if (wms) wms->panByPixels(dx, dy); - return 0; - } -#endif // Snake screen: swipes control direction if (ui_task.isOnSnakeScreen()) { @@ -1907,11 +1675,6 @@ static void lastHeardToggleContact() { if (ui_task.isOnSMSScreen()) return 0; #endif -#ifdef TWATCH_COMPOSE_ENABLED - // TWatch channel screen: no synthesised keys (see mapTouchTap) -- a long - // press on the header/messages must not open the hop-path overlay. - if (ui_task.isOnTWatchChannelScreen()) return 0; -#endif // Snake screen: long press exits to games menu if (ui_task.isOnSnakeScreen()) { @@ -1928,56 +1691,6 @@ static void lastHeardToggleContact() { return 'q'; } -#if defined(MECK_TWATCH) - // Watch FIRST page: long-press on a coloured tile opens its screen. - // The grid renders in the logical (display.width()) space; touchToVirtual - // yields 128-space coords, so scale them back to render space to hit-test. - if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) { - int vx, vy; - touchToVirtual(x, y, vx, vy); - const int W = display.width(); // logical width (120) - int rvx = vx * W / 128; - int rvy = vy * W / 128; - const int cols = 2, rows = 4; - const int tileW = 56, tileH = 24, gapX = 4, gapY = 2; - const int gridW = tileW * cols + gapX * (cols - 1); // 116 - const int gridX = (W - gridW) / 2; // 2 - int gridY = ui_task.getTileGridVY(); - if (rvx >= gridX && rvx < gridX + gridW && - rvy >= gridY && rvy < gridY + rows * (tileH + gapY)) { - int col = (rvx - gridX) / (tileW + gapX); if (col > 1) col = 1; - int row = (rvy - gridY) / (tileH + gapY); if (row > 3) row = 3; - if (row == 0 && col == 0) { ui_task.openTWatchPicker(); return 0; } - if (row == 0 && col == 1) { ui_task.gotoContactsScreen(); return 0; } - if (row == 1 && col == 0) { ui_task.gotoSettingsScreen(); return 0; } - if (row == 1 && col == 1) { ui_task.gotoDiscoveryScreen(); return 0; } - if (row == 2 && col == 0) { ui_task.gotoTraceScreen(); return 0; } -#if defined(LILYGO_TWATCH_S3) - // No GNSS on the plain S3, so this slot is the games launcher. - if (row == 2 && col == 1) { ui_task.gotoGamesMenu(); return 0; } -#elif HAS_GPS - if (row == 2 && col == 1) { - // Maps: mark the tile FS ready (detectZoomRange in enter() needs it) - // before opening; GPS centre + markers are populated in the main loop. - WatchMapScreen* wms = (WatchMapScreen*)ui_task.getMapScreen(); - if (wms) wms->setMapsReady(LittleFS.exists("/tiles")); - ui_task.gotoMapScreen(); - return 0; - } -#endif - if (row == 3 && col == 0) { ui_task.gotoNotesScreen(); return 0; } - if (row == 3 && col == 1) { ui_task.gotoStepsScreen(); return 0; } - } - return 0; // consume long-press on the tile page - } -#endif -#if defined(MECK_TWATCH) - // Steps screen: long press opens the 7-day history subscreen. - // (On the S3 the PWR key's short press also delivers KEY_ENTER here.) - if (ui_task.isOnStepsScreen()) { - return (char)KEY_ENTER; - } -#endif // Home screen: long press = activate current page action // (BLE toggle, send advert, hibernate, GPS toggle, etc.) @@ -2113,13 +1826,6 @@ static void lastHeardToggleContact() { } } return 0; -#elif defined(MECK_TWATCH) - // Watch: long press raises the contact action overlay - // ([Open] [Favourite] [Delete]). The tap handler dispatches the choice; - // "Open" reproduces the type-aware DM/admin routing this branch used to do. - cs->openActionOverlay(); - ui_task.forceRefresh(); - return 0; #else // T-Deck Pro: long press enters select mode cs->enterSelectMode(); @@ -2165,22 +1871,10 @@ static void lastHeardToggleContact() { #if defined(LilyGo_T5S3_EPaper_Pro) ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32); return 0; -#elif defined(MECK_TWATCH) - ui_task.openTWatchKeyboard(2, 0); // TWKB_ADMIN_PASSWORD - return 0; #else return KEY_ENTER; // T-Deck Pro: keyboard handles password entry #endif } -#if defined(MECK_TWATCH) - // Watch: keyboard only for param entry; menu/other states activate the - // highlighted item (no raw CLI keyboard). - if (astate == RepeaterAdminScreen::STATE_PARAM_ENTRY) { - ui_task.openTWatchKeyboard(3, 0); // param value entry - return 0; - } - return KEY_ENTER; -#endif } } @@ -2197,24 +1891,9 @@ static void lastHeardToggleContact() { if (ui_task.isOnSettingsScreen()) { SettingsScreen* ss = (SettingsScreen*)ui_task.getSettingsScreen(); if (ss) { -#if defined(MECK_TWATCH) - // Watch: long-press while editing UTC offset, path hash size, or TX - // power saves the value. - if (ss->isEditing() && - (ss->getCurrentRowType() == ROW_UTC_OFFSET || - ss->getCurrentRowType() == ROW_PATH_HASH_SIZE || - ss->getCurrentRowType() == ROW_TX_POWER)) { - return '\r'; - } -#endif if (ss->isEditing()) { return 0; // Consume — don't interfere with active edit mode } -#if defined(MECK_TWATCH) - // Aim activation at the row under the finger, not just the current - // cursor, so a single long-press opens/edits the pressed row. - { int vx, vy; touchToVirtual(x, y, vx, vy); ss->selectRowAtVY(vy); } -#endif if (ss->isOnDeletableChannel()) { return 'x'; // Long press on channel row → delete } @@ -2446,19 +2125,6 @@ void setup() { } MESH_DEBUG_PRINTLN("setup() - SPIFFS.begin() done"); -#if defined(MECK_TWATCH) - // Mount the dedicated 'maps' partition as LittleFS (map tiles live here, - // kept separate from the SPIFFS DataStore that holds identity/contacts). - // formatOnFail=true so a freshly flashed (raw) maps partition is formatted - // and immediately usable for ESPConnect tile uploads. - if (LittleFS.begin(true, "/maps", 10, "maps")) { - Serial.printf("maps: LittleFS mounted (%u KB total, %u KB used)\n", - (unsigned)(LittleFS.totalBytes() / 1024), - (unsigned)(LittleFS.usedBytes() / 1024)); - } else { - Serial.println("maps: LittleFS mount FAILED"); - } -#endif // --------------------------------------------------------------------------- // Early SD card init -- needed BEFORE the_mesh.begin() so we can restore @@ -2904,15 +2570,6 @@ void setup() { Serial.println("setup() - SD features initialized"); } #endif -#if defined(MECK_TWATCH) - // Watch: load persisted channel-message history from LittleFS (no SD). - { - ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen(); - if (chanScr && chanScr->loadFromLittleFS()) { - MESH_DEBUG_PRINTLN("setup() - Message history loaded from LittleFS"); - } - } -#endif // Check if node name is still the default hex prefix (first 4 bytes of pub key) // If so, launch onboarding wizard to set name and radio preset // --------------------------------------------------------------------------- @@ -2940,9 +2597,6 @@ void setup() { #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); #endif - #if defined(MECK_TWATCH) - board.gpsPowerOn(); // GPS power is the AXP2101 BLDO1 rail - #endif #if defined(LilyGo_TDeck_Pro_Max) // Speed up / improve the MAX GPS fix: enable multi-constellation // (GPS + GLONASS + BeiDou) and EASY predicted ephemeris (warm/hot starts @@ -2960,9 +2614,6 @@ void setup() { #if defined(LilyGo_TDeck_Pro_Max) board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN #endif - #if defined(MECK_TWATCH) - board.gpsPowerOff(); // GPS power is the AXP2101 BLDO1 rail - #endif sensors.setSettingValue("gps", "0"); } Serial.printf("GPS: power %s\n", gps_wanted ? "ON" : "OFF"); @@ -3034,14 +2685,6 @@ void otaResumeRadio() { #endif void loop() { -#if defined(MECK_TWATCH) - // Watch: flush pending channel-message history to LittleFS on a 30s debounce - // (saveToSD marks it dirty on each message; at most one ~57 KB write / 30s). - { - ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen(); - if (chanScr) chanScr->flushMessagesIfDue(30000UL); - } -#endif // T-Echo Card: lazy Codec2 init from shallow stack context. // codec2_create needs ~3KB stack for FFT/trig init. The loop task // has only 4KB total. Calling from render() (deep call chain) overflows. @@ -3146,7 +2789,7 @@ void loop() { // Map screen: periodically update own GPS position and contact markers #ifdef DISPLAY_CLASS - #if HAS_GPS && !defined(LILYGO_TECHO_CARD) && !defined(MECK_TWATCH) + #if HAS_GPS && !defined(LILYGO_TECHO_CARD) if (ui_task.isOnMapScreen()) { static unsigned long lastMapUpdate = 0; if (millis() - lastMapUpdate > 30000) { // Every 30 seconds @@ -3173,41 +2816,6 @@ void loop() { #endif #endif -#if defined(MECK_TWATCH) && HAS_GPS - // Watch map screen: on open, centre on GPS and populate contact markers; then - // refresh own position + markers periodically while it stays open. - { - static bool mapWasOpen = false; - static unsigned long lastMapUpdate = 0; - if (ui_task.isOnMapScreen()) { - WatchMapScreen* wms = (WatchMapScreen*)ui_task.getMapScreen(); - if (wms) { - bool justOpened = !mapWasOpen; - mapWasOpen = true; - if (justOpened || (millis() - lastMapUpdate > 30000)) { - lastMapUpdate = millis(); - if (justOpened) { - wms->setGPSPosition(sensors.node_lat, sensors.node_lon); - } else { - wms->updateGPSPosition(sensors.node_lat, sensors.node_lon); - } - wms->clearMarkers(); - ContactsIterator it = the_mesh.startContactsIterator(); - ContactInfo ci; - while (it.hasNext(&the_mesh, ci)) { - if (ci.gps_lat != 0 || ci.gps_lon != 0) { - double lat = ((double)ci.gps_lat) / 1000000.0; - double lon = ((double)ci.gps_lon) / 1000000.0; - wms->addMarker(lat, lon, ci.name, ci.type); - } - } - } - } - } else { - mapWasOpen = false; - } - } -#endif // CPU frequency auto-timeout back to idle cpuPower.loop(); @@ -3216,7 +2824,7 @@ void loop() { // is active. The mesh radio has its own FIFO so packets are buffered; // 50 ms yield means the loop still runs 20×/sec which is more than enough // to drain the radio FIFO before overflow. -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) { static bool wasLocked = false; bool nowLocked = ui_task.isLocked(); @@ -4031,12 +3639,6 @@ void loop() { // Channel screen: horizontal swipe opens picker instead of cycling if (ui_task.isOnChannelScreen() && (c == 'a' || c == 'd')) { ui_task.gotoChannelPickerScreen(); -#if defined(MECK_TWATCH) - } else if (ui_task.isOnChannelScreen() && (c == 'w' || c == 's')) { - // Watch: a vertical swipe pages the message view (one page/swipe). - ChannelScreen* cs = (ChannelScreen*)ui_task.getChannelScreen(); - if (cs) { cs->pageScroll(c == 'w'); ui_task.forceRefresh(); } -#endif } else { ui_task.injectKey(c); } @@ -4655,7 +4257,7 @@ void loop() { // The RTOS idle task executes WFI (wait-for-interrupt) during delay(), // dramatically reducing CPU power draw. 50 ms gives 20 loop cycles/sec // which is ample for LoRa packet reception (radio has hardware FIFO). -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) if (ui_task.isLocked()) { delay(50); } diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 7aa0ab9b..f0ec7668 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -11,9 +11,6 @@ #if defined(HAS_SDCARD) && defined(ESP32) #include #endif -#if defined(MECK_TWATCH) - #include // watch has no SD -- message history persists to LittleFS -#endif // DM send status codes (session only). Guarded so AbstractUITask.h can carry // the same definitions without a clash. @@ -107,9 +104,6 @@ private: int _msgsPerPage; // Messages that fit on screen uint8_t _viewChannelIdx; // Which channel we're currently viewing bool _sdReady; // SD card is available for persistence -#if defined(MECK_TWATCH) - unsigned long _msgDirtySince = 0; // watch: pending LittleFS write (0 = clean) -#endif bool _showPathOverlay; // Show path detail overlay for last received msg int _pathScrollPos; // Scroll offset within path overlay hop list int _pathHopsVisible; // Hops that fit on screen (set during render) @@ -223,58 +217,7 @@ public: // Sanitize emoji: replace UTF-8 emoji sequences with single-byte escape codes // The text already contains "Sender: message" format -#if defined(MECK_TWATCH) - // Watch build: drop emoji entirely instead of storing sprite escape bytes. - // The watch UI has no sprite rendering and no room for emoji at 120x120 - // virtual. Non-emoji UTF-8 (accented letters etc.) passes through - // unchanged, the same as emojiSanitize does. - { - const uint8_t* s = (const uint8_t*)text; - int srcLen = (int)strlen(text); - int si = 0, di = 0; - while (si < srcLen && di < CHANNEL_MSG_TEXT_LEN - 1) { - uint8_t b = s[si]; - if (b < 0x80) { msg->text[di++] = (char)b; si++; continue; } - int consumed; - uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed); - if (cp == 0xFE0F) { si += consumed; continue; } // variation selector - if (cp == 0xFFFD) { si += consumed; continue; } // invalid UTF-8 -- skip - bool isEmoji = false; - for (int e = 0; e < EMOJI_COUNT; e++) { - if (EMOJI_CODEPOINTS[e].cp == cp) { - if (EMOJI_CODEPOINTS[e].cp2 != 0) { - // two-codepoint emoji: only a match when the pair matches - int consumed2; - if (si + consumed < srcLen) { - uint32_t cp2 = emojiDecodeUtf8(s + si + consumed, srcLen - si - consumed, &consumed2); - if (cp2 == EMOJI_CODEPOINTS[e].cp2) { - si += consumed + consumed2; - isEmoji = true; - break; - } - } - continue; - } - si += consumed; - isEmoji = true; - break; - } - } - if (!isEmoji) { - for (int a = 0; a < EMOJI_ALIAS_COUNT; a++) { - if (EMOJI_ALIASES[a].cp == cp) { si += consumed; isEmoji = true; break; } - } - } - if (isEmoji) continue; // dropped - if (di + consumed >= CHANNEL_MSG_TEXT_LEN) break; - for (int k = 0; k < consumed; k++) msg->text[di++] = (char)s[si + k]; - si += consumed; - } - msg->text[di] = 0; - } -#else emojiSanitize(text, msg->text, CHANNEL_MSG_TEXT_LEN); -#endif if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) { _msgCount++; @@ -570,13 +513,6 @@ public: // Save the entire message buffer to SD card. // File: /meshcore/messages.bin (~50 KB for 300 messages) void saveToSD() { -#if defined(MECK_TWATCH) - // Watch: defer to the LittleFS flush cadence -- a ~57 KB write on every - // message would wear the flash. Timestamp the pending write; the main loop - // flushes it (see flushMessagesIfDue), and shutdown flushes directly. - if (_msgDirtySince == 0) _msgDirtySince = millis(); - return; -#endif #if defined(HAS_SDCARD) && defined(ESP32) if (!_sdReady) return; @@ -619,93 +555,6 @@ public: #endif } -#if defined(MECK_TWATCH) - // --- Watch message-history persistence (LittleFS; the watch has no SD) ------ - // Flush the ring to LittleFS. Called on an interval from the main loop and - // directly on shutdown, so traffic produces at most one write per interval. - void flushMessagesNow() { - if (!LittleFS.exists("/meshcore")) { - LittleFS.mkdir("/meshcore"); - } - File f = LittleFS.open(MSG_FILE_PATH, "w", true); - if (!f) { - Serial.println("ChannelScreen: LittleFS save failed - can't open file"); - return; - } - MsgFileHeader hdr; - hdr.magic = MSG_FILE_MAGIC; - hdr.version = MSG_FILE_VERSION; - hdr.capacity = CHANNEL_MSG_HISTORY_SIZE; - hdr.count = (uint16_t)_msgCount; - hdr.newestIdx = (int16_t)_newestIdx; - f.write((uint8_t*)&hdr, sizeof(hdr)); - for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { - MsgFileRecord rec; - rec.timestamp = _messages[i].timestamp; - rec.path_len = _messages[i].path_len; - rec.channel_idx = _messages[i].channel_idx; - rec.valid = _messages[i].valid ? 1 : 0; - rec.snr = _messages[i].snr; - rec.dm_peer_hash = _messages[i].dm_peer_hash; - memcpy(rec.path, _messages[i].path, MSG_PATH_MAX); - memcpy(rec.text, _messages[i].text, CHANNEL_MSG_TEXT_LEN); - f.write((uint8_t*)&rec, sizeof(rec)); - } - f.close(); - _msgDirtySince = 0; - } - - // Flush only if a write is pending and the debounce interval has elapsed. - void flushMessagesIfDue(unsigned long intervalMs) { - if (_msgDirtySince && millis() - _msgDirtySince >= intervalMs) flushMessagesNow(); - } - - // Load the ring from LittleFS at boot. Returns true if any messages loaded. - bool loadFromLittleFS() { - if (!LittleFS.exists(MSG_FILE_PATH)) { - Serial.println("ChannelScreen: no saved messages on LittleFS"); - return false; - } - File f = LittleFS.open(MSG_FILE_PATH, "r"); - if (!f) { - Serial.println("ChannelScreen: LittleFS load failed - can't open file"); - return false; - } - MsgFileHeader hdr; - if (f.read((uint8_t*)&hdr, sizeof(hdr)) != sizeof(hdr)) { f.close(); return false; } - if (hdr.magic != MSG_FILE_MAGIC) { f.close(); return false; } - if (hdr.version != MSG_FILE_VERSION) { f.close(); return false; } - if (hdr.capacity != CHANNEL_MSG_HISTORY_SIZE) { f.close(); return false; } - int loaded = 0; - for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { - MsgFileRecord rec; - if (f.read((uint8_t*)&rec, sizeof(rec)) != sizeof(rec)) break; - _messages[i].timestamp = rec.timestamp; - _messages[i].path_len = rec.path_len; - _messages[i].channel_idx = rec.channel_idx; - _messages[i].valid = (rec.valid != 0); - _messages[i].snr = rec.snr; - _messages[i].dm_peer_hash = rec.dm_peer_hash; - memcpy(_messages[i].path, rec.path, MSG_PATH_MAX); - memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN); - _messages[i].scope_idx = 0xFF; - _messages[i].send_ref = 0; - _messages[i].dm_status = DM_SEND_NONE; - _messages[i].dm_attempt = 0; - _messages[i].dm_total = 0; - if (_messages[i].valid) loaded++; - } - _msgCount = (int)hdr.count; - _newestIdx = (int)hdr.newestIdx; - _scrollPos = 0; - if (_newestIdx < -1 || _newestIdx >= CHANNEL_MSG_HISTORY_SIZE) _newestIdx = -1; - if (_msgCount < 0 || _msgCount > CHANNEL_MSG_HISTORY_SIZE) _msgCount = loaded; - f.close(); - Serial.printf("ChannelScreen: Loaded %d messages from LittleFS (count=%d, newest=%d)\n", - loaded, _msgCount, _newestIdx); - return loaded > 0; - } -#endif // Load message buffer from SD card. Returns true if messages were loaded. bool loadFromSD() { diff --git a/examples/companion_radio/ui-new/ContactsScreen.h b/examples/companion_radio/ui-new/ContactsScreen.h index 8efe8cbe..61e4b661 100644 --- a/examples/companion_radio/ui-new/ContactsScreen.h +++ b/examples/companion_radio/ui-new/ContactsScreen.h @@ -4,9 +4,6 @@ #include #include -#if defined(MECK_TWATCH) -#include // blue rounded-rect overlay primitives (watch LCD) -#endif // Timestamps before this (Jan 1 2026 UTC) are treated as invalid/unsynced #define EPOCH_2026 1735689600UL @@ -57,17 +54,6 @@ private: bool _selectMode; uint8_t* _selectedBits; // Bitfield: 1 bit per MAX_CONTACTS raw index -#if defined(MECK_TWATCH) - // --- Action overlay state (watch) --- - // Long-press raises a small [Open] [Favourite] [Delete] overlay. Buttons are - // three full-width bands defined in the 128-space touch grid; render() maps - // them to the 120-space display via display.height(). - bool _actionOverlay = false; - static const int OVL_BTN_H = 26; - static const int OVL_BTN0_TOP = 21; // (128 - (3*26 + 2*4)) / 2 - static const int OVL_BTN1_TOP = 51; // 21 + 26 + 4 - static const int OVL_BTN2_TOP = 81; // 51 + 26 + 4 -#endif // --- helpers --- @@ -196,9 +182,6 @@ public: void resetScroll() { _scrollPos = 0; _cacheValid = false; -#if defined(MECK_TWATCH) - _actionOverlay = false; // never carry a stale overlay across screen entry -#endif } FilterMode getFilter() const { return _filter; } @@ -307,21 +290,6 @@ public: return true; } -#if defined(MECK_TWATCH) - // --- Action overlay (watch) --- - void openActionOverlay() { _actionOverlay = true; } - void closeActionOverlay() { _actionOverlay = false; } - bool isActionOverlayOpen() const { return _actionOverlay; } - - // Hit-test a 128-space touch Y against the overlay buttons. - // Returns 0=Open, 1=Favourite, 2=Delete, -1=miss (tap outside dismisses). - int actionOverlayButtonAtVY(int vy) const { - if (vy >= OVL_BTN0_TOP && vy < OVL_BTN0_TOP + OVL_BTN_H) return 0; - if (vy >= OVL_BTN1_TOP && vy < OVL_BTN1_TOP + OVL_BTN_H) return 1; - if (vy >= OVL_BTN2_TOP && vy < OVL_BTN2_TOP + OVL_BTN_H) return 2; - return -1; - } -#endif int render(DisplayDriver& display) override { if (!_cacheValid) rebuildCache(); @@ -336,11 +304,7 @@ public: int selCount = getSelectedCount(); snprintf(tmp, sizeof(tmp), "%d Selected [%s]", selCount, filterLabel(_filter)); } else { -#if defined(MECK_TWATCH) - snprintf(tmp, sizeof(tmp), "Contacts[%s]", filterLabel(_filter)); -#else snprintf(tmp, sizeof(tmp), "Contacts [%s]", filterLabel(_filter)); -#endif } display.print(tmp); @@ -505,11 +469,6 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); } -#elif defined(MECK_TWATCH) - display.setCursor(0, footerY); - if (!_selectMode) { - display.print("Long Press: Enter"); - } #elif defined(LILYGO_TECHO_LITE) display.setCursor(0, footerY); if (_selectMode) { @@ -535,36 +494,6 @@ public: } #endif -#if defined(MECK_TWATCH) - // Action overlay -- drawn last so it sits on top of the contact list. - if (_actionOverlay) { - const int H = display.height(); // 120 on the watch - const int W = display.width(); // 120 - int y0 = OVL_BTN0_TOP * H / 128; - int y1 = OVL_BTN1_TOP * H / 128; - int y2 = OVL_BTN2_TOP * H / 128; - int bh = OVL_BTN_H * H / 128; - int bx = 6; - int bw = W - 12; - - // Dark backdrop to hide the contact list behind the overlay - display.setColor(DisplayDriver::DARK); - display.fillRect(bx - 2, y0 - 4, bw + 4, (y2 + bh) - y0 + 8); - - // Blue rounded buttons with white centred labels, matching the watch's - // Notes/config style (0x231D = the home-tile blue, 0xFFFF = white). - LGFXDisplay* d = (LGFXDisplay*)&display; - d->setRawColor(0x231D); - d->drawRoundRect(bx, y0, bw, bh, 3); - d->drawRoundRect(bx, y1, bw, bh, 3); - d->drawRoundRect(bx, y2, bw, bh, 3); - - d->setRawColor(0xFFFF); - display.drawTextCentered(W / 2, y0 + (bh - 8) / 2, "Open"); - display.drawTextCentered(W / 2, y1 + (bh - 8) / 2, "Favourite"); - display.drawTextCentered(W / 2, y2 + (bh - 8) / 2, "Delete"); - } -#endif return 5000; // e-ink: next render after 5s } diff --git a/examples/companion_radio/ui-new/DiscoveryScreen.h b/examples/companion_radio/ui-new/DiscoveryScreen.h index cd725e77..03c62809 100644 --- a/examples/companion_radio/ui-new/DiscoveryScreen.h +++ b/examples/companion_radio/ui-new/DiscoveryScreen.h @@ -103,14 +103,6 @@ public: display.setColor(DisplayDriver::LIGHT); display.setCursor(4, 28); display.print(active ? "Listening for adverts..." : "No nodes found"); -#if defined(MECK_TWATCH) - display.setCursor(4, 48); - display.print("Hold: Rescan"); - display.setCursor(4, 58); - display.print("Tap: Select"); - display.setCursor(4, 68); - display.print("Tap Again: Add"); -#else if (!active) { display.setCursor(4, 38); #if defined(LilyGo_T5S3_EPaper_Pro) @@ -119,7 +111,6 @@ public: display.print("F: Scan again Q: Back"); #endif } -#endif } else { // Center visible window around selected item int maxVisible = (maxY - headerHeight) / lineHeight; @@ -208,12 +199,6 @@ public: display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); display.print(mid); - const char* right = "Hold:Rescan"; - display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); - display.print(right); -#elif defined(MECK_TWATCH) - display.print("Tap:Slct"); - const char* right = "Hold:Rescan"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); diff --git a/examples/companion_radio/ui-new/GamesMenuScreen.h b/examples/companion_radio/ui-new/GamesMenuScreen.h index c401d689..c1094a69 100644 --- a/examples/companion_radio/ui-new/GamesMenuScreen.h +++ b/examples/companion_radio/ui-new/GamesMenuScreen.h @@ -136,7 +136,7 @@ public: // --- Footer --- display.setColor(DisplayDriver::LIGHT); -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) display.setTextSize(0); display.drawTextCentered(display.width() / 2, display.height() - 8, "Tap to select"); display.setTextSize(1); diff --git a/examples/companion_radio/ui-new/MinesweeperScreen.h b/examples/companion_radio/ui-new/MinesweeperScreen.h index 8cbcb6a8..d80e3b86 100644 --- a/examples/companion_radio/ui-new/MinesweeperScreen.h +++ b/examples/companion_radio/ui-new/MinesweeperScreen.h @@ -42,16 +42,6 @@ class UITask; #define MINE_FTR 14 #define MINE_OFFX_ADJ 2 #define MINE_OFFY_ADJ 5 -#elif defined(MECK_TWATCH) - // Watch LCD (120x120): denser grid than the 9x9 default, 8px cells so the - // numbers render like the T5S3 (size-0 font). 14*8=112 wide, 11*8=88 tall. - #define MINE_GRID_W 14 - #define MINE_GRID_H 11 - #define MINE_COUNT 22 - #define MINE_CELL_W 8 - #define MINE_CELL_H 8 - #define MINE_HDR 14 - #define MINE_FTR 14 #else #define MINE_GRID_W 9 #define MINE_GRID_H 9 @@ -77,9 +67,6 @@ class UITask; #elif defined(LilyGo_T5S3_EPaper_Pro) #define MINE_TEXT_SIZE 0 #define MINE_TEXT_DY 1 -#elif defined(MECK_TWATCH) - #define MINE_TEXT_SIZE 0 - #define MINE_TEXT_DY 1 #else #define MINE_TEXT_SIZE 1 #define MINE_TEXT_DY 3 @@ -413,7 +400,7 @@ public: display.drawTextCentered(cx, y, "Minesweeper"); y += 16; display.setColor(DisplayDriver::GREEN); -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(cx, y, "Swipe to move cursor"); y += 11; display.drawTextCentered(cx, y, "Tap to reveal"); @@ -432,7 +419,7 @@ public: snprintf(info, sizeof(info), "%dx%d grid, %d mines", MINE_GRID_W, MINE_GRID_H, MINE_COUNT); display.drawTextCentered(cx, y, info); y += 16; -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(cx, y, "Tap to start"); #else display.drawTextCentered(cx, y, "Press Enter to start"); @@ -440,7 +427,7 @@ public: // Footer display.setColor(DisplayDriver::LIGHT); -#if !defined(LilyGo_T5S3_EPaper_Pro) && !defined(MECK_TWATCH) +#if !defined(LilyGo_T5S3_EPaper_Pro) int fy = display.height() - 12; display.drawRect(0, fy - 2, display.width(), 1); display.setCursor(2, fy); diff --git a/examples/companion_radio/ui-new/PathEditorScreen.h b/examples/companion_radio/ui-new/PathEditorScreen.h index 3385b63a..fc763d77 100644 --- a/examples/companion_radio/ui-new/PathEditorScreen.h +++ b/examples/companion_radio/ui-new/PathEditorScreen.h @@ -57,9 +57,6 @@ private: bool _dirty; // Path has been modified bool _wantExit; // Set by Save & Exit — caller should navigate back -#if defined(MECK_TWATCH) - bool _wantKeyboard = false; // Set by Add hop -- caller opens the path keyboard -#endif bool _directLocked; // True = path is explicitly set to direct (0 hops, locked) // --- helpers --- @@ -192,9 +189,6 @@ public: _repScroll = 0; _dirty = false; _wantExit = false; -#if defined(MECK_TWATCH) - _wantKeyboard = false; -#endif _directLocked = false; // Load contact info @@ -287,7 +281,7 @@ public: if (selected) { display.setColor(DisplayDriver::LIGHT); -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineH); #else display.fillRect(0, y + 5, display.width(), lineH); @@ -389,12 +383,6 @@ public: const char* right = "Hold:Select"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); -#elif defined(MECK_TWATCH) - display.setCursor(0, footerY); - display.print("Tap:Slct"); - const char* right = "Hold:Entr"; - display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); - display.print(right); #else display.setCursor(0, footerY); display.print("Q:Bk W/S:Nav"); @@ -495,60 +483,6 @@ public: return 5000; } -#if defined(MECK_TWATCH) - // Watch: replace the whole path from a typed string of comma-separated hex - // hops. Each hop is _bytesPerHop bytes (2*_bytesPerHop hex chars), matching - // the current mode toggle -- e.g. 1B "36,21,dd", 2B "3601,2198,dddd", 3B - // "360184,2198a7,dddddd". Returns NULL on success, or an error message (the - // existing path is left unchanged on any error). UITask is only forward - // declared here, so the caller shows the alert rather than this method. - const char* applyComposedPath(const char* str) { - if (str == nullptr) return "Empty path"; - const int expectChars = _bytesPerHop * 2; - uint8_t tmp[MAX_PATH_SIZE]; - int nbytes = 0, hops = 0; - const char* p = str; - while (*p) { - char tok[16]; - int tlen = 0; - while (*p && *p != ',') { - if (*p != ' ') { - if (tlen >= (int)sizeof(tok) - 1) return "Hop too long"; - tok[tlen++] = *p; - } - p++; - } - if (*p == ',') p++; - if (tlen == 0) continue; // tolerate stray/trailing commas - if (tlen != expectChars) return "Wrong hop length"; - if (hops >= 8) return "Max 8 hops"; - for (int b = 0; b < _bytesPerHop; b++) { - int hi = hexNibble(tok[b * 2]); - int lo = hexNibble(tok[b * 2 + 1]); - if (hi < 0 || lo < 0) return "Bad hex"; - if (nbytes >= MAX_PATH_SIZE) return "Path too long"; - tmp[nbytes++] = (uint8_t)((hi << 4) | lo); - } - hops++; - } - if (hops == 0) return "Empty path"; - memset(_pathBuf, 0, sizeof(_pathBuf)); - memcpy(_pathBuf, tmp, nbytes); - _hopCount = hops; - _pathLen = encodePath(); - _directLocked = false; - _dirty = true; - _menuCount = buildMenuCount(); - _menuSel = 0; - return nullptr; - } - static int hexNibble(char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - } -#endif bool handleInput(char c) override { if (_state == STATE_PICK_HOP) { @@ -613,18 +547,11 @@ public: case MENU_ADD_HOP: _directLocked = false; -#if defined(MECK_TWATCH) - // Watch: request the on-screen keyboard to type the whole path as - // comma-separated hex hops. main loop polls wantsKeyboard() and opens - // it (UITask is only forward declared here, so we can't call it). - _wantKeyboard = true; -#else // Enter picker mode -- adding a hop clears direct lock buildRepeaterList(); _repSel = 0; _repScroll = 0; _state = STATE_PICK_HOP; -#endif return true; case MENU_SET_DIRECT: @@ -774,11 +701,6 @@ public: EditorState getState() const { return _state; } bool isDirty() const { return _dirty; } bool wantsExit() const { return _wantExit; } -#if defined(MECK_TWATCH) - bool wantsKeyboard() const { return _wantKeyboard; } - void clearWantKeyboard() { _wantKeyboard = false; } - int getContactIdx() const { return _contactIdx; } -#endif private: void switchMode(int newBytesPerHop) { diff --git a/examples/companion_radio/ui-new/RepeaterAdminScreen.h b/examples/companion_radio/ui-new/RepeaterAdminScreen.h index f034f59b..dbae5729 100644 --- a/examples/companion_radio/ui-new/RepeaterAdminScreen.h +++ b/examples/companion_radio/ui-new/RepeaterAdminScreen.h @@ -595,11 +595,6 @@ public: // --- Header --- display.setTextSize(1); -#if defined(MECK_TWATCH) - // Clip long lines at the screen edge instead of wrapping them onto the row - // below (restored before returning so other screens are unaffected). - ((LGFXDisplay*)&display)->setTextWrap(false); -#endif display.setColor(DisplayDriver::GREEN); display.setCursor(0, 0); const char* hdrPrefix = (_state == STATE_PASSWORD_ENTRY || _state == STATE_LOGGING_IN) @@ -644,8 +639,6 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit"); renderFooterRight(display, footerY, "Hold:Type"); -#elif defined(MECK_TWATCH) - display.print("Long Press: login"); #else display.print("Sh+Del:Exit"); renderFooterRight(display, footerY, "Ent:Login"); @@ -665,8 +658,6 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit"); renderFooterMidRight(display, footerY, "Back:Exit", "Tap:Open", "Swipe:Sel"); -#elif defined(MECK_TWATCH) - display.print("Long Press: Select"); #else display.print("Q:Exit"); renderFooterMidRight(display, footerY, "Q:Exit", "Ent:Open", "W/S:Sel"); @@ -677,8 +668,6 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back"); renderFooterMidRight(display, footerY, "Back:Back", "Tap:Run", "Swipe:Sel"); -#elif defined(MECK_TWATCH) - display.print("Long Press: Run"); #else display.print("Q:Back"); renderFooterMidRight(display, footerY, "Q:Back", "Ent:Run", "W/S:Sel"); @@ -689,8 +678,6 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Cancel"); renderFooterRight(display, footerY, "Tap:Send"); -#elif defined(MECK_TWATCH) - display.print("Tap: Enter value"); #else display.print("Sh+Del:Cancel"); renderFooterRight(display, footerY, "Ent:Send"); @@ -723,9 +710,6 @@ public: break; } -#if defined(MECK_TWATCH) - ((LGFXDisplay*)&display)->setTextWrap(true); // restore default before returning -#endif if (_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) return 30000; // static text; poll()/callbacks force refresh on state change if (_state == STATE_PASSWORD_ENTRY && _lastCharAt > 0 && (millis() - _lastCharAt) < 800) { @@ -1125,11 +1109,6 @@ private: void renderResponse(DisplayDriver& display, int y, int bodyHeight) { display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); int lineHeight = the_mesh.getNodePrefs()->smallLineH(); -#if defined(MECK_TWATCH) - // Watch: clip long response lines at the screen edge instead of wrapping - // them onto the row below. - ((LGFXDisplay*)&display)->setTextWrap(false); -#endif display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT); @@ -1167,9 +1146,6 @@ private: } display.setTextSize(1); -#if defined(MECK_TWATCH) - ((LGFXDisplay*)&display)->setTextWrap(true); // restore default -#endif } bool handleResponseInput(char c) { diff --git a/examples/companion_radio/ui-new/RxLogScreen.h b/examples/companion_radio/ui-new/RxLogScreen.h index 6d4f87e9..7d3e72c8 100644 --- a/examples/companion_radio/ui-new/RxLogScreen.h +++ b/examples/companion_radio/ui-new/RxLogScreen.h @@ -140,10 +140,6 @@ public: int render(DisplayDriver& display) override { int count = the_mesh.getRxLogCount(); -#if defined(MECK_TWATCH) - // Watch: no keyboard or touch scroll, so cycle through entries automatically. - if (count > 1) { _scrollPos++; if (_scrollPos >= count) _scrollPos = 0; } -#endif if (_scrollPos < 0) _scrollPos = 0; if (_scrollPos > count - 1) _scrollPos = (count > 0) ? count - 1 : 0; @@ -157,11 +153,7 @@ public: display.drawRect(0, 11, display.width(), 1); int headerHeight = 14; -#if defined(MECK_TWATCH) - int footerHeight = 2; // no footer on the watch -#else int footerHeight = 14; -#endif int maxY = display.height() - footerHeight; int y = headerHeight; @@ -188,7 +180,6 @@ public: display.setTextSize(1); // === Footer === -#if !defined(MECK_TWATCH) int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); @@ -198,13 +189,8 @@ public: #else display.print("Q:Bk W/S:Scroll"); #endif -#endif -#if defined(MECK_TWATCH) - return 3000; // watch: advance to the next packet every 3s -#else return 5000; // refresh every 5s to pick up newly received packets -#endif } bool handleInput(char c) override { diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 33bfeb94..72c335f7 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -2,11 +2,6 @@ #include #include -#if defined(MECK_TWATCH) -#include // setTextWrap/getCursorX for the row ticker -// Row ticker scroll speed -- matches TW_TICKER_MS_PER_PX in TWatchComposeScreens.h. -#define SETTINGS_TICKER_MS_PER_PX 20 -#endif #include #include #include @@ -347,14 +342,6 @@ private: // T5S3: signal UITask to open VKB when entering text edit mode bool _needsTextVKB; bool _wantsWatchChannels; // Watch: hand off to WatchChannelConfigScreen (polled by UITask) -#if defined(MECK_TWATCH) - // Selected-row ticker (marquee for rows wider than the screen), mirroring - // TWatchChannelScreen: 20 ms/px, 24 px gap between the looping copies. - int _tickerRow = -1; // row index currently anchored - unsigned long _tickerStartMs = 0; // animation anchor - int _tickerTextW = 0; // measured on pass 0 of the previous frame - bool _tickerActive = false; -#endif // 4G modem state (runtime cache of config) #ifdef HAS_4G_MODEM @@ -454,9 +441,6 @@ private: void rebuildRows() { _numRows = 0; -#if defined(MECK_TWATCH) - _tickerRow = -1; // row indices are about to change; re-anchor the ticker -#endif if (_subScreen == SUB_CONTACTS) { // --- Contacts sub-screen: only contact-related rows --- @@ -516,18 +500,14 @@ private: addRow(ROW_BACKLIGHT_BRIGHTNESS); addRow(ROW_KB_BACKLIGHT); #endif -#if !defined(MECK_TWATCH) addRow(ROW_MSG_NOTIFY); -#endif #if HAS_GPS addRow(ROW_GPS_BAUD); #endif addRow(ROW_PATH_HASH_SIZE); addRow(ROW_DEFAULT_SCOPE); -#if !defined(MECK_TWATCH) addRow(ROW_DARK_MODE); -#endif -#if !defined(LILYGO_TECHO_LITE) && !defined(MECK_TWATCH) +#if !defined(LILYGO_TECHO_LITE) addRow(ROW_LARGE_FONT); addRow(ROW_FONT_STYLE); #endif @@ -1830,13 +1810,6 @@ public: int y = headerH; -#if defined(MECK_TWATCH) - // Watch: rows longer than the 120px line (e.g. "GPS Baud: Default (9600)") - // must clip at the screen edge, not wrap onto the row below and overprint - // it. Restored after the loop -- other screens rely on wrap being on. - ((LGFXDisplay*)&display)->setTextWrap(false); - _tickerActive = false; -#endif for (int i = _scrollTop; i < endIdx && y + lineHeight <= maxY; i++) { bool selected = (i == _cursor); @@ -1857,31 +1830,7 @@ public: display.setColor(DisplayDriver::LIGHT); } -#if defined(MECK_TWATCH) - // Selected-row ticker, mirroring TWatchChannelScreen::drawTicker. The - // row text is only known inside the switch below, so the marquee's - // seamless second copy is drawn by running the row's case twice, one - // period apart. Width is measured on pass 0 via getCursorX, so a newly - // selected row shows its first frame unscrolled and scrolls from the - // next frame on. - int rowStartX = 0; - int passes = 1; - if (selected) { - if (_tickerRow != i) { _tickerRow = i; _tickerStartMs = millis(); _tickerTextW = 0; } - if (_tickerTextW > display.width() - sbW - 4) { - int period = _tickerTextW + 24; - int off = (int)(((millis() - _tickerStartMs) / SETTINGS_TICKER_MS_PER_PX) % (unsigned long)period); - rowStartX = 2 - off; - passes = 2; - _tickerActive = true; - } - } - int rowTextW = 0; - for (int tickerPass = 0; tickerPass < passes; tickerPass++) { - display.setCursor(rowStartX + tickerPass * (_tickerTextW + 24), y); -#else display.setCursor(0, y); -#endif switch (_rows[i].type) { case ROW_NAME: @@ -2369,22 +2318,10 @@ public: #endif } -#if defined(MECK_TWATCH) - if (selected && tickerPass == 0) { - // getCursorX() is unusable here: LGFXDisplay::print() calls buffer.println(), - // whose newline resets the cursor X to 0. Measure the string directly. - rowTextW = display.getTextWidth(tmp); - } - } // ticker pass loop - if (selected) _tickerTextW = rowTextW; -#endif y += lineHeight; } -#if defined(MECK_TWATCH) - ((LGFXDisplay*)&display)->setTextWrap(true); -#endif // Scrollbar (track + proportional thumb), mirroring the notif-sound picker. if (showScrollbar) { @@ -3005,17 +2942,9 @@ public: } else if (_subScreen != SUB_NONE) { display.print("Q:Back"); } else { -#if defined(MECK_TWATCH) - display.print("Hold:Edit"); -#else display.print("Q:Bk"); -#endif } -#if defined(MECK_TWATCH) - const char* r = (_subScreen == SUB_NONE) ? "Tap:Cycle" : "Tap/Ent:Edit"; -#else const char* r = "Tap/Ent:Edit"; -#endif display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); display.print(r); } @@ -3029,13 +2958,6 @@ public: return 200; // 200ms — fast enough for web server responsiveness } #endif -#if defined(MECK_TWATCH) - // Marquee running, or a wide row was just selected (its width was measured - // this frame): refresh fast so scrolling starts promptly instead of after - // the 1s idle interval. - if (_tickerActive || - (_tickerRow == _cursor && _tickerTextW > display.width() - 10)) return 60; -#endif return _editMode != EDIT_NONE ? 700 : 1000; } @@ -3870,18 +3792,12 @@ public: Serial.println("Settings: entered Contacts sub-screen"); break; case ROW_CHANNELS_SUBMENU: -#if defined(MECK_TWATCH) - // Watch: the desktop sub-screen's per-channel actions are keyboard - // hotkeys; hand off to the standalone WatchChannelConfigScreen. - _wantsWatchChannels = true; // UITask::loop() polls and opens it -#else _savedTopCursor = _cursor; _subScreen = SUB_CHANNELS; _cursor = 0; _scrollTop = 0; rebuildRows(); Serial.println("Settings: entered Channels sub-screen"); -#endif break; case ROW_RXLOG: diff --git a/examples/companion_radio/ui-new/SnakeScreen.h b/examples/companion_radio/ui-new/SnakeScreen.h index 00267a3a..a2a09a6c 100644 --- a/examples/companion_radio/ui-new/SnakeScreen.h +++ b/examples/companion_radio/ui-new/SnakeScreen.h @@ -37,11 +37,7 @@ class UITask; #define SNAKE_HI_VERSION 1 // Tick cadence: the watch LCD has no e-ink refresh limit, so run a touch faster. -#if defined(MECK_TWATCH) - #define SNAKE_TICK_MS 250 -#else #define SNAKE_TICK_MS 500 -#endif class SnakeScreen : public UIScreen { public: @@ -403,9 +399,7 @@ public: display.drawTextCentered(cx, y, "Classic Snake"); y += 14; display.setColor(DisplayDriver::GREEN); -#if defined(MECK_TWATCH) - display.drawTextCentered(cx, y, "Tap to steer"); -#elif defined(LilyGo_T5S3_EPaper_Pro) +#if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(cx, y, "Swipe to steer"); #else display.drawTextCentered(cx, y, "W/S/A/D to steer"); @@ -440,7 +434,7 @@ public: } display.setColor(DisplayDriver::LIGHT); -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(cx, y, "Tap to start"); #else display.drawTextCentered(cx, y, "Press Enter to start"); @@ -493,16 +487,12 @@ public: ty += 12; } display.setColor(DisplayDriver::GREEN); -#if defined(MECK_TWATCH) - display.drawTextCentered(cx, ty, "Tap to retry"); -#else display.drawTextCentered(cx, ty, "Enter:Retry Sh+Del:Back"); -#endif } } display.setColor(DisplayDriver::LIGHT); -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) char footBuf[32]; snprintf(footBuf, sizeof(footBuf), "Score: %d", _score); display.setTextSize(0); diff --git a/examples/companion_radio/ui-new/Tracescreen.h b/examples/companion_radio/ui-new/Tracescreen.h index d83860ed..517ff8de 100644 --- a/examples/companion_radio/ui-new/Tracescreen.h +++ b/examples/companion_radio/ui-new/Tracescreen.h @@ -91,9 +91,6 @@ private: int _resultScroll; bool _wantExit; -#if defined(MECK_TWATCH) - bool _wantKeyboard; // Watch: Type Path tapped -> UITask opens the on-screen keyboard -#endif // --- Menu helpers (STATE_BUILD) --- // Menu layout: @@ -321,50 +318,6 @@ public: return _state == STATE_BUILD && menuItemAt(_menuSel) == MENU_PATH_SIZE; } -#if defined(MECK_TWATCH) - bool wantsKeyboard() const { return _wantKeyboard; } - void clearWantKeyboard() { _wantKeyboard = false; } - - // Tap-to-select row mapping for the watch (mirrors PathEditor/Discovery). - // vy is in the 128-tall virtual touch space. Returns: - // 0 = miss, 1 = moved the cursor, 2 = tapped the already-selected row. - int selectRowAtVY(int vy) { - if (_state == STATE_BUILD) return selectBuildRowAtVY(vy); - if (_state == STATE_PICK_HOP) return selectPickerRowAtVY(vy); - return 0; - } - - int selectBuildRowAtVY(int vy) { - int menuCount = buildMenuCount(); - if (menuCount == 0) return 0; - const int headerH = 14, footerH = 14, lineH = 11; - const int bodyTop = headerH; // renderBuild draws row 0 at y=14 - if (vy < bodyTop || vy >= 128 - footerH) return 0; - int maxVisible = (128 - headerH - footerH) / lineH; - if (maxVisible < 1) maxVisible = 1; - // Replicate renderBuild's scroll window so a tap maps to the drawn row. - int scrollTop = 0; - if (_menuSel >= scrollTop + maxVisible) scrollTop = _menuSel - maxVisible + 1; - if (_menuSel < scrollTop) scrollTop = _menuSel; - int tappedRow = scrollTop + (vy - bodyTop) / lineH; - if (tappedRow < 0 || tappedRow >= menuCount) return 0; - if (tappedRow == _menuSel) return 2; - _menuSel = tappedRow; - return 1; - } - - int selectPickerRowAtVY(int vy) { - if (_repCount == 0) return 0; - const int headerH = 14, footerH = 14, lineH = 11; - const int bodyTop = headerH; // renderPicker draws row 0 at y=14 - if (vy < bodyTop || vy >= 128 - footerH) return 0; - int tappedRow = _repScroll + (vy - bodyTop) / lineH; - if (tappedRow < 0 || tappedRow >= _repCount) return 0; - if (tappedRow == _repSel) return 2; - _repSel = tappedRow; - return 1; - } -#endif // Returns the current path formatted as a comma-separated string, suitable // for pre-populating an external text editor (e.g. the T5S3 virtual keyboard). @@ -396,9 +349,6 @@ public: _repSel = 0; _repScroll = 0; _wantExit = false; -#if defined(MECK_TWATCH) - _wantKeyboard = false; -#endif _resultScroll = 0; memset(_pathBuf, 0, sizeof(_pathBuf)); memset(&_result, 0, sizeof(_result)); @@ -518,8 +468,6 @@ private: } else { #if defined(LilyGo_T5S3_EPaper_Pro) snprintf(tmp, sizeof(tmp), "%c Type Path: [Long press]", prefix); -#elif defined(MECK_TWATCH) - snprintf(tmp, sizeof(tmp), "%c Type Path: [Tap]", prefix); #else snprintf(tmp, sizeof(tmp), "%c Type Path: [Press Enter]", prefix); #endif @@ -591,8 +539,6 @@ private: } else { #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit Tap:Sel"); -#elif defined(MECK_TWATCH) - display.print("Tap:Sel Hold:Go"); #else display.print("Q:Exit W/S:Nav Ent:Sel"); #endif @@ -656,8 +602,6 @@ private: display.setCursor(0, footerY); #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:Add"); -#elif defined(MECK_TWATCH) - display.print("Tap:Sel Hold:Add"); #else display.print("Q:Back W/S:Scroll Ent:Add"); #endif @@ -810,8 +754,6 @@ private: display.setCursor(0, footerY); #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:New Trace"); -#elif defined(MECK_TWATCH) - display.print("Hold: New Trace"); #else display.print("Q:Back Ent:New Trace"); #endif @@ -908,14 +850,8 @@ private: switch (item) { case MENU_TYPE_PATH: pathToEditBuf(); -#if defined(MECK_TWATCH) - // Watch: no physical keys -- request the on-screen keyboard. The main - // loop polls wantsKeyboard() and opens it, pre-filled with the path. - _wantKeyboard = true; -#else // Enter inline edit mode -- pre-fill with current path if any _editing = true; -#endif return true; case MENU_ADD_HOP: diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index dead472b..8bb9a514 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -16,16 +16,14 @@ #ifdef MECK_WEB_READER #include "WebReaderScreen.h" #endif -#if defined(MECK_TWATCH) && HAS_GPS - #include "WatchMapScreen.h" -#elif HAS_GPS && !defined(LILYGO_TECHO_CARD) +#if HAS_GPS && !defined(LILYGO_TECHO_CARD) #include "MapScreen.h" #endif #include "target.h" #if defined(LilyGo_TDeck_Pro_Max) #include "DRV2605Haptic.h" // haptic motor for "Buzzer (vibrate)" channels #endif -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT) #include "HomeIcons.h" #endif #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) @@ -76,16 +74,6 @@ #include "AudiobookPlayerScreen.h" #include "VoiceMessageScreen.h" #endif -#if defined(MECK_TWATCH) -#include // step history persistence (the "maps" partition) -#endif -#if defined(MECK_TWATCH) -#include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs -#include "WatchChannelConfigScreen.h" // After UITask.h -- needs NodePrefs, the_mesh -#endif -#ifdef TWATCH_COMPOSE_ENABLED -#include "TWatchComposeScreens.h" -#endif #ifdef HAS_4G_MODEM #include "SMSScreen.h" #include "ModemManager.h" @@ -143,11 +131,7 @@ public: // version info display.setColor(DisplayDriver::LIGHT); -#if defined(MECK_TWATCH) - display.setTextSize(1); // 240x240: size 2 overflows the 120px virtual width and wraps -#else display.setTextSize(2); -#endif display.drawTextCentered(display.width()/2, 22, _version_info); display.setTextSize(1); @@ -184,9 +168,7 @@ class HomeScreen : public UIScreen { #if HAS_BQ27220 BATTERY, #endif -#if !defined(MECK_TWATCH) SHUTDOWN, -#endif Count // keep as last }; @@ -369,11 +351,7 @@ public: bool isEditingUTC() const { return _editing_utc; } bool isFirstPage() const { return _page == HomePage::FIRST; } bool isOnRecentPage() const { return _page == HomePage::RECENT; } -#if defined(MECK_TWATCH) - bool isOnShutdownPage() const { return false; } -#else bool isOnShutdownPage() const { return _page == HomePage::SHUTDOWN; } -#endif void cancelEditing() { if (_editing_utc) { _node_prefs->utc_offset_hours = _saved_utc_offset; @@ -390,7 +368,7 @@ public: int render(DisplayDriver& display) override { char tmp[80]; -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) _task->setHomeShowingTiles(false); // Reset — only set true on FIRST page #endif @@ -425,19 +403,11 @@ public: #define HOME_HDR_Y 1 #elif defined(LILYGO_TECHO_LITE) #define HOME_HDR_Y 0 -#elif defined(MECK_TWATCH) - #define HOME_HDR_Y 1 #else #define HOME_HDR_Y -3 #endif display.setCursor(0, HOME_HDR_Y); -#if defined(MECK_TWATCH) - // Watch: render the name in a very small font so long names fit beside the - // centred clock instead of overrunning it. Colour was set above (GREEN). - ((LGFXDisplay*)&display)->printSmallFont(0, HOME_HDR_Y, filtered_name); -#else display.print(filtered_name); -#endif // battery voltage + status icons #ifdef MECK_AUDIO_VARIANT int battLeftX = display.width(); // default if battery doesn't render @@ -448,62 +418,11 @@ public: // alarm enabled indicator (AL icon, left of audio or battery) renderAlarmIndicator(display, battLeftX); -#elif !defined(MECK_TWATCH) +#else renderBatteryIndicator(display, _task->getBattMilliVolts()); #endif // centered clock — only show when time is valid -#if defined(MECK_TWATCH) - // Watch: right-aligned header cluster, right to left -- battery %, unread - // count, then clock, all in the tiny node-name font. Battery % is - // colour-coded by charge level. - { - LGFXDisplay* lcd = (LGFXDisplay*)&display; - - // Battery % from the AXP2101 fuel gauge, rightmost. - int pct = (int)_task->getBatteryPercent(); - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - char battbuf[6]; - sprintf(battbuf, "%d%%", pct); - int battX = display.width() - lcd->smallTextWidth(battbuf) - 2; - uint16_t battColor = (pct >= 50) ? 0x07E0 // green 50-100% - : (pct >= 25) ? 0xFFE0 // yellow 25-50% - : (pct >= 10) ? 0xFD20 // orange 10-25% - : 0xF800; // red 0-10% - lcd->setRawColor(battColor); - lcd->printSmallFont(battX, HOME_HDR_Y, battbuf); - - // Unread count (blue), to the left of the battery %. - char numbuf[6]; - sprintf(numbuf, "%d", _task->getUnreadMsgCount()); - int numX = battX - lcd->smallTextWidth(numbuf) - 3; - display.setColor(DisplayDriver::BLUE); - lcd->printSmallFont(numX, HOME_HDR_Y, numbuf); - - // Clock (white), to the left of the unread count, when time is valid. - uint32_t now = _rtc->getCurrentTime(); - if (now > 1700000000) { // valid timestamp (after ~Nov 2023) - int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600); - int hrs = (local / 3600) % 24; - if (hrs < 0) hrs += 24; - int mins = (local / 60) % 60; - if (mins < 0) mins += 60; - char timeBuf[6]; - sprintf(timeBuf, "%02d:%02d", hrs, mins); - int clockX = (display.width() - lcd->smallTextWidth(timeBuf)) / 2; - // Long node names: keep the clock centred when there is room, - // otherwise push it right so it starts just past the name. When even - // that would run into the unread count, hide the clock instead. - int nameEndX = lcd->smallTextWidth(filtered_name) + 3; - if (clockX < nameEndX) clockX = nameEndX; - if (clockX + lcd->smallTextWidth(timeBuf) + 3 <= numX) { - display.setColor(DisplayDriver::LIGHT); - lcd->printSmallFont(clockX, HOME_HDR_Y, timeBuf); - } - } - } -#else { uint32_t now = _rtc->getCurrentTime(); if (now > 1700000000) { // valid timestamp (after ~Nov 2023) @@ -529,14 +448,11 @@ public: display.setTextSize(1); // restore } } -#endif // curr page indicator #if defined(LILYGO_TECHO_LITE) int y = 13; // Below header #elif defined(LilyGo_T5S3_EPaper_Pro) int y = 14; // Closer to header -#elif defined(MECK_TWATCH) - int y = (_page == HomePage::FIRST) ? 9 : 18; // first page: tuck up under the header #else int y = 14; #endif @@ -550,7 +466,7 @@ public: } if (_page == HomePage::FIRST) { -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) _task->setHomeShowingTiles(true); #endif #if defined(LilyGo_T5S3_EPaper_Pro) @@ -561,23 +477,17 @@ public: #endif #elif defined(LILYGO_TECHO_LITE) int y = 18; // Below page dots -#elif defined(MECK_TWATCH) - int y = 12; // 4-row grid starts just below the dots #else int y = 20; #endif -#if !defined(MECK_TWATCH) display.setColor(DisplayDriver::YELLOW); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getUnreadMsgCount()); display.drawTextCentered(display.width() / 2, y, tmp); -#endif #if defined(LILYGO_TECHO_LITE) y += 12; // Compact #elif defined(LilyGo_TDeck_Pro_Max) y += 10; // MAX: pull < Connected > up under MSG to make room for [T] Phone -#elif defined(MECK_TWATCH) - // watch: no MSG banner gap -- the 4-row grid sits just under the dots #else y += 14; // Reduced from 18 #endif @@ -690,52 +600,6 @@ public: } display.setTextSize(1); -#elif defined(MECK_TWATCH) - // ----- T-Watch S3 Plus: P4-style coloured tile grid (3x2) ----- - // Border colours approximate the Meck P4 home palette (RGB565): a white - // icon + label on a dark navy fill, each tile a distinct bright border. - // setRawColor() pushes exact RGB565 past the Color enum + watch grey remap. - { - struct Tile { const uint8_t* icon; const char* label; uint16_t color; }; - const Tile tiles[4][2] = { - { {icon_envelope, "Messages", 0x0CB1}, {icon_people, "Contacts", 0xCAA0} }, - { {icon_gear, "Settings", 0x0560}, {icon_search, "Discover", 0xF81F} }, -#if defined(LILYGO_TWATCH_S3) - { {icon_trace, "Trace", 0xF800}, {icon_gamepad, "Games", 0x231D} }, -#else - { {icon_trace, "Trace", 0xF800}, {icon_map, "Maps", 0x231D} }, -#endif - { {icon_notepad, "Notes", 0x07FF}, {icon_star, "Steps", 0xFFE0} }, - }; - const uint16_t TILE_FILL = 0x18C5; // dark navy - - const int cols = 2, rows = 4; - const int tileW = 56, tileH = 24, gapX = 4, gapY = 2; - const int gridW = tileW * cols + gapX * (cols - 1); - const int gridX = (display.width() - gridW) / 2; - const int gridY = y + 2; - _task->setTileGridVY(gridY); - - LGFXDisplay* lcd = (LGFXDisplay*)&display; // watch display is always LGFXDisplay - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - int tx = gridX + col * (tileW + gapX); - int ty = gridY + row * (tileH + gapY); - lcd->setRawColor(TILE_FILL); - lcd->fillRoundRect(tx, ty, tileW, tileH, 4); - lcd->setRawColor(tiles[row][col].color); - lcd->drawRoundRect(tx, ty, tileW, tileH, 4); - display.setColor(DisplayDriver::LIGHT); - int iconX = tx + (tileW - HOME_ICON_W) / 2; - int iconY = ty + 3; - display.drawXbm(iconX, iconY, tiles[row][col].icon, HOME_ICON_W, HOME_ICON_H); - display.setTextSize(_node_prefs->smallTextSize()); - display.drawTextCentered(tx + tileW / 2, ty + 16, tiles[row][col].label); - } - } - display.setTextSize(1); - } - #else // Non-T5S3: keyboard shortcut menu #if defined(LILYGO_TECHO_LITE) @@ -922,9 +786,6 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(display.width() / 2, display.height() - 24, "Tap here for full Last Heard list"); -#elif defined(MECK_TWATCH) - display.drawTextCentered(display.width() / 2, display.height() - 24, - "Long Press: Full Last Heard List"); #else display.drawTextCentered(display.width() / 2, display.height() - 24, "H: Full Last Heard list"); @@ -983,8 +844,6 @@ public: display.setTextSize(1); #if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(display.width() / 2, 80, "toggle: " PRESS_LABEL); -#elif defined(MECK_TWATCH) - display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL); #else display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, 78, "or press Enter key"); @@ -1038,8 +897,6 @@ public: #endif #if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(display.width() / 2, 64, "advert: " PRESS_LABEL); -#elif defined(MECK_TWATCH) - display.drawTextCentered(display.width() / 2, 57, "advert: " PRESS_LABEL); #else display.drawTextCentered(display.width() / 2, 57, "advert: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, 67, "or press Enter key"); @@ -1268,7 +1125,6 @@ public: display.drawTextRightAlign(display.width()-1-EINK_X_OFFSET, y, buf); #endif } -#if !defined(MECK_TWATCH) else if (_page == HomePage::SHUTDOWN) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); @@ -1313,7 +1169,6 @@ public: display.drawTextCentered(display.width() / 2, y2, line2); } } -#endif return _editing_utc ? 700 : 5000; } @@ -1352,7 +1207,6 @@ public: return true; // Consume all other keys while editing } -#if !defined(MECK_TWATCH) // SHUTDOWN page -- intercept up/down and Enter before page cycling if (_page == HomePage::SHUTDOWN) { if (_poweroff_confirm) { @@ -1387,7 +1241,6 @@ public: } // Left/right fall through to page cycling below } -#endif if (c == KEY_LEFT || c == KEY_PREV || c == 'a') { _page = (_page + HomePage::Count - 1) % HomePage::Count; @@ -1449,168 +1302,6 @@ public: // T5S3: Long press boot button to lock/unlock. Touch disabled while locked. // T-Deck Pro: Double-press boot button to lock/unlock. Touch+keyboard disabled. // ========================================================================== -#if defined(MECK_TWATCH) -// Watch lock/clock screen: big HH:MM plus a day+date line, shown when the -// display is woken (by raise-to-wake or a tap) after the idle timeout. -class ClockScreen : public UIScreen { - UITask* _task; - mesh::RTCClock* _rtc; - NodePrefs* _node_prefs; -public: - ClockScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* node_prefs) - : _task(task), _rtc(rtc), _node_prefs(node_prefs) { } - - int render(DisplayDriver& display) override { - uint32_t now = _rtc->getCurrentTime(); - char buf[24]; - display.setColor(DisplayDriver::LIGHT); - - // tiny daily step count, above the clock - { - char sbuf[20]; - snprintf(sbuf, sizeof(sbuf), "%u steps", (unsigned)_task->getTodaySteps()); - display.setColor(DisplayDriver::GREEN); // muted grey on the watch palette - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 6, sbuf); - display.setColor(DisplayDriver::LIGHT); // restore for the clock - } - - // big clock - if (now > 1700000000) { // valid timestamp (after ~Nov 2023) - int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600); - int hrs = (local / 3600) % 24; - if (hrs < 0) hrs += 24; - int mins = (local / 60) % 60; - if (mins < 0) mins += 60; - sprintf(buf, "%02d:%02d", hrs, mins); - } else { - strcpy(buf, "--:--"); - } - ((LGFXDisplay*)&display)->printClockFont(display.width() / 2, display.height() / 2 - 30, buf); - - // day + date line below the clock - if (now > 1700000000) { - time_t local = (time_t)now + (time_t)_node_prefs->utc_offset_hours * 3600; - struct tm tmv; - gmtime_r(&local, &tmv); - static const char* const days[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; - static const char* const mons[] = {"Jan","Feb","Mar","Apr","May","Jun", - "Jul","Aug","Sep","Oct","Nov","Dec"}; - sprintf(buf, "%s %d %s", days[tmv.tm_wday], tmv.tm_mday, mons[tmv.tm_mon]); - display.setTextSize(2); - display.setColor(DisplayDriver::GREEN); // light grey on the watch palette - display.drawTextCentered(display.width() / 2, display.height() / 2 + 12, buf); - display.setColor(DisplayDriver::LIGHT); // restore for the battery line - } - - // battery % and unread count - int pct = (int)_task->getBatteryPercent(); - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - sprintf(buf, "%d%% | %d unread", pct, _task->getUnreadMsgCount()); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, display.height() / 2 + 38, buf); - - return 1000; // refresh about once a second - } - - bool handleInput(char c) override { - _task->gotoHomeScreen(); // any input dismisses the clock screen back to the tiles - return true; - } -}; - -// Steps screen: big daily step count in the clock font. Opened by long-pressing -// the Steps home tile; any tap dismisses back to the tiles. -class StepsScreen : public UIScreen { - UITask* _task; -public: - StepsScreen(UITask* task) : _task(task) { } - - int render(DisplayDriver& display) override { - display.setColor(DisplayDriver::LIGHT); - char buf[16]; - snprintf(buf, sizeof(buf), "%u", (unsigned)_task->getTodaySteps()); - ((LGFXDisplay*)&display)->printClockFont(display.width() / 2, display.height() / 2 - 18, buf); - return 1000; - } - - bool handleInput(char c) override { - if (c == KEY_ENTER) { // touch long-press, or the S3's PWR key - _task->gotoStepsHistoryScreen(); - return true; - } - _task->gotoHomeScreen(); - return true; - } -}; - -// Steps history: today plus the previous six days, as labelled bars. Reached by -// long-pressing the steps screen. Any input returns to the steps screen. -class StepsHistoryScreen : public UIScreen { - UITask* _task; - mesh::RTCClock* _rtc; - NodePrefs* _node_prefs; - - static const char* dowShort(int dow) { - static const char* names[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; - return (dow >= 0 && dow < 7) ? names[dow] : "??"; - } - -public: - StepsHistoryScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* node_prefs) - : _task(task), _rtc(rtc), _node_prefs(node_prefs) {} - - int render(DisplayDriver& display) override { - LGFXDisplay* d = (LGFXDisplay*)&display; - const int W = display.width(); - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(W / 2, 3, "Last 7 days"); - - uint32_t vals[7]; - uint32_t maxv = 1; - for (int i = 0; i < 7; i++) { - vals[i] = _task->getStepHistory(i); - if (vals[i] > maxv) maxv = vals[i]; - } - - // Weekday of today, from the local day-of-epoch (1970-01-01 was a Thursday). - int todayDow = -1; - uint32_t now = _rtc->getCurrentTime(); - if (now > 1700000000) { - int32_t localDay = ((int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600)) / 86400; - todayDow = (int)((localDay + 4) % 7); - } - - char buf[12]; - for (int i = 0; i < 7; i++) { - int y = 16 + i * 14; - int barW = (int)(((uint64_t)(W - 6) * vals[i]) / maxv); - if (barW < 2) barW = 2; - - d->setRawColor(i == 0 ? 0x231D : 0x18C5); // today in the tile blue - d->fillRoundRect(3, y, barW, 12, 2); - - const char* label = "-"; - if (i == 0) label = "Today"; - else if (todayDow >= 0) label = dowShort((todayDow - i + 14) % 7); - - d->setRawColor(0xFFFF); - d->printSmallFont(6, y + 8, label); - snprintf(buf, sizeof(buf), "%lu", (unsigned long)vals[i]); - d->printSmallFont(W - 4 - d->smallTextWidth(buf), y + 8, buf); - } - return 1000; - } - - bool handleInput(char c) override { - _task->gotoStepsScreen(); - return true; - } -}; -#endif #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) class LockScreen : public UIScreen { @@ -1691,7 +1382,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _sensors = sensors; _auto_off = millis() + AUTO_OFF_MILLIS; -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) +#if (defined(PIN_USER_BTN)) user_btn.begin(); #endif #if defined(PIN_USER_BTN_ANA) @@ -1780,25 +1471,6 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no minesweeper_screen = new MinesweeperScreen(this); #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) lock_screen = new LockScreen(this, &rtc_clock, node_prefs); -#endif -#if defined(MECK_TWATCH) - lock_screen = new ClockScreen(this, &rtc_clock, node_prefs); - steps_screen = new StepsScreen(this); - steps_history_screen = new StepsHistoryScreen(this, &rtc_clock, node_prefs); - // LittleFS ("/maps" partition) is mounted in setup() before UITask::begin(). - loadSteps(); -#endif -#if defined(MECK_TWATCH) - // LittleFS ("/maps" partition) is mounted in setup() before UITask::begin(). - watch_notes_screen = new WatchNotesScreen(this, &rtc_clock, node_prefs); - watch_channel_cfg_screen = new WatchChannelConfigScreen(this, node_prefs); -#endif -#ifdef TWATCH_COMPOSE_ENABLED - tw_picker = new TWatchChannelPicker(_display); - tw_channel = new TWatchChannelScreen(_display); - tw_keyboard = new TWatchKeyboardScreen(_display); - ((TWatchChannelPicker*)tw_picker)->setStore((ChannelScreen*)channel_screen); - ((TWatchChannelScreen*)tw_channel)->setStore((ChannelScreen*)channel_screen); #endif audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present #ifdef MECK_AUDIO_VARIANT @@ -1808,9 +1480,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #ifdef HAS_4G_MODEM sms_screen = new SMSScreen(this, node_prefs); #endif -#if defined(MECK_TWATCH) && HAS_GPS - map_screen = new WatchMapScreen(this); -#elif HAS_GPS && !defined(LILYGO_TECHO_CARD) +#if HAS_GPS && !defined(LILYGO_TECHO_CARD) map_screen = new MapScreen(this); #else map_screen = nullptr; @@ -2071,10 +1741,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i // app is connected (they'll see it there), mark as read immediately if ((isOnChannelScreen() && ((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) || -#ifdef TWATCH_COMPOSE_ENABLED - (curr == tw_channel && - ((TWatchChannelScreen*)tw_channel)->getChannelIdx() == channel_idx) || -#endif hasConnection()) { ((ChannelScreen *) channel_screen)->markChannelRead(channel_idx); } @@ -2106,21 +1772,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i } if (_display != NULL && !suppressNotif) { -#if defined(MECK_TWATCH) - // Watch: a new message must not wake the screen. The unread counter has - // already been incremented above; leave the display asleep so the count - // is only seen on the next tilt-wake to the clock screen. Only refresh - // when the display is already on. - if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - if (isRoomMsg) { - unsigned long earliest = millis() + 3000; // At most one refresh per 3s during sync - if (_next_refresh < earliest) _next_refresh = earliest; - } else { - _next_refresh = 100; // trigger refresh - } - } -#else if (!_display->isOn() && !hasConnection()) { _display->turnOn(); } @@ -2134,7 +1785,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } -#endif } // Keyboard flash notification (suppress for room sync and muted channels) @@ -2283,7 +1933,7 @@ void UITask::shutdown(bool restart){ } bool UITask::isButtonPressed() const { -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) +#if (defined(PIN_USER_BTN)) return user_btn.isPressed(); #else return false; @@ -2292,21 +1942,7 @@ bool UITask::isButtonPressed() const { void UITask::loop() { char c = 0; -#if defined(MECK_TWATCH) - // TEMP power-debug probe: every 60s, log display state + board power stats. - { - static unsigned long _pwr_dbg_next = 0; - if (millis() >= _pwr_dbg_next) { - _pwr_dbg_next = millis() + 60000; - Serial.printf("[PWR] uptime=%lus display=%s auto_off_in=%ldms\n", - millis() / 1000, - (_display && _display->isOn()) ? "ON" : "off", - (long)(_auto_off - millis())); - board.printPowerDebug(); - } - } -#endif -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) +#if (defined(PIN_USER_BTN)) int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { #if defined(LilyGo_T5S3_EPaper_Pro) @@ -2359,32 +1995,6 @@ void UITask::loop() { } else { c = checkDisplayOn(KEY_NEXT); } -#elif defined(MECK_PMU_BUTTON) - // T-Watch S3: the only key is the AXP2101 PWRON, so a short press is the - // sole button gesture available. It acts as enter/select/confirm (in the - // contacts screen, that opens the path editor). A >=6s hold is a hardware - // power-off and never reaches here. - c = checkDisplayOn(KEY_ENTER); - if (c && isOnContactsScreen()) { - ContactsScreen* cs = (ContactsScreen*)getContactsScreen(); - if (cs) { - int idx = cs->getSelectedContactIdx(); - if (idx >= 0) gotoPathEditor(idx); - } - c = 0; // handled here; do not also pass Enter through - } -#elif defined(LILYGO_TWATCH_S3_PLUS) - // T-Watch S3 Plus: on the contacts screen the boot button opens the path - // editor for the selected contact; elsewhere it wakes the display / KEY_NEXT. - c = checkDisplayOn(KEY_NEXT); - if (c && isOnContactsScreen()) { - ContactsScreen* cs = (ContactsScreen*)getContactsScreen(); - if (cs) { - int idx = cs->getSelectedContactIdx(); - if (idx >= 0) gotoPathEditor(idx); - } - c = 0; - } #else c = checkDisplayOn(KEY_NEXT); #endif @@ -2506,171 +2116,6 @@ void UITask::loop() { if (curr) curr->poll(); -#ifdef TWATCH_COMPOSE_ENABLED - // Channel picker -> channel screen (long-press select) / exit - if (curr == tw_picker) { - TWatchChannelPicker* picker = (TWatchChannelPicker*)tw_picker; - if (picker->isConfirmed()) { - ((TWatchChannelScreen*)tw_channel)->setSelfName(the_mesh.getNodePrefs()->node_name); - ((TWatchChannelScreen*)tw_channel)->activate(picker->getSelectedChannelIdx(), - picker->getSelectedChannelName()); - setCurrScreen(tw_channel); - picker->acknowledgeConfirm(); - } - if (picker->wantsExit()) { - picker->acknowledgeExit(); - gotoHomeScreen(); - } - } - // Channel screen -> keyboard (compose bar tap) / exit - else if (curr == tw_channel) { - TWatchChannelScreen* cs = (TWatchChannelScreen*)tw_channel; - if (cs->wantsCompose()) { - cs->acknowledgeCompose(); - ((TWatchKeyboardScreen*)tw_keyboard)->activate(cs->getChannelIdx(), cs->getChannelName()); - setCurrScreen(tw_keyboard); - } - if (cs->wantsExit()) { - cs->acknowledgeExit(); - gotoHomeScreen(); - } - } - // Keyboard -> send on channel (returns to channel screen) / exit - else if (curr == tw_keyboard) { - TWatchKeyboardScreen* kb = (TWatchKeyboardScreen*)tw_keyboard; - if (kb->wantsExit()) { - kb->acknowledgeExit(); - gotoHomeScreen(); - } - const char* sendText = nullptr; - if (kb->consumeSendRequest(&sendText) && sendText) { - TWatchKeyboardScreen::Purpose purpose = kb->getPurpose(); - int ctxIdx = kb->getContextIdx(); - if (purpose == TWatchKeyboardScreen::TWKB_CHANNEL) { - ChannelDetails ch; - if (the_mesh.getChannel(kb->getChannelIdx(), ch)) { - uint32_t ts = rtc_clock.getCurrentTime(); - the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(), - sendText, strlen(sendText)); - showAlert("Sent!", 800); - addSentChannelMessage(kb->getChannelIdx(), the_mesh.getNodePrefs()->node_name, sendText); - } - kb->clearOutBuf(); - setCurrScreen(tw_channel); - } else if (purpose == TWatchKeyboardScreen::TWKB_DM) { - bool dmSuccess = false; - uint32_t sendRef = 0; - uint8_t sendTotal = 0; - if (strlen(sendText) > 0 && - the_mesh.uiSendDirectMessage((uint32_t)ctxIdx, sendText, &sendRef, &sendTotal)) { - ContactInfo dmRecipient; - if (the_mesh.getContactByIdx(ctxIdx, dmRecipient)) { - addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, sendText, - sendRef, sendTotal); - } - dmSuccess = true; - } - kb->clearOutBuf(); - ContactInfo dmContact; - if (the_mesh.getContactByIdx(ctxIdx, dmContact)) { - gotoDMConversation(dmContact.name, ctxIdx, 0); - } else { - gotoHomeScreen(); - } - showAlert(dmSuccess ? "DM sent!" : "DM failed!", 1500); - } else if (purpose == TWatchKeyboardScreen::TWKB_PATH) { - PathEditorScreen* pe = (PathEditorScreen*)getPathEditorScreen(); - const char* perr = pe ? pe->applyComposedPath(sendText) : "No editor"; - kb->clearOutBuf(); - if (path_editor) setCurrScreen(path_editor); - else gotoHomeScreen(); - if (perr) showAlert(perr, 1500); - } else if (purpose == TWatchKeyboardScreen::TWKB_NOTE) { - WatchNotesScreen* wn = (WatchNotesScreen*)watch_notes_screen; - const char* nerr = wn ? wn->applyComposedNote(sendText) : "No notes"; - kb->clearOutBuf(); - if (watch_notes_screen) setCurrScreen(watch_notes_screen); - else gotoHomeScreen(); - if (nerr) showAlert(nerr, 1200); - } else if (purpose == TWatchKeyboardScreen::TWKB_SCOPE) { - WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)watch_channel_cfg_screen; - const char* serr = wc ? wc->applyComposedScope(sendText) : "No channel cfg"; - kb->clearOutBuf(); - if (watch_channel_cfg_screen) setCurrScreen(watch_channel_cfg_screen); - else gotoHomeScreen(); - if (serr) showAlert(serr, 1200); - } else if (purpose == TWatchKeyboardScreen::TWKB_TRACE_PATH) { - TraceScreen* ts = (TraceScreen*)getTraceScreen(); - if (ts) ts->setTypedPath(sendText); - kb->clearOutBuf(); - if (trace_screen) setCurrScreen(trace_screen); - else gotoHomeScreen(); - } else { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI - RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen(); - if (admin) { - for (int i = 0; sendText[i]; i++) admin->handleInput(sendText[i]); - admin->handleInput('\r'); - } - kb->clearOutBuf(); - if (repeater_admin) setCurrScreen(repeater_admin); - else gotoHomeScreen(); - } - } - } - else if (curr == path_editor && path_editor != nullptr) { - PathEditorScreen* pe = (PathEditorScreen*)path_editor; - if (pe->wantsKeyboard()) { - pe->clearWantKeyboard(); - openTWatchKeyboard(TWatchKeyboardScreen::TWKB_PATH, pe->getContactIdx()); - } - } - else if (curr == watch_notes_screen && watch_notes_screen != nullptr) { - WatchNotesScreen* wn = (WatchNotesScreen*)watch_notes_screen; - if (wn->wantsKeyboard()) { - wn->clearWantKeyboard(); - openTWatchKeyboard(TWatchKeyboardScreen::TWKB_NOTE, 0); - if (wn->editPending()) { - ((TWatchKeyboardScreen*)tw_keyboard)->setInitialText(wn->getEditText()); - } - } - } - else if (curr == watch_channel_cfg_screen && watch_channel_cfg_screen != nullptr) { - WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)watch_channel_cfg_screen; - if (wc->wantsKeyboard()) { - wc->clearWantKeyboard(); - openTWatchKeyboard(TWatchKeyboardScreen::TWKB_SCOPE, 0); - ((TWatchKeyboardScreen*)tw_keyboard)->setInitialText(wc->getEditText()); - } else if (wc->wantsExit()) { - wc->clearWantExit(); - gotoSettingsScreen(); - } - } - else if (curr == trace_screen && trace_screen != nullptr) { - TraceScreen* ts = (TraceScreen*)trace_screen; - if (ts->wantsKeyboard()) { - ts->clearWantKeyboard(); - openTWatchKeyboard(TWatchKeyboardScreen::TWKB_TRACE_PATH, 0); - ((TWatchKeyboardScreen*)tw_keyboard)->setInitialText(ts->getCurrentPathAsText()); - } - } - else if (curr == settings_screen && settings_screen != nullptr) { - SettingsScreen* ss = (SettingsScreen*)settings_screen; - if (ss->wantsWatchChannels()) { - ss->clearWantsWatchChannels(); - if (watch_channel_cfg_screen) { - ((WatchChannelConfigScreen*)watch_channel_cfg_screen)->enter(); - setCurrScreen(watch_channel_cfg_screen); - } - } - // Touch activates rows via injectKey->handleInput, which bypasses the - // main.cpp key handler that consumes this flag. Consume it here too so a - // watch long-press opens the Rx Log (mirrors wantsWatchChannels above). - if (ss->isRxLogRequested()) { - ss->clearRxLogRequest(); - gotoRxLogScreen(); - } - } -#endif if (_display != NULL && _display->isOn()) { if (millis() >= _next_refresh && curr) { @@ -2870,18 +2315,6 @@ if (curr) curr->poll(); } #endif } -#if defined(MECK_TWATCH) - // Raise-to-wake: a wrist-raise (BMA423 tilt) while the display is off turns - // it back on showing the clock screen and restarts the idle timer. - if (_display != NULL && !_display->isOn() && board.tiltFired()) { - _display->turnOn(); - setCurrScreen(lock_screen); - _auto_off = millis() + AUTO_OFF_MILLIS; - } - - // Accumulate steps, roll the day over at local midnight, persist periodically. - updateSteps(); -#endif // Auto-lock idle timer — runs regardless of display on/off state #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) @@ -3403,9 +2836,6 @@ void UITask::toggleGPS() { #if defined(LilyGo_TDeck_Pro_Max) board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN #endif - #if defined(MECK_TWATCH) - board.gpsPowerOff(); // Watch: GPS power is the AXP2101 BLDO1 rail - #endif notify(UIEventType::ack); } else { // Enable GPS — power on hardware @@ -3417,9 +2847,6 @@ void UITask::toggleGPS() { #if defined(LilyGo_TDeck_Pro_Max) board.gpsPowerOn(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN #endif - #if defined(MECK_TWATCH) - board.gpsPowerOn(); // Watch: GPS power is the AXP2101 BLDO1 rail - #endif notify(UIEventType::ack); } the_mesh.savePrefs(); @@ -3545,30 +2972,6 @@ void UITask::gotoChannelPickerScreen() { _next_refresh = 100; } -#ifdef TWATCH_COMPOSE_ENABLED -void UITask::openTWatchPicker() { - TWatchChannelPicker* picker = (TWatchChannelPicker*)tw_picker; - picker->beginChannelSelect(); - ChannelDetails ch; - for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { - if (the_mesh.getChannel(i, ch) && ch.name[0] != 0) { - picker->addChannel(i, ch.name); - } - } - setCurrScreen(tw_picker); -} - -void UITask::openTWatchKeyboard(int purpose, int contextIdx) { - ((TWatchKeyboardScreen*)tw_keyboard)->activateFor( - (TWatchKeyboardScreen::Purpose)purpose, contextIdx); - setCurrScreen(tw_keyboard); - if (_display != NULL && !_display->isOn()) { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; - _next_refresh = 100; -} -#endif void UITask::gotoDMTab() { ((ChannelScreen *) channel_screen)->setViewChannelIdx(0xFF); // switches + marks read @@ -3621,17 +3024,6 @@ void UITask::gotoTextReader() { } void UITask::gotoNotesScreen() { -#if defined(MECK_TWATCH) - // Watch: the shared NotesScreen is SD-bound; use the LittleFS note pad. - if (watch_notes_screen) { - ((WatchNotesScreen*)watch_notes_screen)->enter(); - setCurrScreen(watch_notes_screen); - if (_display != NULL && !_display->isOn()) _display->turnOn(); - _auto_off = millis() + AUTO_OFF_MILLIS; - _next_refresh = 100; - return; - } -#endif if (!notes_screen) return; // Not available on this platform #if !defined(LILYGO_TECHO_LITE) && !defined(LILYGO_TECHO_CARD) NotesScreen* notes = (NotesScreen*)notes_screen; @@ -3903,135 +3295,6 @@ void UITask::gotoTraceScreen() { _next_refresh = 100; } -#if defined(MECK_TWATCH) - -// Persisted step state. Lives on the LittleFS "maps" partition; the watches have -// no SD card. There is no clean-shutdown hook to save from -- a >=6s hold on the -// PWR key is a hardware power cut by the AXP2101 -- so saving is periodic. -#define STEPS_FILE "/steps.dat" -#define STEPS_MAGIC 0x31505453UL // "STP1" -#define STEPS_VERSION 1 -#define STEPS_SAVE_INTERVAL_MS 300000UL // flush at most every 5 minutes - -struct StepStoreV1 { - uint32_t magic; - uint8_t version; - uint8_t _pad[3]; - int32_t lastStepDay; - uint32_t todaySteps; - uint32_t history[6]; -}; - -void UITask::loadSteps() { - File f = LittleFS.open(STEPS_FILE, "r"); - if (!f) return; - StepStoreV1 st; - size_t n = f.read((uint8_t*)&st, sizeof(st)); - f.close(); - if (n != sizeof(st) || st.magic != STEPS_MAGIC || st.version != STEPS_VERSION) return; - - _lastStepDay = st.lastStepDay; - _todaySteps = st.todaySteps; - memcpy(_stepHistory, st.history, sizeof(_stepHistory)); -} - -void UITask::saveSteps() { - StepStoreV1 st; - memset(&st, 0, sizeof(st)); - st.magic = STEPS_MAGIC; - st.version = STEPS_VERSION; - st.lastStepDay = _lastStepDay; - st.todaySteps = _todaySteps; - memcpy(st.history, _stepHistory, sizeof(_stepHistory)); - - File f = LittleFS.open(STEPS_FILE, "w"); - if (!f) { - Serial.println("steps: save FAILED (LittleFS)"); - return; - } - f.write((const uint8_t*)&st, sizeof(st)); - f.close(); - _stepsDirtySince = 0; -} - -void UITask::shiftStepHistory(uint32_t completedDay) { - for (int i = 5; i > 0; i--) _stepHistory[i] = _stepHistory[i - 1]; - _stepHistory[0] = completedDay; -} - -void UITask::rollStepDays(int32_t newDay) { - int32_t gap = newDay - _lastStepDay; - if (gap <= 0) { // clock corrected backwards: re-anchor, keep history - _lastStepDay = newDay; - return; - } - shiftStepHistory(_todaySteps); // the day that just ended - for (int32_t d = 1; d < gap && d < 6; d++) { - shiftStepHistory(0); // days the watch was switched off - } - _todaySteps = 0; - _lastStepDay = newDay; - saveSteps(); -} - -void UITask::updateSteps() { - uint32_t raw = board.getStepCount(); - - // Seed from the chip, not from the file: SensorBMA423::begin() re-flashes the - // feature engine (bma423_write_config_file) on every boot, zeroing the step - // register. Seeding here is correct whether or not the count survived. - if (!_stepsSeeded) { _lastRaw = raw; _stepsSeeded = true; } - - uint32_t delta = (raw >= _lastRaw) ? (raw - _lastRaw) : raw; // raw < last => counter reset - _lastRaw = raw; - - uint32_t now = rtc_clock.getCurrentTime(); - if (now <= 1700000000) return; // clock not set: nothing to attribute steps to - - int32_t localDay = ((int32_t)now + ((int32_t)the_mesh.getNodePrefs()->utc_offset_hours * 3600)) / 86400; - if (_lastStepDay == 0) { - _lastStepDay = localDay; // first valid read on a fresh install - } else if (localDay != _lastStepDay) { - rollStepDays(localDay); - } - - if (delta) { - _todaySteps += delta; - if (_stepsDirtySince == 0) _stepsDirtySince = millis(); - } - if (_stepsDirtySince && millis() - _stepsDirtySince >= STEPS_SAVE_INTERVAL_MS) { - saveSteps(); - } -} - -uint32_t UITask::getTodaySteps() { - return _todaySteps; -} - -uint32_t UITask::getStepHistory(int daysAgo) { - if (daysAgo <= 0) return _todaySteps; - if (daysAgo > 6) return 0; - return _stepHistory[daysAgo - 1]; -} - -void UITask::gotoStepsScreen() { - setCurrScreen(steps_screen); - if (_display != NULL && !_display->isOn()) { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; - _next_refresh = 100; -} - -void UITask::gotoStepsHistoryScreen() { - setCurrScreen(steps_history_screen); - if (_display != NULL && !_display->isOn()) { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; - _next_refresh = 100; -} -#endif void UITask::gotoGamesMenu() { @@ -4104,18 +3367,7 @@ void UITask::gotoWebReader() { #if HAS_GPS void UITask::gotoMapScreen() { if (!map_screen) return; // Not available on this platform (T-Echo Card) -#if defined(MECK_TWATCH) - WatchMapScreen* map = (WatchMapScreen*)map_screen; - if (_display != NULL) { - map->enter(*_display); - } - setCurrScreen(map_screen); - if (_display != NULL && !_display->isOn()) { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; - _next_refresh = 100; -#elif !defined(LILYGO_TECHO_CARD) +#if !defined(LILYGO_TECHO_CARD) MapScreen* map = (MapScreen*)map_screen; if (_display != NULL) { map->enter(*_display); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 2315adaa..5c2fb640 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -34,7 +34,7 @@ #include "AlarmScreen.h" #endif -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) #include "VirtualKeyboard.h" #endif @@ -109,40 +109,11 @@ class UITask : public AbstractUITask { UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required) #endif UIScreen* map_screen; // Map tile screen (GPS + SD card tiles) -#ifdef TWATCH_COMPOSE_ENABLED - UIScreen* tw_picker; - UIScreen* tw_channel; - UIScreen* tw_keyboard; -#endif UIScreen* curr; bool _homeShowingTiles = false; // Set by HomeScreen render when tile grid is visible int _tileGridVY = 44; // Virtual Y of tile grid top (updated each render) -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) UIScreen* lock_screen; // Lock screen (big clock + battery + unread) -#if defined(MECK_TWATCH) - UIScreen* steps_screen; // Steps screen (big daily step count) - UIScreen* steps_history_screen; // Steps subscreen: today + the previous 6 days - // Step state. The BMA423 feature engine is re-flashed by bma423_write_config_file() - // inside SensorBMA423::begin() on every boot, which zeroes the step register. So - // the raw count cannot be used as an absolute; deltas are accumulated instead and - // the totals are persisted. _lastRaw is seeded from the chip on the first read - // rather than from the file, which is correct whether or not the count survived. - int32_t _lastStepDay = 0; // local day-of-epoch that _todaySteps belongs to - uint32_t _todaySteps = 0; // accumulated steps for _lastStepDay - uint32_t _stepHistory[6] = {0}; // [0] = yesterday ... [5] = six days ago - uint32_t _lastRaw = 0; // last raw BMA423 count seen (runtime only) - bool _stepsSeeded = false; // _lastRaw has been seeded from the chip - unsigned long _stepsDirtySince = 0; // millis of oldest unsaved change; 0 = clean - void loadSteps(); - void saveSteps(); - void updateSteps(); // called every loop() - void rollStepDays(int32_t newDay); - void shiftStepHistory(uint32_t completedDay); -#endif -#if defined(MECK_TWATCH) - UIScreen* watch_notes_screen; // LittleFS note pad (shared NotesScreen needs SD) - UIScreen* watch_channel_cfg_screen; // Per-channel region/notif/delete (touch UI) -#endif UIScreen* _screenBeforeLock = nullptr; bool _locked = false; unsigned long _lastInputMillis = 0; // Auto-lock idle tracking @@ -218,10 +189,6 @@ public: void gotoHomeScreen(); void gotoChannelScreen(bool resetDmView = true); // Navigate to channel message screen void gotoChannelPickerScreen(); // Navigate to channel picker (bubble/list) -#ifdef TWATCH_COMPOSE_ENABLED - void openTWatchPicker(); - void openTWatchKeyboard(int purpose, int contextIdx); -#endif void gotoDMTab(); // Navigate directly to DM tab on channel screen void gotoDMConversation(const char* contactName, int contactIdx = -1, uint8_t perms = 0); void gotoContactsScreen(); // Navigate to contacts list @@ -244,12 +211,6 @@ public: void gotoGamesMenu(); // Navigate to games launcher menu void gotoSnakeScreen(); // Navigate to snake game void gotoMinesweeperScreen(); // Navigate to minesweeper game -#if defined(MECK_TWATCH) - void gotoStepsScreen(); // Navigate to the step counter screen - void gotoStepsHistoryScreen(); // Navigate to the 7-day step history - uint32_t getTodaySteps(); // Steps accumulated today - uint32_t getStepHistory(int daysAgo); // 0 = today, 1..6 = previous days -#endif #if HAS_GPS void gotoMapScreen(); // Navigate to map tile screen #endif @@ -311,25 +272,14 @@ public: bool isOnTraceScreen() const { return curr == trace_screen; } bool isOnGamesMenu() const { return curr == games_menu_screen; } bool isOnSnakeScreen() const { return curr == snake_screen; } -#if defined(MECK_TWATCH) - bool isOnStepsScreen() const { return curr == steps_screen; } - bool isOnStepsHistoryScreen() const { return curr == steps_history_screen; } -#endif -#if defined(MECK_TWATCH) - bool isOnWatchNotesScreen() const { return curr == watch_notes_screen; } - bool isOnWatchChannelConfigScreen() const { return curr == watch_channel_cfg_screen; } -#endif -#ifdef TWATCH_COMPOSE_ENABLED - bool isOnTWatchChannelScreen() const { return curr == tw_channel; } -#endif bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; } bool isOnMapScreen() const { return curr == map_screen; } -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) bool isLocked() const { return _locked; } void lockScreen(); void unlockScreen(); #endif -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) bool isVKBActive() const { return _vkbActive; } unsigned long vkbOpenedAt() const { return _vkbOpenedAt; } VirtualKeyboard& getVKB() { return _vkb; } @@ -414,14 +364,6 @@ public: UIScreen* getTraceScreen() const { return trace_screen; } UIScreen* getGamesMenuScreen() const { return games_menu_screen; } UIScreen* getSnakeScreen() const { return snake_screen; } -#if defined(MECK_TWATCH) - UIScreen* getStepsScreen() const { return steps_screen; } - UIScreen* getStepsHistoryScreen() const { return steps_history_screen; } -#endif -#if defined(MECK_TWATCH) - UIScreen* getWatchNotesScreen() const { return watch_notes_screen; } - UIScreen* getWatchChannelConfigScreen() const { return watch_channel_cfg_screen; } -#endif UIScreen* getMinesweeperScreen() const { return minesweeper_screen; } UIScreen* getMapScreen() const { return map_screen; } #ifdef MECK_WEB_READER diff --git a/examples/companion_radio/ui-new/VirtualKeyboard.h b/examples/companion_radio/ui-new/VirtualKeyboard.h index 2080bc92..39d7ed55 100644 --- a/examples/companion_radio/ui-new/VirtualKeyboard.h +++ b/examples/companion_radio/ui-new/VirtualKeyboard.h @@ -12,7 +12,7 @@ // if (keyboard.status() == VKB_SUBMITTED) { ... keyboard.getText() ... } // ============================================================================= -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_TWATCH) +#if defined(LilyGo_T5S3_EPaper_Pro) #ifndef VIRTUAL_KEYBOARD_H #define VIRTUAL_KEYBOARD_H @@ -536,4 +536,4 @@ private: }; #endif // VIRTUAL_KEYBOARD_H -#endif // LilyGo_T5S3_EPaper_Pro || MECK_TWATCH \ No newline at end of file +#endif // LilyGo_T5S3_EPaper_Pro \ No newline at end of file diff --git a/examples/companion_radio/ui-new/WatchChannelConfigScreen.h b/examples/companion_radio/ui-new/WatchChannelConfigScreen.h deleted file mode 100644 index 9fb05280..00000000 --- a/examples/companion_radio/ui-new/WatchChannelConfigScreen.h +++ /dev/null @@ -1,382 +0,0 @@ -#pragma once - -// ============================================================================= -// WatchChannelConfigScreen -- per-channel settings for the T-Watch S3 / S3 Plus. -// -// The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel action -// from physical-keyboard hotkeys on the selected row (Ent: edit region scope, -// N: cycle notifications, T: tone, X/Hold: delete), none of which exist on the -// touch-only watch. This standalone screen replaces it on MECK_TWATCH builds, -// following the WatchAlarmScreen / WatchNotesScreen conventions. -// -// LIST mode shows each channel with its region scope tag and notification -// state; tap a row to select it, tap again (or PWR/Enter) to open it. DETAIL -// mode offers: Region (opens the watch keyboard via TWKB_SCOPE, pre-filled -// with the current scope), Clear region (reverts to the device default), -// Notify (tap-cycles All -> Mentions -> Off, mirroring the desktop order), -// and Delete via the button (channel 1+ only): a short press of the PWR key -// (S3) or the boot button (S3 Plus) arms it, a second press within 3s -// confirms. The tone picker is deliberately absent -- no audio path. -// -// Exit: the footer Back returns to the Settings screen (via a flag polled by -// UITask::loop); the universal top-strip tap still goes home. -// -// Geometry: 240x240 physical, UI_ZOOM=2, so display.width()/height() = 120. -// ============================================================================= - -// NOTE on include order: mirrors WatchNotesScreen.h -- this header uses -// NodePrefs (NOTIF_* constants, channel_notif[]) and the_mesh, which the two -// call sites (UITask.cpp, main.cpp) already have in scope before including it. -#include -#include -#include -#include -#include -#include -#include - -class UITask; - -#ifndef MAX_GROUP_CHANNELS - #define MAX_GROUP_CHANNELS 20 -#endif - -#define WCC_ROWS 4 // list rows per page - -class WatchChannelConfigScreen : public UIScreen { -public: - enum Mode { LIST, DETAIL }; - -private: - UITask* _task; - NodePrefs* _node_prefs; - - uint8_t _chIdx[MAX_GROUP_CHANNELS]; // populated channel slots, in order - int _numCh = 0; - - Mode _mode = LIST; - int _sel = 0; // selected list row (index into _chIdx) - int _scrollTop = 0; - int _detail = -1; // channel slot open in DETAIL (value from _chIdx) - bool _confirmDelete = false; - unsigned long _confirmAt = 0; - - // Keyboard / exit handshake -- UITask::loop() polls these (UITask is only - // forward declared here, so this screen cannot call it directly). - bool _wantKeyboard = false; - bool _wantExit = false; - char _editScope[31]; // pre-fill text handed to the keyboard - - // Layout (logical pixels) - static const int HEADER_H = 14; - static const int ROW_H = 20; - static const int FOOTER_Y = 100; - - void rebuildList() { - // Scan ALL slots -- the companion app may write non-contiguously, and - // gaps can appear after deletion (same scan as SettingsScreen). - _numCh = 0; - for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { - ChannelDetails ch; - if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { - _chIdx[_numCh++] = i; - } - } - if (_sel >= _numCh) _sel = _numCh > 0 ? _numCh - 1 : 0; - if (_scrollTop > _sel) _scrollTop = _sel; - } - - const char* notifLabel(uint8_t idx) const { - uint8_t n = _node_prefs->channel_notif[idx]; - return (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "@" : "All"; - } - - // Replicates SettingsScreen::deleteChannel (kept in sync manually). Note: - // channel_notif[] is not shifted during compaction -- this matches the - // desktop behaviour exactly. - void deleteChannel(uint8_t idx) { - ChannelDetails empty; - memset(&empty, 0, sizeof(empty)); - int total = 0; - for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { - ChannelDetails ch; - if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { - total = i + 1; - } - } - for (int i = idx; i < total - 1; i++) { - ChannelDetails next; - if (the_mesh.getChannel(i + 1, next)) { - the_mesh.setChannel(i, next); - } - } - the_mesh.setChannel(total - 1, empty); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Deleted channel at idx %d, compacted %d channels\n", idx, total); - } - - void ensureVisible() { - if (_sel < _scrollTop) _scrollTop = _sel; - if (_sel >= _scrollTop + WCC_ROWS) _scrollTop = _sel - WCC_ROWS + 1; - } - -public: - WatchChannelConfigScreen(UITask* task, NodePrefs* node_prefs) - : _task(task), _node_prefs(node_prefs) { - _editScope[0] = 0; - } - - void enter() { - rebuildList(); - _mode = LIST; - _detail = -1; - _confirmDelete = false; - _wantKeyboard = false; - _wantExit = false; - } - - // --- handshake (polled from UITask::loop) --- - bool wantsKeyboard() const { return _wantKeyboard; } - void clearWantKeyboard() { _wantKeyboard = false; } - bool wantsExit() const { return _wantExit; } - void clearWantExit() { _wantExit = false; } - const char* getEditText() const { return _editScope; } - - // Save the keyboard result as the open channel's region scope. An empty - // string clears it back to the device default (same as Clear). Returns - // NULL on success, else an error message. - const char* applyComposedScope(const char* txt) { - if (_detail < 0) return "No channel open"; - ChannelDetails ch; - if (!the_mesh.getChannel((uint8_t)_detail, ch)) return "Channel gone"; - if (txt == nullptr) txt = ""; - strncpy(ch.scope_name, txt, sizeof(ch.scope_name)); - ch.scope_name[30] = '\0'; - the_mesh.setChannel((uint8_t)_detail, ch); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Channel %d scope set to '%s'\n", - _detail, ch.scope_name[0] ? ch.scope_name : "(device default)"); - return nullptr; - } - - // --------------------------------------------------------------------------- - // Rendering. 120x120 logical. - // --------------------------------------------------------------------------- -private: - void renderList(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 4, "Channels"); - - if (_numCh == 0) { - d->setRawColor(0x7BEF); - display.drawTextCentered(display.width() / 2, 48, "No channels"); - } - - char buf[40]; - int visible = _numCh - _scrollTop; - if (visible > WCC_ROWS) visible = WCC_ROWS; - for (int i = 0; i < visible; i++) { - uint8_t idx = _chIdx[_scrollTop + i]; - int y = HEADER_H + i * ROW_H; - ChannelDetails ch; - if (!the_mesh.getChannel(idx, ch)) continue; - - if (_scrollTop + i == _sel) { - d->setRawColor(0x231D); // same blue as the home tile - d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3); - } - d->setRawColor(0xFFFF); - char nameBuf[15]; - strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1); - nameBuf[sizeof(nameBuf) - 1] = 0; - display.setCursor(5, y + 2); - display.print(nameBuf); - - snprintf(buf, sizeof(buf), "[%s] %s", - ch.scope_name[0] ? ch.scope_name : "*", notifLabel(idx)); - d->setRawColor(0x7BEF); - d->printSmallFont(5, y + 13, buf); - } - - // Footer: Back (left) and, when the list overflows a page, More (right). - d->setRawColor(0x231D); - d->drawRoundRect(1, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xFFFF); - d->printSmallFont(20, FOOTER_Y + 8, "Back"); - if (_numCh > WCC_ROWS) { - d->setRawColor(0x231D); - d->drawRoundRect(61, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xC618); - d->printSmallFont(78, FOOTER_Y + 8, "More"); - } - } - - void renderDetail(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - char buf[44]; - - ChannelDetails ch; - if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) { - _mode = LIST; - return; - } - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - char nameBuf[18]; - strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1); - nameBuf[sizeof(nameBuf) - 1] = 0; - display.drawTextCentered(display.width() / 2, 3, nameBuf); - - // Region row -- opens the keyboard. - d->setRawColor(0x231D); - d->drawRoundRect(1, 16, display.width() - 2, 18, 3); - snprintf(buf, sizeof(buf), "Region: %s", - ch.scope_name[0] ? ch.scope_name : "(default)"); - d->setRawColor(0xFFFF); - d->printSmallFont(6, 24, buf); - - // Clear-region row -- dim when there is nothing to clear. - d->setRawColor(ch.scope_name[0] ? 0x231D : 0x18C3); - d->drawRoundRect(1, 36, display.width() - 2, 18, 3); - d->setRawColor(ch.scope_name[0] ? 0xC618 : 0x4208); - d->printSmallFont(6, 44, "Clear region (use default)"); - - // Notify row -- tap cycles All -> Mentions -> Off. - d->setRawColor(0x231D); - d->drawRoundRect(1, 56, display.width() - 2, 18, 3); - uint8_t n = _node_prefs->channel_notif[(uint8_t)_detail]; - snprintf(buf, sizeof(buf), "Notify: %s", - (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "Mentions" : "All"); - d->setRawColor(0xFFFF); - d->printSmallFont(6, 64, buf); - - // Delete row -- status only; deletion is driven by the button (see - // handleInput). Channel 0 (public) cannot be deleted, same as desktop. - bool canDel = (_detail > 0); - d->setRawColor(canDel ? 0x231D : 0x18C3); - d->drawRoundRect(1, 76, display.width() - 2, 18, 3); - d->setRawColor(canDel ? (_confirmDelete ? 0xF800 : 0xC618) : 0x4208); - d->printSmallFont(6, 84, !canDel ? "Delete (public ch)" - : _confirmDelete ? "Delete? press again" : "Button: delete"); - - // Footer: Back to the list. - d->setRawColor(0x231D); - d->drawRoundRect(1, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xFFFF); - d->printSmallFont(20, FOOTER_Y + 8, "Back"); - } - -public: - int render(DisplayDriver& display) override { - if (_confirmDelete && millis() - _confirmAt > 3000) _confirmDelete = false; - switch (_mode) { - case LIST: renderList(display); return 500; - case DETAIL: renderDetail(display); return 500; - } - return 500; - } - - // Physical tap in logical coordinates. The top-strip home tap is handled by - // main.cpp before this is called. Returns true if the tap was consumed. - bool handleTap(int lx, int ly) { - if (_mode == LIST) { - if (ly >= FOOTER_Y) { - if (lx < 60) { // Back -> Settings - _wantExit = true; - } else if (_numCh > WCC_ROWS) { // More: next page (wraps) - _scrollTop += WCC_ROWS; - if (_scrollTop >= _numCh) _scrollTop = 0; - _sel = _scrollTop; - } - return true; - } - if (ly >= HEADER_H) { - int i = (ly - HEADER_H) / ROW_H; - int idx = _scrollTop + i; - if (i < WCC_ROWS && idx < _numCh) { - if (idx == _sel) { // second tap on the row: open it - _detail = _chIdx[idx]; - _mode = DETAIL; - _confirmDelete = false; - } else { - _sel = idx; // first tap: select - ensureVisible(); - } - return true; - } - } - return false; - } - - // DETAIL - if (ly >= FOOTER_Y) { // Back -> list - _confirmDelete = false; - _mode = LIST; - rebuildList(); - return true; - } - ChannelDetails ch; - if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) { - _mode = LIST; - return true; - } - if (ly >= 16 && ly < 34) { // Region -> keyboard, pre-filled - strncpy(_editScope, ch.scope_name, sizeof(_editScope)); - _editScope[30] = '\0'; - _wantKeyboard = true; - return true; - } - if (ly >= 36 && ly < 54) { // Clear region - if (ch.scope_name[0]) { - ch.scope_name[0] = '\0'; - the_mesh.setChannel((uint8_t)_detail, ch); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Channel %d scope cleared\n", _detail); - } - return true; - } - if (ly >= 56 && ly < 74) { // Notify: cycle All -> @ -> Off - uint8_t cur = _node_prefs->channel_notif[(uint8_t)_detail]; - _node_prefs->channel_notif[(uint8_t)_detail] = (cur + 1) % 3; - the_mesh.savePrefs(); - return true; - } - return true; // swallow other taps (incl. delete row) - } - - bool handleInput(char c) override { - // The PMU short press (S3) arrives as KEY_ENTER; the S3 Plus boot button - // arrives as KEY_NEXT. - if (_mode == LIST && c == KEY_ENTER) { - if (_sel < _numCh) { - _detail = _chIdx[_sel]; - _mode = DETAIL; - _confirmDelete = false; - } - return true; - } - if (_mode == DETAIL && (c == KEY_ENTER || c == KEY_NEXT)) { - // Button click deletes the open channel (channel 0 is protected): - // first press arms, a second press within 3s confirms. Back is the - // touch footer. - if (_detail > 0) { - if (_confirmDelete) { - deleteChannel((uint8_t)_detail); - _confirmDelete = false; - _detail = -1; - rebuildList(); - _mode = LIST; - } else { - _confirmDelete = true; - _confirmAt = millis(); - } - } - return true; - } - return false; - } -}; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/WatchMapScreen.h b/examples/companion_radio/ui-new/WatchMapScreen.h deleted file mode 100644 index 35733568..00000000 --- a/examples/companion_radio/ui-new/WatchMapScreen.h +++ /dev/null @@ -1,703 +0,0 @@ -#pragma once - -// WatchMapScreen -- T-Watch S3 Plus map screen. -// -// Renders standard OSM "slippy map" B&W PNG tiles from the LittleFS "maps" -// partition onto the LovyanGFX sprite buffer. Tiles are stored at -// /tiles/{zoom}/{x}/{y}.png -- the same layout as the e-ink MapScreen -- but -// read via LittleFS instead of SD. The maps partition is mounted at basePath -// "/maps" in main.cpp, and the Arduino VFS prepends that mountpoint -// automatically, so tile paths passed to LittleFS stay relative ("/tiles/.."). -// -// This is a separate screen from the e-ink MapScreen: the watch draws into a -// 120x120 logical sprite (UI_ZOOM=2 scales it 2x to the 240x240 panel) via the -// generic DisplayDriver / LGFXDisplay interface, whereas MapScreen is welded to -// GxEPDDisplay's drawPixelRaw path. Coordinate math, tile stitching and the -// PNGdec decode are carried over unchanged; only the pixel writes, colours, -// dimensions, filesystem and input (touch instead of WASD) differ. - -#include -#include -#include -#undef local // PNGdec's zutil.h defines 'local' as 'static' -- breaks any variable named 'local' -#include -#include -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- -#define WMAP_TILE_SIZE 256 // Standard OSM tile size in pixels -#define WMAP_DEFAULT_ZOOM 13 -#define WMAP_MIN_ZOOM 1 -#define WMAP_MAX_ZOOM 17 - -// PNG file read buffer (PSRAM). Tiles stream row-by-row via PNGdec. -#define WMAP_PNG_BUF_SIZE (65536) - -// Tile path relative to the LittleFS "maps" mountpoint ("/maps" is prepended -// by the VFS, so the on-disk file is /maps/tiles/{z}/{x}/{y}.png). -#define WMAP_TILE_ROOT "/tiles" - -// Contact type (for label display -- matches AdvertDataHelpers.h) -#ifndef ADV_TYPE_REPEATER - #define ADV_TYPE_REPEATER 2 -#endif - -// Pan step: fraction of viewport to move per press (unused for touch, kept for -// completeness / any future key routing) -#define WMAP_PAN_FRACTION 4 - -// Max contact markers (PSRAM-allocated) -#define WMAP_MAX_MARKERS 500 - -// Footer bar (logical pixels) -#define WMAP_FOOTER_H 16 - -// B&W tile colours (RGB565; the sprite is 8-bit but LovyanGFX converts) -#define WMAP_BLACK 0x0000 -#define WMAP_WHITE 0xFFFF - - -class UITask; - -class WatchMapScreen : public UIScreen { -public: - WatchMapScreen(UITask* task) - : _task(task), - _lgfx(nullptr), - _mapsReady(false), - _needsRedraw(true), - _hasFix(false), - _centerLat(-33.8688), // Default: Sydney - _centerLon(151.2093), - _gpsLat(0.0), - _gpsLon(0.0), - _zoom(WMAP_DEFAULT_ZOOM), - _zoomMin(WMAP_MIN_ZOOM), - _zoomMax(WMAP_MAX_ZOOM), - _pngBuf(nullptr), - _lineBuf(nullptr), - _tileFound(false), - _w(120), - _h(120), - _viewportH(120 - WMAP_FOOTER_H), - _markers(nullptr), - _numMarkers(0) - { - } - - ~WatchMapScreen() { - if (_pngBuf) { free(_pngBuf); _pngBuf = nullptr; } - if (_lineBuf) { free(_lineBuf); _lineBuf = nullptr; } - if (_markers) { free(_markers); _markers = nullptr; } - } - - // Whether the maps LittleFS partition mounted (set from main.cpp on open) - void setMapsReady(bool ready) { _mapsReady = ready; } - - // Set initial GPS position (called when opening map -- centers viewport) - void setGPSPosition(double lat, double lon) { - if (lat != 0.0 || lon != 0.0) { - _gpsLat = lat; - _gpsLon = lon; - _centerLat = lat; - _centerLon = lon; - _hasFix = true; - _needsRedraw = true; - } - } - - // Update own GPS position without moving viewport (called periodically) - void updateGPSPosition(double lat, double lon) { - if (lat == 0.0 && lon == 0.0) return; - if (lat != _gpsLat || lon != _gpsLon) { - _gpsLat = lat; - _gpsLon = lon; - _hasFix = true; - _needsRedraw = true; - } - } - - // Marker management (call once per contact before/while on map) - void clearMarkers() { _numMarkers = 0; } - void addMarker(double lat, double lon, const char* name = "", uint8_t type = 0) { - if (!_markers) { - _markers = (MapMarker*)ps_calloc(WMAP_MAX_MARKERS, sizeof(MapMarker)); - if (!_markers) return; - } - if (_numMarkers >= WMAP_MAX_MARKERS) return; - if (lat == 0.0 && lon == 0.0) return; - _markers[_numMarkers].lat = lat; - _markers[_numMarkers].lon = lon; - _markers[_numMarkers].type = type; - strncpy(_markers[_numMarkers].name, name, sizeof(_markers[0].name) - 1); - _markers[_numMarkers].name[sizeof(_markers[0].name) - 1] = '\0'; - _numMarkers++; - } - int getNumMarkers() const { return _numMarkers; } - - // Called when navigating to the map screen - void enter(DisplayDriver& display) { - _lgfx = (LGFXDisplay*)&display; - _w = display.width(); - _h = display.height(); - _viewportH = _h - WMAP_FOOTER_H; - _needsRedraw = true; - - if (!_markers) { - _markers = (MapMarker*)ps_calloc(WMAP_MAX_MARKERS, sizeof(MapMarker)); - } - if (!_pngBuf) { - _pngBuf = (uint8_t*)ps_malloc(WMAP_PNG_BUF_SIZE); - if (!_pngBuf) _pngBuf = (uint8_t*)malloc(WMAP_PNG_BUF_SIZE); - } - if (!_lineBuf) { - _lineBuf = (uint16_t*)ps_malloc(WMAP_TILE_SIZE * sizeof(uint16_t)); - if (!_lineBuf) _lineBuf = (uint16_t*)malloc(WMAP_TILE_SIZE * sizeof(uint16_t)); - } - - detectZoomRange(); - } - - // ---- Touch actions (called from main.cpp gesture dispatch) ---- - - void zoomIn() { - if (_zoom < _zoomMax) { _zoom++; _needsRedraw = true; } - } - void zoomOut() { - if (_zoom > _zoomMin) { _zoom--; _needsRedraw = true; } - } - void recenter() { - if (_hasFix) { - _centerLat = _gpsLat; - _centerLon = _gpsLon; - _needsRedraw = true; - } - } - - // Pan the viewport by a logical-pixel delta (swipe). Exact: convert centre to - // global pixel space, offset, convert back. - void panByPixels(int dx, int dy) { - int ctX, ctY, cpX, cpY; - latLonToTileXY(_centerLat, _centerLon, _zoom, ctX, ctY, cpX, cpY); - - long globalX = (long)ctX * WMAP_TILE_SIZE + cpX - dx; - long globalY = (long)ctY * WMAP_TILE_SIZE + cpY - dy; - - long span = (long)(1 << _zoom) * WMAP_TILE_SIZE; - // Longitude wraps - while (globalX < 0) globalX += span; - while (globalX >= span) globalX -= span; - // Latitude clamps - if (globalY < 0) globalY = 0; - if (globalY > span - 1) globalY = span - 1; - - int nTileX = (int)(globalX / WMAP_TILE_SIZE); - int nPixX = (int)(globalX % WMAP_TILE_SIZE); - int nTileY = (int)(globalY / WMAP_TILE_SIZE); - int nPixY = (int)(globalY % WMAP_TILE_SIZE); - - tileXYToLatLon(nTileX, nTileY, nPixX, nPixY, _zoom, _centerLat, _centerLon); - _needsRedraw = true; - } - - // Handle a discrete tap at logical coords. Footer +/- buttons zoom; a tap in - // the map area recenters on GPS. Returns true if the tap was consumed. - bool handleTap(int x, int y) { - int footerY = _h - WMAP_FOOTER_H; - if (y >= footerY) { - if (inRect(x, y, _plusX, _btnY, _btnW, _btnH)) { zoomIn(); return true; } - if (inRect(x, y, _minusX, _btnY, _btnW, _btnH)) { zoomOut(); return true; } - return false; // footer, but not on a button - } - // Tap in the map area -> recenter on own position - recenter(); - return true; - } - - // ---- UIScreen interface ---- - - int render(DisplayDriver& display) override { - _lgfx = (LGFXDisplay*)&display; - _w = display.width(); - _h = display.height(); - _viewportH = _h - WMAP_FOOTER_H; - - if (!_mapsReady) { - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - display.setCursor(4, 20); - display.print("No map tiles"); - renderFooter(display); - return 5000; - } - - bool wasRedraw = _needsRedraw; - _needsRedraw = false; - - renderMapViewport(); - renderContactMarkers(); - renderCrosshair(); - renderFooter(display); - - return wasRedraw ? 500 : 30000; - } - -private: - UITask* _task; - LGFXDisplay* _lgfx; - bool _mapsReady; - bool _needsRedraw; - bool _hasFix; - - double _centerLat; - double _centerLon; - double _gpsLat; - double _gpsLon; - int _zoom; - int _zoomMin; - int _zoomMax; - - uint8_t* _pngBuf; - uint16_t* _lineBuf; - bool _tileFound; - - int _w; - int _h; - int _viewportH; - - // Footer button rects (set in renderFooter) - int _plusX = 0, _minusX = 0, _btnY = 0, _btnW = 18, _btnH = 14; - - PNG _png; - - struct MapMarker { - double lat; - double lon; - char name[20]; - uint8_t type; - }; - MapMarker* _markers; - int _numMarkers; - - struct DrawContext { - LGFXDisplay* display; - PNG* png; - int offsetX; - int offsetY; - int viewportH; - int dispW; - uint16_t* lineBuf; - }; - DrawContext _drawCtx; - - static bool inRect(int px, int py, int x, int y, int w, int h) { - return px >= x && px < x + w && py >= y && py < y + h; - } - - // ========================================================================== - // Detect available zoom levels from /tiles/{z}/ directories on LittleFS - // ========================================================================== - void detectZoomRange() { - if (!_mapsReady) return; - - _zoomMin = WMAP_MAX_ZOOM; - _zoomMax = WMAP_MIN_ZOOM; - - char path[32]; - for (int z = WMAP_MIN_ZOOM; z <= WMAP_MAX_ZOOM; z++) { - snprintf(path, sizeof(path), WMAP_TILE_ROOT "/%d", z); - if (LittleFS.exists(path)) { - if (z < _zoomMin) _zoomMin = z; - if (z > _zoomMax) _zoomMax = z; - } - } - - if (_zoomMin > _zoomMax) { - _zoomMin = WMAP_MIN_ZOOM; - _zoomMax = WMAP_MAX_ZOOM; - Serial.println("WatchMapScreen: no tile directories found"); - } else { - Serial.printf("WatchMapScreen: detected zoom range %d-%d\n", _zoomMin, _zoomMax); - } - - if (_zoom > _zoomMax) _zoom = _zoomMax; - if (_zoom < _zoomMin) _zoom = _zoomMin; - } - - // ========================================================================== - // Tile coordinate math (Web Mercator / Slippy Map) -- identical to MapScreen - // ========================================================================== - static void latLonToTileXY(double lat, double lon, int zoom, - int& tileX, int& tileY, - int& pixelX, int& pixelY) - { - int n = 1 << zoom; - double x = (lon + 180.0) / 360.0 * n; - tileX = (int)floor(x); - pixelX = (int)((x - tileX) * WMAP_TILE_SIZE); - - double latRad = lat * PI / 180.0; - double y = (1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / PI) / 2.0 * n; - tileY = (int)floor(y); - pixelY = (int)((y - tileY) * WMAP_TILE_SIZE); - } - - static void tileXYToLatLon(int tileX, int tileY, int pixelX, int pixelY, - int zoom, double& lat, double& lon) - { - int n = 1 << zoom; - double x = tileX + (double)pixelX / WMAP_TILE_SIZE; - double y = tileY + (double)pixelY / WMAP_TILE_SIZE; - - lon = x / n * 360.0 - 180.0; - double latRad = atan(sinh(PI * (1.0 - 2.0 * y / n))); - lat = latRad * 180.0 / PI; - } - - bool latLonToScreen(double lat, double lon, int& screenX, int& screenY) { - int centerTileX, centerTileY, centerPixelX, centerPixelY; - latLonToTileXY(_centerLat, _centerLon, _zoom, - centerTileX, centerTileY, centerPixelX, centerPixelY); - - int targetTileX, targetTileY, targetPixelX, targetPixelY; - latLonToTileXY(lat, lon, _zoom, - targetTileX, targetTileY, targetPixelX, targetPixelY); - - int dx = (targetTileX - centerTileX) * WMAP_TILE_SIZE + (targetPixelX - centerPixelX); - int dy = (targetTileY - centerTileY) * WMAP_TILE_SIZE + (targetPixelY - centerPixelY); - - screenX = _w / 2 + dx; - screenY = _viewportH / 2 + dy; - - return (screenX >= 0 && screenX < _w && - screenY >= 0 && screenY < _viewportH); - } - - // ========================================================================== - // Tile loading and rendering - // ========================================================================== - static void buildTilePath(char* buf, int bufSize, int zoom, int x, int y) { - snprintf(buf, bufSize, WMAP_TILE_ROOT "/%d/%d/%d.png", zoom, x, y); - } - - bool loadAndRenderTile(int tileX, int tileY, int screenX, int screenY) { - if (!_pngBuf || !_lineBuf || !_lgfx) return false; - - char path[64]; - buildTilePath(path, sizeof(path), _zoom, tileX, tileY); - - if (!LittleFS.exists(path)) return false; - - File f = LittleFS.open(path, FILE_READ); - if (!f) return false; - - int fileSize = f.size(); - if (fileSize > WMAP_PNG_BUF_SIZE) { - Serial.printf("WatchMapScreen: tile too large: %s (%d bytes)\n", path, fileSize); - f.close(); - return false; - } - - int bytesRead = f.read(_pngBuf, fileSize); - f.close(); - - if (bytesRead != fileSize) { - Serial.printf("WatchMapScreen: short read: %s (%d/%d)\n", path, bytesRead, fileSize); - return false; - } - - _drawCtx.display = _lgfx; - _drawCtx.png = &_png; - _drawCtx.offsetX = screenX; - _drawCtx.offsetY = screenY; - _drawCtx.viewportH = _viewportH; - _drawCtx.dispW = _w; - _drawCtx.lineBuf = _lineBuf; - - int rc = _png.openRAM(_pngBuf, fileSize, pngDrawCallback); - if (rc != PNG_SUCCESS) { - Serial.printf("WatchMapScreen: PNG open failed: %s (rc=%d)\n", path, rc); - return false; - } - - rc = _png.decode(&_drawCtx, 0); - _png.close(); - - if (rc != PNG_SUCCESS) { - Serial.printf("WatchMapScreen: PNG decode failed: %s (rc=%d)\n", path, rc); - return false; - } - - return true; - } - - // PNGdec scanline callback -- draws one decoded row into the sprite buffer. - static int pngDrawCallback(PNGDRAW* pDraw) { - DrawContext* ctx = (DrawContext*)pDraw->pUser; - if (!ctx || !ctx->display || !ctx->png || !ctx->lineBuf) return 0; - - int screenY = ctx->offsetY + pDraw->y; - if (screenY < 0 || screenY >= ctx->viewportH) return 1; - - uint16_t lineWidth = pDraw->iWidth; - if (lineWidth > WMAP_TILE_SIZE) lineWidth = WMAP_TILE_SIZE; - ctx->png->getLineAsRGB565(pDraw, ctx->lineBuf, PNG_RGB565_LITTLE_ENDIAN, 0xFFFFFFFF); - - for (int x = 0; x < lineWidth; x++) { - int screenX = ctx->offsetX + x; - if (screenX < 0 || screenX >= ctx->dispW) continue; - - uint16_t pixel = ctx->lineBuf[x]; - uint16_t color = (pixel > 0x7FFF) ? WMAP_WHITE : WMAP_BLACK; - ctx->display->drawPixelRaw(screenX, screenY, color); - } - return 1; - } - - // ========================================================================== - // Viewport rendering -- stitch tiles to fill the map area - // ========================================================================== - void renderMapViewport() { - if (!_lgfx) return; - - int centerTileX, centerTileY, centerPixelX, centerPixelY; - latLonToTileXY(_centerLat, _centerLon, _zoom, - centerTileX, centerTileY, centerPixelX, centerPixelY); - - int viewCenterX = _w / 2; - int viewCenterY = _viewportH / 2; - - int baseTileScreenX = viewCenterX - centerPixelX; - int baseTileScreenY = viewCenterY - centerPixelY; - - int startDX = 0, startDY = 0; - int endDX = 0, endDY = 0; - - while (baseTileScreenX + startDX * WMAP_TILE_SIZE > 0) startDX--; - while (baseTileScreenY + startDY * WMAP_TILE_SIZE > 0) startDY--; - while (baseTileScreenX + (endDX + 1) * WMAP_TILE_SIZE < _w) endDX++; - while (baseTileScreenY + (endDY + 1) * WMAP_TILE_SIZE < _viewportH) endDY++; - - int maxTile = (1 << _zoom) - 1; - int loaded = 0, missing = 0; - - for (int dy = startDY; dy <= endDY; dy++) { - for (int dx = startDX; dx <= endDX; dx++) { - int tx = centerTileX + dx; - int ty = centerTileY + dy; - - if (tx < 0) tx += (1 << _zoom); - if (tx > maxTile) tx -= (1 << _zoom); - if (ty < 0 || ty > maxTile) continue; - - int screenX = baseTileScreenX + dx * WMAP_TILE_SIZE; - int screenY = baseTileScreenY + dy * WMAP_TILE_SIZE; - - if (loadAndRenderTile(tx, ty, screenX, screenY)) loaded++; - else missing++; - yield(); - } - } - - Serial.printf("WatchMapScreen: rendered %d tiles, %d missing\n", loaded, missing); - _tileFound = (loaded > 0); - } - - // ========================================================================== - // Contact marker overlay - // ========================================================================== - void renderContactMarkers() { - if (!_lgfx || !_markers) return; - - for (int i = 0; i < _numMarkers; i++) { - int sx, sy; - if (latLonToScreen(_markers[i].lat, _markers[i].lon, sx, sy)) { - int r = markerRadius(); - drawDiamond(sx, sy, r); - if (_markers[i].name[0] != '\0' && - (_markers[i].type == ADV_TYPE_REPEATER || _zoom >= 14)) { - drawLabel(sx, sy - r - 2, _markers[i].name); - } - } - } - - if (_hasFix) { - int sx, sy; - if (latLonToScreen(_gpsLat, _gpsLon, sx, sy)) { - drawOwnPosition(sx, sy); - } - } - } - - // Marker radius scaled by zoom (smaller than e-ink -- 120px canvas) - int markerRadius() { - int r = _zoom - 9; - if (r < 2) r = 2; - if (r > 6) r = 6; - return r; - } - - void drawDiamond(int cx, int cy, int r) { - // White outline (1px larger than fill) - for (int dy = -(r + 1); dy <= (r + 1); dy++) { - int span = (r + 1) - abs(dy); - int innerSpan = r - abs(dy); - for (int dx = -span; dx <= span; dx++) { - if (abs(dy) <= r && abs(dx) <= innerSpan) continue; - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < _w && py >= 0 && py < _viewportH) { - _lgfx->drawPixelRaw(px, py, WMAP_WHITE); - } - } - } - // Filled black diamond - for (int dy = -r; dy <= r; dy++) { - int span = r - abs(dy); - for (int dx = -span; dx <= span; dx++) { - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < _w && py >= 0 && py < _viewportH) { - _lgfx->drawPixelRaw(px, py, WMAP_BLACK); - } - } - } - } - - static int extractAsciiLabel(const char* src, char* dest, int destSize) { - int j = 0; - for (int i = 0; src[i] != '\0' && j < destSize - 1; i++) { - uint8_t ch = (uint8_t)src[i]; - if (ch >= 0x20 && ch <= 0x7E) dest[j++] = src[i]; - } - dest[j] = '\0'; - int start = 0; - while (dest[start] == ' ') start++; - if (start > 0) { - memmove(dest, dest + start, j - start + 1); - j -= start; - } - return j; - } - - // Draw a marker label with a white background box, using the normal buffered - // text API (Font0 6x8). Kept short -- the canvas is only 120px wide. - void drawLabel(int cx, int topY, const char* text) { - if (!_lgfx) return; - char clean[16]; - int len = extractAsciiLabel(text, clean, sizeof(clean)); - if (len == 0) return; - if (len > 10) { len = 10; clean[len] = '\0'; } - - int textW = len * 6; - int textH = 8; - int lx = cx - textW / 2; - int ly = topY - textH; - - if (lx < 1) lx = 1; - if (lx + textW >= _w - 1) lx = _w - textW - 1; - if (ly < 0) ly = 0; - if (ly + textH >= _viewportH) return; - - _lgfx->setColor(DisplayDriver::LIGHT); - _lgfx->fillRect(lx - 1, ly - 1, textW + 2, textH + 2); - _lgfx->setColor(DisplayDriver::DARK); - _lgfx->setTextSize(1); - _lgfx->setCursor(lx, ly); - _lgfx->print(clean); - } - - // Own-position marker: bold circle with filled centre dot - void drawOwnPosition(int cx, int cy) { - int r = 6; - // White halo - for (int dy = -(r + 2); dy <= (r + 2); dy++) { - for (int dx = -(r + 2); dx <= (r + 2); dx++) { - if (dx * dx + dy * dy <= (r + 2) * (r + 2)) { - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < _w && py >= 0 && py < _viewportH) - _lgfx->drawPixelRaw(px, py, WMAP_WHITE); - } - } - } - // Black ring - for (int dy = -r; dy <= r; dy++) { - for (int dx = -r; dx <= r; dx++) { - int d2 = dx * dx + dy * dy; - if (d2 >= (r - 2) * (r - 2) && d2 <= r * r) { - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < _w && py >= 0 && py < _viewportH) - _lgfx->drawPixelRaw(px, py, WMAP_BLACK); - } - } - } - // Centre dot - for (int dy = -2; dy <= 2; dy++) { - for (int dx = -2; dx <= 2; dx++) { - if (dx * dx + dy * dy <= 4) { - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < _w && py >= 0 && py < _viewportH) - _lgfx->drawPixelRaw(px, py, WMAP_BLACK); - } - } - } - } - - // ========================================================================== - // Crosshair at viewport centre - // ========================================================================== - void renderCrosshair() { - if (!_lgfx) return; - - int cx = _w / 2; - int cy = _viewportH / 2; - int len = markerRadius() + 2; - - for (int x = cx - len; x <= cx + len; x++) { - if (x >= 0 && x < _w) { - if (cy - 1 >= 0) _lgfx->drawPixelRaw(x, cy - 1, WMAP_WHITE); - if (cy + 1 < _viewportH) _lgfx->drawPixelRaw(x, cy + 1, WMAP_WHITE); - _lgfx->drawPixelRaw(x, cy, WMAP_BLACK); - } - } - for (int y = cy - len; y <= cy + len; y++) { - if (y >= 0 && y < _viewportH) { - if (cx - 1 >= 0) _lgfx->drawPixelRaw(cx - 1, y, WMAP_WHITE); - if (cx + 1 < _w) _lgfx->drawPixelRaw(cx + 1, y, WMAP_WHITE); - _lgfx->drawPixelRaw(cx, y, WMAP_BLACK); - } - } - } - - // ========================================================================== - // Footer -- zoom level + on-screen +/- zoom buttons - // ========================================================================== - void renderFooter(DisplayDriver& display) { - int footerY = _h - WMAP_FOOTER_H; - - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, footerY - 1, _w, 1); - - // Left: zoom level - char left[8]; - snprintf(left, sizeof(left), "Z%d", _zoom); - display.setCursor(1, footerY + 4); - display.print(left); - - // Right: [-] [+] buttons - _btnW = 18; - _btnH = WMAP_FOOTER_H - 2; - _btnY = footerY + 1; - _plusX = _w - _btnW - 1; - _minusX = _plusX - _btnW - 2; - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(_minusX, _btnY, _btnW, _btnH); - display.drawRect(_plusX, _btnY, _btnW, _btnH); - display.setCursor(_minusX + 6, _btnY + 3); - display.print("-"); - display.setCursor(_plusX + 5, _btnY + 3); - display.print("+"); - } -}; diff --git a/examples/companion_radio/ui-new/WatchNotesScreen.h b/examples/companion_radio/ui-new/WatchNotesScreen.h deleted file mode 100644 index 8cc39664..00000000 --- a/examples/companion_radio/ui-new/WatchNotesScreen.h +++ /dev/null @@ -1,433 +0,0 @@ -#pragma once - -// ============================================================================= -// WatchNotesScreen -- short notes for the LilyGo T-Watch S3 / S3 Plus. -// -// The T-Deck Pro's NotesScreen.h is bound to the SD card (list/read/write/ -// rename all via SD.h), which the watches do not have. This is a separate, -// much smaller screen that stores notes as .txt files on the LittleFS "maps" -// partition (the same one WatchAlarmScreen uses for /alarms.cfg). -// -// Notes are composed and edited on the TWatchKeyboardScreen (purpose -// TWKB_NOTE), so a note is capped at the keyboard buffer (133 chars). Editing -// opens the keyboard pre-filled with the note text and saving replaces the -// file. Files longer than the view buffer (e.g. copied on by other means) are -// shown truncated, and editing one truncates it to the keyboard cap. -// -// Filenames are auto timestamps, YYMMDD-HHMMSS.txt in local time, so a plain -// descending name sort is newest-first. If the clock is not set yet the name -// falls back to u.txt. -// -// LIST: tap a row to select it, tap the selected row again (or press -// PWR/Enter) to open it; the [New] footer button composes a new note. -// VIEW: footer [Back] [Edit] [Del]; Del asks once ("Sure?") before deleting; -// tapping the text area scrolls when the note is longer than one screen. -// The top status strip is handled by main.cpp before this screen and returns -// home, matching the other watch screens. -// -// Geometry: the display is 240x240 physical, UI_ZOOM=2, so display.width() -// and display.height() both report 120 logical pixels. -// ============================================================================= - -// NOTE on include order: this header uses NodePrefs, which lives at -// examples/companion_radio/NodePrefs.h and is not on this variant's -I path. -// UITask.h pulls it in via "../NodePrefs.h", so WatchNotesScreen.h must always -// be included *after* UITask.h. Both call sites (UITask.cpp, main.cpp) do. -// This mirrors WatchAlarmScreen.h. -#include -#include -#include -#include // mesh::RTCClock -#include -#include -#include - -class UITask; - -#define WATCH_NOTES_DIR "/notes" -#define WATCH_NOTES_MAX 24 // list cap -#define WATCH_NOTE_NAME_LEN 20 // "YYMMDD-HHMMSS.txt" + NUL -#define WATCH_NOTE_PREVIEW 22 // first-line preview chars kept per entry -#define WATCH_NOTE_BUF 512 // view/edit buffer (on-watch notes are <=133) -#define WATCH_NOTE_COLS 19 // wrap width at textSize(1) on 120px -#define WATCH_NOTE_VIEW_LINES 9 // wrapped lines visible above the footer - -class WatchNotesScreen : public UIScreen { -public: - enum Mode { LIST, VIEW }; - -private: - UITask* _task; - mesh::RTCClock* _rtc; - NodePrefs* _node_prefs; - - Mode _mode = LIST; - int _sel = 0; // selected row in LIST - int _scroll = 0; // first visible row in LIST - - int _count = 0; - char _names[WATCH_NOTES_MAX][WATCH_NOTE_NAME_LEN]; - char _previews[WATCH_NOTES_MAX][WATCH_NOTE_PREVIEW]; - - // VIEW state - char _curName[WATCH_NOTE_NAME_LEN]; - char _noteBuf[WATCH_NOTE_BUF]; - int _noteLen = 0; - int _lineCount = 0; - int _viewScroll = 0; // first visible wrapped line - bool _delArmed = false; - - // Keyboard handshake (UITask::loop polls these; UITask is only forward - // declared here, so this screen never calls it directly) - bool _wantKeyboard = false; - bool _editPending = false; // true = prefill keyboard with _noteBuf - char _editName[WATCH_NOTE_NAME_LEN]; - - int32_t localNow() const { - uint32_t utc = _rtc->getCurrentTime(); - if (utc < 1700000000) return -1; // clock not set yet - return (int32_t)utc + ((int32_t)_node_prefs->utc_offset_hours * 3600); - } - - // days-from-epoch -> civil date (Howard Hinnant's algorithm) - static void civilFromDays(int32_t z, int& y, int& m, int& d) { - z += 719468; - int32_t era = (z >= 0 ? z : z - 146096) / 146097; - uint32_t doe = (uint32_t)(z - era * 146097); - uint32_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - int32_t yr = (int32_t)yoe + era * 400; - uint32_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - uint32_t mp = (5 * doy + 2) / 153; - d = (int)(doy - (153 * mp + 2) / 5 + 1); - m = (int)(mp < 10 ? mp + 3 : mp - 9); - y = (int)(yr + (m <= 2)); - } - - void makeName(char* out, size_t outLen) { - int32_t local = localNow(); - if (local < 0) { - snprintf(out, outLen, "u%07lu.txt", (unsigned long)(millis() % 10000000UL)); - return; - } - int y, mo, d; - civilFromDays(local / 86400, y, mo, d); - int hh = (local / 3600) % 24; - int mi = (local / 60) % 60; - int ss = local % 60; - snprintf(out, outLen, "%02d%02d%02d-%02d%02d%02d.txt", - y % 100, mo, d, hh, mi, ss); - } - - // "260714-173245.txt" -> "14/07 17:32"; u-names -> "no clock" - static void dateLabel(const char* name, char* out, size_t outLen) { - if (name[0] == 'u' || strlen(name) < 17) { - snprintf(out, outLen, "no clock"); - return; - } - snprintf(out, outLen, "%.2s/%.2s %.2s:%.2s", - name + 4, name + 2, name + 7, name + 9); - } - - static const char* leafOf(const char* n) { - const char* s = strrchr(n, '/'); - return s ? s + 1 : n; - } - - void readPreview(const char* leaf, char* out, size_t outLen) { - char path[40]; - snprintf(path, sizeof(path), WATCH_NOTES_DIR "/%s", leaf); - out[0] = 0; - File f = LittleFS.open(path, "r"); - if (!f) return; - size_t i = 0; - while (i < outLen - 1 && f.available()) { - int ch = f.read(); - if (ch < 0 || ch == '\n' || ch == '\r') break; - out[i++] = (char)ch; - } - out[i] = 0; - f.close(); - } - - void scanNotes() { - _count = 0; - if (!LittleFS.exists(WATCH_NOTES_DIR)) LittleFS.mkdir(WATCH_NOTES_DIR); - File root = LittleFS.open(WATCH_NOTES_DIR); - if (!root || !root.isDirectory()) return; - File f = root.openNextFile(); - while (f && _count < WATCH_NOTES_MAX) { - if (!f.isDirectory()) { - const char* leaf = leafOf(f.name()); - size_t n = strlen(leaf); - if (n > 4 && n < WATCH_NOTE_NAME_LEN && - strcmp(leaf + n - 4, ".txt") == 0) { - // insertion sort, descending name = newest first - int pos = _count; - while (pos > 0 && strcmp(leaf, _names[pos - 1]) > 0) pos--; - for (int k = _count; k > pos; k--) { - memcpy(_names[k], _names[k - 1], WATCH_NOTE_NAME_LEN); - } - strncpy(_names[pos], leaf, WATCH_NOTE_NAME_LEN - 1); - _names[pos][WATCH_NOTE_NAME_LEN - 1] = 0; - _count++; - } - } - f = root.openNextFile(); - } - root.close(); - for (int i = 0; i < _count; i++) readPreview(_names[i], _previews[i], WATCH_NOTE_PREVIEW); - if (_sel >= _count) _sel = _count ? _count - 1 : 0; - if (_scroll > _sel) _scroll = _sel; - } - - void countLines() { - _lineCount = (_noteLen + WATCH_NOTE_COLS - 1) / WATCH_NOTE_COLS; - if (_lineCount < 1) _lineCount = 1; - } - - bool loadNote(const char* leaf) { - char path[40]; - snprintf(path, sizeof(path), WATCH_NOTES_DIR "/%s", leaf); - File f = LittleFS.open(path, "r"); - if (!f) return false; - _noteLen = f.read((uint8_t*)_noteBuf, WATCH_NOTE_BUF - 1); - if (_noteLen < 0) _noteLen = 0; - _noteBuf[_noteLen] = 0; - f.close(); - strncpy(_curName, leaf, WATCH_NOTE_NAME_LEN - 1); - _curName[WATCH_NOTE_NAME_LEN - 1] = 0; - countLines(); - _viewScroll = 0; - _delArmed = false; - return true; - } - - void openSelected() { - if (_sel < 0 || _sel >= _count) return; - if (loadNote(_names[_sel])) _mode = VIEW; - } - - void deleteCurrent() { - char path[40]; - snprintf(path, sizeof(path), WATCH_NOTES_DIR "/%s", _curName); - LittleFS.remove(path); - _delArmed = false; - _mode = LIST; - scanNotes(); - } - - // --------------------------------------------------------------------------- - // Rendering. 120x120 logical. - // --------------------------------------------------------------------------- - static const int ROW_Y0 = 18; - static const int ROW_H = 20; - static const int ROWS_VISIBLE = 4; - static const int FOOTER_Y = 104; - - void ensureVisible() { - if (_sel < _scroll) _scroll = _sel; - if (_sel >= _scroll + ROWS_VISIBLE) _scroll = _sel - ROWS_VISIBLE + 1; - } - - void renderList(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 6, "Notes"); - - if (_count == 0) { - d->setRawColor(0x7BEF); - display.drawTextCentered(display.width() / 2, 50, "No notes yet"); - } - - char buf[16]; - for (int i = 0; i < ROWS_VISIBLE; i++) { - int idx = _scroll + i; - if (idx >= _count) break; - int y = ROW_Y0 + i * ROW_H; - - if (idx == _sel) { - d->setRawColor(0x231D); // same blue as the home tile - d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3); - } - - d->setRawColor(0xFFFF); - display.setCursor(5, y + 2); - display.print(_previews[idx][0] ? _previews[idx] : "(empty)"); - - dateLabel(_names[idx], buf, sizeof(buf)); - d->setRawColor(0x7BEF); - d->printSmallFont(5, y + 13, buf); - } - - d->setRawColor(0x231D); - d->drawRoundRect(31, FOOTER_Y, 58, 14, 3); - d->setRawColor(0xFFFF); - d->printSmallFont(52, FOOTER_Y + 8, "New"); - } - - void renderView(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - char buf[16]; - - dateLabel(_curName, buf, sizeof(buf)); - d->setRawColor(0x7BEF); - d->printSmallFont(4, 8, buf); - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - int maxScroll = _lineCount - WATCH_NOTE_VIEW_LINES; - if (maxScroll < 0) maxScroll = 0; - if (_viewScroll > maxScroll) _viewScroll = maxScroll; - char line[WATCH_NOTE_COLS + 1]; - for (int i = 0; i < WATCH_NOTE_VIEW_LINES; i++) { - int ln = _viewScroll + i; - int off = ln * WATCH_NOTE_COLS; - if (off >= _noteLen) break; - int n = _noteLen - off; - if (n > WATCH_NOTE_COLS) n = WATCH_NOTE_COLS; - memcpy(line, _noteBuf + off, n); - line[n] = 0; - d->setRawColor(0xFFFF); - display.setCursor(2, 14 + i * 10); - display.print(line); - } - - // Footer: [Back] [Edit] [Del] - d->setRawColor(0x231D); - d->drawRoundRect(1, FOOTER_Y, 38, 14, 3); - d->drawRoundRect(41, FOOTER_Y, 38, 14, 3); - d->setRawColor(_delArmed ? 0xF800 : 0x231D); - d->drawRoundRect(81, FOOTER_Y, 38, 14, 3); - d->setRawColor(0xC618); - d->printSmallFont(9, FOOTER_Y + 8, "Back"); - d->printSmallFont(49, FOOTER_Y + 8, "Edit"); - d->setRawColor(_delArmed ? 0xF800 : 0xC618); - d->printSmallFont(87, FOOTER_Y + 8, _delArmed ? "Sure?" : "Del"); - } - -public: - WatchNotesScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* node_prefs) - : _task(task), _rtc(rtc), _node_prefs(node_prefs) { - _curName[0] = 0; - _editName[0] = 0; - } - - void enter() { - _mode = LIST; - _delArmed = false; - _wantKeyboard = false; - _editPending = false; - scanNotes(); - _sel = 0; - _scroll = 0; - } - - Mode getMode() const { return _mode; } - - // --- Keyboard handshake, polled from UITask::loop --- - bool wantsKeyboard() const { return _wantKeyboard; } - void clearWantKeyboard() { _wantKeyboard = false; } - bool editPending() const { return _editPending; } - const char* getEditText() const { return _noteBuf; } - - // Result of a TWKB_NOTE keyboard session. Returns NULL on success or an - // error message for UITask to show; the caller returns to this screen. - const char* applyComposedNote(const char* text) { - if (text == nullptr || text[0] == 0) { _editPending = false; return "Empty note"; } - char name[WATCH_NOTE_NAME_LEN]; - if (_editPending && _editName[0]) { - strncpy(name, _editName, sizeof(name) - 1); - name[sizeof(name) - 1] = 0; - } else { - makeName(name, sizeof(name)); - } - _editPending = false; - _editName[0] = 0; - char path[40]; - snprintf(path, sizeof(path), WATCH_NOTES_DIR "/%s", name); - if (!LittleFS.exists(WATCH_NOTES_DIR)) LittleFS.mkdir(WATCH_NOTES_DIR); - File f = LittleFS.open(path, "w"); - if (!f) return "Save failed"; - f.print(text); - f.close(); - scanNotes(); - if (loadNote(name)) _mode = VIEW; else _mode = LIST; - return nullptr; - } - - // Physical tap in logical (display.width()) coordinates. Returns true if the - // tap was consumed. The status-bar strip is handled by main.cpp before this. - bool handleTap(int lx, int ly) { - if (_mode == LIST) { - if (ly >= FOOTER_Y) { // footer: New - if (lx >= 31 && lx < 89) { - _editPending = false; - _editName[0] = 0; - _wantKeyboard = true; - } - return true; - } - if (ly >= ROW_Y0) { - int row = (ly - ROW_Y0) / ROW_H; - int idx = _scroll + row; - if (row >= 0 && row < ROWS_VISIBLE && idx < _count) { - if (idx == _sel) openSelected(); // second tap opens - else { _sel = idx; ensureVisible(); } // first tap selects - } - return true; - } - return false; - } - - // VIEW - if (ly >= FOOTER_Y) { - if (lx < 40) { _delArmed = false; _mode = LIST; } // Back - else if (lx < 80) { // Edit - _delArmed = false; - strncpy(_editName, _curName, sizeof(_editName) - 1); - _editName[sizeof(_editName) - 1] = 0; - _editPending = true; - _wantKeyboard = true; - } else { // Del - if (_delArmed) deleteCurrent(); - else _delArmed = true; - } - return true; - } - _delArmed = false; - if (_lineCount > WATCH_NOTE_VIEW_LINES) { // scroll zones - if (ly < FOOTER_Y / 2) { if (_viewScroll > 0) _viewScroll--; } - else { if (_viewScroll < _lineCount - WATCH_NOTE_VIEW_LINES) _viewScroll++; } - } - return true; - } - - bool handleInput(char c) override { - // The PMU short press (S3) / user button arrives as KEY_ENTER. - if (_mode == LIST) { - if (c == KEY_ENTER) { openSelected(); return true; } - return false; // KEY_CANCEL falls through -> home - } - // VIEW - if (c == KEY_ENTER) { // button = Edit, matching the footer - _delArmed = false; - strncpy(_editName, _curName, sizeof(_editName) - 1); - _editName[sizeof(_editName) - 1] = 0; - _editPending = true; - _wantKeyboard = true; - return true; - } - if (c == KEY_CANCEL) { _delArmed = false; _mode = LIST; return true; } - return true; // swallow the rest while viewing - } - - int render(DisplayDriver& display) override { - switch (_mode) { - case LIST: renderList(display); return 500; - case VIEW: renderView(display); return 500; - } - return 500; - } -}; diff --git a/src/helpers/ui/LGFXDisplay.cpp b/src/helpers/ui/LGFXDisplay.cpp index 6cc9031c..5ed13273 100644 --- a/src/helpers/ui/LGFXDisplay.cpp +++ b/src/helpers/ui/LGFXDisplay.cpp @@ -56,28 +56,16 @@ void LGFXDisplay::setColor(Color c) { _color = TFT_WHITE; break; case RED: -#if defined(MECK_TWATCH) - _color = 0x7BEF; // dark grey -- the watch UI uses a grey/white/black palette -#else _color = TFT_RED; -#endif break; case GREEN: -#if defined(MECK_TWATCH) - _color = 0xC618; // light grey -#else _color = TFT_GREEN; -#endif break; case BLUE: _color = TFT_BLUE; break; case YELLOW: -#if defined(MECK_TWATCH) - _color = TFT_WHITE; -#else _color = TFT_YELLOW; -#endif break; case ORANGE: _color = TFT_ORANGE; diff --git a/src/helpers/ui/LGFXDisplay.h b/src/helpers/ui/LGFXDisplay.h index d10a571b..0f277cdd 100644 --- a/src/helpers/ui/LGFXDisplay.h +++ b/src/helpers/ui/LGFXDisplay.h @@ -28,58 +28,6 @@ public: void startFrame(Color bkg = DARK) override; void setTextSize(int sz) override; void setColor(Color c) override; -#if defined(MECK_TWATCH) - // Set an exact RGB565 colour, bypassing the Color enum + watch grey remap. - // Used by the watch P4-style tile grid for per-tile border/fill colours. - void setRawColor(uint16_t c) { _color = c; buffer.setTextColor(c); } - // Rounded-rect helpers for the P4-style tile grid (LovyanGFX buffer). - void fillRoundRect(int x, int y, int w, int h, int r) { buffer.fillRoundRect(x, y, w, h, r, _color); } - void drawRoundRect(int x, int y, int w, int h, int r) { buffer.drawRoundRect(x, y, w, h, r, _color); } - // Plot a single pixel at an exact RGB565 colour directly into the sprite - // buffer. Used by WatchMapScreen to draw decoded map tiles, markers and the - // crosshair (parallels GxEPDDisplay::drawPixelRaw on the e-ink boards). - void drawPixelRaw(int x, int y, uint16_t c) { buffer.drawPixel(x, y, c); } - // Render a string in a very small font at (x,y) so long node names fit - // beside the centred clock, then restore the default GLCD font + size so - // later text is unaffected. Uses the colour set via setColor()/setRawColor(). - void printSmallFont(int x, int y, const char* str) { - buffer.setFont(&fonts::TomThumb); - buffer.setTextColor(_color); - buffer.setTextSize(1); - buffer.setCursor(x, y); - buffer.print(str); - buffer.setFont(&fonts::Font0); // restore default 6x8 GLCD font - buffer.setTextSize(1); - } - // Width of a string in the tiny font (the font printSmallFont uses), for - // right-aligning tiny-font header text. Restores the default font after. - uint16_t smallTextWidth(const char* str) { - buffer.setFont(&fonts::TomThumb); - uint16_t w = buffer.textWidth(str); - buffer.setFont(&fonts::Font0); - return w; - } - // Enable/disable horizontal text wrap on the sprite buffer. The compose - // ticker turns wrap off so a long scrolling line is clipped at the screen - // edge instead of wrapping onto the rows below. - void setTextWrap(bool en) { buffer.setTextWrap(en, false); } - // Current print-cursor X on the sprite buffer -- lets a caller measure how - // wide a just-printed row was (used by the settings row ticker). - int getCursorX() { return buffer.getCursorX(); } - // Draw a string centred horizontally at mid_x with its top at y, in - // FreeSansBold 24pt (used for the clock HH:MM), then restore the default - // GLCD font + size so later text is unaffected. - void printClockFont(int mid_x, int y, const char* str) { - buffer.setFont(&fonts::FreeSansBold24pt7b); - buffer.setTextColor(_color); - buffer.setTextSize(1); - int w = buffer.textWidth(str); - buffer.setCursor(mid_x - w / 2, y); - buffer.print(str); - buffer.setFont(&fonts::Font0); // restore default 6x8 GLCD font - buffer.setTextSize(1); - } -#endif void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/variants/lilygo_twatch_s3/CPUPowerManager.h b/variants/lilygo_twatch_s3/CPUPowerManager.h deleted file mode 100644 index 4a24b715..00000000 --- a/variants/lilygo_twatch_s3/CPUPowerManager.h +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include - -// CPU Frequency Scaling for ESP32-S3 -// -// Typical current draw (CPU only, rough): -// 240 MHz ~70-80 mA -// 160 MHz ~50-60 mA -// 80 MHz ~30-40 mA -// 40 MHz ~15-20 mA (low-power / lock screen mode) -// -// SPI peripherals and UART use their own clock dividers from the APB clock, -// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz. - -#ifdef ESP32 - -#ifndef CPU_FREQ_IDLE -#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening -#endif - -#ifndef CPU_FREQ_BOOST -#define CPU_FREQ_BOOST 80 // MHz -- S3: no map screen, so cap boost to idle to save power -#endif - -#ifndef CPU_FREQ_LOW_POWER -#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C) -#endif - -#ifndef CPU_BOOST_TIMEOUT_MS -#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds -#endif - -class CPUPowerManager { -public: - CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {} - - void begin() { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - _boosted = false; - _lowPower = false; - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - - void loop() { - if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { - // Return to low-power if locked, otherwise normal idle - if (_lowPower) { - setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); - MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER); - } else { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - _boosted = false; - } - } - - void setBoost() { - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_BOOST); - _boosted = true; - MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST); - } - _boost_started = millis(); - } - - void setIdle() { - if (_boosted) { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - _boosted = false; - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - if (_lowPower) { - _lowPower = false; - } - } - - // Low-power mode — drops CPU to 40 MHz for lock screen standby. - // If currently boosted, the boost timeout will return to 40 MHz - // instead of 80 MHz. - void setLowPower() { - _lowPower = true; - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); - MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER); - } - // If boosted, the loop() timeout will drop to low-power instead of idle - } - - // Exit low-power mode — returns to normal idle (80 MHz). - // If currently boosted, the boost timeout will return to idle - // instead of low-power. - void clearLowPower() { - _lowPower = false; - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE); - } - // If boosted, the loop() timeout will drop to idle as normal - } - - bool isBoosted() const { return _boosted; } - bool isLowPower() const { return _lowPower; } - uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } - -private: - bool _boosted; - bool _lowPower; - unsigned long _boost_started; -}; - -#endif // ESP32 \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/DRV2605Haptic.h b/variants/lilygo_twatch_s3/DRV2605Haptic.h deleted file mode 100644 index 037edf8e..00000000 --- a/variants/lilygo_twatch_s3/DRV2605Haptic.h +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once - -#include -#include - -// Minimal inline DRV2605 haptic driver for the LilyGo T-Watch S3. -// -// Copied from variants/lilygo_tdeck_max/DRV2605Haptic.h so that the MAX build is -// untouched. Register sequence is identical; only the power note differs. -// -// The DRV2605L (schematic sheet 3, U47) sits on the shared Wire bus at I2C 0x5A -// and drives the 0827 coin motor on J11. Unlike the T-Deck Pro MAX, whose motor -// supply is gated by an XL9555 expander pin (board.motorEnable()), the watch -// powers the DRV2605 from the AXP2101 BLDO2 rail, which TWatchS3Board::power_init() -// already brings up at 3300 mV. There is therefore nothing to enable before -// begin(); if it does not ACK, BLDO2 is off or the I2C bus is not up yet. -// -// The register sequence mirrors Adafruit_DRV2605::begin() followed by -// selectLibrary(1) + setMode(INTTRIG) - i.e. exactly what the LilyGo basic.ino -// example does: an ERM motor, open-loop, internal-trigger, effect library 1. -class DRV2605Haptic { -public: - DRV2605Haptic(uint8_t addr = 0x5A, TwoWire* wire = &Wire) - : _addr(addr), _wire(wire) {} - - // Returns false if the DRV2605 does not ACK (e.g. BLDO2 still off). - bool begin() { - _wire->beginTransmission(_addr); - if (_wire->endTransmission() != 0) return false; - - writeReg(REG_MODE, 0x00); // exit standby, internal trigger - writeReg(REG_RTPIN, 0x00); // no real-time playback input - writeReg(REG_WAVESEQ1, 1); // default effect: strong click - writeReg(REG_WAVESEQ2, 0); // end of sequence - writeReg(REG_OVERDRIVE, 0); - writeReg(REG_SUSTAINPOS, 0); - writeReg(REG_SUSTAINNEG, 0); - writeReg(REG_BREAK, 0); - writeReg(REG_AUDIOMAX, 0x64); - writeReg(REG_FEEDBACK, readReg(REG_FEEDBACK) & 0x7F); // N_ERM_LRA = 0 -> ERM - writeReg(REG_CONTROL3, readReg(REG_CONTROL3) | 0x20); // ERM_OPEN_LOOP = 1 - writeReg(REG_LIBRARY, 1); // ERM effect library 1 - writeReg(REG_MODE, 0x00); // internal trigger - return true; - } - - // Fire a single library effect (1 = strong click, 100%). - void buzz(uint8_t effect = 1) { - writeReg(REG_WAVESEQ1, effect); - writeReg(REG_WAVESEQ2, 0); - writeReg(REG_GO, 1); - } - - // Return the DRV2605 to standby (~1.8uA). begin() must be run again before the - // next buzz -- a GO in standby is ignored -- so callers reset their ready flag. - void standby() { - writeReg(REG_MODE, 0x40); // STANDBY = 1, internal trigger - } - -private: - static const uint8_t REG_MODE = 0x01; - static const uint8_t REG_RTPIN = 0x02; - static const uint8_t REG_LIBRARY = 0x03; - static const uint8_t REG_WAVESEQ1 = 0x04; - static const uint8_t REG_WAVESEQ2 = 0x05; - static const uint8_t REG_GO = 0x0C; - static const uint8_t REG_OVERDRIVE = 0x0D; - static const uint8_t REG_SUSTAINPOS = 0x0E; - static const uint8_t REG_SUSTAINNEG = 0x0F; - static const uint8_t REG_BREAK = 0x10; - static const uint8_t REG_AUDIOMAX = 0x13; - static const uint8_t REG_FEEDBACK = 0x1A; - static const uint8_t REG_CONTROL3 = 0x1D; - - uint8_t _addr; - TwoWire* _wire; - - void writeReg(uint8_t reg, uint8_t val) { - _wire->beginTransmission(_addr); - _wire->write(reg); - _wire->write(val); - _wire->endTransmission(); - } - - uint8_t readReg(uint8_t reg) { - _wire->beginTransmission(_addr); - _wire->write(reg); - _wire->endTransmission(false); - _wire->requestFrom(_addr, (uint8_t)1); - return _wire->available() ? _wire->read() : 0; - } -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/PMUButton.h b/variants/lilygo_twatch_s3/PMUButton.h deleted file mode 100644 index 97b2ebf2..00000000 --- a/variants/lilygo_twatch_s3/PMUButton.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include // BUTTON_EVENT_* constants -#include "TWatchS3Board.h" - -// PMUButton -- a MomentaryButton work-alike for the LilyGo T-Watch S3, whose -// only control is the side PWR key. That key is not a GPIO: schematic sheet 1 -// wires SW7 to net PWR_KEY, which lands on AXP2101 pin 30 (PWRON). Presses are -// therefore reported as PMU interrupts and read back over I2C. -// -// It exposes the three methods UITask uses on `user_btn` (begin, isPressed, -// check), so it drops into target.h in place of MomentaryButton. -// -// Event mapping, set up in TWatchS3Board::power_init(): -// press < 1s PKEY_SHORT_IRQ -> BUTTON_EVENT_CLICK -// 1s <= press < 6s PKEY_LONG_IRQ masked; no event -// press >= 6s hardware power-off never reaches firmware -// -// BUTTON_EVENT_LONG_PRESS, _DOUBLE_CLICK and _TRIPLE_CLICK are never returned: -// the long hold belongs to the PMU's power-off, and the AXP2101 reports no -// multi-click. -// -// The IRQ line (PIN_PMU_IRQ, GPIO21) is not used. check() polls the PMU's -// latched status registers directly, which removes any dependence on that pin -// being wired, pulled up, or serviced by an ISR. Three I2C register reads every -// POLL_INTERVAL_MS is negligible on a bus already shared with the RTC and -// accelerometer. Note that clearIrqStatus() is write-1-to-clear across all -// three status registers, so an edge landing inside the ~200us between the read -// and the clear is dropped; press and release are ~100ms apart, so this cannot -// swallow a real event. -class PMUButton { - TWatchS3Board& _board; - unsigned long _last_poll = 0; - bool _down = false; - - static const unsigned long POLL_INTERVAL_MS = 30; - -public: - PMUButton(TWatchS3Board& board) : _board(board) {} - - // The PWRON key needs no pin setup; the PMU is configured in power_init(), - // which board.begin() runs before UITask::begin() reaches here. - void begin() {} - - // True while the key is held down. Updated by check(), so it is at most - // POLL_INTERVAL_MS stale. UITask uses this to abort a pending shutdown. - bool isPressed() const { return _down; } - - int check() { - if (millis() - _last_poll < POLL_INTERVAL_MS) return BUTTON_EVENT_NONE; - _last_poll = millis(); - - XPowersAXP2101* pmu = _board.getPMU(); - if (pmu == NULL) return BUTTON_EVENT_NONE; - - pmu->getIrqStatus(); // latch INTSTS1..3 into the driver's buffer - - // Apply the edges before the click, so that a poll which catches press, - // release and short-press together still leaves _down false. - if (pmu->isPekeyNegativeIrq()) _down = true; // PWRON is active low: press - if (pmu->isPekeyPositiveIrq()) _down = false; // release - - int ev = pmu->isPekeyShortPressIrq() ? BUTTON_EVENT_CLICK : BUTTON_EVENT_NONE; - - pmu->clearIrqStatus(); - return ev; - } -}; diff --git a/variants/lilygo_twatch_s3/TWatchComposeScreens.h b/variants/lilygo_twatch_s3/TWatchComposeScreens.h deleted file mode 100644 index d626c589..00000000 --- a/variants/lilygo_twatch_s3/TWatchComposeScreens.h +++ /dev/null @@ -1,795 +0,0 @@ -#pragma once -// ============================================================================= -// TWatchComposeScreens — touch keyboard compose system for the LilyGo -// T-Watch S3 Plus (ST7789 240x240, capacitive touch, companion_radio ui-new). -// -// Three UIScreen subclasses, compiled only when TWATCH_COMPOSE_ENABLED is -// defined (companion build only). All drawing and hit-testing is in the -// LGFXDisplay virtual coordinate space, which is 120x120 on this build -// (240px panel / UI_ZOOM=2). getTouch() returns coords already divided by -// UI_ZOOM, so a tap coordinate is directly comparable to draw coordinates. -// -// TWatchChannelPicker -- long-press on the home screen opens this. A single -// tap selects the channel row under the finger; a -// long-press on a row opens it; the back arrow -// (top-left) returns home. -// TWatchChannelScreen -- shows the most recent messages (sent + received) -// for the selected channel, read live from the shared -// ChannelScreen history store. Tap a line to ticker- -// scroll it; tap the compose bar to open the keyboard; -// back arrow returns home. -// TWatchKeyboardScreen — on-screen QWERTY. Bottom-left mode key cycles -// lower -> UPPER -> SYM. Bottom row is mode | space | -// enter | backspace. Enter sends on the channel; back -// arrow returns home. -// -// Each screen reads getTouch() itself in poll() and does its own release-edge -// detection, so actions fire on touch release (finger up) — this keeps the -// held finger from carrying into the next screen after a transition. Send/exit -// are delegated to UITask via the consume/flag pattern; message history and -// unread counts are read from the shared ChannelScreen store. -// ============================================================================= - -#ifdef TWATCH_COMPOSE_ENABLED - -#include -#include -#include -#include -#include -#include "ChannelScreen.h" // shared message history store (-I examples/companion_radio/ui-new) - -// ---- Tunables --------------------------------------------------------------- -#define TW_LONG_PRESS_MS 600 // hold to select a channel in the picker -#define TW_OUT_BUF_LEN 134 // MeshCore per-channel msg cap (~133) + NUL -#define TW_TICKER_MS_PER_PX 20 // channel-screen ticker scroll speed (ms per pixel) -#define TW_CH_NAME_LEN 32 -#define TW_PICKER_MAX 20 // matches MAX_GROUP_CHANNELS - -// ============================================================================= -// TWatchChannelPicker -// ============================================================================= -class TWatchChannelPicker : public UIScreen { - DisplayDriver* _display; - - struct Entry { uint8_t idx; char name[TW_CH_NAME_LEN]; }; - Entry _channels[TW_PICKER_MAX]; - uint8_t _numChannels; - uint8_t _highlighted; - uint8_t _scrollTop; - ChannelScreen* _store; // unread badge source - - // touch edge state - bool _touchDown; - int _downX, _downY; - unsigned long _downAt; - - // cross-screen flags (UITask polls these) - bool _confirmed; - bool _wantsExit; - - static const int HEADER_H = 14; - static const int ROW_H = 14; - - int visibleRows() const { - int h = _display ? _display->height() : 120; - int rows = (h - HEADER_H) / ROW_H; - return rows < 1 ? 1 : rows; - } - void ensureVisible() { - int vis = visibleRows(); - if (_highlighted < _scrollTop) _scrollTop = _highlighted; - else if (_highlighted >= _scrollTop + vis) _scrollTop = (uint8_t)(_highlighted - vis + 1); - } - void moveUp() { - if (_numChannels == 0) return; - _highlighted = (_highlighted == 0) ? (uint8_t)(_numChannels - 1) : (uint8_t)(_highlighted - 1); - ensureVisible(); - } - void moveDown() { - if (_numChannels == 0) return; - _highlighted = (uint8_t)((_highlighted + 1) % _numChannels); - ensureVisible(); - } - // Map a draw-space y coordinate to a channel index, or -1 if the tap is - // above the list or on an empty row. getTouch() returns coords already - // divided by UI_ZOOM, so _downY is directly comparable to the row layout. - int rowAtY(int y) const { - if (y < HEADER_H) return -1; - int i = (y - HEADER_H) / ROW_H; - if (i < 0 || i >= visibleRows()) return -1; - int ci = _scrollTop + i; - if (ci >= _numChannels) return -1; - return ci; - } - -public: - TWatchChannelPicker(DisplayDriver* display) - : _display(display), _numChannels(0), _highlighted(0), _scrollTop(0), - _store(nullptr), - _touchDown(false), _downX(0), _downY(0), _downAt(0), - _confirmed(false), _wantsExit(false) { - memset(_channels, 0, sizeof(_channels)); - } - - // Shared history store, used for the per-channel unread badges. - void setStore(ChannelScreen* store) { _store = store; } - - // Called by UITask before showing the screen. - void beginChannelSelect() { - _numChannels = 0; _highlighted = 0; _scrollTop = 0; - _touchDown = false; _confirmed = false; _wantsExit = false; - } - void addChannel(uint8_t idx, const char* name) { - if (_numChannels >= TW_PICKER_MAX) return; - _channels[_numChannels].idx = idx; - strncpy(_channels[_numChannels].name, name ? name : "", TW_CH_NAME_LEN - 1); - _channels[_numChannels].name[TW_CH_NAME_LEN - 1] = 0; - _numChannels++; - } - - // UITask bridges - bool isConfirmed() const { return _confirmed; } - void acknowledgeConfirm() { _confirmed = false; } - uint8_t getSelectedChannelIdx() const { return _channels[_highlighted].idx; } - const char* getSelectedChannelName() const { return _channels[_highlighted].name; } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - bool handleInput(char c) override { return false; } // touch handled in poll() - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; _downAt = millis(); - } else if (!now && _touchDown) { - _touchDown = false; - unsigned long held = millis() - _downAt; - if (_downX < 20 && _downY < HEADER_H) { _wantsExit = true; return; } // back arrow - int row = rowAtY(_downY); - if (row < 0) return; // tap outside the channel list - _highlighted = (uint8_t)row; // single tap selects the tapped row - ensureVisible(); - if (held >= TW_LONG_PRESS_MS) _confirmed = true; // long press opens it - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - display.setTextSize(1); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(W / 2, 0, "CHANNELS"); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, HEADER_H - 2, W, 1); - - if (_numChannels == 0) { - display.setCursor(2, HEADER_H + 2); - display.print("(no channels)"); - return 500; - } - - int vis = visibleRows(); - int y = HEADER_H; - for (int i = 0; i < vis; i++) { - int ci = _scrollTop + i; - if (ci >= _numChannels) break; - if (ci == _highlighted) { - display.setColor(DisplayDriver::GREEN); - display.fillRect(0, y, W, ROW_H); - display.setColor(DisplayDriver::DARK); - } else { - display.setColor(DisplayDriver::LIGHT); - } - int nameMaxW = W - 6; - int unread = _store ? _store->getUnreadForChannel(_channels[ci].idx) : 0; - if (unread > 0) { - char cnt[8]; - snprintf(cnt, sizeof(cnt), "%d", unread > 99 ? 99 : unread); - int cw = display.getTextWidth(cnt); - display.setColor((ci == _highlighted) ? DisplayDriver::DARK : DisplayDriver::BLUE); - display.drawTextRightAlign(W - 3, y + 2, cnt); - display.setColor((ci == _highlighted) ? DisplayDriver::DARK : DisplayDriver::LIGHT); - nameMaxW = W - 6 - cw - 4; - } - display.drawTextEllipsized(3, y + 2, nameMaxW, _channels[ci].name); - y += ROW_H; - } - return 500; - } -}; - -// ============================================================================= -// TWatchChannelScreen -// ============================================================================= -class TWatchChannelScreen : public UIScreen { - DisplayDriver* _display; - ChannelScreen* _store; // shared message history store - uint8_t _channelIdx; - char _channelName[TW_CH_NAME_LEN]; - char _selfPrefix[TW_CH_NAME_LEN + 2]; // "NodeName: " -- marks sent lines - - bool _touchDown; - int _downX, _downY; - - bool _wantsCompose; - bool _wantsExit; - - int _selectedN; // recency index shown as ticker, -1 = none - unsigned long _tickerStartMs; - const void* _lastNewest; // newest store entry seen last render - uint32_t _lastNewestTs; // (a change dismisses the ticker) - bool _showPath; // hop-path overlay visible - int _pathMsgN; // recency index the overlay describes - - static const int HEADER_H = 14; - static const int COMPOSE_BAR_H = 18; - static const int MSG_LINE_H = 11; - static const int MSG_TOP = HEADER_H + 2; - - int rowsThatFit() const { - int h = _display ? _display->height() : 120; - int rows = (h - COMPOSE_BAR_H - 1 - MSG_TOP) / MSG_LINE_H; - return rows < 1 ? 1 : rows; - } - - // Store messages available for this channel, capped at what fits on screen. - int visibleCount() const { - if (!_store) return 0; - int maxRows = rowsThatFit(); - int n = 0; - while (n < maxRows && _store->getChannelMsgByRecency(_channelIdx, n)) n++; - return n; - } - - // Sent messages are stored with path_len 0 and a "NodeName: " prefix. - bool isSent(const ChannelScreen::ChannelMessage* m) const { - return m->path_len == 0 && _selfPrefix[0] != 0 && - strncmp(m->text, _selfPrefix, strlen(_selfPrefix)) == 0; - } - - void drawMsgLine(DisplayDriver& display, int y, const char* text, bool sent) { - const int W = display.width(); - const int maxW = W - 4; - display.setColor(DisplayDriver::LIGHT); - if (!sent) { - display.drawTextEllipsized(2, y, maxW, text); // incoming: left-aligned - return; - } - if (display.getTextWidth(text) <= maxW) { // sent: right-aligned - display.drawTextRightAlign(W - 2, y, text); - return; - } - char buf[CHANNEL_MSG_TEXT_LEN + 4]; - strncpy(buf, text, sizeof(buf) - 4); - buf[sizeof(buf) - 4] = 0; - int ellW = display.getTextWidth("..."); - int len = (int)strlen(buf); - while (len > 0 && display.getTextWidth(buf) > maxW - ellW) { buf[--len] = 0; } - strcat(buf, "..."); - display.drawTextRightAlign(W - 2, y, buf); - } - - void drawTicker(DisplayDriver& display, int y, const char* text, bool sent) { - const int W = display.width(); - int textW = display.getTextWidth(text); - display.setColor(DisplayDriver::LIGHT); - if (textW <= W - 4) { // fits -> nothing to scroll, keep alignment - if (sent) display.drawTextRightAlign(W - 2, y, text); - else { display.setCursor(2, y); display.print(text); } - return; - } - int period = textW + 24; // full text width + trailing gap - unsigned long elapsed = millis() - _tickerStartMs; - int off = (int)((elapsed / TW_TICKER_MS_PER_PX) % (unsigned long)period); - // Wrap off while the marquee draws: a long line must clip at the screen - // edge, not wrap onto the rows below. Restored straight after. - ((LGFXDisplay*)_display)->setTextWrap(false); - display.setCursor(2 - off, y); - display.print(text); - display.setCursor(2 - off + period, y); - display.print(text); - ((LGFXDisplay*)_display)->setTextWrap(true); - } - - // Renders the "Heard by" list (repeaters that echoed our sent message + the - // SNR we heard each at) into the already-drawn overlay box. Data comes from - // MyMesh::getHeardBy, keyed by the fingerprint stashed in the sent bubble's - // path[]. Same blue box as the hop-path overlay. - void drawHeardBy(LGFXDisplay* d, const ChannelScreen::ChannelMessage* m, - int bx, int by, int bh) { - uint8_t bph = 0, count = 0; - uint8_t hashes[16]; - int8_t snrs[8]; - (void)bh; - the_mesh.getHeardBy(m->path, 12, bph, count, hashes, snrs, 8); - - char head[28]; - snprintf(head, sizeof(head), "Heard by: %d", count); // distinct repeaters - d->setRawColor(0x7BEF); - d->printSmallFont(bx + 5, by + 8, head); - - if (count == 0 || bph == 0) { - d->setRawColor(0xFFFF); - d->printSmallFont(bx + 5, by + 19, "No repeater echoes yet"); - return; - } - - const int maxLines = 5; // 5 repeaters, then a "+N more" 6th line - int shown = (count > maxLines) ? maxLines : count; - int y = by + 19; - for (int i = 0; i < shown; i++) { - const int off = i * bph; - char hex[6]; - if (bph == 1) snprintf(hex, sizeof(hex), "%02x", hashes[off]); - else snprintf(hex, sizeof(hex), "%02x%02x", hashes[off], hashes[off + 1]); - - char nm[16]; nm[0] = 0; - uint32_t numC = the_mesh.getNumContacts(); - ContactInfo c; - for (int pass = 0; pass < 2 && nm[0] == 0; pass++) { - for (uint32_t ci = 0; ci < numC; ci++) { - if (!the_mesh.getContactByIdx(ci, c)) continue; - if (memcmp(c.id.pub_key, &hashes[off], bph) != 0) continue; - if (pass == 0 && c.type != ADV_TYPE_REPEATER) continue; - strncpy(nm, c.name, sizeof(nm) - 1); nm[sizeof(nm) - 1] = 0; - break; - } - } - - int8_t s = snrs[i]; - char line[40]; - snprintf(line, sizeof(line), "(%s) %s %d.%ddB", - hex, nm[0] ? nm : "?", s / 4, ((abs((int)s) % 4) * 10) / 4); - d->setRawColor(0xFFFF); - d->printSmallFont(bx + 5, y, line); - y += 9; - } - if (count > shown) { - d->setRawColor(0xFFFF); - char more[16]; - snprintf(more, sizeof(more), "+%d more", count - shown); - d->printSmallFont(bx + 5, y, more); - } - } - - // Hop-path overlay for the message at recency _pathMsgN. Drawn last so it - // sits above the message list; toggled by the button (see handleInput), - // dismissed by any tap (see poll). - void drawPathOverlay(DisplayDriver& display) { - const ChannelScreen::ChannelMessage* m = - _store ? _store->getChannelMsgByRecency(_channelIdx, _pathMsgN) : nullptr; - if (!m) { _showPath = false; return; } - - LGFXDisplay* d = (LGFXDisplay*)&display; - const int bx = 3, bw = display.width() - 6, by = 18, bh = 76; - d->setRawColor(0x0000); - d->fillRoundRect(bx, by, bw, bh, 4); - d->setRawColor(0x231D); - d->drawRoundRect(bx, by, bw, bh, 4); - - // Sent message with a captured fingerprint: show which repeaters echoed it - // ("Heard by") instead of a hop path, which a sent bubble doesn't have. - if (m->path_len == 0 && m->has_fp) { - drawHeardBy(d, m, bx, by, bh); - return; - } - - char head[28]; - if (m->path_len == 0) { - snprintf(head, sizeof(head), "%s", isSent(m) ? "Sent (no path)" : "Direct (0 hops)"); - } else if (m->path_len == 0xFF) { - snprintf(head, sizeof(head), "%s", "Flood (no path)"); - } else { - snprintf(head, sizeof(head), "%d hops, %d-byte", - m->path_len & 63, (m->path_len >> 6) + 1); - } - d->setRawColor(0x7BEF); - d->printSmallFont(bx + 5, by + 8, head); - - if (m->path_len == 0 || m->path_len == 0xFF) return; - - int byteLen = (int)mesh::Packet::getPathByteLenFor(m->path_len); - if (byteLen > MSG_PATH_MAX) byteLen = MSG_PATH_MAX; - int bph = (m->path_len >> 6) + 1; - - // Hex hops in the path-editor format ("3601,2198,..."), wrapped - // token-wise across small-font lines. - char line[36]; - int ll = 0, y = by + 19; - d->setRawColor(0xFFFF); - for (int b = 0; b < byteLen; b += bph) { - char tok[9]; - int tl = 0; - for (int k = 0; k < bph && b + k < byteLen; k++) - tl += snprintf(tok + tl, sizeof(tok) - tl, "%02x", m->path[b + k]); - if (b + bph < byteLen) { tok[tl++] = ','; tok[tl] = 0; } - if (ll + tl > 26) { - line[ll] = 0; - d->printSmallFont(bx + 5, y, line); - y += 9; ll = 0; - if (y > by + bh - 8) { d->printSmallFont(bx + 5, y, "..."); return; } - } - memcpy(line + ll, tok, tl); ll += tl; - } - if (ll > 0) { line[ll] = 0; d->printSmallFont(bx + 5, y, line); } - } - -public: - TWatchChannelScreen(DisplayDriver* display) - : _display(display), _store(nullptr), _channelIdx(0), - _touchDown(false), _downX(0), _downY(0), - _wantsCompose(false), _wantsExit(false), - _selectedN(-1), _tickerStartMs(0), - _lastNewest(nullptr), _lastNewestTs(0), - _showPath(false), _pathMsgN(0) { - _channelName[0] = 0; - _selfPrefix[0] = 0; - } - - // Shared history store this screen renders from. - void setStore(ChannelScreen* store) { _store = store; } - - // Own node name, used to right-align sent lines ("NodeName: text"). - void setSelfName(const char* name) { - if (!name || !name[0]) { _selfPrefix[0] = 0; return; } - snprintf(_selfPrefix, sizeof(_selfPrefix), "%s: ", name); - } - - // Switch to a channel (called when a channel is selected in the picker). - // Opening a channel marks its messages as read. - void activate(uint8_t idx, const char* name) { - _channelIdx = idx; - strncpy(_channelName, name ? name : "", TW_CH_NAME_LEN - 1); - _channelName[TW_CH_NAME_LEN - 1] = 0; - _touchDown = false; _wantsCompose = false; _wantsExit = false; - _selectedN = -1; - _showPath = false; - _lastNewest = nullptr; _lastNewestTs = 0; - if (_store) _store->markChannelRead(_channelIdx); - } - - uint8_t getChannelIdx() const { return _channelIdx; } - const char* getChannelName() const { return _channelName; } - - bool wantsCompose() const { return _wantsCompose; } - void acknowledgeCompose() { _wantsCompose = false; } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - // Short press of the PWR key (S3, KEY_ENTER) or the boot button (S3 Plus, - // KEY_NEXT): toggle the hop-path overlay for the ticker-selected message - // (the newest one if none is selected). Touch is handled in poll(). - bool handleInput(char c) override { - if (c == KEY_ENTER || c == KEY_NEXT) { - if (_showPath) { _showPath = false; return true; } - int n = (_selectedN >= 0) ? _selectedN : 0; - if (_store && _store->getChannelMsgByRecency(_channelIdx, n)) { - _pathMsgN = n; - _showPath = true; - } - return true; - } - return false; - } - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; - } else if (!now && _touchDown) { - _touchDown = false; - int H = _display->height(); - if (_showPath) { _showPath = false; return; } // any tap dismisses the path overlay - if (_downX < 20 && _downY < HEADER_H) { _selectedN = -1; _wantsExit = true; return; } // back arrow - if (_downY >= H - COMPOSE_BAR_H) { _selectedN = -1; _wantsCompose = true; return; } // compose bar - // message area -> tap to open/close ticker - if (_downY >= MSG_TOP && _downY < H - COMPOSE_BAR_H) { - int visualRow = (_downY - MSG_TOP) / MSG_LINE_H; - int count = visibleCount(); - if (visualRow >= 0 && visualRow < count) { - int n = (count - 1) - visualRow; // top row = oldest visible - if (_selectedN == n) _selectedN = -1; - else { _selectedN = n; _tickerStartMs = millis(); } - } - } - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - const int H = display.height(); - display.setTextSize(1); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(W / 2, 0, _channelName); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, HEADER_H - 2, W, 1); - - // A new message (sent or received) shifts the rows, so dismiss any open - // ticker when the newest store entry for this channel changes. - const ChannelScreen::ChannelMessage* newest = - _store ? _store->getChannelMsgByRecency(_channelIdx, 0) : nullptr; - if (newest != _lastNewest || (newest && newest->timestamp != _lastNewestTs)) { - _lastNewest = newest; - _lastNewestTs = newest ? newest->timestamp : 0; - _selectedN = -1; - } - - int count = visibleCount(); - if (count == 0) { - display.setColor(DisplayDriver::LIGHT); - display.setCursor(2, MSG_TOP); - display.print("(no messages)"); - } else { - int y = MSG_TOP; - for (int n = count - 1; n >= 0; n--) { // oldest visible (top) -> newest (bottom) - const ChannelScreen::ChannelMessage* m = _store->getChannelMsgByRecency(_channelIdx, n); - if (!m) continue; - bool sent = isSent(m); - if (n == _selectedN) drawTicker(display, y, m->text, sent); - else drawMsgLine(display, y, m->text, sent); - y += MSG_LINE_H; - } - } - - // compose bar - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, H - COMPOSE_BAR_H, W, COMPOSE_BAR_H - 1); - display.drawTextEllipsized(3, H - COMPOSE_BAR_H + 4, W - 6, "Tap to compose"); - if (_showPath) drawPathOverlay(display); - return (_selectedN >= 0 || _showPath) ? 60 : 500; - } -}; - -// ============================================================================= -// TWatchKeyboardScreen -// ============================================================================= -class TWatchKeyboardScreen : public UIScreen { - DisplayDriver* _display; - uint8_t _channelIdx; - -public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE, TWKB_TRACE_PATH }; - -private: - Purpose _purpose; - int _contextIdx; // channel idx (channel) or contact idx (DM/admin) - bool _mask; // render composed text as '*' (admin password) - unsigned long _lastCharAt; // when the last char was typed (for reveal window) - - static const unsigned long PW_REVEAL_MS = 2000; // show last char this long before masking - - char _outBuf[TW_OUT_BUF_LEN]; - uint16_t _outLen; - - enum Mode { LOWER, UPPER, SYM }; - Mode _mode; - - bool _touchDown; - int _downX, _downY; - - bool _wantsSend; - bool _wantsExit; - - static const int MAXLEN = TW_OUT_BUF_LEN - 1; // 133 - - // layout geometry (120x120 virtual space) - static const int TOPBAR_H = 15; - static const int KEY_W = 12; // 10 * 12 = 120 wide - static const int KEY_H = 26; - static const int GRID_Y = 15; // row0 top; rows at GRID_Y + row*KEY_H - - const char* const* layout() const { - static const char* const lower[3] = { "qwertyuiop", "asdfghjkl'", "zxcvbnm,.?" }; - static const char* const upper[3] = { "QWERTYUIOP", "ASDFGHJKL\"", "ZXCVBNM<>/" }; - static const char* const syms[3] = { "123!@#$%^&", "456()[]{}-", "7890+=_:;|" }; - return _mode == SYM ? syms : (_mode == UPPER ? upper : lower); - } - const char* modeLabel() const { - return _mode == SYM ? "SYM" : (_mode == UPPER ? "A-Z" : "a-z"); - } - - void appendChar(char ch) { - if (_outLen < MAXLEN) { _outBuf[_outLen++] = ch; _outBuf[_outLen] = 0; _lastCharAt = millis(); } - } - void backspace() { - if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; _lastCharAt = 0; } - } - void cycleMode() { - _mode = (_mode == LOWER) ? UPPER : (_mode == UPPER ? SYM : LOWER); - } - // bottom row zones: mode 0..24 | space 24..72 | enter 72..96 | backspace 96..120 - void handleBottomRow(int x) { - if (x < 24) cycleMode(); - else if (x < 72) appendChar(' '); - else if (x < 96) { if (_outLen > 0) _wantsSend = true; } - else backspace(); - } - -public: - TWatchKeyboardScreen(DisplayDriver* display) - : _display(display), _channelIdx(0), - _purpose(TWKB_CHANNEL), _contextIdx(0), _mask(false), _lastCharAt(0), - _outLen(0), _mode(LOWER), - _touchDown(false), _downX(0), _downY(0), - _wantsSend(false), _wantsExit(false) { - _outBuf[0] = 0; - } - - // Enter compose on the given channel (called when the compose bar is tapped). - void activate(uint8_t idx, const char* name) { - (void)name; - _channelIdx = idx; - _purpose = TWKB_CHANNEL; - _contextIdx = idx; - _mask = false; - _outLen = 0; _outBuf[0] = 0; - _mode = LOWER; - _touchDown = false; _wantsSend = false; _wantsExit = false; - } - - // Enter text for a non-channel purpose (DM / admin password / admin CLI). - void activateFor(Purpose purpose, int contextIdx) { - _purpose = purpose; - _contextIdx = contextIdx; - _channelIdx = 0; - _mask = (purpose == TWKB_ADMIN_PASSWORD); - _outLen = 0; _outBuf[0] = 0; - _mode = LOWER; - _touchDown = false; _wantsSend = false; _wantsExit = false; - } - - Purpose getPurpose() const { return _purpose; } - int getContextIdx() const { return _contextIdx; } - - uint8_t getChannelIdx() const { return _channelIdx; } - bool consumeSendRequest(const char** textOut) { - if (!_wantsSend) return false; - _wantsSend = false; - if (textOut) *textOut = _outBuf; - return true; - } - void clearOutBuf() { _outLen = 0; _outBuf[0] = 0; } - // Pre-fill the compose buffer (e.g. editing an existing note). Call after - // activateFor(), which clears the buffer; input is clamped at MAXLEN. - void setInitialText(const char* s) { - _outLen = 0; - if (s) { - while (s[_outLen] && _outLen < MAXLEN) { _outBuf[_outLen] = s[_outLen]; _outLen++; } - } - _outBuf[_outLen] = 0; - } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - bool handleInput(char c) override { return false; } - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; - } else if (!now && _touchDown) { - _touchDown = false; - int x0 = _downX, y0 = _downY; - if (x0 < 20 && y0 < TOPBAR_H) { _wantsExit = true; return; } // back arrow - if (y0 < GRID_Y) return; // top bar (text) area - int row = (y0 - GRID_Y) / KEY_H; - if (row <= 2) { - int col = x0 / KEY_W; - char ch = layout()[row][col]; - if (ch) appendChar(ch); - } else { - handleBottomRow(x0); // bottom row - } - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - display.setTextSize(1); - - // ---- top bar: back arrow + composed text tail + cursor ---- - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - - display.setColor(DisplayDriver::LIGHT); - { - // Mask the composed text with '*' for the admin password purpose. - char masked[TW_OUT_BUF_LEN]; - const char* src = _outBuf; - if (_mask) { - int m = (_outLen < TW_OUT_BUF_LEN - 1) ? _outLen : TW_OUT_BUF_LEN - 1; - for (int i = 0; i < m; i++) masked[i] = '*'; - // Reveal the most recently typed char briefly so it can be read. - if (m > 0 && (millis() - _lastCharAt < PW_REVEAL_MS)) masked[m - 1] = _outBuf[m - 1]; - masked[m] = 0; - src = masked; - } - const int avail = W - 16 - 4; // room after the back arrow, minus cursor - int len = _outLen; - int fit = 0; - for (int n = 1; n <= len; n++) { - if (display.getTextWidth(src + (len - n)) > avail) break; - fit = n; - } - char tail[TW_OUT_BUF_LEN + 2]; - strncpy(tail, src + (len - fit), sizeof(tail) - 2); - tail[sizeof(tail) - 2] = 0; - size_t tl = strlen(tail); - tail[tl] = '_'; tail[tl + 1] = 0; - display.setCursor(16, 2); - display.print(tail); - } - - // ---- key grid rows 0..2 ---- - const char* const* lay = layout(); - for (int row = 0; row < 3; row++) { - int ry = GRID_Y + row * KEY_H; - const char* r = lay[row]; - for (int col = 0; col < 10; col++) { - int kx = col * KEY_W; - display.setColor(DisplayDriver::GREEN); - display.drawRect(kx, ry, KEY_W, KEY_H); - char ch[2] = { r[col], 0 }; - display.setColor(DisplayDriver::LIGHT); - int tw = display.getTextWidth(ch); - display.setCursor(kx + (KEY_W - tw) / 2, ry + (KEY_H - 8) / 2); - display.print(ch); - } - } - - // ---- bottom row: mode | space | enter | backspace ---- - int by = GRID_Y + 3 * KEY_H; - - display.setColor(DisplayDriver::GREEN); - display.fillRect(0, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* ml = modeLabel(); - int tw = display.getTextWidth(ml); - display.setCursor((24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(ml); - } - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(24, by, 48, KEY_H); // space - - display.setColor(DisplayDriver::GREEN); - display.fillRect(72, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* el = "OK"; - int tw = display.getTextWidth(el); - display.setCursor(72 + (24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(el); - } - - display.setColor(DisplayDriver::ORANGE); - display.fillRect(96, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* bl = "DEL"; - int tw = display.getTextWidth(bl); - display.setCursor(96 + (24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(bl); - } - - return 500; - } -}; - -#endif // TWATCH_COMPOSE_ENABLED \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/TWatchS3Board.cpp b/variants/lilygo_twatch_s3/TWatchS3Board.cpp deleted file mode 100644 index 9dff8bb2..00000000 --- a/variants/lilygo_twatch_s3/TWatchS3Board.cpp +++ /dev/null @@ -1,243 +0,0 @@ -#include -#include "TWatchS3Board.h" -#include -#include // power-debug: esp_bt_controller_get_status() - -volatile bool TWatchS3Board::_tilt_flag = false; -volatile uint32_t TWatchS3Board::_tilt_isr_count = 0; // TEMP diagnostic - -// memw forces the _tilt_flag store to retire to SRAM before this ISR -// returns. Without it, tiltFired() in the main loop can read a stale -// value and raise-to-wake is missed. Do not remove. -void IRAM_ATTR TWatchS3Board::onTiltISR() { _tilt_flag = true; _tilt_isr_count++; asm volatile("memw" ::: "memory"); } - -// ---- Wrapper-free BMA423 step counter (raw I2C) ---------------------------- -// SensorLib's SensorBMA423 step-counter methods do not compile in this build, -// so the step counter is driven directly over I2C. Register/offset/mask values -// are from the Bosch BMA423 driver. -#define BMA423_REG_STEP_CNT_OUT 0x1E // 4-byte little-endian step count output -#define BMA423_REG_FEATURE_CONFIG 0x5E // 64-byte feature config stream -#define BMA423_FEATURE_LEN 64 -#define BMA423_STEP_EN_BYTE 0x37 // BMA423_STEP_CNTR_OFFSET(0x36) + 1 -#define BMA423_STEP_EN_BIT 0x10 // BMA423_STEP_CNTR_EN_MSK -// Step counter watermark: a 10-bit field (BMA423_STEP_CNTR_WM_MSK = 0x03FF) -// spanning feature_config[0x36] as LSB and the low two bits of [0x37] as MSB. -// It does not collide with the enable bit, which is bit 4 of [0x37] (bit 12 of -// the 16-bit word). Bosch's bma423_step_counter_set_watermark() writes exactly -// these bits; SensorLib's enableStepCounter() calls it with 1, and LilyGo's own -// firmware calls setStepCounterWatermark(1). Meck never set it, which is the -// prime suspect for the undercounting. -#define BMA423_STEP_WM_LSB_BYTE 0x36 // BMA423_STEP_CNTR_OFFSET -#define BMA423_STEP_WM_MSK 0x03FF // BMA423_STEP_CNTR_WM_MSK -#define BMA423_STEP_WM_LEVEL 1 // matches LilyGoLib and the SensorLib example - -#define BMA423_REG_POWER_CONF 0x7C // BMA4_POWER_CONF_ADDR -#define BMA423_ADV_PWR_SAVE_BIT 0x01 // BMA4_ADVANCE_POWER_SAVE_MSK - -static bool bma423ReadRegs(uint8_t reg, uint8_t* buf, uint8_t len) { - Wire.beginTransmission(I2C_ADDR_ACCEL); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) return false; - if (Wire.requestFrom((int)I2C_ADDR_ACCEL, (int)len) != len) return false; - for (uint8_t i = 0; i < len; i++) buf[i] = Wire.read(); - return true; -} - -static bool bma423WriteRegs(uint8_t reg, const uint8_t* buf, uint8_t len) { - Wire.beginTransmission(I2C_ADDR_ACCEL); - Wire.write(reg); - for (uint8_t i = 0; i < len; i++) Wire.write(buf[i]); - return Wire.endTransmission() == 0; -} - -// Enable the step counter by setting its enable bit in the feature config, -// preserving every other byte (tilt lives at a different offset, 0x3A, so it is -// untouched). The feature config can only be written with advanced-power-save -// disabled, so we bracket the write and restore the prior power state after. -static void bma423EnableStepCounter() { - uint8_t pc; - if (!bma423ReadRegs(BMA423_REG_POWER_CONF, &pc, 1)) return; // save power state - uint8_t off = pc & ~BMA423_ADV_PWR_SAVE_BIT; // disable adv power save - bma423WriteRegs(BMA423_REG_POWER_CONF, &off, 1); - delay(2); // wake from low-power (>=450us) - - uint8_t cfg[BMA423_FEATURE_LEN]; - if (bma423ReadRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN)) { - // Watermark first, then the enable bit, in a single read-modify-write. - uint16_t wm = ((uint16_t)cfg[BMA423_STEP_EN_BYTE] << 8) | cfg[BMA423_STEP_WM_LSB_BYTE]; - wm = (wm & ~BMA423_STEP_WM_MSK) | (BMA423_STEP_WM_LEVEL & BMA423_STEP_WM_MSK); - cfg[BMA423_STEP_WM_LSB_BYTE] = (uint8_t)(wm & 0xFF); - cfg[BMA423_STEP_EN_BYTE] = (uint8_t)((wm >> 8) & 0xFF); - cfg[BMA423_STEP_EN_BYTE] |= BMA423_STEP_EN_BIT; - bma423WriteRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN); - delay(1); // write settle - } - - bma423WriteRegs(BMA423_REG_POWER_CONF, &pc, 1); // restore power state -} - -void TWatchS3Board::begin() { - ESP32Board::begin(); - power_init(); - - // BMA423 accelerometer (always-on I2C, 0x19): enable the tilt / wrist-raise - // feature and its interrupt (routed to PIN1 -> GPIO14) for raise-to-wake. - _accel = new SensorBMA423(); - if (_accel->begin(Wire, I2C_ADDR_ACCEL, PIN_BOARD_SDA, PIN_BOARD_SCL)) { - _accel->setRemapAxes(SensorRemap::BOTTOM_LAYER_TOP_RIGHT_CORNER); - // 100 Hz ODR: SensorLib's BMA423_StepDetector example runs the pedometer at - // 100 Hz, not the 50 Hz used here previously. Everything else already matched - // (NORMAL, FS_2G, OSR2_AVG2, CIC_AVG_MODE). Revert this one literal to 50.0f - // to A/B it against the watermark change above. - _accel->configAccelerometer(OperationMode::NORMAL, AccelFullScaleRange::FS_2G, - 100.0f, AccelBandwidth::OSR2_AVG2, AccelPerfMode::CIC_AVG_MODE); - // INT1 pin electrical config: level trigger, active high, push-pull, - // output enabled. INT1_IO_CTRL resets to output-disabled, so without - // this the pin never drives and INPUT_PULLDOWN reads low forever. - _accel->setInterruptPinConfig(InterruptPinMap::PIN1, false, false, true, false); - pinMode(PIN_ACCEL_IRQ, INPUT_PULLDOWN); - // Attach the edge ISR BEFORE enabling the tilt source, so the first - // assertion cannot occur before the handler is armed (a missed first edge - // on a self-clearing line otherwise locks tilt-wake out permanently). - attachInterrupt(digitalPinToInterrupt(PIN_ACCEL_IRQ), onTiltISR, RISING); - _accel->enableTiltDetector(true, true); - // Enable the hardware step counter via raw I2C (SensorLib's wrapper method - // does not compile in this build). It then counts in the BMA423 feature - // engine with no CPU cost, even while the display is off. - bma423EnableStepCounter(); - } - - esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_DEEPSLEEP) { - long wakeup_source = esp_sleep_get_ext1_wakeup_status(); - if (wakeup_source & (1 << P_LORA_DIO_1)) { - startup_reason = BD_STARTUP_RX_PACKET; - } - rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); - rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); - } -} - -bool TWatchS3Board::power_init() { - _axp = new XPowersAXP2101(Wire, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_ADDR_PMU); - PMU = _axp; // same object; see the note in TWatchS3Board.h - if (!PMU->init()) { - MESH_DEBUG_PRINTLN("Warning: Failed to find AXP2101 power management"); - delete _axp; - _axp = NULL; - PMU = NULL; - return false; - } - - PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); - - // Power rails per the T-Watch S3 PowerManage table, cross-checked against the - // schematic (rev 25-03-24): - // ALDO1 = unused, ALDO2 = display backlight, - // ALDO3 = display + touch, ALDO4 = LoRa (schematic net LDO4 -> HPD16B3 VCC), - // BLDO1 = unused (no GNSS), BLDO2 = DRV2605 haptic, - // DLDO1 = MAX98357A speaker amp VDD (schematic sheet 6, net SPK_VDD), - // VBACKUP = MS412FE rechargeable coin cell backing the PCF8563 RTC domain. - // - // LilyGo's hardware doc lists DLDO1 as unused. The schematic disagrees: it is - // the speaker rail. Meck compiles no audio, so it stays off, which fully - // unpowers the amp rather than merely idling it. - PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); // LoRa radio - PMU->enablePowerOutput(XPOWERS_ALDO4); - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // display + touch - PMU->enablePowerOutput(XPOWERS_ALDO3); - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // display backlight - PMU->enablePowerOutput(XPOWERS_ALDO2); - PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // DRV2605 haptic - PMU->enablePowerOutput(XPOWERS_BLDO2); - - PMU->disablePowerOutput(XPOWERS_DCDC2); - PMU->disablePowerOutput(XPOWERS_DCDC3); - PMU->disablePowerOutput(XPOWERS_DCDC4); - PMU->disablePowerOutput(XPOWERS_DCDC5); - PMU->disablePowerOutput(XPOWERS_ALDO1); // unused - PMU->disablePowerOutput(XPOWERS_BLDO1); // GNSS rail on the Plus; unpopulated here - PMU->disablePowerOutput(XPOWERS_DLDO1); // MAX98357A speaker amp -- audio not compiled in - PMU->disablePowerOutput(XPOWERS_DLDO2); - - // RTC backup cell. The PCF8563 has a single VDD pin (no separate battery - // input), and the schematic diode-ORs it against the MS412FE on J12, which is - // charged from the AXP2101 BACKUP pin. Leaving this off drains the cell with - // nothing to replenish it. 3300 mV matches LilyGo's own firmware. - // setPowerChannelVoltage/enablePowerOutput on XPOWERS_VBACKUP map onto - // setButtonBatteryChargeVoltage()/enableButtonBatteryCharge(). - PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); - PMU->enablePowerOutput(XPOWERS_VBACKUP); - - // PWR key. The side switch (schematic SW7) is wired to PWRON, not a GPIO. - // press < 1s -> PKEY_SHORT_IRQ, consumed by PMUButton as a click - // 1s <= press < 6s -> nothing (PKEY_LONG_IRQ is left masked) - // press >= 6s -> hardware power-off, firmware never sees it - // hold 2s from off -> power-on - // Matches the 2S ON / 6S OFF behaviour printed on LilyGo's own pin diagram. - PMU->setPowerKeyPressOnTime(XPOWERS_POWERON_2S); - PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_6S); - _axp->setIrqLevelTime(XPOWERS_AXP2101_IRQ_TIME_1S); // not on XPowersLibInterface - - PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - PMU->clearIrqStatus(); - // SHORT gives the click; NEGATIVE/POSITIVE are the press/release edges that - // back PMUButton::isPressed(). - PMU->enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ | - XPOWERS_AXP2101_PKEY_NEGATIVE_IRQ | - XPOWERS_AXP2101_PKEY_POSITIVE_IRQ); - - // 150 mA = 0.32C on the 470 mAh cell -- a modest bump over the 125 mA - // vendor default (LilyGoWatchS3.cpp uses 125), well under the 0.5C rule. - PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_150MA); - // The fitted cell (Tewaycell 552530, label verified) is 3.8V nominal LiHV, - // i.e. 4.35V max charge. LilyGoLib's init for this board sets 4V35 for the - // same reason. Charging to 4V2 forfeits the top ~12-15% of capacity. - PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V35); - - PMU->disableTSPinMeasure(); - PMU->enableSystemVoltageMeasure(); - PMU->enableVbusVoltageMeasure(); - PMU->enableBattVoltageMeasure(); - - Serial.printf("[PWR] rails: ALDO2(bl)=%d ALDO3(disp/touch)=%d ALDO4(LoRa)=%d BLDO2(haptic)=%d DLDO1(spk)=%d VBACKUP(rtc)=%d\n", - PMU->isPowerChannelEnable(XPOWERS_ALDO2), - PMU->isPowerChannelEnable(XPOWERS_ALDO3), - PMU->isPowerChannelEnable(XPOWERS_ALDO4), - PMU->isPowerChannelEnable(XPOWERS_BLDO2), - PMU->isPowerChannelEnable(XPOWERS_DLDO1), - PMU->isPowerChannelEnable(XPOWERS_VBACKUP)); - return true; -} - -void TWatchS3Board::printPowerDebug() { - if (!PMU) return; - Serial.printf("[PWR] batt=%dmV %d%% vbus=%dmV charging=%d cpu=%dMHz bt=%d\n", - PMU->getBattVoltage(), PMU->getBatteryPercent(), - PMU->getVbusVoltage(), PMU->isCharging(), - getCpuFrequencyMhz(), (int)esp_bt_controller_get_status()); -} - -bool TWatchS3Board::tiltFired() { - if (_tilt_flag) { // set by the GPIO14 rising-edge ISR - _tilt_flag = false; - _accel->update(); // reading the status clears the sensor INT - return true; - } - // TEMP diagnostic: periodically report the raw INT pin level and ISR count, - // to distinguish "pin stuck high / never re-arms" from "never asserts". - static unsigned long _tilt_dbg_next = 0; - if (millis() >= _tilt_dbg_next) { - _tilt_dbg_next = millis() + 5000; - Serial.printf("[TILT] pin=%d isr_count=%u\n", - digitalRead(PIN_ACCEL_IRQ), (unsigned)_tilt_isr_count); - } - return false; -} - -uint32_t TWatchS3Board::getStepCount() { - uint8_t d[4]; - if (!bma423ReadRegs(BMA423_REG_STEP_CNT_OUT, d, 4)) return 0; - return (uint32_t)d[0] | ((uint32_t)d[1] << 8) | - ((uint32_t)d[2] << 16) | ((uint32_t)d[3] << 24); -} \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/TWatchS3Board.h b/variants/lilygo_twatch_s3/TWatchS3Board.h deleted file mode 100644 index ec31e72b..00000000 --- a/variants/lilygo_twatch_s3/TWatchS3Board.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once - -#include "variant.h" // Board-specific pin definitions (I2C, addresses, IRQs) - -#include -#include -#include "XPowersLib.h" -#include "helpers/ESP32Board.h" -#include - -// LilyGo T-Watch S3 board (non-GPS, 470 mAh). -// -// Power is managed by an AXP2101 PMU on the main I2C bus. Two pointers to the -// same object are kept, because XPowersLib splits its API by access specifier: -// PMU (XPowersLibInterface*) -- the power-channel ops (setPowerChannelVoltage, -// enable/disablePowerOutput, isPowerChannelEnable) are PROTECTED on -// XPowersAXP2101 and only reachable through the interface. -// _axp (XPowersAXP2101*) -- setIrqLevelTime() and the PWRON press/release -// edges isPekeyNegativeIrq()/isPekeyPositiveIrq() are AXP2101-only and -// absent from the interface. -// Everything else is public on both. -class SensorBMA423; // full include kept in the .cpp to avoid a BLE-build header clash - -class TWatchS3Board : public ESP32Board { - XPowersLibInterface* PMU = NULL; - XPowersAXP2101* _axp = NULL; // same object as PMU, concrete type - SensorBMA423* _accel = nullptr; - static volatile bool _tilt_flag; - static volatile uint32_t _tilt_isr_count; // TEMP diagnostic - static void IRAM_ATTR onTiltISR(); // defined in the .cpp (IRAM relocation) - - bool power_init(); - -public: - void begin(); - - // Returns true once when the BMA423 tilt (wrist-raise) interrupt has fired. - bool tiltFired(); - - // The AXP2101 handle, for PMUButton. NULL if the PMU failed to init. - XPowersAXP2101* getPMU() { return _axp; } - - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - // NOTE: the PWR key is not a GPIO on this board, so it cannot be added to - // the ext1 mask. pin_wake_btn is accepted for signature compatibility with - // ESP32Board but callers on this variant pass -1. - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); - } else { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000ULL); - } - - esp_deep_sleep_start(); - } - - uint16_t getBattMilliVolts() override { - return PMU ? PMU->getBattVoltage() : 0; - } - - uint8_t getBatteryPercent() override { - return PMU ? PMU->getBatteryPercent() : 0; - } - - // Wrapper-free BMA423 step count (raw I2C; defined in the .cpp). - uint32_t getStepCount(); - - // Power-debug probe: battery, VBUS, CPU clock, BT controller state and live - // rail states. Called periodically from UITask::loop. - void printPowerDebug(); - - const char* getManufacturerName() const override { - return "LilyGo T-Watch S3"; - } -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/TWatchS3Display.h b/variants/lilygo_twatch_s3/TWatchS3Display.h deleted file mode 100644 index dd1ad140..00000000 --- a/variants/lilygo_twatch_s3/TWatchS3Display.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -// LovyanGFX display + touch for the LilyGo T-Watch S3. -// ST7789V 240x240 on SPI3 + PWM backlight (GPIO45) + FT6336U capacitive touch -// on a separate I2C bus (Wire1: SDA 39 / SCL 40, INT 16, no RST pin). -// -// LGFXDisplay.h pulls in LovyanGFX (it defines LGFX_USE_V1 and includes -// LovyanGFX.hpp), so we do not redefine those here. - -#include - -class LGFX_TWatchS3 : public lgfx::LGFX_Device { - lgfx::Panel_ST7789 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_FT5x06 _touch_instance; - -public: - LGFX_TWatchS3(void) { - { - auto cfg = _bus_instance.config(); - cfg.spi_host = SPI3_HOST; - cfg.spi_mode = 0; - cfg.freq_write = 40000000; - cfg.freq_read = 16000000; - cfg.spi_3wire = true; - cfg.use_lock = true; - cfg.dma_channel = SPI_DMA_CH_AUTO; - cfg.pin_sclk = 18; - cfg.pin_mosi = 13; - cfg.pin_miso = -1; - cfg.pin_dc = 38; - _bus_instance.config(cfg); - _panel_instance.setBus(&_bus_instance); - } - - { - auto cfg = _panel_instance.config(); - cfg.pin_cs = 12; - cfg.pin_rst = -1; - cfg.pin_busy = -1; - // ST7789 GRAM is 240x320; memory_height must be 320 so the 80px rotation - // offset is applied (otherwise a strip of the panel shows noise). - cfg.memory_width = 240; - cfg.memory_height = 320; - cfg.panel_width = 240; - cfg.panel_height = 240; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 1; - cfg.readable = false; - cfg.invert = true; - cfg.rgb_order = false; - cfg.dlen_16bit = false; - cfg.bus_shared = false; - _panel_instance.config(cfg); - } - - { - auto cfg = _light_instance.config(); - cfg.pin_bl = 45; - cfg.invert = false; - cfg.freq = 44100; - cfg.pwm_channel = 7; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); - } - - { - auto cfg = _touch_instance.config(); - cfg.x_min = 0; - cfg.x_max = 239; - cfg.y_min = 0; - cfg.y_max = 239; - cfg.pin_int = 16; - cfg.pin_rst = -1; - cfg.bus_shared = false; - cfg.offset_rotation = 2; // touch IC mounted 180deg vs the LCD horizontal axis - cfg.i2c_port = 1; - cfg.i2c_addr = 0x38; - cfg.pin_sda = 39; - cfg.pin_scl = 40; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); - } -}; - -class TWatchS3Display : public LGFXDisplay { - LGFX_TWatchS3 disp; -public: - TWatchS3Display() : LGFXDisplay(240, 240, disp) {} -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/WatchAlarmScreen.h b/variants/lilygo_twatch_s3/WatchAlarmScreen.h deleted file mode 100644 index 7a4d8d69..00000000 --- a/variants/lilygo_twatch_s3/WatchAlarmScreen.h +++ /dev/null @@ -1,430 +0,0 @@ -#pragma once - -// ============================================================================= -// WatchAlarmScreen -- vibrate-only alarm clock for the LilyGo T-Watch S3. -// -// The T-Deck Pro's AlarmScreen.h is bound to ESP32-audioI2S, an SD card and a -// 1-21 volume scale, none of which exist here. This is a separate, much smaller -// screen that keeps AlarmScreen's conventions (bit 0 = Sunday day-of-week mask, -// dowFromEpoch, the 3-buzz / 4s-pause vibrate cadence, effect 14) so the two -// stay recognisably related. -// -// Alarm is the DRV2605L on I2C 0x5A driving the 0827 coin motor. Its BLDO2 rail -// is already up from power_init(), so no motorEnable() call is needed. -// -// A ringing alarm is dismissed ONLY by the PWR key (which arrives as KEY_ENTER), -// or by the WATCH_ALARM_RINGING_MS timeout. Taps are swallowed, and UITask::loop -// holds this screen current and the display awake for the duration -- otherwise -// the buzzing trips the BMA423 tilt detector and raise-to-wake steals the screen. -// -// Config lives on the LittleFS partition mounted at /maps in main.cpp -- the -// same one the Plus uses for map tiles. The watch has no SD card. -// -// Geometry: the display is 240x240 physical, UI_ZOOM=2, so display.width() and -// display.height() both report 120 logical pixels. -// ============================================================================= - -// NOTE on include order: this header uses NodePrefs, which lives at -// examples/companion_radio/NodePrefs.h and is not on this variant's -I path. -// UITask.h pulls it in via "../NodePrefs.h", so WatchAlarmScreen.h must always -// be included *after* UITask.h. Both call sites (UITask.cpp, main.cpp) do. -// This mirrors how TWatchComposeScreens.h depends on ChannelScreen.h. -#include -#include -#include -#include // mesh::RTCClock -#include -#include -#include -#include "DRV2605Haptic.h" - -class UITask; - -#define WATCH_ALARM_SLOT_COUNT 4 -#define WATCH_ALARM_CFG_PATH "/alarms.cfg" -#define WATCH_ALARM_CFG_MAGIC 0x334B414CUL // "LAK3" -#define WATCH_ALARM_CFG_VERSION 1 - -// Day-of-week bitmask, bit 0 = Sunday (matches AlarmScreen.h) -#define WDOW_SUN (1 << 0) -#define WDOW_MON (1 << 1) -#define WDOW_TUE (1 << 2) -#define WDOW_WED (1 << 3) -#define WDOW_THU (1 << 4) -#define WDOW_FRI (1 << 5) -#define WDOW_SAT (1 << 6) -#define WDOW_ALL 0x7F -#define WDOW_WEEKDAYS (WDOW_MON | WDOW_TUE | WDOW_WED | WDOW_THU | WDOW_FRI) -#define WDOW_WEEKEND (WDOW_SAT | WDOW_SUN) - -// Vibrate cadence, identical to AlarmScreen's silent alarm on the T-Deck MAX. -#define WATCH_ALARM_EFFECT 14 // library 1: strong buzz, ~1s -#define WATCH_ALARM_BUZZ_MS 1200 // spacing between buzz starts -#define WATCH_ALARM_GROUP 3 // buzzes per group -#define WATCH_ALARM_PAUSE_MS 4000 // pause after each group -#define WATCH_ALARM_RINGING_MS 300000 // 5 minute auto-dismiss -#define WATCH_ALARM_FIRE_COOLDOWN 90 // seconds; do not re-fire the same slot - -struct WatchAlarmSlot { - bool enabled; - uint8_t hour; // 0-23 - uint8_t minute; // 0-59 - uint8_t days; // day-of-week bitmask; WDOW_ALL = every day -}; - -struct WatchAlarmConfig { - uint32_t magic; - uint8_t version; - uint8_t _pad[3]; - WatchAlarmSlot slots[WATCH_ALARM_SLOT_COUNT]; -}; - -class WatchAlarmScreen : public UIScreen { -public: - enum Mode { LIST, EDIT, RINGING }; - -private: - UITask* _task; - mesh::RTCClock* _rtc; - NodePrefs* _node_prefs; - - WatchAlarmConfig _cfg; - Mode _mode = LIST; - int _sel = 0; // selected slot in LIST / slot under EDIT - WatchAlarmSlot _edit; // working copy while editing - - // Ringing state - int _ringSlot = -1; - unsigned long _ringStartMs = 0; - unsigned long _vibNextMs = 0; - int _vibCount = 0; - - // Fire tracking - uint32_t _lastFireEpoch[WATCH_ALARM_SLOT_COUNT] = {0}; - - DRV2605Haptic _haptic; - bool _hapticReady = false; - - static int dowFromEpoch(uint32_t epoch) { - // 1970-01-01 was a Thursday (index 4 with 0 = Sunday) - return (int)((epoch / 86400 + 4) % 7); - } - - int32_t localNow() const { - uint32_t utc = _rtc->getCurrentTime(); - if (utc < 1700000000) return -1; // clock not set yet - return (int32_t)utc + ((int32_t)_node_prefs->utc_offset_hours * 3600); - } - - static const char* daysLabel(uint8_t d) { - if (d == WDOW_ALL) return "Daily"; - if (d == WDOW_WEEKDAYS) return "Mon-Fri"; - if (d == WDOW_WEEKEND) return "Sat/Sun"; - if (d == 0) return "Once"; - return "Custom"; - } - - static uint8_t nextDaysPreset(uint8_t d) { - if (d == WDOW_ALL) return WDOW_WEEKDAYS; - if (d == WDOW_WEEKDAYS) return WDOW_WEEKEND; - if (d == WDOW_WEEKEND) return 0; // Once - return WDOW_ALL; - } - - void loadDefaults() { - memset(&_cfg, 0, sizeof(_cfg)); - _cfg.magic = WATCH_ALARM_CFG_MAGIC; - _cfg.version = WATCH_ALARM_CFG_VERSION; - for (int i = 0; i < WATCH_ALARM_SLOT_COUNT; i++) { - _cfg.slots[i].enabled = false; - _cfg.slots[i].hour = 7; - _cfg.slots[i].minute = 0; - _cfg.slots[i].days = WDOW_ALL; - } - } - -public: - WatchAlarmScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* node_prefs) - : _task(task), _rtc(rtc), _node_prefs(node_prefs) { - loadDefaults(); - } - - // Read the config from LittleFS. Called once from UITask::begin() after the - // /maps partition is mounted. A missing or malformed file leaves the defaults. - void load() { - File f = LittleFS.open(WATCH_ALARM_CFG_PATH, "r"); - if (!f) return; - WatchAlarmConfig tmp; - size_t n = f.read((uint8_t*)&tmp, sizeof(tmp)); - f.close(); - if (n != sizeof(tmp)) return; - if (tmp.magic != WATCH_ALARM_CFG_MAGIC) return; - if (tmp.version != WATCH_ALARM_CFG_VERSION) return; - for (int i = 0; i < WATCH_ALARM_SLOT_COUNT; i++) { - if (tmp.slots[i].hour > 23 || tmp.slots[i].minute > 59) return; // reject junk - } - _cfg = tmp; - } - - void save() { - File f = LittleFS.open(WATCH_ALARM_CFG_PATH, "w"); - if (!f) { - Serial.println("WatchAlarmScreen: save FAILED (LittleFS)"); - return; - } - f.write((const uint8_t*)&_cfg, sizeof(_cfg)); - f.close(); - } - - Mode getMode() const { return _mode; } - bool isRinging() const { return _mode == RINGING; } - - void enter() { _mode = LIST; } - - // --------------------------------------------------------------------------- - // Firing. Driven from UITask::loop() so an alarm still fires with the display - // off or another screen showing. Returns true on the loop where it fires. - // --------------------------------------------------------------------------- - bool tick() { - if (_mode == RINGING) { - driveVibrate(); - if (millis() - _ringStartMs > WATCH_ALARM_RINGING_MS) dismiss(); - return false; - } - - int32_t local = localNow(); - if (local < 0) return false; - - int hrs = (local / 3600) % 24; if (hrs < 0) hrs += 24; - int mins = (local / 60) % 60; if (mins < 0) mins += 60; - int dow = dowFromEpoch((uint32_t)local); - uint32_t nowEpoch = (uint32_t)local; - - for (int i = 0; i < WATCH_ALARM_SLOT_COUNT; i++) { - const WatchAlarmSlot& s = _cfg.slots[i]; - if (!s.enabled) continue; - if (s.hour != hrs || s.minute != mins) continue; - if (s.days != 0 && !(s.days & (1 << dow))) continue; // days==0 means Once - if (nowEpoch - _lastFireEpoch[i] < WATCH_ALARM_FIRE_COOLDOWN) continue; - - _lastFireEpoch[i] = nowEpoch; - if (_cfg.slots[i].days == 0) { // one-shot: disarm after firing - _cfg.slots[i].enabled = false; - save(); - } - startRinging(i); - return true; - } - return false; - } - - void startRinging(int slot) { - _mode = RINGING; - _ringSlot = slot; - _ringStartMs = millis(); - _vibNextMs = 0; - _vibCount = 0; - } - - void dismiss() { - if (_hapticReady) { - _haptic.standby(); // drop DRV2605 back to ~1.8uA once the alarm ends - _hapticReady = false; // force begin() (exit standby) on the next alarm - } - _mode = LIST; - _ringSlot = -1; - } - -private: - void driveVibrate() { - if (!_hapticReady) { - _hapticReady = _haptic.begin(); // BLDO2 is already up; no motorEnable() - if (!_hapticReady) return; - } - if (millis() < _vibNextMs) return; - - _haptic.buzz(WATCH_ALARM_EFFECT); - if (++_vibCount >= WATCH_ALARM_GROUP) { - _vibCount = 0; - _vibNextMs = millis() + WATCH_ALARM_PAUSE_MS; - } else { - _vibNextMs = millis() + WATCH_ALARM_BUZZ_MS; - } - } - - // --------------------------------------------------------------------------- - // Rendering. 120x120 logical. - // --------------------------------------------------------------------------- - static const int ROW_Y0 = 22; // first list row top - static const int ROW_H = 22; // list row height - static const int TOGGLE_X = 88; // left edge of the ON/OFF badge column - - void renderList(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 6, "Alarms"); - - char buf[16]; - for (int i = 0; i < WATCH_ALARM_SLOT_COUNT; i++) { - int y = ROW_Y0 + i * ROW_H; - const WatchAlarmSlot& s = _cfg.slots[i]; - - if (i == _sel) { - d->setRawColor(0x231D); // same blue as the home tile - d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3); - } - - d->setRawColor(s.enabled ? 0xFFFF : 0x7BEF); // white when armed, grey when off - snprintf(buf, sizeof(buf), "%02d:%02d", s.hour, s.minute); - display.setCursor(5, y + 3); - display.print(buf); - - d->setRawColor(0x7BEF); - d->printSmallFont(5, y + 14, daysLabel(s.days)); - - d->setRawColor(s.enabled ? 0xC618 : 0x4208); - d->printSmallFont(TOGGLE_X + 4, y + 8, s.enabled ? "ON" : "OFF"); - } - - display.setColor(DisplayDriver::LIGHT); - d->setRawColor(0x7BEF); - d->printSmallFont(4, display.height() - 7, "Tap time:edit ON/OFF:arm"); - } - - void renderEdit(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 4, "Edit alarm"); - - // Up arrows / value / down arrows. Hour occupies x 10..54, minute 66..110. - d->setRawColor(0xC618); - display.drawTextCentered(32, 22, "+"); - display.drawTextCentered(88, 22, "+"); - display.drawTextCentered(32, 74, "-"); - display.drawTextCentered(88, 74, "-"); - - char buf[8]; - d->setRawColor(0xFFFF); - display.setTextSize(2); - snprintf(buf, sizeof(buf), "%02d", _edit.hour); - display.drawTextCentered(32, 44, buf); - snprintf(buf, sizeof(buf), "%02d", _edit.minute); - display.drawTextCentered(88, 44, buf); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 48, ":"); - - // Footer: left half cycles the day preset, right half saves. - d->setRawColor(0x231D); - d->drawRoundRect(1, 92, 58, 18, 3); - d->drawRoundRect(61, 92, 58, 18, 3); - d->setRawColor(0xC618); - d->printSmallFont(6, 100, daysLabel(_edit.days)); - d->setRawColor(0xFFFF); - d->printSmallFont(78, 100, "Save"); - } - - void renderRinging(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - char buf[8]; - - d->setRawColor(0xFFFF); - if (_ringSlot >= 0) { - snprintf(buf, sizeof(buf), "%02d:%02d", _cfg.slots[_ringSlot].hour, - _cfg.slots[_ringSlot].minute); - } else { - strcpy(buf, "--:--"); - } - d->printClockFont(display.width() / 2, display.height() / 2 - 22, buf); - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, display.height() - 24, "ALARM"); - ((LGFXDisplay*)&display)->printSmallFont(14, display.height() - 10, "Press PWR to dismiss"); - } - -public: - int render(DisplayDriver& display) override { - switch (_mode) { - case LIST: renderList(display); return 500; - case EDIT: renderEdit(display); return 200; - case RINGING: renderRinging(display); return 250; - } - return 500; - } - - // Physical tap in logical (display.width()) coordinates. Returns true if the - // tap was consumed. The status-bar strip is handled by main.cpp before this. - bool handleTap(int lx, int ly) { - // Ringing: swallow taps. Only the PWR key dismisses (see handleInput). - if (_mode == RINGING) return true; - - if (_mode == LIST) { - for (int i = 0; i < WATCH_ALARM_SLOT_COUNT; i++) { - int y = ROW_Y0 + i * ROW_H; - if (ly < y || ly >= y + ROW_H - 2) continue; - if (lx >= TOGGLE_X) { // ON/OFF badge: arm / disarm - _cfg.slots[i].enabled = !_cfg.slots[i].enabled; - _sel = i; - save(); - } else { // time area: open the editor - _sel = i; - _edit = _cfg.slots[i]; - _mode = EDIT; - } - return true; - } - return false; - } - - // EDIT - if (ly >= 92) { // footer - if (lx < 60) { - _edit.days = nextDaysPreset(_edit.days); - } else { - commitEdit(); - } - return true; - } - bool hourCol = (lx < display_mid()); - if (ly < 40) { // upper half: increment - if (hourCol) _edit.hour = (_edit.hour + 1) % 24; - else _edit.minute = (_edit.minute + 1) % 60; - return true; - } - if (ly >= 62) { // lower half: decrement - if (hourCol) _edit.hour = (_edit.hour + 23) % 24; - else _edit.minute = (_edit.minute + 59) % 60; - return true; - } - return false; - } - - bool handleInput(char c) override { - // The PMU short press arrives as KEY_ENTER. - if (_mode == RINGING) { - if (c == KEY_ENTER) dismiss(); // PWR key only - return true; // swallow everything else - } - if (_mode == EDIT && c == KEY_ENTER) { commitEdit(); return true; } - if (_mode == LIST && c == KEY_ENTER) { - _edit = _cfg.slots[_sel]; - _mode = EDIT; - return true; - } - return false; - } - -private: - int display_mid() const { return 60; } // half of the 120px logical width - - void commitEdit() { - _cfg.slots[_sel] = _edit; - _cfg.slots[_sel].enabled = true; // saving an edit arms the alarm - save(); - _mode = LIST; - } -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/meck_twatch_16mb.csv b/variants/lilygo_twatch_s3/meck_twatch_16mb.csv deleted file mode 100644 index f14bfb43..00000000 --- a/variants/lilygo_twatch_s3/meck_twatch_16mb.csv +++ /dev/null @@ -1,21 +0,0 @@ -# Meck -- LilyGo T-Watch S3 16 MB custom partition layout -# -# Single factory app slot (no OTA): the watch has no SD card and does not -# define MECK_OTA_UPDATE, so no firmware-OTA path is compiled -- an OTA -# app1 slot would only waste flash. Maps get a dedicated LittleFS partition; -# the DataStore keeps its own SPIFFS partition. -# -# Both data partitions use the 'spiffs' subtype (the standard ESP32 data -# subtype). The DataStore is mounted as SPIFFS via SPIFFS.begin() (first -# spiffs-subtype partition by offset); maps is mounted as LittleFS by the -# explicit "maps" label, so the mount type is chosen at mount time, not by -# subtype. -# -# NOTE: flashing this shifts the layout and wipes the existing SPIFFS -- -# re-provision identity/contacts after flashing. -# -# Name, Type, SubType, Offset, Size -nvs, data, nvs, 0x9000, 0x5000 -app0, app, factory, 0x10000, 0x200000 -spiffs, data, spiffs, 0x210000, 0x400000 -maps, data, spiffs, 0x610000, 0x9F0000 diff --git a/variants/lilygo_twatch_s3/platformio.ini b/variants/lilygo_twatch_s3/platformio.ini deleted file mode 100644 index 0feae813..00000000 --- a/variants/lilygo_twatch_s3/platformio.ini +++ /dev/null @@ -1,140 +0,0 @@ -[LilyGo_TWatchS3] -extends = esp32_base -extra_scripts = post:merge_firmware.py -board = lilygo_twatch_s3 -board_build.flash_mode = qio -board_build.f_flash = 80000000L -board_build.arduino.memory_type = qio_opi -board_upload.flash_size = 16MB -board_build.partitions = variants/lilygo_twatch_s3/meck_twatch_16mb.csv -build_flags = - ${esp32_base.build_flags} - ${sensor_base.build_flags} - -I variants/lilygo_twatch_s3 - ; MECK_TWATCH gates the watch form factor (240x240, UI_ZOOM=2, touch, lock - ; screen, tile grid, grey palette) and is shared with the S3 Plus. - ; LILYGO_TWATCH_S3 gates hardware unique to this board. - -D MECK_TWATCH - -D LILYGO_TWATCH_S3 - ; The only key is the AXP2101 PWRON. Replaces PIN_USER_BTN, which is undefined - ; here because there is no GPIO button (see variant.h). - -D MECK_PMU_BUTTON - -D TWATCH_COMPOSE_ENABLED - -D BOARD_HAS_PSRAM=1 - -D CORE_DEBUG_LEVEL=1 - -D FORMAT_SPIFFS_IF_FAILED=1 - -D FORMAT_LITTLEFS_IF_FAILED=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 - ; ---- LoRa SX1262 ---- - -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 - -D SX126X_DIO2_AS_RF_SWITCH - -D SX126X_DIO3_TCXO_VOLTAGE=1.8f - -D SX126X_CURRENT_LIMIT=140 - -D SX126X_RX_BOOSTED_GAIN=1 - -D P_LORA_NSS=5 - -D P_LORA_DIO_1=9 - -D P_LORA_RESET=8 - -D P_LORA_BUSY=7 - -D P_LORA_SCLK=3 - -D P_LORA_MISO=4 - -D P_LORA_MOSI=1 - ; ---- Display (ST7789V 240x240) + FT6336U touch via LovyanGFX ---- - -D DISPLAY_CLASS=TWatchS3Display - -D UI_ZOOM=2 - ; ---- Misc ---- - -D AUTO_SHUTDOWN_MILLIVOLTS=3100 - -D ARDUINO_LOOP_STACK_SIZE=32768 - ; ---- No GPS ---- - ; HAS_GPS is deliberately left undefined, which drops the watch map screen. - ; ENV_INCLUDE_GPS must be explicitly zeroed: sensor_base defaults it to 1, and - ; it gates gpsStream, board.gpsPowerOn/Off and the GPS home page. - -D ENV_INCLUDE_GPS=0 - -D ENV_INCLUDE_AHTX0=0 - -D ENV_INCLUDE_BME280=0 - -D ENV_INCLUDE_BMP280=0 - -D ENV_INCLUDE_SHTC3=0 - -D ENV_INCLUDE_SHT4X=0 - -D ENV_INCLUDE_LPS22HB=0 - -D ENV_INCLUDE_INA3221=0 - -D ENV_INCLUDE_INA219=0 - -D ENV_INCLUDE_INA226=0 - -D ENV_INCLUDE_INA260=0 - -D ENV_INCLUDE_MLX90614=0 - -D ENV_INCLUDE_VL53L0X=0 - -D ENV_INCLUDE_BME680=0 - -D ENV_INCLUDE_BMP085=0 -build_src_filter = ${esp32_base.build_src_filter} - +<../variants/lilygo_twatch_s3> - + -lib_deps = - ${esp32_base.lib_deps} - ${sensor_base.lib_deps} - lovyan03/LovyanGFX @ ^1.2.0 - lewisxhe/XPowersLib @ ^0.2.7 - lewisxhe/SensorLib @ ^0.4.1 - adafruit/Adafruit GFX Library @ ^1.11.0 - bitbank2/PNGdec @ ^1.0.1 - WebServer - DNSServer - Update - -; --------------------------------------------------------------------------- -; Standalone (no BLE companion), touch + display + lock screen. -; MAX_CONTACTS=2000 -- contact + sort arrays allocated in PSRAM via -; BaseChatMesh::initContacts(). -; --------------------------------------------------------------------------- -[env:meck_twatch_s3_standalone] -extends = LilyGo_TWatchS3 -build_flags = - ${LilyGo_TWatchS3.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=2000 - -D MAX_GROUP_CHANNELS=40 - -D OFFLINE_QUEUE_SIZE=1 - -D ESP32_CPU_FREQ=80 - -D FIRMWARE_VERSION='"Meck TWatch S3 v0.3"' -build_src_filter = ${LilyGo_TWatchS3.build_src_filter} - + - - - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> - + -lib_deps = - ${LilyGo_TWatchS3.lib_deps} - densaugeo/base64 @ ~1.4.0 -lib_ignore = - AsyncTCP - ESPAsyncWebServer - ESP32 BLE Arduino - -; --------------------------------------------------------------------------- -; BLE companion build -- same touch UI as the standalone, plus the BLE serial -; interface so a phone app can connect on occasion. -; Flash: pio run -e meck_twatch_s3_ble -t upload -; --------------------------------------------------------------------------- -[env:meck_twatch_s3_ble] -extends = LilyGo_TWatchS3 -build_flags = - ${LilyGo_TWatchS3.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=2000 - -D MAX_GROUP_CHANNELS=20 - -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=256 - -D ESP32_CPU_FREQ=80 - -D FIRMWARE_VERSION='"Meck TWatch S3 BLE v0.3"' -build_src_filter = ${LilyGo_TWatchS3.build_src_filter} - + - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> - + -lib_deps = - ${LilyGo_TWatchS3.lib_deps} - densaugeo/base64 @ ~1.4.0 -lib_ignore = - AsyncTCP - ESPAsyncWebServer \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/target.cpp b/variants/lilygo_twatch_s3/target.cpp deleted file mode 100644 index 76ba036a..00000000 --- a/variants/lilygo_twatch_s3/target.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include "variant.h" -#include "target.h" - -TWatchS3Board board; - -// LoRa SX1262 (HPD16B3 module) on its own SPI bus (the display uses SPI3_HOST -// separately). -static SPIClass loraSpi; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi); - -WRAPPER_CLASS radio_driver(radio, board); - -ESP32RTCClock fallback_clock; -AutoDiscoverRTCClock rtc_clock(fallback_clock); - -// No GNSS and no environment sensors on this board, so the base SensorManager -// is used. node_lat/node_lon stay at 0 and advert location policy has nothing -// to publish. -SensorManager sensors; - -#ifdef DISPLAY_CLASS - DISPLAY_CLASS display; - PMUButton user_btn(board); -#endif - -bool radio_init() { - // NOTE: board.begin() is called by main.cpp setup() before radio_init(); - // Wire is already initialised there with the correct pins. - fallback_clock.begin(); - rtc_clock.begin(Wire); - - loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); - return radio.std_init(&loraSpi); -} - -uint32_t radio_get_rng_seed() { - return radio.random(0x7FFFFFFF); -} - -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { - radio.setFrequency(freq); - radio.setSpreadingFactor(sf); - radio.setBandwidth(bw); - radio.setCodingRate(cr); - - // Longer preamble for low SF improves reliability -- each symbol is shorter - // at low SF, so more symbols are needed for reliable detection. - uint16_t preamble = (sf <= 9) ? 32 : 16; - radio.setPreambleLength(preamble); -} - -void radio_set_tx_power(uint8_t dbm) { - radio.setOutputPower(dbm); -} - -mesh::LocalIdentity radio_new_identity() { - RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); -} - -void radio_reset_agc() { - // Power-saving RX gain on the watch (deliberate): boosted gain costs - // ~0.6-0.7 mA continuously for ~+2 dB sensitivity. The SetRxGain write - // itself is what resets the AGC; false selects the power-saving value. - radio.setRxBoostedGainMode(false); -} \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/target.h b/variants/lilygo_twatch_s3/target.h deleted file mode 100644 index 9a7a6b98..00000000 --- a/variants/lilygo_twatch_s3/target.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -// Include variant.h first to ensure all board-specific defines are available -#include "variant.h" - -#define RADIOLIB_STATIC_ONLY 1 -#include -#include -#include -#include -#include -#include - -#ifdef DISPLAY_CLASS - #include - #include -#endif - -extern TWatchS3Board board; -extern WRAPPER_CLASS radio_driver; -extern AutoDiscoverRTCClock rtc_clock; -extern SensorManager sensors; - -#ifdef DISPLAY_CLASS - extern DISPLAY_CLASS display; - // Not a MomentaryButton: the only key on this board is the AXP2101 PWRON. - extern PMUButton user_btn; -#endif - -bool radio_init(); -uint32_t radio_get_rng_seed(); -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); -void radio_set_tx_power(uint8_t dbm); -mesh::LocalIdentity radio_new_identity(); -void radio_reset_agc(); diff --git a/variants/lilygo_twatch_s3/variant.h b/variants/lilygo_twatch_s3/variant.h deleted file mode 100644 index 57bafc0a..00000000 --- a/variants/lilygo_twatch_s3/variant.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -// ============================================================================= -// LilyGo T-Watch S3 (non-GPS, 470 mAh) - Board-level pin definitions -// -// Sources, in order of authority: -// 1. T_WATCH-S3 schematic (rev 25-03-24) -// 2. Xinyuan-LilyGO/TTGO_TWatch_Library, branch t-watch-s3, src/utilities.h -// 3. LilyGo hardware doc docs/hardware/lilygo-t-watch-s3.md -// -// Every pin below is identical to the T-Watch S3 Plus. The two boards differ in -// what is *populated*, not in how the ESP32-S3 is wired: -// - no onboard GNSS (the Plus has a MIA-M10Q on BLDO1; here BLDO1 is unused -// and GPIO 41/42 go to an optional external GPS shield) -// - no GPIO user button; the only control is the side PWR key, which is wired -// to the AXP2101 PWRON pin (schematic sheet 1: SW7 -> PWR_KEY -> pin 30) -// - MAX98357A speaker amp on DLDO1, PDM mic on +3V3, IR LED on GPIO2 -- none -// of which Meck compiles in -// -// NOTE: LoRa (P_LORA_*) pins and the SX126x radio parameters are supplied as -// -D build flags in this variant's platformio.ini, matching the T-Watch S3 Plus -// and T-Deck Pro convention. The display SPI/backlight and FT6336U touch pins -// live inline in TWatchS3Display.h because LovyanGFX needs them in its panel -// config. -// ============================================================================= - -// ----------------------------------------------------------------------------- -// Main I2C bus (shared): AXP2101 PMU, PCF8563 RTC, BMA423 accel, DRV2605 haptic -// The FT6336U touch panel is on a SEPARATE bus (Wire1), configured in the -// display class -- it is not on this bus. -// ----------------------------------------------------------------------------- -#define I2C_SDA 10 -#define I2C_SCL 11 - -// Aliases for ESP32Board base class compatibility -#define PIN_BOARD_SDA I2C_SDA -#define PIN_BOARD_SCL I2C_SCL - -// ----------------------------------------------------------------------------- -// I2C device addresses (7-bit) -// ----------------------------------------------------------------------------- -#define I2C_ADDR_PMU 0x34 // AXP2101 power management -#define I2C_ADDR_RTC 0x51 // PCF8563 real-time clock -#define I2C_ADDR_ACCEL 0x19 // BMA423 accelerometer -#define I2C_ADDR_HAPTIC 0x5A // DRV2605 haptic driver -#define I2C_ADDR_TOUCH 0x38 // FT6336U capacitive touch (on Wire1) - -// ----------------------------------------------------------------------------- -// Interrupt / control pins -// ----------------------------------------------------------------------------- -#ifndef PIN_PMU_IRQ - #define PIN_PMU_IRQ 21 // AXP2101 interrupt (open-drain, active LOW) -#endif -#define PIN_RTC_IRQ 17 // PCF8563 interrupt -#define PIN_ACCEL_IRQ 14 // BMA423 interrupt - -// User button: deliberately NOT defined. -// -// The T-Watch S3 has no GPIO button. Schematic sheet 1 wires the side tact -// switch SW7 to net PWR_KEY, which lands on AXP2101 pin 30 (PWRON). Button -// events therefore arrive over I2C as PMU interrupts, not as a pin level, and -// are read by PMUButton (see PMUButton.h). MECK_PMU_BUTTON is defined in this -// variant's platformio.ini in place of PIN_USER_BTN. -// -// #define PIN_USER_BTN - -// ----------------------------------------------------------------------------- -// Display dimensions (ST7789V 1.54" IPS, 240x240) -// ----------------------------------------------------------------------------- -#define LCD_HOR_SIZE 240 -#define LCD_VER_SIZE 240 - -// ----------------------------------------------------------------------------- -// Storage -// ----------------------------------------------------------------------------- -// The T-Watch S3 has no SD card slot. Notes/Reader/Epub screens reference -// SDCARD_CS unconditionally, so it must be defined for them to compile. -1 is a -// safe no-op on ESP32 (digitalWrite/pinMode reject out-of-range pins), and SD -// mounts will simply fail at runtime. -#define SDCARD_CS -1 - -// ----------------------------------------------------------------------------- -// GPS: none. HAS_GPS and ENV_INCLUDE_GPS are left undefined / zeroed in -// platformio.ini, which drops the map screen, the GPS home page and the BLDO1 -// rail control. The optional external GPS shield lands on GPIO41 (RX) / -// GPIO42 (TX) if it is ever wired up. -// ----------------------------------------------------------------------------- -// #define HAS_GPS 1 -// #define GPS_BAUDRATE 38400 -// #define GPS_RX_PIN 41 -// #define GPS_TX_PIN 42 - -// Fallback for code that references GPS_BAUDRATE without a HAS_GPS guard -// (e.g. MyMesh.cpp "gps.baud" CLI command) -#ifndef GPS_BAUDRATE -#define GPS_BAUDRATE 9600 -#endif diff --git a/variants/lilygo_twatch_s3_plus/CPUPowerManager.h b/variants/lilygo_twatch_s3_plus/CPUPowerManager.h deleted file mode 100644 index 444a90bd..00000000 --- a/variants/lilygo_twatch_s3_plus/CPUPowerManager.h +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include - -// CPU Frequency Scaling for ESP32-S3 -// -// Typical current draw (CPU only, rough): -// 240 MHz ~70-80 mA -// 160 MHz ~50-60 mA -// 80 MHz ~30-40 mA -// 40 MHz ~15-20 mA (low-power / lock screen mode) -// -// SPI peripherals and UART use their own clock dividers from the APB clock, -// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz. - -#ifdef ESP32 - -#ifndef CPU_FREQ_IDLE -#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening -#endif - -#ifndef CPU_FREQ_BOOST -#define CPU_FREQ_BOOST 240 // MHz — heavy processing -#endif - -#ifndef CPU_FREQ_LOW_POWER -#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C) -#endif - -#ifndef CPU_BOOST_TIMEOUT_MS -#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds -#endif - -class CPUPowerManager { -public: - CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {} - - void begin() { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - _boosted = false; - _lowPower = false; - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - - void loop() { - if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { - // Return to low-power if locked, otherwise normal idle - if (_lowPower) { - setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); - MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER); - } else { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - _boosted = false; - } - } - - void setBoost() { - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_BOOST); - _boosted = true; - MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST); - } - _boost_started = millis(); - } - - void setIdle() { - if (_boosted) { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - _boosted = false; - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); - } - if (_lowPower) { - _lowPower = false; - } - } - - // Low-power mode — drops CPU to 40 MHz for lock screen standby. - // If currently boosted, the boost timeout will return to 40 MHz - // instead of 80 MHz. - void setLowPower() { - _lowPower = true; - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); - MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER); - } - // If boosted, the loop() timeout will drop to low-power instead of idle - } - - // Exit low-power mode — returns to normal idle (80 MHz). - // If currently boosted, the boost timeout will return to idle - // instead of low-power. - void clearLowPower() { - _lowPower = false; - if (!_boosted) { - setCpuFrequencyMhz(CPU_FREQ_IDLE); - MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE); - } - // If boosted, the loop() timeout will drop to idle as normal - } - - bool isBoosted() const { return _boosted; } - bool isLowPower() const { return _lowPower; } - uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } - -private: - bool _boosted; - bool _lowPower; - unsigned long _boost_started; -}; - -#endif // ESP32 \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/GPSStreamCounter.h b/variants/lilygo_twatch_s3_plus/GPSStreamCounter.h deleted file mode 100644 index 5992fb71..00000000 --- a/variants/lilygo_twatch_s3_plus/GPSStreamCounter.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include - -// Transparent Stream wrapper that counts NMEA sentences (newline-delimited) -// flowing from the GPS serial port to the MicroNMEA parser. -// -// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock); -// Use: GPSStreamCounter gpsStream(Serial2); -// MicroNMEALocationProvider gps(gpsStream, &rtc_clock); -// -// Every read() call passes through to the underlying stream; when a '\n' -// is seen the sentence counter increments. This lets the UI display a -// live "nmea" count so users can confirm the baud rate is correct and -// the GPS module is actually sending data. - -class GPSStreamCounter : public Stream { -public: - GPSStreamCounter(Stream& inner) - : _inner(inner), _sentences(0), _sentences_snapshot(0), - _last_snapshot(0), _sentences_per_sec(0) {} - - // --- Stream read interface (passes through) --- - int available() override { return _inner.available(); } - int peek() override { return _inner.peek(); } - - int read() override { - int c = _inner.read(); - if (c == '\n') { - _sentences++; - } - return c; - } - - // --- Stream write interface (pass through for NMEA commands if needed) --- - size_t write(uint8_t b) override { return _inner.write(b); } - - // --- Sentence counting API --- - - // Total sentences received since boot (or last reset) - uint32_t getSentenceCount() const { return _sentences; } - - // Sentences received per second (updated each time you call it, - // with a 1-second rolling window) - uint16_t getSentencesPerSec() { - unsigned long now = millis(); - unsigned long elapsed = now - _last_snapshot; - if (elapsed >= 1000) { - uint32_t delta = _sentences - _sentences_snapshot; - // Scale to per-second if interval wasn't exactly 1000ms - _sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed); - _sentences_snapshot = _sentences; - _last_snapshot = now; - } - return _sentences_per_sec; - } - - // Reset all counters (e.g. when GPS hardware power cycles) - void resetCounters() { - _sentences = 0; - _sentences_snapshot = 0; - _sentences_per_sec = 0; - _last_snapshot = millis(); - } - -private: - Stream& _inner; - volatile uint32_t _sentences; - uint32_t _sentences_snapshot; - unsigned long _last_snapshot; - uint16_t _sentences_per_sec; -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h deleted file mode 100644 index d626c589..00000000 --- a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h +++ /dev/null @@ -1,795 +0,0 @@ -#pragma once -// ============================================================================= -// TWatchComposeScreens — touch keyboard compose system for the LilyGo -// T-Watch S3 Plus (ST7789 240x240, capacitive touch, companion_radio ui-new). -// -// Three UIScreen subclasses, compiled only when TWATCH_COMPOSE_ENABLED is -// defined (companion build only). All drawing and hit-testing is in the -// LGFXDisplay virtual coordinate space, which is 120x120 on this build -// (240px panel / UI_ZOOM=2). getTouch() returns coords already divided by -// UI_ZOOM, so a tap coordinate is directly comparable to draw coordinates. -// -// TWatchChannelPicker -- long-press on the home screen opens this. A single -// tap selects the channel row under the finger; a -// long-press on a row opens it; the back arrow -// (top-left) returns home. -// TWatchChannelScreen -- shows the most recent messages (sent + received) -// for the selected channel, read live from the shared -// ChannelScreen history store. Tap a line to ticker- -// scroll it; tap the compose bar to open the keyboard; -// back arrow returns home. -// TWatchKeyboardScreen — on-screen QWERTY. Bottom-left mode key cycles -// lower -> UPPER -> SYM. Bottom row is mode | space | -// enter | backspace. Enter sends on the channel; back -// arrow returns home. -// -// Each screen reads getTouch() itself in poll() and does its own release-edge -// detection, so actions fire on touch release (finger up) — this keeps the -// held finger from carrying into the next screen after a transition. Send/exit -// are delegated to UITask via the consume/flag pattern; message history and -// unread counts are read from the shared ChannelScreen store. -// ============================================================================= - -#ifdef TWATCH_COMPOSE_ENABLED - -#include -#include -#include -#include -#include -#include "ChannelScreen.h" // shared message history store (-I examples/companion_radio/ui-new) - -// ---- Tunables --------------------------------------------------------------- -#define TW_LONG_PRESS_MS 600 // hold to select a channel in the picker -#define TW_OUT_BUF_LEN 134 // MeshCore per-channel msg cap (~133) + NUL -#define TW_TICKER_MS_PER_PX 20 // channel-screen ticker scroll speed (ms per pixel) -#define TW_CH_NAME_LEN 32 -#define TW_PICKER_MAX 20 // matches MAX_GROUP_CHANNELS - -// ============================================================================= -// TWatchChannelPicker -// ============================================================================= -class TWatchChannelPicker : public UIScreen { - DisplayDriver* _display; - - struct Entry { uint8_t idx; char name[TW_CH_NAME_LEN]; }; - Entry _channels[TW_PICKER_MAX]; - uint8_t _numChannels; - uint8_t _highlighted; - uint8_t _scrollTop; - ChannelScreen* _store; // unread badge source - - // touch edge state - bool _touchDown; - int _downX, _downY; - unsigned long _downAt; - - // cross-screen flags (UITask polls these) - bool _confirmed; - bool _wantsExit; - - static const int HEADER_H = 14; - static const int ROW_H = 14; - - int visibleRows() const { - int h = _display ? _display->height() : 120; - int rows = (h - HEADER_H) / ROW_H; - return rows < 1 ? 1 : rows; - } - void ensureVisible() { - int vis = visibleRows(); - if (_highlighted < _scrollTop) _scrollTop = _highlighted; - else if (_highlighted >= _scrollTop + vis) _scrollTop = (uint8_t)(_highlighted - vis + 1); - } - void moveUp() { - if (_numChannels == 0) return; - _highlighted = (_highlighted == 0) ? (uint8_t)(_numChannels - 1) : (uint8_t)(_highlighted - 1); - ensureVisible(); - } - void moveDown() { - if (_numChannels == 0) return; - _highlighted = (uint8_t)((_highlighted + 1) % _numChannels); - ensureVisible(); - } - // Map a draw-space y coordinate to a channel index, or -1 if the tap is - // above the list or on an empty row. getTouch() returns coords already - // divided by UI_ZOOM, so _downY is directly comparable to the row layout. - int rowAtY(int y) const { - if (y < HEADER_H) return -1; - int i = (y - HEADER_H) / ROW_H; - if (i < 0 || i >= visibleRows()) return -1; - int ci = _scrollTop + i; - if (ci >= _numChannels) return -1; - return ci; - } - -public: - TWatchChannelPicker(DisplayDriver* display) - : _display(display), _numChannels(0), _highlighted(0), _scrollTop(0), - _store(nullptr), - _touchDown(false), _downX(0), _downY(0), _downAt(0), - _confirmed(false), _wantsExit(false) { - memset(_channels, 0, sizeof(_channels)); - } - - // Shared history store, used for the per-channel unread badges. - void setStore(ChannelScreen* store) { _store = store; } - - // Called by UITask before showing the screen. - void beginChannelSelect() { - _numChannels = 0; _highlighted = 0; _scrollTop = 0; - _touchDown = false; _confirmed = false; _wantsExit = false; - } - void addChannel(uint8_t idx, const char* name) { - if (_numChannels >= TW_PICKER_MAX) return; - _channels[_numChannels].idx = idx; - strncpy(_channels[_numChannels].name, name ? name : "", TW_CH_NAME_LEN - 1); - _channels[_numChannels].name[TW_CH_NAME_LEN - 1] = 0; - _numChannels++; - } - - // UITask bridges - bool isConfirmed() const { return _confirmed; } - void acknowledgeConfirm() { _confirmed = false; } - uint8_t getSelectedChannelIdx() const { return _channels[_highlighted].idx; } - const char* getSelectedChannelName() const { return _channels[_highlighted].name; } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - bool handleInput(char c) override { return false; } // touch handled in poll() - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; _downAt = millis(); - } else if (!now && _touchDown) { - _touchDown = false; - unsigned long held = millis() - _downAt; - if (_downX < 20 && _downY < HEADER_H) { _wantsExit = true; return; } // back arrow - int row = rowAtY(_downY); - if (row < 0) return; // tap outside the channel list - _highlighted = (uint8_t)row; // single tap selects the tapped row - ensureVisible(); - if (held >= TW_LONG_PRESS_MS) _confirmed = true; // long press opens it - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - display.setTextSize(1); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(W / 2, 0, "CHANNELS"); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, HEADER_H - 2, W, 1); - - if (_numChannels == 0) { - display.setCursor(2, HEADER_H + 2); - display.print("(no channels)"); - return 500; - } - - int vis = visibleRows(); - int y = HEADER_H; - for (int i = 0; i < vis; i++) { - int ci = _scrollTop + i; - if (ci >= _numChannels) break; - if (ci == _highlighted) { - display.setColor(DisplayDriver::GREEN); - display.fillRect(0, y, W, ROW_H); - display.setColor(DisplayDriver::DARK); - } else { - display.setColor(DisplayDriver::LIGHT); - } - int nameMaxW = W - 6; - int unread = _store ? _store->getUnreadForChannel(_channels[ci].idx) : 0; - if (unread > 0) { - char cnt[8]; - snprintf(cnt, sizeof(cnt), "%d", unread > 99 ? 99 : unread); - int cw = display.getTextWidth(cnt); - display.setColor((ci == _highlighted) ? DisplayDriver::DARK : DisplayDriver::BLUE); - display.drawTextRightAlign(W - 3, y + 2, cnt); - display.setColor((ci == _highlighted) ? DisplayDriver::DARK : DisplayDriver::LIGHT); - nameMaxW = W - 6 - cw - 4; - } - display.drawTextEllipsized(3, y + 2, nameMaxW, _channels[ci].name); - y += ROW_H; - } - return 500; - } -}; - -// ============================================================================= -// TWatchChannelScreen -// ============================================================================= -class TWatchChannelScreen : public UIScreen { - DisplayDriver* _display; - ChannelScreen* _store; // shared message history store - uint8_t _channelIdx; - char _channelName[TW_CH_NAME_LEN]; - char _selfPrefix[TW_CH_NAME_LEN + 2]; // "NodeName: " -- marks sent lines - - bool _touchDown; - int _downX, _downY; - - bool _wantsCompose; - bool _wantsExit; - - int _selectedN; // recency index shown as ticker, -1 = none - unsigned long _tickerStartMs; - const void* _lastNewest; // newest store entry seen last render - uint32_t _lastNewestTs; // (a change dismisses the ticker) - bool _showPath; // hop-path overlay visible - int _pathMsgN; // recency index the overlay describes - - static const int HEADER_H = 14; - static const int COMPOSE_BAR_H = 18; - static const int MSG_LINE_H = 11; - static const int MSG_TOP = HEADER_H + 2; - - int rowsThatFit() const { - int h = _display ? _display->height() : 120; - int rows = (h - COMPOSE_BAR_H - 1 - MSG_TOP) / MSG_LINE_H; - return rows < 1 ? 1 : rows; - } - - // Store messages available for this channel, capped at what fits on screen. - int visibleCount() const { - if (!_store) return 0; - int maxRows = rowsThatFit(); - int n = 0; - while (n < maxRows && _store->getChannelMsgByRecency(_channelIdx, n)) n++; - return n; - } - - // Sent messages are stored with path_len 0 and a "NodeName: " prefix. - bool isSent(const ChannelScreen::ChannelMessage* m) const { - return m->path_len == 0 && _selfPrefix[0] != 0 && - strncmp(m->text, _selfPrefix, strlen(_selfPrefix)) == 0; - } - - void drawMsgLine(DisplayDriver& display, int y, const char* text, bool sent) { - const int W = display.width(); - const int maxW = W - 4; - display.setColor(DisplayDriver::LIGHT); - if (!sent) { - display.drawTextEllipsized(2, y, maxW, text); // incoming: left-aligned - return; - } - if (display.getTextWidth(text) <= maxW) { // sent: right-aligned - display.drawTextRightAlign(W - 2, y, text); - return; - } - char buf[CHANNEL_MSG_TEXT_LEN + 4]; - strncpy(buf, text, sizeof(buf) - 4); - buf[sizeof(buf) - 4] = 0; - int ellW = display.getTextWidth("..."); - int len = (int)strlen(buf); - while (len > 0 && display.getTextWidth(buf) > maxW - ellW) { buf[--len] = 0; } - strcat(buf, "..."); - display.drawTextRightAlign(W - 2, y, buf); - } - - void drawTicker(DisplayDriver& display, int y, const char* text, bool sent) { - const int W = display.width(); - int textW = display.getTextWidth(text); - display.setColor(DisplayDriver::LIGHT); - if (textW <= W - 4) { // fits -> nothing to scroll, keep alignment - if (sent) display.drawTextRightAlign(W - 2, y, text); - else { display.setCursor(2, y); display.print(text); } - return; - } - int period = textW + 24; // full text width + trailing gap - unsigned long elapsed = millis() - _tickerStartMs; - int off = (int)((elapsed / TW_TICKER_MS_PER_PX) % (unsigned long)period); - // Wrap off while the marquee draws: a long line must clip at the screen - // edge, not wrap onto the rows below. Restored straight after. - ((LGFXDisplay*)_display)->setTextWrap(false); - display.setCursor(2 - off, y); - display.print(text); - display.setCursor(2 - off + period, y); - display.print(text); - ((LGFXDisplay*)_display)->setTextWrap(true); - } - - // Renders the "Heard by" list (repeaters that echoed our sent message + the - // SNR we heard each at) into the already-drawn overlay box. Data comes from - // MyMesh::getHeardBy, keyed by the fingerprint stashed in the sent bubble's - // path[]. Same blue box as the hop-path overlay. - void drawHeardBy(LGFXDisplay* d, const ChannelScreen::ChannelMessage* m, - int bx, int by, int bh) { - uint8_t bph = 0, count = 0; - uint8_t hashes[16]; - int8_t snrs[8]; - (void)bh; - the_mesh.getHeardBy(m->path, 12, bph, count, hashes, snrs, 8); - - char head[28]; - snprintf(head, sizeof(head), "Heard by: %d", count); // distinct repeaters - d->setRawColor(0x7BEF); - d->printSmallFont(bx + 5, by + 8, head); - - if (count == 0 || bph == 0) { - d->setRawColor(0xFFFF); - d->printSmallFont(bx + 5, by + 19, "No repeater echoes yet"); - return; - } - - const int maxLines = 5; // 5 repeaters, then a "+N more" 6th line - int shown = (count > maxLines) ? maxLines : count; - int y = by + 19; - for (int i = 0; i < shown; i++) { - const int off = i * bph; - char hex[6]; - if (bph == 1) snprintf(hex, sizeof(hex), "%02x", hashes[off]); - else snprintf(hex, sizeof(hex), "%02x%02x", hashes[off], hashes[off + 1]); - - char nm[16]; nm[0] = 0; - uint32_t numC = the_mesh.getNumContacts(); - ContactInfo c; - for (int pass = 0; pass < 2 && nm[0] == 0; pass++) { - for (uint32_t ci = 0; ci < numC; ci++) { - if (!the_mesh.getContactByIdx(ci, c)) continue; - if (memcmp(c.id.pub_key, &hashes[off], bph) != 0) continue; - if (pass == 0 && c.type != ADV_TYPE_REPEATER) continue; - strncpy(nm, c.name, sizeof(nm) - 1); nm[sizeof(nm) - 1] = 0; - break; - } - } - - int8_t s = snrs[i]; - char line[40]; - snprintf(line, sizeof(line), "(%s) %s %d.%ddB", - hex, nm[0] ? nm : "?", s / 4, ((abs((int)s) % 4) * 10) / 4); - d->setRawColor(0xFFFF); - d->printSmallFont(bx + 5, y, line); - y += 9; - } - if (count > shown) { - d->setRawColor(0xFFFF); - char more[16]; - snprintf(more, sizeof(more), "+%d more", count - shown); - d->printSmallFont(bx + 5, y, more); - } - } - - // Hop-path overlay for the message at recency _pathMsgN. Drawn last so it - // sits above the message list; toggled by the button (see handleInput), - // dismissed by any tap (see poll). - void drawPathOverlay(DisplayDriver& display) { - const ChannelScreen::ChannelMessage* m = - _store ? _store->getChannelMsgByRecency(_channelIdx, _pathMsgN) : nullptr; - if (!m) { _showPath = false; return; } - - LGFXDisplay* d = (LGFXDisplay*)&display; - const int bx = 3, bw = display.width() - 6, by = 18, bh = 76; - d->setRawColor(0x0000); - d->fillRoundRect(bx, by, bw, bh, 4); - d->setRawColor(0x231D); - d->drawRoundRect(bx, by, bw, bh, 4); - - // Sent message with a captured fingerprint: show which repeaters echoed it - // ("Heard by") instead of a hop path, which a sent bubble doesn't have. - if (m->path_len == 0 && m->has_fp) { - drawHeardBy(d, m, bx, by, bh); - return; - } - - char head[28]; - if (m->path_len == 0) { - snprintf(head, sizeof(head), "%s", isSent(m) ? "Sent (no path)" : "Direct (0 hops)"); - } else if (m->path_len == 0xFF) { - snprintf(head, sizeof(head), "%s", "Flood (no path)"); - } else { - snprintf(head, sizeof(head), "%d hops, %d-byte", - m->path_len & 63, (m->path_len >> 6) + 1); - } - d->setRawColor(0x7BEF); - d->printSmallFont(bx + 5, by + 8, head); - - if (m->path_len == 0 || m->path_len == 0xFF) return; - - int byteLen = (int)mesh::Packet::getPathByteLenFor(m->path_len); - if (byteLen > MSG_PATH_MAX) byteLen = MSG_PATH_MAX; - int bph = (m->path_len >> 6) + 1; - - // Hex hops in the path-editor format ("3601,2198,..."), wrapped - // token-wise across small-font lines. - char line[36]; - int ll = 0, y = by + 19; - d->setRawColor(0xFFFF); - for (int b = 0; b < byteLen; b += bph) { - char tok[9]; - int tl = 0; - for (int k = 0; k < bph && b + k < byteLen; k++) - tl += snprintf(tok + tl, sizeof(tok) - tl, "%02x", m->path[b + k]); - if (b + bph < byteLen) { tok[tl++] = ','; tok[tl] = 0; } - if (ll + tl > 26) { - line[ll] = 0; - d->printSmallFont(bx + 5, y, line); - y += 9; ll = 0; - if (y > by + bh - 8) { d->printSmallFont(bx + 5, y, "..."); return; } - } - memcpy(line + ll, tok, tl); ll += tl; - } - if (ll > 0) { line[ll] = 0; d->printSmallFont(bx + 5, y, line); } - } - -public: - TWatchChannelScreen(DisplayDriver* display) - : _display(display), _store(nullptr), _channelIdx(0), - _touchDown(false), _downX(0), _downY(0), - _wantsCompose(false), _wantsExit(false), - _selectedN(-1), _tickerStartMs(0), - _lastNewest(nullptr), _lastNewestTs(0), - _showPath(false), _pathMsgN(0) { - _channelName[0] = 0; - _selfPrefix[0] = 0; - } - - // Shared history store this screen renders from. - void setStore(ChannelScreen* store) { _store = store; } - - // Own node name, used to right-align sent lines ("NodeName: text"). - void setSelfName(const char* name) { - if (!name || !name[0]) { _selfPrefix[0] = 0; return; } - snprintf(_selfPrefix, sizeof(_selfPrefix), "%s: ", name); - } - - // Switch to a channel (called when a channel is selected in the picker). - // Opening a channel marks its messages as read. - void activate(uint8_t idx, const char* name) { - _channelIdx = idx; - strncpy(_channelName, name ? name : "", TW_CH_NAME_LEN - 1); - _channelName[TW_CH_NAME_LEN - 1] = 0; - _touchDown = false; _wantsCompose = false; _wantsExit = false; - _selectedN = -1; - _showPath = false; - _lastNewest = nullptr; _lastNewestTs = 0; - if (_store) _store->markChannelRead(_channelIdx); - } - - uint8_t getChannelIdx() const { return _channelIdx; } - const char* getChannelName() const { return _channelName; } - - bool wantsCompose() const { return _wantsCompose; } - void acknowledgeCompose() { _wantsCompose = false; } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - // Short press of the PWR key (S3, KEY_ENTER) or the boot button (S3 Plus, - // KEY_NEXT): toggle the hop-path overlay for the ticker-selected message - // (the newest one if none is selected). Touch is handled in poll(). - bool handleInput(char c) override { - if (c == KEY_ENTER || c == KEY_NEXT) { - if (_showPath) { _showPath = false; return true; } - int n = (_selectedN >= 0) ? _selectedN : 0; - if (_store && _store->getChannelMsgByRecency(_channelIdx, n)) { - _pathMsgN = n; - _showPath = true; - } - return true; - } - return false; - } - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; - } else if (!now && _touchDown) { - _touchDown = false; - int H = _display->height(); - if (_showPath) { _showPath = false; return; } // any tap dismisses the path overlay - if (_downX < 20 && _downY < HEADER_H) { _selectedN = -1; _wantsExit = true; return; } // back arrow - if (_downY >= H - COMPOSE_BAR_H) { _selectedN = -1; _wantsCompose = true; return; } // compose bar - // message area -> tap to open/close ticker - if (_downY >= MSG_TOP && _downY < H - COMPOSE_BAR_H) { - int visualRow = (_downY - MSG_TOP) / MSG_LINE_H; - int count = visibleCount(); - if (visualRow >= 0 && visualRow < count) { - int n = (count - 1) - visualRow; // top row = oldest visible - if (_selectedN == n) _selectedN = -1; - else { _selectedN = n; _tickerStartMs = millis(); } - } - } - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - const int H = display.height(); - display.setTextSize(1); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(W / 2, 0, _channelName); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, HEADER_H - 2, W, 1); - - // A new message (sent or received) shifts the rows, so dismiss any open - // ticker when the newest store entry for this channel changes. - const ChannelScreen::ChannelMessage* newest = - _store ? _store->getChannelMsgByRecency(_channelIdx, 0) : nullptr; - if (newest != _lastNewest || (newest && newest->timestamp != _lastNewestTs)) { - _lastNewest = newest; - _lastNewestTs = newest ? newest->timestamp : 0; - _selectedN = -1; - } - - int count = visibleCount(); - if (count == 0) { - display.setColor(DisplayDriver::LIGHT); - display.setCursor(2, MSG_TOP); - display.print("(no messages)"); - } else { - int y = MSG_TOP; - for (int n = count - 1; n >= 0; n--) { // oldest visible (top) -> newest (bottom) - const ChannelScreen::ChannelMessage* m = _store->getChannelMsgByRecency(_channelIdx, n); - if (!m) continue; - bool sent = isSent(m); - if (n == _selectedN) drawTicker(display, y, m->text, sent); - else drawMsgLine(display, y, m->text, sent); - y += MSG_LINE_H; - } - } - - // compose bar - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, H - COMPOSE_BAR_H, W, COMPOSE_BAR_H - 1); - display.drawTextEllipsized(3, H - COMPOSE_BAR_H + 4, W - 6, "Tap to compose"); - if (_showPath) drawPathOverlay(display); - return (_selectedN >= 0 || _showPath) ? 60 : 500; - } -}; - -// ============================================================================= -// TWatchKeyboardScreen -// ============================================================================= -class TWatchKeyboardScreen : public UIScreen { - DisplayDriver* _display; - uint8_t _channelIdx; - -public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE, TWKB_TRACE_PATH }; - -private: - Purpose _purpose; - int _contextIdx; // channel idx (channel) or contact idx (DM/admin) - bool _mask; // render composed text as '*' (admin password) - unsigned long _lastCharAt; // when the last char was typed (for reveal window) - - static const unsigned long PW_REVEAL_MS = 2000; // show last char this long before masking - - char _outBuf[TW_OUT_BUF_LEN]; - uint16_t _outLen; - - enum Mode { LOWER, UPPER, SYM }; - Mode _mode; - - bool _touchDown; - int _downX, _downY; - - bool _wantsSend; - bool _wantsExit; - - static const int MAXLEN = TW_OUT_BUF_LEN - 1; // 133 - - // layout geometry (120x120 virtual space) - static const int TOPBAR_H = 15; - static const int KEY_W = 12; // 10 * 12 = 120 wide - static const int KEY_H = 26; - static const int GRID_Y = 15; // row0 top; rows at GRID_Y + row*KEY_H - - const char* const* layout() const { - static const char* const lower[3] = { "qwertyuiop", "asdfghjkl'", "zxcvbnm,.?" }; - static const char* const upper[3] = { "QWERTYUIOP", "ASDFGHJKL\"", "ZXCVBNM<>/" }; - static const char* const syms[3] = { "123!@#$%^&", "456()[]{}-", "7890+=_:;|" }; - return _mode == SYM ? syms : (_mode == UPPER ? upper : lower); - } - const char* modeLabel() const { - return _mode == SYM ? "SYM" : (_mode == UPPER ? "A-Z" : "a-z"); - } - - void appendChar(char ch) { - if (_outLen < MAXLEN) { _outBuf[_outLen++] = ch; _outBuf[_outLen] = 0; _lastCharAt = millis(); } - } - void backspace() { - if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; _lastCharAt = 0; } - } - void cycleMode() { - _mode = (_mode == LOWER) ? UPPER : (_mode == UPPER ? SYM : LOWER); - } - // bottom row zones: mode 0..24 | space 24..72 | enter 72..96 | backspace 96..120 - void handleBottomRow(int x) { - if (x < 24) cycleMode(); - else if (x < 72) appendChar(' '); - else if (x < 96) { if (_outLen > 0) _wantsSend = true; } - else backspace(); - } - -public: - TWatchKeyboardScreen(DisplayDriver* display) - : _display(display), _channelIdx(0), - _purpose(TWKB_CHANNEL), _contextIdx(0), _mask(false), _lastCharAt(0), - _outLen(0), _mode(LOWER), - _touchDown(false), _downX(0), _downY(0), - _wantsSend(false), _wantsExit(false) { - _outBuf[0] = 0; - } - - // Enter compose on the given channel (called when the compose bar is tapped). - void activate(uint8_t idx, const char* name) { - (void)name; - _channelIdx = idx; - _purpose = TWKB_CHANNEL; - _contextIdx = idx; - _mask = false; - _outLen = 0; _outBuf[0] = 0; - _mode = LOWER; - _touchDown = false; _wantsSend = false; _wantsExit = false; - } - - // Enter text for a non-channel purpose (DM / admin password / admin CLI). - void activateFor(Purpose purpose, int contextIdx) { - _purpose = purpose; - _contextIdx = contextIdx; - _channelIdx = 0; - _mask = (purpose == TWKB_ADMIN_PASSWORD); - _outLen = 0; _outBuf[0] = 0; - _mode = LOWER; - _touchDown = false; _wantsSend = false; _wantsExit = false; - } - - Purpose getPurpose() const { return _purpose; } - int getContextIdx() const { return _contextIdx; } - - uint8_t getChannelIdx() const { return _channelIdx; } - bool consumeSendRequest(const char** textOut) { - if (!_wantsSend) return false; - _wantsSend = false; - if (textOut) *textOut = _outBuf; - return true; - } - void clearOutBuf() { _outLen = 0; _outBuf[0] = 0; } - // Pre-fill the compose buffer (e.g. editing an existing note). Call after - // activateFor(), which clears the buffer; input is clamped at MAXLEN. - void setInitialText(const char* s) { - _outLen = 0; - if (s) { - while (s[_outLen] && _outLen < MAXLEN) { _outBuf[_outLen] = s[_outLen]; _outLen++; } - } - _outBuf[_outLen] = 0; - } - bool wantsExit() const { return _wantsExit; } - void acknowledgeExit() { _wantsExit = false; } - - bool handleInput(char c) override { return false; } - - void poll() override { - if (!_display) return; - int x, y; - bool now = ((LGFXDisplay*)_display)->getTouch(&x, &y); - if (now && !_touchDown) { - _touchDown = true; _downX = x; _downY = y; - } else if (!now && _touchDown) { - _touchDown = false; - int x0 = _downX, y0 = _downY; - if (x0 < 20 && y0 < TOPBAR_H) { _wantsExit = true; return; } // back arrow - if (y0 < GRID_Y) return; // top bar (text) area - int row = (y0 - GRID_Y) / KEY_H; - if (row <= 2) { - int col = x0 / KEY_W; - char ch = layout()[row][col]; - if (ch) appendChar(ch); - } else { - handleBottomRow(x0); // bottom row - } - } - } - - int render(DisplayDriver& display) override { - const int W = display.width(); - display.setTextSize(1); - - // ---- top bar: back arrow + composed text tail + cursor ---- - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, 0); - display.print("<"); - - display.setColor(DisplayDriver::LIGHT); - { - // Mask the composed text with '*' for the admin password purpose. - char masked[TW_OUT_BUF_LEN]; - const char* src = _outBuf; - if (_mask) { - int m = (_outLen < TW_OUT_BUF_LEN - 1) ? _outLen : TW_OUT_BUF_LEN - 1; - for (int i = 0; i < m; i++) masked[i] = '*'; - // Reveal the most recently typed char briefly so it can be read. - if (m > 0 && (millis() - _lastCharAt < PW_REVEAL_MS)) masked[m - 1] = _outBuf[m - 1]; - masked[m] = 0; - src = masked; - } - const int avail = W - 16 - 4; // room after the back arrow, minus cursor - int len = _outLen; - int fit = 0; - for (int n = 1; n <= len; n++) { - if (display.getTextWidth(src + (len - n)) > avail) break; - fit = n; - } - char tail[TW_OUT_BUF_LEN + 2]; - strncpy(tail, src + (len - fit), sizeof(tail) - 2); - tail[sizeof(tail) - 2] = 0; - size_t tl = strlen(tail); - tail[tl] = '_'; tail[tl + 1] = 0; - display.setCursor(16, 2); - display.print(tail); - } - - // ---- key grid rows 0..2 ---- - const char* const* lay = layout(); - for (int row = 0; row < 3; row++) { - int ry = GRID_Y + row * KEY_H; - const char* r = lay[row]; - for (int col = 0; col < 10; col++) { - int kx = col * KEY_W; - display.setColor(DisplayDriver::GREEN); - display.drawRect(kx, ry, KEY_W, KEY_H); - char ch[2] = { r[col], 0 }; - display.setColor(DisplayDriver::LIGHT); - int tw = display.getTextWidth(ch); - display.setCursor(kx + (KEY_W - tw) / 2, ry + (KEY_H - 8) / 2); - display.print(ch); - } - } - - // ---- bottom row: mode | space | enter | backspace ---- - int by = GRID_Y + 3 * KEY_H; - - display.setColor(DisplayDriver::GREEN); - display.fillRect(0, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* ml = modeLabel(); - int tw = display.getTextWidth(ml); - display.setCursor((24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(ml); - } - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(24, by, 48, KEY_H); // space - - display.setColor(DisplayDriver::GREEN); - display.fillRect(72, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* el = "OK"; - int tw = display.getTextWidth(el); - display.setCursor(72 + (24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(el); - } - - display.setColor(DisplayDriver::ORANGE); - display.fillRect(96, by, 24, KEY_H); - display.setColor(DisplayDriver::DARK); - { - const char* bl = "DEL"; - int tw = display.getTextWidth(bl); - display.setCursor(96 + (24 - tw) / 2, by + (KEY_H - 8) / 2); - display.print(bl); - } - - return 500; - } -}; - -#endif // TWATCH_COMPOSE_ENABLED \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp deleted file mode 100644 index 92139bbe..00000000 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include -#include "TWatchS3PlusBoard.h" -#include -#include // TEMP power-debug: esp_bt_controller_get_status() - -volatile bool TWatchS3PlusBoard::_tilt_flag = false; - -// memw forces the _tilt_flag store to retire to SRAM before this ISR -// returns. Without it, tiltFired() in the main loop can read a stale -// value and raise-to-wake is missed. Do not remove. -void IRAM_ATTR TWatchS3PlusBoard::onTiltISR() { _tilt_flag = true; asm volatile("memw" ::: "memory"); } - -// ---- Wrapper-free BMA423 step counter (raw I2C) ---------------------------- -// SensorLib's SensorBMA423 step-counter methods do not compile in this build, -// so the step counter is driven directly over I2C. Register/offset/mask values -// are from the Bosch BMA423 driver. -#define BMA423_REG_STEP_CNT_OUT 0x1E // 4-byte little-endian step count output -#define BMA423_REG_FEATURE_CONFIG 0x5E // 64-byte feature config stream -#define BMA423_FEATURE_LEN 64 -#define BMA423_STEP_EN_BYTE 0x37 // BMA423_STEP_CNTR_OFFSET(0x36) + 1 -#define BMA423_STEP_EN_BIT 0x10 // BMA423_STEP_CNTR_EN_MSK -// Step counter watermark: a 10-bit field (BMA423_STEP_CNTR_WM_MSK = 0x03FF) -// spanning feature_config[0x36] as LSB and the low two bits of [0x37] as MSB. -// It does not collide with the enable bit, which is bit 4 of [0x37] (bit 12 of -// the 16-bit word). Bosch's bma423_step_counter_set_watermark() writes exactly -// these bits; SensorLib's enableStepCounter() calls it with 1, and LilyGo's own -// firmware calls setStepCounterWatermark(1). Meck never set it, which is the -// prime suspect for the undercounting. -#define BMA423_STEP_WM_LSB_BYTE 0x36 // BMA423_STEP_CNTR_OFFSET -#define BMA423_STEP_WM_MSK 0x03FF // BMA423_STEP_CNTR_WM_MSK -#define BMA423_STEP_WM_LEVEL 1 // matches LilyGoLib and the SensorLib example - -#define BMA423_REG_POWER_CONF 0x7C // BMA4_POWER_CONF_ADDR -#define BMA423_ADV_PWR_SAVE_BIT 0x01 // BMA4_ADVANCE_POWER_SAVE_MSK - -static bool bma423ReadRegs(uint8_t reg, uint8_t* buf, uint8_t len) { - Wire.beginTransmission(I2C_ADDR_ACCEL); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) return false; - if (Wire.requestFrom((int)I2C_ADDR_ACCEL, (int)len) != len) return false; - for (uint8_t i = 0; i < len; i++) buf[i] = Wire.read(); - return true; -} - -static bool bma423WriteRegs(uint8_t reg, const uint8_t* buf, uint8_t len) { - Wire.beginTransmission(I2C_ADDR_ACCEL); - Wire.write(reg); - for (uint8_t i = 0; i < len; i++) Wire.write(buf[i]); - return Wire.endTransmission() == 0; -} - -// Enable the step counter by setting its enable bit in the feature config, -// preserving every other byte (tilt lives at a different offset, 0x3A, so it is -// untouched). The feature config can only be written with advanced-power-save -// disabled, so we bracket the write and restore the prior power state after. -static void bma423EnableStepCounter() { - uint8_t pc; - if (!bma423ReadRegs(BMA423_REG_POWER_CONF, &pc, 1)) return; // save power state - uint8_t off = pc & ~BMA423_ADV_PWR_SAVE_BIT; // disable adv power save - bma423WriteRegs(BMA423_REG_POWER_CONF, &off, 1); - delay(2); // wake from low-power (>=450us) - - uint8_t cfg[BMA423_FEATURE_LEN]; - if (bma423ReadRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN)) { - // Watermark first, then the enable bit, in a single read-modify-write. - uint16_t wm = ((uint16_t)cfg[BMA423_STEP_EN_BYTE] << 8) | cfg[BMA423_STEP_WM_LSB_BYTE]; - wm = (wm & ~BMA423_STEP_WM_MSK) | (BMA423_STEP_WM_LEVEL & BMA423_STEP_WM_MSK); - cfg[BMA423_STEP_WM_LSB_BYTE] = (uint8_t)(wm & 0xFF); - cfg[BMA423_STEP_EN_BYTE] = (uint8_t)((wm >> 8) & 0xFF); - cfg[BMA423_STEP_EN_BYTE] |= BMA423_STEP_EN_BIT; - bma423WriteRegs(BMA423_REG_FEATURE_CONFIG, cfg, BMA423_FEATURE_LEN); - delay(1); // write settle - } - - bma423WriteRegs(BMA423_REG_POWER_CONF, &pc, 1); // restore power state -} - -void TWatchS3PlusBoard::begin() { - ESP32Board::begin(); - power_init(); - - // BMA423 accelerometer (always-on I2C, 0x19): enable the tilt / wrist-raise - // feature and its interrupt (routed to PIN1 -> GPIO14) for raise-to-wake. - _accel = new SensorBMA423(); - if (_accel->begin(Wire, I2C_ADDR_ACCEL, PIN_BOARD_SDA, PIN_BOARD_SCL)) { - _accel->setRemapAxes(SensorRemap::BOTTOM_LAYER_TOP_RIGHT_CORNER); - // 100 Hz ODR: SensorLib's BMA423_StepDetector example runs the pedometer at - // 100 Hz, not the 50 Hz used here previously. Everything else already matched - // (NORMAL, FS_2G, OSR2_AVG2, CIC_AVG_MODE). Revert this one literal to 50.0f - // to A/B it against the watermark change above. - _accel->configAccelerometer(OperationMode::NORMAL, AccelFullScaleRange::FS_2G, - 100.0f, AccelBandwidth::OSR2_AVG2, AccelPerfMode::CIC_AVG_MODE); - // INT1 pin electrical config: level trigger, active high, push-pull, - // output enabled. INT1_IO_CTRL resets to output-disabled, so without - // this the pin never drives and INPUT_PULLDOWN reads low forever. - _accel->setInterruptPinConfig(InterruptPinMap::PIN1, false, false, true, false); - pinMode(PIN_ACCEL_IRQ, INPUT_PULLDOWN); - // Attach the edge ISR BEFORE enabling the tilt source, so the first - // assertion cannot occur before the handler is armed (a missed first edge - // on a self-clearing line otherwise locks tilt-wake out permanently). - attachInterrupt(digitalPinToInterrupt(PIN_ACCEL_IRQ), onTiltISR, RISING); - _accel->enableTiltDetector(true, true); - // Enable the hardware step counter via raw I2C (SensorLib's wrapper method - // does not compile in this build). It then counts in the BMA423 feature - // engine with no CPU cost, even while the display is off. - bma423EnableStepCounter(); - } - - esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_DEEPSLEEP) { - long wakeup_source = esp_sleep_get_ext1_wakeup_status(); - if (wakeup_source & (1 << P_LORA_DIO_1)) { - startup_reason = BD_STARTUP_RX_PACKET; - } - rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); - rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); - } -} - -bool TWatchS3PlusBoard::power_init() { - PMU = new XPowersAXP2101(Wire, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_ADDR_PMU); - if (!PMU->init()) { - MESH_DEBUG_PRINTLN("Warning: Failed to find AXP2101 power management"); - delete PMU; - PMU = NULL; - return false; - } - - PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); - - // Power rails per the T-Watch S3 Plus PowerManage table: - // ALDO2 = display backlight, ALDO3 = display + touch, - // ALDO4 = LoRa, BLDO1 = GNSS, BLDO2 = DRV2605, ALDO1 = unused. - PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); // LoRa radio - PMU->enablePowerOutput(XPOWERS_ALDO4); - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // display + touch - PMU->enablePowerOutput(XPOWERS_ALDO3); - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // display backlight - PMU->enablePowerOutput(XPOWERS_ALDO2); - PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // DRV2605 haptic - PMU->enablePowerOutput(XPOWERS_BLDO2); - // GNSS (MIA-M10Q) on BLDO1 -- set the rail voltage but leave it OFF at boot. - // It is powered on demand via gpsPowerOn() when gps_enabled is set. - PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); - PMU->disablePowerOutput(XPOWERS_BLDO1); - - PMU->disablePowerOutput(XPOWERS_DCDC2); - PMU->disablePowerOutput(XPOWERS_DCDC3); - PMU->disablePowerOutput(XPOWERS_DCDC4); - PMU->disablePowerOutput(XPOWERS_DCDC5); - PMU->disablePowerOutput(XPOWERS_ALDO1); // unused on the Plus - PMU->disablePowerOutput(XPOWERS_DLDO1); - PMU->disablePowerOutput(XPOWERS_DLDO2); - PMU->disablePowerOutput(XPOWERS_VBACKUP); - - PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - PMU->clearIrqStatus(); - - PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_200MA); - // The fitted cell (Tewaycell 112530-2P, label verified) is 3.8V nominal - // LiHV -- i.e. 4.35V max charge, same chemistry as the S3's cell. Charging - // to 4V2 forfeits the top ~12-15% of capacity. - PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V35); - - PMU->disableTSPinMeasure(); - PMU->enableSystemVoltageMeasure(); - PMU->enableVbusVoltageMeasure(); - PMU->enableBattVoltageMeasure(); - - PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); - - // TEMP power-debug: one-shot rail dump at boot. Rail OFF means the attached - // chip has no power at all (not merely sleeping). - Serial.printf("[PWR] rails: ALDO2(bl)=%d ALDO3(disp/touch)=%d ALDO4(LoRa)=%d BLDO1(GPS)=%d BLDO2(haptic)=%d\n", - PMU->isPowerChannelEnable(XPOWERS_ALDO2), - PMU->isPowerChannelEnable(XPOWERS_ALDO3), - PMU->isPowerChannelEnable(XPOWERS_ALDO4), - PMU->isPowerChannelEnable(XPOWERS_BLDO1), - PMU->isPowerChannelEnable(XPOWERS_BLDO2)); - Serial.printf("[PWR] GPS rail (BLDO1) at boot: %s\n", - PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF (fully powered down)"); - return true; -} - -// TEMP power-debug probe. -void TWatchS3PlusBoard::printPowerDebug() { - if (!PMU) return; - Serial.printf("[PWR] batt=%dmV %d%% vbus=%dmV charging=%d cpu=%dMHz bt=%d gps_rail=%s\n", - PMU->getBattVoltage(), PMU->getBatteryPercent(), - PMU->getVbusVoltage(), PMU->isCharging(), - getCpuFrequencyMhz(), (int)esp_bt_controller_get_status(), - PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF"); -} - -void TWatchS3PlusBoard::gpsPowerOn() { - if (PMU) { - PMU->enablePowerOutput(XPOWERS_BLDO1); - delay(100); // allow the module to boot before we expect NMEA - } -} - -void TWatchS3PlusBoard::gpsPowerOff() { - if (PMU) PMU->disablePowerOutput(XPOWERS_BLDO1); -} - -bool TWatchS3PlusBoard::tiltFired() { - if (_tilt_flag) { // set by the GPIO14 rising-edge ISR - _tilt_flag = false; - _accel->update(); // reading the status clears the sensor INT - return true; - } - return false; -} - -uint32_t TWatchS3PlusBoard::getStepCount() { - uint8_t d[4]; - if (!bma423ReadRegs(BMA423_REG_STEP_CNT_OUT, d, 4)) return 0; - return (uint32_t)d[0] | ((uint32_t)d[1] << 8) | - ((uint32_t)d[2] << 16) | ((uint32_t)d[3] << 24); -} \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h deleted file mode 100644 index 50fa738c..00000000 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include "variant.h" // Board-specific pin definitions (I2C, user btn, addresses) - -#include -#include -#include "XPowersLib.h" -#include "helpers/ESP32Board.h" -#include - -// LilyGo T-Watch S3 Plus board. -// -// Power is managed by an AXP2101 PMU on the main I2C bus. The PMU power rails -// (per the T-Watch S3 Plus PowerManage table) are brought up in power_init(). -class SensorBMA423; // full include kept in the .cpp to avoid a BLE-build header clash - -class TWatchS3PlusBoard : public ESP32Board { - XPowersLibInterface* PMU = NULL; - SensorBMA423* _accel = nullptr; - static volatile bool _tilt_flag; - static void IRAM_ATTR onTiltISR(); // defined in the .cpp (IRAM relocation) - - bool power_init(); - -public: - void begin(); - - // Returns true once when the BMA423 tilt (wrist-raise) interrupt has fired. - bool tiltFired(); - - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); - } else { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000ULL); - } - - esp_deep_sleep_start(); - } - - uint16_t getBattMilliVolts() override { - return PMU ? PMU->getBattVoltage() : 0; - } - - uint8_t getBatteryPercent() override { - return PMU ? PMU->getBatteryPercent() : 0; - } - - // Wrapper-free BMA423 step count (raw I2C; defined in the .cpp). - uint32_t getStepCount(); - - // GPS power is the AXP2101 BLDO1 rail. Off at boot; toggled at runtime via - // the gps_enabled pref (boot) and the "gps on/off" CLI command (live). - void gpsPowerOn(); - void gpsPowerOff(); - - // TEMP power-debug probe: battery, VBUS, CPU clock, BT controller state and - // live rail states (incl. GPS/BLDO1). Called periodically from UITask::loop. - void printPowerDebug(); - - const char* getManufacturerName() const override { - return "LilyGo T-Watch S3 Plus"; - } -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusDisplay.h b/variants/lilygo_twatch_s3_plus/TWatchS3PlusDisplay.h deleted file mode 100644 index 4af6c007..00000000 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusDisplay.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -// LovyanGFX display + touch for the LilyGo T-Watch S3 Plus. -// ST7789V3 240x240 on SPI3 + PWM backlight (GPIO45) + FT6336U capacitive touch -// on a separate I2C bus (Wire1: SDA 39 / SCL 40, INT 16, no RST pin). -// -// LGFXDisplay.h pulls in LovyanGFX (it defines LGFX_USE_V1 and includes -// LovyanGFX.hpp), so we do not redefine those here. - -#include - -class LGFX_TWatchS3Plus : public lgfx::LGFX_Device { - lgfx::Panel_ST7789 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_FT5x06 _touch_instance; - -public: - LGFX_TWatchS3Plus(void) { - { - auto cfg = _bus_instance.config(); - cfg.spi_host = SPI3_HOST; - cfg.spi_mode = 0; - cfg.freq_write = 40000000; - cfg.freq_read = 16000000; - cfg.spi_3wire = true; - cfg.use_lock = true; - cfg.dma_channel = SPI_DMA_CH_AUTO; - cfg.pin_sclk = 18; - cfg.pin_mosi = 13; - cfg.pin_miso = -1; - cfg.pin_dc = 38; - _bus_instance.config(cfg); - _panel_instance.setBus(&_bus_instance); - } - - { - auto cfg = _panel_instance.config(); - cfg.pin_cs = 12; - cfg.pin_rst = -1; - cfg.pin_busy = -1; - // ST7789 GRAM is 240x320; memory_height must be 320 so the 80px rotation - // offset is applied (otherwise a strip of the panel shows noise). - cfg.memory_width = 240; - cfg.memory_height = 320; - cfg.panel_width = 240; - cfg.panel_height = 240; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 1; - cfg.readable = false; - cfg.invert = true; - cfg.rgb_order = false; - cfg.dlen_16bit = false; - cfg.bus_shared = false; - _panel_instance.config(cfg); - } - - { - auto cfg = _light_instance.config(); - cfg.pin_bl = 45; - cfg.invert = false; - cfg.freq = 44100; - cfg.pwm_channel = 7; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); - } - - { - auto cfg = _touch_instance.config(); - cfg.x_min = 0; - cfg.x_max = 239; - cfg.y_min = 0; - cfg.y_max = 239; - cfg.pin_int = 16; - cfg.pin_rst = -1; - cfg.bus_shared = false; - cfg.offset_rotation = 2; // touch IC mounted 180deg vs the LCD horizontal axis - cfg.i2c_port = 1; - cfg.i2c_addr = 0x38; - cfg.pin_sda = 39; - cfg.pin_scl = 40; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); - } -}; - -class TWatchS3PlusDisplay : public LGFXDisplay { - LGFX_TWatchS3Plus disp; -public: - TWatchS3PlusDisplay() : LGFXDisplay(240, 240, disp) {} -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/WatchChannelConfigScreen.h b/variants/lilygo_twatch_s3_plus/WatchChannelConfigScreen.h deleted file mode 100644 index 9fb05280..00000000 --- a/variants/lilygo_twatch_s3_plus/WatchChannelConfigScreen.h +++ /dev/null @@ -1,382 +0,0 @@ -#pragma once - -// ============================================================================= -// WatchChannelConfigScreen -- per-channel settings for the T-Watch S3 / S3 Plus. -// -// The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel action -// from physical-keyboard hotkeys on the selected row (Ent: edit region scope, -// N: cycle notifications, T: tone, X/Hold: delete), none of which exist on the -// touch-only watch. This standalone screen replaces it on MECK_TWATCH builds, -// following the WatchAlarmScreen / WatchNotesScreen conventions. -// -// LIST mode shows each channel with its region scope tag and notification -// state; tap a row to select it, tap again (or PWR/Enter) to open it. DETAIL -// mode offers: Region (opens the watch keyboard via TWKB_SCOPE, pre-filled -// with the current scope), Clear region (reverts to the device default), -// Notify (tap-cycles All -> Mentions -> Off, mirroring the desktop order), -// and Delete via the button (channel 1+ only): a short press of the PWR key -// (S3) or the boot button (S3 Plus) arms it, a second press within 3s -// confirms. The tone picker is deliberately absent -- no audio path. -// -// Exit: the footer Back returns to the Settings screen (via a flag polled by -// UITask::loop); the universal top-strip tap still goes home. -// -// Geometry: 240x240 physical, UI_ZOOM=2, so display.width()/height() = 120. -// ============================================================================= - -// NOTE on include order: mirrors WatchNotesScreen.h -- this header uses -// NodePrefs (NOTIF_* constants, channel_notif[]) and the_mesh, which the two -// call sites (UITask.cpp, main.cpp) already have in scope before including it. -#include -#include -#include -#include -#include -#include -#include - -class UITask; - -#ifndef MAX_GROUP_CHANNELS - #define MAX_GROUP_CHANNELS 20 -#endif - -#define WCC_ROWS 4 // list rows per page - -class WatchChannelConfigScreen : public UIScreen { -public: - enum Mode { LIST, DETAIL }; - -private: - UITask* _task; - NodePrefs* _node_prefs; - - uint8_t _chIdx[MAX_GROUP_CHANNELS]; // populated channel slots, in order - int _numCh = 0; - - Mode _mode = LIST; - int _sel = 0; // selected list row (index into _chIdx) - int _scrollTop = 0; - int _detail = -1; // channel slot open in DETAIL (value from _chIdx) - bool _confirmDelete = false; - unsigned long _confirmAt = 0; - - // Keyboard / exit handshake -- UITask::loop() polls these (UITask is only - // forward declared here, so this screen cannot call it directly). - bool _wantKeyboard = false; - bool _wantExit = false; - char _editScope[31]; // pre-fill text handed to the keyboard - - // Layout (logical pixels) - static const int HEADER_H = 14; - static const int ROW_H = 20; - static const int FOOTER_Y = 100; - - void rebuildList() { - // Scan ALL slots -- the companion app may write non-contiguously, and - // gaps can appear after deletion (same scan as SettingsScreen). - _numCh = 0; - for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { - ChannelDetails ch; - if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { - _chIdx[_numCh++] = i; - } - } - if (_sel >= _numCh) _sel = _numCh > 0 ? _numCh - 1 : 0; - if (_scrollTop > _sel) _scrollTop = _sel; - } - - const char* notifLabel(uint8_t idx) const { - uint8_t n = _node_prefs->channel_notif[idx]; - return (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "@" : "All"; - } - - // Replicates SettingsScreen::deleteChannel (kept in sync manually). Note: - // channel_notif[] is not shifted during compaction -- this matches the - // desktop behaviour exactly. - void deleteChannel(uint8_t idx) { - ChannelDetails empty; - memset(&empty, 0, sizeof(empty)); - int total = 0; - for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { - ChannelDetails ch; - if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { - total = i + 1; - } - } - for (int i = idx; i < total - 1; i++) { - ChannelDetails next; - if (the_mesh.getChannel(i + 1, next)) { - the_mesh.setChannel(i, next); - } - } - the_mesh.setChannel(total - 1, empty); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Deleted channel at idx %d, compacted %d channels\n", idx, total); - } - - void ensureVisible() { - if (_sel < _scrollTop) _scrollTop = _sel; - if (_sel >= _scrollTop + WCC_ROWS) _scrollTop = _sel - WCC_ROWS + 1; - } - -public: - WatchChannelConfigScreen(UITask* task, NodePrefs* node_prefs) - : _task(task), _node_prefs(node_prefs) { - _editScope[0] = 0; - } - - void enter() { - rebuildList(); - _mode = LIST; - _detail = -1; - _confirmDelete = false; - _wantKeyboard = false; - _wantExit = false; - } - - // --- handshake (polled from UITask::loop) --- - bool wantsKeyboard() const { return _wantKeyboard; } - void clearWantKeyboard() { _wantKeyboard = false; } - bool wantsExit() const { return _wantExit; } - void clearWantExit() { _wantExit = false; } - const char* getEditText() const { return _editScope; } - - // Save the keyboard result as the open channel's region scope. An empty - // string clears it back to the device default (same as Clear). Returns - // NULL on success, else an error message. - const char* applyComposedScope(const char* txt) { - if (_detail < 0) return "No channel open"; - ChannelDetails ch; - if (!the_mesh.getChannel((uint8_t)_detail, ch)) return "Channel gone"; - if (txt == nullptr) txt = ""; - strncpy(ch.scope_name, txt, sizeof(ch.scope_name)); - ch.scope_name[30] = '\0'; - the_mesh.setChannel((uint8_t)_detail, ch); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Channel %d scope set to '%s'\n", - _detail, ch.scope_name[0] ? ch.scope_name : "(device default)"); - return nullptr; - } - - // --------------------------------------------------------------------------- - // Rendering. 120x120 logical. - // --------------------------------------------------------------------------- -private: - void renderList(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 4, "Channels"); - - if (_numCh == 0) { - d->setRawColor(0x7BEF); - display.drawTextCentered(display.width() / 2, 48, "No channels"); - } - - char buf[40]; - int visible = _numCh - _scrollTop; - if (visible > WCC_ROWS) visible = WCC_ROWS; - for (int i = 0; i < visible; i++) { - uint8_t idx = _chIdx[_scrollTop + i]; - int y = HEADER_H + i * ROW_H; - ChannelDetails ch; - if (!the_mesh.getChannel(idx, ch)) continue; - - if (_scrollTop + i == _sel) { - d->setRawColor(0x231D); // same blue as the home tile - d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3); - } - d->setRawColor(0xFFFF); - char nameBuf[15]; - strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1); - nameBuf[sizeof(nameBuf) - 1] = 0; - display.setCursor(5, y + 2); - display.print(nameBuf); - - snprintf(buf, sizeof(buf), "[%s] %s", - ch.scope_name[0] ? ch.scope_name : "*", notifLabel(idx)); - d->setRawColor(0x7BEF); - d->printSmallFont(5, y + 13, buf); - } - - // Footer: Back (left) and, when the list overflows a page, More (right). - d->setRawColor(0x231D); - d->drawRoundRect(1, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xFFFF); - d->printSmallFont(20, FOOTER_Y + 8, "Back"); - if (_numCh > WCC_ROWS) { - d->setRawColor(0x231D); - d->drawRoundRect(61, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xC618); - d->printSmallFont(78, FOOTER_Y + 8, "More"); - } - } - - void renderDetail(DisplayDriver& display) { - LGFXDisplay* d = (LGFXDisplay*)&display; - char buf[44]; - - ChannelDetails ch; - if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) { - _mode = LIST; - return; - } - - display.setColor(DisplayDriver::LIGHT); - display.setTextSize(1); - char nameBuf[18]; - strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1); - nameBuf[sizeof(nameBuf) - 1] = 0; - display.drawTextCentered(display.width() / 2, 3, nameBuf); - - // Region row -- opens the keyboard. - d->setRawColor(0x231D); - d->drawRoundRect(1, 16, display.width() - 2, 18, 3); - snprintf(buf, sizeof(buf), "Region: %s", - ch.scope_name[0] ? ch.scope_name : "(default)"); - d->setRawColor(0xFFFF); - d->printSmallFont(6, 24, buf); - - // Clear-region row -- dim when there is nothing to clear. - d->setRawColor(ch.scope_name[0] ? 0x231D : 0x18C3); - d->drawRoundRect(1, 36, display.width() - 2, 18, 3); - d->setRawColor(ch.scope_name[0] ? 0xC618 : 0x4208); - d->printSmallFont(6, 44, "Clear region (use default)"); - - // Notify row -- tap cycles All -> Mentions -> Off. - d->setRawColor(0x231D); - d->drawRoundRect(1, 56, display.width() - 2, 18, 3); - uint8_t n = _node_prefs->channel_notif[(uint8_t)_detail]; - snprintf(buf, sizeof(buf), "Notify: %s", - (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "Mentions" : "All"); - d->setRawColor(0xFFFF); - d->printSmallFont(6, 64, buf); - - // Delete row -- status only; deletion is driven by the button (see - // handleInput). Channel 0 (public) cannot be deleted, same as desktop. - bool canDel = (_detail > 0); - d->setRawColor(canDel ? 0x231D : 0x18C3); - d->drawRoundRect(1, 76, display.width() - 2, 18, 3); - d->setRawColor(canDel ? (_confirmDelete ? 0xF800 : 0xC618) : 0x4208); - d->printSmallFont(6, 84, !canDel ? "Delete (public ch)" - : _confirmDelete ? "Delete? press again" : "Button: delete"); - - // Footer: Back to the list. - d->setRawColor(0x231D); - d->drawRoundRect(1, FOOTER_Y, 58, 18, 3); - d->setRawColor(0xFFFF); - d->printSmallFont(20, FOOTER_Y + 8, "Back"); - } - -public: - int render(DisplayDriver& display) override { - if (_confirmDelete && millis() - _confirmAt > 3000) _confirmDelete = false; - switch (_mode) { - case LIST: renderList(display); return 500; - case DETAIL: renderDetail(display); return 500; - } - return 500; - } - - // Physical tap in logical coordinates. The top-strip home tap is handled by - // main.cpp before this is called. Returns true if the tap was consumed. - bool handleTap(int lx, int ly) { - if (_mode == LIST) { - if (ly >= FOOTER_Y) { - if (lx < 60) { // Back -> Settings - _wantExit = true; - } else if (_numCh > WCC_ROWS) { // More: next page (wraps) - _scrollTop += WCC_ROWS; - if (_scrollTop >= _numCh) _scrollTop = 0; - _sel = _scrollTop; - } - return true; - } - if (ly >= HEADER_H) { - int i = (ly - HEADER_H) / ROW_H; - int idx = _scrollTop + i; - if (i < WCC_ROWS && idx < _numCh) { - if (idx == _sel) { // second tap on the row: open it - _detail = _chIdx[idx]; - _mode = DETAIL; - _confirmDelete = false; - } else { - _sel = idx; // first tap: select - ensureVisible(); - } - return true; - } - } - return false; - } - - // DETAIL - if (ly >= FOOTER_Y) { // Back -> list - _confirmDelete = false; - _mode = LIST; - rebuildList(); - return true; - } - ChannelDetails ch; - if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) { - _mode = LIST; - return true; - } - if (ly >= 16 && ly < 34) { // Region -> keyboard, pre-filled - strncpy(_editScope, ch.scope_name, sizeof(_editScope)); - _editScope[30] = '\0'; - _wantKeyboard = true; - return true; - } - if (ly >= 36 && ly < 54) { // Clear region - if (ch.scope_name[0]) { - ch.scope_name[0] = '\0'; - the_mesh.setChannel((uint8_t)_detail, ch); - the_mesh.saveChannels(); - Serial.printf("WatchChannelCfg: Channel %d scope cleared\n", _detail); - } - return true; - } - if (ly >= 56 && ly < 74) { // Notify: cycle All -> @ -> Off - uint8_t cur = _node_prefs->channel_notif[(uint8_t)_detail]; - _node_prefs->channel_notif[(uint8_t)_detail] = (cur + 1) % 3; - the_mesh.savePrefs(); - return true; - } - return true; // swallow other taps (incl. delete row) - } - - bool handleInput(char c) override { - // The PMU short press (S3) arrives as KEY_ENTER; the S3 Plus boot button - // arrives as KEY_NEXT. - if (_mode == LIST && c == KEY_ENTER) { - if (_sel < _numCh) { - _detail = _chIdx[_sel]; - _mode = DETAIL; - _confirmDelete = false; - } - return true; - } - if (_mode == DETAIL && (c == KEY_ENTER || c == KEY_NEXT)) { - // Button click deletes the open channel (channel 0 is protected): - // first press arms, a second press within 3s confirms. Back is the - // touch footer. - if (_detail > 0) { - if (_confirmDelete) { - deleteChannel((uint8_t)_detail); - _confirmDelete = false; - _detail = -1; - rebuildList(); - _mode = LIST; - } else { - _confirmDelete = true; - _confirmAt = millis(); - } - } - return true; - } - return false; - } -}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/meck_twatch_16mb.csv b/variants/lilygo_twatch_s3_plus/meck_twatch_16mb.csv deleted file mode 100644 index b6b16a9d..00000000 --- a/variants/lilygo_twatch_s3_plus/meck_twatch_16mb.csv +++ /dev/null @@ -1,21 +0,0 @@ -# Meck -- LilyGo T-Watch S3 Plus 16 MB custom partition layout -# -# Single factory app slot (no OTA): the watch has no SD card and does not -# define MECK_OTA_UPDATE, so no firmware-OTA path is compiled -- an OTA -# app1 slot would only waste flash. Maps get a dedicated LittleFS partition; -# the DataStore keeps its own SPIFFS partition. -# -# Both data partitions use the 'spiffs' subtype (the standard ESP32 data -# subtype). The DataStore is mounted as SPIFFS via SPIFFS.begin() (first -# spiffs-subtype partition by offset); maps is mounted as LittleFS by the -# explicit "maps" label, so the mount type is chosen at mount time, not by -# subtype. -# -# NOTE: flashing this shifts the layout and wipes the existing SPIFFS -- -# re-provision identity/contacts after flashing. -# -# Name, Type, SubType, Offset, Size -nvs, data, nvs, 0x9000, 0x5000 -app0, app, factory, 0x10000, 0x200000 -spiffs, data, spiffs, 0x210000, 0x400000 -maps, data, spiffs, 0x610000, 0x9F0000 diff --git a/variants/lilygo_twatch_s3_plus/platformio.ini b/variants/lilygo_twatch_s3_plus/platformio.ini deleted file mode 100644 index 90496547..00000000 --- a/variants/lilygo_twatch_s3_plus/platformio.ini +++ /dev/null @@ -1,156 +0,0 @@ -[LilyGo_TWatchS3Plus] -extends = esp32_base -extra_scripts = post:merge_firmware.py -board = lilygo_twatch_s3_plus -board_build.flash_mode = qio -board_build.f_flash = 80000000L -board_build.arduino.memory_type = qio_opi -board_upload.flash_size = 16MB -board_build.partitions = variants/lilygo_twatch_s3_plus/meck_twatch_16mb.csv -build_flags = - ${esp32_base.build_flags} - ${sensor_base.build_flags} - -I variants/lilygo_twatch_s3_plus - ; MECK_TWATCH gates the watch form factor (240x240, UI_ZOOM=2, touch, lock - ; screen, tile grid, grey palette). LILYGO_TWATCH_S3_PLUS gates hardware that - ; is specific to the Plus (onboard GNSS on BLDO1, GPIO0 user button). - -D MECK_TWATCH - -D LILYGO_TWATCH_S3_PLUS - -D TWATCH_COMPOSE_ENABLED - -D BOARD_HAS_PSRAM=1 - -D CORE_DEBUG_LEVEL=1 - -D FORMAT_SPIFFS_IF_FAILED=1 - -D FORMAT_LITTLEFS_IF_FAILED=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 - ; ---- LoRa SX1262 ---- - -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 - -D SX126X_DIO2_AS_RF_SWITCH - -D SX126X_DIO3_TCXO_VOLTAGE=1.8f - -D SX126X_CURRENT_LIMIT=140 - -D SX126X_RX_BOOSTED_GAIN=1 - -D P_LORA_NSS=5 - -D P_LORA_DIO_1=9 - -D P_LORA_RESET=8 - -D P_LORA_BUSY=7 - -D P_LORA_SCLK=3 - -D P_LORA_MISO=4 - -D P_LORA_MOSI=1 - ; ---- Display (ST7789 240x240) + FT6336U touch via LovyanGFX ---- - -D DISPLAY_CLASS=TWatchS3PlusDisplay - -D UI_ZOOM=2 - ; ---- Misc ---- - -D AUTO_SHUTDOWN_MILLIVOLTS=3100 - -D ARDUINO_LOOP_STACK_SIZE=32768 - ; ---- GPS (u-blox MIA-M10Q on Serial2) ---- - ; Rail-managed: BLDO1 is off at boot and gated by the gps_enabled pref - ; (defaults to 0) plus the "gps on/off" CLI command. - ; HAS_GPS is a build flag (not only variant.h) so it is defined globally when - ; UITask.h is parsed -- the gotoMapScreen() declaration is behind #if HAS_GPS, - ; and UITask.h is included before target.h/variant.h in the .cpp TUs. - ; PIN_GPS_RX/PIN_GPS_TX feed the shared initBasicGPS() Serial1 path and mirror - ; the GPS_RX_PIN/GPS_TX_PIN values in variant.h (used by the Serial2 provider). - -D HAS_GPS=1 - -D ENV_INCLUDE_GPS=1 - -D ENV_SKIP_GPS_DETECT=1 - -D PIN_GPS_RX=41 - -D PIN_GPS_TX=42 - -D ENV_INCLUDE_AHTX0=0 - -D ENV_INCLUDE_BME280=0 - -D ENV_INCLUDE_BMP280=0 - -D ENV_INCLUDE_SHTC3=0 - -D ENV_INCLUDE_SHT4X=0 - -D ENV_INCLUDE_LPS22HB=0 - -D ENV_INCLUDE_INA3221=0 - -D ENV_INCLUDE_INA219=0 - -D ENV_INCLUDE_INA226=0 - -D ENV_INCLUDE_INA260=0 - -D ENV_INCLUDE_MLX90614=0 - -D ENV_INCLUDE_VL53L0X=0 - -D ENV_INCLUDE_BME680=0 - -D ENV_INCLUDE_BMP085=0 -build_src_filter = ${esp32_base.build_src_filter} - +<../variants/lilygo_twatch_s3_plus> - + -lib_deps = - ${esp32_base.lib_deps} - ${sensor_base.lib_deps} - lovyan03/LovyanGFX @ ^1.2.0 - lewisxhe/XPowersLib @ ^0.2.7 - lewisxhe/SensorLib @ ^0.4.1 - adafruit/Adafruit GFX Library @ ^1.11.0 - bitbank2/PNGdec @ ^1.0.1 - WebServer - DNSServer - Update - -; --------------------------------------------------------------------------- -; Phase 1: standalone (no BLE companion), touch + display + lock screen. -; MAX_CONTACTS=2000 -- contact + sort arrays allocated in PSRAM via -; BaseChatMesh::initContacts(). GPS is compiled in but its BLDO1 rail stays -; off until toggled on ("gps on"); maps, tiles and the on-screen keyboard are -; deferred to later phases. -; --------------------------------------------------------------------------- -[env:meck_twatch_s3_plus_standalone] -extends = LilyGo_TWatchS3Plus -build_flags = - ${LilyGo_TWatchS3Plus.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=2000 - -D MAX_GROUP_CHANNELS=40 - -D OFFLINE_QUEUE_SIZE=1 - -D ESP32_CPU_FREQ=80 - -D FIRMWARE_VERSION='"Meck TWatch v0.3"' -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${LilyGo_TWatchS3Plus.build_src_filter} - + - - - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> - + -lib_deps = - ${LilyGo_TWatchS3Plus.lib_deps} - densaugeo/base64 @ ~1.4.0 -lib_ignore = - AsyncTCP - ESPAsyncWebServer - ESP32 BLE Arduino - -; --------------------------------------------------------------------------- -; BLE companion build -- same touch UI as the standalone, plus the BLE serial -; interface so a phone app can connect on occasion. Differs from standalone: -; BLE_PIN_CODE set, OFFLINE_QUEUE_SIZE raised, SerialBLEInterface.cpp compiled -; in (no longer excluded), and the BLE Arduino lib no longer in lib_ignore. -; Contacts stay PSRAM-allocated via initContacts(), covering the BLE stack's -; internal-SRAM use. ESP32_CPU_FREQ=80 sets the boot clock low; note that -; CPUPowerManager already governs the idle clock to 80 MHz at runtime. -; Flash: pio run -e meck_twatch_s3_plus_ble -t upload -; --------------------------------------------------------------------------- -[env:meck_twatch_s3_plus_ble] -extends = LilyGo_TWatchS3Plus -build_flags = - ${LilyGo_TWatchS3Plus.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=2000 - -D MAX_GROUP_CHANNELS=20 - -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=256 - -D ESP32_CPU_FREQ=80 - -D FIRMWARE_VERSION='"Meck TWatch BLE v0.3"' - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${LilyGo_TWatchS3Plus.build_src_filter} - + - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> - + -lib_deps = - ${LilyGo_TWatchS3Plus.lib_deps} - densaugeo/base64 @ ~1.4.0 -lib_ignore = - AsyncTCP - ESPAsyncWebServer \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/target.cpp b/variants/lilygo_twatch_s3_plus/target.cpp deleted file mode 100644 index 30058b49..00000000 --- a/variants/lilygo_twatch_s3_plus/target.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include "variant.h" -#include "target.h" - -TWatchS3PlusBoard board; - -// LoRa SX1262 on its own SPI bus (the display uses SPI3_HOST separately). -static SPIClass loraSpi; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi); - -WRAPPER_CLASS radio_driver(radio, board); - -ESP32RTCClock fallback_clock; -AutoDiscoverRTCClock rtc_clock(fallback_clock); - -// GPS: u-blox MIA-M10Q on Serial2 (the GPS pins are reclaimed from Serial1 to -// Serial2 in main.cpp setup(), matching the Meck T-Deck Pro pattern). The -// BLDO1 rail that powers the module is off at boot and gated by gps_enabled. -#if HAS_GPS - GPSStreamCounter gpsStream(Serial2); - MicroNMEALocationProvider gps(gpsStream, &rtc_clock); - EnvironmentSensorManager sensors(gps); -#else - SensorManager sensors; -#endif - -#ifdef DISPLAY_CLASS - DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true); -#endif - -bool radio_init() { - // NOTE: board.begin() is called by main.cpp setup() before radio_init(); - // Wire is already initialised there with the correct pins. - fallback_clock.begin(); - rtc_clock.begin(Wire); - - loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); - return radio.std_init(&loraSpi); -} - -uint32_t radio_get_rng_seed() { - return radio.random(0x7FFFFFFF); -} - -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { - radio.setFrequency(freq); - radio.setSpreadingFactor(sf); - radio.setBandwidth(bw); - radio.setCodingRate(cr); - - // Longer preamble for low SF improves reliability -- each symbol is shorter - // at low SF, so more symbols are needed for reliable detection. - uint16_t preamble = (sf <= 9) ? 32 : 16; - radio.setPreambleLength(preamble); -} - -void radio_set_tx_power(uint8_t dbm) { - radio.setOutputPower(dbm); -} - -mesh::LocalIdentity radio_new_identity() { - RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); -} - -void radio_reset_agc() { - // Power-saving RX gain on the watch (deliberate): boosted gain costs - // ~0.6-0.7 mA continuously for ~+2 dB sensitivity. The SetRxGain write - // itself is what resets the AGC; false selects the power-saving value. - radio.setRxBoostedGainMode(false); -} \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/target.h b/variants/lilygo_twatch_s3_plus/target.h deleted file mode 100644 index da37c025..00000000 --- a/variants/lilygo_twatch_s3_plus/target.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -// Include variant.h first to ensure all board-specific defines are available -#include "variant.h" - -#define RADIOLIB_STATIC_ONLY 1 -#include -#include -#include -#include -#include - -#ifdef DISPLAY_CLASS - #include - #include -#endif - -#if HAS_GPS - #include "helpers/sensors/EnvironmentSensorManager.h" - #include "helpers/sensors/MicroNMEALocationProvider.h" - #include "GPSStreamCounter.h" -#else - #include -#endif - -extern TWatchS3PlusBoard board; -extern WRAPPER_CLASS radio_driver; -extern AutoDiscoverRTCClock rtc_clock; - -#if HAS_GPS - extern GPSStreamCounter gpsStream; - extern EnvironmentSensorManager sensors; -#else - extern SensorManager sensors; -#endif - -#ifdef DISPLAY_CLASS - extern DISPLAY_CLASS display; - extern MomentaryButton user_btn; -#endif - -bool radio_init(); -uint32_t radio_get_rng_seed(); -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); -void radio_set_tx_power(uint8_t dbm); -mesh::LocalIdentity radio_new_identity(); -void radio_reset_agc(); \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/variant.h b/variants/lilygo_twatch_s3_plus/variant.h deleted file mode 100644 index 0084821d..00000000 --- a/variants/lilygo_twatch_s3_plus/variant.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -// ============================================================================= -// LilyGo T-Watch S3 Plus - Board-level pin definitions -// Source: LilyGo hardware doc (lilygo-t-watch-s3-plus.md) and the working -// MeshCore companion build. -// -// NOTE: LoRa (P_LORA_*) pins and the SX126x radio parameters are supplied as -// -D build flags in this variant's platformio.ini, matching the Meck T-Deck -// Pro convention. The display SPI/backlight and FT6336U touch pins live inline -// in TWatchS3PlusDisplay.h because LovyanGFX needs them in its panel config. -// ============================================================================= - -// ----------------------------------------------------------------------------- -// Main I2C bus (shared): AXP2101 PMU, PCF8563 RTC, BMA423 accel, DRV2605 haptic -// The FT6336U touch panel is on a SEPARATE bus (Wire1), configured in the -// display class -- it is not on this bus. -// ----------------------------------------------------------------------------- -#define I2C_SDA 10 -#define I2C_SCL 11 - -// Aliases for ESP32Board base class compatibility -#define PIN_BOARD_SDA I2C_SDA -#define PIN_BOARD_SCL I2C_SCL - -// ----------------------------------------------------------------------------- -// I2C device addresses (7-bit) -// ----------------------------------------------------------------------------- -#define I2C_ADDR_PMU 0x34 // AXP2101 power management -#define I2C_ADDR_RTC 0x51 // PCF8563 real-time clock -#define I2C_ADDR_ACCEL 0x19 // BMA423 accelerometer -#define I2C_ADDR_HAPTIC 0x5A // DRV2605 haptic driver -#define I2C_ADDR_TOUCH 0x38 // FT6336U capacitive touch (on Wire1) - -// ----------------------------------------------------------------------------- -// Interrupt / control pins -// ----------------------------------------------------------------------------- -#ifndef PIN_PMU_IRQ - #define PIN_PMU_IRQ 21 // AXP2101 interrupt -#endif -#define PIN_RTC_IRQ 17 // PCF8563 interrupt -#define PIN_ACCEL_IRQ 14 // BMA423 interrupt - -// User button -- GPIO0 "Custom Button" on the T-Watch S3 Plus (idles HIGH, -// active LOW), the same arrangement Meck uses on the T-Deck Pro. -#define PIN_USER_BTN 0 - -// ----------------------------------------------------------------------------- -// Display dimensions (ST7789V3 IPS, 240x240) -// ----------------------------------------------------------------------------- -#define LCD_HOR_SIZE 240 -#define LCD_VER_SIZE 240 - -// ----------------------------------------------------------------------------- -// Storage -// ----------------------------------------------------------------------------- -// The T-Watch S3 Plus has no SD card slot. Notes/Reader/Epub screens reference -// SDCARD_CS unconditionally, so it must be defined for them to compile. -1 is a -// safe no-op on ESP32 (digitalWrite/pinMode reject out-of-range pins), and SD -// mounts will simply fail at runtime. Persistent storage arrives in Phase 3 via -// a LittleFS partition. -#define SDCARD_CS -1 - -// ----------------------------------------------------------------------------- -// GPS (u-blox MIA-M10Q on Serial2) -// Pin values mirror the working MeshCore companion build. Power is on the -// AXP2101 BLDO1 rail and is gated at runtime via the gps_enabled pref + -// board.gpsPowerOn()/gpsPowerOff() -- it is NOT a GPIO enable line, so -// PIN_GPS_EN is deliberately left undefined (the main.cpp PIN_GPS_EN blocks -// are skipped and the watch branch toggles BLDO1 instead). -// -// NOTE: GPS RX/TX wiring labelling is ambiguous between the LilyGo doc and the -// MeshCore build. These values match the verified MeshCore config; if the -// module never acquires, swapping GPS_RX_PIN/GPS_TX_PIN is the first thing to try. -// ----------------------------------------------------------------------------- -#define HAS_GPS 1 -#define GPS_BAUDRATE 38400 -#define GPS_RX_PIN 41 -#define GPS_TX_PIN 42 \ No newline at end of file