Files
roro9stack/site/content/dev/milestones/f1.md
T
twislaandClaude Sonnet 5.5 3b4100dc6a
CI / build (pull_request) Successful in 8m38s
Site / build (pull_request) Successful in 9s
Site: the developer docs (phase 4), with the Debug Builds and the Debug Console first
/dev/ has Debug Builds and the Debug Console (builds and the token, the
console and its protocol, files and screenshots, driving the UI, crashes and
Safe Mode, the command reference), Build, test and release (including how an
update works), the architecture decisions and the milestone plans.

Generated from the repository by site/tools/gen_dev_docs.py: the ADRs, the
milestones, the README's sections, and the command reference, read from the
firmware's own `help` text. The pages are committed (Zola cannot read outside
its folder); the Site workflow checks they are current, and now also runs
when src/main.cpp changes. M0, M1 and CONTEXT.md are not published.
README: the gnss commands that the table lacked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 21:25:33 +02:00

18 KiB

+++ title = "Files and Notes" description = "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…" weight = 60

[extra] docs = true source = "docs/milestones/F1.md" tag = "F1" +++ Status: in progress. The Storage App (issue #3) shipped as v0.9.0 on 2026-10-06. Notes (#19) shipped as v0.10.0 the same day. 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

  1. 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.
  2. 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.
  3. The App: browsing, the clipboard, dialogs, details.
  4. Maintenance: usage, Clean-up and Erase moved in from Settings, with the warning.
  5. Viewers: text, hex, .pcap, .gpx, .ota.
  6. 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 with File; 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 in lib/files): text through TextPager, 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 .gpx and an .ota are 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 m anywhere 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), and rm and du through the same code, so rm now takes folders and follows the rules; ls shows 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.
  • 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.

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's e. 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 .tmp next 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 key command stamps the power timer from millis(), 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 in PowerPolicy::update with a test. Keys from the keyboard were never affected. It explains remote keys "lost" in earlier sessions.
  • scripts/rdbg.py held back piped lines written while it was still connecting, until the next line came (a buffered readline() behind select()). 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.