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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
17 KiB
F1 — Files and Notes
Status: in progress. The Storage App (issue #3) shipped as v0.9.0 on 2026-10-06. Notes (#19) is built and checked on the device, on branch notes, not merged yet. 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).
The Storage App (issue #3)
Until now the card could be looked at only through the Debug Console (ls, get, put), and Settings > Storage could only delete whole categories by age.
On the card today: six top-level folders, irc, wifi, updates, gnss, gemini and captures. No notes yet; settings are in flash, not on the card.
Decisions (design round 2026-10-06)
| # | Decision |
|---|---|
| Q128 | An App of its own, Storage, in the Launcher. Settings > Storage goes away: its usage figures, Storage Clean-up and "Erase SD card" move into the App, under Maintenance, behind a warning that these delete things for good. |
| Q129 | A row shows the name, then the size or "folder", then the date modified. Folders first, then by name; s cycles the sort (name, date, size). The top line shows the path and the card's free space. |
| Q130 | Nothing is hidden. Read-only: /gemini/cache; any file the firmware has open right now (today's IRC log, a Track or Capture being recorded, a file being received); and the top-level folders themselves, which can't be renamed or deleted though their contents can. Everything else, the user's own data included, can be renamed, moved or deleted, always after a confirmation. |
| Q131 | One item at a time, with a clipboard: Enter opens; Back goes up, and leaves the App at the top; c copy, x cut, v paste into the current folder; r rename; d delete; n new folder; i details. |
| Q132 | Copy, move and delete work on folders too, recursively. The confirmation says what's inside: "Delete saved and its 42 files?". |
| Q133 | A copy is a job on the storage task in 4 KB pieces, with a progress Toast; Back cancels it. It checks free space first and asks before replacing anything. Afterwards the sizes are compared, not the contents: the driver is trusted since v0.6.1 (ADR 0007). A move within the card is a rename. |
| Q134 | Viewers by type. Text (.txt, .log, .gmi, .csv, .gpx, and anything that looks like text): read from the card as you scroll, so size doesn't matter; logs open at the end. .pcap: the LoRa Scanner's packet list. .gpx: a summary (start, duration, points, distance), Tab for the text. .ota: version, size, whether the signature is valid; Enter installs through Update from SD. Anything else: a hex dump. |
| Q135 | No editing: that comes with Notes (#19). |
| Q136 | A listing holds up to 256 entries, packed, about 10 KB; a bigger folder shows the first 256 by name and says how many more there are. The App refuses to open below the memory floors (Q86). |
| Q137 | The Clock also sets the system time, so files are dated correctly with GNSS alone and not only after NTP. A file dated before 2020 shows "-". |
| Q138 | Console: cp, mv and mkdir, next to ls and rm. |
| Q139 | Left out, each with its issue: selecting several items (#41), finding files by name (#42), opening a .gmi in the Gemini App (#43), a table view for .csv (#44), images (#45). |
| Q140 | Ships as v0.9.0 when done and checked. |
Done when
- The Storage App lists any folder of the card with sizes and dates, sorted three ways, and says so when a folder has more than 256 entries.
- A file can be copied, moved, renamed and deleted, and a folder too; a new folder can be made. Each destructive action asks first; a copy shows progress and can be cancelled.
- The read-only rules of Q130 hold, with a reason given when something is refused.
- Each viewer of Q134 opens its type, and a 1 MB text file scrolls without loading whole.
- Maintenance shows the card's usage and does what Settings > Storage did, behind its warning; Settings no longer has a Storage row; the Storage Warning points at the Storage App.
- A file written with only a GNSS Fix (no Wi-Fi) is dated correctly.
- Free heap stays above the floors with the App open, Wi-Fi and IRC on TLS.
Work breakdown
- Model (host-tested): paths and names, the read-only rules, the packed listing and its sorts, file types, sizes and dates for display, the GPX summary.
- Card operations: listing a folder, copy, move, delete (recursive, counted), new folder, as storage jobs with progress and cancel;
cp,mv,mkdir; the Clock sets the system time. - The App: browsing, the clipboard, dialogs, details.
- Maintenance: usage, Clean-up and Erase moved in from Settings, with the warning.
- Viewers: text, hex,
.pcap,.gpx,.ota. - Checks on the device, recorded here.
As built
FileOps(src/services/file_ops) does the card's work for the App and for the console alike: list, count, copy, move, delete, new folder. One operation at a time on the storage task, in turns of about 150 ms that queue themselves again, so Log lines and a Capture are written in between. The rules of Q130 are checked there, whoever asks.- A listing reads the folder straight from FatFs. Through the Arduino
File, every entry was looked up by name again for its size and again for its date: 329 entries took over two seconds. One pass now, and it's there before the screen has redrawn. Counting, copying and deleting still walk withFile; they show progress and can be stopped. - A copy shows its progress in a box in the App, not a Toast (Q133): it has a bar and says Back cancels. A cancelled or failed copy deletes what it had written. The copy gets today's date, like
cp. - The viewers (
src/apps/file_viewer, models inlib/files): text throughTextPager, which reads about a kilobyte around the screen and wraps at spaces, 38 columns; going back a line wraps the paragraph before again, so a file reads the same in both directions. A.pcap, a.gpxand an.otaare read through once by a storage job, in the same 150 ms turns. An Update File is fed to the installer's own parser with a sink that writes nothing, so "would it install" is the same answer an install gives. - Tab in a viewer shows the same file as hex, or as text (not in Q134).
- Maintenance is the last row at the top of the card, and
manywhere in the App. It's the old Settings > Storage page behind a dialog. - The Storage Warning was only ever a Toast; "selecting it opens Storage Clean-up" (CONTEXT.md) was never built. It now reads "SD card over 80% full: see Storage".
- Console:
cp,mv,mkdir(Q138), andrmandduthrough the same code, sormnow takes folders and follows the rules;lsshows dates. Debug Builds:sd fill <folder> <count>makes test files.
Checks on the device (2026-10-06, v0.8.1-2 Debug Build)
All in a scratch folder, /f1test, removed afterwards.
| Check | Result |
|---|---|
| Host tests | 424 pass (411 before the viewers' models) |
| Browsing | Folders first, sizes and dates, the three sorts; a 300-file and a 329-file folder show "first 256 of 300" and "of 329" |
| New folder, rename, copy, cut and paste, delete | Each works on a file and on a folder; a copy next to its original is named (2); a name in the way asks "Replace it?" |
| A folder of 11 files, 8.4 MB, copied | 19.4 s, 435 KB/s, the bar moving; two Log lines queued meanwhile were written |
| The same copy cancelled at 1.8 MB | "Cancelled: nothing was copied", and nothing was left behind |
| Delete | 341 files in 9.7 s; the dialog had counted them first |
| Read-only rules | /irc, /gnss (top-level folders), /, /gemini/cache and a folder made inside it, a folder into itself, a name with :; a Capture being recorded and the folder holding it; a folder under /irc while IRC runs. Each refused with its reason; the Capture could still be copied |
| Text | A 1 MB log opens at its last line at once; top, pages, lines; a file without an extension that looks like text opens as text |
| Hex | A 5 KB binary file; Tab from any other viewer |
.pcap |
A LoRa Capture: 3 packets as the Scanner lists them, Enter shows the Meshtastic header and bytes |
.gpx |
400 points: start, 33 min 15 s, 4.68 km; Tab shows the text |
.ota |
A signed file: version, "intact", "older than what's running", Enter asks to install (not confirmed). A tampered one: "image corrupted (hash mismatch)" |
| Maintenance | The warning, then usage, Clean-up's categories and Erase (not run) |
| Date with GNSS only | NTP pointed at an address that doesn't answer, restart: the Clock came from the Fix, and a folder made then is dated 2026-10-06 08:39. A Track from the day before, written the same way by v0.8.1, shows "-" |
| Memory | IRC connected, the App open on 256 entries: 61 KB free (70 KB before opening). Lowest since boot 29.7 KB, during IRC's TLS handshake |
| Stacks | storage 3.1 KB free of 6 KB at worst, loopTask 1.5 KB |
Not checked by hand: how the keys feel on the device itself; everything above was driven through the Debug Console's key command and screenshots.
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.
ein 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
- Model (host-tested): the text buffer with its cursor, wrapping and scrolling; file names from first lines.
- The editor on the device: loading, drawing, keys, autosave through a temporary file, recovery.
- The Notes App: the list with titles, new, rename, delete.
ein the Storage App.- Checks on the device, recorded here.
As built
NoteText(lib/notes, host-tested) is the text, its cursor and the screen around it. A line owns the space or the newline it ends with, so every byte is on exactly one line and the cursor has one place for each. No index of lines is kept: a note of newlines alone would need twice its own size for one. Where a line starts is worked out from the start of its paragraph.- One buffer, 16 KB, for as long as the editor is open. It's reserved when the note is opened, the file is read straight into it, and typing never makes it grow. On this device a failed allocation is an abort, and with IRC connected the largest free block is about 31 KB whatever the total says: the first version read the file into one string and copied it into another, and opening a full note with IRC connected restarted the device. The editor now also refuses to open without a free block of 24 KB.
NoteEditor(src/apps/note_editor) is shared by the Notes App and the Storage App'se. A save runs on the storage task while the main loop waits for it: no second copy of the note, and at 16 KB the wait is a fraction of a second at a moment when nobody has typed for five.- A save writes
<note>.tmp, checks its size, deletes the note and renames the temporary file (FAT can't rename onto a file). A cut between the last two steps leaves only the.tmp: the Notes list puts such a file back under its name. A.tmpnext to its note is an unfinished save: opening the note offers it. - Titles in the list are read from the card for the eight rows on screen, when the list moves.
- Before powering off, the firmware now leaves the foreground App (
PowerService::beforePowerOff), which makes the editor save. - Shift or Alt with Fn+Up and Fn+Down moves a page (not in Q145).
Found on the way
- The screen could go "off" for one tick after a key sent through the Debug Console, and the next key was then swallowed as a wake-up: the
keycommand stamps the power timer frommillis(), the power tick compares with its pass's older time, and the unsigned difference read as 49 days idle. The same shape as #46. Fixed inPowerPolicy::updatewith a test. Keys from the keyboard were never affected. It explains remote keys "lost" in earlier sessions. scripts/rdbg.pyheld back piped lines written while it was still connecting, until the next line came (a bufferedreadline()behindselect()). Fixed.
Checks on the device (2026-10-06, Debug Build of branch notes)
Test notes were made in /notes and removed afterwards; the folder is left, empty.
| Check | Result |
|---|---|
| Host tests | 439 pass |
| A first note | "No notes yet", n, typed three lines: the top line says "typing", then "saved" five seconds after the last key, under shopping-list.txt. 63 keys in a row all arrived |
| Leaving | Back saves and returns to the list, which shows the note under its first line. Home in the middle of a new note saved it as ideas.txt |
| The cursor | Down, Right, an insertion in the middle of a line; the screen scrolls through a note of about 230 lines |
| A power cut | Typed, waited seven seconds, typed more and restarted the device at once (reset): the note has what was saved, whole, and not the last keys |
| An unfinished save | A .tmp next to its note: "Unsaved copy... Keep the note / Use the copy"; using it brings its text back and saves it. A .tmp alone was put back under its name when the list opened |
| 16 KB | A note of exactly 16,384 bytes opens and scrolls; one more character: "This note is full: 16 KB". A file of 16,398 bytes: "Too big to edit: 16 KB at most" |
| Rename, delete, sort | r renamed orphan.txt to orphan2.txt; d asked, then deleted; s switched between newest first and by file name |
e in the Storage App |
A note opened from the text viewer, edited, saved on Back; the listing shows its new size |
| Memory | IRC connected, the full 16 KB note open: 55 KB free, largest block 31.7 KB (72 KB free before opening) |
Not checked: accents through the Compose Key and Ctrl+A / Ctrl+E (the remote key command can't send them; the model's tests cover both), the power button's save (it needs a hand on the device), a missing card, and how typing feels on the keyboard itself.
One slip during the checks: a key sequence sent right after a restart opened IRC instead of Notes, and the test letters went into IRC's input line. Nothing was sent: the line was cleared and the App left. IRC connected to Libera as it does when opened.