From 2d5b9b14320f9da4e18a9532f4a42e84179860a9 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:54:46 +1000 Subject: [PATCH] =?UTF-8?q?T-Watches.:=20S3=20fix=20(CHG=5FVOL=5F4V2=20?= =?UTF-8?q?=E2=86=92=20CHG=5FVOL=5F4V35=20in=20TWatchS3Board.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes screen: WatchNotesScreen.h → both variants/lilygo_twatch_s3/ and variants/lilygo_twatch_s3_plus/ (byte-identical copies, like TWatchComposeScreens.h). TWatchComposeScreens.h → both variant dirs. Adds TWKB_NOTE to the Purpose enum and a setInitialText() (clamped at the 133-char MAXLEN) for prefill editing. UITask.h → watch_notes_screen member, isOnWatchNotesScreen(), getWatchNotesScreen(), all MECK_TWATCH-gated, mirroring the alarm-screen pattern exactly. UITask.cpp → include, construction in begin() (after the LittleFS mount, like the alarm), gotoNotesScreen() now routes to the watch screen on MECK_TWATCH (T-Deck/T5S3 behaviour untouched — the SD NotesScreen path below is unreached on watches), the TWKB_NOTE result case (save via applyComposedNote, alert on error, return to the notes screen), and the loop poll that opens the keyboard — prefilling via setInitialText(getEditText()) when editPending(). main.cpp → include plus the tap-dispatch block, same shape as the alarm's. --- examples/companion_radio/main.cpp | 12 + examples/companion_radio/ui-new/UITask.cpp | 35 ++ examples/companion_radio/ui-new/UITask.h | 9 + .../companion_radio/ui-new/WatchNotesScreen.h | 433 ++++++++++++++++++ .../lilygo_twatch_s3/TWatchComposeScreens.h | 11 +- variants/lilygo_twatch_s3/TWatchS3Board.cpp | 7 +- .../TWatchComposeScreens.h | 32 +- 7 files changed, 519 insertions(+), 20 deletions(-) create mode 100644 examples/companion_radio/ui-new/WatchNotesScreen.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d8bb1259..038c12bb 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1128,6 +1128,9 @@ static uint32_t _atoi(const char* sp) { #if defined(LILYGO_TWATCH_S3) #include "WatchAlarmScreen.h" // After UITask.h -- needs NodePrefs #endif + #if defined(MECK_TWATCH) + #include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs + #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) @@ -1374,6 +1377,15 @@ static void lastHeardToggleContact() { 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; + } +#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 diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 7db11bdc..23086247 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -82,6 +82,9 @@ #if defined(LILYGO_TWATCH_S3) #include "WatchAlarmScreen.h" #endif +#if defined(MECK_TWATCH) +#include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs +#endif #ifdef TWATCH_COMPOSE_ENABLED #include "TWatchComposeScreens.h" #endif @@ -1792,6 +1795,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no watch_alarm_screen = new WatchAlarmScreen(this, &rtc_clock, node_prefs); ((WatchAlarmScreen*)watch_alarm_screen)->load(); #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); +#endif #ifdef TWATCH_COMPOSE_ENABLED tw_picker = new TWatchChannelPicker(_display); tw_channel = new TWatchChannelScreen(_display); @@ -2599,6 +2606,13 @@ if (curr) curr->poll(); 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 { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen(); if (admin) { @@ -2618,6 +2632,16 @@ if (curr) curr->poll(); 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()); + } + } + } #endif if (_display != NULL && _display->isOn()) { @@ -3556,6 +3580,17 @@ 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; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 1db1fab9..ae2ffb3f 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -141,6 +141,9 @@ class UITask : public AbstractUITask { #endif #if defined(LILYGO_TWATCH_S3) UIScreen* watch_alarm_screen; // Vibrate-only alarm clock (no audio hardware) +#endif +#if defined(MECK_TWATCH) + UIScreen* watch_notes_screen; // LittleFS note pad (shared NotesScreen needs SD) #endif UIScreen* _screenBeforeLock = nullptr; bool _locked = false; @@ -319,6 +322,9 @@ public: #endif #if defined(LILYGO_TWATCH_S3) bool isOnWatchAlarmScreen() const { return curr == watch_alarm_screen; } +#endif +#if defined(MECK_TWATCH) + bool isOnWatchNotesScreen() const { return curr == watch_notes_screen; } #endif bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; } bool isOnMapScreen() const { return curr == map_screen; } @@ -415,6 +421,9 @@ public: #endif #if defined(LILYGO_TWATCH_S3) UIScreen* getWatchAlarmScreen() const { return watch_alarm_screen; } +#endif +#if defined(MECK_TWATCH) + UIScreen* getWatchNotesScreen() const { return watch_notes_screen; } #endif UIScreen* getMinesweeperScreen() const { return minesweeper_screen; } UIScreen* getMapScreen() const { return map_screen; } diff --git a/examples/companion_radio/ui-new/WatchNotesScreen.h b/examples/companion_radio/ui-new/WatchNotesScreen.h new file mode 100644 index 00000000..8cc39664 --- /dev/null +++ b/examples/companion_radio/ui-new/WatchNotesScreen.h @@ -0,0 +1,433 @@ +#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/variants/lilygo_twatch_s3/TWatchComposeScreens.h b/variants/lilygo_twatch_s3/TWatchComposeScreens.h index 81ffee25..98128d51 100644 --- a/variants/lilygo_twatch_s3/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3/TWatchComposeScreens.h @@ -406,7 +406,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE }; private: Purpose _purpose; @@ -507,6 +507,15 @@ public: 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; } diff --git a/variants/lilygo_twatch_s3/TWatchS3Board.cpp b/variants/lilygo_twatch_s3/TWatchS3Board.cpp index 6037d6f8..c053f563 100644 --- a/variants/lilygo_twatch_s3/TWatchS3Board.cpp +++ b/variants/lilygo_twatch_s3/TWatchS3Board.cpp @@ -184,7 +184,10 @@ bool TWatchS3Board::power_init() { XPOWERS_AXP2101_PKEY_POSITIVE_IRQ); PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_125MA); - PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); + // 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(); @@ -223,4 +226,4 @@ uint32_t TWatchS3Board::getStepCount() { 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/TWatchComposeScreens.h b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h index 626b76c0..98128d51 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h @@ -91,17 +91,6 @@ class TWatchChannelPicker : public UIScreen { _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) @@ -148,11 +137,11 @@ public: _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 + if (held >= TW_LONG_PRESS_MS) { + if (_numChannels > 0) _confirmed = true; // select highlighted + } else { + if (_downY < _display->height() / 2) moveUp(); else moveDown(); // tap zones + } } } @@ -417,7 +406,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE }; private: Purpose _purpose; @@ -518,6 +507,15 @@ public: 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; }