Public Access
M1 step 4b: IRC Service on the device; 8-bit frame buffer
- IrcService: networking on its own task, TLS with the built-in CA bundle (or trust-on-first-use pinning when self-signed is allowed), plain TCP when TLS is off; connects only while Wi-Fi is Connected, marks pauses for Monitoring, reconnects with backoff, pings a quiet server, writes Logs, raises Notifications for Mentions - Session: forget /quit once disconnected (it was handled every loop); the server Buffer never counts as unread (MOTD showed as [4]) - Status Bar: unread count - Frame buffer 16 -> 8-bit colour (M1 Q46): min free heap with IRC on TLS went from 51 KB to 79 KB - Serial: irc start / say / dump Verified on the device against irc.libera.chat:6697: certificate checked, joined #roro9stack-test, sent a message, quit cleanly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -50,6 +50,9 @@ sudo usermod -aG dialout "$USER"
|
|||||||
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
||||||
| `log <text>` | Appends a line to a test IRC Log (`/irc/dev/#test/<date>.log`) |
|
| `log <text>` | Appends a line to a test IRC Log (`/irc/dev/#test/<date>.log`) |
|
||||||
| `sd list` | Lists the files of each Storage Clean-up category |
|
| `sd list` | Lists the files of each Storage Clean-up category |
|
||||||
|
| `irc start` | Starts the IRC Service (normally done by opening the IRC App) |
|
||||||
|
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
||||||
|
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
||||||
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap |
|
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap |
|
||||||
|
|
||||||
`scripts/flash.sh` stops a running serial log first, since it would hold the port.
|
`scripts/flash.sh` stops a running serial log first, since it would hold the port.
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ void IrcSession::add(int b, IrcLine::Kind kind, const std::string& nick, const s
|
|||||||
effects_.logs.push_back({buf.name, line});
|
effects_.logs.push_back({buf.name, line});
|
||||||
|
|
||||||
bool fromOthers = kind == IrcLine::Kind::Message || kind == IrcLine::Kind::Action || kind == IrcLine::Kind::Notice;
|
bool fromOthers = kind == IrcLine::Kind::Message || kind == IrcLine::Kind::Action || kind == IrcLine::Kind::Notice;
|
||||||
if (b != viewing_ && fromOthers) {
|
if (b != viewing_ && fromOthers && buf.type != IrcBuffer::Type::Server) {
|
||||||
buf.unread++;
|
buf.unread++;
|
||||||
if (mention) {
|
if (mention) {
|
||||||
buf.mentioned = true;
|
buf.mentioned = true;
|
||||||
@@ -114,6 +114,7 @@ void IrcSession::connected(int64_t) {
|
|||||||
|
|
||||||
void IrcSession::disconnected(int64_t utc, const std::string& reason) {
|
void IrcSession::disconnected(int64_t utc, const std::string& reason) {
|
||||||
registered_ = false;
|
registered_ = false;
|
||||||
|
quit_ = false;
|
||||||
rejoin_.clear();
|
rejoin_.clear();
|
||||||
for (int i = 0; i < bufferCount(); i++) {
|
for (int i = 0; i < bufferCount(); i++) {
|
||||||
if (buffers_[i].type == IrcBuffer::Type::Channel && buffers_[i].joined) rejoin_.push_back(buffers_[i].name);
|
if (buffers_[i].type == IrcBuffer::Type::Channel && buffers_[i].joined) rejoin_.push_back(buffers_[i].name);
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#include "key_mapper.h"
|
#include "key_mapper.h"
|
||||||
#include "platform/nvs_store.h"
|
#include "platform/nvs_store.h"
|
||||||
#include "service_manager.h"
|
#include "service_manager.h"
|
||||||
|
#include "platform/identity.h"
|
||||||
#include "services/battery_service.h"
|
#include "services/battery_service.h"
|
||||||
|
#include "services/irc_service.h"
|
||||||
#include "services/clock_service.h"
|
#include "services/clock_service.h"
|
||||||
#include "services/power_service.h"
|
#include "services/power_service.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
@@ -37,6 +39,7 @@ static PowerService* power;
|
|||||||
static ClockService* clockService;
|
static ClockService* clockService;
|
||||||
static SavedNetworks* savedNetworks;
|
static SavedNetworks* savedNetworks;
|
||||||
static WifiService* wifi;
|
static WifiService* wifi;
|
||||||
|
static IrcService* irc;
|
||||||
static Notifier* notifier;
|
static Notifier* notifier;
|
||||||
static LauncherApp launcher;
|
static LauncherApp launcher;
|
||||||
static AppManager* apps;
|
static AppManager* apps;
|
||||||
@@ -66,6 +69,7 @@ static StatusInfo currentStatus() {
|
|||||||
s.sdPresent = storageState.present;
|
s.sdPresent = storageState.present;
|
||||||
s.sdLevel = storageState.level;
|
s.sdLevel = storageState.level;
|
||||||
s.compose = keyMapper.pendingCompose();
|
s.compose = keyMapper.pendingCompose();
|
||||||
|
s.unread = irc->totalUnread();
|
||||||
using WifiState = WifiController::State;
|
using WifiState = WifiController::State;
|
||||||
switch (wifi->state()) {
|
switch (wifi->state()) {
|
||||||
case WifiState::Connected: {
|
case WifiState::Connected: {
|
||||||
@@ -97,6 +101,7 @@ void setup() {
|
|||||||
savedNetworks = new SavedNetworks(nvs);
|
savedNetworks = new SavedNetworks(nvs);
|
||||||
savedNetworks->load();
|
savedNetworks->load();
|
||||||
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
||||||
|
irc = new IrcService(nvs, "roro_" + identity::defaultShortName(), *wifi, *storageService, *clockService, bus);
|
||||||
notifier = new Notifier(bus, settings);
|
notifier = new Notifier(bus, settings);
|
||||||
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
||||||
services.add(*power);
|
services.add(*power);
|
||||||
@@ -104,6 +109,7 @@ void setup() {
|
|||||||
services.add(*battery);
|
services.add(*battery);
|
||||||
services.add(*storageService);
|
services.add(*storageService);
|
||||||
services.add(*wifi);
|
services.add(*wifi);
|
||||||
|
services.add(*irc);
|
||||||
|
|
||||||
apps = new AppManager(launcher);
|
apps = new AppManager(launcher);
|
||||||
launcher.setManager(*apps);
|
launcher.setManager(*apps);
|
||||||
@@ -172,6 +178,27 @@ static void serialCommands() {
|
|||||||
storageService->requestListing();
|
storageService->requestListing();
|
||||||
listingWanted = true;
|
listingWanted = true;
|
||||||
}
|
}
|
||||||
|
if (line == "irc start") irc->connect();
|
||||||
|
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>
|
||||||
|
String rest = line.substring(8);
|
||||||
|
int space = rest.indexOf(' ');
|
||||||
|
int b = rest.substring(0, space).toInt();
|
||||||
|
std::string text = rest.substring(space + 1).c_str();
|
||||||
|
irc->withSession([&](IrcSession& s) { s.input(b, text, clockService->utcNow()); });
|
||||||
|
}
|
||||||
|
if (line == "irc dump") {
|
||||||
|
Serial.printf("irc: status %d, unread %d, heap %u min %u\n", (int)irc->status(), irc->totalUnread(),
|
||||||
|
ESP.getFreeHeap(), ESP.getMinFreeHeap());
|
||||||
|
irc->withSession([](IrcSession& s) {
|
||||||
|
for (int i = 0; i < s.bufferCount(); i++) {
|
||||||
|
const auto& b = s.buffer(i);
|
||||||
|
Serial.printf("irc buffer %d %s unread %d%s\n", i, b.name.c_str(), b.unread, b.joined ? " joined" : "");
|
||||||
|
size_t from = b.lines.size() > 6 ? b.lines.size() - 6 : 0;
|
||||||
|
for (size_t j = from; j < b.lines.size(); j++)
|
||||||
|
Serial.printf(" <%s> %s\n", b.lines[j].nick.c_str(), b.lines[j].text.c_str());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
if (line == "wifi status")
|
if (line == "wifi status")
|
||||||
Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(),
|
Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(),
|
||||||
wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(),
|
wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(),
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class ClockService : public Service {
|
|||||||
bool set(int64_t utcSeconds, TimeSource source) { return model_.set(utcSeconds, source, millis()); }
|
bool set(int64_t utcSeconds, TimeSource source) { return model_.set(utcSeconds, source, millis()); }
|
||||||
const ClockModel& model() const { return model_; }
|
const ClockModel& model() const { return model_; }
|
||||||
|
|
||||||
|
// UTC seconds, or -1 if the clock isn't set.
|
||||||
|
int64_t utcNow() const { return model_.isSet() ? model_.utcNow(millis()) : -1; }
|
||||||
|
|
||||||
// Local "YYYY-MM-DD" once set; otherwise empty (Logs then go to undated files).
|
// Local "YYYY-MM-DD" once set; otherwise empty (Logs then go to undated files).
|
||||||
std::string localDate() const { return model_.isSet() ? ClockModel::formatLocalDate(model_.utcNow(millis())) : ""; }
|
std::string localDate() const { return model_.isSet() ? ClockModel::formatLocalDate(model_.utcNow(millis())) : ""; }
|
||||||
// Days since 1970-01-01 (UTC), or -1 if the clock isn't set.
|
// Days since 1970-01-01 (UTC), or -1 if the clock isn't set.
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
#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", 8192, this, 1, &task_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IrcService::connect() {
|
||||||
|
if (wanted_) return;
|
||||||
|
retryAtMs_ = millis();
|
||||||
|
backoff_.reset();
|
||||||
|
wanted_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IrcService::totalUnread() {
|
||||||
|
Lock l(lock_);
|
||||||
|
return session_->totalUnread();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string IrcService::saveConfig() {
|
||||||
|
std::string error = config_.save();
|
||||||
|
if (error.empty() && !wanted_) {
|
||||||
|
Lock l(lock_);
|
||||||
|
session_.reset(new IrcSession(config_)); // a new server or nick starts a fresh session
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
bool pinning = config_.tls && config_.allowSelfSigned;
|
||||||
|
conn_ = config_.tls ? static_cast<NetworkClient*>(&tlsClient_) : &plainClient_;
|
||||||
|
if (config_.tls) {
|
||||||
|
tlsClient_.setTimeout(15); // seconds, for the handshake
|
||||||
|
if (pinning) tlsClient_.setInsecure();
|
||||||
|
else tlsClient_.useBuiltinCACertBundle();
|
||||||
|
}
|
||||||
|
if (!conn_->connect(config_.host.c_str(), config_.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 (config_.pinnedSha256.empty()) {
|
||||||
|
config_.pinnedSha256 = fingerprint; // trust on first use
|
||||||
|
config_.save();
|
||||||
|
} else if (config_.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();
|
||||||
|
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", config_.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 && open_) {
|
||||||
|
delay(300); // let QUIT reach the server
|
||||||
|
close("");
|
||||||
|
wanted_ = false;
|
||||||
|
status_ = Status::Stopped;
|
||||||
|
Lock l(lock_);
|
||||||
|
session_->disconnected(clock_.utcNow(), "disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IrcService::loop() {
|
||||||
|
for (;;) {
|
||||||
|
uint32_t now = millis();
|
||||||
|
bool wifiUp = wifi_.state() == WifiController::State::Connected;
|
||||||
|
bool monitoring = wifi_.state() == WifiController::State::Monitoring;
|
||||||
|
|
||||||
|
if (!wanted_) {
|
||||||
|
if (open_) close("disconnected");
|
||||||
|
status_ = Status::Stopped;
|
||||||
|
} 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 " + config_.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushEffects();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(open_ ? 30 : 250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <NetworkClientSecure.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "event_bus.h"
|
||||||
|
#include "irc_config.h"
|
||||||
|
#include "irc_session.h"
|
||||||
|
#include "reconnect_policy.h"
|
||||||
|
#include "service.h"
|
||||||
|
#include "services/clock_service.h"
|
||||||
|
#include "services/storage_service.h"
|
||||||
|
#include "services/wifi_service.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
// Keeps the IRC connection alive in the background once started (see IRC Service in CONTEXT.md):
|
||||||
|
// connects over TLS while Wi-Fi is Connected, pauses while Monitoring, reconnects with backoff,
|
||||||
|
// writes Logs and raises Notifications for Mentions. Networking runs on its own task; the
|
||||||
|
// session is shared with the IRC App under a lock.
|
||||||
|
class IrcService : public Service {
|
||||||
|
public:
|
||||||
|
enum class Status { Stopped, WaitingForWifi, Connecting, Registering, Online, Paused, Retrying };
|
||||||
|
|
||||||
|
IrcService(KeyValueStore& store, const std::string& defaultNick, WifiService& wifi, StorageService& storage,
|
||||||
|
ClockService& clock, EventBus& bus);
|
||||||
|
const char* name() const override { return "irc"; }
|
||||||
|
void start() override;
|
||||||
|
|
||||||
|
// Starts the session (the IRC App does this when opened); /quit stops it.
|
||||||
|
void connect();
|
||||||
|
bool running() const { return wanted_; }
|
||||||
|
Status status() const { return status_; }
|
||||||
|
|
||||||
|
// Read or act on the session while holding its lock: withSession([](IrcSession& s) { ... }).
|
||||||
|
template <typename F>
|
||||||
|
auto withSession(F f) {
|
||||||
|
Lock l(lock_);
|
||||||
|
return f(*session_);
|
||||||
|
}
|
||||||
|
int totalUnread();
|
||||||
|
|
||||||
|
IrcConfig& config() { return config_; }
|
||||||
|
// Saves the config; takes effect at the next connection.
|
||||||
|
std::string saveConfig();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Lock {
|
||||||
|
explicit Lock(SemaphoreHandle_t m) : m_(m) { xSemaphoreTake(m_, portMAX_DELAY); }
|
||||||
|
~Lock() { xSemaphoreGive(m_); }
|
||||||
|
SemaphoreHandle_t m_;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void taskEntry(void* self);
|
||||||
|
void loop();
|
||||||
|
bool open();
|
||||||
|
void close(const std::string& reason);
|
||||||
|
void readLines();
|
||||||
|
void flushEffects();
|
||||||
|
void scheduleRetry(const std::string& why);
|
||||||
|
|
||||||
|
IrcConfig config_;
|
||||||
|
WifiService& wifi_;
|
||||||
|
StorageService& storage_;
|
||||||
|
ClockService& clock_;
|
||||||
|
EventBus& bus_;
|
||||||
|
std::unique_ptr<IrcSession> session_;
|
||||||
|
SemaphoreHandle_t lock_ = nullptr;
|
||||||
|
TaskHandle_t task_ = nullptr;
|
||||||
|
NetworkClientSecure tlsClient_;
|
||||||
|
NetworkClient plainClient_;
|
||||||
|
NetworkClient* conn_ = &tlsClient_; // whichever the config asks for
|
||||||
|
ReconnectPolicy backoff_;
|
||||||
|
std::string partial_;
|
||||||
|
volatile bool wanted_ = false;
|
||||||
|
volatile Status status_ = Status::Stopped;
|
||||||
|
bool open_ = false;
|
||||||
|
uint32_t retryAtMs_ = 0;
|
||||||
|
uint32_t lastRxMs_ = 0;
|
||||||
|
bool pingSent_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
namespace roro {
|
namespace roro {
|
||||||
|
|
||||||
bool Screen::begin() {
|
bool Screen::begin() {
|
||||||
canvas_.setColorDepth(16);
|
canvas_.setColorDepth(8); // RGB332: saves 32 KB of RAM (M1 Q46); theme colours are converted
|
||||||
canvas_.setPsram(false);
|
canvas_.setPsram(false);
|
||||||
return canvas_.createSprite(theme::kWidth, theme::kHeight) != nullptr;
|
return canvas_.createSprite(theme::kWidth, theme::kHeight) != nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ namespace roro {
|
|||||||
// Composes the frame (Status Bar, foreground App, Toast) off-screen and pushes it to the display.
|
// Composes the frame (Status Bar, foreground App, Toast) off-screen and pushes it to the display.
|
||||||
class Screen {
|
class Screen {
|
||||||
public:
|
public:
|
||||||
bool begin(); // allocates the frame buffer (~64 KB)
|
bool begin(); // allocates the frame buffer (~32 KB at 8-bit colour)
|
||||||
void render(AppManager& apps, const StatusInfo& status, const Toast* toast);
|
void render(AppManager& apps, const StatusInfo& status, const Toast* toast);
|
||||||
Canvas& canvas() { return canvas_; }
|
Canvas& canvas() { return canvas_; }
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ void statusBar(Canvas& c, const StatusInfo& info) {
|
|||||||
x -= c.textWidth(text.c_str()) + 6;
|
x -= c.textWidth(text.c_str()) + 6;
|
||||||
};
|
};
|
||||||
right(info.clock, kText);
|
right(info.clock, kText);
|
||||||
|
if (info.unread > 0) right("[" + std::to_string(info.unread) + "]", kMessage);
|
||||||
if (info.batteryPercent >= 0)
|
if (info.batteryPercent >= 0)
|
||||||
right(std::to_string(info.batteryPercent) + "%", info.batteryPercent <= 15 ? kWarning : kText);
|
right(std::to_string(info.batteryPercent) + "%", info.batteryPercent <= 15 ? kWarning : kText);
|
||||||
if (info.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kMuted);
|
if (info.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kMuted);
|
||||||
|
|||||||
+2
-1
@@ -23,11 +23,12 @@ struct StatusInfo {
|
|||||||
char compose = 0; // pending Compose Key state
|
char compose = 0; // pending Compose Key state
|
||||||
enum class Wifi { None, Searching, Connected, Monitoring } wifi = Wifi::None;
|
enum class Wifi { None, Searching, Connected, Monitoring } wifi = Wifi::None;
|
||||||
int wifiBars = 0; // 0..3 when Connected
|
int wifiBars = 0; // 0..3 when Connected
|
||||||
|
int unread = 0; // unread messages across Services
|
||||||
|
|
||||||
bool operator==(const StatusInfo& o) const {
|
bool operator==(const StatusInfo& o) const {
|
||||||
return title == o.title && batteryPercent == o.batteryPercent && clock == o.clock &&
|
return title == o.title && batteryPercent == o.batteryPercent && clock == o.clock &&
|
||||||
sdPresent == o.sdPresent && sdLevel == o.sdLevel && compose == o.compose && wifi == o.wifi &&
|
sdPresent == o.sdPresent && sdLevel == o.sdLevel && compose == o.compose && wifi == o.wifi &&
|
||||||
wifiBars == o.wifiBars;
|
wifiBars == o.wifiBars && unread == o.unread;
|
||||||
}
|
}
|
||||||
bool operator!=(const StatusInfo& o) const { return !(*this == o); }
|
bool operator!=(const StatusInfo& o) const { return !(*this == o); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -316,6 +316,23 @@ void test_buffer_keeps_only_recent_lines() {
|
|||||||
TEST_ASSERT_EQUAL(IrcSession::kLinesPerBuffer, f.session->buffer(f.buffer("#roro")).lines.size());
|
TEST_ASSERT_EQUAL(IrcSession::kLinesPerBuffer, f.session->buffer(f.buffer("#roro")).lines.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_quit_is_forgotten_once_disconnected() {
|
||||||
|
Fixture f;
|
||||||
|
f.registerNow();
|
||||||
|
f.session->input(0, "/quit", 0);
|
||||||
|
f.session->disconnected(0, "disconnected");
|
||||||
|
TEST_ASSERT_FALSE(f.session->quitRequested());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_server_buffer_never_counts_as_unread() {
|
||||||
|
Fixture f;
|
||||||
|
f.session->connected(0);
|
||||||
|
f.recv(":srv NOTICE * :*** Looking up your hostname...");
|
||||||
|
f.recv(":srv 372 roro :- Message of the day");
|
||||||
|
f.recv(":srv 001 roro :Welcome");
|
||||||
|
TEST_ASSERT_EQUAL(0, f.session->totalUnread());
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_plain_registration_sends_nick_and_user);
|
RUN_TEST(test_plain_registration_sends_nick_and_user);
|
||||||
@@ -341,5 +358,7 @@ int main() {
|
|||||||
RUN_TEST(test_names_reply_only_shown_when_asked);
|
RUN_TEST(test_names_reply_only_shown_when_asked);
|
||||||
RUN_TEST(test_disconnect_marks_a_gap_and_reconnect_rejoins);
|
RUN_TEST(test_disconnect_marks_a_gap_and_reconnect_rejoins);
|
||||||
RUN_TEST(test_buffer_keeps_only_recent_lines);
|
RUN_TEST(test_buffer_keeps_only_recent_lines);
|
||||||
|
RUN_TEST(test_quit_is_forgotten_once_disconnected);
|
||||||
|
RUN_TEST(test_server_buffer_never_counts_as_unread);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user