Public Access
Storage: view pictures, PNG, JPEG, BMP and GIF (#45)
Enter on a picture shows it: shrunk to fit the screen, or at its own size with Enter again and the arrows to move. Dithered to the screen's 256 colours; a colour the screen has exactly is left alone, so screenshots are shown as they are. The picture is decoded once, straight into the screen's buffer, and kept there (App::retainsContent): no copy in memory. Decoding runs on the storage task, so the keys keep working and a 12 megapixel photograph appears as it comes instead of tripping the watchdog. PNG, BMP and GIF are read by decoders of our own, host-tested against files made by Pillow; the PNG one needs 32 KB where the display library's needed 44 KB in one block, which the device often doesn't have. JPEG uses the library's TJpgDec. Also corrects two sentences that still gave 16 KB as the editing limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -224,3 +224,77 @@ Test notes were copied to `/notes` and removed afterwards; the note that was alr
|
||||
**Not checked:** the power button's path (side file only), the screen turning off, a card pulled while editing, and memory with IRC connected, which wasn't connected for these checks: the editor's own use hasn't changed, and it still refuses to open without a free block of 24 KB. The real keyboard's Ctrl with Fn and the arrows. A file of tens of megabytes. Renaming or deleting a note from the Storage App leaves its side file behind.
|
||||
|
||||
**Measured against what was said:** the first build rewrote 1.2 MB in 3.5 to 4.5 s, with 2 KB blocks. With 4 KB blocks it is 2.6 s, about 450 KB a second, which is what the card gives a plain copy.
|
||||
|
||||
## Pictures in the Storage App (issue #45)
|
||||
|
||||
Q139 left images out: the firmware wrote none. Since the Shell's `screenshot` it does.
|
||||
|
||||
### Decisions (design round 2026-10-07)
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| Q233 | **PNG, JPEG, BMP and GIF.** A GIF shows its first picture; it doesn't move. |
|
||||
| Q234 | *Revised while building.* **Our own PNG decoder**, a row at a time, with the window the file's compression asks for: 32 KB at most. The plan was the display library's, which takes 44 KB in one block: after one picture the largest free block was 43 to 47 KB, and every second PNG was refused. **The firmware's own screenshots** are read with no decoding at all: they are stored uncompressed, each byte already a colour of the screen. |
|
||||
| Q235 | **The picture is decoded once, straight into the screen's buffer, and left there.** No copy in memory (it would be up to 30 KB). `App::retainsContent()` tells the screen not to clear the App's part; `contentLost()` tells the App that it was cleared after all, or that a Toast or the help panel drawn over it has gone: then it is decoded again. |
|
||||
| Q236 | **Shrunk to fit; Enter shows it at its own size**, the arrows then moving half a screen. A picture smaller than the screen sits in the middle at its size. |
|
||||
| Q237 | **Ordered dithering** to the screen's 256 colours (a 4 x 4 pattern). A colour the screen has exactly comes out as itself wherever it lands, so a screenshot isn't touched. |
|
||||
| Q238 | Shrinking takes, for each pixel of the screen, the first of the picture's that falls on it. No averaging: there is nowhere to keep the sums. Thin lines break up; a JPEG looks better, its decoder halving it up to three times first. |
|
||||
| Q239 | **What can't be shown opens as hex, with the reason:** a progressive JPEG, an interlaced PNG, a BMP that is compressed or has 16 bits. The rotation a camera stores in the file is ignored. |
|
||||
| Q240 | *Revised while building.* **Decoding runs on the storage task while the main loop goes on.** The picture appears as it comes, and anything that needs the screen back stops the decoding first. The plan was to wait for it, behind a "Decoding..." line: 12 megapixels took longer than the watchdog allows the main loop to stand still, and the device restarted. |
|
||||
| Q241 | The Storage App: Enter on `.png`, `.jpg`, `.jpeg`, `.bmp`, `.gif`, or on a file with no known extension whose first bytes say what it is. Tab gives the hex. `i`, and opening, show the size in pixels on the last line for three seconds. |
|
||||
| Q242 | Not in this one: animation, opening a picture from the Gemini App, a slideshow. |
|
||||
|
||||
### As built
|
||||
|
||||
- **`lib/files/src/image_file.h`** (host-tested): what a file is and how big, where each pixel lands (`ImageFrame`, `ImageMap`), the dithering, and readers for BMP and GIF that hand their pixels on as they get them. **`png_reader.h`**: the PNG decoder, with its own inflate: every colour type and bit depth, palettes with transparency. Transparent pixels are left as the background.
|
||||
- **`ImagePane`** (`src/apps/image_pane`) is the view. One decoding is a `Job` shared with the storage task; `cancel()` flags it and waits behind it in the task's queue, which is how the screen is known to be free again.
|
||||
- **JPEG is the one decoder that isn't ours:** the display library's TJpgDec, with its 3.9 KB pool. It shrinks by 2, 4 or 8 while decoding, which is why a photograph is possible at all.
|
||||
- **A decoder stops early** once the rest of the file is below the screen (a picture at its own size), and a BMP's rows that aren't shown aren't read.
|
||||
- **The note** on the last line is written over the picture; the strip under it (2.6 KB) is kept and put back, so showing it costs no decoding.
|
||||
- **A BMP is read in the order its rows are stored**, last row first: reading it top to bottom meant going back through the file for every row, a second for 135 rows.
|
||||
- **Cost:** 21 KB of flash. Nothing while no picture is shown.
|
||||
|
||||
### Measured on the device
|
||||
|
||||
| Picture | Fitted | Its own size |
|
||||
|---|---|---|
|
||||
| A screenshot of ours, 240 x 135 | 80 ms | 78 ms |
|
||||
| PNG, 800 x 600 | 855 ms | 575 ms |
|
||||
| PNG with transparency, 800 x 600 | 1,098 ms | |
|
||||
| JPEG, 800 x 600 | 305 ms | |
|
||||
| JPEG, 4000 x 3000 (2.6 MB) | 6.9 s | 7.7 s (the middle of it) |
|
||||
| GIF, 800 x 600 | 642 ms | |
|
||||
| BMP, 800 x 600 (1.4 MB) | 991 ms | |
|
||||
| BMP, 240 x 135 | 124 ms | |
|
||||
|
||||
Free memory fell to 48.8 KB at the lowest while a PNG was decoded, from 104 KB. The storage task's stack: 3.2 KB never used, of 6.
|
||||
|
||||
### Host tests (20, `test/test_image_file` and `test/test_png_reader`)
|
||||
|
||||
The pictures in them were made with Pillow, so the readers are checked against an encoder that isn't ours: GIFs plain, interlaced, transparent and long enough for the codes to reach 12 bits; BMPs of 8 and 24 bits; PNGs of every kind (colour, with alpha, palette of 8 and 4 bits with a transparent entry, greys of 1, 8 and 16 bits, grey with alpha, not compressed, and one that refers 21,600 bytes back). Every reader is also fed its file with a byte changed, at every few bytes: an answer each time, and no pixel outside the picture.
|
||||
|
||||
### Checks on the device (2026-10-07, driven over the Debug Console)
|
||||
|
||||
Test pictures were copied to a scratch folder and removed afterwards, with the test screenshot.
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Colour bars as PNG, JPEG, BMP and GIF, 240 x 135 and 800 x 600 | The same picture each time, the colours in the right order |
|
||||
| A PNG with a transparent square | The square is the background |
|
||||
| A screenshot taken in the Shell | Shown; at its own size it is the screen, pixel for pixel |
|
||||
| A progressive JPEG | Hex, with "A progressive JPEG can't be shown" |
|
||||
| An animated GIF | Its first picture |
|
||||
| 12 megapixels | Arrives from the top down in 6.9 s; the size is noted when it is whole |
|
||||
| Enter, then the arrows | Its own size from the middle, then half a screen at a time |
|
||||
| Back in the middle of a decoding | The folder's listing at once |
|
||||
| Tab to hex and back, three times; the help panel, then closed | The picture again each time |
|
||||
|
||||
**Not checked:** a photograph from a real camera (the test JPEGs were made by Pillow); a Toast over a picture; a PNG with IRC connected, when there may not be the memory; the card pulled while decoding; the real keyboard.
|
||||
|
||||
### What went wrong while building it
|
||||
|
||||
**The watchdog.** The first version waited for the decoder. The main loop is watched: five seconds without a pass and the device restarts, which is what it did on the 12-megapixel test. The crash report named the decoder's line. The decoding moved to the background, which also made the picture appear as it comes.
|
||||
|
||||
**A decoder that worked once.** The library's PNG decoder showed the first picture and refused the next five: "no memory". It wants 44 KB in one piece, and after some use the largest piece is 43 to 47 KB. Writing a decoder that needs 32 KB was less work than it sounds, and unlike the library's it has tests.
|
||||
|
||||
**Two sentences still said "up to 16 KB"** about editing, in the README and the Storage guide, after issue #47 lifted that. Corrected here.
|
||||
|
||||
@@ -34,12 +34,23 @@ The App says why when it refuses.
|
||||
|
||||
<kbd>Enter</kbd> on a file opens it by its type, and <kbd>Tab</kbd> switches the same file to a hex dump or to text:
|
||||
|
||||
- **Text** (`.txt`, `.log`, `.gmi`, `.csv`, and anything that looks like text): only a screenful is read from the card, so a file of any size opens at once, and logs open at the end. Up and down move a line, left and right a page, <kbd>t</kbd> and <kbd>b</kbd> go to the top and the end, and <kbd>e</kbd> edits it (up to 16 KB, as in [Notes](/guide/notes/)).
|
||||
- **Text** (`.txt`, `.log`, `.gmi`, `.csv`, and anything that looks like text): only a screenful is read from the card, so a file of any size opens at once, and logs open at the end. Up and down move a line, left and right a page, <kbd>t</kbd> and <kbd>b</kbd> go to the top and the end, and <kbd>e</kbd> edits it, whatever its size (as in [Notes](/guide/notes/)).
|
||||
- **Pictures** (`.png`, `.jpg`, `.bmp`, `.gif`): see [Pictures](#pictures) below.
|
||||
- **Captures** (`.pcap`): the packets as the [LoRa Scanner](/guide/lora-scanner/) lists them; <kbd>Enter</kbd> 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 is checked as an install checks it, signature and contents, without writing anything. <kbd>Enter</kbd> then installs it (see [Updates](/guide/updates/)).
|
||||
- **Anything else:** a hex dump.
|
||||
|
||||
## Pictures
|
||||
|
||||
<kbd>Enter</kbd> on a PNG, a JPEG, a BMP or a GIF shows it. A picture bigger than the screen is shrunk to fit; <kbd>Enter</kbd> again shows it **at its own size**, and the arrow keys then move around it, half a screen at a time. <kbd>i</kbd> gives its size in pixels, and <kbd>Tab</kbd> the file as hex.
|
||||
|
||||
- **The screen has 256 colours.** A photograph is dithered to them; a drawing or a screenshot usually has colours the screen shows exactly. The screenshots the [Shell](/guide/shell/) saves are shown pixel for pixel at their own size.
|
||||
- **A big photograph takes time**, because every pixel of the file goes through the decoder, shown or not: about 7 seconds for 12 megapixels. The picture appears as it is decoded, and the keys keep working: Back leaves at once.
|
||||
- **A GIF shows its first picture only.** It doesn't move.
|
||||
- **What can't be shown** opens as hex, with the reason: a progressive JPEG, an interlaced PNG, a BMP that is compressed or has 16 bits a pixel.
|
||||
- **A PNG needs 32 KB of memory in one piece** while it is decoded, and a few more (a JPEG needs 4 KB, a GIF 17 KB). With IRC connected there may not be that much: the viewer says so. The firmware's own screenshots need none.
|
||||
|
||||
## Maintenance
|
||||
|
||||
At the top of the card, the last row, **Maintenance** (or <kbd>m</kbd>), shows the card's usage and holds **Storage clean-up** and **Erase SD card**. They delete for good, so they sit behind a warning. Clean-up deletes old logs and captures by category and age, showing the space it would free first. Notes and Saved Pages are never offered.
|
||||
@@ -50,4 +61,4 @@ The firmware warns once per start when the card passes **80%** full; past **90%*
|
||||
|
||||
What <kbd>Fn</kbd> + <kbd>h</kbd> shows on these screens. These tables are generated from the firmware's own lists, so they are always the current ones.
|
||||
|
||||
{{ keys(scopes=["storage", "storage-details", "storage-name", "storage-busy", "maintenance", "viewer-text", "viewer-hex", "viewer-pcap", "viewer-packet", "viewer-gpx", "viewer-ota"]) }}
|
||||
{{ keys(scopes=["storage", "storage-details", "storage-name", "storage-busy", "maintenance", "viewer-text", "viewer-hex", "viewer-pcap", "viewer-packet", "viewer-gpx", "viewer-ota", "viewer-image"]) }}
|
||||
|
||||
Reference in New Issue
Block a user