Files
roro9stack/src/services/gemini_service.h
T
twislaandClaude Opus 5.5 7b8d9391a4 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
2026-10-05 00:58:01 +02:00

83 lines
3.3 KiB
C++

#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <string>
#include "gemini_response.h"
#include "key_value_store.h"
#include "services/storage_service.h"
#include "text_buffer.h"
class NetworkClientSecure;
namespace roro {
// What a fetch came back with.
struct GeminiPage {
enum class Outcome { Ok, Failed, CertificateChanged };
Outcome outcome = Outcome::Failed;
std::string requested; // what was asked for
std::string url; // where it ended up, after redirects
gemini::Header header; // the final response's header
gemini::TextBuffer text; // 2x only: the body as lines, up to kMaxBody
bool truncated = false; // the body was cut: see truncatedWhy
std::string truncatedWhy;
std::string error; // Failed: why, for a human
std::string host; // CertificateChanged: whose, and both fingerprints
int port = 1965;
std::string pinned, presented;
std::string cachePath; // with a card: the whole body as received (Saved Pages copy it)
size_t receivedBytes = 0; // the whole body, which may be more than `text` holds
uint32_t ms = 0;
};
// Gemini (docs/milestones/G1.md). Each fetch runs on a short-lived task of its own: a TLS
// handshake can block for seconds, which the main loop's watchdog wouldn't allow, and an idle
// client should cost no stack. Certificates are trusted on first use, per host and port (Q71).
class GeminiService {
public:
static constexpr size_t kMaxBody = 64 * 1024; // Q74
static constexpr size_t kStartFloor = 55 * 1024; // Q86: no fetch below this
static constexpr size_t kSteadyFloor = 40 * 1024; // Q86: what must be left once the page is in
static constexpr size_t kTransientFloor = 20 * 1024; // Q86: while the TLS connection is open
static constexpr int kMaxRedirects = 5; // Q72
static constexpr size_t kMaxOnCard = 1024 * 1024; // a text page larger than this is cut
static constexpr const char* kCachePath = "/gemini/cache/page.gmi";
GeminiService(KeyValueStore& store, StorageService& storage);
// Starts a fetch; false if one is running. The result comes from takeResult().
bool fetch(const std::string& url);
bool busy() const { return busy_; }
bool takeResult(GeminiPage& out);
void cancel() { cancel_ = true; }
// After the user accepts a changed certificate.
void trust(const std::string& host, int port, const std::string& fingerprint);
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
bool fetchToConsole(const std::string& url);
private:
static void taskEntry(void* self);
void run();
void fetchOne(const std::string& url, GeminiPage& page);
void report(const GeminiPage& page, size_t heapBefore, size_t heapLowest);
std::string pinKey(const std::string& host, int port) const;
bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page);
void loadFromCard(GeminiPage& page, size_t freeBefore);
KeyValueStore& store_;
StorageService& storage_;
SemaphoreHandle_t lock_;
std::string pendingUrl_;
GeminiPage result_;
volatile bool busy_ = false, ready_ = false, cancel_ = false, toConsole_ = false;
size_t lowest_ = 0;
};
} // namespace roro