Stop IRC by hand; remove mDNS

IrcService::disconnect() stops the session from any state: QUIT if
connected, then no more retries. /quit goes through it (before, it only
stopped a connected session; while waiting for Wi-Fi or retrying it did
nothing), and so does the new `irc stop` command. Stopped by hand,
opening the IRC App no longer reconnects; typing a line does.

mDNS is gone: it never crossed the dev box's routed network, and it
cost about 7.5 KB of RAM. Pushes go to the IP shown in Settings ->
Firmware, which drops its Name row. OTA Q54 records the change.

On the device, Debug Build: 105.6 KB free with Wi-Fi (was 98); with IRC
on TLS 54 KB free (was 46); after `irc stop`, back to 99 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-04 23:17:48 +02:00
co-authored by Claude Opus 5.5
parent db265fb757
commit 4e439410cd
12 changed files with 39 additions and 23 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ A Wi-Fi network the device may join on its own (name, password). When several ar
_Avoid_: profile, known network
**IRC Service**:
The Service that keeps the IRC connection alive in the background once the IRC App has started it, until the user disconnects. It reconnects after drops, and pauses while the Wi-Fi Service is Monitoring. It does not start by itself after a reboot.
The Service that keeps the IRC connection alive in the background once the IRC App has started it, until the user stops it (`/quit`, or `irc stop` on the console). Once stopped by hand, opening the App again doesn't reconnect; typing a line does. It reconnects after drops, and pauses while the Wi-Fi Service is Monitoring. It does not start by itself after a reboot.
_Avoid_: IRC client (that's the App)
**Buffer**:
+1
View File
@@ -67,6 +67,7 @@ To install from the SD card instead, copy the `.ota` file from `.pio/build/cardp
| `sd list` | Lists the files of each Storage Clean-up category |
| `cat <path>` | Prints the first ~1.2 KB of a file on the SD card |
| `irc start` | Starts the IRC Service (normally done by opening the IRC App) |
| `irc stop` | Stops it, as `/quit` does: QUIT if connected, no more retries, and the App stays disconnected until you type |
| `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 |
+1 -1
View File
@@ -8,7 +8,7 @@
|---|---|
| Q52 | Two sources: **push over Wi-Fi** from the PC, and **from the SD card**. Pulling from Gitea releases is deferred. |
| Q53 | **Signed Update Files** (ECDSA P-256 over SHA-256). The private key stays in `~/.config/roro9stack/`, and the firmware embeds the public key (ADR 0003). |
| Q54 | The device **always listens** for pushes on the LAN while Wi-Fi is Connected, announced as `roro9stack-<id>.local`. |
| Q54 | The device **always listens** for pushes on the LAN while Wi-Fi is Connected. *Revised in M2:* it was announced over mDNS as `roro9stack-<id>.local`; mDNS was removed to save RAM (it never crossed the dev box's routed network anyway). Pushes go to the IP shown in Settings → Firmware. |
| Q55 | New firmware runs on **Probation**. It's confirmed once booted, UI drawn, Services started, 30 s without a crash, and Wi-Fi connected (if configured). Otherwise **Rollback**. A Toast reports either outcome. |
| Q56 | **Downgrades are allowed**, with "older than the installed version" shown. |
| Q57 | A valid push **installs right away**: progress screen, then reboot. The reboot waits for Text Entry to end, 60 s at most. |
+1 -1
View File
@@ -2,7 +2,7 @@
"""Pushes a signed Update File to a Cardputer over Wi-Fi (TCP 3232) and reports the result.
Usage: scripts/ota_push.py <file.ota> <host>
<host> is the device's IP (shown in Settings > About), or its name when mDNS works on your network.
<host> is the device's IP (shown in Settings > Firmware).
"""
import socket
import sys
-2
View File
@@ -78,7 +78,6 @@ void FirmwarePage::draw(Canvas& c) {
case kVersion: return "Version";
case kStatus: return "Status";
case kAddress: return "Push to";
case kName: return "Name";
case kSdHeader: return listed ? (found.empty() ? "No .ota files in /updates" : "On the SD card:") : "Looking on the SD card...";
default: return " " + found[i - kFixed].substr(std::string(kUpdatesFolder).size() + 1);
}
@@ -88,7 +87,6 @@ void FirmwarePage::draw(Canvas& c) {
case kVersion: return versionString();
case kStatus: return update_.onProbation() ? "on probation" : "confirmed";
case kAddress: return ip.empty() ? "Wi-Fi not connected" : ip + ":" + std::to_string(UpdateService::kPort);
case kName: return update_.hostname() + ".local";
case kSdHeader: return "";
default: return "install >";
}
+1 -1
View File
@@ -28,7 +28,7 @@ class FirmwarePage {
void draw(Canvas& c);
private:
enum Row { kVersion, kStatus, kAddress, kName, kSdHeader, kFixed };
enum Row { kVersion, kStatus, kAddress, kSdHeader, kFixed };
std::vector<std::string> files();
+1 -1
View File
@@ -42,7 +42,7 @@ std::string lowered(std::string s) {
void IrcApp::onEnter() {
page_ = Page::Chat;
editing_ = false;
irc_.connect();
if (!irc_.stoppedByUser()) irc_.connect(); // stopped by hand: stay stopped, typing reconnects
view(current_);
}
+2 -1
View File
@@ -347,7 +347,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"
"irc start | 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"
"sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
#ifdef RORO_DEBUG
@@ -463,6 +463,7 @@ static void runCommand(String line) {
listingWanted = true;
}
if (line == "irc start") irc->connect();
if (line == "irc stop") irc->disconnect();
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>
String rest = line.substring(8);
int space = rest.indexOf(' ');
+24 -7
View File
@@ -37,12 +37,19 @@ void IrcService::start() {
}
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();
@@ -176,13 +183,10 @@ void IrcService::flushEffects() {
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");
if (quit) { // /quit: QUIT is already on its way if connected; stop in any state
stoppedByUser_ = true;
stopRequested_ = true;
quitSent_ = true;
}
}
@@ -192,6 +196,19 @@ void IrcService::loop() {
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_) {
+7 -1
View File
@@ -31,9 +31,12 @@ class IrcService : public Service {
const char* name() const override { return "irc"; }
void start() override;
// Starts the session (the IRC App does this when opened); /quit stops it.
// Starts the session (the IRC App does this when opened, unless you stopped it).
void connect();
// Stops it, from any state: says QUIT if connected, and stops retrying. /quit, `irc stop`.
void disconnect();
bool running() const { return wanted_; }
bool stoppedByUser() const { return stoppedByUser_; }
Status status() const { return status_; }
// Read or act on the session while holding its lock: withSession([](IrcSession& s) { ... }).
@@ -78,6 +81,9 @@ class IrcService : public Service {
ReconnectPolicy backoff_;
std::string partial_;
volatile bool wanted_ = false;
volatile bool stoppedByUser_ = false; // a manual stop: opening the App doesn't reconnect
volatile bool stopRequested_ = false;
bool quitSent_ = false; // /quit already queued its QUIT
volatile bool restart_ = false;
volatile Status status_ = Status::Stopped;
bool open_ = false;
-5
View File
@@ -1,13 +1,11 @@
#include "update_service.h"
#include <ESPmDNS.h>
#include <SD.h>
#include <WiFi.h>
#include <esp_ota_ops.h>
#include <memory>
#include "platform/identity.h"
#include "platform/ota_device.h"
#include "platform/system_info.h"
#include "probation.h"
@@ -66,7 +64,6 @@ class FileSource : public UpdateSource {
UpdateService::UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
EventBus& bus, const Settings& settings)
: store_(store), wifi_(wifi), saved_(saved), storage_(storage), bus_(bus), settings_(settings) {
hostname_ = "roro9stack-" + identity::defaultShortName();
}
void UpdateService::notify(const std::string& text, NotificationLevel level) {
@@ -196,11 +193,9 @@ void UpdateService::listen() {
bool connected = wifi_.state() == WifiController::State::Connected;
if (connected && !listening) {
server.begin();
if (MDNS.begin(hostname_.c_str())) MDNS.addService("roro9stack", "tcp", kPort);
listening = true;
} else if (!connected && listening) {
server.end();
MDNS.end();
listening = false;
}
// The main loop restarts into an installed update when it's safe. If it never does (stuck,
-2
View File
@@ -35,7 +35,6 @@ class UpdateService : public Service {
int percent() const { return percent_; }
std::string incomingVersion() const;
bool onProbation() const { return probation_; }
const std::string& hostname() const { return hostname_; }
// Installs an Update File from the SD card (runs on the storage task).
void installFromSd(const std::string& path);
@@ -58,7 +57,6 @@ class UpdateService : public Service {
EventBus& bus_;
const Settings& settings_;
TaskHandle_t task_ = nullptr;
std::string hostname_;
volatile Phase phase_ = Phase::Idle;
volatile int percent_ = 0;
std::string incoming_;