Files
roro9stack/site/content/devlog/roro9stack-share/index.md
T
twislaandClaude Opus 5.5 e3fe618c7f
CI / build (pull_request) Successful in 1m25s
Site / build (pull_request) Successful in 9s
Devlog: "Press w" is v0.18.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-08 00:35:15 +02:00

12 KiB

+++ title = '''Press w''' description = '''I asked whether roro9stack should get an FTP, SFTP or WebDAV server, to move files to and from my phone. The answer was none of them: a web page. Less than an hour later I pressed one key on the Cardputer, opened my phone's browser, and my SD card was in it. It works, and I'm still grinning.''' date = 2026-10-08T00:30:00+02:00

[extra] topics = '''ESP32-S3 · HTTP · Files''' read_label = '''Read how the card got into my phone →''' uid = '''share: on at http://172.16.42.25/''' dek = "A short one, written straight after it worked, because I'm too pleased to wait. roro9stack, my firmware for the M5Stack Cardputer, can now hand its SD card to any browser on the same Wi-Fi: no cable, no app, no computer. One key, one code, done." byline = '''one question asked, the wrong three answers offered, a fourth taken'''

[extra.sign] label = "Apps installed on the phone to make this work" note = "A browser was already there." count = "0"

extra.cast name = "The question" role = ""FTP, SFTP or WebDAV?"" text = "Three ways to serve files, all of which want an app on the phone. I'd have picked one and been mildly unhappy with it for months."

extra.cast name = "The w key" role = "in the Storage App" text = "Starts a web server and shows where it is. Back stops it. That is the entire user interface on the device."

extra.cast name = "The code" role = "six digits, new every time" text = "On the device's screen and nowhere else. Type it in the page and you're in. Five wrong tries and the door stays shut for a minute."

extra.cast name = "The page" role = "5.4 KB, one file" text = "A list of what's on the card, an Upload button, a New folder button, and a Delete next to every row. Served from the firmware's own flash."

extra.cast name = "The storage task" role = "the only one allowed to touch the card" text = "Every byte in either direction goes through it, 8 KB at a time. It was there long before this and didn't need to learn anything new." +++

TL;DR

  • Press w in the Storage App (v0.18.0), scan the QR code with a phone on the same Wi-Fi, and the SD card is a web page: list, download, upload, new folder, delete.
  • Nothing to install, on the phone or anywhere else. That was the whole point.
  • It runs only while that screen is open. A six-digit code, new each time, keeps the rest of the network out.
  • It is not encrypted, and the screen says so. Fine at home; think first elsewhere.
  • About 200 KB a second, one request at a time, 57 KB of flash, 13 KB of memory while it's on.
  • Also since the last post: the device shows pictures (v0.16.0), Fn+p takes a screenshot anywhere (v0.17.0), and this site has a search.

It works!

I'll skip the build-up. I pressed w. This came up:

{{ figure(src="share.png", alt="The Cardputer's screen at 2x: a large QR code on the left; on the right, In a browser, on this network: 172.16.42.25/, Code 825 132 in large blue digits, Nothing asked yet, Not encrypted, and backtick stops sharing.", width=480, height=270, caption="The whole feature, as the device sees it. The code in this picture stopped being valid the moment I pressed Back.") }}

I pointed my phone's camera at the QR code, and there was my SD card, in the browser. No address to type, no code to type. It works. That's fucking awesome.

{{ figure(src="phone.png", alt="A phone's browser in dark mode at the address 172.16.42.25, at 00:15: roro9stack: the SD card, a link SD card, a bright blue Upload files button and a New folder button, then rows captures, gemini, gnss, irc, notes, screenshots, updates and wifi, each with a Delete button.", width=462, height=1001, caption="My phone, at a quarter past midnight. Its browser, my card, nothing else.") }}

{{ figure(src="device-photo.jpg", alt="A photograph of the Cardputer ADV on a wooden table. Its small screen shows the QR code, the address, the code 997 216, and 2.5 MB in, 3.2 MB out. Below the screen, the whole keyboard.", width=900, height=864, landscape=true, caption="And the other end of it, for scale. The whole server is in there, behind a screen smaller than the QR code on most posters.") }}

Files, from my phone, to a computer the size of a biscuit and back. Over Wi-Fi. With nothing installed on either end that wasn't there this morning.

Most of this devlog is about things that took a week of measuring and still bit me. This one I can explain to anybody in a sentence: it's a web page with your files on it.

The question I asked, and the one I should have

Until tonight a file reached the card in one of two ways: the Debug Console's put command, which wants a PC, a Python script and a token, or pulling the card out. My phone can do neither. So I asked the obvious question: FTP, SFTP or WebDAV?

The answer was a table, and the table was unkind to all three:

{% table() %}

On the phone On the device
FTP needs an app; passwords in clear easy
SFTP needs an app a whole SSH server: hundreds of KB, and a key exchange this chip would feel
WebDAV needs an app, on iOS and Android both fine, but see the first column
A web page any browser a small HTTP server
{% end %}

I had been choosing a protocol. What I wanted was to move a file with my thumb. Every phone made in the last fifteen years has exactly one file-transfer client that needs no setup, and it's the browser.

What's in it

On the device, almost nothing. w starts the server and draws a screen. Back stops it. The server is the one that ships inside ESP-IDF, the framework the firmware is built on, so there was no library to choose. Seven requests: the page, the code, a listing, a download, an upload, a new folder, a delete.

The QR code carries the code. Scan it and you're in without typing; type the address by hand and the page asks for the six digits. The code is made fresh from the hardware random generator each time w is pressed, and pressing Back throws every browser out.

