mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-08 08:37:38 +03:00
Compare commits
3 Commits
6fe28da41b
...
9f4f71fabe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4f71fabe | ||
|
|
d23020e268 | ||
|
|
f4491875ab |
@ -214,10 +214,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
|
||||||
XML_SetCharacterDataHandler(parser, characterData);
|
|
||||||
|
|
||||||
FILE* file = fopen(filepath, "r");
|
FILE* file = fopen(filepath, "r");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath);
|
Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath);
|
||||||
@ -225,10 +221,17 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XML_SetUserData(parser, this);
|
||||||
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@ -238,6 +241,9 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
|
|
||||||
if (ferror(file)) {
|
if (ferror(file)) {
|
||||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@ -248,12 +254,18 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} while (!done);
|
} while (!done);
|
||||||
|
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ bool ContainerParser::setup() {
|
|||||||
|
|
||||||
ContainerParser::~ContainerParser() {
|
ContainerParser::~ContainerParser() {
|
||||||
if (parser) {
|
if (parser) {
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ bool ContentOpfParser::setup() {
|
|||||||
|
|
||||||
ContentOpfParser::~ContentOpfParser() {
|
ContentOpfParser::~ContentOpfParser() {
|
||||||
if (parser) {
|
if (parser) {
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
}
|
}
|
||||||
@ -40,6 +43,9 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
|
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
return 0;
|
return 0;
|
||||||
@ -51,6 +57,9 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -18,6 +18,9 @@ bool TocNcxParser::setup() {
|
|||||||
|
|
||||||
TocNcxParser::~TocNcxParser() {
|
TocNcxParser::~TocNcxParser() {
|
||||||
if (parser) {
|
if (parser) {
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
}
|
}
|
||||||
@ -35,6 +38,11 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
|
XML_ParserFree(parser);
|
||||||
|
parser = nullptr;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +52,11 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
|
XML_ParserFree(parser);
|
||||||
|
parser = nullptr;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,28 @@
|
|||||||
#include "CrossPointWebServerActivity.h"
|
#include "CrossPointWebServerActivity.h"
|
||||||
|
|
||||||
|
#include <DNSServer.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
#include "WifiSelectionActivity.h"
|
#include "WifiSelectionActivity.h"
|
||||||
#include "config.h"
|
#include "config.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;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@ -20,7 +36,9 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
state = WebServerActivityState::WIFI_SELECTION;
|
state = WebServerActivityState::MODE_SELECTION;
|
||||||
|
networkMode = NetworkMode::JOIN_NETWORK;
|
||||||
|
isApMode = false;
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectedSSID.clear();
|
connectedSSID.clear();
|
||||||
lastHandleClientTime = 0;
|
lastHandleClientTime = 0;
|
||||||
@ -33,14 +51,12 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Turn on WiFi immediately
|
// Launch network mode selection subactivity
|
||||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
|
||||||
WiFi.mode(WIFI_STA);
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
|
renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||||
// Launch WiFi selection subactivity
|
[this]() { onGoBack(); } // Cancel goes back to home
|
||||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
));
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
|
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onExit() {
|
void CrossPointWebServerActivity::onExit() {
|
||||||
@ -53,14 +69,30 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
// Stop the web server first (before disconnecting WiFi)
|
// Stop the web server first (before disconnecting WiFi)
|
||||||
stopWebServer();
|
stopWebServer();
|
||||||
|
|
||||||
|
// Stop mDNS
|
||||||
|
MDNS.end();
|
||||||
|
|
||||||
|
// Stop DNS server if running (AP mode)
|
||||||
|
if (dnsServer) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Stopping DNS server...\n", millis());
|
||||||
|
dnsServer->stop();
|
||||||
|
delete dnsServer;
|
||||||
|
dnsServer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// CRITICAL: Wait for LWIP stack to flush any pending packets
|
// CRITICAL: Wait for LWIP stack to flush any pending packets
|
||||||
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
|
||||||
delay(500);
|
delay(500);
|
||||||
|
|
||||||
// Disconnect WiFi gracefully
|
// Disconnect WiFi gracefully
|
||||||
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
|
if (isApMode) {
|
||||||
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
Serial.printf("[%lu] [WEBACT] Stopping WiFi AP...\n", millis());
|
||||||
delay(100); // Allow disconnect frame to be sent
|
WiFi.softAPdisconnect(true);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
|
||||||
|
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
||||||
|
}
|
||||||
|
delay(100); // Allow disconnect frame to be sent
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
@ -89,6 +121,33 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(),
|
||||||
|
mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot");
|
||||||
|
|
||||||
|
networkMode = mode;
|
||||||
|
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
||||||
|
|
||||||
|
// Exit mode selection subactivity
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
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, inputManager,
|
||||||
|
[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) {
|
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
||||||
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
||||||
|
|
||||||
@ -96,17 +155,83 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
|||||||
// Get connection info before exiting subactivity
|
// Get connection info before exiting subactivity
|
||||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
||||||
connectedSSID = WiFi.SSID().c_str();
|
connectedSSID = WiFi.SSID().c_str();
|
||||||
|
isApMode = false;
|
||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
|
// Start mDNS for hostname resolution
|
||||||
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
||||||
|
}
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
startWebServer();
|
startWebServer();
|
||||||
} else {
|
} else {
|
||||||
// User cancelled - go back
|
// User cancelled - go back to mode selection
|
||||||
onGoBack();
|
exitActivity();
|
||||||
|
state = WebServerActivityState::MODE_SELECTION;
|
||||||
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
|
renderer, inputManager, [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);
|
||||||
|
} 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() {
|
void CrossPointWebServerActivity::startWebServer() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
||||||
|
|
||||||
@ -150,6 +275,11 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
|
|
||||||
// Handle different states
|
// Handle different states
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
||||||
|
// Handle DNS requests for captive portal (AP mode only)
|
||||||
|
if (isApMode && dnsServer) {
|
||||||
|
dnsServer->processNextRequest();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle web server requests - call handleClient multiple times per loop
|
// Handle web server requests - call handleClient multiple times per loop
|
||||||
// to improve responsiveness and upload throughput
|
// to improve responsiveness and upload throughput
|
||||||
if (webServer && webServer->isRunning()) {
|
if (webServer && webServer->isRunning()) {
|
||||||
@ -193,35 +323,71 @@ void CrossPointWebServerActivity::displayTaskLoop() {
|
|||||||
|
|
||||||
void CrossPointWebServerActivity::render() const {
|
void CrossPointWebServerActivity::render() const {
|
||||||
// Only render our own UI when server is running
|
// Only render our own UI when server is running
|
||||||
// WiFi selection handles its own rendering
|
// Subactivities handle their own rendering
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderServerRunning();
|
renderServerRunning();
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
} else if (state == WebServerActivityState::AP_STARTING) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::renderServerRunning() const {
|
void CrossPointWebServerActivity::renderServerRunning() const {
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
|
||||||
const auto top = (pageHeight - height * 5) / 2;
|
|
||||||
|
|
||||||
renderer.drawCenteredText(READER_FONT_ID, top - 30, "File Transfer", true, BOLD);
|
// Use consistent line spacing
|
||||||
|
constexpr int LINE_SPACING = 28; // Space between lines
|
||||||
|
|
||||||
std::string ssidInfo = "Network: " + connectedSSID;
|
renderer.drawCenteredText(READER_FONT_ID, 15, "File Transfer", true, BOLD);
|
||||||
if (ssidInfo.length() > 28) {
|
|
||||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
if (isApMode) {
|
||||||
|
// AP mode display - center the content block
|
||||||
|
const int startY = 55;
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY, "Hotspot Mode", true, BOLD);
|
||||||
|
|
||||||
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network",
|
||||||
|
true, REGULAR);
|
||||||
|
|
||||||
|
// Show primary URL (hostname)
|
||||||
|
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, BOLD);
|
||||||
|
|
||||||
|
// Show IP address as fallback
|
||||||
|
std::string ipUrl = "or http://" + connectedIP + "/";
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser", true, REGULAR);
|
||||||
|
} else {
|
||||||
|
// STA mode display (original behavior)
|
||||||
|
const int startY = 65;
|
||||||
|
|
||||||
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
|
if (ssidInfo.length() > 28) {
|
||||||
|
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY, ssidInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
std::string ipInfo = "IP Address: " + connectedIP;
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING, ipInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
// Show web server URL prominently
|
||||||
|
std::string webInfo = "http://" + connectedIP + "/";
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, BOLD);
|
||||||
|
|
||||||
|
// Also show hostname URL
|
||||||
|
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser", true, REGULAR);
|
||||||
}
|
}
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
|
|
||||||
|
|
||||||
std::string ipInfo = "IP Address: " + connectedIP;
|
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
|
|
||||||
|
|
||||||
// Show web server URL prominently
|
|
||||||
std::string webInfo = "http://" + connectedIP + "/";
|
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 70, webInfo.c_str(), true, BOLD);
|
|
||||||
|
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, top + 110, "Open this URL in your browser", true, REGULAR);
|
|
||||||
|
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR);
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,15 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
#include "network/CrossPointWebServer.h"
|
#include "network/CrossPointWebServer.h"
|
||||||
|
|
||||||
// Web server activity states
|
// Web server activity states
|
||||||
enum class WebServerActivityState {
|
enum class WebServerActivityState {
|
||||||
WIFI_SELECTION, // WiFi selection subactivity is active
|
MODE_SELECTION, // Choosing between Join Network and Create Hotspot
|
||||||
|
WIFI_SELECTION, // WiFi selection subactivity is active (for Join Network mode)
|
||||||
|
AP_STARTING, // Starting Access Point mode
|
||||||
SERVER_RUNNING, // Web server is running and handling requests
|
SERVER_RUNNING, // Web server is running and handling requests
|
||||||
SHUTTING_DOWN // Shutting down server and WiFi
|
SHUTTING_DOWN // Shutting down server and WiFi
|
||||||
};
|
};
|
||||||
@ -20,8 +23,10 @@ enum class WebServerActivityState {
|
|||||||
/**
|
/**
|
||||||
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
||||||
* It:
|
* It:
|
||||||
* - Immediately turns on WiFi and launches WifiSelectionActivity on enter
|
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
|
||||||
* - When WifiSelectionActivity completes successfully, starts the CrossPointWebServer
|
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
|
||||||
|
* - For AP mode: Creates an Access Point that clients can connect to
|
||||||
|
* - Starts the CrossPointWebServer when connected
|
||||||
* - Handles client requests in its loop() function
|
* - Handles client requests in its loop() function
|
||||||
* - Cleans up the server and shuts down WiFi on exit
|
* - Cleans up the server and shuts down WiFi on exit
|
||||||
*/
|
*/
|
||||||
@ -29,15 +34,19 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
|||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
WebServerActivityState state = WebServerActivityState::WIFI_SELECTION;
|
WebServerActivityState state = WebServerActivityState::MODE_SELECTION;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
|
// Network mode
|
||||||
|
NetworkMode networkMode = NetworkMode::JOIN_NETWORK;
|
||||||
|
bool isApMode = false;
|
||||||
|
|
||||||
// Web server - owned by this activity
|
// Web server - owned by this activity
|
||||||
std::unique_ptr<CrossPointWebServer> webServer;
|
std::unique_ptr<CrossPointWebServer> webServer;
|
||||||
|
|
||||||
// Server status
|
// Server status
|
||||||
std::string connectedIP;
|
std::string connectedIP;
|
||||||
std::string connectedSSID;
|
std::string connectedSSID; // For STA mode: network name, For AP mode: AP name
|
||||||
|
|
||||||
// Performance monitoring
|
// Performance monitoring
|
||||||
unsigned long lastHandleClientTime = 0;
|
unsigned long lastHandleClientTime = 0;
|
||||||
@ -47,7 +56,9 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
|||||||
void render() const;
|
void render() const;
|
||||||
void renderServerRunning() const;
|
void renderServerRunning() const;
|
||||||
|
|
||||||
|
void onNetworkModeSelected(NetworkMode mode);
|
||||||
void onWifiSelectionComplete(bool connected);
|
void onWifiSelectionComplete(bool connected);
|
||||||
|
void startAccessPoint();
|
||||||
void startWebServer();
|
void startWebServer();
|
||||||
void stopWebServer();
|
void stopWebServer();
|
||||||
|
|
||||||
|
|||||||
128
src/activities/network/NetworkModeSelectionActivity.cpp
Normal file
128
src/activities/network/NetworkModeSelectionActivity.cpp
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int MENU_ITEM_COUNT = 2;
|
||||||
|
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Create Hotspot"};
|
||||||
|
const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {"Connect to an existing WiFi network",
|
||||||
|
"Create a WiFi network others can join"};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<NetworkModeSelectionActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
// Reset selection
|
||||||
|
selectedIndex = 0;
|
||||||
|
|
||||||
|
// Trigger first update
|
||||||
|
updateRequired = true;
|
||||||
|
|
||||||
|
xTaskCreate(&NetworkModeSelectionActivity::taskTrampoline, "NetworkModeTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::loop() {
|
||||||
|
// Handle back button - cancel
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle confirm button - select current option
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT;
|
||||||
|
onModeSelected(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
const bool prevPressed =
|
||||||
|
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
|
||||||
|
const bool nextPressed =
|
||||||
|
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
|
||||||
|
|
||||||
|
if (prevPressed) {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
} else if (nextPressed) {
|
||||||
|
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::render() const {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Draw header
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 10, "File Transfer", true, BOLD);
|
||||||
|
|
||||||
|
// Draw subtitle
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 50, "How would you like to connect?", true, REGULAR);
|
||||||
|
|
||||||
|
// Draw menu items centered on screen
|
||||||
|
constexpr int itemHeight = 50; // Height for each menu item (including description)
|
||||||
|
const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10;
|
||||||
|
|
||||||
|
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
|
||||||
|
const int itemY = startY + i * itemHeight;
|
||||||
|
const bool isSelected = (i == selectedIndex);
|
||||||
|
|
||||||
|
// Draw selection highlight (black fill) for selected item
|
||||||
|
if (isSelected) {
|
||||||
|
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text: black=false (white text) when selected (on black background)
|
||||||
|
// black=true (black text) when not selected (on white background)
|
||||||
|
renderer.drawText(UI_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw help text at bottom
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press OK to select, BACK to cancel", true, REGULAR);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
41
src/activities/network/NetworkModeSelectionActivity.h
Normal file
41
src/activities/network/NetworkModeSelectionActivity.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
// Enum for network mode selection
|
||||||
|
enum class NetworkMode { JOIN_NETWORK, CREATE_HOTSPOT };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NetworkModeSelectionActivity presents the user with a choice:
|
||||||
|
* - "Join a Network" - Connect to an existing WiFi network (STA mode)
|
||||||
|
* - "Create Hotspot" - Create an Access Point that others can connect to (AP mode)
|
||||||
|
*
|
||||||
|
* The onModeSelected callback is called with the user's choice.
|
||||||
|
* The onCancel callback is called if the user presses back.
|
||||||
|
*/
|
||||||
|
class NetworkModeSelectionActivity final : public Activity {
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void(NetworkMode)> onModeSelected;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
|
const std::function<void(NetworkMode)>& onModeSelected,
|
||||||
|
const std::function<void()>& onCancel)
|
||||||
|
: Activity("NetworkModeSelection", renderer, inputManager), onModeSelected(onModeSelected), onCancel(onCancel) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
242
src/activities/settings/OtaUpdateActivity.cpp
Normal file
242
src/activities/settings/OtaUpdateActivity.cpp
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
#include "OtaUpdateActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "network/OtaUpdater.h"
|
||||||
|
|
||||||
|
void OtaUpdateActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<OtaUpdateActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [OTA] WiFi connection failed, exiting\n", millis());
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] WiFi connected, checking for update\n", millis());
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = CHECKING_FOR_UPDATE;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
const auto res = updater.checkForUpdate();
|
||||||
|
if (res != OtaUpdater::OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update check failed: %d\n", millis(), res);
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FAILED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updater.isUpdateNewer()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No new update available\n", millis());
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = NO_UPDATE;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = WAITING_CONFIRMATION;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onEnter() {
|
||||||
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
xTaskCreate(&OtaUpdateActivity::taskTrampoline, "OtaUpdateActivityTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
|
||||||
|
// Turn on WiFi immediately
|
||||||
|
Serial.printf("[%lu] [OTA] Turning on WiFi...\n", millis());
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
// Launch WiFi selection subactivity
|
||||||
|
Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis());
|
||||||
|
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
|
||||||
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onExit() {
|
||||||
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
|
// Turn off wifi
|
||||||
|
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
||||||
|
delay(100); // Allow disconnect frame to be sent
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100); // Allow WiFi hardware to fully power down
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::render() {
|
||||||
|
if (subActivity) {
|
||||||
|
// Subactivity handles its own rendering
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float updaterProgress = 0;
|
||||||
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.processedSize, updater.totalSize);
|
||||||
|
updaterProgress = static_cast<float>(updater.processedSize) / static_cast<float>(updater.totalSize);
|
||||||
|
// Only update every 2% at the most
|
||||||
|
if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 10, "Update", true, BOLD);
|
||||||
|
|
||||||
|
if (state == CHECKING_FOR_UPDATE) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Checking for update...", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == WAITING_CONFIRMATION) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 200, "New update available!", true, BOLD);
|
||||||
|
renderer.drawText(UI_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION);
|
||||||
|
renderer.drawText(UI_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str());
|
||||||
|
|
||||||
|
renderer.drawRect(25, pageHeight - 40, 106, 40);
|
||||||
|
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Cancel")) / 2, pageHeight - 35,
|
||||||
|
"Cancel");
|
||||||
|
|
||||||
|
renderer.drawRect(130, pageHeight - 40, 106, 40);
|
||||||
|
renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Update")) / 2, pageHeight - 35,
|
||||||
|
"Update");
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 310, "Updating...", true, BOLD);
|
||||||
|
renderer.drawRect(20, 350, pageWidth - 40, 50);
|
||||||
|
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 420, (std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
|
||||||
|
renderer.drawCenteredText(
|
||||||
|
UI_FONT_ID, 440, (std::to_string(updater.processedSize) + " / " + std::to_string(updater.totalSize)).c_str());
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == NO_UPDATE) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "No update available", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Update failed", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FINISHED) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Update complete", true, BOLD);
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 350, "Press and hold power button to turn back on");
|
||||||
|
renderer.displayBuffer();
|
||||||
|
state = SHUTTING_DOWN;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == WAITING_CONFIRMATION) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis());
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = UPDATE_IN_PROGRESS;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
const auto res = updater.installUpdate([this](const size_t, const size_t) { updateRequired = true; });
|
||||||
|
|
||||||
|
if (res != OtaUpdater::OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update failed: %d\n", millis(), res);
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FAILED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FINISHED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == NO_UPDATE) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == SHUTTING_DOWN) {
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/activities/settings/OtaUpdateActivity.h
Normal file
43
src/activities/settings/OtaUpdateActivity.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "network/OtaUpdater.h"
|
||||||
|
|
||||||
|
class OtaUpdateActivity : public ActivityWithSubactivity {
|
||||||
|
enum State {
|
||||||
|
WIFI_SELECTION,
|
||||||
|
CHECKING_FOR_UPDATE,
|
||||||
|
WAITING_CONFIRMATION,
|
||||||
|
UPDATE_IN_PROGRESS,
|
||||||
|
NO_UPDATE,
|
||||||
|
FAILED,
|
||||||
|
FINISHED,
|
||||||
|
SHUTTING_DOWN
|
||||||
|
};
|
||||||
|
|
||||||
|
// Can't initialize this to 0 or the first render doesn't happen
|
||||||
|
static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111;
|
||||||
|
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void()> goBack;
|
||||||
|
State state = WIFI_SELECTION;
|
||||||
|
unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE;
|
||||||
|
OtaUpdater updater;
|
||||||
|
|
||||||
|
void onWifiSelectionComplete(bool success);
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render();
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OtaUpdateActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& goBack)
|
||||||
|
: ActivityWithSubactivity("OtaUpdate", renderer, inputManager), goBack(goBack), updater() {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
@ -4,16 +4,19 @@
|
|||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "OtaUpdateActivity.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 3;
|
constexpr int settingsCount = 4;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
||||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}};
|
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}},
|
||||||
|
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||||
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
@ -41,7 +44,7 @@ void SettingsActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::onExit() {
|
void SettingsActivity::onExit() {
|
||||||
Activity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
@ -54,6 +57,11 @@ void SettingsActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::loop() {
|
void SettingsActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle actions with early return
|
// Handle actions with early return
|
||||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
toggleCurrentSetting();
|
toggleCurrentSetting();
|
||||||
@ -81,7 +89,7 @@ void SettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::toggleCurrentSetting() const {
|
void SettingsActivity::toggleCurrentSetting() {
|
||||||
// Validate index
|
// Validate index
|
||||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||||
return;
|
return;
|
||||||
@ -96,6 +104,16 @@ void SettingsActivity::toggleCurrentSetting() const {
|
|||||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||||
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||||
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
||||||
|
} else if (setting.type == SettingType::ACTION) {
|
||||||
|
if (std::string(setting.name) == "Check for updates") {
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new OtaUpdateActivity(renderer, inputManager, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only toggle if it's a toggle type and has a value pointer
|
// Only toggle if it's a toggle type and has a value pointer
|
||||||
return;
|
return;
|
||||||
@ -107,7 +125,7 @@ void SettingsActivity::toggleCurrentSetting() const {
|
|||||||
|
|
||||||
void SettingsActivity::displayTaskLoop() {
|
void SettingsActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired && !subActivity) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
render();
|
render();
|
||||||
@ -152,6 +170,8 @@ void SettingsActivity::render() const {
|
|||||||
|
|
||||||
// Draw help text
|
// Draw help text
|
||||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
|
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
|
||||||
|
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||||
|
pageHeight - 30, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
// Always use standard refresh for settings screen
|
// Always use standard refresh for settings screen
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -3,16 +3,15 @@
|
|||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE, ENUM };
|
enum class SettingType { TOGGLE, ENUM, ACTION };
|
||||||
|
|
||||||
// Structure to hold setting information
|
// Structure to hold setting information
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
@ -22,7 +21,7 @@ struct SettingInfo {
|
|||||||
std::vector<std::string> enumValues;
|
std::vector<std::string> enumValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsActivity final : public Activity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
@ -32,11 +31,11 @@ class SettingsActivity final : public Activity {
|
|||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render() const;
|
void render() const;
|
||||||
void toggleCurrentSetting() const;
|
void toggleCurrentSetting();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
||||||
: Activity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
|
: ActivityWithSubactivity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -30,12 +30,22 @@ void CrossPointWebServer::begin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
// Check if we have a valid network connection (either STA connected or AP mode)
|
||||||
Serial.printf("[%lu] [WEB] Cannot start webserver - WiFi not connected\n", millis());
|
const wifi_mode_t wifiMode = WiFi.getMode();
|
||||||
|
const bool isStaConnected = (wifiMode & WIFI_MODE_STA) && (WiFi.status() == WL_CONNECTED);
|
||||||
|
const bool isInApMode = (wifiMode & WIFI_MODE_AP) && (WiFi.softAPgetStationNum() >= 0); // AP is running
|
||||||
|
|
||||||
|
if (!isStaConnected && !isInApMode) {
|
||||||
|
Serial.printf("[%lu] [WEB] Cannot start webserver - no valid network (mode=%d, status=%d)\n", millis(), wifiMode,
|
||||||
|
WiFi.status());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store AP mode flag for later use (e.g., in handleStatus)
|
||||||
|
apMode = isInApMode;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
Serial.printf("[%lu] [WEB] Network mode: %s\n", millis(), apMode ? "AP" : "STA");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
||||||
server.reset(new WebServer(port));
|
server.reset(new WebServer(port));
|
||||||
@ -70,7 +80,9 @@ void CrossPointWebServer::begin() {
|
|||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
||||||
Serial.printf("[%lu] [WEB] Access at http://%s/\n", millis(), WiFi.localIP().toString().c_str());
|
// Show the correct IP based on network mode
|
||||||
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
Serial.printf("[%lu] [WEB] Access at http://%s/\n", millis(), ipAddr.c_str());
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,10 +153,14 @@ void CrossPointWebServer::handleNotFound() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleStatus() const {
|
void CrossPointWebServer::handleStatus() const {
|
||||||
|
// Get correct IP based on AP vs STA mode
|
||||||
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
|
||||||
String json = "{";
|
String json = "{";
|
||||||
json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\",";
|
json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\",";
|
||||||
json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
|
json += "\"ip\":\"" + ipAddr + "\",";
|
||||||
json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
|
json += "\"mode\":\"" + String(apMode ? "AP" : "STA") + "\",";
|
||||||
|
json += "\"rssi\":" + String(apMode ? 0 : WiFi.RSSI()) + ","; // RSSI not applicable in AP mode
|
||||||
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
|
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
|
||||||
json += "\"uptime\":" + String(millis() / 1000);
|
json += "\"uptime\":" + String(millis() / 1000);
|
||||||
json += "}";
|
json += "}";
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class CrossPointWebServer {
|
|||||||
private:
|
private:
|
||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
uint16_t port = 80;
|
uint16_t port = 80;
|
||||||
|
|
||||||
// File scanning
|
// File scanning
|
||||||
|
|||||||
169
src/network/OtaUpdater.cpp
Normal file
169
src/network/OtaUpdater.cpp
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
#include "OtaUpdater.h"
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <Update.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/daveallie/crosspoint-reader/releases/latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||||
|
const std::unique_ptr<WiFiClientSecure> client(new WiFiClientSecure);
|
||||||
|
client->setInsecure();
|
||||||
|
HTTPClient http;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), latestReleaseUrl);
|
||||||
|
|
||||||
|
http.begin(*client, latestReleaseUrl);
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
|
||||||
|
const int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] HTTP error: %d\n", millis(), httpCode);
|
||||||
|
http.end();
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
const DeserializationError error = deserializeJson(doc, *client);
|
||||||
|
http.end();
|
||||||
|
if (error) {
|
||||||
|
Serial.printf("[%lu] [OTA] JSON parse failed: %s\n", millis(), error.c_str());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["tag_name"].is<std::string>()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No tag_name found\n", millis());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
if (!doc["assets"].is<JsonArray>()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No assets found\n", millis());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion = doc["tag_name"].as<std::string>();
|
||||||
|
|
||||||
|
for (int i = 0; i < doc["assets"].size(); i++) {
|
||||||
|
if (doc["assets"][i]["name"] == "firmware.bin") {
|
||||||
|
otaUrl = doc["assets"][i]["browser_download_url"].as<std::string>();
|
||||||
|
otaSize = doc["assets"][i]["size"].as<size_t>();
|
||||||
|
totalSize = otaSize;
|
||||||
|
updateAvailable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateAvailable) {
|
||||||
|
Serial.printf("[%lu] [OTA] No firmware.bin asset found\n", millis());
|
||||||
|
return NO_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Found update: %s\n", millis(), latestVersion.c_str());
|
||||||
|
return OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OtaUpdater::isUpdateNewer() {
|
||||||
|
if (!updateAvailable || latestVersion.empty() || latestVersion == CROSSPOINT_VERSION) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// semantic version check (only match on 3 segments)
|
||||||
|
const auto updateMajor = stoi(latestVersion.substr(0, latestVersion.find('.')));
|
||||||
|
const auto updateMinor = stoi(
|
||||||
|
latestVersion.substr(latestVersion.find('.') + 1, latestVersion.find_last_of('.') - latestVersion.find('.') - 1));
|
||||||
|
const auto updatePatch = stoi(latestVersion.substr(latestVersion.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
std::string currentVersion = CROSSPOINT_VERSION;
|
||||||
|
const auto currentMajor = stoi(currentVersion.substr(0, currentVersion.find('.')));
|
||||||
|
const auto currentMinor = stoi(currentVersion.substr(
|
||||||
|
currentVersion.find('.') + 1, currentVersion.find_last_of('.') - currentVersion.find('.') - 1));
|
||||||
|
const auto currentPatch = stoi(currentVersion.substr(currentVersion.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
if (updateMajor > currentMajor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (updateMajor < currentMajor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateMinor > currentMinor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (updateMinor < currentMinor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatePatch > currentPatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& OtaUpdater::getLatestVersion() { return latestVersion; }
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate(const std::function<void(size_t, size_t)>& onProgress) {
|
||||||
|
if (!isUpdateNewer()) {
|
||||||
|
return UPDATE_OLDER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::unique_ptr<WiFiClientSecure> client(new WiFiClientSecure);
|
||||||
|
client->setInsecure();
|
||||||
|
HTTPClient http;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), otaUrl.c_str());
|
||||||
|
|
||||||
|
http.begin(*client, otaUrl.c_str());
|
||||||
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
const int httpCode = http.GET();
|
||||||
|
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Download failed: %d\n", millis(), httpCode);
|
||||||
|
http.end();
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get length and stream
|
||||||
|
const size_t contentLength = http.getSize();
|
||||||
|
|
||||||
|
if (contentLength != otaSize) {
|
||||||
|
Serial.printf("[%lu] [OTA] Invalid content length\n", millis());
|
||||||
|
http.end();
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Begin the ESP-IDF Update process
|
||||||
|
if (!Update.begin(otaSize)) {
|
||||||
|
Serial.printf("[%lu] [OTA] Not enough space. Error: %s\n", millis(), Update.errorString());
|
||||||
|
http.end();
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->totalSize = otaSize;
|
||||||
|
Serial.printf("[%lu] [OTA] Update started\n", millis());
|
||||||
|
Update.onProgress([this, onProgress](const size_t progress, const size_t total) {
|
||||||
|
this->processedSize = progress;
|
||||||
|
this->totalSize = total;
|
||||||
|
onProgress(progress, total);
|
||||||
|
});
|
||||||
|
const size_t written = Update.writeStream(*client);
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
if (written == otaSize) {
|
||||||
|
Serial.printf("[%lu] [OTA] Successfully written %u bytes\n", millis(), written);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [OTA] Written only %u/%u bytes. Error: %s\n", millis(), written, otaSize,
|
||||||
|
Update.errorString());
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Update.end() && Update.isFinished()) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update complete\n", millis());
|
||||||
|
return OK;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [OTA] Error Occurred: %s\n", millis(), Update.errorString());
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/network/OtaUpdater.h
Normal file
30
src/network/OtaUpdater.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class OtaUpdater {
|
||||||
|
bool updateAvailable = false;
|
||||||
|
std::string latestVersion;
|
||||||
|
std::string otaUrl;
|
||||||
|
size_t otaSize = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum OtaUpdaterError {
|
||||||
|
OK = 0,
|
||||||
|
NO_UPDATE,
|
||||||
|
HTTP_ERROR,
|
||||||
|
JSON_PARSE_ERROR,
|
||||||
|
UPDATE_OLDER_ERROR,
|
||||||
|
INTERNAL_UPDATE_ERROR,
|
||||||
|
OOM_ERROR,
|
||||||
|
};
|
||||||
|
size_t processedSize = 0;
|
||||||
|
size_t totalSize = 0;
|
||||||
|
|
||||||
|
OtaUpdater() = default;
|
||||||
|
bool isUpdateNewer();
|
||||||
|
const std::string& getLatestVersion();
|
||||||
|
OtaUpdaterError checkForUpdate();
|
||||||
|
OtaUpdaterError installUpdate(const std::function<void(size_t, size_t)>& onProgress);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user