Merge branch 'feat-sdcard-info' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -348,3 +348,4 @@ STR_AUTHOR: "Author"
|
|||||||
STR_SERIES: "Series"
|
STR_SERIES: "Series"
|
||||||
STR_FILE_SIZE: "Size"
|
STR_FILE_SIZE: "Size"
|
||||||
STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
|
STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
|
||||||
|
STR_SYSTEM_INFO: "System Information"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <FS.h> // need to be included before SdFat.h for compatibility with FS.h's File class
|
#include <FS.h> // need to be included before SdFat.h for compatibility with FS.h's File class
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
@@ -11,6 +12,9 @@
|
|||||||
|
|
||||||
HalStorage HalStorage::instance;
|
HalStorage HalStorage::instance;
|
||||||
|
|
||||||
|
// Local SdFat instance for volume stats only
|
||||||
|
static SdFat halStorageSdFat;
|
||||||
|
|
||||||
HalStorage::HalStorage() {
|
HalStorage::HalStorage() {
|
||||||
storageMutex = xSemaphoreCreateMutex();
|
storageMutex = xSemaphoreCreateMutex();
|
||||||
assert(storageMutex != nullptr);
|
assert(storageMutex != nullptr);
|
||||||
@@ -54,6 +58,36 @@ bool HalStorage::writeFile(const char* path, const String& content) {
|
|||||||
|
|
||||||
bool HalStorage::ensureDirectoryExists(const char* path) { HAL_STORAGE_WRAPPED_CALL(ensureDirectoryExists, path); }
|
bool HalStorage::ensureDirectoryExists(const char* path) { HAL_STORAGE_WRAPPED_CALL(ensureDirectoryExists, path); }
|
||||||
|
|
||||||
|
uint64_t HalStorage::sdTotalBytes() const {
|
||||||
|
StorageLock lock;
|
||||||
|
// Try to initialize if not already
|
||||||
|
if (!halStorageSdFat.begin()) return 0;
|
||||||
|
auto* vol = halStorageSdFat.vol();
|
||||||
|
if (!vol) return 0;
|
||||||
|
return (uint64_t)vol->clusterCount() * vol->bytesPerCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t HalStorage::sdUsedBytes() const {
|
||||||
|
StorageLock lock;
|
||||||
|
if (!halStorageSdFat.begin()) return 0;
|
||||||
|
auto* vol = halStorageSdFat.vol();
|
||||||
|
if (!vol) return 0;
|
||||||
|
const int32_t freeClusters = vol->freeClusterCount();
|
||||||
|
if (freeClusters < 0) return 0; // error reading FAT
|
||||||
|
const uint64_t clusterCount = vol->clusterCount();
|
||||||
|
uint64_t cappedFreeClusters = freeClusters < 0 ? 0 : (uint64_t)freeClusters;
|
||||||
|
if (cappedFreeClusters > clusterCount) cappedFreeClusters = clusterCount;
|
||||||
|
const uint64_t bytesPerCluster = vol->bytesPerCluster();
|
||||||
|
return (clusterCount - cappedFreeClusters) * bytesPerCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t HalStorage::sdFreeBytes() const {
|
||||||
|
uint64_t total = sdTotalBytes();
|
||||||
|
uint64_t used = sdUsedBytes();
|
||||||
|
if (total <= used) return 0;
|
||||||
|
return total - used;
|
||||||
|
}
|
||||||
|
|
||||||
class HalFile::Impl {
|
class HalFile::Impl {
|
||||||
public:
|
public:
|
||||||
Impl(FsFile&& fsFile) : file(std::move(fsFile)) {}
|
Impl(FsFile&& fsFile) : file(std::move(fsFile)) {}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ class HalStorage {
|
|||||||
bool openFileForWrite(const char* moduleName, const String& path, HalFile& file);
|
bool openFileForWrite(const char* moduleName, const String& path, HalFile& file);
|
||||||
bool removeDir(const char* path);
|
bool removeDir(const char* path);
|
||||||
|
|
||||||
|
uint64_t sdTotalBytes() const;
|
||||||
|
uint64_t sdUsedBytes() const;
|
||||||
|
uint64_t sdFreeBytes() const;
|
||||||
|
|
||||||
static HalStorage& getInstance() { return instance; }
|
static HalStorage& getInstance() { return instance; }
|
||||||
|
|
||||||
class StorageLock; // private class, used internally
|
class StorageLock; // private class, used internally
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <WiFi.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 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 uptimeSeconds;
|
||||||
|
uint64_t sdTotalBytes;
|
||||||
|
uint64_t sdUsedBytes;
|
||||||
|
uint64_t sdFreeBytes;
|
||||||
|
|
||||||
|
static SystemStatus collect() {
|
||||||
|
SystemStatus s;
|
||||||
|
s.version = CROSSPOINT_VERSION;
|
||||||
|
s.freeHeapBytes = ESP.getFreeHeap();
|
||||||
|
s.uptimeSeconds = millis() / 1000;
|
||||||
|
s.macAddress = WiFi.macAddress().c_str();
|
||||||
|
s.sdTotalBytes = Storage.sdTotalBytes();
|
||||||
|
s.sdUsedBytes = Storage.sdUsedBytes();
|
||||||
|
s.sdFreeBytes = Storage.sdFreeBytes();
|
||||||
|
|
||||||
|
const wifi_mode_t mode = WiFi.getMode();
|
||||||
|
const bool isAP = (mode == WIFI_MODE_AP) || (mode == WIFI_MODE_APSTA);
|
||||||
|
|
||||||
|
if (isAP) {
|
||||||
|
s.wifiMode = "AP";
|
||||||
|
s.ip = WiFi.softAPIP().toString().c_str();
|
||||||
|
s.rssi = 0;
|
||||||
|
} else if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
s.wifiMode = "STA";
|
||||||
|
s.ip = WiFi.localIP().toString().c_str();
|
||||||
|
s.rssi = WiFi.RSSI();
|
||||||
|
} else {
|
||||||
|
s.wifiMode = "Off";
|
||||||
|
s.ip = "-";
|
||||||
|
s.rssi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ButtonRemapActivity.h"
|
#include "ButtonRemapActivity.h"
|
||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
|
#include "SystemInformationActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "LanguageSelectActivity.h"
|
#include "LanguageSelectActivity.h"
|
||||||
@@ -52,6 +53,7 @@ void SettingsActivity::onEnter() {
|
|||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
|
||||||
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo));
|
||||||
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
|
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
|
||||||
|
|
||||||
// Reset selection to first category
|
// Reset selection to first category
|
||||||
@@ -192,6 +194,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::Language:
|
case SettingAction::Language:
|
||||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::SystemInfo:
|
||||||
|
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
break;
|
||||||
case SettingAction::None:
|
case SettingAction::None:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum class SettingAction {
|
|||||||
ClearCache,
|
ClearCache,
|
||||||
CheckForUpdates,
|
CheckForUpdates,
|
||||||
Language,
|
Language,
|
||||||
|
SystemInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#include "SystemInformationActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "SystemStatus.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
static std::string formatBytes(uint64_t bytes) {
|
||||||
|
char buf[16];
|
||||||
|
if (bytes >= 1024ULL * 1024 * 1024) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||||
|
} else if (bytes >= 1024ULL * 1024) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.1f MB", bytes / (1024.0 * 1024.0));
|
||||||
|
} else if (bytes >= 1024ULL) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.1f KB", bytes / 1024.0);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "%llu B", static_cast<unsigned long long>(bytes));
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemInformationActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemInformationActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void SystemInformationActivity::loop() {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemInformationActivity::render(RenderLock&&) {
|
||||||
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYSTEM_INFO),
|
||||||
|
CROSSPOINT_VERSION);
|
||||||
|
|
||||||
|
const auto status = SystemStatus::collect();
|
||||||
|
|
||||||
|
// Layout: label on the left, value right of the midpoint
|
||||||
|
const int leftX = metrics.verticalSpacing * 3;
|
||||||
|
const int valueX = pageWidth / 2;
|
||||||
|
const int lineH = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
const int startY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing * 3;
|
||||||
|
|
||||||
|
auto drawRow = [&](int row, const char* label, const std::string& value) {
|
||||||
|
const int y = startY + row * (lineH + metrics.verticalSpacing);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, leftX, y, label, true, EpdFontFamily::BOLD);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, valueX, y, value.c_str());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Device
|
||||||
|
drawRow(0, "Version", status.version);
|
||||||
|
drawRow(1, "Free heap", std::to_string(status.freeHeapBytes / 1024) + " KB");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
drawRow(5, "MAC address", status.macAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
class SystemInformationActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit SystemInformationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("SystemInformation", renderer, mappedInput) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "SettingsList.h"
|
#include "SettingsList.h"
|
||||||
|
#include "SystemStatus.h"
|
||||||
#include "WebDAVHandler.h"
|
#include "WebDAVHandler.h"
|
||||||
#include "html/FilesPageHtml.generated.h"
|
#include "html/FilesPageHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
@@ -316,16 +317,17 @@ void CrossPointWebServer::handleNotFound() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleStatus() const {
|
void CrossPointWebServer::handleStatus() const {
|
||||||
// Get correct IP based on AP vs STA mode
|
const auto status = SystemStatus::collect();
|
||||||
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["version"] = CROSSPOINT_VERSION;
|
doc["version"] = status.version;
|
||||||
doc["ip"] = ipAddr;
|
doc["ip"] = status.ip;
|
||||||
doc["mode"] = apMode ? "AP" : "STA";
|
doc["mode"] = status.wifiMode;
|
||||||
doc["rssi"] = apMode ? 0 : WiFi.RSSI();
|
doc["rssi"] = status.rssi;
|
||||||
doc["freeHeap"] = ESP.getFreeHeap();
|
doc["freeHeap"] = status.freeHeapBytes;
|
||||||
doc["uptime"] = millis() / 1000;
|
doc["uptime"] = status.uptimeSeconds;
|
||||||
|
doc["sdTotal"] = status.sdTotalBytes;
|
||||||
|
doc["sdUsed"] = status.sdUsedBytes;
|
||||||
|
|
||||||
String json;
|
String json;
|
||||||
serializeJson(doc, json);
|
serializeJson(doc, json);
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ class CrossPointWebServer {
|
|||||||
|
|
||||||
// File scanning
|
// File scanning
|
||||||
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
|
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
|
||||||
String formatFileSize(size_t bytes) const;
|
|
||||||
bool isEpubFile(const String& filename) const;
|
bool isEpubFile(const String& filename) const;
|
||||||
|
|
||||||
// Request handlers
|
// Request handlers
|
||||||
|
|||||||
@@ -124,6 +124,10 @@
|
|||||||
<span class="label">Free Memory</span>
|
<span class="label">Free Memory</span>
|
||||||
<span class="value" id="free-heap"></span>
|
<span class="value" id="free-heap"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">SD Card</span>
|
||||||
|
<span class="value" id="sd-space"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -132,6 +136,14 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes == null) return 'N/A';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let val = bytes, i = 0;
|
||||||
|
while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
|
||||||
|
return parseFloat(val.toFixed(2)).toLocaleString() + ' ' + units[i];
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchStatus() {
|
async function fetchStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/status');
|
const response = await fetch('/api/status');
|
||||||
@@ -144,6 +156,9 @@
|
|||||||
document.getElementById('free-heap').textContent = data.freeHeap
|
document.getElementById('free-heap').textContent = data.freeHeap
|
||||||
? data.freeHeap.toLocaleString() + ' bytes'
|
? data.freeHeap.toLocaleString() + ' bytes'
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
|
document.getElementById('sd-space').textContent = data.sdTotal
|
||||||
|
? formatBytes(data.sdUsed) + ' / ' + formatBytes(data.sdTotal)
|
||||||
|
: 'N/A';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching status:', error);
|
console.error('Error fetching status:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user