diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index b453bae4..23253403 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network" STR_OPEN_URL_HINT: "Open this URL in your browser" STR_OR_HTTP_PREFIX: "or http://" STR_SCAN_QR_HINT: "or scan QR code with your phone:" +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "No signal" +STR_SIGNAL_QUALITY_POOR: "Poor" +STR_SIGNAL_QUALITY_WEAK: "Weak" +STR_SIGNAL_QUALITY_GOOD: "Good" +STR_SIGNAL_QUALITY_EXCELLENT: "Excellent" STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WEB_URL: "Calibre Web URL" STR_NETWORK_LEGEND: "* = Encrypted | + = Saved" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 609bc425..0bf0d8b2 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce WiFi" STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou scannez le QR code :" +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Pas de signal" +STR_SIGNAL_QUALITY_POOR: "Faible" +STR_SIGNAL_QUALITY_WEAK: "Très faible" +STR_SIGNAL_QUALITY_GOOD: "Bon" +STR_SIGNAL_QUALITY_EXCELLENT: "Excellent" STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil" STR_CALIBRE_WEB_URL: "URL Web Calibre" STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 1de48bd6..8f69dd64 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden" STR_OPEN_URL_HINT: "Diese URL im Browser öffnen" STR_OR_HTTP_PREFIX: "oder http://" STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:" +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Kein Signal" +STR_SIGNAL_QUALITY_POOR: "Schlecht" +STR_SIGNAL_QUALITY_WEAK: "Schwach" +STR_SIGNAL_QUALITY_GOOD: "Gut" +STR_SIGNAL_QUALITY_EXCELLENT: "Ausgezeichnet" STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WEB_URL: "Calibre-Web-URL" STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 8c805347..10c58894 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connettere il dispositivo a questa rete WiFi" STR_OPEN_URL_HINT: "Aprire questo URL nel browser" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "oppure scansionare il codice QR col telefono:" +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Nessun segnale" +STR_SIGNAL_QUALITY_POOR: "Scarso" +STR_SIGNAL_QUALITY_WEAK: "Debole" +STR_SIGNAL_QUALITY_GOOD: "Buono" +STR_SIGNAL_QUALITY_EXCELLENT: "Eccellente" STR_CALIBRE_WIRELESS: "Calibre wireless" STR_CALIBRE_WEB_URL: "URL Web Calibre" STR_NETWORK_LEGEND: "* = Protetta | + = Salvata" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 76098cb4..e4054460 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Подключите устройство к этой с STR_OPEN_URL_HINT: "Откройте этот адрес в браузере" STR_OR_HTTP_PREFIX: "или http://" STR_SCAN_QR_HINT: "или отсканируйте QR-код:" +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Нет сигнала" +STR_SIGNAL_QUALITY_POOR: "Плохо" +STR_SIGNAL_QUALITY_WEAK: "Слабо" +STR_SIGNAL_QUALITY_GOOD: "Хорошо" +STR_SIGNAL_QUALITY_EXCELLENT: "Отлично" STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi" STR_CALIBRE_WEB_URL: "Web-адрес Calibre" STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена" diff --git a/src/SystemStatus.h b/src/SystemStatus.h index d800c788..238ee460 100644 --- a/src/SystemStatus.h +++ b/src/SystemStatus.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -72,9 +73,11 @@ struct SystemStatus { } static void fillSdStatus(SystemStatus& s) { + uint32_t t0 = millis(); s.sdTotalBytes = Storage.sdTotalBytes(); s.sdUsedBytes = Storage.sdUsedBytes(); s.sdFreeBytes = Storage.sdFreeBytes(); + LOG_DBG("SYSINFO", "Filled SD status in %u ms", millis() - t0); } static SystemStatus collect() { diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 4f3b4f34..c76270d1 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -244,6 +244,10 @@ void CrossPointWebServerActivity::startWebServer() { if (webServer->isRunning()) { state = WebServerActivityState::SERVER_RUNNING; + if (!isApMode) { + currentRssi = WiFi.RSSI(); + lastRssiUpdateTime = millis(); + } LOG_DBG("WEBACT", "Web server started successfully"); // Force an immediate render since we're transitioning from a subactivity @@ -293,6 +297,12 @@ void CrossPointWebServerActivity::loop() { LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", rssi); } } + + if (millis() - lastRssiUpdateTime > 5000) { // Refresh signal indicator every 5 seconds + lastRssiUpdateTime = millis(); + currentRssi = WiFi.RSSI(); + requestUpdate(); + } } // Handle web server requests - maximize throughput with watchdog safety @@ -366,6 +376,69 @@ void CrossPointWebServerActivity::render(RenderLock&&) { } } +namespace { + +StrId getRssiQualityStrId(int rssi) { + if (rssi <= -90) { + return StrId::STR_SIGNAL_QUALITY_POOR; + } + if (rssi <= -75) { + return StrId::STR_SIGNAL_QUALITY_WEAK; + } + if (rssi <= -60) { + return StrId::STR_SIGNAL_QUALITY_GOOD; + } + return StrId::STR_SIGNAL_QUALITY_EXCELLENT; +} + +int rssiToSignalBars(int rssi) { + if (rssi <= -90) { + return 1; + } + if (rssi <= -75) { + return 2; + } + if (rssi <= -60) { + return 3; + } + return 4; +} + +void drawSignalStrength(const GfxRenderer& renderer, const Rect& bounds, int rssi) { + const int x = bounds.x; + const int y = bounds.y; + const int width = bounds.width; + const int height = bounds.height; + const int barCount = 4; + const int gap = 6; + const int barWidth = std::min(10, (width - (barCount - 1) * gap) / barCount); + const int totalWidth = barCount * barWidth + (barCount - 1) * gap; + const int startX = x + (width - totalWidth) / 2; + const int maxBarHeight = height - 8; + const int bars = rssi == 0 ? 0 : rssiToSignalBars(rssi); + const int baseY = y + height - 2; + for (int i = 0; i < barCount; ++i) { + const int barHeight = ((i + 1) * maxBarHeight) / barCount; + const int barX = startX + i * (barWidth + gap); + const int barY = baseY - barHeight; + renderer.drawRect(barX, barY, barWidth, barHeight, true); + if (i < bars) { + renderer.fillRect(barX + 1, barY + 1, barWidth - 2, barHeight - 2); + } + } +} + +std::string rssiLabel(int rssi) { + if (rssi == 0) { + return std::string(tr(STR_NO_SIGNAL)); + } + const char* quality = I18N.get(getRssiQualityStrId(rssi)); + std::string label = std::string(tr(STR_RSSI)) + ": " + std::to_string(rssi) + " dBm (" + quality + ")"; + return label; +} + +} // namespace + void CrossPointWebServerActivity::renderServerRunning() const { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect contentRect = UITheme::getContentRect(renderer, true, false); @@ -412,6 +485,13 @@ void CrossPointWebServerActivity::renderServerRunning() const { hostnameUrl.c_str()); renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100, ipUrl.c_str()); + + const int signalHeight = 22; + const int signalWidth = contentRect.width - metrics.contentSidePadding * 2; + const int signalY = startY + 120; + const Rect signalBounds(contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight); + drawSignalStrength(renderer, signalBounds, 0); + renderer.drawCenteredText(SMALL_FONT_ID, signalY + signalHeight + 2, tr(STR_HOTSPOT_MODE)); } else { startY += metrics.verticalSpacing * 2; @@ -435,8 +515,19 @@ void CrossPointWebServerActivity::renderServerRunning() const { // Also show hostname URL std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/"; renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true); + + // AP mode: no external RSSI metric available, but keep UI spacing consistent. } - const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); + if (!isApMode) { + const int signalHeight = 22; + const int signalWidth = contentRect.width - metrics.contentSidePadding * 2; + const int signalY = startY + height10 + metrics.verticalSpacing * 2; + const Rect signalBounds(contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight); + drawSignalStrength(renderer, signalBounds, currentRssi); + renderer.drawCenteredText(SMALL_FONT_ID, signalY + signalHeight + 2, rssiLabel(currentRssi).c_str(), true); + } + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index 5069f199..ed8c11f9 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -40,6 +40,8 @@ class CrossPointWebServerActivity final : public Activity { // Server status std::string connectedIP; std::string connectedSSID; // For STA mode: network name, For AP mode: AP name + int currentRssi = 0; + unsigned long lastRssiUpdateTime = 0; // Performance monitoring unsigned long lastHandleClientTime = 0; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 04822a38..366e1ba0 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -17,6 +17,7 @@ #include "html/FilesPageHtml.generated.h" #include "html/HomePageHtml.generated.h" #include "html/SettingsPageHtml.generated.h" +#include "html/WelcomePageHtml.generated.h" #include "html/js/jszip_minJs.generated.h" namespace { @@ -162,11 +163,13 @@ void CrossPointWebServer::begin() { // Setup routes LOG_DBG("WEB", "Setting up routes..."); - server->on("/", HTTP_GET, [this] { handleRoot(); }); - server->on("/files", HTTP_GET, [this] { handleFileList(); }); + server->on("/", HTTP_GET, [this] { handleWelcomePage(); }); + server->on("/systeminfo", HTTP_GET, [this] { handleSystemInfoPage(); }); + server->on("/files", HTTP_GET, [this] { handleRoot(); }); server->on("/js/jszip.min.js", HTTP_GET, [this] { handleJszip(); }); server->on("/api/status", HTTP_GET, [this] { handleStatus(); }); + server->on("/api/status/fast", HTTP_GET, [this] { handleStatusFast(); }); server->on("/api/files", HTTP_GET, [this] { handleFileListData(); }); server->on("/download", HTTP_GET, [this] { handleDownload(); }); @@ -353,8 +356,24 @@ static void sendHtmlContent(WebServer* server, const char* data, size_t len) { } void CrossPointWebServer::handleRoot() const { + int32_t t0 = millis(); + sendHtmlContent(server.get(), FilesPageHtml, sizeof(FilesPageHtml)); + int32_t t1 = millis(); + LOG_DBG("WEB", "Served file manager page in %d ms", t1 - t0); +} + +void CrossPointWebServer::handleWelcomePage() const { + int32_t t0 = millis(); + sendHtmlContent(server.get(), WelcomePageHtml, sizeof(WelcomePageHtml)); + int32_t t1 = millis(); + LOG_DBG("WEB", "Served welcome page in %d ms", t1 - t0); +} + +void CrossPointWebServer::handleSystemInfoPage() const { + int32_t t0 = millis(); sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml)); - LOG_DBG("WEB", "Served root page"); + int32_t t1 = millis(); + LOG_DBG("WEB", "Served system info page in %d ms", t1 - t0); } void CrossPointWebServer::handleJszip() const { @@ -370,10 +389,14 @@ void CrossPointWebServer::handleNotFound() const { } void CrossPointWebServer::handleStatus() const { - const bool fastOnly = server->hasArg("phase") && server->arg("phase") == "fast"; - + const bool fastOnly = server->hasArg("phase") && server->arg("phase").equalsIgnoreCase("fast"); + LOG_DBG("SYSINFO", "handleStatus request received (fastOnly=%d)", fastOnly); + int32_t t0 = millis(); SystemStatus status = SystemStatus::collectFast(); + int32_t t1 = millis(); + LOG_DBG("SYSINFO", "Collected fast status in %d ms (fastOnly=%d)", t1 - t0, fastOnly); if (!fastOnly) { + LOG_DBG("SYSINFO", "handleStatus will collect SD stats"); SystemStatus::fillSdStatus(status); } @@ -404,6 +427,41 @@ void CrossPointWebServer::handleStatus() const { server->send(200, "application/json", json); } +void CrossPointWebServer::handleStatusFast() const { + LOG_DBG("SYSINFO", "handleStatusFast request received"); + int32_t t0 = millis(); + SystemStatus status = SystemStatus::collectFast(); + int32_t t1 = millis(); + LOG_DBG("SYSINFO", "Collected fast-only status in %d ms", t1 - t0); + + 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"] = false; + // Still include SD stats in response, but client should ignore them when sdReady=false + doc["sdTotal"] = status.sdTotalBytes; + doc["sdUsed"] = status.sdUsedBytes; + doc["sdFree"] = status.sdFreeBytes; + + String json; + serializeJson(doc, json); + server->send(200, "application/json", json); +} + void CrossPointWebServer::scanFiles(const char* path, const std::function& callback) const { FsFile root = Storage.open(path); if (!root) { diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 9a8444ec..14a71ad8 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -89,9 +89,12 @@ class CrossPointWebServer { // Request handlers void handleRoot() const; + void handleWelcomePage() const; + void handleSystemInfoPage() const; void handleJszip() const; void handleNotFound() const; void handleStatus() const; + void handleStatusFast() const; void handleFileList() const; void handleFileListData() const; void handleDownload() const; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index 54830b0f..96795d17 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -1742,9 +1742,9 @@