G1 step 3: the Gemini fetcher (TOFU, redirects, floors, pages via the card)

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
This commit is contained in:
2026-10-05 00:58:01 +02:00
co-authored by Claude Opus 5.5
parent babb1d114e
commit 7b8d9391a4
12 changed files with 552 additions and 130 deletions
+8 -2
View File
@@ -55,7 +55,7 @@ static StorageService* storageService;
static PowerService* power;
static ClockService* clockService;
static GnssService* gnssService;
static GeminiService gemini;
static GeminiService* geminiService;
static SavedNetworks* savedNetworks;
static WifiService* wifi;
static IrcService* irc;
@@ -147,6 +147,7 @@ void setup() {
storageService = new StorageService(bus);
power = new PowerService(settings);
clockService = new ClockService(settings, bus);
geminiService = new GeminiService(nvs, *storageService);
gnssService = new GnssService(settings, *clockService, *storageService, bus);
savedNetworks = new SavedNetworks(nvs);
savedNetworks->load();
@@ -465,8 +466,13 @@ static void runCommand(String line) {
storageService->requestListing();
listingWanted = true;
}
if (line.startsWith("gemini trust ")) { // gemini trust <host> <port> <sha256>: accept a changed certificate
char host[96] = "", fp[80] = "";
int port = 1965;
if (geminiService && sscanf(line.substring(13).c_str(), "%95s %d %79s", host, &port, fp) == 3) geminiService->trust(host, port, fp);
}
if (line.startsWith("gemini get "))
if (!gemini.fetchToConsole(line.substring(11).c_str())) console.println("gemini: a fetch is already running");
if (!geminiService || !geminiService->fetchToConsole(line.substring(11).c_str())) console.println("gemini: busy, or unavailable in Safe Mode");
if (line == "irc start") irc->connect();
if (line == "irc stop") irc->disconnect();
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>