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. |
|
| 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. |
|
| 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. |
|
| 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 `?`. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
## Done when
|
||||||
|
|
||||||
- `gemini get gemini://geminiprotocol.net/` prints the header, size and fingerprint on the console.
|
- `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/irc_service.h"
|
||||||
#include "services/clock_service.h"
|
#include "services/clock_service.h"
|
||||||
#include "services/debug_console.h"
|
#include "services/debug_console.h"
|
||||||
|
#include "services/gemini_service.h"
|
||||||
#include "services/gnss_service.h"
|
#include "services/gnss_service.h"
|
||||||
#include "services/power_service.h"
|
#include "services/power_service.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
@@ -54,6 +55,7 @@ static StorageService* storageService;
|
|||||||
static PowerService* power;
|
static PowerService* power;
|
||||||
static ClockService* clockService;
|
static ClockService* clockService;
|
||||||
static GnssService* gnssService;
|
static GnssService* gnssService;
|
||||||
|
static GeminiService gemini;
|
||||||
static SavedNetworks* savedNetworks;
|
static SavedNetworks* savedNetworks;
|
||||||
static WifiService* wifi;
|
static WifiService* wifi;
|
||||||
static IrcService* irc;
|
static IrcService* irc;
|
||||||
@@ -347,6 +349,7 @@ static const char* const kHelp =
|
|||||||
"coredump erase forget the core dump in flash\n"
|
"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"
|
"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"
|
"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"
|
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
|
||||||
"ls [folder] | rm <path> | install <path.ota> (Update from SD)\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"
|
"sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||||
@@ -462,6 +465,8 @@ static void runCommand(String line) {
|
|||||||
storageService->requestListing();
|
storageService->requestListing();
|
||||||
listingWanted = true;
|
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 start") irc->connect();
|
||||||
if (line == "irc stop") irc->disconnect();
|
if (line == "irc stop") irc->disconnect();
|
||||||
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>
|
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