diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 7f44a93c..99ce21c5 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -342,3 +342,15 @@ STR_SCREENSHOT_BUTTON: "Take screenshot" STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" STR_SYSTEM_INFO: "System Information" +STR_FW_VERSION: "FW version" +STR_CHIP: "Chip" +STR_CPU: "CPU" +STR_MHZ: "MHz" +STR_FREE_RAM: "Free RAM" +STR_MIN_FREE: "Min free" +STR_MAX_BLOCK: "Max block" +STR_FLASH_USED: "Flash used" +STR_UPTIME: "Uptime" +STR_CHARGING: "Charging" +STR_GATHERING_DATA: "Gathering data..." +STR_READING: "Reading..." diff --git a/src/SystemStatus.h b/src/SystemStatus.h index f76ea392..d800c788 100644 --- a/src/SystemStatus.h +++ b/src/SystemStatus.h @@ -3,30 +3,53 @@ #include #include #include +#include + +#include "HalPowerManager.h" // Snapshot of device system status, shared between the web server and the // System Information activity so both surfaces show consistent data. struct SystemStatus { const char* version; + std::string chipVersion; + uint32_t cpuFreqMHz; std::string ip; std::string wifiMode; // "STA", "AP", or "Off" int rssi; // dBm; 0 when not in STA mode std::string macAddress; uint32_t freeHeapBytes; + uint32_t minFreeHeapBytes; + uint32_t maxAllocHeapBytes; + uint64_t flashBytes; + uint64_t flashAppUsedBytes; + uint64_t flashAppFreeBytes; + uint16_t batteryPercent; + bool charging; uint32_t uptimeSeconds; uint64_t sdTotalBytes; uint64_t sdUsedBytes; uint64_t sdFreeBytes; - static SystemStatus collect() { + static SystemStatus collectFast() { SystemStatus s; s.version = CROSSPOINT_VERSION; + s.chipVersion = ESP.getChipModel(); + s.chipVersion += " rev "; + s.chipVersion += std::to_string(ESP.getChipRevision()); + s.cpuFreqMHz = static_cast(getCpuFrequencyMhz()); s.freeHeapBytes = ESP.getFreeHeap(); + s.minFreeHeapBytes = ESP.getMinFreeHeap(); + s.maxAllocHeapBytes = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); + s.flashBytes = static_cast(ESP.getFlashChipSize()); + s.flashAppUsedBytes = static_cast(ESP.getSketchSize()); + s.flashAppFreeBytes = static_cast(ESP.getFreeSketchSpace()); + s.batteryPercent = powerManager.getBatteryPercentage(); + s.charging = digitalRead(UART0_RXD) == HIGH; s.uptimeSeconds = millis() / 1000; s.macAddress = WiFi.macAddress().c_str(); - s.sdTotalBytes = Storage.sdTotalBytes(); - s.sdUsedBytes = Storage.sdUsedBytes(); - s.sdFreeBytes = Storage.sdFreeBytes(); + s.sdTotalBytes = 0; + s.sdUsedBytes = 0; + s.sdFreeBytes = 0; const wifi_mode_t mode = WiFi.getMode(); const bool isAP = (mode == WIFI_MODE_AP) || (mode == WIFI_MODE_APSTA); @@ -47,4 +70,16 @@ struct SystemStatus { return s; } + + static void fillSdStatus(SystemStatus& s) { + s.sdTotalBytes = Storage.sdTotalBytes(); + s.sdUsedBytes = Storage.sdUsedBytes(); + s.sdFreeBytes = Storage.sdFreeBytes(); + } + + static SystemStatus collect() { + SystemStatus s = collectFast(); + fillSdStatus(s); + return s; + } }; diff --git a/src/activities/settings/SystemInformationActivity.cpp b/src/activities/settings/SystemInformationActivity.cpp index 68d167f0..7a79e538 100644 --- a/src/activities/settings/SystemInformationActivity.cpp +++ b/src/activities/settings/SystemInformationActivity.cpp @@ -25,6 +25,7 @@ static std::string formatBytes(uint64_t bytes) { void SystemInformationActivity::onEnter() { Activity::onEnter(); status_.reset(); + sdStatusReady_ = false; requestUpdate(); } @@ -35,10 +36,17 @@ void SystemInformationActivity::loop() { finish(); return; } - // Collect status (includes the potentially slow SD FAT walk) outside of render() - // so the screen is shown with a "Reading..." placeholder first. + // Collect fast fields first so this page appears immediately. if (!status_.has_value()) { - status_ = SystemStatus::collect(); + status_ = SystemStatus::collectFast(); + requestUpdate(); + return; + } + + // SD stats can be slower to compute on large cards. + if (!sdStatusReady_) { + SystemStatus::fillSdStatus(*status_); + sdStatusReady_ = true; requestUpdate(); } } @@ -46,7 +54,6 @@ void SystemInformationActivity::loop() { void SystemInformationActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); @@ -67,40 +74,46 @@ void SystemInformationActivity::render(RenderLock&&) { if (!status_.has_value()) { // Stats not yet collected — show a placeholder so the screen updates immediately - drawRow(0, "Version", CROSSPOINT_VERSION); - drawRow(3, "SD card", "Reading..."); + drawRow(0, tr(STR_FW_VERSION), CROSSPOINT_VERSION); + drawRow(2, "", tr(STR_GATHERING_DATA)); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; } const auto& status = *status_; - // Device - drawRow(0, "Version", status.version); - drawRow(1, "Free heap", std::to_string(status.freeHeapBytes / 1024) + " KB"); + drawRow(0, tr(STR_FW_VERSION), status.version); + drawRow(1, tr(STR_CHIP), status.chipVersion); + drawRow(2, tr(STR_CPU), std::to_string(status.cpuFreqMHz) + " " + tr(STR_MHZ)); + drawRow(3, tr(STR_FREE_RAM), formatBytes(status.freeHeapBytes)); + drawRow(4, tr(STR_MIN_FREE), formatBytes(status.minFreeHeapBytes)); + drawRow(5, tr(STR_MAX_BLOCK), formatBytes(status.maxAllocHeapBytes)); + drawRow( + 6, tr(STR_FLASH_USED), + formatBytes(status.flashAppUsedBytes) + " / " + formatBytes(status.flashAppUsedBytes + status.flashAppFreeBytes)); + std::string batteryLabel = std::to_string(status.batteryPercent) + "%"; + if (status.charging) { + batteryLabel += " ("; + batteryLabel += tr(STR_CHARGING); + batteryLabel += ")"; + } + drawRow(7, tr(STR_BATTERY), batteryLabel); const uint32_t h = status.uptimeSeconds / 3600; const uint32_t m = (status.uptimeSeconds % 3600) / 60; const uint32_t s = status.uptimeSeconds % 60; char uptimeBuf[16]; snprintf(uptimeBuf, sizeof(uptimeBuf), "%uh %02um %02us", h, m, s); - drawRow(2, "Uptime", uptimeBuf); + drawRow(8, tr(STR_UPTIME), uptimeBuf); - // SD card - const std::string sdUsed = formatBytes(status.sdUsedBytes) + " / " + formatBytes(status.sdTotalBytes); - drawRow(3, "SD card", status.sdTotalBytes > 0 ? sdUsed : "N/A"); - - // WiFi (shown as-is; "Off" when not connected) - std::string wifiLabel = status.wifiMode; - if (status.rssi != 0) { - wifiLabel += " (" + std::to_string(status.rssi) + " dBm)"; - } - drawRow(4, "WiFi", wifiLabel); - if (status.wifiMode != "Off") { - drawRow(5, "IP address", status.ip); - drawRow(6, "MAC address", status.macAddress); + if (!sdStatusReady_) { + drawRow(9, tr(STR_SD_CARD), tr(STR_READING)); + } else if (status.sdTotalBytes > 0) { + drawRow(9, tr(STR_SD_CARD), formatBytes(status.sdUsedBytes) + " / " + formatBytes(status.sdTotalBytes)); } else { - drawRow(5, "MAC address", status.macAddress); + drawRow(9, tr(STR_SD_CARD), tr(STR_NOT_SET)); } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); diff --git a/src/activities/settings/SystemInformationActivity.h b/src/activities/settings/SystemInformationActivity.h index d135dab5..cb917f48 100644 --- a/src/activities/settings/SystemInformationActivity.h +++ b/src/activities/settings/SystemInformationActivity.h @@ -17,4 +17,5 @@ class SystemInformationActivity final : public Activity { private: std::optional status_; + bool sdStatusReady_ = false; }; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index d0cae11e..850849df 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -317,17 +317,34 @@ void CrossPointWebServer::handleNotFound() const { } void CrossPointWebServer::handleStatus() const { - const auto status = SystemStatus::collect(); + const bool fastOnly = server->hasArg("phase") && server->arg("phase") == "fast"; + + SystemStatus status = SystemStatus::collectFast(); + if (!fastOnly) { + SystemStatus::fillSdStatus(status); + } JsonDocument doc; doc["version"] = status.version; + doc["chipVersion"] = status.chipVersion; + doc["cpuMHz"] = status.cpuFreqMHz; doc["ip"] = status.ip; doc["mode"] = status.wifiMode; doc["rssi"] = status.rssi; + doc["macAddress"] = status.macAddress; doc["freeHeap"] = status.freeHeapBytes; + doc["minFreeHeap"] = status.minFreeHeapBytes; + doc["maxAllocHeap"] = status.maxAllocHeapBytes; + doc["flashTotal"] = status.flashBytes; + doc["flashAppUsed"] = status.flashAppUsedBytes; + doc["flashAppFree"] = status.flashAppFreeBytes; + doc["batteryPercent"] = status.batteryPercent; + doc["charging"] = status.charging; doc["uptime"] = status.uptimeSeconds; + doc["sdReady"] = !fastOnly; doc["sdTotal"] = status.sdTotalBytes; doc["sdUsed"] = status.sdUsedBytes; + doc["sdFree"] = status.sdFreeBytes; String json; serializeJson(doc, json); diff --git a/src/network/html/HomePage.html b/src/network/html/HomePage.html index 27e7175c..ef52ed49 100644 --- a/src/network/html/HomePage.html +++ b/src/network/html/HomePage.html @@ -128,18 +128,50 @@ Version Loading... +
+ Chip + Loading... +
+
+ CPU + Loading... +
WiFi Status - Connected + Loading...
IP Address Loading...
+
+ MAC Address + Loading... +
Free Memory Loading...
+
+ Min Free + Loading... +
+
+ Max Block + Loading... +
+
+ Flash Used + Loading... +
+
+ Battery + Loading... +
+
+ Uptime + Loading... +
SD Card Loading... @@ -160,21 +192,64 @@ return parseFloat(val.toFixed(2)).toLocaleString() + ' ' + units[i]; } - async function fetchStatus() { - try { - const response = await fetch('/api/status'); - if (!response.ok) { - throw new Error('Failed to fetch status: ' + response.status + ' ' + response.statusText); - } - const data = await response.json(); - document.getElementById('version').textContent = data.version || 'N/A'; - document.getElementById('ip-address').textContent = data.ip || 'N/A'; - document.getElementById('free-heap').textContent = data.freeHeap - ? data.freeHeap.toLocaleString() + ' bytes' - : 'N/A'; + function formatUptime(seconds) { + if (seconds == null) return 'N/A'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return h + 'h ' + String(m).padStart(2, '0') + 'm ' + String(s).padStart(2, '0') + 's'; + } + + function applyStatus(data, includeSd) { + document.getElementById('version').textContent = data.version || 'N/A'; + document.getElementById('chip-version').textContent = data.chipVersion || 'N/A'; + document.getElementById('cpu-mhz').textContent = data.cpuMHz + ? data.cpuMHz.toLocaleString() + ' MHz' + : 'N/A'; + const wifiStatus = document.getElementById('wifi-status'); + wifiStatus.textContent = data.mode || 'N/A'; + document.getElementById('ip-address').textContent = data.ip || 'N/A'; + document.getElementById('mac-address').textContent = data.macAddress || 'N/A'; + document.getElementById('free-heap').textContent = data.freeHeap + ? formatBytes(data.freeHeap) + : 'N/A'; + document.getElementById('min-free-heap').textContent = data.minFreeHeap + ? formatBytes(data.minFreeHeap) + : 'N/A'; + document.getElementById('max-alloc-heap').textContent = data.maxAllocHeap + ? formatBytes(data.maxAllocHeap) + : 'N/A'; + document.getElementById('flash-used').textContent = data.flashAppUsed + ? (formatBytes(data.flashAppUsed) + ' / ' + formatBytes((data.flashAppUsed || 0) + (data.flashAppFree || 0))) + : 'N/A'; + document.getElementById('battery').textContent = (data.batteryPercent != null) + ? (data.batteryPercent + '%' + (data.charging ? ' (Charging)' : '')) + : 'N/A'; + document.getElementById('uptime').textContent = formatUptime(data.uptime); + + if (includeSd) { document.getElementById('sd-space').textContent = data.sdTotal ? formatBytes(data.sdUsed) + ' / ' + formatBytes(data.sdTotal) : 'N/A'; + } + } + + async function fetchStatus() { + try { + const fastResponse = await fetch('/api/status?phase=fast'); + if (!fastResponse.ok) { + throw new Error('Failed to fetch fast status: ' + fastResponse.status + ' ' + fastResponse.statusText); + } + const fastData = await fastResponse.json(); + applyStatus(fastData, false); + document.getElementById('sd-space').textContent = 'Reading...'; + + const response = await fetch('/api/status'); + if (!response.ok) { + throw new Error('Failed to fetch full status: ' + response.status + ' ' + response.statusText); + } + const data = await response.json(); + applyStatus(data, true); } catch (error) { console.error('Error fetching status:', error); }