From 4ab873e9f8fc9af707d4f7f148437d698efbf940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Tue, 6 Oct 2026 09:29:38 +0200 Subject: [PATCH 1/4] F1 plan: the Notes design round (Q141-Q150) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- docs/milestones/F1.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/milestones/F1.md b/docs/milestones/F1.md index 638ee5e..2e55cad 100644 --- a/docs/milestones/F1.md +++ b/docs/milestones/F1.md @@ -1,6 +1,6 @@ # F1 — Files and Notes -**Status:** in progress. The Storage App (issue #3) shipped as **v0.9.0** on 2026-10-06. Notes (#19) and the card as a USB drive (#1) come after. +**Status:** in progress. The Storage App (issue #3) shipped as **v0.9.0** on 2026-10-06. Notes (#19) is being built on branch `notes`. The card as a USB drive (#1) comes after. **Goal:** get at what's on the SD card from the device itself: browse it, look inside the files the firmware writes, copy, move, rename and delete, and keep notes. A side milestone, like G1 and S1; Files and Notes were M3's original second half (Q30, Q89). @@ -86,3 +86,40 @@ All in a scratch folder, `/f1test`, removed afterwards. **One slip during the checks:** a scripted key sequence ran in the wrong folder and renamed `/gemini/saved` to `saved2`, then copied it to the top of the card. Both were put right at once (renamed back, the copy deleted; 7 files, 53,798 bytes, as before). **Found on the way:** a panic at Wi-Fi join, there since v0.7.0 (SNTP started twice, issue #46). Fixed in v0.9.0. + +## Notes (issue #19) + +Plain text notes on the SD card, written on the device. Q30 settled the base: `.txt` files in `/notes`, created, edited and deleted from the device, never offered by Storage Clean-up. + +### Decisions (design round 2026-10-06) + +| # | Decision | +|---|---| +| Q141 | A **Notes** App in the Launcher. One row per note: its first line as the title, then the date. Newest first; `s` switches to by name. `n` new, Enter opens, `d` deletes after a confirmation, `r` renames the file. | +| Q142 | A new note's file name is never typed: it comes from the first line when the note is first saved (`shopping-list.txt`), or `note-20261006-0919.txt` if that line is empty. It doesn't change afterwards unless the note is renamed. | +| Q143 | **Autosave, no "discard changes?" prompt:** five seconds after the last key, on leaving the note or the App, and when the screen turns off. A save writes a temporary file and renames it over the note, so a power cut loses the last few seconds at most. A temporary file left behind is offered back at the next open. | +| Q144 | The whole note is in memory while it's edited, up to **16 KB**. A bigger text file opens read-only in the Storage App's viewer. The App refuses to open below the memory floors (Q86). **Editing files of any size must come in a later release: issue #47.** | +| Q145 | The editor wraps at spaces, 38 columns by 8 rows, with a line for the name and the state. Enter is a new line, Del deletes backwards, Fn+arrows move (the Text Entry rule), Ctrl+A and Ctrl+E go to the start and the end of the line, Tab types two spaces, Back saves and returns. The Compose Key works as elsewhere. | +| Q146 | The Storage App's text viewer gets `e`: edit this file with the same editor, for a text file up to 16 KB that isn't read-only. That lifts Q135 without Apps opening each other (#43 stays). | +| Q147 | The list is flat: the files directly in `/notes`. Sub-folders are reached through the Storage App. | +| Q148 | UTF-8, LF line ends; a file with CRLF is saved back with LF. Characters the font lacks are kept on save. | +| Q149 | Left out, each with its issue: editing files of any size (#47), searching inside notes (#48), undo (#49), selecting and copying text (#50). | +| Q150 | Ships as **v0.10.0** when built and checked on the device. | + +### Done when + +- A note can be started, typed with accents, left and found again in the list under its first line; renamed; deleted after a confirmation. +- What's typed is on the card five seconds after the last key, and after Back, Home, or the screen turning off, without a prompt. +- Pulling the power while typing loses a few seconds at most, and the note is never left empty or half-written. +- The cursor moves by character and by line through wrapped text, and the screen follows it; a 16 KB note edits without lag. +- A note at 16 KB refuses more text and says so; a bigger file opens read-only. +- `e` in the Storage App's text viewer edits a file; a read-only one is refused with its reason. +- Free heap stays above the floors with a 16 KB note open and IRC connected. + +### Work breakdown + +1. **Model** (host-tested): the text buffer with its cursor, wrapping and scrolling; file names from first lines. +2. **The editor on the device:** loading, drawing, keys, autosave through a temporary file, recovery. +3. **The Notes App:** the list with titles, new, rename, delete. +4. **`e` in the Storage App.** +5. **Checks on the device**, recorded here. From c868977f1c3e9d6166ab7a3659e1a56778f5420a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Tue, 6 Oct 2026 10:08:10 +0200 Subject: [PATCH 2/4] A key sent through the Debug Console could turn the screen "off" for a tick The `key` command stamps the power timer from millis(); the power tick then compared with its pass's older time, and the unsigned difference read as 49 days without a key. The screen state went Off for one tick, and the next key was swallowed as a wake-up: about one remote key in twenty-five. Keys from the keyboard pass the loop's own time and were never affected. The same shape as #46. PowerPolicy::update now treats a stamp from the future as "just now", with a test. rdbg.py: piped lines written while it was still connecting stayed in Python's read buffer until the next line arrived (readline() behind select()). It reads the descriptor directly now. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- lib/services/src/power_policy.cpp | 5 ++++- scripts/rdbg.py | 11 ++++++++--- test/test_power/test_power.cpp | 10 ++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/services/src/power_policy.cpp b/lib/services/src/power_policy.cpp index 0fd5b01..bb48c4a 100644 --- a/lib/services/src/power_policy.cpp +++ b/lib/services/src/power_policy.cpp @@ -10,7 +10,10 @@ bool PowerPolicy::activity(uint32_t nowMs) { } ScreenState PowerPolicy::update(uint32_t nowMs) { - uint32_t idle = nowMs - lastActivityMs_; + // Activity stamped from a clock read after this pass's nowMs (the Debug Console's `key`) is in + // the future, not 49 days ago: unsigned, the screen went off for a tick and ate the next key. + int32_t since = static_cast(nowMs - lastActivityMs_); + uint32_t idle = since < 0 ? 0 : static_cast(since); state_ = idle >= offMs_ ? ScreenState::Off : idle >= dimMs_ ? ScreenState::Dimmed : ScreenState::On; if (notifying_) { if (static_cast(nowMs - notifyUntilMs_) >= 0) diff --git a/scripts/rdbg.py b/scripts/rdbg.py index d75bba8..deccc60 100755 --- a/scripts/rdbg.py +++ b/scripts/rdbg.py @@ -224,10 +224,15 @@ def interactive(sock): sys.stdout.write(data.decode(errors="replace")) sys.stdout.flush() if sys.stdin in ready: - line = sys.stdin.readline() - if not line: + # Straight from the descriptor: readline() takes every waiting line into Python's own + # buffer and hands over one, and select() then sees nothing more to read. Piped input + # written while we were still connecting got stuck until the next line came. + data = os.read(sys.stdin.fileno(), 4096) + if not data: return - sock.sendall(line.encode()) + sock.setblocking(True) + sock.sendall(data) + sock.setblocking(False) def main(): diff --git a/test/test_power/test_power.cpp b/test/test_power/test_power.cpp index 60bd00d..93fba26 100644 --- a/test/test_power/test_power.cpp +++ b/test/test_power/test_power.cpp @@ -31,6 +31,15 @@ void test_key_on_off_screen_only_wakes() { TEST_ASSERT_EQUAL(static_cast(ScreenState::On), static_cast(p.update(70001))); } +// A key stamped a few milliseconds after the time the same pass updates with must not read as +// 49 days of idleness: the screen stays on, and the next key acts. +void test_activity_stamped_after_the_pass_time_keeps_the_screen_on() { + PowerPolicy p(30000, 60000); + TEST_ASSERT_FALSE(p.activity(10003)); + TEST_ASSERT_TRUE(p.update(10000) == ScreenState::On); + TEST_ASSERT_FALSE(p.activity(10050)); +} + void test_new_timeouts_apply_immediately() { PowerPolicy p(30000, 60000); p.activity(0); @@ -91,6 +100,7 @@ int main() { RUN_TEST(test_screen_dims_then_turns_off_after_inactivity); RUN_TEST(test_key_on_dimmed_screen_wakes_and_still_acts); RUN_TEST(test_key_on_off_screen_only_wakes); + RUN_TEST(test_activity_stamped_after_the_pass_time_keeps_the_screen_on); RUN_TEST(test_new_timeouts_apply_immediately); RUN_TEST(test_notification_lights_an_off_screen_dimmed_until_it_ends); RUN_TEST(test_notification_does_not_count_as_activity); From e8a654a15fcf8f786e19efc62a5667409e14a086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Tue, 6 Oct 2026 10:08:10 +0200 Subject: [PATCH 3/4] Notes: plain text notes on the SD card, with an editor that saves by itself (#19) The Notes App lists the files of /notes by their first line, newest first: n starts a note, Enter opens it, r renames its file, d deletes it after asking, s sorts by name. A new note's file is named after its first line. The editor wraps at spaces, 38 columns by 8 rows; Fn+arrows move through the wrapped text, Ctrl+A and Ctrl+E go to the ends of the line. There is no save key: the note is written five seconds after the last key, on Back, on leaving the App, when the screen turns off and before the device powers off. A save writes a temporary file and puts it in the note's place; a save cut short is put back, or offered, the next time. A note is up to 16 KB, held in one buffer reserved when it's opened: the file is read straight into it and typing never makes it grow. A failed allocation aborts on this device, and with IRC connected the largest free block is about 31 KB: a first version that copied the note once on loading restarted the device when a full note was opened with IRC connected. Editing files of any size is #47. The Storage App's text viewer gets `e`, which edits a text file up to 16 KB with the same editor unless the file is read-only. 439 host tests. Checked on the device: docs/milestones/F1.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- CONTEXT.md | 4 + README.md | 12 +- docs/milestones/F1.md | 38 ++- lib/notes/src/note_text.cpp | 329 +++++++++++++++++++++++ lib/notes/src/note_text.h | 81 ++++++ src/apps/file_viewer.cpp | 14 +- src/apps/file_viewer.h | 8 + src/apps/note_editor.cpp | 303 ++++++++++++++++++++++ src/apps/note_editor.h | 60 +++++ src/apps/notes_app.cpp | 346 +++++++++++++++++++++++++ src/apps/notes_app.h | 71 +++++ src/apps/storage_app.cpp | 32 ++- src/apps/storage_app.h | 10 +- src/main.cpp | 6 +- src/services/power_service.cpp | 1 + src/services/power_service.h | 5 + test/test_note_text/test_note_text.cpp | 288 ++++++++++++++++++++ 17 files changed, 1599 insertions(+), 9 deletions(-) create mode 100644 lib/notes/src/note_text.cpp create mode 100644 lib/notes/src/note_text.h create mode 100644 src/apps/note_editor.cpp create mode 100644 src/apps/note_editor.h create mode 100644 src/apps/notes_app.cpp create mode 100644 src/apps/notes_app.h create mode 100644 test/test_note_text/test_note_text.cpp diff --git a/CONTEXT.md b/CONTEXT.md index 45a41c6..863c25c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -128,6 +128,10 @@ The Notification raised once per boot when the SD card passes 80% full. It point **Storage Clean-up**: The screen where the user deletes old Logs and Captures by category and age, with a preview of the space freed. Notes are never offered for deletion. It lives in the Storage App, under Maintenance. +**Note**: +A plain text file in `/notes`, written on the device in the Notes App. Listed by its first line. Saved without being asked; never offered by Storage Clean-up. +_Avoid_: memo, document + **Storage App**: The App that browses the SD card: folders and files, a clipboard for one item at a time (copy, cut, paste), rename, delete, new folder, and a viewer for each kind of file the firmware writes. The top-level folders, `/gemini/cache` and files being written are read-only. _Avoid_: file manager, Files, explorer diff --git a/README.md b/README.md index fa7cae7..2b989bc 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ A copy runs in the background of the card (about 400 KB a second) in short turns Enter on a file opens it by type; Tab switches to the same file as a hex dump or as text: -- **Text** (`.txt`, `.log`, `.gmi`, `.csv`, and anything that looks like text): only the screen's worth is read from the card, so a file of any size opens at once. Logs open at the end. Up and Down move a line, Left and Right a page, `t` and `b` go to the top and the end. +- **Text** (`.txt`, `.log`, `.gmi`, `.csv`, and anything that looks like text): only the screen's worth is read from the card, so a file of any size opens at once. Logs open at the end. Up and Down move a line, Left and Right a page, `t` and `b` go to the top and the end, `e` edits it (up to 16 KB, see Notes). - **Captures** (`.pcap`): the packets as the LoRa Scanner lists them; Enter shows one with its Meshtastic header and bytes. - **Tracks** (`.gpx`): the number of points, the start, the duration and the distance. - **Update Files** (`.ota`): the version, and whether the file would install: it's checked as an install checks it (signature and contents) without writing anything. Enter then installs it. @@ -107,6 +107,16 @@ Enter on a file opens it by type; Tab switches to the same file as a hex dump or At the top of the card the last row, **Maintenance** (also `m`), holds the card's usage, Storage Clean-up and "Erase SD card", behind a warning: those delete for good. It replaces Settings > Storage. +## Notes + +The Notes App (docs/milestones/F1.md) keeps plain text notes in `/notes` on the SD card. The list shows each note's first line and its date, newest first; `s` switches to by file name. `n` starts a note, Enter opens one, `r` renames its file, `d` deletes it after asking. + +In the editor, type. Enter starts a line, Del deletes backwards, Fn with the arrows moves the cursor through the wrapped text, Ctrl+A and Ctrl+E go to the start and the end of the line, Tab types two spaces, and the Compose Key gives accents as everywhere. **There's no save key:** the note is written five seconds after the last key, on Back, on leaving the App, when the screen turns off and before the device powers off. The top line says "typing" or "saved". Each save writes a temporary file and then puts it in the note's place, so a power cut costs a few seconds of typing and never the note; if a save was cut short, opening the note offers its copy back. + +A new note has no file until something is typed; its file is then named after its first line (`shopping-list.txt`), or `note--