#include "SystemInformationActivity.h" #include #include #include #include "MappedInputManager.h" #include "SystemStatus.h" #include "components/UITheme.h" #include "fontIds.h" static const char* pickUnit(uint64_t maxBytes, double& outDivisor) { if (maxBytes >= 1024ULL * 1024 * 1024) { outDivisor = 1024.0 * 1024.0 * 1024.0; return "GB"; } if (maxBytes >= 1024ULL * 1024) { outDivisor = 1024.0 * 1024.0; return "MB"; } if (maxBytes >= 1024ULL) { outDivisor = 1024.0; return "KB"; } outDivisor = 1.0; return "B"; } static std::string formatBytes(uint64_t bytes) { double div; const char* unit = pickUnit(bytes, div); char buf[16]; if (div == 1.0) { snprintf(buf, sizeof(buf), "%llu %s", static_cast(bytes), unit); } else { snprintf(buf, sizeof(buf), "%.1f %s", bytes / div, unit); } return buf; } // Format three byte values on a single line sharing one trailing unit. The // unit is chosen from the largest of the three so all values fit sensibly. static std::string formatBytesTriple(uint64_t a, uint64_t b, uint64_t c) { double div; const char* unit = pickUnit(std::max({a, b, c}), div); char buf[48]; if (div == 1.0) { snprintf(buf, sizeof(buf), "%llu / %llu / %llu %s", static_cast(a), static_cast(b), static_cast(c), unit); } else { snprintf(buf, sizeof(buf), "%.1f / %.1f / %.1f %s", a / div, b / div, c / div, unit); } return buf; } void SystemInformationActivity::onEnter() { Activity::onEnter(); status_.reset(); sdStatusReady_ = false; sdLoadRequested_ = false; requestUpdate(); } void SystemInformationActivity::onExit() { Activity::onExit(); } void SystemInformationActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finish(); return; } // Collect fast fields first so this page appears immediately. if (!status_.has_value()) { status_ = SystemStatus::collectFast(); requestUpdate(); return; } // SD stats can be slower to compute on large cards. Load them only when the // user explicitly confirms. if (!sdStatusReady_) { if (sdLoadRequested_) { SystemStatus::fillSdStatus(*status_); sdStatusReady_ = true; sdLoadRequested_ = false; requestUpdate(); } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { sdLoadRequested_ = true; requestUpdate(); } } } void SystemInformationActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect contentRect = UITheme::getContentRect(renderer, /*hasBottomHints=*/true, /*hasSideHints=*/false); renderer.clearScreen(); GUI.drawHeader(renderer, Rect{contentRect.x, contentRect.y + metrics.topPadding, contentRect.width, metrics.headerHeight}, tr(STR_SYSTEM_INFO), CROSSPOINT_VERSION); // Two-column layout with interleaved section headers (drawn via the theme's // subheader so the full-width underline is consistent with the rest of the // UI). Data rows use a bold label on the left and the value at the column // midpoint; row step is tightened so all sections fit on one screen. const int leftX = contentRect.x + metrics.verticalSpacing * 3; const int valueX = contentRect.x + contentRect.width / 2; const int lineH = renderer.getLineHeight(UI_10_FONT_ID); const int rowStep = lineH + 2; const int subHeaderHeight = lineH + 6; int y = contentRect.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; auto drawSection = [&](const char* title) { GUI.drawSubHeader(renderer, Rect{contentRect.x, y, contentRect.width, subHeaderHeight}, title); y += subHeaderHeight + 2; }; auto drawRow = [&](const char* label, const std::string& value) { renderer.drawText(UI_10_FONT_ID, leftX, y, label, true, EpdFontFamily::BOLD); renderer.drawText(UI_10_FONT_ID, valueX, y, value.c_str()); y += rowStep; }; if (!status_.has_value()) { // Stats not yet collected — show a placeholder so the screen updates immediately drawRow(tr(STR_FW_VERSION), CROSSPOINT_VERSION); y += rowStep; drawRow("", 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_; drawSection(tr(STR_SEC_VERSION)); drawRow(tr(STR_FW_VERSION), status.version); drawRow(tr(STR_DEVICE), std::string(status.deviceType) + " (" + std::to_string(status.displayWidth) + " x " + std::to_string(status.displayHeight) + " px)"); drawSection(tr(STR_SEC_CHIP)); drawRow(tr(STR_CHIP), status.chipVersion); drawRow(tr(STR_CPU), std::to_string(status.cpuFreqMHz) + " " + tr(STR_MHZ)); drawSection(tr(STR_SEC_MEMORY)); drawRow(tr(STR_MEM_COMBINED), formatBytesTriple(status.freeHeapBytes, status.minFreeHeapBytes, status.maxAllocHeapBytes)); drawSection(tr(STR_SEC_FLASH)); drawRow(tr(STR_APP_PARTITION), formatBytes(status.flashAppPartitionSize)); drawRow(tr(STR_FLASH_TOTAL), formatBytes(status.flashBytes)); drawSection(tr(STR_SEC_RUNTIME)); 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(tr(STR_UPTIME), uptimeBuf); std::string batteryLabel = std::to_string(status.batteryPercent) + "%"; if (status.charging) { batteryLabel += " ("; batteryLabel += tr(STR_CHARGING); batteryLabel += ")"; } drawRow(tr(STR_BATTERY), batteryLabel); drawSection(tr(STR_SEC_STORAGE)); if (!sdStatusReady_) { const char* sdMessage = sdLoadRequested_ ? tr(STR_READING) : tr(STR_SD_UPDATE_PROMPT); drawRow(tr(STR_SD_CARD), sdMessage); } else if (status.sdTotalBytes > 0) { drawRow(tr(STR_SD_CARD), formatBytes(status.sdUsedBytes) + " / " + formatBytes(status.sdTotalBytes)); } else { drawRow(tr(STR_SD_CARD), tr(STR_NOT_SET)); } const auto labels = mappedInput.mapLabels(tr(STR_BACK), sdStatusReady_ ? "" : tr(STR_UPDATE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); }