Files

162 lines
17 KiB
Markdown

# F1 — Files and Notes
**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.