Files
roro9stack/src/services/irc_service.cpp
T
twislaandClaude Sonnet 5.5 510ce42a99 Updates from Gitea, step 3: install from Gitea, and IRC steps aside for it
A release downloads straight into the inactive slot through the existing
install path: the signature is checked after 160 bytes, before anything
is written, the hash at the end. Tried on the device against the real
server: a download cut short, a flipped byte in the signature and one in
the image are each refused with the running firmware untouched; the real
v0.10.0 installed, restarted, and confirmed itself on Probation.

A TLS connection to Gitea peaks at about 52 KB of heap whether or not the
certificate is verified. With IRC connected (66 KB free) a check left 3 KB
and a download 836 bytes. A check, list or install a person asks for now
makes IRC step aside (holdForUpdate) and come back after: the lowest free
heap during a full download with IRC connected is 38 KB. The daily check
never interrupts IRC; with IRC up it waits. A TLS connection starts with
80 KB free (it was 55).

Debug Builds get test knobs: update probe <host>, update damage cut|flip,
update pretend <version>.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 15:51:10 +02:00

267 lines
8.5 KiB
C++

#include "irc_service.h"
#include <Arduino.h>
#include <cstdio>
#include "storage_paths.h"
namespace roro {
namespace {
constexpr uint32_t kQuietPingMs = 240000; // no traffic for 4 min: ping the server
constexpr uint32_t kDeadMs = 330000; // and give up if it still says nothing
std::string hex(const uint8_t* bytes, size_t n) {
std::string out;
char buf[3];
for (size_t i = 0; i < n; i++) {
std::snprintf(buf, sizeof(buf), "%02x", bytes[i]);
out += buf;
}
return out;
}
} // namespace
IrcService::IrcService(KeyValueStore& store, const std::string& defaultNick, WifiService& wifi, StorageService& storage,
ClockService& clock, EventBus& bus)
: config_(store), wifi_(wifi), storage_(storage), clock_(clock), bus_(bus) {
config_.load(defaultNick);
session_.reset(new IrcSession(config_));
}
void IrcService::start() {
if (task_) return;
lock_ = xSemaphoreCreateMutex();
xTaskCreate(taskEntry, "irc", 6144, this, 1, &task_); // peak 4.0 KB (TLS handshake, M2)
}
void IrcService::connect() {
stoppedByUser_ = false;
if (wanted_) return;
retryAtMs_ = millis();
backoff_.reset();
wanted_ = true;
}
void IrcService::disconnect() {
stoppedByUser_ = true;
stopRequested_ = true; // the IRC task says QUIT and closes; nothing to do if it never started
if (!task_) wanted_ = false;
}
int IrcService::totalUnread() {
Lock l(lock_);
return session_->totalUnread();
}
IrcConfig IrcService::draftConfig() {
Lock l(lock_);
return config_;
}
std::string IrcService::applyConfig(const IrcConfig& draft) {
std::string error = draft.validate();
if (!error.empty()) return error;
Lock l(lock_);
config_.copySettingsFrom(draft);
config_.save();
if (wanted_) restart_ = true; // the task reconnects with a fresh session
else session_.reset(new IrcSession(config_));
return "";
}
void IrcService::taskEntry(void* self) { static_cast<IrcService*>(self)->loop(); }
void IrcService::scheduleRetry(const std::string& why) {
uint32_t delay = backoff_.nextDelayMs();
retryAtMs_ = millis() + delay;
status_ = Status::Retrying;
Lock l(lock_);
session_->disconnected(clock_.utcNow(), why + ", retrying in " + std::to_string(delay / 1000) + " s");
}
bool IrcService::open() {
status_ = Status::Connecting;
IrcConfig settings = draftConfig(); // a snapshot: the UI may apply new settings meanwhile
bool pinning = settings.tls && settings.allowSelfSigned;
conn_ = settings.tls ? static_cast<NetworkClient*>(&tlsClient_) : &plainClient_;
if (settings.tls) {
tlsClient_.setTimeout(15); // seconds, for the handshake
if (pinning) tlsClient_.setInsecure();
else tlsClient_.useBuiltinCACertBundle();
}
if (!conn_->connect(settings.host.c_str(), settings.port)) return false;
if (pinning) {
uint8_t sha[32];
if (!tlsClient_.getFingerprintSHA256(sha)) {
tlsClient_.stop();
return false;
}
std::string fingerprint = hex(sha, sizeof(sha));
if (settings.pinnedSha256.empty()) {
Lock l(lock_);
config_.pinnedSha256 = fingerprint; // trust on first use
config_.save();
} else if (settings.pinnedSha256 != fingerprint) {
tlsClient_.stop();
Lock l(lock_);
session_->disconnected(clock_.utcNow(), "server certificate changed: not connecting");
return false;
}
}
open_ = true;
partial_.clear();
lastRxMs_ = millis();
pingSent_ = false;
status_ = Status::Registering;
Lock l(lock_);
session_->connected(clock_.utcNow());
return true;
}
void IrcService::close(const std::string& reason) {
if (open_) conn_->stop();
open_ = false;
if (!reason.empty()) {
Lock l(lock_);
session_->disconnected(clock_.utcNow(), reason);
}
}
void IrcService::readLines() {
while (conn_->available()) {
char c = static_cast<char>(conn_->read());
if (c == '\n') {
lastRxMs_ = millis();
pingSent_ = false;
Lock l(lock_);
session_->receive(partial_, clock_.utcNow());
partial_.clear();
} else if (c != '\r' && partial_.size() < 1024) {
partial_ += c;
}
}
}
void IrcService::flushEffects() {
IrcEffects fx;
bool registered, quit;
{
Lock l(lock_);
fx = session_->takeEffects();
registered = session_->registered();
quit = session_->quitRequested();
}
for (auto& line : fx.send)
if (open_) conn_->print((line + "\r\n").c_str());
std::string date = clock_.localDate();
std::string host;
{
Lock l(lock_);
host = config_.host;
}
for (auto& entry : fx.logs) {
const IrcLine& l = entry.line;
std::string when = l.utc >= 0 ? ClockModel::formatLocalTime(l.utc) : "--:--";
std::string text;
switch (l.kind) {
case IrcLine::Kind::Action:
case IrcLine::Kind::OwnAction: text = "* " + l.nick + " " + l.text; break;
case IrcLine::Kind::Notice: text = "-" + l.nick + "- " + l.text; break;
case IrcLine::Kind::Info: text = l.text; break;
default: text = "<" + l.nick + "> " + l.text; break;
}
storage_.appendLine(storage::dailyLogPath({"irc", host, entry.buffer}, date), when + " " + text);
}
for (auto& n : fx.notifications)
bus_.publish(Event::withText(EventType::Notification, n.c_str(), static_cast<int32_t>(NotificationLevel::Message)));
if (registered && status_ == Status::Registering) {
status_ = Status::Online;
backoff_.reset();
}
if (quit) { // /quit: QUIT is already on its way if connected; stop in any state
stoppedByUser_ = true;
stopRequested_ = true;
quitSent_ = true;
}
}
void IrcService::loop() {
for (;;) {
uint32_t now = millis();
bool wifiUp = wifi_.state() == WifiController::State::Connected;
bool monitoring = wifi_.state() == WifiController::State::Monitoring;
if (stopRequested_) {
stopRequested_ = false;
if (open_) {
if (!quitSent_) conn_->print("QUIT :roro9stack\r\n");
vTaskDelay(pdMS_TO_TICKS(300)); // let QUIT reach the server
close("");
}
quitSent_ = false;
wanted_ = false;
status_ = Status::Stopped;
Lock l(lock_);
session_->disconnected(clock_.utcNow(), "disconnected (Enter reconnects)");
}
if (restart_) {
restart_ = false;
if (open_) {
conn_->print("QUIT :reconfiguring\r\n");
vTaskDelay(pdMS_TO_TICKS(300));
close("");
}
Lock l(lock_);
session_.reset(new IrcSession(config_));
retryAtMs_ = now;
backoff_.reset();
}
if (!wanted_) {
if (open_) close("disconnected");
status_ = Status::Stopped;
} else if (held_) {
if (open_) {
conn_->print("QUIT :updating\r\n");
vTaskDelay(pdMS_TO_TICKS(300));
close("paused while the device updates");
}
status_ = Status::Paused;
retryAtMs_ = now; // back at once when released
} else if (!wifiUp) {
if (open_) close(monitoring ? "paused for Wi-Fi monitoring" : "Wi-Fi lost");
status_ = monitoring ? Status::Paused : Status::WaitingForWifi;
retryAtMs_ = now; // reconnect as soon as Wi-Fi is back
} else if (!open_) {
if (static_cast<int32_t>(now - retryAtMs_) >= 0) {
if (!open()) scheduleRetry("could not connect to " + draftConfig().host);
}
} else if (!conn_->connected()) {
close("");
scheduleRetry("connection lost");
} else {
readLines();
uint32_t quiet = millis() - lastRxMs_;
if (quiet > kDeadMs) {
close("");
scheduleRetry("server stopped answering");
} else if (quiet > kQuietPingMs && !pingSent_) {
conn_->print("PING :roro9stack\r\n");
pingSent_ = true;
}
}
{
Lock l(lock_);
session_->tick(millis());
}
flushEffects();
vTaskDelay(pdMS_TO_TICKS(open_ ? 30 : 250));
}
}
} // namespace roro