G1 step 1: gemini get, and two TLS connections measured

`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
This commit is contained in:
2026-10-05 00:27:05 +02:00
co-authored by Claude Opus 5.5
parent dc9b0e14ce
commit 2d384f5cff
4 changed files with 137 additions and 1 deletions
+104
View File
@@ -0,0 +1,104 @@
#include "services/gemini_service.h"
#include <NetworkClientSecure.h>
#include <esp_heap_caps.h>
#include <algorithm>
#include <memory>
#include "platform/console.h"
namespace roro {
namespace {
struct ConsoleFetch {
GeminiService* service;
std::string url;
volatile bool* busy;
};
std::string hex(const uint8_t* data, size_t len) {
static const char* digits = "0123456789abcdef";
std::string out;
for (size_t i = 0; i < len; i++) {
out += digits[data[i] >> 4];
out += digits[data[i] & 15];
}
return out;
}
} // namespace
bool GeminiService::fetchToConsole(const std::string& url) {
if (busy_) return false;
busy_ = true;
auto* job = new ConsoleFetch{this, url, &busy_};
if (xTaskCreate(consoleTask, "gemini", 6144, job, 1, nullptr) != pdPASS) {
delete job;
busy_ = false;
return false;
}
return true;
}
void GeminiService::consoleTask(void* arg) {
std::unique_ptr<ConsoleFetch> job(static_cast<ConsoleFetch*>(arg));
// Until step 2's URL parser: gemini://host[:port]/path
std::string rest = job->url.substr(job->url.find("://") == std::string::npos ? 0 : job->url.find("://") + 3);
std::string authority = rest.substr(0, rest.find('/'));
std::string host = authority.substr(0, authority.find(':'));
uint16_t port = authority.find(':') == std::string::npos ? 1965 : atoi(authority.substr(authority.find(':') + 1).c_str());
size_t before = esp_get_free_heap_size(), lowest = before;
auto track = [&lowest]() { lowest = std::min<size_t>(lowest, esp_get_free_heap_size()); };
uint32_t start = millis();
{
NetworkClientSecure tls;
tls.setInsecure(); // trust on first use: the fingerprint is what counts (Q71)
tls.setTimeout(15);
if (!tls.connect(host.c_str(), port)) {
console.printf("gemini: cannot connect to %s:%u\n", host.c_str(), port);
} else {
uint32_t connected = millis();
track(); // the handshake's buffers are allocated now
uint8_t sha[32];
std::string fingerprint = tls.getFingerprintSHA256(sha) ? hex(sha, 32) : "?";
tls.print((job->url + "\r\n").c_str());
std::string header, sample;
size_t body = 0;
bool inBody = false;
uint32_t last = millis();
while (tls.connected() || tls.available()) {
int c = tls.read();
if (c < 0) {
if (millis() - last > 15000) break;
delay(5);
continue;
}
last = millis();
if ((body & 255) == 0) track();
if (!inBody) {
if (c == '\n') inBody = true;
else if (c != '\r' && header.size() < 1100) header += static_cast<char>(c);
} else {
body++;
if (sample.size() < 400) sample += static_cast<char>(c);
}
}
console.printf("gemini: %s\ngemini: header '%s', %u bytes, TLS %lu ms, total %lu ms\n", job->url.c_str(),
header.c_str(), (unsigned)body, (unsigned long)(connected - start),
(unsigned long)(millis() - start));
console.printf("gemini: certificate sha256 %s\n", fingerprint.c_str());
console.printf("--- first bytes ---\n%s\n--- end ---\n", sample.c_str());
}
tls.stop();
}
console.printf("gemini: heap %u before, %u lowest during, %u after; stack left %u\n", (unsigned)before,
(unsigned)lowest, (unsigned)esp_get_free_heap_size(), (unsigned)uxTaskGetStackHighWaterMark(nullptr));
*job->busy = false;
job.reset();
vTaskDelete(nullptr);
}
} // namespace roro
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <string>
namespace roro {
// 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.
class GeminiService {
public:
// `gemini get <url>`: fetches and prints the header, size, certificate fingerprint, the first
// lines and the heap around the fetch to the console. False if a fetch is already running.
bool fetchToConsole(const std::string& url);
private:
static void consoleTask(void* arg);
volatile bool busy_ = false;
};
} // namespace roro