#include "system_app.h" #include #include #include #include #include #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(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(ch); } return 1; } std::vector 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(i)); requestRedraw(); } void SystemApp::onExit() { // nothing is kept while it's closed (Q120) std::vector().swap(before_); std::vector().swap(rows_); std::vector().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(View::Count), v = static_cast(view_); view_ = static_cast(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(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((static_cast(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 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(std::max(0, load_[core]))); } heapHistory_.push(static_cast(ESP.getFreeHeap() / 1024)); for (size_t i = 0; i < kUsers; i++) { net::Traffic t = net::traffic(static_cast(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(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((i - 1) * gw / kHistory), x1 = gx + 1 + static_cast(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(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(top, heapHistory_.at(i) + 8); auto yOf = [&](int kb) { return gy + gh - std::clamp(kb, 0, static_cast(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(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((i - 1) * (gw - 2) / kHistory), x1 = gx + 1 + static_cast(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(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 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(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