A page larger than memory allows is now windowed instead of cut: one
pass over its file (cache or Saved Page) counts lines, indexes every
64th (offset, and the preformatted state there in bit 31: about 1 KB
for a 1 MB page) and loads the first window. Scrolling near either end
reads the next or previous window on the Gemini task; the line on top
of the screen stays put, the scrollbar follows the whole page, and Tab
at a window's edge pages on instead of wrapping. Window budgets count
the memory the old window gives back.
Display pages alternate between two cache files so the one on screen
is never overwritten by the next fetch; jobs use a third.
On the device, Cosmos with IRC connected: 226 of 419 lines at first,
then lines 192-419, back to 64 and 0 while scrolling. `key space` added
to the console's key command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Everything touching the network or the card is a job on the Gemini
task (a missing card can block 5 s, past the main loop's watchdog):
about:start is composed from /gemini/bookmarks.gmi and the Saved Pages
(by capsule, newest first); file:// opens a Saved Page; s, S, r, d, b
and downloads report one line to the URL bar, S with progress Toasts.
A Saved Page is the cached body with a first line "> Saved from <url>
on <date>": it shows as a quote and gives relative links their base;
inside one, links to other Saved Pages open the saved copy, others go
online or say "Not saved, and offline". Input prompts (11 masked)
request the same URL with the answer as its query.
Fixed on the way: re-wrapping indented lines rebuilt the text from its
rows, inserting a space where a long word had been cut; refreshing
loaded the whole Saved Page next to a TLS connection (heap down to 436
bytes), now it reads one line and every fetch checks the 55 KB floor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Gemtext as Q75 has it: headings bold (# in the accent colour), lists
with a middle dot, quotes muted, links as » labels, preformatted lines
unwrapped and scrolled sideways together; other text/* as is. Pages are
wrapped once for each line's first row (4 bytes a line) and only the
lines on screen are wrapped again to draw. Tab and Shift+Tab move
between links, Enter follows (relative links resolved), Back or Delete
go back to where the page was scrolled, g opens the address line.
Non-Gemini links say so in the URL bar; a changed certificate opens a
dialog; errors and refusals get a page with a "Try again" link; a page
only partly in memory says why at its end.
A status message timed with millis() after the loop's clock read was
cleared before it was drawn (unsigned wrap): now a signed comparison,
as for the toasts in M0.
Verified on the device: start page, Project Gemini, its relative news/
link, Back with the scroll restored, and the YouTube link refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
GeminiService fetches on a short-lived task and hands the App a
GeminiPage: header, the body as lines in 4 KB chunks (TextBuffer: no
large block, no doubling copies), the final URL after up to 5
redirects. Certificates are pinned on first use per host and port; a
change comes back as its own outcome with both fingerprints. No fetch
starts below 55 KB free (Q86).
With a card, the body streams to /gemini/cache/page.gmi in 1 KB pieces
while the connection is open, then loads into RAM once its memory is
back (Q87); StorageService::runAndWait (moved from the Debug Console)
keeps every card access on the storage task. Without a card: RAM, with
the steady and transient floors.
Measured with IRC connected: Cosmos (31.6 KB) went from 4.6 KB to the
whole page on the card and 20 KB on screen; lowest free heap 19.5 KB
in transfer, 43 KB once loaded. `gemini get` and `gemini trust` on the
console. 14 Gemini tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
lib/gemini: URLs split per RFC 3986 appendix B and resolved per section
5.2 (all 32 reference examples of 5.4 pass, with gemini:// for http://),
the request form (no fragment, lower-case host, never an empty path),
query encoding for input prompts, Saved Page paths; the response header
(status, category, MIME type and parameters, 1024-byte meta limit);
gemtext's line types; and display text for the Latin-1 fonts. 12 tests.
Q86, decided after step 1: free heap stays above 40 KB in steady state
and 20 KB during a handshake; a fetch won't start below 55 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
`gemini get <url>` fetches on a short-lived task (a handshake would trip
the main loop's watchdog; an idle client should cost no stack) and
reports the header, size, certificate fingerprint and heap. First page:
geminiprotocol.net, 20 text/gemini, 1,184 bytes in 0.7-1.1 s.
With IRC connected over TLS, a fetch dips to about 24 KB free during its
handshake, about 36-40 KB after it; nothing leaks. Antenna is down, so
the default aggregator becomes Cosmos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
custom_sdkconfig makes pioarduino regenerate the ESP-IDF libraries:
asymmetric TLS buffers (16 KB in, 4 KB out) and dynamic buffers that
free handshake-only data once connected. The rebuild also follows the
board: no PSRAM, 8 MB flash. The project now carries the partition
table the hybrid build needs (identical to the framework's: the app
slots must not move under updates over the air). Generated files are
ignored.
Debug Build, GNSS on, IRC on TLS: 78 KB free and a 59 KB low (M2 began
at 31 KB and 12.6 KB; the floor is 40 KB). The full stress set passes
on it: refused installs over Wi-Fi and from SD, put/get, screenshot,
core dump decode, Probation; under all of it at once, the low is 46 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
IrcService::disconnect() stops the session from any state: QUIT if
connected, then no more retries. /quit goes through it (before, it only
stopped a connected session; while waiting for Wi-Fi or retrying it did
nothing), and so does the new `irc stop` command. Stopped by hand,
opening the IRC App no longer reconnects; typing a line does.
mDNS is gone: it never crossed the dev box's routed network, and it
cost about 7.5 KB of RAM. Pushes go to the IP shown in Settings ->
Firmware, which drops its Name row. OTA Q54 records the change.
On the device, Debug Build: 105.6 KB free with Wi-Fi (was 98); with IRC
on TLS 54 KB free (was 46); after `irc stop`, back to 99 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Peak stack use was measured through each task's worst case (an
ECDSA-checked install over Wi-Fi and from SD, get/put, a core dump
fetch, an IRC TLS handshake); stacks are now peak plus about 2 KB: loop
8 -> 6 KB, update 8 -> 5, storage 10 -> 6, irc 8 -> 6. The Debug Build's
console ring goes 6 -> 4 KB, serial TX 2 -> 1 KB, the GNSS UART buffer
1 KB -> 512 B.
On the device, with IRC on TLS: 46 KB free (was 31), an 18 KB low (was
9.4). The re-run of every worst case left at least 1.6 KB of stack free
in each task.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
`r` in the GNSS App (or `gnss track start|stop`) records a Track to
/gnss/tracks/YYYYMMDD-HHMMSS.gpx: a point every 5 s once moved 5 m
(lib/gnss/track, 7 tests: haversine, the rule, GPX text, the file name).
It keeps recording with the App closed, shows REC in the Status Bar, and
announces start and stop with a Toast. It needs a card and the time;
switching GNSS off stops it. Tracks are written like Captures: past 90 %
card usage, until full. Storage Clean-up gets a GNSS tracks category.
A Track cut short by a reset or power loss has no GPX footer; the GNSS
Service closes such files at the next boot.
Verified on the device: a recorded Track and one interrupted by a reset
both parse as GPX 1.1 on the PC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Position: the Fix line (or how long it has been searching), latitude and
longitude in decimal degrees or degrees-minutes-seconds (Settings ->
Coordinates), the Maidenhead locator, altitude, speed and course, HDOP
and UTC. Sky: the satellites by azimuth and elevation, coloured by
constellation, filled when used in the Fix, with used/in-view counts.
Tab switches. lib/gnss/geo_format holds the formatting, the locator and
the sky projection, host-tested (6 tests; locators checked against
FN31pr and JN58td).
Verified on the device by a window: 3D Fix from GPS, GLONASS, Galileo
and BeiDou.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Q61: a muted G while searching (or the receiver is silent), G with a 2D
Fix, G and the satellite count with a 3D Fix; nothing when GNSS is off.
Verified on the device by a window: 3D Fix, 9 of 11 satellites used
(GPS, GLONASS, BeiDou), HDOP 1.3, Status Bar "G9", and "gnss: clock set".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
The GNSS Service reads the receiver (UART 15/13, 115200, 1 KB buffer)
from its 50 ms tick and feeds the NMEA parser. With a Fix it sets the
clock (GNSS is the most trusted TimeSource) and refreshes it every 10
min. Settings gets GNSS On/Off and the coordinate format (Q64).
Off puts the receiver in standby with $PCAS12,65535, renewed hourly; On
wakes it with a hot start, $PCAS10,0. Both measured on the device: the
output stops within a second, and a command wakes it within a second.
Commands: gnss status (Fix, satellites per constellation, bytes and
sentences), gnss nmea on|off, gnss send <sentence>, gnss restart. The
probe is gone; never drive GPIO 15 (the receiver's output): the first
probe's swapped-pin attempt silenced it until a power cycle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
lib/gnss: RMC, GGA, GSA and GSV into one GnssState: Fix type, position,
altitude, speed, course, HDOP, UTC time (trusted only with a Fix) and the
satellites in view across constellations. GSV sequences are kept per
constellation and signal band and merged per satellite with the stronger
SNR; GSA's system ID marks which satellites are used. 16 tests, using
lines captured from the device.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
A temporary `gnss probe` command listens on both pin orders at 115200
and 9600. Only RX 15 / TX 13 at 115200 carries NMEA, as Meshtastic's
board file says; M5Stack's GPIO 8/9 are the keyboard's I2C bus. The
output format (NMEA 4.10, GSA system IDs, GSV per signal) is recorded in
docs/milestones/M2.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
The serial and Debug Console `key` command could type characters but not
erase them, so text typed into a field by mistake couldn't be cleared
remotely. `key del` and `key tab` inject Delete and Tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
New commands everywhere: ls, rm and install <path> (Update from SD
without the Firmware page), all as Storage Service jobs. In Debug
Builds the console task answers get and put (one storage job per
transfer, file kept open, TCP flow control: about 300 KB/s, against
55 KB/s over serial) and screenshot (the RGB332 frame the UI composes
into). rdbg.py turns those into files and PNGs.
A failed put closes the connection: the rest of the file had been
parsed as commands. Card writes are retried 3 times after closing,
truncating to the last good byte and reopening, since FATFS keeps a
file in error after one failed write (seen once at 1.3 MB on this card).
onStorage() decides with one compare-and-swap whether the job or the
timeout wins, so an abandoned job can't touch a returned stack frame.
Verified on the device: screenshot; a get round trip byte-identical;
4 puts in a row; a tampered .ota refused by install; a good one put,
installed from SD, confirmed on Probation. The retry path itself has
not fired since it was added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Every build now records at boot which version runs and, after a crash
restart, which one crashed (even across a Rollback). The core dump
summary (task, PC, reason, backtrace) is printed and raised as a
Notification; `crash` shows it later. After 3 crash restarts in a row
the firmware starts in Safe Mode: clock, Wi-Fi, Update Service and Debug
Console only (SafeMode, 2 host tests). A normal restart or a minute up
resets the count.
The main loop is now on the task watchdog (enableLoopWDT): Arduino only
watched core 0's idle task, so a stuck loop hung the device for good.
The Update Service restarts into an installed update by itself if the
main loop hasn't after 90 s.
Debug Builds: `coredump get` and `reset` are answered by the console's
own task; rdbg.py crash decodes the backtrace and rdbg.py coredump runs
esp-coredump, against ELFs archived by version and digest in .pio/elves.
The StorageService mutex is now made in the constructor: Safe Mode never
starts that Service, and `info` crashed on the null mutex, 29 times in a
row before the fix was pushed into Safe Mode over Wi-Fi.
Verified on the device: crash report and full core dump decoded over
Wi-Fi; Safe Mode at exactly 3 crashes, left by `reboot`; a hung loop
caught by the watchdog in 5 s; `reset` from the console task. ADR 0005.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
cardputer-adv-debug (-DRORO_DEBUG, version +debug) adds a Debug Console:
after a token line, a client gets the last 6 KB of console output, live
lines (ESP-IDF logs included) and the serial commands. The socket task
only queues lines; the main loop runs them. Release builds compile none
of it. The token lives in ~/.config/roro9stack/debug-token, created by
_docker.sh and passed into the container.
All output now goes through `console`, which never waits for USB: a host
that was attached but not reading stalled the main loop up to 2 s per
line. New commands everywhere: info (slots with their versions from NVS,
since the framework stamps its own into each image), tasks, reboot,
boot other, log level, help. scripts/rdbg.py is the client; flash.sh
--debug builds it; CI builds both variants. ADR 0004.
Verified on the device: USB-flashed, then updated over Wi-Fi to a Debug
Build that confirmed on Probation; both slots hold Debug Builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
scripts/sd_put.sh <file> [card path] sends a file (by default into
/updates, for Update from SD) without taking the card out. The serial
driver drops bytes once its receive buffer is full, so the transfer is
stop-and-wait: 1 KB chunks, each acknowledged once the Storage Service
has written it, into a 2 KB receive buffer. The device checks the
SHA-256 before renaming <path>.part into place, and gives up after 5 s
of silence or a card job that never returns. FileReceiver holds the
logic, with 12 host tests. About 55 KB/s: 1.6 MB in under 30 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Verified on the device: a crashing update pushed over Wi-Fi died once,
and the next boot was the previous firmware, with the "Update ... failed"
Toast. bootGuard() stays as a second line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Arduino-ESP32's initArduino() marks a PENDING_VERIFY image valid unless
the sketch overrides the weak verifyRollbackLater(). Every update was
therefore VALID before bootGuard() or Probation ever ran (otadata read
back state 0x2 on a crash-looping test build), and nothing rolled back.
The bootloader was never the problem. Override it to return true, so
Probation decides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
A deliberately crashing update looped forever on the device: the
prebuilt bootloader ignores ESP_OTA_IMG_PENDING_VERIFY despite the
app-side rollback config. UpdateService::bootGuard() now runs first in
setup(): it counts starts on Probation in NVS and, on the second
unconfirmed start, marks the image invalid and reboots into the
previous one. Confirming (or the Wi-Fi rollback) resets the counter.
ADR 0003 records the limit: a crash in the first milliseconds still
needs USB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
The Arduino network client treats a half-closed connection as closed,
so the device's reply after the sender's EOF was lost. The header
already carries the image size: UpdateParser::complete() lets the
device finish and answer while the connection is open. ota_push.py
half-closes only if no answer comes within 3 s, for older firmware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- EcdsaVerifier (mbedTLS, embedded public key) and EspOtaSink (writes
the inactive app slot, esp_ota_end validates the image, then sets
the boot partition)
- UpdateService: listens on TCP 3232 (and mDNS roro9stack-<id>) while
Wi-Fi is Connected; streams into UpdateParser; replies OK/ERR to the
sender; remembers the pending version so a Rollback is reported
after the reboot
- Probation (host-tested): confirm after the first frame + 30 s + Wi-Fi
(if configured); roll back if configured Wi-Fi never connects in 3 min
- Main loop: full-screen progress while receiving; restart once
installed, waiting up to 60 s for Text Entry to end
- Settings > Firmware: version, Probation status, push address and
name, and the .ota files in /updates on the SD card to install
- StorageService.runJob() runs work on the storage task (SD installs)
- wifi status prints IP and running version; RORO_TEST_CRASH test hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- scripts/ota_keygen.sh: ECDSA P-256 key pair; the private key goes to
~/.config/roro9stack/ (0600), the public key to keys/ and
src/platform/ota_public_key.h; .gitignore refuses *key.pem
- scripts/make_ota.py: wraps firmware.bin into a signed .ota (openssl)
- scripts/ota_push.py: sends it over TCP 3232, prints the device's answer
- scripts/flash.sh --ota <host>: build, sign, push
Checked: a generated .ota has the documented layout and its signature
verifies with openssl against the committed public key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
lib/ota: a 160-byte header (magic, format, image size and SHA-256,
version, ECDSA signature over the first 80 bytes) then the image.
UpdateParser checks the header and signature before writing anything,
hashes the image as it streams into an UpdateSink, and only finishes
the sink when the hash matches. Downgrades are flagged, not refused.
Includes a dependency-free SHA-256 and semver comparison.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
IRC: join after NickServ login, keyed auto-join, SASL->NickServ
fallback, nick regain; Alt scrolls and Up/Down recall sent lines; /j.
Wi-Fi Tools: sort and filter the network list, and scan logging to CSV.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- lib/wifi (host-tested): scan_log (CSV field quoting, header/row, a
once-per-interval throttle) and network_list_view (sort by signal /
channel / name, filter open-only / hide-hidden / strong-only)
- ScanEntry moved to lib/wifi so both are testable on the PC
- Wi-Fi Tools networks view: s sort, o/h/w filters, l toggles logging
to /wifi/scans/<date>.csv (header + one row per AP per logged scan,
throttled 30 s, needs the clock for timestamps); foreground-only
- Wi-Fi scan logs replace probe-request logs as a clean-up category
- Serial: cat <path>, and key <char> injects a character
Verified on the device: CSV written with header, timestamps, BSSID,
channel, RSSI, security and SSID; hidden networks marked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- Alt + ; / Alt + . scroll the Buffer back and forward
- Up / Down (Fn + ; / Fn + .) browse the last 30 sent lines, shell-style,
returning to the draft past the newest (InputHistory, host-tested)
- /j is short for /join
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- With NickServ, auto-join waits for the logged-in reply (900) or 2 s
at most, so registered-only IRC channels let us in
- A failed SASL login falls back to NickServ (its own password, or the
SASL account and password)
- IDENTIFY names the account explicitly, and once logged in on a
fallback nick, REGAIN takes ours back from a stale session
- Auto-join entries take keys ("#private key, #public"); keys from
/join are reused when rejoining; keyed channels go first in JOIN
- Serial irc dump: whole Buffers, and which login is configured
(never the secrets)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
The IRC App edited the live config while the IRC task could be reading
it to connect. The App now edits a copy; applyConfig() validates it and
swaps it in under the lock, and the task connects from a snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- lib/wifi (host-tested): channel occupancy (neighbour spill, signal
weighting, quietest of 1/6/11) and a signal tracker (history, lost
detection, click interval)
- WifiService: scan results carry BSSID, channel and security; a scan
can target one channel for quick tracker refreshes; endListScans()
turns the radio back off when Wi-Fi is disabled
- Wi-Fi Tools App: networks nearby, channel occupancy bars, signal
tracker with clicks (m to mute)
- M1 plan: Monitoring-mode views and captures deferred
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- Chat: header (Buffer n/N, unread elsewhere, topic or connection
state), wrapped hh:mm <nick> lines (own in accent, Mentions green,
info grey), input line; Tab cycles Buffers, Fn+Up/Down scrolls back,
Enter sends, Back leaves while the IRC Service keeps running
- /settings: server form (host, port, TLS, self-signed pinning, nick,
SASL, NickServ, auto-join); Save & reconnect restarts the session
- Opening the App starts the IRC Service; viewing a Buffer clears its
unread count; IrcSession gains a revision counter for redraws
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- IrcService: networking on its own task, TLS with the built-in CA
bundle (or trust-on-first-use pinning when self-signed is allowed),
plain TCP when TLS is off; connects only while Wi-Fi is Connected,
marks pauses for Monitoring, reconnects with backoff, pings a quiet
server, writes Logs, raises Notifications for Mentions
- Session: forget /quit once disconnected (it was handled every loop);
the server Buffer never counts as unread (MOTD showed as [4])
- Status Bar: unread count
- Frame buffer 16 -> 8-bit colour (M1 Q46): min free heap with IRC on
TLS went from 51 KB to 79 KB
- Serial: irc start / say / dump
Verified on the device against irc.libera.chat:6697: certificate
checked, joined #roro9stack-test, sent a message, quit cleanly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
lib/irc: IrcMessage parse/serialize, base64, ReconnectPolicy
(5 s .. 5 min), IrcConfig (validated, persisted) and IrcSession:
registration with SASL PLAIN or NickServ, nick fallback, PING, Buffers
capped at 50 lines with unread counts, Mentions, CTCP ACTION/VERSION,
topics, /names on request only, others' join/part/quit hidden, user
commands, and rejoining after a pause. No networking: the session
returns lines to send, Log entries and Notifications.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
- lib/storage_model (host-tested): FAT-safe names, daily Log paths,
dates from Log/Capture file names, CleanupPlan by category and age,
byte formatting
- StorageService does all card I/O on its task: queued Log lines are
written in batches each second (dropped while Logs are paused),
plus file listing and deletion jobs
- Settings > Storage moves into StoragePage: usage, Clean up
(category, age with size preview, confirmation), Erase SD card
- Clock: local date for Log names
- Serial: log <text>, sd list
Verified on the device: lines land in /irc/dev/#test/2026-10-02.log
with folders created as needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT