Public Access
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:
@@ -16,7 +16,7 @@ Gemini (geminiprotocol.net): one request per TLS connection on port 1965, the re
|
||||
| Q75 | Gemtext rendering: text wrapped to the 40-column screen; `#`/`##`/`###` headings in bold and accent; `*` lists with bullets; `>` quotes indented and muted; preformatted blocks unwrapped, Left/Right to scroll; `=>` links with their label, numbered. |
|
||||
| Q76 | Up/Down scroll; Tab and Shift+Tab move between links; Enter follows; Backspace goes back; `g` opens the address line. Links to other protocols show their URL and aren't followed. |
|
||||
| Q77 | Back history of 20 URLs in RAM, with scroll positions; going back refetches (or reopens a Saved Page). **Bookmarks** in `/gemini/bookmarks.gmi` (a gemtext page, shown on the start page); `b` adds the current page. Without a card, a built-in start page. |
|
||||
| Q78 | Start page: bookmarks, then Saved Pages, then defaults: geminiprotocol.net, a search engine (kennedy.gemi.dev), an aggregator (Antenna). |
|
||||
| Q78 | Start page: bookmarks, then Saved Pages, then defaults: geminiprotocol.net, a search engine (kennedy.gemi.dev), an aggregator (Cosmos; Antenna was down when measured). |
|
||||
| Q79 | UTF-8 decoded; characters outside the Latin-1 fonts shown as `?`. |
|
||||
| Q80 | IRC and Gemini can run together: each fetch opens one connection, reads and closes it. If there isn't memory for a second TLS connection, the fetch fails with a clear message and IRC is untouched. Measured in step 1. |
|
||||
| Q81 | Debug aid: `gemini get <url>` prints the status, MIME type, size, certificate fingerprint and the first lines. URL resolution (RFC 3986), the response header and gemtext parsing are host-tested. |
|
||||
@@ -25,6 +25,12 @@ Gemini (geminiprotocol.net): one request per TLS connection on port 1965, the re
|
||||
| Q84 | The start page lists Saved Pages, newest first, grouped by capsule; they open with no network. In a Saved Page, a link to another Saved Page opens the saved copy; other links fetch online if Wi-Fi is up, or say "not saved, offline". A Saved Page shows when it was saved; `r` refreshes it. |
|
||||
| Q85 | Saved Pages are deleted from the App only (`d`, with confirmation), never by Storage Clean-up's age rules, like Notes. |
|
||||
|
||||
## Measured (step 1)
|
||||
|
||||
- `gemini://geminiprotocol.net/`: `20 text/gemini`, 1,184 bytes, TLS handshake 0.7–1.1 s, whole fetch 0.7–1.1 s; kennedy.gemi.dev 1.9 s. The fetch task's stack peaks at about 3.6 KB of 6.
|
||||
- **Heap, Debug Build, IRC connected over TLS:** about 68 KB free before a fetch. After the handshake the fetch holds about 32 KB (36–40 KB left); the handshake itself (certificate chain parsed with the 16 KB receive buffer allocated) dips to about **24 KB** for a second or two. Nothing leaks: the heap after matches the heap before.
|
||||
- Antenna (`warmedal.se`) doesn't answer, from the PC either; the default aggregator becomes Cosmos (`gemini://skyjake.fi/~Cosmos/`, which redirects to `cosmos.skyjake.fi`).
|
||||
|
||||
## Done when
|
||||
|
||||
- `gemini get gemini://geminiprotocol.net/` prints the header, size and fingerprint on the console.
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "services/irc_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/debug_console.h"
|
||||
#include "services/gemini_service.h"
|
||||
#include "services/gnss_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/storage_service.h"
|
||||
@@ -54,6 +55,7 @@ static StorageService* storageService;
|
||||
static PowerService* power;
|
||||
static ClockService* clockService;
|
||||
static GnssService* gnssService;
|
||||
static GeminiService gemini;
|
||||
static SavedNetworks* savedNetworks;
|
||||
static WifiService* wifi;
|
||||
static IrcService* irc;
|
||||
@@ -347,6 +349,7 @@ static const char* const kHelp =
|
||||
"coredump erase forget the core dump in flash\n"
|
||||
"key <name|char> press a key: up down left right select back home del tab, or one character\n"
|
||||
"wifi status | wifi add <ssid><TAB><password>\n"
|
||||
"gemini get <url> fetch a Gemini page and report header, size, certificate, heap\n"
|
||||
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
|
||||
"ls [folder] | rm <path> | install <path.ota> (Update from SD)\n"
|
||||
"sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||
@@ -462,6 +465,8 @@ static void runCommand(String line) {
|
||||
storageService->requestListing();
|
||||
listingWanted = true;
|
||||
}
|
||||
if (line.startsWith("gemini get "))
|
||||
if (!gemini.fetchToConsole(line.substring(11).c_str())) console.println("gemini: a fetch is already running");
|
||||
if (line == "irc start") irc->connect();
|
||||
if (line == "irc stop") irc->disconnect();
|
||||
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user