Public Access
S1 #11: the System App: tasks, memory, network and system, live
Five views, Tab between them: Overview (each core's load, memory, traffic, battery, two minutes of load), Tasks (share of a core over the last second, lowest free stack, flagged under 512 bytes; `s` sorts), Memory (free heap against the floors of Q86), Network (bytes per service, and what's moving now), System (what `info` prints, plus battery, card, radio, GNSS). It samples once a second and keeps history only while open. The arithmetic is host-tested, including the trap found on the device: a task's run-time counter only moves when it's switched out, so the task that samples (the main loop, alone on its core) gets what's left of its core. `tasks` now samples across a second of normal running instead of inside its own wait. The main loop uses 100 % of core 1 at rest (#40). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
#include "system_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
#include "cleanup_plan.h"
|
||||
#include "platform/system_info.h"
|
||||
#include "sd_fault.h"
|
||||
#include "services/gemini_service.h"
|
||||
#include "ui/canvas.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kUsers = static_cast<size_t>(net::User::Count);
|
||||
|
||||
// Collects what the console's `info` prints, so the System view can't drift from it (Q125).
|
||||
class Lines : public Print {
|
||||
public:
|
||||
size_t write(uint8_t ch) override {
|
||||
if (ch == '\n') {
|
||||
lines.push_back(current_);
|
||||
current_.clear();
|
||||
} else if (ch != '\r') {
|
||||
current_ += static_cast<char>(ch);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
std::vector<std::string> lines;
|
||||
|
||||
private:
|
||||
std::string current_;
|
||||
};
|
||||
|
||||
std::string duration(uint32_t seconds) {
|
||||
char s[24];
|
||||
if (seconds < 3600) std::snprintf(s, sizeof s, "%lu min %02lu s", (unsigned long)(seconds / 60), (unsigned long)(seconds % 60));
|
||||
else std::snprintf(s, sizeof s, "%lu h %02lu min", (unsigned long)(seconds / 3600), (unsigned long)(seconds / 60 % 60));
|
||||
return s;
|
||||
}
|
||||
|
||||
// A filled bar for a percentage; the warning colour from 90 %.
|
||||
void bar(Canvas& c, int x, int y, int w, int percent) {
|
||||
c.drawRect(x, y, w, 9, theme::kMuted);
|
||||
int filled = std::clamp(percent, 0, 100) * (w - 2) / 100;
|
||||
if (filled > 0) c.fillRect(x + 1, y + 1, filled, 7, percent >= 90 ? theme::kWarning : theme::kAccent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SystemApp::onEnter() {
|
||||
view_ = View::Overview;
|
||||
beforeTotal_ = system_info::sampleTasks(before_);
|
||||
sampledMs_ = millis();
|
||||
for (size_t i = 0; i < kUsers; i++) traffic_[i] = net::traffic(static_cast<net::User>(i));
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
void SystemApp::onExit() { // nothing is kept while it's closed (Q120)
|
||||
std::vector<TaskSample>().swap(before_);
|
||||
std::vector<TaskRow>().swap(rows_);
|
||||
std::vector<std::string>().swap(systemLines_);
|
||||
for (auto& h : loadHistory_) h.clear();
|
||||
heapHistory_.clear();
|
||||
load_[0] = load_[1] = -1;
|
||||
}
|
||||
|
||||
bool SystemApp::onKey(const KeyEvent& e) {
|
||||
if (e.key == Key::Tab) {
|
||||
int n = static_cast<int>(View::Count), v = static_cast<int>(view_);
|
||||
view_ = static_cast<View>(e.shift ? (v + n - 1) % n : (v + 1) % n);
|
||||
taskTop_ = systemTop_ = 0;
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
int step = e.key == Key::Up ? -1 : e.key == Key::Down ? 1 : 0;
|
||||
if (step && view_ == View::Tasks) {
|
||||
taskTop_ = std::clamp(taskTop_ + step, 0, std::max(0, static_cast<int>(rows_.size()) - kTaskRows));
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
if (step && view_ == View::System) {
|
||||
systemTop_ = std::clamp(systemTop_ + step, 0, std::max(0, wrappedLines_ - 8));
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
if (view_ == View::Tasks && e.key == Key::Char && (e.ch == 's' || e.ch == 'S')) { // Q122
|
||||
sort_ = static_cast<TaskSort>((static_cast<int>(sort_) + 1) % 3);
|
||||
sortTasks(rows_, sort_);
|
||||
taskTop_ = 0;
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
return false; // Back leaves the App
|
||||
}
|
||||
|
||||
void SystemApp::update(uint32_t nowMs) {
|
||||
if (nowMs - sampledMs_ >= 1000) sample(nowMs);
|
||||
}
|
||||
|
||||
// Once a second (Q119): the tasks' shares over that second, the cores' load, the heap, the traffic.
|
||||
void SystemApp::sample(uint32_t nowMs) {
|
||||
uint32_t elapsedMs = nowMs - sampledMs_;
|
||||
sampledMs_ = nowMs;
|
||||
|
||||
std::vector<TaskSample> now;
|
||||
uint32_t total = system_info::sampleTasks(now);
|
||||
rows_ = taskRows(before_, beforeTotal_, now, total);
|
||||
sortTasks(rows_, sort_);
|
||||
before_.swap(now);
|
||||
beforeTotal_ = total;
|
||||
for (int core = 0; core < 2; core++) {
|
||||
load_[core] = coreLoad(rows_, core);
|
||||
loadHistory_[core].push(static_cast<uint8_t>(std::max(0, load_[core])));
|
||||
}
|
||||
heapHistory_.push(static_cast<uint16_t>(ESP.getFreeHeap() / 1024));
|
||||
|
||||
for (size_t i = 0; i < kUsers; i++) {
|
||||
net::Traffic t = net::traffic(static_cast<net::User>(i));
|
||||
rateIn_[i] = net::bytesPerSecond(traffic_[i].in, t.in, elapsedMs);
|
||||
rateOut_[i] = net::bytesPerSecond(traffic_[i].out, t.out, elapsedMs);
|
||||
traffic_[i] = t;
|
||||
}
|
||||
|
||||
if (view_ == View::System) { // the slow things: only while they're on screen
|
||||
Lines out;
|
||||
system_info::printSystem(out);
|
||||
const BatteryEstimator& b = battery_.estimator();
|
||||
if (b.hasReading()) out.printf("battery: %d %%, %d.%02d V\n", b.percent(), b.millivolts() / 1000, b.millivolts() % 1000 / 10);
|
||||
StorageState card = storage_.state();
|
||||
if (card.present)
|
||||
out.printf("sd: %s of %s used, %u write faults\n", formatBytes(card.usedBytes).c_str(), formatBytes(card.totalBytes).c_str(),
|
||||
(unsigned)sdLastFault().count);
|
||||
else out.println("sd: no card");
|
||||
out.printf("radio: %s\n", !radio_.present() ? "none" : radio_.sweeping() ? "sweeping" : radio_.listening() ? "listening" : "asleep");
|
||||
const auto& g = gnss_.state();
|
||||
out.printf("gnss: %s, %d satellites used\n", !gnss_.on() ? "off" : gnss_.receiving(nowMs) ? "receiving" : "silent", g.satellitesUsed);
|
||||
system_info::printSlots(out, store_);
|
||||
systemLines_.swap(out.lines);
|
||||
}
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
void SystemApp::footer(Canvas& c, const char* keys) {
|
||||
const auto& area = theme::kContent;
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.setTextDatum(top_left);
|
||||
c.drawString(keys, 4, area.y + area.h - 9);
|
||||
}
|
||||
|
||||
void SystemApp::draw(Canvas& c) {
|
||||
c.setTextDatum(top_left);
|
||||
switch (view_) {
|
||||
case View::Overview: drawOverview(c); break;
|
||||
case View::Tasks: drawTasks(c); break;
|
||||
case View::Memory: drawMemory(c); break;
|
||||
case View::Network: drawNetwork(c); break;
|
||||
case View::System: drawSystem(c); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void SystemApp::drawOverview(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
char line[72];
|
||||
c.setFont(&fonts::body);
|
||||
int y = area.y + 2;
|
||||
for (int core = 0; core < 2; core++) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
std::snprintf(line, sizeof line, "Core %d", core);
|
||||
c.drawString(line, 4, y);
|
||||
bar(c, 52, y + 2, 130, load_[core]);
|
||||
c.setTextColor(theme::kText);
|
||||
if (load_[core] >= 0) std::snprintf(line, sizeof line, "%d%%", load_[core]);
|
||||
else std::snprintf(line, sizeof line, "...");
|
||||
c.drawString(line, 190, y);
|
||||
y += theme::kLineHeight;
|
||||
}
|
||||
auto row = [&](const char* label, const std::string& value) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(label, 4, y);
|
||||
c.setTextColor(theme::kText);
|
||||
c.drawString(value.c_str(), 62, y);
|
||||
y += theme::kLineHeight;
|
||||
};
|
||||
std::snprintf(line, sizeof line, "%u KB free, lowest %u", (unsigned)(ESP.getFreeHeap() / 1024), (unsigned)(ESP.getMinFreeHeap() / 1024));
|
||||
row("Memory", line);
|
||||
uint32_t in = 0, out = 0;
|
||||
for (size_t i = 0; i < kUsers; i++) in += rateIn_[i], out += rateOut_[i];
|
||||
if (wifi_.state() == WifiController::State::Connected)
|
||||
std::snprintf(line, sizeof line, "%s/s in, %s/s out", net::formatTraffic(in).c_str(), net::formatTraffic(out).c_str());
|
||||
else std::snprintf(line, sizeof line, "not connected");
|
||||
row("Network", line);
|
||||
const BatteryEstimator& b = battery_.estimator();
|
||||
if (b.hasReading()) std::snprintf(line, sizeof line, "%d%%, %d.%02d V", b.percent(), b.millivolts() / 1000, b.millivolts() % 1000 / 10);
|
||||
else std::snprintf(line, sizeof line, "...");
|
||||
row("Battery", line);
|
||||
std::snprintf(line, sizeof line, "%s, %.0f C", duration(millis() / 1000).c_str(), temperatureRead());
|
||||
row("Uptime", line);
|
||||
|
||||
// Both cores over the last two minutes: core 0 in the accent colour, core 1 in green.
|
||||
int gx = 4, gy = y + 3, gw = static_cast<int>(kHistory) * 2 - 8, gh = area.y + area.h - 12 - gy;
|
||||
if (gh > 8) {
|
||||
c.drawRect(gx, gy, gw + 2, gh + 2, theme::kMuted);
|
||||
for (int core = 0; core < 2; core++) {
|
||||
const auto& h = loadHistory_[core];
|
||||
for (size_t i = 1; i < h.size(); i++) {
|
||||
int x0 = gx + 1 + static_cast<int>((i - 1) * gw / kHistory), x1 = gx + 1 + static_cast<int>(i * gw / kHistory);
|
||||
c.drawLine(x0, gy + gh - h.at(i - 1) * gh / 100, x1, gy + gh - h.at(i) * gh / 100, core ? theme::kMessage : theme::kAccent);
|
||||
}
|
||||
}
|
||||
}
|
||||
footer(c, "Tab: tasks, memory, network, system");
|
||||
}
|
||||
|
||||
void SystemApp::drawTasks(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("task", 4, area.y + 2);
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString("core", 128, area.y + 2);
|
||||
c.drawString("cpu", 176, area.y + 2);
|
||||
c.drawString("stack left", area.w - 4, area.y + 2);
|
||||
c.setFont(&fonts::body);
|
||||
char cell[16];
|
||||
for (int i = 0; i < kTaskRows && taskTop_ + i < static_cast<int>(rows_.size()); i++) {
|
||||
const TaskRow& r = rows_[taskTop_ + i];
|
||||
int y = area.y + 12 + i * theme::kLineHeight;
|
||||
c.setTextDatum(top_left);
|
||||
c.setTextColor(r.idle ? theme::kMuted : theme::kText);
|
||||
c.drawString(r.name.c_str(), 4, y);
|
||||
c.setTextDatum(top_right);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(r.core < 0 ? "any" : std::to_string(r.core).c_str(), 128, y);
|
||||
c.setTextColor(r.idle ? theme::kMuted : theme::kText);
|
||||
std::snprintf(cell, sizeof cell, "%u.%u%%", r.permille / 10, r.permille % 10);
|
||||
c.drawString(cell, 176, y);
|
||||
c.setTextColor(r.lowStack ? theme::kWarning : theme::kMuted); // Q122: under 512 bytes
|
||||
c.drawString(std::to_string(r.stackFree).c_str(), area.w - 4, y);
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
std::string keys = std::string("s: by ") + (sort_ == TaskSort::Share ? "stack" : sort_ == TaskSort::Stack ? "name" : "cpu") +
|
||||
" " + std::to_string(rows_.size()) + " tasks Tab: memory";
|
||||
footer(c, keys.c_str());
|
||||
}
|
||||
|
||||
void SystemApp::drawMemory(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
char line[72];
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(theme::kText);
|
||||
std::snprintf(line, sizeof line, "%.1f KB free, lowest %.1f", ESP.getFreeHeap() / 1024.0, ESP.getMinFreeHeap() / 1024.0);
|
||||
c.drawString(line, 4, area.y + 2);
|
||||
c.setTextColor(theme::kMuted);
|
||||
std::snprintf(line, sizeof line, "largest free block %.1f KB", heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) / 1024.0);
|
||||
c.drawString(line, 4, area.y + 2 + theme::kLineHeight);
|
||||
|
||||
// Free heap over two minutes, with the floors of Q86 as lines.
|
||||
int gx = 26, gy = area.y + 32, gw = area.w - gx - 6, gh = area.y + area.h - 12 - gy;
|
||||
uint16_t top = 128; // KB at the top of the scale: more if the heap has been higher
|
||||
for (size_t i = 0; i < heapHistory_.size(); i++) top = std::max<uint16_t>(top, heapHistory_.at(i) + 8);
|
||||
auto yOf = [&](int kb) { return gy + gh - std::clamp(kb, 0, static_cast<int>(top)) * gh / top; };
|
||||
c.drawRect(gx, gy, gw, gh + 1, theme::kMuted);
|
||||
c.setFont(&fonts::small);
|
||||
struct {
|
||||
size_t bytes;
|
||||
uint16_t color;
|
||||
} floors[] = {{GeminiService::kStartFloor, theme::kMuted}, {GeminiService::kSteadyFloor, theme::kWarning}, {GeminiService::kTransientFloor, theme::kMessage}};
|
||||
for (auto& f : floors) {
|
||||
int kb = static_cast<int>(f.bytes / 1024), y = yOf(kb);
|
||||
for (int x = gx + 1; x < gx + gw - 1; x += 4) c.drawPixel(x, y, f.color);
|
||||
c.setTextColor(f.color);
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString(std::to_string(kb).c_str(), gx - 3, y - 3);
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
for (size_t i = 1; i < heapHistory_.size(); i++) {
|
||||
int x0 = gx + 1 + static_cast<int>((i - 1) * (gw - 2) / kHistory), x1 = gx + 1 + static_cast<int>(i * (gw - 2) / kHistory);
|
||||
c.drawLine(x0, yOf(heapHistory_.at(i - 1)), x1, yOf(heapHistory_.at(i)), theme::kAccent);
|
||||
}
|
||||
footer(c, "KB free, 2 min, with the floors Tab: network");
|
||||
}
|
||||
|
||||
void SystemApp::drawNetwork(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
char line[72];
|
||||
c.setFont(&fonts::body);
|
||||
WifiService::Connection n = wifi_.connection();
|
||||
c.setTextColor(n.connected ? theme::kText : theme::kWarning);
|
||||
if (n.connected) std::snprintf(line, sizeof line, "%s, %d dBm", wifi_.ssid().c_str(), wifi_.rssi());
|
||||
else std::snprintf(line, sizeof line, "Wi-Fi isn't connected");
|
||||
c.drawString(line, 4, area.y + 2);
|
||||
if (n.connected) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
std::snprintf(line, sizeof line, "%s/%d %s, gw %s", n.address.c_str(), n.prefix, n.fixed ? "fixed" : "DHCP",
|
||||
n.gateway.empty() ? "none" : n.gateway.c_str());
|
||||
c.drawString(line, 4, area.y + 2 + theme::kLineHeight);
|
||||
}
|
||||
// Per service (Q121): totals since boot, and the last second.
|
||||
int y = area.y + 32;
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("service", 4, y);
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString("in", 124, y);
|
||||
c.drawString("out", 168, y);
|
||||
c.drawString("now, /s", area.w - 4, y);
|
||||
c.setFont(&fonts::body);
|
||||
y += 10;
|
||||
for (size_t i = 0; i < kUsers; i++) {
|
||||
bool active = rateIn_[i] || rateOut_[i];
|
||||
c.setTextDatum(top_left);
|
||||
c.setTextColor(active ? theme::kMessage : theme::kText);
|
||||
c.drawString(net::userName(static_cast<net::User>(i)), 4, y);
|
||||
c.setTextDatum(top_right);
|
||||
c.setTextColor(theme::kText);
|
||||
c.drawString(net::formatTraffic(traffic_[i].in).c_str(), 124, y);
|
||||
c.drawString(net::formatTraffic(traffic_[i].out).c_str(), 168, y);
|
||||
c.setTextColor(active ? theme::kMessage : theme::kMuted);
|
||||
c.drawString(active ? net::formatTraffic(rateIn_[i] + rateOut_[i]).c_str() : "-", area.w - 4, y);
|
||||
y += theme::kLineHeight;
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
footer(c, "bytes since boot Tab: system");
|
||||
}
|
||||
|
||||
void SystemApp::drawSystem(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
if (systemLines_.empty()) {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("Reading...", 4, area.y + 4);
|
||||
return;
|
||||
}
|
||||
// The console's lines are longer than the screen: wrap them, continuation lines indented.
|
||||
std::vector<std::string> wrapped;
|
||||
auto measure = widgets::bodyMeasure(c);
|
||||
for (const std::string& line : systemLines_) {
|
||||
bool first = true;
|
||||
for (const std::string& part : wrapText(line, area.w - 14, measure)) {
|
||||
wrapped.push_back(first ? part : " " + part);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
wrappedLines_ = static_cast<int>(wrapped.size());
|
||||
systemTop_ = std::clamp(systemTop_, 0, std::max(0, wrappedLines_ - 8));
|
||||
widgets::textLines(c, wrapped, systemTop_, {area.x + 2, area.y + 2, area.w - 2, area.h - 12});
|
||||
footer(c, "Tab: overview");
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user