Files
crosspoint-reader-mod/src/activities/network/CrossPointWebServerActivity.cpp
Eliz 128eb614a6 feat: Current page as QR (#1099)
## Summary

* **What is the goal of this PR?** Implements QR text of the current
page
* **What changes are included?**

## Additional Context

I saw this feature request at #982 
It made sense to me so I implemented. But if the team thinks it is not
necessary please let me know and we can close the PR.

| Page | Menu | QR |
|------|-------|----|
|
![IMG_6601.bmp](https://github.com/user-attachments/files/25473201/IMG_6601.bmp)
|
![IMG_6599.bmp](https://github.com/user-attachments/files/25473202/IMG_6599.bmp)
|
![IMG_6600.bmp](https://github.com/user-attachments/files/25473205/IMG_6600.bmp)
|


---

### AI Usage


Did you use AI tools to help write this code? _** YES

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
2026-02-25 12:12:31 +03:00

439 lines
15 KiB
C++

#include "CrossPointWebServerActivity.h"
#include <DNSServer.h>
#include <ESPmDNS.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include <cstddef>
#include "MappedInputManager.h"
#include "NetworkModeSelectionActivity.h"
#include "WifiSelectionActivity.h"
#include "activities/network/CalibreConnectActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/QrUtils.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;
constexpr int QR_CODE_WIDTH = 198;
constexpr int QR_CODE_HEIGHT = 198;
// DNS server for captive portal (redirects all DNS queries to our IP)
DNSServer* dnsServer = nullptr;
constexpr uint16_t DNS_PORT = 53;
} // namespace
void CrossPointWebServerActivity::onEnter() {
ActivityWithSubactivity::onEnter();
LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap());
// Reset state
state = WebServerActivityState::MODE_SELECTION;
networkMode = NetworkMode::JOIN_NETWORK;
isApMode = false;
connectedIP.clear();
connectedSSID.clear();
lastHandleClientTime = 0;
requestUpdate();
// Launch network mode selection subactivity
LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity...");
enterNewActivity(new NetworkModeSelectionActivity(
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); } // Cancel goes back to home
));
}
void CrossPointWebServerActivity::onExit() {
ActivityWithSubactivity::onExit();
LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN;
// Stop the web server first (before disconnecting WiFi)
stopWebServer();
// Stop mDNS
MDNS.end();
// Stop DNS server if running (AP mode)
if (dnsServer) {
LOG_DBG("WEBACT", "Stopping DNS server...");
dnsServer->stop();
delete dnsServer;
dnsServer = nullptr;
}
// Brief wait for LWIP stack to flush pending packets
delay(50);
// Disconnect WiFi gracefully
if (isApMode) {
LOG_DBG("WEBACT", "Stopping WiFi AP...");
WiFi.softAPdisconnect(true);
} else {
LOG_DBG("WEBACT", "Disconnecting WiFi (graceful)...");
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
}
delay(30); // Allow disconnect frame to be sent
LOG_DBG("WEBACT", "Setting WiFi mode OFF...");
WiFi.mode(WIFI_OFF);
delay(30); // Allow WiFi hardware to power down
LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
}
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";
}
LOG_DBG("WEBACT", "Network mode selected: %s", 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
LOG_DBG("WEBACT", "Turning on WiFi (STA mode)...");
WiFi.mode(WIFI_STA);
state = WebServerActivityState::WIFI_SELECTION;
LOG_DBG("WEBACT", "Launching WifiSelectionActivity...");
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
} else {
// AP mode - start access point
state = WebServerActivityState::AP_STARTING;
requestUpdate();
startAccessPoint();
}
}
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
LOG_DBG("WEBACT", "WifiSelectionActivity completed, connected=%d", 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)) {
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
}
// 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() {
LOG_DBG("WEBACT", "Starting Access Point mode...");
LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", 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) {
LOG_ERR("WEBACT", "ERROR: Failed to start Access Point!");
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;
LOG_DBG("WEBACT", "Access Point started!");
LOG_DBG("WEBACT", "SSID: %s", AP_SSID);
LOG_DBG("WEBACT", "IP: %s", connectedIP.c_str());
// Start mDNS for hostname resolution
if (MDNS.begin(AP_HOSTNAME)) {
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
} else {
LOG_DBG("WEBACT", "WARNING: mDNS failed to start");
}
// 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);
LOG_DBG("WEBACT", "DNS server started for captive portal");
LOG_DBG("WEBACT", "Free heap after AP start: %d bytes", ESP.getFreeHeap());
// Start the web server
startWebServer();
}
void CrossPointWebServerActivity::startWebServer() {
LOG_DBG("WEBACT", "Starting web server...");
// Create the web server instance
webServer.reset(new CrossPointWebServer());
webServer->begin();
if (webServer->isRunning()) {
state = WebServerActivityState::SERVER_RUNNING;
LOG_DBG("WEBACT", "Web server started successfully");
// 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.
{
RenderLock lock(*this);
render(std::move(lock));
}
LOG_DBG("WEBACT", "Rendered File Transfer screen");
} else {
LOG_ERR("WEBACT", "ERROR: Failed to start web server!");
webServer.reset();
// Go back on error
onGoBack();
}
}
void CrossPointWebServerActivity::stopWebServer() {
if (webServer && webServer->isRunning()) {
LOG_DBG("WEBACT", "Stopping web server...");
webServer->stop();
LOG_DBG("WEBACT", "Web server stopped");
}
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) {
LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus);
// Show error and exit gracefully
state = WebServerActivityState::SHUTTING_DOWN;
requestUpdate();
return;
}
// Log weak signal warnings
const int rssi = WiFi.RSSI();
if (rssi < -75) {
LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", 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) {
LOG_DBG("WEBACT", "WARNING: %lu ms gap since last handleClient", 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();
// Force trigger an update of which buttons are being pressed so be have accurate state
// for back button checking
mappedInput.update();
// Check for exit button inside loop for responsiveness
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
return;
}
}
}
lastHandleClientTime = millis();
}
// Handle exit on Back button (also check outside loop)
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
return;
}
}
}
void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
// Only render our own UI when server is running
// Subactivities handle their own rendering
if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
if (state == WebServerActivityState::SERVER_RUNNING) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
renderServerRunning();
} else {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_STARTING_HOTSPOT));
}
renderer.displayBuffer();
}
}
void CrossPointWebServerActivity::renderServerRunning() const {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2;
int height10 = renderer.getLineHeight(UI_10_FONT_ID);
if (isApMode) {
// AP mode display
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_CONNECT_WIFI_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
// Show QR code for Wifi
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
const Rect qrBoundsWifi(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
QrUtils::drawQrCode(renderer, qrBoundsWifi, wifiConfig);
// Show network name
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
connectedSSID.c_str());
startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing;
// Show primary URL (hostname)
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_OPEN_URL_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/";
// Show QR code for URL
const Rect qrBoundsUrl(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
QrUtils::drawQrCode(renderer, qrBoundsUrl, hostnameUrl);
// Show IP address as fallback
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
hostnameUrl.c_str());
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100,
ipUrl.c_str());
} else {
startY += metrics.verticalSpacing * 2;
// STA mode display (original behavior)
// std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_OPEN_URL_HINT), true, EpdFontFamily::BOLD);
startY += height10;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_SCAN_QR_HINT), true, EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
// Show QR code for URL
std::string webInfo = "http://" + connectedIP + "/";
const Rect qrBounds((pageWidth - QR_CODE_WIDTH) / 2, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
QrUtils::drawQrCode(renderer, qrBounds, webInfo);
startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2;
// Show web server URL prominently
renderer.drawCenteredText(UI_10_FONT_ID, startY, webInfo.c_str(), true);
startY += height10 + 5;
// 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);
}
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}