An upload is just the request's body. The browser sends the file as it is with a PUT, so there is no form to pick apart on a device with 100 KB of free memory. It lands on the card as photo.jpg.part, 8 KB at a time, and is renamed when the last byte has arrived. A transfer that dies halfway leaves nothing behind. If the name is taken, the page asks before replacing it, and the device refuses until it has.

The Storage App's rules still hold. The page can't delete the folders the firmware keeps its own files in, and it says why:

{{ figure(src="page.png", alt="The page in a browser at a phone's width: roro9stack: the SD card, a link SD card, buttons Upload files and New folder, then rows a-web, captures, gemini, gnss, irc, notes, screenshots, updates and wifi, each with a Delete button. Below, in orange: The firmware keeps its files in /notes.", width=390, height=630, caption="The page, at a phone's width, just after it was asked to delete /notes. It said no, in the device's own words.") }}

Nobody gets to walk out of the card. /a-web/../wifi is refused before anything looks at the disk, by a function with a test that tries a dozen ways of asking.

What it isn't

Encrypted. A TLS server costs this device about 40 KB of memory per connection, and it has around 100 KB on a good day. So the files and the code cross the Wi-Fi in clear. On my own network I don't mind. On a hotel's, I'd think about it. The device's screen says "Not encrypted." in plain words every time, because a limit you have to read the docs to find is a trap.

Fast. 200 KB a second, give or take. A 2.6 MB photo takes eleven to seventeen seconds going up. I tried bigger pieces and smaller ones; the numbers moved around more between two runs of the same setting than between settings, so it's 8 KB and I stopped fiddling.

Able to do two things at once. The server answers one request at a time. Start a big download and the page waits until it's done. I found that by asking for a listing in the middle of a download and watching it time out. It's written in the guide and left as it is.

What went wrong, for about four minutes each

A file that included itself. The part that can be tested on a PC lived in web_share.h. So did the service, in another folder. The service's header said #include "web_share.h", meaning the other one, and the compiler quite reasonably gave it itself. The testable half is now called share_rules.h.

The scanned address did nothing, sometimes. If the page was already open and you then went to the same address with the code after the #, nothing happened: to a browser that isn't a new page, so the script never ran again. One line, onhashchange. Found by a browser test, not by me, which is the right way round.

That's the list. Two.

What I checked, and what I didn't

Checked, before I went anywhere near my phone:

  • Every request and every refusal, from a PC: wrong code, right code, a path with .. in it, deleting a protected folder, deleting a folder that isn't empty, uploading over a file that exists.
  • 2.6 MB up, then down again, compared byte for byte. The same.
  • The page in a real browser at a phone's width: uploads, a download, a new folder, a delete, a replace.
  • Five wrong codes: shut for a minute, for the right code too. A browser that was already in stays in.
  • Back: the server is gone and the memory comes back.

And then on my actual phone, where it works, which is the sentence this post exists for.

Not checked: Safari. Pulling the card out mid-transfer. Sharing while IRC is connected, when memory is tighter. What happens if the screen turns off while it's sharing.

Also since last time

The day didn't stop at the last post:

  • Pictures. The Storage App opens PNG, JPEG, BMP and GIF files (v0.16.0). The interesting part was the PNG decoder: the one in the display library wanted 44 KB in a single block, got it once, and refused the next five pictures. The firmware has its own now, which needs 32.
  • Fn+p takes a screenshot on any screen (v0.17.0), except the one that shows the Debug Console's token, where it politely declines.
  • This site has a search.
  • A WireGuard tunnel is sitting in a pull request. It works against a test server on my own network and is waiting to meet a real one.

By the numbers

{% table() %}

Keys to press on the device 1
Apps to install on the phone 0
Digits in the code 6
Wrong codes before it shuts for a minute 5
The page 5.4 KB
Flash 57 KB
Memory while sharing 13 KB
Speed about 200 KB/s
Requests at a time 1
Bytes that differed after 2.6 MB went up and came back 0
Host tests 533
Things that went wrong 2
{% end %}

Where it stands

{% steps() %}

  1. M0 and M1: the skeleton, Wi-Fi, IRC, Wi-Fi Tools. v0.1.0 to v0.2.1, the first post.

  2. Updates and debugging over the air. v0.3.0, Look, no cables.

  3. M2: GNSS. v0.4.0, Seventeen satellites.

  4. G1: Gemini. v0.5.0, A browser in the RAM IRC left over.

  5. M3: the LoRa radio, listening. v0.6.0, The loudest thing it hears is itself.

  6. S1: the card, fixed addresses, the System App. v0.6.1 to v0.8.1, One byte too early.

  7. F1 and the start of R1: files, notes, signed releases, updates from Gitea. v0.9.0 to v0.11.0, 836 bytes.

  8. W1: the website, and one firmware with the Debug Console in it. v0.12.0, It was off.

  9. A help key, the Shell, notes of any size. v0.13.0 to v0.15.0, It said "No".

  10. Pictures, and a screenshot key. v0.16.0 and v0.17.0.

  11. The card in a phone's browser. v0.18.0, this post.

  12. Next: the WireGuard tunnel, once it has talked to a real server. And M4, the mesh, which still wants a second node. {% end %}

{% signoff() %} I asked which of three servers to build and got told to build none of them. Then I pressed a key, picked up my phone, and my files were on it. Most of what this firmware does took days of careful measuring to get right. This took less than an evening, and it works, and I'm going to enjoy that at least until the next thing breaks. {% end %}