Public Access
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
267 lines
8.5 KiB
C++
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
|