Some checks failed
CI / build (push) Failing after 3m20s
- Fix ignored return value in TxtReaderActivity - Remove unused variables across multiple files - Add const correctness to DictionaryMargins and EpubReaderActivity - Replace inefficient substr patterns with resize+append - Use STL algorithms (find_if, any_of, copy_if, transform) where applicable - Remove dead sync placeholder code in EpubReaderChapterSelectionActivity - Add cppcheck suppression for ValueFlow analysis limitation Resolves all low, medium, and high severity cppcheck defects.
765 lines
29 KiB
C++
765 lines
29 KiB
C++
#include "CrossPointWebServerActivity.h"
|
|
|
|
#include <DNSServer.h>
|
|
#include <ESPmDNS.h>
|
|
#include <GfxRenderer.h>
|
|
#include <WiFi.h>
|
|
#include <esp_task_wdt.h>
|
|
#include <esp_wifi.h>
|
|
#include <qrcode.h>
|
|
|
|
#include <cstddef>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "NetworkModeSelectionActivity.h"
|
|
#include "WifiSelectionActivity.h"
|
|
#include "activities/network/CalibreConnectActivity.h"
|
|
#include "fontIds.h"
|
|
|
|
namespace {
|
|
// AP Mode configuration
|
|
constexpr const char* AP_SSID = "CrossPoint-Reader";
|
|
constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
|
|
constexpr const char* AP_HOSTNAME = "crosspoint";
|
|
constexpr uint8_t AP_CHANNEL = 1;
|
|
constexpr uint8_t AP_MAX_CONNECTIONS = 4;
|
|
|
|
// DNS server for captive portal (redirects all DNS queries to our IP)
|
|
DNSServer* dnsServer = nullptr;
|
|
constexpr uint16_t DNS_PORT = 53;
|
|
|
|
// mDNS service discovery - advertises the device for network scanning
|
|
// Controlled by build flags in platformio.ini:
|
|
// MDNS_SERVICE_DISCOVERY - enables service advertisement (required for Android NSD)
|
|
// MDNS_SERVICE_TXT_RECORDS - enables TXT record metadata (device type, ports)
|
|
void setupMdnsServiceDiscovery(uint16_t httpPort, uint16_t wsPort) {
|
|
#if MDNS_SERVICE_DISCOVERY
|
|
// Advertise HTTP service for network discovery (required for Android NSD)
|
|
MDNS.addService("http", "tcp", httpPort);
|
|
Serial.printf("[%lu] [WEBACT] mDNS service advertised: _http._tcp port %d\n", millis(), httpPort);
|
|
|
|
#if MDNS_SERVICE_TXT_RECORDS
|
|
// Add TXT records with device info for enhanced discovery
|
|
MDNS.addServiceTxt("http", "tcp", "device", "crosspoint");
|
|
MDNS.addServiceTxt("http", "tcp", "wsPort", String(wsPort).c_str());
|
|
Serial.printf("[%lu] [WEBACT] mDNS TXT records added (device=crosspoint, wsPort=%d)\n", millis(), wsPort);
|
|
#else
|
|
(void)wsPort;
|
|
#endif
|
|
|
|
#else
|
|
(void)httpPort;
|
|
(void)wsPort;
|
|
Serial.printf("[%lu] [WEBACT] mDNS service discovery disabled\n", millis());
|
|
#endif
|
|
}
|
|
} // namespace
|
|
|
|
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
|
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
|
self->displayTaskLoop();
|
|
}
|
|
|
|
void CrossPointWebServerActivity::onEnter() {
|
|
ActivityWithSubactivity::onEnter();
|
|
|
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
|
|
// Reset state
|
|
state = WebServerActivityState::MODE_SELECTION;
|
|
networkMode = NetworkMode::JOIN_NETWORK;
|
|
isApMode = false;
|
|
connectedIP.clear();
|
|
connectedSSID.clear();
|
|
lastHandleClientTime = 0;
|
|
currentScreen = FileTransferScreen::COMPANION_APP_LIBRARY;
|
|
lastStatsRefresh = 0;
|
|
updateRequired = true;
|
|
|
|
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
|
6144, // Stack size (increased: QR code + string ops need ~4KB)
|
|
this, // Parameters
|
|
1, // Priority
|
|
&displayTaskHandle // Task handle
|
|
);
|
|
|
|
// Launch network mode selection subactivity
|
|
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
|
|
enterNewActivity(new NetworkModeSelectionActivity(
|
|
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
|
[this]() { onGoBack(); } // Cancel goes back to home
|
|
));
|
|
}
|
|
|
|
void CrossPointWebServerActivity::onExit() {
|
|
ActivityWithSubactivity::onExit();
|
|
|
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d, Largest: %d\n", millis(), ESP.getFreeHeap(),
|
|
ESP.getMaxAllocHeap());
|
|
|
|
state = WebServerActivityState::SHUTTING_DOWN;
|
|
|
|
// Stop the web server first (before disconnecting WiFi)
|
|
stopWebServer();
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After stopWebServer: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
// Stop mDNS
|
|
MDNS.end();
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After MDNS.end: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
// Stop DNS server if running (AP mode)
|
|
if (dnsServer) {
|
|
Serial.printf("[%lu] [WEBACT] Stopping DNS server...\n", millis());
|
|
dnsServer->stop();
|
|
delete dnsServer;
|
|
dnsServer = nullptr;
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After DNS server cleanup: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
}
|
|
|
|
// Brief wait for LWIP stack to flush pending packets
|
|
delay(50);
|
|
|
|
// Disconnect WiFi gracefully
|
|
if (isApMode) {
|
|
Serial.printf("[%lu] [WEBACT] Stopping WiFi AP...\n", millis());
|
|
WiFi.softAPdisconnect(true);
|
|
} else {
|
|
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi...\n", millis());
|
|
WiFi.disconnect(false); // false = don't erase credentials, just send disconnect frame
|
|
}
|
|
delay(100); // Allow disconnect frame to be sent and event callbacks to complete
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
|
WiFi.mode(WIFI_OFF);
|
|
delay(50);
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After WiFi.mode(OFF): %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
// Aggressive WiFi cleanup: stop the WiFi driver to recover more memory
|
|
// WiFi is on-demand for File Transfer only, so full cleanup is safe
|
|
Serial.printf("[%lu] [WEBACT] Stopping WiFi driver (esp_wifi_stop)...\n", millis());
|
|
esp_wifi_stop();
|
|
delay(100); // Allow LWIP stack to fully release
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After esp_wifi_stop: %d, Largest: %d\n", millis(), ESP.getFreeHeap(),
|
|
ESP.getMaxAllocHeap());
|
|
|
|
// Acquire mutex before deleting task
|
|
Serial.printf("[%lu] [WEBACT] Acquiring rendering mutex before task deletion...\n", millis());
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
// Delete the display task
|
|
Serial.printf("[%lu] [WEBACT] Deleting display task...\n", millis());
|
|
if (displayTaskHandle) {
|
|
// Log stack high-water mark before deleting task (stack size: 6144 bytes)
|
|
LOG_STACK_WATERMARK("CrossPointWebServerActivity", displayTaskHandle);
|
|
vTaskDelete(displayTaskHandle);
|
|
displayTaskHandle = nullptr;
|
|
// Allow idle task to free the task stack
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
Serial.printf("[%lu] [WEBACT] [MEM] After task delete + yield: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
}
|
|
|
|
// Delete the mutex
|
|
Serial.printf("[%lu] [WEBACT] Deleting mutex...\n", millis());
|
|
vSemaphoreDelete(renderingMutex);
|
|
renderingMutex = nullptr;
|
|
|
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d, Largest: %d, MinFree: %d\n", millis(),
|
|
ESP.getFreeHeap(), ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
|
|
}
|
|
|
|
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
|
|
const char* modeName = "Join Network";
|
|
if (mode == NetworkMode::CONNECT_CALIBRE) {
|
|
modeName = "Connect to Calibre";
|
|
} else if (mode == NetworkMode::CREATE_HOTSPOT) {
|
|
modeName = "Create Hotspot";
|
|
}
|
|
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(), modeName);
|
|
|
|
networkMode = mode;
|
|
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
|
|
|
// Exit mode selection subactivity
|
|
exitActivity();
|
|
|
|
if (mode == NetworkMode::CONNECT_CALIBRE) {
|
|
exitActivity();
|
|
enterNewActivity(new CalibreConnectActivity(renderer, mappedInput, [this] {
|
|
exitActivity();
|
|
state = WebServerActivityState::MODE_SELECTION;
|
|
enterNewActivity(new NetworkModeSelectionActivity(
|
|
renderer, mappedInput, [this](const NetworkMode nextMode) { onNetworkModeSelected(nextMode); },
|
|
[this]() { onGoBack(); }));
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (mode == NetworkMode::JOIN_NETWORK) {
|
|
// STA mode - launch WiFi selection
|
|
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
state = WebServerActivityState::WIFI_SELECTION;
|
|
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
} else {
|
|
// AP mode - start access point
|
|
state = WebServerActivityState::AP_STARTING;
|
|
updateRequired = true;
|
|
startAccessPoint();
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
|
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
|
|
|
if (connected) {
|
|
// Get connection info before exiting subactivity
|
|
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
|
connectedSSID = WiFi.SSID().c_str();
|
|
isApMode = false;
|
|
|
|
exitActivity();
|
|
|
|
// Start mDNS for hostname resolution
|
|
if (MDNS.begin(AP_HOSTNAME)) {
|
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
|
setupMdnsServiceDiscovery(80, 81);
|
|
}
|
|
|
|
// Start the web server
|
|
startWebServer();
|
|
} else {
|
|
// User cancelled - go back to mode selection
|
|
exitActivity();
|
|
state = WebServerActivityState::MODE_SELECTION;
|
|
enterNewActivity(new NetworkModeSelectionActivity(
|
|
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
|
[this]() { onGoBack(); }));
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::startAccessPoint() {
|
|
Serial.printf("[%lu] [WEBACT] Starting Access Point mode...\n", millis());
|
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap before AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
// Configure and start the AP
|
|
WiFi.mode(WIFI_AP);
|
|
delay(100);
|
|
|
|
// Start soft AP
|
|
bool apStarted;
|
|
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
|
|
apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
|
|
} else {
|
|
// Open network (no password)
|
|
apStarted = WiFi.softAP(AP_SSID, nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
|
|
}
|
|
|
|
if (!apStarted) {
|
|
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start Access Point!\n", millis());
|
|
onGoBack();
|
|
return;
|
|
}
|
|
|
|
delay(100); // Wait for AP to fully initialize
|
|
|
|
// Get AP IP address
|
|
const IPAddress apIP = WiFi.softAPIP();
|
|
char ipStr[16];
|
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]);
|
|
connectedIP = ipStr;
|
|
connectedSSID = AP_SSID;
|
|
|
|
Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis());
|
|
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
|
|
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
|
|
|
|
// Start mDNS for hostname resolution
|
|
if (MDNS.begin(AP_HOSTNAME)) {
|
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
|
setupMdnsServiceDiscovery(80, 81);
|
|
} else {
|
|
Serial.printf("[%lu] [WEBACT] WARNING: mDNS failed to start\n", millis());
|
|
}
|
|
|
|
// Start DNS server for captive portal behavior
|
|
// This redirects all DNS queries to our IP, making any domain typed resolve to us
|
|
dnsServer = new DNSServer();
|
|
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
|
dnsServer->start(DNS_PORT, "*", apIP);
|
|
Serial.printf("[%lu] [WEBACT] DNS server started for captive portal\n", millis());
|
|
|
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
|
|
|
// Start the web server
|
|
startWebServer();
|
|
}
|
|
|
|
void CrossPointWebServerActivity::startWebServer() {
|
|
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
|
|
|
// Create the web server instance
|
|
webServer.reset(new CrossPointWebServer());
|
|
webServer->begin();
|
|
|
|
if (webServer->isRunning()) {
|
|
state = WebServerActivityState::SERVER_RUNNING;
|
|
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
|
|
|
|
// Force an immediate render since we're transitioning from a subactivity
|
|
// that had its own rendering task. We need to make sure our display is shown.
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
render();
|
|
xSemaphoreGive(renderingMutex);
|
|
Serial.printf("[%lu] [WEBACT] Rendered File Transfer screen\n", millis());
|
|
} else {
|
|
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start web server!\n", millis());
|
|
webServer.reset();
|
|
// Go back on error
|
|
onGoBack();
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::stopWebServer() {
|
|
if (webServer && webServer->isRunning()) {
|
|
Serial.printf("[%lu] [WEBACT] Stopping web server...\n", millis());
|
|
webServer->stop();
|
|
Serial.printf("[%lu] [WEBACT] Web server stopped\n", millis());
|
|
}
|
|
webServer.reset();
|
|
}
|
|
|
|
void CrossPointWebServerActivity::loop() {
|
|
if (subActivity) {
|
|
// Forward loop to subactivity
|
|
subActivity->loop();
|
|
return;
|
|
}
|
|
|
|
// Handle different states
|
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
|
// Handle DNS requests for captive portal (AP mode only)
|
|
if (isApMode && dnsServer) {
|
|
dnsServer->processNextRequest();
|
|
}
|
|
|
|
// STA mode: Monitor WiFi connection health
|
|
if (!isApMode && webServer && webServer->isRunning()) {
|
|
static unsigned long lastWifiCheck = 0;
|
|
if (millis() - lastWifiCheck > 2000) { // Check every 2 seconds
|
|
lastWifiCheck = millis();
|
|
const wl_status_t wifiStatus = WiFi.status();
|
|
if (wifiStatus != WL_CONNECTED) {
|
|
Serial.printf("[%lu] [WEBACT] WiFi disconnected! Status: %d\n", millis(), wifiStatus);
|
|
// Show error and exit gracefully
|
|
state = WebServerActivityState::SHUTTING_DOWN;
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
// Log weak signal warnings
|
|
const int rssi = WiFi.RSSI();
|
|
if (rssi < -75) {
|
|
Serial.printf("[%lu] [WEBACT] Warning: Weak WiFi signal: %d dBm\n", millis(), rssi);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle web server requests - maximize throughput with watchdog safety
|
|
if (webServer && webServer->isRunning()) {
|
|
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
|
|
|
|
// Log if there's a significant gap between handleClient calls (>100ms)
|
|
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
|
|
Serial.printf("[%lu] [WEBACT] WARNING: %lu ms gap since last handleClient\n", millis(),
|
|
timeSinceLastHandleClient);
|
|
}
|
|
|
|
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
|
|
esp_task_wdt_reset();
|
|
|
|
// Process HTTP requests in tight loop for maximum throughput
|
|
// More iterations = more data processed per main loop cycle
|
|
constexpr int MAX_ITERATIONS = 500;
|
|
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
|
|
webServer->handleClient();
|
|
// Reset watchdog every 32 iterations
|
|
if ((i & 0x1F) == 0x1F) {
|
|
esp_task_wdt_reset();
|
|
}
|
|
// Yield and check for exit button every 64 iterations
|
|
if ((i & 0x3F) == 0x3F) {
|
|
yield();
|
|
// Check for exit button inside loop for responsiveness
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
onGoBack();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
lastHandleClientTime = millis();
|
|
}
|
|
|
|
// Handle screen switching with front arrow buttons (Left or Right cycles forward)
|
|
// Order: COMPANION_APP_LIBRARY -> WEB_BROWSER -> COMPANION_APP -> back to COMPANION_APP_LIBRARY
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Left) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
switch (currentScreen) {
|
|
case FileTransferScreen::COMPANION_APP_LIBRARY:
|
|
currentScreen = FileTransferScreen::WEB_BROWSER;
|
|
break;
|
|
case FileTransferScreen::WEB_BROWSER:
|
|
currentScreen = FileTransferScreen::COMPANION_APP;
|
|
break;
|
|
case FileTransferScreen::COMPANION_APP:
|
|
currentScreen = FileTransferScreen::COMPANION_APP_LIBRARY;
|
|
break;
|
|
}
|
|
updateRequired = true;
|
|
Serial.printf("[%lu] [WEBACT] Switched to screen: %d\n", millis(), static_cast<int>(currentScreen));
|
|
}
|
|
|
|
// Refresh stats every 30 seconds
|
|
if (millis() - lastStatsRefresh >= 30000) {
|
|
lastStatsRefresh = millis();
|
|
updateRequired = true;
|
|
Serial.printf("[%lu] [WEBACT] Stats refresh triggered\n", millis());
|
|
}
|
|
|
|
// Handle exit on Back button (also check outside loop)
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
onGoBack();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::displayTaskLoop() {
|
|
while (true) {
|
|
if (updateRequired) {
|
|
updateRequired = false;
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
render();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::render() const {
|
|
// Only render our own UI when server is running
|
|
// Subactivities handle their own rendering
|
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
|
renderer.clearScreen();
|
|
renderServerRunning();
|
|
renderer.displayBuffer();
|
|
} else if (state == WebServerActivityState::AP_STARTING) {
|
|
renderer.clearScreen();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
const int bezelTop = renderer.getBezelOffsetTop();
|
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
|
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
|
|
renderer.drawCenteredText(UI_12_FONT_ID, centerY - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD);
|
|
renderer.displayBuffer();
|
|
}
|
|
}
|
|
|
|
// Draw QR code at specified position with configurable pixel size per module
|
|
// Returns the size of the QR code in pixels (width = height = size * pixelsPerModule)
|
|
int drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data,
|
|
const uint8_t pixelsPerModule = 7) {
|
|
QRCode qrcode;
|
|
uint8_t qrcodeBytes[qrcode_getBufferSize(4)];
|
|
Serial.printf("[%lu] [WEBACT] QR Code (%lu): %s\n", millis(), data.length(), data.c_str());
|
|
|
|
qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str());
|
|
for (uint8_t cy = 0; cy < qrcode.size; cy++) {
|
|
for (uint8_t cx = 0; cx < qrcode.size; cx++) {
|
|
if (qrcode_getModule(&qrcode, cx, cy)) {
|
|
renderer.fillRect(x + pixelsPerModule * cx, y + pixelsPerModule * cy, pixelsPerModule, pixelsPerModule, true);
|
|
}
|
|
}
|
|
}
|
|
return qrcode.size * pixelsPerModule;
|
|
}
|
|
|
|
// Helper to format bytes into human-readable sizes
|
|
std::string formatBytes(size_t bytes) {
|
|
if (bytes < 1024) {
|
|
return std::to_string(bytes) + " B";
|
|
} else if (bytes < 1024 * 1024) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%.1f KB", bytes / 1024.0);
|
|
return buf;
|
|
} else {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%.1f MB", bytes / (1024.0 * 1024.0));
|
|
return buf;
|
|
}
|
|
}
|
|
|
|
// Helper to format uptime in human-readable format
|
|
std::string formatUptime(unsigned long seconds) {
|
|
if (seconds < 60) {
|
|
return std::to_string(seconds) + "s";
|
|
} else if (seconds < 3600) {
|
|
unsigned long mins = seconds / 60;
|
|
unsigned long secs = seconds % 60;
|
|
return std::to_string(mins) + "m " + std::to_string(secs) + "s";
|
|
} else {
|
|
unsigned long hours = seconds / 3600;
|
|
unsigned long mins = (seconds % 3600) / 60;
|
|
return std::to_string(hours) + "h " + std::to_string(mins) + "m";
|
|
}
|
|
}
|
|
|
|
std::string CrossPointWebServerActivity::getCompanionAppUrl() const {
|
|
// Generate deep link URL for companion Android app - Device/Files tab
|
|
// Format: crosspoint://files?host=<ip>&port=<port>&wsPort=<wsPort>
|
|
std::string url = "crosspoint://files?host=" + connectedIP;
|
|
if (webServer) {
|
|
url += "&port=" + std::to_string(webServer->getPort());
|
|
url += "&wsPort=" + std::to_string(webServer->getWsPort());
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string CrossPointWebServerActivity::getCompanionAppLibraryUrl() const {
|
|
// Generate deep link URL for companion Android app - Library tab
|
|
// Format: crosspoint://library?host=<ip>&port=<port>&wsPort=<wsPort>
|
|
std::string url = "crosspoint://library?host=" + connectedIP;
|
|
if (webServer) {
|
|
url += "&port=" + std::to_string(webServer->getPort());
|
|
url += "&wsPort=" + std::to_string(webServer->getWsPort());
|
|
}
|
|
return url;
|
|
}
|
|
|
|
void CrossPointWebServerActivity::renderServerRunning() const {
|
|
// Switch to landscape orientation for the QR code screens (800x480)
|
|
const auto originalOrientation = renderer.getOrientation();
|
|
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
|
|
|
// Dispatch to the appropriate screen
|
|
switch (currentScreen) {
|
|
case FileTransferScreen::WEB_BROWSER:
|
|
renderWebBrowserScreen();
|
|
break;
|
|
case FileTransferScreen::COMPANION_APP:
|
|
renderCompanionAppScreen();
|
|
break;
|
|
case FileTransferScreen::COMPANION_APP_LIBRARY:
|
|
renderCompanionAppLibraryScreen();
|
|
break;
|
|
}
|
|
|
|
// Render stats bar at the bottom (above button hints)
|
|
renderStats();
|
|
|
|
// Restore orientation before drawing button hints (they handle their own orientation internally)
|
|
renderer.setOrientation(originalOrientation);
|
|
|
|
// Draw button hints - Left/Right cycle screens, Back exits
|
|
const auto labels = mappedInput.mapLabels("« Exit", "", "", "Next »");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
}
|
|
|
|
void CrossPointWebServerActivity::renderStats() const {
|
|
if (!webServer) return;
|
|
|
|
// In landscape (800x480), position stats near bottom but above button area
|
|
const int screenHeight = renderer.getScreenHeight(); // 480 in landscape
|
|
const int statsY = screenHeight - 30;
|
|
|
|
// Get stats from web server
|
|
const unsigned long uptime = webServer->getServerUptime();
|
|
const size_t bytesUp = webServer->getTotalBytesUploaded();
|
|
const size_t bytesDown = webServer->getTotalBytesDownloaded();
|
|
const size_t filesUp = webServer->getTotalFilesUploaded();
|
|
const size_t filesDown = webServer->getTotalFilesDownloaded();
|
|
|
|
// Format stats string
|
|
std::string statsLine = "Up: " + formatUptime(uptime);
|
|
statsLine += " | Recv: " + formatBytes(bytesUp);
|
|
if (filesUp > 0) {
|
|
statsLine += " (" + std::to_string(filesUp) + ")";
|
|
}
|
|
statsLine += " | Sent: " + formatBytes(bytesDown);
|
|
if (filesDown > 0) {
|
|
statsLine += " (" + std::to_string(filesDown) + ")";
|
|
}
|
|
|
|
renderer.drawCenteredText(UI_10_FONT_ID, statsY, statsLine.c_str());
|
|
}
|
|
|
|
void CrossPointWebServerActivity::renderWebBrowserScreen() const {
|
|
// Landscape layout (800x480): QR on left, text on right
|
|
constexpr int QR_X = 15;
|
|
constexpr int QR_Y = 15;
|
|
constexpr int QR_PX = 7; // pixels per QR module
|
|
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
|
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
|
constexpr int LINE_SPACING = 32;
|
|
|
|
// Draw title on right side
|
|
renderer.drawText(NOTOSANS_14_FONT_ID, TEXT_X, QR_Y, "File Transfer - Web Browser", true, EpdFontFamily::BOLD);
|
|
|
|
int textY = QR_Y + LINE_SPACING + 10;
|
|
|
|
if (isApMode) {
|
|
// AP mode: Show WiFi QR code on left, connection info on right
|
|
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
|
|
drawQRCode(renderer, QR_X, QR_Y, wifiConfig, QR_PX);
|
|
|
|
std::string ssidInfo = "Network: " + connectedSSID;
|
|
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
|
|
textY += LINE_SPACING;
|
|
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Scan QR code to connect to WiFi,");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "then open the URL in your browser.");
|
|
textY += LINE_SPACING + 8;
|
|
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Browser URL:", true, EpdFontFamily::BOLD);
|
|
textY += LINE_SPACING - 4;
|
|
|
|
std::string webUrl = "http://" + connectedIP + "/";
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str(), true, EpdFontFamily::BOLD);
|
|
textY += LINE_SPACING - 4;
|
|
|
|
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, hostnameUrl.c_str());
|
|
} else {
|
|
// STA mode: Show URL QR code on left, connection info on right
|
|
std::string webUrl = "http://" + connectedIP + "/";
|
|
drawQRCode(renderer, QR_X, QR_Y, webUrl, QR_PX);
|
|
|
|
std::string ssidInfo = "Network: " + connectedSSID;
|
|
if (ssidInfo.length() > 35) {
|
|
ssidInfo.resize(32);
|
|
ssidInfo += "...";
|
|
}
|
|
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
|
|
textY += LINE_SPACING;
|
|
|
|
std::string ipInfo = "IP Address: " + connectedIP;
|
|
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ipInfo.c_str());
|
|
textY += LINE_SPACING + 8;
|
|
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str(), true, EpdFontFamily::BOLD);
|
|
textY += LINE_SPACING - 4;
|
|
|
|
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, hostnameUrl.c_str());
|
|
textY += LINE_SPACING + 4;
|
|
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Open this URL in your browser");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "or scan QR code with your phone:");
|
|
}
|
|
}
|
|
|
|
void CrossPointWebServerActivity::renderCompanionAppScreen() const {
|
|
// Landscape layout (800x480): QR on left, text on right
|
|
constexpr int QR_X = 15;
|
|
constexpr int QR_Y = 15;
|
|
constexpr int QR_PX = 7; // pixels per QR module
|
|
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
|
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
|
constexpr int LINE_SPACING = 32;
|
|
|
|
// Draw title on right side
|
|
renderer.drawText(NOTOSANS_14_FONT_ID, TEXT_X, QR_Y, "Companion App - Files", true, EpdFontFamily::BOLD);
|
|
|
|
int textY = QR_Y + LINE_SPACING + 10;
|
|
|
|
// Show network info
|
|
std::string ssidInfo = "Network: " + connectedSSID;
|
|
if (ssidInfo.length() > 35) {
|
|
ssidInfo.resize(32);
|
|
ssidInfo += "...";
|
|
}
|
|
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
|
|
textY += LINE_SPACING;
|
|
|
|
// Show scan instructions
|
|
if (isApMode) {
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Connect to this WiFi first, then scan");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "QR code with the Companion app.");
|
|
} else {
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Scan the QR code with a phone that");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "has the CrossPoint Companion app.");
|
|
}
|
|
textY += LINE_SPACING + 8;
|
|
|
|
// Show alternative browser access
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Alternative Browser Access:", true, EpdFontFamily::BOLD);
|
|
textY += LINE_SPACING - 4;
|
|
std::string webUrl = "http://" + connectedIP + "/files";
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str());
|
|
|
|
// Draw QR code on left
|
|
const std::string appUrl = getCompanionAppUrl();
|
|
drawQRCode(renderer, QR_X, QR_Y, appUrl, QR_PX);
|
|
|
|
// Show deep link URL below QR code
|
|
const int urlY = QR_Y + QR_SIZE + 10;
|
|
renderer.drawText(UI_12_FONT_ID, QR_X, urlY, appUrl.c_str(), true, EpdFontFamily::BOLD);
|
|
}
|
|
|
|
void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const {
|
|
// Landscape layout (800x480): QR on left, text on right
|
|
constexpr int QR_X = 15;
|
|
constexpr int QR_Y = 15;
|
|
constexpr int QR_PX = 7; // pixels per QR module
|
|
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
|
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
|
constexpr int LINE_SPACING = 32;
|
|
|
|
// Draw title on right side
|
|
renderer.drawText(NOTOSANS_14_FONT_ID, TEXT_X, QR_Y, "Companion App - Library", true, EpdFontFamily::BOLD);
|
|
|
|
int textY = QR_Y + LINE_SPACING + 10;
|
|
|
|
// Show network info
|
|
std::string ssidInfo = "Network: " + connectedSSID;
|
|
if (ssidInfo.length() > 35) {
|
|
ssidInfo.resize(32);
|
|
ssidInfo += "...";
|
|
}
|
|
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
|
|
textY += LINE_SPACING;
|
|
|
|
// Show scan instructions
|
|
if (isApMode) {
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Connect to this WiFi first, then scan");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "QR code with the Companion app.");
|
|
} else {
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Scan the QR code with a phone that");
|
|
textY += LINE_SPACING - 4;
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "has the CrossPoint Companion app.");
|
|
}
|
|
textY += LINE_SPACING + 8;
|
|
|
|
// Show alternative browser access
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, "Alternative Browser Access:", true, EpdFontFamily::BOLD);
|
|
textY += LINE_SPACING - 4;
|
|
std::string webUrl = "http://" + connectedIP + "/";
|
|
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str());
|
|
|
|
// Draw QR code on left
|
|
const std::string appUrl = getCompanionAppLibraryUrl();
|
|
drawQRCode(renderer, QR_X, QR_Y, appUrl, QR_PX);
|
|
|
|
// Show deep link URL below QR code
|
|
const int urlY = QR_Y + QR_SIZE + 10;
|
|
renderer.drawText(UI_12_FONT_ID, QR_X, urlY, appUrl.c_str(), true, EpdFontFamily::BOLD);
|
|
}
|