From f009c1561d2af766f7a5234fda845c4d4733b2c8 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 19:09:25 -0600 Subject: [PATCH 01/25] Initial refactor --- include/HTTP_Server_Basic.h | 43 +- include/http/HTTPCore.h | 44 ++ include/http/HTTPFileSystem.h | 57 ++ include/http/HTTPFirmware.h | 66 +++ include/http/HTTPRoutes.h | 69 +++ include/http/HTTPSettings.h | 40 ++ include/settings.h | 12 + include/telegram/TelegramManager.h | 49 ++ include/wifi/WiFiManager.h | 34 ++ src/HTTP_Server_Basic.cpp | 818 +---------------------------- src/http/HTTPCore.cpp | 73 +++ src/http/HTTPFileSystem.cpp | 189 +++++++ src/http/HTTPFirmware.cpp | 221 ++++++++ src/http/HTTPRoutes.cpp | 304 +++++++++++ src/http/HTTPSettings.cpp | 285 ++++++++++ src/wifi/WiFiManager.cpp | 131 +++++ 16 files changed, 1594 insertions(+), 841 deletions(-) create mode 100644 include/http/HTTPCore.h create mode 100644 include/http/HTTPFileSystem.h create mode 100644 include/http/HTTPFirmware.h create mode 100644 include/http/HTTPRoutes.h create mode 100644 include/http/HTTPSettings.h create mode 100644 include/telegram/TelegramManager.h create mode 100644 include/wifi/WiFiManager.h create mode 100644 src/http/HTTPCore.cpp create mode 100644 src/http/HTTPFileSystem.cpp create mode 100644 src/http/HTTPFirmware.cpp create mode 100644 src/http/HTTPRoutes.cpp create mode 100644 src/http/HTTPSettings.cpp create mode 100644 src/wifi/WiFiManager.cpp diff --git a/include/HTTP_Server_Basic.h b/include/HTTP_Server_Basic.h index d2c3b1df..cb899996 100644 --- a/include/HTTP_Server_Basic.h +++ b/include/HTTP_Server_Basic.h @@ -8,40 +8,13 @@ #pragma once #include - -#define HTTP_SERVER_LOG_TAG "HTTP_Server" - -class HTTP_Server { - private: - public: - bool internetConnection; - - void start(); - void stop(); - static void handleBTScanner(); - static void handleLittleFSFile(); - static void handleIndexFile(); - static void settingsProcessor(); - static void handleHrSlider(); - static void FirmwareUpdate(); - - static void webClientUpdate(); - - HTTP_Server() { internetConnection = false; } -}; - -#ifdef USE_TELEGRAM -#define SEND_TO_TELEGRAM(message) sendTelegram(message); - -void sendTelegram(String textToSend); -void telegramUpdate(void *pvParameters); -#else -#define SEND_TO_TELEGRAM(message) (void)message -#endif - -// wifi Function -void startWifi(); -void stopWifi(); - +#include "http/HTTPCore.h" +#include "http/HTTPRoutes.h" +#include "http/HTTPFileSystem.h" +#include "http/HTTPSettings.h" +#include "http/HTTPFirmware.h" + +// For backward compatibility, use HTTPCore as HTTP_Server +using HTTP_Server = HTTPCore; extern HTTP_Server httpServer; diff --git a/include/http/HTTPCore.h b/include/http/HTTPCore.h new file mode 100644 index 00000000..19fafd73 --- /dev/null +++ b/include/http/HTTPCore.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include "http/HTTPRoutes.h" +#include "http/HTTPFileSystem.h" +#include "http/HTTPSettings.h" +#include "http/HTTPFirmware.h" +#include "wifi/WiFiManager.h" + +#define HTTP_SERVER_LOG_TAG "HTTP_Server" +#define WEBSERVER_DELAY 10 // ms + +class HTTPCore { +public: + HTTPCore(); + void start(); + void stop(); + void update(); + bool hasInternetConnection() const; + void setInternetConnection(bool connected); + WebServer& getServer(); + +private: + bool internetConnection; + WebServer server; + unsigned long lastUpdateTime; + unsigned long lastMDNSUpdate; + + void setupRoutes(); + void updateMDNS(); + static const unsigned long MDNS_UPDATE_INTERVAL = 30000; // 30 seconds +}; + +extern HTTPCore httpServer; diff --git a/include/http/HTTPFileSystem.h b/include/http/HTTPFileSystem.h new file mode 100644 index 00000000..9ef21dfa --- /dev/null +++ b/include/http/HTTPFileSystem.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include +#include + +#define HTTP_SERVER_LOG_TAG "HTTP_Server" + +class HTTPFileSystem { +public: + static bool initialize(); + static void handleFileRead(WebServer& server, const String& path, const String& contentType = ""); + static void handleFileUpload(WebServer& server); + static void handleFileDelete(WebServer& server); + static void handleFileList(WebServer& server); + + // File upload status tracking + static bool isUploadInProgress() { return uploadInProgress; } + static size_t getUploadProgress() { return uploadProgress; } + static size_t getUploadTotal() { return uploadTotal; } + +private: + static String getContentType(const String& filename); + static bool exists(const String& path); + static bool isDirectory(const String& path); + static File openFile(const String& path, const char* mode); + + // File upload handling + static File fsUploadFile; + static bool uploadInProgress; + static size_t uploadProgress; + static size_t uploadTotal; + + static void beginFileUpload(const String& filename); + static void continueFileUpload(uint8_t* data, size_t len); + static void endFileUpload(); + + // Constants + static const char* const TEXT_PLAIN; + static const char* const TEXT_HTML; + static const char* const TEXT_CSS; + static const char* const TEXT_JAVASCRIPT; + static const char* const TEXT_JSON; + static const char* const IMAGE_PNG; + static const char* const IMAGE_JPG; + static const char* const IMAGE_GIF; + static const char* const IMAGE_ICO; + static const char* const APPLICATION_OCTET_STREAM; +}; diff --git a/include/http/HTTPFirmware.h b/include/http/HTTPFirmware.h new file mode 100644 index 00000000..a91bbb16 --- /dev/null +++ b/include/http/HTTPFirmware.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "settings.h" +#include "http/HTTPCore.h" + +#define HTTP_SERVER_LOG_TAG "HTTP_Server" + +// Forward declaration of Version class +class Version { +public: + Version(const char* version); + bool operator>(const Version& other) const; +private: + int major; + int minor; + int patch; + void parseVersion(const char* version); +}; + +class HTTPFirmware { +public: + static void checkForUpdates(); + static void handleOTAUpdate(WebServer& server); + +private: + // Update components + static void updateLittleFS(); + static void updateFirmware(); + static bool downloadFile(const String& url, const String& filename); + + // Version management + static bool needsUpdate(const String& currentVersion, const String& availableVersion); + static bool validateVersion(const String& version); + + // Security + static void setupSecureClient(WiFiClientSecure& client); + static const char* getRootCACertificate(); + + // Update handlers + static void handleFirmwareUpdate(WebServer& server); + static void handleFileSystemUpdate(WebServer& server); + static void handleUpdateProgress(size_t progress, size_t total); + + // Error handling + static void handleUpdateError(int error); + static String getUpdateErrorString(int error); + + // File system operations + static bool downloadFileList(); + static bool processFileList(const String& fileListContent); + static bool downloadAndSaveFile(const String& filename); +}; diff --git a/include/http/HTTPRoutes.h b/include/http/HTTPRoutes.h new file mode 100644 index 00000000..38c709e6 --- /dev/null +++ b/include/http/HTTPRoutes.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include "Builtin_Pages.h" + +class HTTPRoutes { +public: + using HandlerFunction = std::function; + + // Static handler functions that can be used directly with WebServer + static HandlerFunction handleIndexFile; + static HandlerFunction handleBTScanner; + static HandlerFunction handleLittleFSFile; + static HandlerFunction handleConfigJSON; + static HandlerFunction handleRuntimeConfigJSON; + static HandlerFunction handlePWCJSON; + static HandlerFunction handleShift; + static HandlerFunction handleHRSlider; + static HandlerFunction handleWattsSlider; + static HandlerFunction handleCadSlider; + static HandlerFunction handleERGMode; + static HandlerFunction handleTargetWattsSlider; + static HandlerFunction handleLogin; + static HandlerFunction handleOTAUpdate; + static HandlerFunction handleFileUpload; + + // Setup function to register all routes + static void setupRoutes(WebServer& server); + + // Initialize handlers with server reference + static void initialize(WebServer& server); + +private: + static void setupDefaultRoutes(WebServer& server); + static void setupFileRoutes(WebServer& server); + static void setupControlRoutes(WebServer& server); + static void setupUpdateRoutes(WebServer& server); + + // Store WebServer reference for handlers + static WebServer* currentServer; + static File fsUploadFile; + + // Actual handler implementations + static void _handleIndexFile(); + static void _handleBTScanner(); + static void _handleLittleFSFile(); + static void _handleConfigJSON(); + static void _handleRuntimeConfigJSON(); + static void _handlePWCJSON(); + static void _handleShift(); + static void _handleHRSlider(); + static void _handleWattsSlider(); + static void _handleCadSlider(); + static void _handleERGMode(); + static void _handleTargetWattsSlider(); + static void _handleLogin(); + static void _handleOTAUpdate(); + static void _handleFileUpload(); +}; diff --git a/include/http/HTTPSettings.h b/include/http/HTTPSettings.h new file mode 100644 index 00000000..847c80bd --- /dev/null +++ b/include/http/HTTPSettings.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include "settings.h" + +#define HTTP_SERVER_LOG_TAG "HTTP_Server" + +class HTTPSettings { +public: + static void processSettings(WebServer& server); + +private: + // Network settings + static void processNetworkSettings(WebServer& server); + static void processDeviceSettings(WebServer& server); + static void processStepperSettings(WebServer& server); + static void processPowerSettings(WebServer& server); + static void processERGSettings(WebServer& server); + static void processFeatureSettings(WebServer& server); + static bool processBluetoothSettings(WebServer& server); // Changed return type to bool + static void processPWCSettings(WebServer& server); + + // Helper functions + static bool processCheckbox(WebServer& server, const char* name, bool defaultValue = false); + static float processFloatValue(WebServer& server, const char* name, float min, float max); + static int processIntValue(WebServer& server, const char* name, int min, int max); + static String processStringValue(WebServer& server, const char* name); + + // Response handling + static void sendSettingsResponse(WebServer& server, bool wasBTUpdate, bool wasSettingsUpdate, bool requiresReboot = false); + static String buildRedirectResponse(const String& message, const String& page, int delay = 1000); +}; diff --git a/include/settings.h b/include/settings.h index 2a66d45a..d9e4c8ec 100644 --- a/include/settings.h +++ b/include/settings.h @@ -58,9 +58,21 @@ const char* const DEFAULT_PASSWORD = "password"; // Stepper peak current in ma. This is hardware restricted to a maximum of 2000ma on the TMC2225. RMS current is less. #define DEFAULT_STEPPER_POWER 900 +// Minimum stepper power setting +#define MIN_STEPPER_POWER 100 + +// Maximum stepper power setting +#define MAX_STEPPER_POWER 2000 + // Default Shift Step. The amount to move the stepper motor for a shift press. #define DEFAULT_SHIFT_STEP 1200 +// Minimum shift step setting +#define MIN_SHIFT_STEP 10 + +// Maximum shift step setting +#define MAX_SHIFT_STEP 6000 + // Stepper Acceleration in steps/s^2 #define STEPPER_ACCELERATION 3000 diff --git a/include/telegram/TelegramManager.h b/include/telegram/TelegramManager.h new file mode 100644 index 00000000..ddfb3c04 --- /dev/null +++ b/include/telegram/TelegramManager.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include + +#ifdef USE_TELEGRAM +#include +#include + +class TelegramManager { +public: + static void initialize(WiFiClientSecure& client); + static void sendMessage(const String& message); + static void update(); + static void stop(); + +private: + static TaskHandle_t telegramTask; + static bool messageWaiting; + static String pendingMessage; + static UniversalTelegramBot* bot; + static int failureCount; + + // Task management + static void telegramUpdateTask(void* pvParameters); + static void resetFailureCount(); + + // Message handling + static void processPendingMessage(); + static bool isMessageRateLimited(); + static void clearPendingMessage(); + + // Constants + static const int MAX_FAILURES = 3; + static const int MAX_MESSAGES = 5; + static const unsigned long MESSAGE_TIMEOUT = 120000; // 2 minutes +}; + +#define SEND_TO_TELEGRAM(message) TelegramManager::sendMessage(message) + +#else +#define SEND_TO_TELEGRAM(message) (void)message +#endif // USE_TELEGRAM diff --git a/include/wifi/WiFiManager.h b/include/wifi/WiFiManager.h new file mode 100644 index 00000000..34b6be8d --- /dev/null +++ b/include/wifi/WiFiManager.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include +#include + +class WiFiManager { +public: + static void startWifi(); + static void stopWifi(); + static IPAddress getIP(); + static bool isConnected(); + static void processDNS(); + +private: + static void setupStationMode(); + static void setupAPMode(); + static void setupMDNS(); + static void syncClock(); + + static const byte DNS_PORT = 53; + static const int WIFI_CONNECT_TIMEOUT = 20; // seconds + + static DNSServer dnsServer; + static IPAddress myIP; +}; diff --git a/src/HTTP_Server_Basic.cpp b/src/HTTP_Server_Basic.cpp index 180d7862..d78f01f4 100644 --- a/src/HTTP_Server_Basic.cpp +++ b/src/HTTP_Server_Basic.cpp @@ -5,814 +5,20 @@ * SPDX-License-Identifier: GPL-2.0-only */ -#include "Main.h" -#include "Version_Converter.h" -#include "Builtin_Pages.h" #include "HTTP_Server_Basic.h" -#include "cert.h" +#include "Main.h" #include "SS2KLog.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -File fsUploadFile; - -IPAddress myIP; - -// DNS server -const byte DNS_PORT = 53; -DNSServer dnsServer; -HTTP_Server httpServer; -WiFiClientSecure client; -WebServer server(80); - -#ifdef USE_TELEGRAM -#include -TaskHandle_t telegramTask; -bool telegramMessageWaiting = false; -UniversalTelegramBot bot(TELEGRAM_TOKEN, client); -String telegramMessage = ""; -#endif // USE_TELEGRAM - -void _staSetup() { - WiFi.setHostname(userConfig->getDeviceName()); - WiFi.mode(WIFI_STA); - WiFi.begin(userConfig->getSsid(), userConfig->getPassword()); - WiFi.setAutoReconnect(true); -} - -void _APSetup() { - // WiFi.eraseAP(); //Needed if we switch back to espressif32 @6.5.0 - WiFi.mode(WIFI_AP); - WiFi.setHostname("reset"); // Fixes a bug when switching Arduino Core Versions - WiFi.softAPsetHostname("reset"); - WiFi.setHostname(userConfig->getDeviceName()); - WiFi.softAPsetHostname(userConfig->getDeviceName()); - WiFi.enableAP(true); - vTaskDelay(500); // Micro controller requires some time to reset the mode -} - -// ********************************WIFI Setup************************* -void startWifi() { - int i = 0; - - // Trying Station mode first: - if (strcmp(userConfig->getSsid(), DEVICE_NAME) != 0) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connecting to: %s", userConfig->getSsid()); - _staSetup(); - while (WiFi.status() != WL_CONNECTED) { - vTaskDelay(1000 / portTICK_RATE_MS); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for connection to be established..."); - i++; - if (i > WIFI_CONNECT_TIMEOUT) { - i = 0; - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Couldn't Connect. Switching to AP mode"); - WiFi.disconnect(true, true); - WiFi.setAutoReconnect(false); - WiFi.mode(WIFI_MODE_NULL); - vTaskDelay(1000 / portTICK_RATE_MS); - break; - } - } - } - - // Did we connect in STA mode? - if (WiFi.status() == WL_CONNECTED) { - myIP = WiFi.localIP(); - httpServer.internetConnection = true; - } - - // Couldn't connect to existing network, Create SoftAP - if (WiFi.status() != WL_CONNECTED) { - _APSetup(); - if (strcmp(userConfig->getSsid(), DEVICE_NAME) == 0) { - // If default SSID is still in use, let the user select a new password. - // Else fall back to the default password. - WiFi.softAP(userConfig->getDeviceName(), userConfig->getPassword()); - } else { - WiFi.softAP(userConfig->getDeviceName(), DEFAULT_PASSWORD); - } - vTaskDelay(50); - myIP = WiFi.softAPIP(); - /* Setup the DNS server redirecting all the domains to the apIP */ - dnsServer.setErrorReplyCode(DNSReplyCode::NoError); - dnsServer.start(DNS_PORT, "*", myIP); - } - - if (!MDNS.begin(userConfig->getDeviceName())) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Error setting up MDNS responder!"); - } - - MDNS.addService("http", "_tcp", 80); - MDNS.addServiceTxt("http", "_tcp", "lf", "0"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connected to %s IP address: %s", userConfig->getSsid(), myIP.toString().c_str()); -#ifdef USE_TELEGRAM - SEND_TO_TELEGRAM("Connected to " + String(userConfig->getSsid()) + " IP address: " + myIP.toString()); -#endif - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Open http://%s.local/", userConfig->getDeviceName()); - WiFi.setTxPower(WIFI_POWER_19_5dBm); - - if (WiFi.getMode() == WIFI_STA) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Syncing clock..."); - configTime(0, 0, "pool.ntp.org"); // get UTC time via NTP - time_t now = time(nullptr); - while (now < 10) { // wait 10 seconds - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for clock sync..."); - delay(100); - now = time(nullptr); - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Clock synced to: %.f", difftime(now, (time_t)0)); - } -} - -void stopWifi() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Closing connection to: %s", userConfig->getSsid()); - WiFi.disconnect(); -} - -void HTTP_Server::start() { - server.enableCORS(true); - server.onNotFound(handleIndexFile); - - /***************************Begin Handlers*******************/ - server.on("/", handleIndexFile); - server.on("/index.html", handleIndexFile); - server.on("/generate_204", handleIndexFile); // Android captive portal - server.on("/fwlink", handleIndexFile); // Microsoft captive portal - server.on("/hotspot-detect.html", handleIndexFile); // Apple captive portal - server.on("/style.css", handleLittleFSFile); - server.on("/btsimulator.html", handleLittleFSFile); - server.on("/develop.html", handleLittleFSFile); - server.on("/shift.html", handleLittleFSFile); - server.on("/settings.html", handleLittleFSFile); - server.on("/status.html", handleLittleFSFile); - server.on("/bluetoothscanner.html", handleBTScanner); - server.on("/streamfit.html", handleLittleFSFile); - server.on("/hrtowatts.html", handleLittleFSFile); - server.on("/favicon.ico", handleLittleFSFile); - server.on("/send_settings", settingsProcessor); - server.on("/jquery.js.gz", handleLittleFSFile); - - server.on("/BLEScan", []() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Scanning from web request"); - String response = - "Scanning for BLE Devices. Please wait " - "15 seconds."; - // spinBLEClient.resetDevices(); - spinBLEClient.dontBlockScan = true; - spinBLEClient.doScan = true; - server.send(200, "text/html", response); - }); - - server.on("/load_defaults.html", []() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Setting Defaults from Web Request"); - ss2k->resetDefaultsFlag = true; - String response = - "

Defaults have been " - "loaded.



Please reconnect to the device on WiFi " - "network: " + - myIP.toString() + "

"; - server.send(200, "text/html", response); - }); - - server.on("/reboot.html", []() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Rebooting from Web Request"); - String response = "Rebooting...."; - server.send(200, "text/html", response); - ss2k->rebootFlag = true; - }); - - server.on("/hrslider", []() { - String value = server.arg("value"); - if (value == "enable") { - rtConfig->hr.setSimulate(true); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned on"); - } else if (value == "disable") { - rtConfig->hr.setSimulate(false); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned off"); - } else { - rtConfig->hr.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR is now: %d", rtConfig->hr.getValue()); - server.send(200, "text/plain", "OK"); - } - }); - - server.on("/wattsslider", []() { - String value = server.arg("value"); - if (value == "enable") { - rtConfig->watts.setSimulate(true); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned on"); - } else if (value == "disable") { - rtConfig->watts.setSimulate(false); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned off"); - } else { - rtConfig->watts.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watts are now: %d", rtConfig->watts.getValue()); - server.send(200, "text/plain", "OK"); - } - }); - - server.on("/cadslider", []() { - String value = server.arg("value"); - if (value == "enable") { - rtConfig->cad.setSimulate(true); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned on"); - } else if (value == "disable") { - rtConfig->cad.setSimulate(false); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned off"); - } else { - rtConfig->cad.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD is now: %d", rtConfig->cad.getValue()); - server.send(200, "text/plain", "OK"); - } - }); - - server.on("/ergmode", []() { - String value = server.arg("value"); - if (value == "enable") { - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned on"); - } else { - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::RequestControl); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned off"); - } - }); - - server.on("/targetwattsslider", []() { - String value = server.arg("value"); - if (value == "enable") { - rtConfig->setSimTargetWatts(true); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned on"); - } else if (value == "disable") { - rtConfig->setSimTargetWatts(false); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned off"); - } else { - rtConfig->watts.setTarget(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts are now: %d", rtConfig->watts.getTarget()); - server.send(200, "text/plain", "OK"); - } - }); - - server.on("/shift", []() { - int value = server.arg("value").toInt(); - if ((value > -10) && (value < 10)) { - rtConfig->setShifterPosition(rtConfig->getShifterPosition() + value); - server.send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Shift From HTML"); - } else { - rtConfig->setShifterPosition(value); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Invalid HTML Shift"); - server.send(200, "text/plain", "OK"); - } - // BLE Shift notifications are handles by the shift processing in main.cpp - }); - - server.on("/configJSON", []() { - String tString; - tString = userConfig->returnJSON(); - server.send(200, "text/plain", tString); - }); - - server.on("/runtimeConfigJSON", []() { - String tString; - tString = rtConfig->returnJSON(); - server.send(200, "text/plain", tString); - }); - - server.on("/PWCJSON", []() { - String tString; - tString = userPWC->returnJSON(); - server.send(200, "text/plain", tString); - }); - - server.on("/login", HTTP_GET, []() { - server.sendHeader("Connection", "close"); - server.send(200, "text/html", OTALoginIndex); - }); - - server.on("/OTAIndex", HTTP_GET, []() { - ss2k->stopTasks(); - server.sendHeader("Connection", "close"); - server.send(200, "text/html", OTAServerIndex); - }); - - /*handling uploading firmware file */ - server.on( - "/update", HTTP_POST, - []() { - server.sendHeader("Connection", "close"); - server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - }, - []() { - HTTPUpload &upload = server.upload(); - if (upload.filename == String("firmware.bin").c_str()) { - if (upload.status == UPLOAD_FILE_START) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update: %s", upload.filename.c_str()); - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { // start with max - // available size - Update.printError(Serial); - } - } else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing firmware to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - Update.printError(Serial); - } - } else if (upload.status == UPLOAD_FILE_END) { - if (Update.end(true)) { // true to set the size to the - // current progress - server.send(200, "text/plain", "Firmware Uploaded Successfully. Rebooting..."); - ESP.restart(); - } else { - Update.printError(Serial); - } - } - } else if (upload.filename == String("littlefs.bin").c_str()) { - if (upload.status == UPLOAD_FILE_START) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update: %s", upload.filename.c_str()); - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { // start with max - // available size - Update.printError(Serial); - } - } else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing firmware to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - Update.printError(Serial); - } - } else if (upload.status == UPLOAD_FILE_END) { - if (Update.end(true)) { // true to set the size to the - // current progress - server.send(200, "text/plain", "Littlefs Uploaded Successfully. Rebooting..."); - userConfig->saveToLittleFS(); - userPWC->saveToLittleFS(); - ss2k->rebootFlag == true; - } else { - Update.printError(Serial); - } - } - } else { - if (upload.status == UPLOAD_FILE_START) { - String filename = upload.filename; - if (!filename.startsWith("/")) { - filename = "/" + filename; - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Name: %s", filename.c_str()); - fsUploadFile = LittleFS.open(filename, "w"); - filename = String(); - } else if (upload.status == UPLOAD_FILE_WRITE) { - if (fsUploadFile) { - fsUploadFile.write(upload.buf, upload.currentSize); - } - } else if (upload.status == UPLOAD_FILE_END) { - if (fsUploadFile) { - fsUploadFile.close(); - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Size: %zu", upload.totalSize); - server.send(200, "text/plain", String(upload.filename + " Uploaded Successfully.")); - } - } - }); - - /********************************************End Server - * Handlers*******************************/ - -#ifdef USE_TELEGRAM - xTaskCreatePinnedToCore(telegramUpdate, /* Task function. */ - "telegramUpdate", /* name of task. */ - 4900, /* Stack size of task*/ - NULL, /* parameter of the task */ - 1, /* priority of the task - higher number is higher priority*/ - &telegramTask, /* Task handle to keep track of created task */ - 1); /* pin task to core 1 */ -#endif // USE_TELEGRAM - server.begin(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP server started"); -} - -void HTTP_Server::webClientUpdate() { - static unsigned long int _webClientTimer = millis(); - if (millis() - _webClientTimer > WEBSERVER_DELAY) { - _webClientTimer = millis(); - static unsigned long mDnsTimer = millis(); // NOLINT: There is no overload in String for uint64_t - server.handleClient(); - if (WiFi.getMode() != WIFI_MODE_STA) { - dnsServer.processNextRequest(); - } - // Keep MDNS alive - if ((millis() - mDnsTimer) > 30000) { - MDNS.addServiceTxt("http", "_tcp", "lf", String(mDnsTimer)); - mDnsTimer = millis(); - } - } -} - -void HTTP_Server::handleBTScanner() { - spinBLEClient.doScan = true; - handleLittleFSFile(); -} - -void HTTP_Server::handleIndexFile() { - String filename = "/index.html"; - if (LittleFS.exists(filename)) { - File file = LittleFS.open(filename, FILE_READ); - server.streamFile(file, "text/html"); - file.close(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending builtin Index.html", filename.c_str()); - server.send(200, "text/html", noIndexHTML); - } -} - -void HTTP_Server::handleLittleFSFile() { - String filename = server.uri(); - int dotPosition = filename.lastIndexOf("."); - String fileType = filename.substring((dotPosition + 1), filename.length()); - if (LittleFS.exists(filename)) { - File file = LittleFS.open(filename, FILE_READ); - if (fileType == "gz") { - fileType = "html"; // no need to change content type as it's done automatically by .streamfile below VV - } - server.streamFile(file, "text/" + fileType); - file.close(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); - } else if (!LittleFS.exists("/index.html")) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found and no filesystem. Sending builtin index.html", filename.c_str()); - handleIndexFile(); - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending 404.", filename.c_str()); - String outputhtml = "

ERROR 404
FILE NOT FOUND!" + filename + "

"; - server.send(404, "text/html", outputhtml); - } -} - -void HTTP_Server::settingsProcessor() { - String tString; - bool wasBTUpdate = false; - bool wasSettingsUpdate = false; - bool reboot = false; - if (!server.arg("ssid").isEmpty()) { - tString = server.arg("ssid"); - tString.trim(); - userConfig->setSsid(tString); - } - if (!server.arg("password").isEmpty()) { - tString = server.arg("password"); - tString.trim(); - userConfig->setPassword(tString); - } - if (!server.arg("deviceName").isEmpty()) { - tString = server.arg("deviceName"); - tString.trim(); - userConfig->setDeviceName(tString); - } - if (!server.arg("shiftStep").isEmpty()) { - uint64_t shiftStep = server.arg("shiftStep").toInt(); - if (shiftStep >= 10 && shiftStep <= 6000) { - userConfig->setShiftStep(shiftStep); - } - wasSettingsUpdate = true; - } - if (!server.arg("stepperPower").isEmpty()) { - uint64_t stepperPower = server.arg("stepperPower").toInt(); - if (stepperPower >= 100 && stepperPower <= 2000) { - userConfig->setStepperPower(stepperPower); - ss2k->updateStepperPower(); - } - } - if (!server.arg("maxWatts").isEmpty()) { - uint64_t maxWatts = server.arg("maxWatts").toInt(); - if (maxWatts >= 0 && maxWatts <= 2000) { - userConfig->setMaxWatts(maxWatts); - } - } - if (!server.arg("minWatts").isEmpty()) { - uint64_t minWatts = server.arg("minWatts").toInt(); - if (minWatts >= 0 && minWatts <= 200) { - userConfig->setMinWatts(minWatts); - } - } - if (!server.arg("ERGSensitivity").isEmpty()) { - float ERGSensitivity = server.arg("ERGSensitivity").toFloat(); - if (ERGSensitivity >= .1 && ERGSensitivity <= 20) { - userConfig->setERGSensitivity(ERGSensitivity); - } - } - // checkboxes don't report off, so need to check using another parameter - // that's always present on that page - if (!server.arg("autoUpdate").isEmpty()) { - userConfig->setAutoUpdate(true); - } else if (wasSettingsUpdate) { - userConfig->setAutoUpdate(false); - } - if (!server.arg("stepperDir").isEmpty()) { - userConfig->setStepperDir(true); - } else if (wasSettingsUpdate) { - userConfig->setStepperDir(false); - } - if (!server.arg("shifterDir").isEmpty()) { - userConfig->setShifterDir(true); - } else if (wasSettingsUpdate) { - userConfig->setShifterDir(false); - } - if (!server.arg("udpLogEnabled").isEmpty()) { - userConfig->setUdpLogEnabled(true); - } else if (wasSettingsUpdate) { - userConfig->setUdpLogEnabled(false); - } - if (!server.arg("stealthChop").isEmpty()) { - userConfig->setStealthChop(true); - ss2k->updateStealthChop(); - } else if (wasSettingsUpdate) { - userConfig->setStealthChop(false); - ss2k->updateStealthChop(); - } - if (!server.arg("inclineMultiplier").isEmpty()) { - float inclineMultiplier = server.arg("inclineMultiplier").toFloat(); - if (inclineMultiplier >= 0 && inclineMultiplier <= 10) { - userConfig->setInclineMultiplier(inclineMultiplier); - } - } - if (!server.arg("powerCorrectionFactor").isEmpty()) { - float powerCorrectionFactor = server.arg("powerCorrectionFactor").toFloat(); - if (powerCorrectionFactor >= MIN_PCF && powerCorrectionFactor <= MAX_PCF) { - userConfig->setPowerCorrectionFactor(powerCorrectionFactor); - } - } - if (!server.arg("blePMDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("blePMDropdown")) { - tString = server.arg("blePMDropdown"); - if (tString != userConfig->getConnectedPowerMeter()) { - userConfig->setConnectedPowerMeter(tString); - spinBLEClient.reconnectAllDevices(); - } - } else { - userConfig->setConnectedPowerMeter("any"); - } - } - if (!server.arg("bleHRDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("bleHRDropdown")) { - bool reset = false; - tString = server.arg("bleHRDropdown"); - if (tString != userConfig->getConnectedHeartMonitor()) { - spinBLEClient.reconnectAllDevices(); - } - userConfig->setConnectedHeartMonitor(server.arg("bleHRDropdown")); - } else { - userConfig->setConnectedHeartMonitor("any"); - } - } - if (!server.arg("bleRemoteDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("bleRemoteDropdown")) { - bool reset = false; - tString = server.arg("bleRemoteDropdown"); - if (tString != userConfig->getConnectedRemote()) { - spinBLEClient.reconnectAllDevices(); - } - userConfig->setConnectedRemote(server.arg("bleRemoteDropdown")); - } else { - userConfig->setConnectedRemote("any"); - } - } - if (!server.arg("session1HR").isEmpty()) { // Needs checking for unrealistic numbers. - userPWC->session1HR = server.arg("session1HR").toInt(); - } - if (!server.arg("session1Pwr").isEmpty()) { - userPWC->session1Pwr = server.arg("session1Pwr").toInt(); - } - if (!server.arg("session2HR").isEmpty()) { - userPWC->session2HR = server.arg("session2HR").toInt(); - } - if (!server.arg("session2Pwr").isEmpty()) { - userPWC->session2Pwr = server.arg("session2Pwr").toInt(); - - if (!server.arg("hr2Pwr").isEmpty()) { - userPWC->hr2Pwr = true; - } else { - userPWC->hr2Pwr = false; - } - } - String response = "

"; - - if (wasBTUpdate) { // Special BT page update response - response += - "Selections Saved!

"; - } else if (wasSettingsUpdate) { // Special Settings Page update response - response += - "Network settings will be applied at next reboot.
Everything " - "else is available immediately."; - } else { // Normal response - response += - "Network settings will be applied at next reboot.
Everything " - "else is available immediately."; - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Config Updated From Web"); - ss2k->saveFlag = true; - if (reboot) { - response += - "Please wait while your settings are saved and SmartSpin2k reboots."; - server.send(200, "text/html", response); - ss2k->rebootFlag = true; - } - server.send(200, "text/html", response); -} - -void HTTP_Server::stop() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Stopping Http Server"); - server.stop(); - server.close(); -} - -// github fingerprint -// 70:94:DE:DD:E6:C4:69:48:3A:92:70:A1:48:56:78:2D:18:64:E0:B7 - -void HTTP_Server::FirmwareUpdate() { - HTTPClient http; - // WiFiClientSecure client; - client.setCACert(rootCACertificate); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Checking for newer firmware:"); - http.begin(userConfig->getFirmwareUpdateURL() + String(FW_VERSIONFILE), - rootCACertificate); // check version URL - delay(100); - int httpCode = http.GET(); // get data from version file - delay(100); - String payload; - if (httpCode == HTTP_CODE_OK) { // if version received - payload = http.getString(); // save received version - payload.trim(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, " - Server version: %s", payload.c_str()); - httpServer.internetConnection = true; - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "error downloading %s %d", FW_VERSIONFILE, httpCode); - httpServer.internetConnection = false; - } - - http.end(); - if (httpCode == HTTP_CODE_OK) { // if version received - bool updateAnyway = false; - if (!LittleFS.exists("/index.html")) { - // updateAnyway = true; - SS2K_LOG(HTTP_SERVER_LOG_TAG, " -index.html not found."); - } - Version availableVer(payload.c_str()); - Version currentVer(FIRMWARE_VERSION); - - if (((availableVer > currentVer) && (userConfig->getAutoUpdate())) || (!LittleFS.exists("/index.html"))) { - //////////////// Update LittleFS////////////// - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Updating FileSystem"); - http.begin(DATA_UPDATEURL + String(DATA_FILELIST), - rootCACertificate); // check version URL - vTaskDelay(100 / portTICK_PERIOD_MS); - httpCode = http.GET(); // get data from version file - vTaskDelay(100 / portTICK_PERIOD_MS); - StaticJsonDocument<500> doc; - if (httpCode == HTTP_CODE_OK) { // if version received - String payload; - payload = http.getString(); // save received version - payload.trim(); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, payload); - if (error) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to read file list"); - return; - } - httpServer.internetConnection = true; - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "error downloading %s %d", DATA_FILELIST, httpCode); - httpServer.internetConnection = false; - } - JsonArray files = doc.as(); - // iterate through file list and download files individually - for (JsonVariant v : files) { - String fileName = "/" + v.as(); - http.begin(DATA_UPDATEURL + fileName, - rootCACertificate); // check version URL - vTaskDelay(100 / portTICK_PERIOD_MS); - httpCode = http.GET(); - vTaskDelay(100 / portTICK_PERIOD_MS); - if (httpCode == HTTP_CODE_OK) { - String payload; - payload = http.getString(); - payload.trim(); - LittleFS.remove(fileName); - File file = LittleFS.open(fileName, FILE_WRITE, true); - if (!file) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to create file, %s", fileName); - return; - } - file.print(payload); - file.close(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Created: %s", fileName); - httpServer.internetConnection = true; - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Error downloading %s %d", fileName, httpCode); - httpServer.internetConnection = false; - } - } - - //////// Update Firmware ///////// - if (((availableVer > currentVer) || updateAnyway) && (userConfig->getAutoUpdate())) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "New firmware detected!"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Upgrading from %s to %s", FIRMWARE_VERSION, payload.c_str()); - t_httpUpdate_return ret = httpUpdate.update(client, userConfig->getFirmwareUpdateURL() + String(FW_BINFILE)); - switch (ret) { - case HTTP_UPDATE_FAILED: - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_FAILED Error %d : %s", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str()); - break; - - case HTTP_UPDATE_NO_UPDATES: - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_NO_UPDATES"); - break; - - case HTTP_UPDATE_OK: - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_OK"); - break; - } - } - } else { // don't update - SS2K_LOG(HTTP_SERVER_LOG_TAG, " - Current Version: %s", FIRMWARE_VERSION); - } - } -} - -#ifdef USE_TELEGRAM -// Function to handle sending telegram text to the non blocking task -void sendTelegram(String textToSend) { - static int numberOfMessages = 0; - static uint64_t timeout = 120000; // reset every two minutes - static uint64_t startTime = millis(); - - if (millis() - startTime > timeout) { // Let one message send every two minutes - numberOfMessages = MAX_TELEGRAM_MESSAGES - 1; - telegramMessage += " " + String(userConfig->getSsid()) + " "; - startTime = millis(); - } +#include "wifi/WiFiManager.h" - if ((numberOfMessages < MAX_TELEGRAM_MESSAGES) && (WiFi.getMode() == WIFI_STA)) { - telegramMessage += "\n" + textToSend; - telegramMessageWaiting = true; - numberOfMessages++; - } -} +// Initialize the global instance (already declared in HTTPCore.cpp) +// extern HTTP_Server httpServer; -// Non blocking task to send telegram message -void telegramUpdate(void *pvParameters) { - // client.setInsecure(); - client.setCACert(TELEGRAM_CERTIFICATE_ROOT); - for (;;) { - static int telegramFailures = 0; - if (telegramMessageWaiting && internetConnection) { - telegramMessageWaiting = false; - bool rm = (bot.sendMessage(TELEGRAM_CHAT_ID, telegramMessage, "")); - if (!rm) { - telegramFailures++; - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Telegram failed to send! %s", TELEGRAM_CHAT_ID); - if (telegramFailures > 2) { - internetConnection = false; - } - } else { // Success - reset Telegram Failures - telegramFailures = 0; - } +// The original HTTP_Server_Basic.cpp functionality has been refactored into: +// - HTTPCore: Core server functionality +// - HTTPRoutes: Route handlers +// - HTTPFileSystem: File system operations +// - HTTPSettings: Settings processing +// - HTTPFirmware: Firmware updates +// - WiFiManager: WiFi connection management - client.stop(); - telegramMessage = ""; - } -#ifdef DEBUG_STACK - Serial.printf("Telegram: %d \n", uxTaskGetStackHighWaterMark(telegramTask)); - Serial.printf("Web: %d \n", uxTaskGetStackHighWaterMark(webClientTask)); - Serial.printf("Free: %d \n", ESP.getFreeHeap()); -#endif // DEBUG_STACK - vTaskDelay(4000 / portTICK_RATE_MS); - } -} -#endif // USE_TELEGRAM +// This file remains for backward compatibility and initialization diff --git a/src/http/HTTPCore.cpp b/src/http/HTTPCore.cpp new file mode 100644 index 00000000..1de7ebdb --- /dev/null +++ b/src/http/HTTPCore.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "http/HTTPCore.h" +#include "Main.h" +#include "SS2KLog.h" +#include + +// Initialize the global instance +HTTPCore httpServer; + +HTTPCore::HTTPCore() + : internetConnection(false) + , server(80) + , lastUpdateTime(0) + , lastMDNSUpdate(0) { +} + +void HTTPCore::start() { + server.enableCORS(true); + + // Setup all routes through HTTPRoutes + HTTPRoutes::setupRoutes(server); + + server.begin(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP server started"); +} + +void HTTPCore::stop() { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Stopping Http Server"); + server.stop(); + server.close(); +} + +void HTTPCore::update() { + unsigned long currentTime = millis(); + + // Handle client requests with rate limiting + if (currentTime - lastUpdateTime > WEBSERVER_DELAY) { + lastUpdateTime = currentTime; + server.handleClient(); + + // Process DNS requests if in AP mode + WiFiManager::processDNS(); + + // Update MDNS periodically + updateMDNS(); + } +} + +void HTTPCore::updateMDNS() { + unsigned long currentTime = millis(); + if (currentTime - lastMDNSUpdate > MDNS_UPDATE_INTERVAL) { + MDNS.addServiceTxt("http", "_tcp", "lf", String(currentTime)); + lastMDNSUpdate = currentTime; + } +} + +bool HTTPCore::hasInternetConnection() const { + return internetConnection; +} + +void HTTPCore::setInternetConnection(bool connected) { + internetConnection = connected; +} + +WebServer& HTTPCore::getServer() { + return server; +} diff --git a/src/http/HTTPFileSystem.cpp b/src/http/HTTPFileSystem.cpp new file mode 100644 index 00000000..b05c75bd --- /dev/null +++ b/src/http/HTTPFileSystem.cpp @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "http/HTTPFileSystem.h" +#include "SS2KLog.h" + +// Initialize static members +File HTTPFileSystem::fsUploadFile; +bool HTTPFileSystem::uploadInProgress = false; +size_t HTTPFileSystem::uploadProgress = 0; +size_t HTTPFileSystem::uploadTotal = 0; + +// Initialize content type constants +const char* const HTTPFileSystem::TEXT_PLAIN = "text/plain"; +const char* const HTTPFileSystem::TEXT_HTML = "text/html"; +const char* const HTTPFileSystem::TEXT_CSS = "text/css"; +const char* const HTTPFileSystem::TEXT_JAVASCRIPT = "application/javascript"; +const char* const HTTPFileSystem::TEXT_JSON = "application/json"; +const char* const HTTPFileSystem::IMAGE_PNG = "image/png"; +const char* const HTTPFileSystem::IMAGE_JPG = "image/jpeg"; +const char* const HTTPFileSystem::IMAGE_GIF = "image/gif"; +const char* const HTTPFileSystem::IMAGE_ICO = "image/x-icon"; +const char* const HTTPFileSystem::APPLICATION_OCTET_STREAM = "application/octet-stream"; + +bool HTTPFileSystem::initialize() { + if (!LittleFS.begin()) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "LittleFS Mount Failed"); + return false; + } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "LittleFS Mounted Successfully"); + return true; +} + +void HTTPFileSystem::handleFileRead(WebServer& server, const String& path, const String& contentType) { + String finalPath = path; + if (finalPath.endsWith("/")) { + finalPath += "index.html"; + } + + String mimeType = contentType; + if (mimeType.isEmpty()) { + mimeType = getContentType(finalPath); + } + + if (exists(finalPath)) { + File file = openFile(finalPath, "r"); + if (file) { + server.streamFile(file, mimeType); + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", finalPath.c_str()); + return; + } + } + + SS2K_LOG(HTTP_SERVER_LOG_TAG, "File Not Found: %s", finalPath.c_str()); + server.send(404, TEXT_PLAIN, "File Not Found"); +} + +void HTTPFileSystem::handleFileUpload(WebServer& server) { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + uploadInProgress = true; + uploadProgress = 0; + uploadTotal = 0; + + String filename = upload.filename; + if (!filename.startsWith("/")) { + filename = "/" + filename; + } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Name: %s", filename.c_str()); + beginFileUpload(filename); + + } else if (upload.status == UPLOAD_FILE_WRITE) { + uploadProgress += upload.currentSize; + uploadTotal = upload.totalSize; + + if (fsUploadFile) { + continueFileUpload(upload.buf, upload.currentSize); + } + + } else if (upload.status == UPLOAD_FILE_END) { + if (fsUploadFile) { + endFileUpload(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Size: %zu", upload.totalSize); + } + uploadInProgress = false; + } +} + +void HTTPFileSystem::handleFileDelete(WebServer& server) { + if (!server.hasArg("path")) { + server.send(400, TEXT_PLAIN, "Path Argument Missing"); + return; + } + + String path = server.arg("path"); + if (!exists(path)) { + server.send(404, TEXT_PLAIN, "File Not Found"); + return; + } + + if (LittleFS.remove(path)) { + server.send(200, TEXT_PLAIN, "File Deleted"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "File Deleted: %s", path.c_str()); + } else { + server.send(500, TEXT_PLAIN, "Delete Failed"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Delete Failed: %s", path.c_str()); + } +} + +void HTTPFileSystem::handleFileList(WebServer& server) { + String path = server.hasArg("dir") ? server.arg("dir") : "/"; + + File dir = LittleFS.open(path); + if (!dir || !dir.isDirectory()) { + server.send(400, TEXT_PLAIN, "Directory Not Found"); + return; + } + + String output = "["; + File entry = dir.openNextFile(); + while (entry) { + if (output != "[") { + output += ','; + } + output += "{\"name\":\"" + String(entry.name()) + "\","; + output += "\"size\":" + String(entry.size()) + ","; + output += "\"isDir\":" + String(entry.isDirectory() ? "true" : "false") + "}"; + entry = dir.openNextFile(); + } + output += "]"; + + server.send(200, TEXT_JSON, output); +} + +String HTTPFileSystem::getContentType(const String& filename) { + if (filename.endsWith(".html")) return TEXT_HTML; + else if (filename.endsWith(".css")) return TEXT_CSS; + else if (filename.endsWith(".js")) return TEXT_JAVASCRIPT; + else if (filename.endsWith(".json")) return TEXT_JSON; + else if (filename.endsWith(".png")) return IMAGE_PNG; + else if (filename.endsWith(".jpg")) return IMAGE_JPG; + else if (filename.endsWith(".gif")) return IMAGE_GIF; + else if (filename.endsWith(".ico")) return IMAGE_ICO; + else if (filename.endsWith(".gz")) { + String baseFilename = filename.substring(0, filename.length() - 3); + return getContentType(baseFilename); + } + return TEXT_PLAIN; +} + +bool HTTPFileSystem::exists(const String& path) { + return LittleFS.exists(path); +} + +bool HTTPFileSystem::isDirectory(const String& path) { + File file = LittleFS.open(path); + bool isDir = file && file.isDirectory(); + file.close(); + return isDir; +} + +File HTTPFileSystem::openFile(const String& path, const char* mode) { + return LittleFS.open(path, mode); +} + +void HTTPFileSystem::beginFileUpload(const String& filename) { + if (fsUploadFile) { + fsUploadFile.close(); + } + fsUploadFile = LittleFS.open(filename, "w"); +} + +void HTTPFileSystem::continueFileUpload(uint8_t* data, size_t len) { + if (fsUploadFile && data && len > 0) { + fsUploadFile.write(data, len); + } +} + +void HTTPFileSystem::endFileUpload() { + if (fsUploadFile) { + fsUploadFile.close(); + } +} diff --git a/src/http/HTTPFirmware.cpp b/src/http/HTTPFirmware.cpp new file mode 100644 index 00000000..26a67fbc --- /dev/null +++ b/src/http/HTTPFirmware.cpp @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "http/HTTPFirmware.h" +#include "Main.h" +#include "SS2KLog.h" +#include "cert.h" +#include + +// Version class implementation +Version::Version(const char* version) : major(0), minor(0), patch(0) { + parseVersion(version); +} + +void Version::parseVersion(const char* version) { + sscanf(version, "%d.%d.%d", &major, &minor, &patch); +} + +bool Version::operator>(const Version& other) const { + if (major != other.major) return major > other.major; + if (minor != other.minor) return minor > other.minor; + return patch > other.patch; +} + +void HTTPFirmware::checkForUpdates() { + HTTPClient http; + WiFiClientSecure client; + client.setCACert(getRootCACertificate()); + + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Checking for newer firmware:"); + http.begin(userConfig->getFirmwareUpdateURL() + String(FW_VERSIONFILE), getRootCACertificate()); + + delay(100); + int httpCode = http.GET(); + delay(100); + + String payload; + if (httpCode == HTTP_CODE_OK) { + payload = http.getString(); + payload.trim(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, " - Server version: %s", payload.c_str()); + httpServer.setInternetConnection(true); + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "error downloading %s %d", FW_VERSIONFILE, httpCode); + httpServer.setInternetConnection(false); + } + http.end(); + + if (httpCode == HTTP_CODE_OK) { + bool updateAnyway = false; + if (!LittleFS.exists("/index.html")) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, " -index.html not found."); + } + + Version availableVer(payload.c_str()); + Version currentVer(FIRMWARE_VERSION); + + if (((availableVer > currentVer) && (userConfig->getAutoUpdate())) || (!LittleFS.exists("/index.html"))) { + // Update LittleFS first + updateLittleFS(); + + // Then update firmware if needed + if ((availableVer > currentVer) || updateAnyway) { + if (userConfig->getAutoUpdate()) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "New firmware detected!"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Upgrading from %s to %s", FIRMWARE_VERSION, payload.c_str()); + updateFirmware(); + } + } + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, " - Current Version: %s", FIRMWARE_VERSION); + } + } +} + +void HTTPFirmware::updateLittleFS() { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Updating FileSystem"); + + if (!downloadFileList()) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to download file list"); + return; + } +} + +void HTTPFirmware::updateFirmware() { + WiFiClientSecure client; + client.setCACert(getRootCACertificate()); + + t_httpUpdate_return ret = httpUpdate.update(client, userConfig->getFirmwareUpdateURL() + String(FW_BINFILE)); + + switch (ret) { + case HTTP_UPDATE_FAILED: + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_FAILED Error %d : %s", + httpUpdate.getLastError(), + httpUpdate.getLastErrorString().c_str()); + break; + + case HTTP_UPDATE_NO_UPDATES: + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_NO_UPDATES"); + break; + + case HTTP_UPDATE_OK: + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HTTP_UPDATE_OK"); + break; + } +} + +bool HTTPFirmware::downloadFileList() { + HTTPClient http; + http.begin(DATA_UPDATEURL + String(DATA_FILELIST), getRootCACertificate()); + + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "error downloading %s %d", DATA_FILELIST, httpCode); + http.end(); + return false; + } + + String payload = http.getString(); + payload.trim(); + http.end(); + + return processFileList(payload); +} + +bool HTTPFirmware::processFileList(const String& fileListContent) { + StaticJsonDocument<500> doc; + DeserializationError error = deserializeJson(doc, fileListContent); + + if (error) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to parse file list"); + return false; + } + + JsonArray files = doc.as(); + bool success = true; + + for (JsonVariant v : files) { + String fileName = "/" + v.as(); + if (!downloadAndSaveFile(fileName)) { + success = false; + } + } + + return success; +} + +bool HTTPFirmware::downloadAndSaveFile(const String& filename) { + HTTPClient http; + http.begin(DATA_UPDATEURL + filename, getRootCACertificate()); + + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Error downloading %s %d", filename.c_str(), httpCode); + http.end(); + return false; + } + + String payload = http.getString(); + payload.trim(); + http.end(); + + LittleFS.remove(filename); + File file = LittleFS.open(filename, FILE_WRITE, true); + if (!file) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to create file, %s", filename.c_str()); + return false; + } + + if (!file.print(payload)) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Failed to write to file, %s", filename.c_str()); + file.close(); + return false; + } + + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Created: %s", filename.c_str()); + return true; +} + +void HTTPFirmware::handleOTAUpdate(WebServer& server) { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + if (upload.filename == String("firmware.bin")) { + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { + Update.printError(Serial); + } + } else if (upload.filename == String("littlefs.bin")) { + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + Update.printError(Serial); + } + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) { + if (upload.filename == String("firmware.bin")) { + server.send(200, "text/plain", "Firmware Uploaded Successfully. Rebooting..."); + ESP.restart(); + } else if (upload.filename == String("littlefs.bin")) { + server.send(200, "text/plain", "Littlefs Uploaded Successfully. Rebooting..."); + userConfig->saveToLittleFS(); + userPWC->saveToLittleFS(); + ss2k->rebootFlag = true; + } + } else { + Update.printError(Serial); + } + } +} + +const char* HTTPFirmware::getRootCACertificate() { + return rootCACertificate; +} diff --git a/src/http/HTTPRoutes.cpp b/src/http/HTTPRoutes.cpp new file mode 100644 index 00000000..cdcca0b5 --- /dev/null +++ b/src/http/HTTPRoutes.cpp @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "http/HTTPRoutes.h" +#include "Main.h" +#include "SS2KLog.h" +#include "Builtin_Pages.h" +#include +#include + +// Initialize static members +WebServer* HTTPRoutes::currentServer = nullptr; +File HTTPRoutes::fsUploadFile; + +// Initialize static handler functions +HTTPRoutes::HandlerFunction HTTPRoutes::handleIndexFile = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleBTScanner = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleLittleFSFile = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleConfigJSON = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleRuntimeConfigJSON = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handlePWCJSON = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleShift = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleHRSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleWattsSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleCadSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleERGMode = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleTargetWattsSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleLogin = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleOTAUpdate = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleFileUpload = nullptr; + +void HTTPRoutes::initialize(WebServer& server) { + currentServer = &server; + + // Initialize handler functions + handleIndexFile = std::bind(&HTTPRoutes::_handleIndexFile); + handleBTScanner = std::bind(&HTTPRoutes::_handleBTScanner); + handleLittleFSFile = std::bind(&HTTPRoutes::_handleLittleFSFile); + handleConfigJSON = std::bind(&HTTPRoutes::_handleConfigJSON); + handleRuntimeConfigJSON = std::bind(&HTTPRoutes::_handleRuntimeConfigJSON); + handlePWCJSON = std::bind(&HTTPRoutes::_handlePWCJSON); + handleShift = std::bind(&HTTPRoutes::_handleShift); + handleHRSlider = std::bind(&HTTPRoutes::_handleHRSlider); + handleWattsSlider = std::bind(&HTTPRoutes::_handleWattsSlider); + handleCadSlider = std::bind(&HTTPRoutes::_handleCadSlider); + handleERGMode = std::bind(&HTTPRoutes::_handleERGMode); + handleTargetWattsSlider = std::bind(&HTTPRoutes::_handleTargetWattsSlider); + handleLogin = std::bind(&HTTPRoutes::_handleLogin); + handleOTAUpdate = std::bind(&HTTPRoutes::_handleOTAUpdate); + handleFileUpload = std::bind(&HTTPRoutes::_handleFileUpload); +} + +void HTTPRoutes::_handleIndexFile() { + String filename = "/index.html"; + if (LittleFS.exists(filename)) { + File file = LittleFS.open(filename, FILE_READ); + currentServer->streamFile(file, "text/html"); + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending builtin Index.html", filename.c_str()); + currentServer->send(200, "text/html", noIndexHTML); + } +} + +void HTTPRoutes::_handleBTScanner() { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Scanning from web request"); + spinBLEClient.dontBlockScan = true; + spinBLEClient.doScan = true; + _handleLittleFSFile(); +} + +void HTTPRoutes::_handleLittleFSFile() { + String filename = currentServer->uri(); + int dotPosition = filename.lastIndexOf("."); + String fileType = filename.substring((dotPosition + 1), filename.length()); + + if (LittleFS.exists(filename)) { + File file = LittleFS.open(filename, FILE_READ); + if (fileType == "gz") { + fileType = "html"; + } + currentServer->streamFile(file, "text/" + fileType); + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); + } else if (!LittleFS.exists("/index.html")) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found and no filesystem. Sending builtin index.html", filename.c_str()); + _handleIndexFile(); + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending 404.", filename.c_str()); + String outputhtml = "

ERROR 404
FILE NOT FOUND!" + filename + "

"; + currentServer->send(404, "text/html", outputhtml); + } +} + +void HTTPRoutes::_handleConfigJSON() { + String tString = userConfig->returnJSON(); + currentServer->send(200, "text/plain", tString); +} + +void HTTPRoutes::_handleRuntimeConfigJSON() { + String tString = rtConfig->returnJSON(); + currentServer->send(200, "text/plain", tString); +} + +void HTTPRoutes::_handlePWCJSON() { + String tString = userPWC->returnJSON(); + currentServer->send(200, "text/plain", tString); +} + +void HTTPRoutes::_handleShift() { + int value = currentServer->arg("value").toInt(); + if ((value > -10) && (value < 10)) { + rtConfig->setShifterPosition(rtConfig->getShifterPosition() + value); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Shift From HTML"); + } else { + rtConfig->setShifterPosition(value); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Invalid HTML Shift"); + currentServer->send(200, "text/plain", "OK"); + } +} + +void HTTPRoutes::_handleHRSlider() { + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->hr.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned on"); + } else if (value == "disable") { + rtConfig->hr.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned off"); + } else { + rtConfig->hr.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR is now: %d", rtConfig->hr.getValue()); + currentServer->send(200, "text/plain", "OK"); + } +} + +void HTTPRoutes::_handleWattsSlider() { + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->watts.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned on"); + } else if (value == "disable") { + rtConfig->watts.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned off"); + } else { + rtConfig->watts.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watts are now: %d", rtConfig->watts.getValue()); + currentServer->send(200, "text/plain", "OK"); + } +} + +void HTTPRoutes::_handleCadSlider() { + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->cad.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned on"); + } else if (value == "disable") { + rtConfig->cad.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned off"); + } else { + rtConfig->cad.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD is now: %d", rtConfig->cad.getValue()); + currentServer->send(200, "text/plain", "OK"); + } +} + +void HTTPRoutes::_handleERGMode() { + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned on"); + } else { + rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::RequestControl); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned off"); + } +} + +void HTTPRoutes::_handleTargetWattsSlider() { + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->setSimTargetWatts(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned on"); + } else if (value == "disable") { + rtConfig->setSimTargetWatts(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned off"); + } else { + rtConfig->watts.setTarget(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts are now: %d", rtConfig->watts.getTarget()); + currentServer->send(200, "text/plain", "OK"); + } +} + +void HTTPRoutes::_handleLogin() { + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/html", OTALoginIndex); +} + +void HTTPRoutes::_handleOTAUpdate() { + ss2k->stopTasks(); + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/html", OTAServerIndex); +} + +void HTTPRoutes::_handleFileUpload() { + HTTPUpload& upload = currentServer->upload(); + if (upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if (!filename.startsWith("/")) { + filename = "/" + filename; + } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Name: %s", filename.c_str()); + fsUploadFile = LittleFS.open(filename, "w"); + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (fsUploadFile) { + fsUploadFile.write(upload.buf, upload.currentSize); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (fsUploadFile) { + fsUploadFile.close(); + } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Size: %zu", upload.totalSize); + } +} + +void HTTPRoutes::setupRoutes(WebServer& server) { + initialize(server); + setupDefaultRoutes(server); + setupFileRoutes(server); + setupControlRoutes(server); + setupUpdateRoutes(server); +} + +void HTTPRoutes::setupDefaultRoutes(WebServer& server) { + // Default routes + server.on("/", HTTP_GET, handleIndexFile); + server.on("/index.html", HTTP_GET, handleIndexFile); + server.on("/generate_204", HTTP_GET, handleIndexFile); // Android captive portal + server.on("/fwlink", HTTP_GET, handleIndexFile); // Microsoft captive portal + server.on("/hotspot-detect.html", HTTP_GET, handleIndexFile); // Apple captive portal +} + +void HTTPRoutes::setupFileRoutes(WebServer& server) { + // Static file routes + server.on("/style.css", HTTP_GET, handleLittleFSFile); + server.on("/btsimulator.html", HTTP_GET, handleLittleFSFile); + server.on("/develop.html", HTTP_GET, handleLittleFSFile); + server.on("/shift.html", HTTP_GET, handleLittleFSFile); + server.on("/settings.html", HTTP_GET, handleLittleFSFile); + server.on("/status.html", HTTP_GET, handleLittleFSFile); + server.on("/bluetoothscanner.html", HTTP_GET, handleBTScanner); + server.on("/streamfit.html", HTTP_GET, handleLittleFSFile); + server.on("/hrtowatts.html", HTTP_GET, handleLittleFSFile); + server.on("/favicon.ico", HTTP_GET, handleLittleFSFile); + server.on("/jquery.js.gz", HTTP_GET, handleLittleFSFile); + + // API routes + server.on("/configJSON", HTTP_GET, handleConfigJSON); + server.on("/runtimeConfigJSON", HTTP_GET, handleRuntimeConfigJSON); + server.on("/PWCJSON", HTTP_GET, handlePWCJSON); + server.on("/BLEScan", HTTP_GET, handleBTScanner); +} + +void HTTPRoutes::setupControlRoutes(WebServer& server) { + // Control routes + server.on("/hrslider", HTTP_GET, handleHRSlider); + server.on("/wattsslider", HTTP_GET, handleWattsSlider); + server.on("/cadslider", HTTP_GET, handleCadSlider); + server.on("/ergmode", HTTP_GET, handleERGMode); + server.on("/targetwattsslider", HTTP_GET, handleTargetWattsSlider); + server.on("/shift", HTTP_GET, handleShift); +} + +void HTTPRoutes::setupUpdateRoutes(WebServer& server) { + // Update routes + server.on("/login", HTTP_GET, handleLogin); + server.on("/OTAIndex", HTTP_GET, handleOTAUpdate); + + // File upload handler + server.on("/update", HTTP_POST, + []() { + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + }, + handleFileUpload + ); +} + + diff --git a/src/http/HTTPSettings.cpp b/src/http/HTTPSettings.cpp new file mode 100644 index 00000000..c4f6e12f --- /dev/null +++ b/src/http/HTTPSettings.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "http/HTTPSettings.h" +#include "Main.h" +#include "SS2KLog.h" + +void HTTPSettings::processSettings(WebServer& server) { + bool wasBTUpdate = false; + bool wasSettingsUpdate = false; + bool reboot = false; + + // Process Network Settings + processNetworkSettings(server); + processDeviceSettings(server); + processStepperSettings(server); + processPowerSettings(server); + processERGSettings(server); + processFeatureSettings(server); + + // Process Bluetooth settings and capture the result + wasBTUpdate = processBluetoothSettings(server); + + processPWCSettings(server); + + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Config Updated From Web"); + ss2k->saveFlag = true; + + if (reboot) { + sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, true); + ss2k->rebootFlag = true; + } else { + sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, false); + } +} + +void HTTPSettings::processNetworkSettings(WebServer& server) { + if (!server.arg("ssid").isEmpty()) { + String ssid = server.arg("ssid"); + ssid.trim(); + userConfig->setSsid(ssid); + } + + if (!server.arg("password").isEmpty()) { + String password = server.arg("password"); + password.trim(); + userConfig->setPassword(password); + } +} + +void HTTPSettings::processDeviceSettings(WebServer& server) { + if (!server.arg("deviceName").isEmpty()) { + String deviceName = server.arg("deviceName"); + deviceName.trim(); + userConfig->setDeviceName(deviceName); + } +} + +void HTTPSettings::processStepperSettings(WebServer& server) { + if (!server.arg("shiftStep").isEmpty()) { + uint64_t shiftStep = server.arg("shiftStep").toInt(); + if (shiftStep >= MIN_SHIFT_STEP && shiftStep <= MAX_SHIFT_STEP) { + userConfig->setShiftStep(shiftStep); + } + } + + if (!server.arg("stepperPower").isEmpty()) { + uint64_t stepperPower = server.arg("stepperPower").toInt(); + if (stepperPower >= MIN_STEPPER_POWER && stepperPower <= MAX_STEPPER_POWER) { + userConfig->setStepperPower(stepperPower); + ss2k->updateStepperPower(); + } + } + + if (!server.arg("stepperDir").isEmpty()) { + userConfig->setStepperDir(true); + } else { + userConfig->setStepperDir(false); + } + + if (!server.arg("stealthChop").isEmpty()) { + userConfig->setStealthChop(true); + ss2k->updateStealthChop(); + } else { + userConfig->setStealthChop(false); + ss2k->updateStealthChop(); + } +} + +void HTTPSettings::processPowerSettings(WebServer& server) { + if (!server.arg("maxWatts").isEmpty()) { + uint64_t maxWatts = server.arg("maxWatts").toInt(); + if (maxWatts >= 0 && maxWatts <= DEFAULT_MAX_WATTS) { + userConfig->setMaxWatts(maxWatts); + } + } + + if (!server.arg("minWatts").isEmpty()) { + uint64_t minWatts = server.arg("minWatts").toInt(); + if (minWatts >= 0 && minWatts <= DEFAULT_MIN_WATTS) { + userConfig->setMinWatts(minWatts); + } + } + + if (!server.arg("powerCorrectionFactor").isEmpty()) { + float powerCorrectionFactor = server.arg("powerCorrectionFactor").toFloat(); + if (powerCorrectionFactor >= MIN_PCF && powerCorrectionFactor <= MAX_PCF) { + userConfig->setPowerCorrectionFactor(powerCorrectionFactor); + } + } +} + +void HTTPSettings::processERGSettings(WebServer& server) { + if (!server.arg("ERGSensitivity").isEmpty()) { + float ERGSensitivity = server.arg("ERGSensitivity").toFloat(); + if (ERGSensitivity >= 0.1 && ERGSensitivity <= 20) { + userConfig->setERGSensitivity(ERGSensitivity); + } + } + + if (!server.arg("inclineMultiplier").isEmpty()) { + float inclineMultiplier = server.arg("inclineMultiplier").toFloat(); + if (inclineMultiplier >= 0 && inclineMultiplier <= 10) { + userConfig->setInclineMultiplier(inclineMultiplier); + } + } +} + +void HTTPSettings::processFeatureSettings(WebServer& server) { + if (!server.arg("autoUpdate").isEmpty()) { + userConfig->setAutoUpdate(true); + } else { + userConfig->setAutoUpdate(false); + } + + if (!server.arg("shifterDir").isEmpty()) { + userConfig->setShifterDir(true); + } else { + userConfig->setShifterDir(false); + } + + if (!server.arg("udpLogEnabled").isEmpty()) { + userConfig->setUdpLogEnabled(true); + } else { + userConfig->setUdpLogEnabled(false); + } +} + +bool HTTPSettings::processBluetoothSettings(WebServer& server) { + bool wasBTUpdate = false; + + if (!server.arg("blePMDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("blePMDropdown")) { + String powerMeter = server.arg("blePMDropdown"); + if (powerMeter != userConfig->getConnectedPowerMeter()) { + userConfig->setConnectedPowerMeter(powerMeter); + spinBLEClient.reconnectAllDevices(); + } + } else { + userConfig->setConnectedPowerMeter("any"); + } + } + + if (!server.arg("bleHRDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("bleHRDropdown")) { + String heartMonitor = server.arg("bleHRDropdown"); + if (heartMonitor != userConfig->getConnectedHeartMonitor()) { + spinBLEClient.reconnectAllDevices(); + } + userConfig->setConnectedHeartMonitor(heartMonitor); + } else { + userConfig->setConnectedHeartMonitor("any"); + } + } + + if (!server.arg("bleRemoteDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("bleRemoteDropdown")) { + String remote = server.arg("bleRemoteDropdown"); + if (remote != userConfig->getConnectedRemote()) { + spinBLEClient.reconnectAllDevices(); + } + userConfig->setConnectedRemote(remote); + } else { + userConfig->setConnectedRemote("any"); + } + } + + return wasBTUpdate; +} + +void HTTPSettings::processPWCSettings(WebServer& server) { + if (!server.arg("session1HR").isEmpty()) { + userPWC->session1HR = server.arg("session1HR").toInt(); + } + if (!server.arg("session1Pwr").isEmpty()) { + userPWC->session1Pwr = server.arg("session1Pwr").toInt(); + } + if (!server.arg("session2HR").isEmpty()) { + userPWC->session2HR = server.arg("session2HR").toInt(); + } + if (!server.arg("session2Pwr").isEmpty()) { + userPWC->session2Pwr = server.arg("session2Pwr").toInt(); + } + if (!server.arg("hr2Pwr").isEmpty()) { + userPWC->hr2Pwr = true; + } else { + userPWC->hr2Pwr = false; + } +} + +void HTTPSettings::sendSettingsResponse(WebServer& server, bool wasBTUpdate, bool wasSettingsUpdate, bool requiresReboot) { + String response; + if (wasBTUpdate) { + response = buildRedirectResponse("Selections Saved!", "/bluetoothscanner.html"); + } else if (wasSettingsUpdate || requiresReboot) { + response = buildRedirectResponse( + "Network settings will be applied at next reboot.
" + "Everything else is available immediately.", + "/settings.html" + ); + } else { + response = buildRedirectResponse( + "Network settings will be applied at next reboot.
" + "Everything else is available immediately.", + "/index.html" + ); + } + + if (requiresReboot) { + response = buildRedirectResponse( + "Please wait while your settings are saved and SmartSpin2k reboots.", + "/bluetoothscanner.html", + 5000 + ); + } + + server.send(200, "text/html", response); +} + +String HTTPSettings::buildRedirectResponse(const String& message, const String& page, int delay) { + return "

" + message + + "

"; +} + +bool HTTPSettings::processCheckbox(WebServer& server, const char* name, bool defaultValue) { + if (!server.hasArg(name)) { + return defaultValue; + } + return server.arg(name) == "true" || server.arg(name) == "1"; +} + +float HTTPSettings::processFloatValue(WebServer& server, const char* name, float min, float max) { + if (!server.hasArg(name)) { + return min; + } + float value = server.arg(name).toFloat(); + if (value < min) return min; + if (value > max) return max; + return value; +} + +int HTTPSettings::processIntValue(WebServer& server, const char* name, int min, int max) { + if (!server.hasArg(name)) { + return min; + } + int value = server.arg(name).toInt(); + if (value < min) return min; + if (value > max) return max; + return value; +} + +String HTTPSettings::processStringValue(WebServer& server, const char* name) { + if (!server.hasArg(name)) { + return ""; + } + return server.arg(name); +} diff --git a/src/wifi/WiFiManager.cpp b/src/wifi/WiFiManager.cpp new file mode 100644 index 00000000..cc56b4a6 --- /dev/null +++ b/src/wifi/WiFiManager.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "wifi/WiFiManager.h" +#include "Main.h" +#include "SS2KLog.h" +#include + +// Initialize static members +DNSServer WiFiManager::dnsServer; +IPAddress WiFiManager::myIP; + +void WiFiManager::startWifi() { + int attempts = 0; + + // Try Station mode first if SSID is not default + if (strcmp(userConfig->getSsid(), DEVICE_NAME) != 0) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connecting to: %s", userConfig->getSsid()); + setupStationMode(); + + while (WiFi.status() != WL_CONNECTED) { + vTaskDelay(1000 / portTICK_RATE_MS); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for connection to be established..."); + attempts++; + if (attempts > WIFI_CONNECT_TIMEOUT) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Couldn't Connect. Switching to AP mode"); + WiFi.disconnect(true, true); + WiFi.setAutoReconnect(false); + WiFi.mode(WIFI_MODE_NULL); + vTaskDelay(1000 / portTICK_RATE_MS); + break; + } + } + } + + // If connected in STA mode, set IP and return + if (WiFi.status() == WL_CONNECTED) { + myIP = WiFi.localIP(); + setupMDNS(); + if (WiFi.getMode() == WIFI_STA) { + syncClock(); + } + return; + } + + // Otherwise set up AP mode + setupAPMode(); + + if (strcmp(userConfig->getSsid(), DEVICE_NAME) == 0) { + // If default SSID is still in use, let the user select a new password + WiFi.softAP(userConfig->getDeviceName(), userConfig->getPassword()); + } else { + WiFi.softAP(userConfig->getDeviceName(), DEFAULT_PASSWORD); + } + + vTaskDelay(50); + myIP = WiFi.softAPIP(); + + // Setup DNS server for captive portal + dnsServer.setErrorReplyCode(DNSReplyCode::NoError); + dnsServer.start(DNS_PORT, "*", myIP); + + setupMDNS(); + + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connected to %s IP address: %s", + userConfig->getSsid(), myIP.toString().c_str()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Open http://%s.local/", userConfig->getDeviceName()); + + WiFi.setTxPower(WIFI_POWER_19_5dBm); +} + +void WiFiManager::stopWifi() { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Closing connection to: %s", userConfig->getSsid()); + WiFi.disconnect(); +} + +void WiFiManager::setupStationMode() { + WiFi.setHostname(userConfig->getDeviceName()); + WiFi.mode(WIFI_STA); + WiFi.begin(userConfig->getSsid(), userConfig->getPassword()); + WiFi.setAutoReconnect(true); +} + +void WiFiManager::setupAPMode() { + WiFi.mode(WIFI_AP); + WiFi.setHostname("reset"); // Fixes a bug when switching Arduino Core Versions + WiFi.softAPsetHostname("reset"); + WiFi.setHostname(userConfig->getDeviceName()); + WiFi.softAPsetHostname(userConfig->getDeviceName()); + WiFi.enableAP(true); + vTaskDelay(500); // Microcontroller requires some time to reset the mode +} + +void WiFiManager::setupMDNS() { + if (!MDNS.begin(userConfig->getDeviceName())) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Error setting up MDNS responder!"); + return; + } + MDNS.addService("http", "_tcp", 80); + MDNS.addServiceTxt("http", "_tcp", "lf", "0"); +} + +void WiFiManager::syncClock() { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Syncing clock..."); + configTime(0, 0, "pool.ntp.org"); // get UTC time via NTP + time_t now = time(nullptr); + while (now < 10) { // wait 10 seconds + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for clock sync..."); + delay(100); + now = time(nullptr); + } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Clock synced to: %.f", difftime(now, (time_t)0)); +} + +void WiFiManager::processDNS() { + if (WiFi.getMode() != WIFI_MODE_STA) { + dnsServer.processNextRequest(); + } +} + +IPAddress WiFiManager::getIP() { + return myIP; +} + +bool WiFiManager::isConnected() { + return WiFi.status() == WL_CONNECTED; +} From cb364c395c2b4815ead8c867db536605bdeb2e1d Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 20:38:39 -0600 Subject: [PATCH 02/25] Initial testing complete --- include/HTTP_Server_Basic.h | 20 -- include/Main.h | 6 +- include/http/HTTPCore.h | 1 - include/http/HTTPRoutes.h | 4 +- include/wifi/WiFiManager.h | 1 - src/HTTP_Server_Basic.cpp | 24 --- src/Main.cpp | 18 +- src/http/HTTPFirmware.cpp | 27 +-- src/http/HTTPRoutes.cpp | 416 +++++++++++++++++------------------- 9 files changed, 224 insertions(+), 293 deletions(-) delete mode 100644 include/HTTP_Server_Basic.h delete mode 100644 src/HTTP_Server_Basic.cpp diff --git a/include/HTTP_Server_Basic.h b/include/HTTP_Server_Basic.h deleted file mode 100644 index cb899996..00000000 --- a/include/HTTP_Server_Basic.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 2020 Anthony Doud & Joel Baranick - * All rights reserved - * - * SPDX-License-Identifier: GPL-2.0-only - */ - -#pragma once - -#include -#include "http/HTTPCore.h" -#include "http/HTTPRoutes.h" -#include "http/HTTPFileSystem.h" -#include "http/HTTPSettings.h" -#include "http/HTTPFirmware.h" - -// For backward compatibility, use HTTPCore as HTTP_Server -using HTTP_Server = HTTPCore; - -extern HTTP_Server httpServer; diff --git a/include/Main.h b/include/Main.h index 54805573..6fde63a7 100644 --- a/include/Main.h +++ b/include/Main.h @@ -7,10 +7,12 @@ #pragma once -#include "HTTP_Server_Basic.h" +#include "http/HTTPCore.h" +#include "http/HTTPRoutes.h" +#include "http/HTTPSettings.h" +#include "http/HTTPFirmware.h" #include "SmartSpin_parameters.h" #include "BLE_Common.h" -// #include "LittleFS_Upgrade.h" #include "boards.h" #include "SensorCollector.h" #include "SS2KLog.h" diff --git a/include/http/HTTPCore.h b/include/http/HTTPCore.h index 19fafd73..b75d0fcd 100644 --- a/include/http/HTTPCore.h +++ b/include/http/HTTPCore.h @@ -18,7 +18,6 @@ #include "wifi/WiFiManager.h" #define HTTP_SERVER_LOG_TAG "HTTP_Server" -#define WEBSERVER_DELAY 10 // ms class HTTPCore { public: diff --git a/include/http/HTTPRoutes.h b/include/http/HTTPRoutes.h index 38c709e6..e546170c 100644 --- a/include/http/HTTPRoutes.h +++ b/include/http/HTTPRoutes.h @@ -10,8 +10,8 @@ #include #include #include +#include #include -#include "Builtin_Pages.h" class HTTPRoutes { public: @@ -33,6 +33,7 @@ class HTTPRoutes { static HandlerFunction handleLogin; static HandlerFunction handleOTAUpdate; static HandlerFunction handleFileUpload; + static HandlerFunction handleSendSettings; // Added this handler // Setup function to register all routes static void setupRoutes(WebServer& server); @@ -66,4 +67,5 @@ class HTTPRoutes { static void _handleLogin(); static void _handleOTAUpdate(); static void _handleFileUpload(); + static void _handleSendSettings(); // Added this handler implementation }; diff --git a/include/wifi/WiFiManager.h b/include/wifi/WiFiManager.h index 34b6be8d..ce490b2c 100644 --- a/include/wifi/WiFiManager.h +++ b/include/wifi/WiFiManager.h @@ -27,7 +27,6 @@ class WiFiManager { static void syncClock(); static const byte DNS_PORT = 53; - static const int WIFI_CONNECT_TIMEOUT = 20; // seconds static DNSServer dnsServer; static IPAddress myIP; diff --git a/src/HTTP_Server_Basic.cpp b/src/HTTP_Server_Basic.cpp deleted file mode 100644 index d78f01f4..00000000 --- a/src/HTTP_Server_Basic.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2020 Anthony Doud & Joel Baranick - * All rights reserved - * - * SPDX-License-Identifier: GPL-2.0-only - */ - -#include "HTTP_Server_Basic.h" -#include "Main.h" -#include "SS2KLog.h" -#include "wifi/WiFiManager.h" - -// Initialize the global instance (already declared in HTTPCore.cpp) -// extern HTTP_Server httpServer; - -// The original HTTP_Server_Basic.cpp functionality has been refactored into: -// - HTTPCore: Core server functionality -// - HTTPRoutes: Route handlers -// - HTTPFileSystem: File system operations -// - HTTPSettings: Settings processing -// - HTTPFirmware: Firmware updates -// - WiFiManager: WiFi connection management - -// This file remains for backward compatibility and initialization diff --git a/src/Main.cpp b/src/Main.cpp index 49f80c72..43cdb07a 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -121,8 +121,8 @@ void setup() { // Check for firmware update. It's important that this stays before BLE & // HTTP setup because otherwise they use too much traffic and the device // fails to update which really sucks when it corrupts your settings. - startWifi(); - httpServer.FirmwareUpdate(); +WiFiManager::startWifi(); +HTTPFirmware::checkForUpdates(); pinMode(currentBoard.shiftUpPin, INPUT_PULLUP); // Push-Button with input Pullup pinMode(currentBoard.shiftDownPin, INPUT_PULLUP); // Push-Button with input Pullup @@ -192,7 +192,7 @@ void SS2K::maintenanceLoop(void *pvParameters) { // Run what used to be in the ERG Mode Task. powerTable->runERG(); // Run what used to be in the WebClient Task. - httpServer.webClientUpdate(); + httpServer.update(); // If we're in ERG mode, modify shift commands to inc/dec the target watts instead. ss2k->FTMSModeShiftModifier(); // If we have a resistance bike attached, slow down when we're close to the limits. @@ -387,12 +387,12 @@ void SS2K::FTMSModeShiftModifier() { } void SS2K::restartWifi() { - httpServer.stop(); - vTaskDelay(100 / portTICK_RATE_MS); - stopWifi(); - vTaskDelay(100 / portTICK_RATE_MS); - startWifi(); - httpServer.start(); + httpServer.stop(); + vTaskDelay(100 / portTICK_RATE_MS); + WiFiManager::stopWifi(); + vTaskDelay(100 / portTICK_RATE_MS); + WiFiManager::startWifi(); + httpServer.start(); } void SS2K::moveStepper() { diff --git a/src/http/HTTPFirmware.cpp b/src/http/HTTPFirmware.cpp index 26a67fbc..967090af 100644 --- a/src/http/HTTPFirmware.cpp +++ b/src/http/HTTPFirmware.cpp @@ -186,14 +186,9 @@ void HTTPFirmware::handleOTAUpdate(WebServer& server) { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { - if (upload.filename == String("firmware.bin")) { - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { - Update.printError(Serial); - } - } else if (upload.filename == String("littlefs.bin")) { - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { - Update.printError(Serial); - } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update: %s", upload.filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { + Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { @@ -201,18 +196,16 @@ void HTTPFirmware::handleOTAUpdate(WebServer& server) { } } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { - if (upload.filename == String("firmware.bin")) { - server.send(200, "text/plain", "Firmware Uploaded Successfully. Rebooting..."); - ESP.restart(); - } else if (upload.filename == String("littlefs.bin")) { - server.send(200, "text/plain", "Littlefs Uploaded Successfully. Rebooting..."); - userConfig->saveToLittleFS(); - userPWC->saveToLittleFS(); - ss2k->rebootFlag = true; - } + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update Success: %u bytes\nRebooting...", upload.totalSize); + server.send(200, "text/plain", "Update successful. Rebooting..."); + delay(1000); + ESP.restart(); } else { Update.printError(Serial); } + } else if (upload.status == UPLOAD_FILE_ABORTED) { + Update.end(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update aborted"); } } diff --git a/src/http/HTTPRoutes.cpp b/src/http/HTTPRoutes.cpp index cdcca0b5..59750bcf 100644 --- a/src/http/HTTPRoutes.cpp +++ b/src/http/HTTPRoutes.cpp @@ -9,6 +9,7 @@ #include "Main.h" #include "SS2KLog.h" #include "Builtin_Pages.h" +#include "HTTPUpdateServer.h" #include #include @@ -17,288 +18,267 @@ WebServer* HTTPRoutes::currentServer = nullptr; File HTTPRoutes::fsUploadFile; // Initialize static handler functions -HTTPRoutes::HandlerFunction HTTPRoutes::handleIndexFile = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleBTScanner = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleLittleFSFile = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleConfigJSON = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleIndexFile = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleBTScanner = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleLittleFSFile = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleConfigJSON = nullptr; HTTPRoutes::HandlerFunction HTTPRoutes::handleRuntimeConfigJSON = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handlePWCJSON = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleShift = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleHRSlider = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleWattsSlider = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleCadSlider = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleERGMode = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handlePWCJSON = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleShift = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleHRSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleWattsSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleCadSlider = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleERGMode = nullptr; HTTPRoutes::HandlerFunction HTTPRoutes::handleTargetWattsSlider = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleLogin = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleOTAUpdate = nullptr; -HTTPRoutes::HandlerFunction HTTPRoutes::handleFileUpload = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleLogin = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleOTAUpdate = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleFileUpload = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleSendSettings = nullptr; void HTTPRoutes::initialize(WebServer& server) { - currentServer = &server; - - // Initialize handler functions - handleIndexFile = std::bind(&HTTPRoutes::_handleIndexFile); - handleBTScanner = std::bind(&HTTPRoutes::_handleBTScanner); - handleLittleFSFile = std::bind(&HTTPRoutes::_handleLittleFSFile); - handleConfigJSON = std::bind(&HTTPRoutes::_handleConfigJSON); - handleRuntimeConfigJSON = std::bind(&HTTPRoutes::_handleRuntimeConfigJSON); - handlePWCJSON = std::bind(&HTTPRoutes::_handlePWCJSON); - handleShift = std::bind(&HTTPRoutes::_handleShift); - handleHRSlider = std::bind(&HTTPRoutes::_handleHRSlider); - handleWattsSlider = std::bind(&HTTPRoutes::_handleWattsSlider); - handleCadSlider = std::bind(&HTTPRoutes::_handleCadSlider); - handleERGMode = std::bind(&HTTPRoutes::_handleERGMode); - handleTargetWattsSlider = std::bind(&HTTPRoutes::_handleTargetWattsSlider); - handleLogin = std::bind(&HTTPRoutes::_handleLogin); - handleOTAUpdate = std::bind(&HTTPRoutes::_handleOTAUpdate); - handleFileUpload = std::bind(&HTTPRoutes::_handleFileUpload); + currentServer = &server; + + // Initialize handler functions using lambda functions to capture the static methods + handleIndexFile = []() { _handleIndexFile(); }; + handleBTScanner = []() { _handleBTScanner(); }; + handleLittleFSFile = []() { _handleLittleFSFile(); }; + handleConfigJSON = []() { _handleConfigJSON(); }; + handleRuntimeConfigJSON = []() { _handleRuntimeConfigJSON(); }; + handlePWCJSON = []() { _handlePWCJSON(); }; + handleShift = []() { _handleShift(); }; + handleHRSlider = []() { _handleHRSlider(); }; + handleWattsSlider = []() { _handleWattsSlider(); }; + handleCadSlider = []() { _handleCadSlider(); }; + handleERGMode = []() { _handleERGMode(); }; + handleTargetWattsSlider = []() { _handleTargetWattsSlider(); }; + handleLogin = []() { _handleLogin(); }; + handleOTAUpdate = []() { _handleOTAUpdate(); }; + handleFileUpload = []() { _handleFileUpload(); }; + handleSendSettings = []() { _handleSendSettings(); }; } void HTTPRoutes::_handleIndexFile() { - String filename = "/index.html"; - if (LittleFS.exists(filename)) { - File file = LittleFS.open(filename, FILE_READ); - currentServer->streamFile(file, "text/html"); - file.close(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending builtin Index.html", filename.c_str()); - currentServer->send(200, "text/html", noIndexHTML); - } + String filename = "/index.html"; + if (LittleFS.exists(filename)) { + File file = LittleFS.open(filename, FILE_READ); + currentServer->streamFile(file, "text/html"); + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending builtin Index.html", filename.c_str()); + currentServer->send(200, "text/html", noIndexHTML); + } } void HTTPRoutes::_handleBTScanner() { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Scanning from web request"); - spinBLEClient.dontBlockScan = true; - spinBLEClient.doScan = true; - _handleLittleFSFile(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Scanning from web request"); + spinBLEClient.dontBlockScan = true; + spinBLEClient.doScan = true; + _handleLittleFSFile(); } void HTTPRoutes::_handleLittleFSFile() { - String filename = currentServer->uri(); - int dotPosition = filename.lastIndexOf("."); - String fileType = filename.substring((dotPosition + 1), filename.length()); - - if (LittleFS.exists(filename)) { - File file = LittleFS.open(filename, FILE_READ); - if (fileType == "gz") { - fileType = "html"; - } - currentServer->streamFile(file, "text/" + fileType); - file.close(); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); - } else if (!LittleFS.exists("/index.html")) { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found and no filesystem. Sending builtin index.html", filename.c_str()); - _handleIndexFile(); - } else { - SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending 404.", filename.c_str()); - String outputhtml = "

ERROR 404
FILE NOT FOUND!" + filename + "

"; - currentServer->send(404, "text/html", outputhtml); + String filename = currentServer->uri(); + int dotPosition = filename.lastIndexOf("."); + String fileType = filename.substring((dotPosition + 1), filename.length()); + + if (LittleFS.exists(filename)) { + File file = LittleFS.open(filename, FILE_READ); + if (fileType == "gz") { + fileType = "html"; } + currentServer->streamFile(file, "text/" + fileType); + file.close(); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Served %s", filename.c_str()); + } else if (!LittleFS.exists("/index.html")) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found and no filesystem. Sending builtin index.html", filename.c_str()); + _handleIndexFile(); + } else { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "%s not found. Sending 404.", filename.c_str()); + String outputhtml = "

ERROR 404
FILE NOT FOUND!" + filename + "

"; + currentServer->send(404, "text/html", outputhtml); + } } void HTTPRoutes::_handleConfigJSON() { - String tString = userConfig->returnJSON(); - currentServer->send(200, "text/plain", tString); + String tString = userConfig->returnJSON(); + currentServer->send(200, "text/plain", tString); } void HTTPRoutes::_handleRuntimeConfigJSON() { - String tString = rtConfig->returnJSON(); - currentServer->send(200, "text/plain", tString); + String tString = rtConfig->returnJSON(); + currentServer->send(200, "text/plain", tString); } void HTTPRoutes::_handlePWCJSON() { - String tString = userPWC->returnJSON(); - currentServer->send(200, "text/plain", tString); + String tString = userPWC->returnJSON(); + currentServer->send(200, "text/plain", tString); } void HTTPRoutes::_handleShift() { - int value = currentServer->arg("value").toInt(); - if ((value > -10) && (value < 10)) { - rtConfig->setShifterPosition(rtConfig->getShifterPosition() + value); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Shift From HTML"); - } else { - rtConfig->setShifterPosition(value); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Invalid HTML Shift"); - currentServer->send(200, "text/plain", "OK"); - } + int value = currentServer->arg("value").toInt(); + if ((value > -10) && (value < 10)) { + rtConfig->setShifterPosition(rtConfig->getShifterPosition() + value); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Shift From HTML"); + } else { + rtConfig->setShifterPosition(value); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Invalid HTML Shift"); + currentServer->send(200, "text/plain", "OK"); + } } void HTTPRoutes::_handleHRSlider() { - String value = currentServer->arg("value"); - if (value == "enable") { - rtConfig->hr.setSimulate(true); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned on"); - } else if (value == "disable") { - rtConfig->hr.setSimulate(false); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned off"); - } else { - rtConfig->hr.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR is now: %d", rtConfig->hr.getValue()); - currentServer->send(200, "text/plain", "OK"); - } + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->hr.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned on"); + } else if (value == "disable") { + rtConfig->hr.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR Simulator turned off"); + } else { + rtConfig->hr.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "HR is now: %d", rtConfig->hr.getValue()); + currentServer->send(200, "text/plain", "OK"); + } } void HTTPRoutes::_handleWattsSlider() { - String value = currentServer->arg("value"); - if (value == "enable") { - rtConfig->watts.setSimulate(true); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned on"); - } else if (value == "disable") { - rtConfig->watts.setSimulate(false); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned off"); - } else { - rtConfig->watts.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watts are now: %d", rtConfig->watts.getValue()); - currentServer->send(200, "text/plain", "OK"); - } + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->watts.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned on"); + } else if (value == "disable") { + rtConfig->watts.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watt Simulator turned off"); + } else { + rtConfig->watts.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Watts are now: %d", rtConfig->watts.getValue()); + currentServer->send(200, "text/plain", "OK"); + } } void HTTPRoutes::_handleCadSlider() { - String value = currentServer->arg("value"); - if (value == "enable") { - rtConfig->cad.setSimulate(true); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned on"); - } else if (value == "disable") { - rtConfig->cad.setSimulate(false); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned off"); - } else { - rtConfig->cad.setValue(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD is now: %d", rtConfig->cad.getValue()); - currentServer->send(200, "text/plain", "OK"); - } + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->cad.setSimulate(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned on"); + } else if (value == "disable") { + rtConfig->cad.setSimulate(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD Simulator turned off"); + } else { + rtConfig->cad.setValue(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "CAD is now: %d", rtConfig->cad.getValue()); + currentServer->send(200, "text/plain", "OK"); + } } void HTTPRoutes::_handleERGMode() { - String value = currentServer->arg("value"); - if (value == "enable") { - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned on"); - } else { - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::RequestControl); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned off"); - } + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned on"); + } else { + rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::RequestControl); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "ERG Mode turned off"); + } } void HTTPRoutes::_handleTargetWattsSlider() { - String value = currentServer->arg("value"); - if (value == "enable") { - rtConfig->setSimTargetWatts(true); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned on"); - } else if (value == "disable") { - rtConfig->setSimTargetWatts(false); - currentServer->send(200, "text/plain", "OK"); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned off"); - } else { - rtConfig->watts.setTarget(value.toInt()); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts are now: %d", rtConfig->watts.getTarget()); - currentServer->send(200, "text/plain", "OK"); - } + String value = currentServer->arg("value"); + if (value == "enable") { + rtConfig->setSimTargetWatts(true); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned on"); + } else if (value == "disable") { + rtConfig->setSimTargetWatts(false); + currentServer->send(200, "text/plain", "OK"); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts Simulator turned off"); + } else { + rtConfig->watts.setTarget(value.toInt()); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Target Watts are now: %d", rtConfig->watts.getTarget()); + currentServer->send(200, "text/plain", "OK"); + } } void HTTPRoutes::_handleLogin() { - currentServer->sendHeader("Connection", "close"); - currentServer->send(200, "text/html", OTALoginIndex); + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/html", OTALoginIndex); } void HTTPRoutes::_handleOTAUpdate() { - ss2k->stopTasks(); - currentServer->sendHeader("Connection", "close"); - currentServer->send(200, "text/html", OTAServerIndex); + ss2k->stopTasks(); + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/html", OTAServerIndex); } void HTTPRoutes::_handleFileUpload() { - HTTPUpload& upload = currentServer->upload(); - if (upload.status == UPLOAD_FILE_START) { - String filename = upload.filename; - if (!filename.startsWith("/")) { - filename = "/" + filename; - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Name: %s", filename.c_str()); - fsUploadFile = LittleFS.open(filename, "w"); - } else if (upload.status == UPLOAD_FILE_WRITE) { - if (fsUploadFile) { - fsUploadFile.write(upload.buf, upload.currentSize); - } - } else if (upload.status == UPLOAD_FILE_END) { - if (fsUploadFile) { - fsUploadFile.close(); - } - SS2K_LOG(HTTP_SERVER_LOG_TAG, "handleFileUpload Size: %zu", upload.totalSize); - } + HTTPFirmware::handleOTAUpdate(*currentServer); } void HTTPRoutes::setupRoutes(WebServer& server) { - initialize(server); - setupDefaultRoutes(server); - setupFileRoutes(server); - setupControlRoutes(server); - setupUpdateRoutes(server); + initialize(server); + setupDefaultRoutes(server); + setupFileRoutes(server); + setupControlRoutes(server); + setupUpdateRoutes(server); } void HTTPRoutes::setupDefaultRoutes(WebServer& server) { - // Default routes - server.on("/", HTTP_GET, handleIndexFile); - server.on("/index.html", HTTP_GET, handleIndexFile); - server.on("/generate_204", HTTP_GET, handleIndexFile); // Android captive portal - server.on("/fwlink", HTTP_GET, handleIndexFile); // Microsoft captive portal - server.on("/hotspot-detect.html", HTTP_GET, handleIndexFile); // Apple captive portal + server.on("/", HTTP_GET, handleIndexFile); + server.on("/index.html", HTTP_GET, handleIndexFile); + server.on("/generate_204", HTTP_GET, handleIndexFile); // Android captive portal + server.on("/fwlink", HTTP_GET, handleIndexFile); // Microsoft captive portal + server.on("/hotspot-detect.html", HTTP_GET, handleIndexFile); // Apple captive portal } void HTTPRoutes::setupFileRoutes(WebServer& server) { - // Static file routes - server.on("/style.css", HTTP_GET, handleLittleFSFile); - server.on("/btsimulator.html", HTTP_GET, handleLittleFSFile); - server.on("/develop.html", HTTP_GET, handleLittleFSFile); - server.on("/shift.html", HTTP_GET, handleLittleFSFile); - server.on("/settings.html", HTTP_GET, handleLittleFSFile); - server.on("/status.html", HTTP_GET, handleLittleFSFile); - server.on("/bluetoothscanner.html", HTTP_GET, handleBTScanner); - server.on("/streamfit.html", HTTP_GET, handleLittleFSFile); - server.on("/hrtowatts.html", HTTP_GET, handleLittleFSFile); - server.on("/favicon.ico", HTTP_GET, handleLittleFSFile); - server.on("/jquery.js.gz", HTTP_GET, handleLittleFSFile); - - // API routes - server.on("/configJSON", HTTP_GET, handleConfigJSON); - server.on("/runtimeConfigJSON", HTTP_GET, handleRuntimeConfigJSON); - server.on("/PWCJSON", HTTP_GET, handlePWCJSON); - server.on("/BLEScan", HTTP_GET, handleBTScanner); + server.on("/style.css", HTTP_GET, handleLittleFSFile); + server.on("/btsimulator.html", HTTP_GET, handleLittleFSFile); + server.on("/develop.html", HTTP_GET, handleLittleFSFile); + server.on("/shift.html", HTTP_GET, handleLittleFSFile); + server.on("/settings.html", HTTP_GET, handleLittleFSFile); + server.on("/status.html", HTTP_GET, handleLittleFSFile); + server.on("/bluetoothscanner.html", HTTP_GET, handleBTScanner); + server.on("/streamfit.html", HTTP_GET, handleLittleFSFile); + server.on("/hrtowatts.html", HTTP_GET, handleLittleFSFile); + server.on("/favicon.ico", HTTP_GET, handleLittleFSFile); + server.on("/jquery.js.gz", HTTP_GET, handleLittleFSFile); } void HTTPRoutes::setupControlRoutes(WebServer& server) { - // Control routes - server.on("/hrslider", HTTP_GET, handleHRSlider); - server.on("/wattsslider", HTTP_GET, handleWattsSlider); - server.on("/cadslider", HTTP_GET, handleCadSlider); - server.on("/ergmode", HTTP_GET, handleERGMode); - server.on("/targetwattsslider", HTTP_GET, handleTargetWattsSlider); - server.on("/shift", HTTP_GET, handleShift); + server.on("/hrslider", HTTP_GET, handleHRSlider); + server.on("/wattsslider", HTTP_GET, handleWattsSlider); + server.on("/cadslider", HTTP_GET, handleCadSlider); + server.on("/ergmode", HTTP_GET, handleERGMode); + server.on("/targetwattsslider", HTTP_GET, handleTargetWattsSlider); + server.on("/shift", HTTP_GET, handleShift); + server.on("/configJSON", HTTP_GET, handleConfigJSON); + server.on("/runtimeConfigJSON", HTTP_GET, handleRuntimeConfigJSON); + server.on("/PWCJSON", HTTP_GET, handlePWCJSON); + server.on("/BLEScan", HTTP_GET, handleBTScanner); + server.on("/send_settings", HTTP_GET, handleSendSettings); } void HTTPRoutes::setupUpdateRoutes(WebServer& server) { - // Update routes - server.on("/login", HTTP_GET, handleLogin); - server.on("/OTAIndex", HTTP_GET, handleOTAUpdate); - - // File upload handler - server.on("/update", HTTP_POST, - []() { - currentServer->sendHeader("Connection", "close"); - currentServer->send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - }, - handleFileUpload - ); -} + server.on("/login", HTTP_GET, handleLogin); + server.on("/OTAIndex", HTTP_GET, handleOTAUpdate); + server.on( + "/update", HTTP_POST, + []() { + currentServer->sendHeader("Connection", "close"); + currentServer->send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + }, + handleFileUpload); +} +void HTTPRoutes::_handleSendSettings() { HTTPSettings::processSettings(*currentServer); } From cc3d869a296e679593b8c98fb553944c665f0ea7 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 21:08:16 -0600 Subject: [PATCH 03/25] Fixed reboot handler --- include/http/HTTPRoutes.h | 6 ++++-- src/http/HTTPRoutes.cpp | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/include/http/HTTPRoutes.h b/include/http/HTTPRoutes.h index e546170c..2db1bb68 100644 --- a/include/http/HTTPRoutes.h +++ b/include/http/HTTPRoutes.h @@ -33,7 +33,8 @@ class HTTPRoutes { static HandlerFunction handleLogin; static HandlerFunction handleOTAUpdate; static HandlerFunction handleFileUpload; - static HandlerFunction handleSendSettings; // Added this handler + static HandlerFunction handleSendSettings; + static HandlerFunction handleReboot; // Setup function to register all routes static void setupRoutes(WebServer& server); @@ -67,5 +68,6 @@ class HTTPRoutes { static void _handleLogin(); static void _handleOTAUpdate(); static void _handleFileUpload(); - static void _handleSendSettings(); // Added this handler implementation + static void _handleSendSettings(); + static void _handleReboot(); }; diff --git a/src/http/HTTPRoutes.cpp b/src/http/HTTPRoutes.cpp index 59750bcf..9beb9f66 100644 --- a/src/http/HTTPRoutes.cpp +++ b/src/http/HTTPRoutes.cpp @@ -34,6 +34,7 @@ HTTPRoutes::HandlerFunction HTTPRoutes::handleLogin = nullptr; HTTPRoutes::HandlerFunction HTTPRoutes::handleOTAUpdate = nullptr; HTTPRoutes::HandlerFunction HTTPRoutes::handleFileUpload = nullptr; HTTPRoutes::HandlerFunction HTTPRoutes::handleSendSettings = nullptr; +HTTPRoutes::HandlerFunction HTTPRoutes::handleReboot = nullptr; void HTTPRoutes::initialize(WebServer& server) { currentServer = &server; @@ -55,6 +56,7 @@ void HTTPRoutes::initialize(WebServer& server) { handleOTAUpdate = []() { _handleOTAUpdate(); }; handleFileUpload = []() { _handleFileUpload(); }; handleSendSettings = []() { _handleSendSettings(); }; + handleReboot = []() { _handleReboot(); }; } void HTTPRoutes::_handleIndexFile() { @@ -220,9 +222,7 @@ void HTTPRoutes::_handleOTAUpdate() { currentServer->send(200, "text/html", OTAServerIndex); } -void HTTPRoutes::_handleFileUpload() { - HTTPFirmware::handleOTAUpdate(*currentServer); -} +void HTTPRoutes::_handleFileUpload() { HTTPFirmware::handleOTAUpdate(*currentServer); } void HTTPRoutes::setupRoutes(WebServer& server) { initialize(server); @@ -266,6 +266,7 @@ void HTTPRoutes::setupControlRoutes(WebServer& server) { server.on("/PWCJSON", HTTP_GET, handlePWCJSON); server.on("/BLEScan", HTTP_GET, handleBTScanner); server.on("/send_settings", HTTP_GET, handleSendSettings); + server.on("/reboot.html", HTTP_GET, handleReboot); } void HTTPRoutes::setupUpdateRoutes(WebServer& server) { @@ -282,3 +283,13 @@ void HTTPRoutes::setupUpdateRoutes(WebServer& server) { } void HTTPRoutes::_handleSendSettings() { HTTPSettings::processSettings(*currentServer); } + +void HTTPRoutes::_handleReboot() { + ss2k->rebootFlag = true; + String response = + "Please wait while your SmartSpin2k reboots."; + currentServer->send(200, "text/html", response); + ss2k->rebootFlag = true; +} From 0f6854f4b8d4d1f5760ae1c659c2eac958041ab8 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 21:22:59 -0600 Subject: [PATCH 04/25] sending logs before reboot --- src/Main.cpp | 27 ++++++++++++++------------- src/http/HTTPFirmware.cpp | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 43cdb07a..a8617db5 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -121,8 +121,8 @@ void setup() { // Check for firmware update. It's important that this stays before BLE & // HTTP setup because otherwise they use too much traffic and the device // fails to update which really sucks when it corrupts your settings. -WiFiManager::startWifi(); -HTTPFirmware::checkForUpdates(); + WiFiManager::startWifi(); + HTTPFirmware::checkForUpdates(); pinMode(currentBoard.shiftUpPin, INPUT_PULLUP); // Push-Button with input Pullup pinMode(currentBoard.shiftDownPin, INPUT_PULLUP); // Push-Button with input Pullup @@ -236,11 +236,12 @@ void SS2K::maintenanceLoop(void *pvParameters) { // Handle flag set for rebooting if (ss2k->rebootFlag) { static bool _loopOnce = false; - vTaskDelay(1000 / portTICK_RATE_MS); // Let the main task loop complete once before rebooting if (_loopOnce) { + SS2K_LOG(MAIN_LOG_TAG, "Reboot flag set."); // Important to keep this delay high in order to allow coms to finish. - vTaskDelay(1000 / portTICK_RATE_MS); + logHandler.writeLogs(); + vTaskDelay(2000 / portTICK_RATE_MS); ESP.restart(); } _loopOnce = true; @@ -283,7 +284,7 @@ void SS2K::maintenanceLoop(void *pvParameters) { // Inactivity detected if (((millis() - rebootTimer) > 1800000)) { // Timer expired - SS2K_LOGW(MAIN_LOG_TAG, "Rebooting due to inactivity."); + SS2K_LOG(MAIN_LOG_TAG, "Rebooting due to inactivity."); ss2k->rebootFlag = true; logHandler.writeLogs(); webSocketAppender.Loop(); @@ -387,12 +388,12 @@ void SS2K::FTMSModeShiftModifier() { } void SS2K::restartWifi() { - httpServer.stop(); - vTaskDelay(100 / portTICK_RATE_MS); - WiFiManager::stopWifi(); - vTaskDelay(100 / portTICK_RATE_MS); - WiFiManager::startWifi(); - httpServer.start(); + httpServer.stop(); + vTaskDelay(100 / portTICK_RATE_MS); + WiFiManager::stopWifi(); + vTaskDelay(100 / portTICK_RATE_MS); + WiFiManager::startWifi(); + httpServer.start(); } void SS2K::moveStepper() { @@ -471,7 +472,7 @@ void SS2K::moveStepper() { if (rtConfig->cad.getValue() > 1) { stepper->enableOutputs(); stepper->setAutoEnable(false); - }else{ + } else { stepper->setAutoEnable(true); } @@ -531,7 +532,7 @@ void SS2K::resetIfShiftersHeld() { userConfig->saveToLittleFS(); vTaskDelay(200 / portTICK_PERIOD_MS); } - ESP.restart(); + ss2k->rebootFlag = true; } } diff --git a/src/http/HTTPFirmware.cpp b/src/http/HTTPFirmware.cpp index 967090af..9cd9c589 100644 --- a/src/http/HTTPFirmware.cpp +++ b/src/http/HTTPFirmware.cpp @@ -199,7 +199,7 @@ void HTTPFirmware::handleOTAUpdate(WebServer& server) { SS2K_LOG(HTTP_SERVER_LOG_TAG, "Update Success: %u bytes\nRebooting...", upload.totalSize); server.send(200, "text/plain", "Update successful. Rebooting..."); delay(1000); - ESP.restart(); + ss2k->rebootFlag = true; } else { Update.printError(Serial); } From 72232de09f9eae25c1819f46ad04910be4b962da Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 22:31:44 -0600 Subject: [PATCH 05/25] New instant http settings page (backwards compatable) --- data/settings.html | 69 ++++-- include/http/HTTPSettings.h | 4 +- src/http/HTTPSettings.cpp | 428 ++++++++++++++++++------------------ 3 files changed, 264 insertions(+), 237 deletions(-) diff --git a/data/settings.html b/data/settings.html index bfdb371a..28ad000d 100644 --- a/data/settings.html +++ b/data/settings.html @@ -21,7 +21,7 @@

Settings

-
+
@@ -36,7 +36,7 @@

@@ -48,7 +48,7 @@

-

- @@ -86,7 +86,7 @@

+ onchange="updateSliderAndSend(this, document.getElementById('shiftStepValue'))" />

@@ -109,10 +109,9 @@

value="-"> + onchange="updateSliderAndSend(this, document.getElementById('inclineMultiplierValue'))" /> -

@@ -132,7 +131,7 @@

value="-"> + onchange="updateSliderAndSend(this, document.getElementById('ERGSensitivityValue'))" /> @@ -152,7 +151,7 @@

+ onchange="updateSliderAndSend(this, document.getElementById('minWattsValue'))" />

@@ -171,7 +170,7 @@

+ onchange="updateSliderAndSend(this, document.getElementById('maxWattsValue'))" /> @@ -192,7 +191,7 @@

+ onchange="updateSliderAndSend(this, document.getElementById('stepperPowerValue'))" /> @@ -205,7 +204,7 @@

@@ -218,7 +217,7 @@

@@ -231,7 +230,7 @@

@@ -240,7 +239,7 @@

Shifter DirectionChange Shifter Direction

@@ -250,14 +249,13 @@

10.000

- +
+

Show @@ -67,7 +67,7 @@

+
-
-
-
-
-
- - +

@@ -358,13 +356,18 @@

valueElement.innerHTML = x; } + function updateSliderAndSend(element, valueElement) { + updateSlider(element.value, valueElement); + sendSetting(element); + } + function clickStep(element, incOrDec) { if (incOrDec == "+") { element.value = parseFloat(element.value) + parseFloat(element.step); } else { element.value = parseFloat(element.value) - parseFloat(element.step); } - updateSlider(element.value, document.getElementById(element.name + 'Value')); + updateSliderAndSend(element, document.getElementById(element.name + 'Value')); } function toggleShowPassword() { @@ -375,6 +378,32 @@

x.type = "password"; } } + + function sendSetting(element) { + var params = new URLSearchParams(); + + // Add the changed setting + var value = element.type === 'checkbox' ? element.checked : element.value; + if((element.type == 'checkbox') && element.checked){ + params.append(element.name, value);} + + // Always include shiftStep + params.append('shiftStep', document.getElementById('shiftStep').value); + + // Always include all checkbox values that are true + const checkboxes = ['stealthChop', 'autoUpdate', 'stepperDir', 'shifterDir', 'udpLogEnabled']; + checkboxes.forEach(id => { + if (document.getElementById(id).checked) { + params.append(id, 'true'); + } + }); + + fetch('/send_settings?' + params.toString(), { + method: 'GET' + }).catch(function(error) { + console.error('Error:', error); + }); + } - \ No newline at end of file + diff --git a/include/http/HTTPSettings.h b/include/http/HTTPSettings.h index 847c80bd..9a4d0795 100644 --- a/include/http/HTTPSettings.h +++ b/include/http/HTTPSettings.h @@ -21,11 +21,11 @@ class HTTPSettings { // Network settings static void processNetworkSettings(WebServer& server); static void processDeviceSettings(WebServer& server); - static void processStepperSettings(WebServer& server); + static bool processStepperSettings(WebServer& server); static void processPowerSettings(WebServer& server); static void processERGSettings(WebServer& server); static void processFeatureSettings(WebServer& server); - static bool processBluetoothSettings(WebServer& server); // Changed return type to bool + static bool processBluetoothSettings(WebServer& server); static void processPWCSettings(WebServer& server); // Helper functions diff --git a/src/http/HTTPSettings.cpp b/src/http/HTTPSettings.cpp index c4f6e12f..42948dad 100644 --- a/src/http/HTTPSettings.cpp +++ b/src/http/HTTPSettings.cpp @@ -10,276 +10,274 @@ #include "SS2KLog.h" void HTTPSettings::processSettings(WebServer& server) { - bool wasBTUpdate = false; - bool wasSettingsUpdate = false; - bool reboot = false; - - // Process Network Settings - processNetworkSettings(server); - processDeviceSettings(server); - processStepperSettings(server); - processPowerSettings(server); - processERGSettings(server); + bool wasBTUpdate = false; + bool wasSettingsUpdate = false; + bool reboot = false; + + // Process Network Settings + processNetworkSettings(server); + processDeviceSettings(server); + wasSettingsUpdate = processStepperSettings(server); + processPowerSettings(server); + processERGSettings(server); + if (wasSettingsUpdate) { processFeatureSettings(server); - - // Process Bluetooth settings and capture the result - wasBTUpdate = processBluetoothSettings(server); - - processPWCSettings(server); - - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Config Updated From Web"); - ss2k->saveFlag = true; - - if (reboot) { - sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, true); - ss2k->rebootFlag = true; - } else { - sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, false); - } + } + // Process Bluetooth settings and capture the result + wasBTUpdate = processBluetoothSettings(server); + + processPWCSettings(server); + + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Config Updated From Web"); + ss2k->saveFlag = true; + + if (reboot) { + sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, true); + ss2k->rebootFlag = true; + } else { + sendSettingsResponse(server, wasBTUpdate, wasSettingsUpdate, false); + } } void HTTPSettings::processNetworkSettings(WebServer& server) { - if (!server.arg("ssid").isEmpty()) { - String ssid = server.arg("ssid"); - ssid.trim(); - userConfig->setSsid(ssid); - } - - if (!server.arg("password").isEmpty()) { - String password = server.arg("password"); - password.trim(); - userConfig->setPassword(password); - } + if (!server.arg("ssid").isEmpty()) { + String ssid = server.arg("ssid"); + ssid.trim(); + userConfig->setSsid(ssid); + } + + if (!server.arg("password").isEmpty()) { + String password = server.arg("password"); + password.trim(); + userConfig->setPassword(password); + } } void HTTPSettings::processDeviceSettings(WebServer& server) { - if (!server.arg("deviceName").isEmpty()) { - String deviceName = server.arg("deviceName"); - deviceName.trim(); - userConfig->setDeviceName(deviceName); - } + if (!server.arg("deviceName").isEmpty()) { + String deviceName = server.arg("deviceName"); + deviceName.trim(); + userConfig->setDeviceName(deviceName); + } } -void HTTPSettings::processStepperSettings(WebServer& server) { - if (!server.arg("shiftStep").isEmpty()) { - uint64_t shiftStep = server.arg("shiftStep").toInt(); - if (shiftStep >= MIN_SHIFT_STEP && shiftStep <= MAX_SHIFT_STEP) { - userConfig->setShiftStep(shiftStep); - } +bool HTTPSettings::processStepperSettings(WebServer& server) { + bool wasSettingsUpdate = false; + if (!server.arg("shiftStep").isEmpty()) { + wasSettingsUpdate = true; // shiftStep is our signal that it was a settings update. + uint64_t shiftStep = server.arg("shiftStep").toInt(); + if (shiftStep >= MIN_SHIFT_STEP && shiftStep <= MAX_SHIFT_STEP) { + userConfig->setShiftStep(shiftStep); } + } - if (!server.arg("stepperPower").isEmpty()) { - uint64_t stepperPower = server.arg("stepperPower").toInt(); - if (stepperPower >= MIN_STEPPER_POWER && stepperPower <= MAX_STEPPER_POWER) { - userConfig->setStepperPower(stepperPower); - ss2k->updateStepperPower(); - } - } - - if (!server.arg("stepperDir").isEmpty()) { - userConfig->setStepperDir(true); - } else { - userConfig->setStepperDir(false); - } - - if (!server.arg("stealthChop").isEmpty()) { - userConfig->setStealthChop(true); - ss2k->updateStealthChop(); - } else { - userConfig->setStealthChop(false); - ss2k->updateStealthChop(); + if (!server.arg("stepperPower").isEmpty()) { + uint64_t stepperPower = server.arg("stepperPower").toInt(); + if (stepperPower >= MIN_STEPPER_POWER && stepperPower <= MAX_STEPPER_POWER) { + userConfig->setStepperPower(stepperPower); + ss2k->updateStepperPower(); } + } + + if (!server.arg("stepperDir").isEmpty()) { + userConfig->setStepperDir(true); + } else if (wasSettingsUpdate) { + userConfig->setStepperDir(false); + } + + if (!server.arg("stealthChop").isEmpty()) { + userConfig->setStealthChop(true); + ss2k->updateStealthChop(); + } else if (wasSettingsUpdate) { + userConfig->setStealthChop(false); + ss2k->updateStealthChop(); + } + return wasSettingsUpdate; } void HTTPSettings::processPowerSettings(WebServer& server) { - if (!server.arg("maxWatts").isEmpty()) { - uint64_t maxWatts = server.arg("maxWatts").toInt(); - if (maxWatts >= 0 && maxWatts <= DEFAULT_MAX_WATTS) { - userConfig->setMaxWatts(maxWatts); - } + if (!server.arg("maxWatts").isEmpty()) { + uint64_t maxWatts = server.arg("maxWatts").toInt(); + if (maxWatts >= 0 && maxWatts <= DEFAULT_MAX_WATTS) { + userConfig->setMaxWatts(maxWatts); } + } - if (!server.arg("minWatts").isEmpty()) { - uint64_t minWatts = server.arg("minWatts").toInt(); - if (minWatts >= 0 && minWatts <= DEFAULT_MIN_WATTS) { - userConfig->setMinWatts(minWatts); - } + if (!server.arg("minWatts").isEmpty()) { + uint64_t minWatts = server.arg("minWatts").toInt(); + if (minWatts >= 0 && minWatts <= DEFAULT_MIN_WATTS) { + userConfig->setMinWatts(minWatts); } + } - if (!server.arg("powerCorrectionFactor").isEmpty()) { - float powerCorrectionFactor = server.arg("powerCorrectionFactor").toFloat(); - if (powerCorrectionFactor >= MIN_PCF && powerCorrectionFactor <= MAX_PCF) { - userConfig->setPowerCorrectionFactor(powerCorrectionFactor); - } + if (!server.arg("powerCorrectionFactor").isEmpty()) { + float powerCorrectionFactor = server.arg("powerCorrectionFactor").toFloat(); + if (powerCorrectionFactor >= MIN_PCF && powerCorrectionFactor <= MAX_PCF) { + userConfig->setPowerCorrectionFactor(powerCorrectionFactor); } + } } void HTTPSettings::processERGSettings(WebServer& server) { - if (!server.arg("ERGSensitivity").isEmpty()) { - float ERGSensitivity = server.arg("ERGSensitivity").toFloat(); - if (ERGSensitivity >= 0.1 && ERGSensitivity <= 20) { - userConfig->setERGSensitivity(ERGSensitivity); - } + if (!server.arg("ERGSensitivity").isEmpty()) { + float ERGSensitivity = server.arg("ERGSensitivity").toFloat(); + if (ERGSensitivity >= 0.1 && ERGSensitivity <= 20) { + userConfig->setERGSensitivity(ERGSensitivity); } + } - if (!server.arg("inclineMultiplier").isEmpty()) { - float inclineMultiplier = server.arg("inclineMultiplier").toFloat(); - if (inclineMultiplier >= 0 && inclineMultiplier <= 10) { - userConfig->setInclineMultiplier(inclineMultiplier); - } + if (!server.arg("inclineMultiplier").isEmpty()) { + float inclineMultiplier = server.arg("inclineMultiplier").toFloat(); + if (inclineMultiplier >= 0 && inclineMultiplier <= 10) { + userConfig->setInclineMultiplier(inclineMultiplier); } + } } void HTTPSettings::processFeatureSettings(WebServer& server) { - if (!server.arg("autoUpdate").isEmpty()) { - userConfig->setAutoUpdate(true); - } else { - userConfig->setAutoUpdate(false); - } - - if (!server.arg("shifterDir").isEmpty()) { - userConfig->setShifterDir(true); - } else { - userConfig->setShifterDir(false); - } - - if (!server.arg("udpLogEnabled").isEmpty()) { - userConfig->setUdpLogEnabled(true); - } else { - userConfig->setUdpLogEnabled(false); - } + if (!server.arg("autoUpdate").isEmpty()) { + userConfig->setAutoUpdate(true); + } else { + userConfig->setAutoUpdate(false); + } + + if (!server.arg("shifterDir").isEmpty()) { + userConfig->setShifterDir(true); + } else { + userConfig->setShifterDir(false); + } + + if (!server.arg("udpLogEnabled").isEmpty()) { + userConfig->setUdpLogEnabled(true); + } else { + userConfig->setUdpLogEnabled(false); + } } bool HTTPSettings::processBluetoothSettings(WebServer& server) { - bool wasBTUpdate = false; - - if (!server.arg("blePMDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("blePMDropdown")) { - String powerMeter = server.arg("blePMDropdown"); - if (powerMeter != userConfig->getConnectedPowerMeter()) { - userConfig->setConnectedPowerMeter(powerMeter); - spinBLEClient.reconnectAllDevices(); - } - } else { - userConfig->setConnectedPowerMeter("any"); - } + bool wasBTUpdate = false; + + if (!server.arg("blePMDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("blePMDropdown")) { + String powerMeter = server.arg("blePMDropdown"); + if (powerMeter != userConfig->getConnectedPowerMeter()) { + userConfig->setConnectedPowerMeter(powerMeter); + spinBLEClient.reconnectAllDevices(); + } + } else { + userConfig->setConnectedPowerMeter("any"); } - - if (!server.arg("bleHRDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("bleHRDropdown")) { - String heartMonitor = server.arg("bleHRDropdown"); - if (heartMonitor != userConfig->getConnectedHeartMonitor()) { - spinBLEClient.reconnectAllDevices(); - } - userConfig->setConnectedHeartMonitor(heartMonitor); - } else { - userConfig->setConnectedHeartMonitor("any"); - } + } + + if (!server.arg("bleHRDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("bleHRDropdown")) { + String heartMonitor = server.arg("bleHRDropdown"); + if (heartMonitor != userConfig->getConnectedHeartMonitor()) { + spinBLEClient.reconnectAllDevices(); + } + userConfig->setConnectedHeartMonitor(heartMonitor); + } else { + userConfig->setConnectedHeartMonitor("any"); } - - if (!server.arg("bleRemoteDropdown").isEmpty()) { - wasBTUpdate = true; - if (server.arg("bleRemoteDropdown")) { - String remote = server.arg("bleRemoteDropdown"); - if (remote != userConfig->getConnectedRemote()) { - spinBLEClient.reconnectAllDevices(); - } - userConfig->setConnectedRemote(remote); - } else { - userConfig->setConnectedRemote("any"); - } + } + + if (!server.arg("bleRemoteDropdown").isEmpty()) { + wasBTUpdate = true; + if (server.arg("bleRemoteDropdown")) { + String remote = server.arg("bleRemoteDropdown"); + if (remote != userConfig->getConnectedRemote()) { + spinBLEClient.reconnectAllDevices(); + } + userConfig->setConnectedRemote(remote); + } else { + userConfig->setConnectedRemote("any"); } + } - return wasBTUpdate; + return wasBTUpdate; } void HTTPSettings::processPWCSettings(WebServer& server) { - if (!server.arg("session1HR").isEmpty()) { - userPWC->session1HR = server.arg("session1HR").toInt(); - } - if (!server.arg("session1Pwr").isEmpty()) { - userPWC->session1Pwr = server.arg("session1Pwr").toInt(); - } - if (!server.arg("session2HR").isEmpty()) { - userPWC->session2HR = server.arg("session2HR").toInt(); - } - if (!server.arg("session2Pwr").isEmpty()) { - userPWC->session2Pwr = server.arg("session2Pwr").toInt(); - } - if (!server.arg("hr2Pwr").isEmpty()) { - userPWC->hr2Pwr = true; - } else { - userPWC->hr2Pwr = false; - } + if (!server.arg("session1HR").isEmpty()) { + userPWC->session1HR = server.arg("session1HR").toInt(); + } else { // wasn't pwc settings page + return; + } + if (!server.arg("session1Pwr").isEmpty()) { + userPWC->session1Pwr = server.arg("session1Pwr").toInt(); + } + if (!server.arg("session2HR").isEmpty()) { + userPWC->session2HR = server.arg("session2HR").toInt(); + } + if (!server.arg("session2Pwr").isEmpty()) { + userPWC->session2Pwr = server.arg("session2Pwr").toInt(); + } + if (!server.arg("hr2Pwr").isEmpty()) { + userPWC->hr2Pwr = true; + } else { + userPWC->hr2Pwr = false; + } } void HTTPSettings::sendSettingsResponse(WebServer& server, bool wasBTUpdate, bool wasSettingsUpdate, bool requiresReboot) { - String response; - if (wasBTUpdate) { - response = buildRedirectResponse("Selections Saved!", "/bluetoothscanner.html"); - } else if (wasSettingsUpdate || requiresReboot) { - response = buildRedirectResponse( - "Network settings will be applied at next reboot.
" - "Everything else is available immediately.", - "/settings.html" - ); - } else { - response = buildRedirectResponse( - "Network settings will be applied at next reboot.
" - "Everything else is available immediately.", - "/index.html" - ); - } - - if (requiresReboot) { - response = buildRedirectResponse( - "Please wait while your settings are saved and SmartSpin2k reboots.", - "/bluetoothscanner.html", - 5000 - ); - } - - server.send(200, "text/html", response); + String response; + if (wasBTUpdate) { + response = buildRedirectResponse("Selections Saved!", "/bluetoothscanner.html"); + } else if (wasSettingsUpdate || requiresReboot) { + response = buildRedirectResponse( + "Network settings will be applied at next reboot.
" + "Everything else is available immediately.", + "/settings.html"); + } else { + response = buildRedirectResponse( + "Network settings will be applied at next reboot.
" + "Everything else is available immediately.", + "/index.html"); + } + + if (requiresReboot) { + response = buildRedirectResponse("Please wait while your settings are saved and SmartSpin2k reboots.", "/bluetoothscanner.html", 5000); + } + + server.send(200, "text/html", response); } String HTTPSettings::buildRedirectResponse(const String& message, const String& page, int delay) { - return "

" + message + - "

"; + return "

" + message + "

"; } bool HTTPSettings::processCheckbox(WebServer& server, const char* name, bool defaultValue) { - if (!server.hasArg(name)) { - return defaultValue; - } - return server.arg(name) == "true" || server.arg(name) == "1"; + if (!server.hasArg(name)) { + return defaultValue; + } + return server.arg(name) == "true" || server.arg(name) == "1"; } float HTTPSettings::processFloatValue(WebServer& server, const char* name, float min, float max) { - if (!server.hasArg(name)) { - return min; - } - float value = server.arg(name).toFloat(); - if (value < min) return min; - if (value > max) return max; - return value; + if (!server.hasArg(name)) { + return min; + } + float value = server.arg(name).toFloat(); + if (value < min) return min; + if (value > max) return max; + return value; } int HTTPSettings::processIntValue(WebServer& server, const char* name, int min, int max) { - if (!server.hasArg(name)) { - return min; - } - int value = server.arg(name).toInt(); - if (value < min) return min; - if (value > max) return max; - return value; + if (!server.hasArg(name)) { + return min; + } + int value = server.arg(name).toInt(); + if (value < min) return min; + if (value > max) return max; + return value; } String HTTPSettings::processStringValue(WebServer& server, const char* name) { - if (!server.hasArg(name)) { - return ""; - } - return server.arg(name); + if (!server.hasArg(name)) { + return ""; + } + return server.arg(name); } From b7e1a31c66ad54dbf411f4838908dc273b8f242e Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 17 Nov 2024 22:52:58 -0600 Subject: [PATCH 06/25] cleaned up .css --- data/settings.html | 3 - data/style.css | 238 +-------------------------------------------- 2 files changed, 1 insertion(+), 240 deletions(-) diff --git a/data/settings.html b/data/settings.html index 28ad000d..cf4219c1 100644 --- a/data/settings.html +++ b/data/settings.html @@ -382,15 +382,12 @@

function sendSetting(element) { var params = new URLSearchParams(); - // Add the changed setting var value = element.type === 'checkbox' ? element.checked : element.value; if((element.type == 'checkbox') && element.checked){ params.append(element.name, value);} - // Always include shiftStep params.append('shiftStep', document.getElementById('shiftStep').value); - // Always include all checkbox values that are true const checkboxes = ['stealthChop', 'autoUpdate', 'stepperDir', 'shifterDir', 'udpLogEnabled']; checkboxes.forEach(id => { if (document.getElementById(id).checked) { diff --git a/data/style.css b/data/style.css index d5e8b2a6..1f402b66 100644 --- a/data/style.css +++ b/data/style.css @@ -1,237 +1 @@ -html { - font-family: sans-serif; - display: inline-block; - margin: 5px auto; - text-align: center; - background-color: #03245c; - line-height: 1em; -} - -label { - font-size: medium; -} - -div { - font-size: medium; -} - -a { - color: #000000; -} -a:visited { - color: #000000; -} -h1 { - color: #03245c; - padding: 0.5rem; - line-height: 1em; -} -h2 { - color: #000000; - font-size: 1.5rem; - font-weight: bold; -} -p { - font-size: 1rem; -} -.button { - display: inline-block; - background-color: #2a9df4; - border: line; - border-radius: 4px; - color: #d0efff; - padding: 10px 40px; - text-decoration: none; - font-size: 20px; - margin: 0px; - cursor: pointer; -} -.button2 { - background-color: #f44336; - padding: 10px 35px; -} -.switch { - position: relative; - display: inline-block; - width: 80px; - height: 40px; -} -.switch input { - display: none; -} -.slider { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - transition: 0.4s; - background-color: #03254c; - border-radius: 34px; -} -.slider:before { - position: absolute; - content: ""; - height: 37px; - width: 37px; - left: 2px; - bottom: 2px; - background-color: #d0efff; - -webkit-transition: 0.4s; - transition: 0.4s; - border-radius: 68px; -} -input:checked + .slider { - transition: 0.4s; - background-color: #2a9df4; -} -input:checked + .slider:before { - -webkit-transform: translateX(38px); - -ms-transform: translateX(38px); - transform: translateX(38px); -} -.slider2 { - -webkit-appearance: none; - margin: 5px; - width: 270px; - height: 20px; - background: #d0efff; - /*outline:8px ridge rgba(170,50,220, .6); - border-radius: 2rem;*/ - outline: none; - -webkit-transition: 0.2s; - transition: opacity 0.2s; -} -.slider2::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 30px; - height: 30px; - background: #03254c; - cursor: pointer; -} -.slider2::-moz-range-thumb { - width: 30px; - height: 30px; - background: #1167b1; - cursor: pointer; -} - -table.center { - margin-left: auto; - margin-right: auto; -} - -.tooltip { - position: relative; - display: inline-block; - border-bottom: 1px dotted #03254c; -} - -.tooltip .tooltiptext { - visibility: hidden; - width: 120px; - background-color: #03254c; - color: #d0efff; - text-align: center; - border-radius: 6px; - padding: 5px 0; - - /* Position the tooltip */ - position: absolute; - z-index: 1; -} - -.tooltip:hover .tooltiptext { - visibility: visible; -} - -.watermark { - display: inline; - position: fixed; - top: 0px; - left: 0px; - transform: translate(calc(50vw - 200px), calc(50vh - 170px)) rotate(45deg); - transition: 0.4s ease-in-out; - opacity: 0.7; - z-index: 99; - color: grey; - font-size: 7rem; -} - -.watermark:hidden { - transition: visibility 0s 2s, opacity 2s linear; -} - -.shiftButton { - -webkit-appearance: none; - -webkit-text-stroke: 2px rgba(104, 104, 104, 0.412); - appearance: auto; - width: 16%; - height: 6rem; - background: #03254c; - color: white; - cursor: pointer; - font-weight: bold; - font-size: calc(1vw + 1vh); -} - -.shiftBox { - background: #2a9df4; - color: white; - font-weight: bold; - font-size: calc(1vw + 1vh); - width: 4%; - text-align: center; -} - -body { - display: block; - margin: 0 auto; - background-color: #1167b1; - opacity: 1; - transition: 0.5s ease-in-out; - height: 100%; - width: 100%; -} - -fieldset { - border: 10px solid; - border-color: #1e3252; - box-sizing: border-box; - grid-area: 1 / 1; - padding: 5px; - margin: 0 auto; - z-index: -1; -} - -.confirmation-dialog { - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - background-color: rgba(201, 201, 201, 0.7); - z-index: 100; -} - -.confirmation-dialog > .confirmation-panel { - background-color: #03245c; - color: white; - padding: 20px; - border-radius: 10px; -} - -.confirmation-panel > .confirmation-buttongroup { - padding-top: 10px; -} - -.confirmation-buttongroup > input[type="button"] { - width: 75px; - height: 40px; - font-size: 1em; - border-radius: 10px; -} +html{font-family:sans-serif;display:inline-block;margin:5px auto;text-align:center;background-color:#03245c;line-height:1em}label,div{font-size:medium}a,a:visited{color:#000}h1{color:#03245c;padding:.5rem;line-height:1em}h2{color:#000;font-size:1.5rem;font-weight:700}p{font-size:1rem}.switch{position:relative;display:inline-block;width:80px;height:40px}.switch input{display:none}.slider{position:absolute;inset:0;transition:.4s;background-color:#03254c;border-radius:34px}.slider:before{position:absolute;content:"";height:37px;width:37px;left:2px;bottom:2px;background-color:#d0efff;transition:.4s;border-radius:68px}input:checked+.slider{background-color:#2a9df4}input:checked+.slider:before{transform:translateX(38px)}.slider2{-webkit-appearance:none;margin:5px;width:270px;height:20px;background:#d0efff;outline:0;transition:opacity .2s}.slider2::-webkit-slider-thumb{-webkit-appearance:none;width:30px;height:30px;background:#03254c;cursor:pointer}.slider2::-moz-range-thumb{width:30px;height:30px;background:#1167b1;cursor:pointer}table.center{margin:0 auto}.tooltip{position:relative;display:inline-block;border-bottom:1px dotted #03254c}.tooltip .tooltiptext{visibility:hidden;width:120px;background-color:#03254c;color:#d0efff;text-align:center;border-radius:6px;padding:5px 0;position:absolute;z-index:1}.tooltip:hover .tooltiptext{visibility:visible}.watermark{display:inline;position:fixed;top:0;left:0;transform:translate(calc(50vw - 200px),calc(50vh - 170px))rotate(45deg);transition:.4s;opacity:.7;z-index:99;color:grey;font-size:7rem}.watermark:hidden{transition:visibility 0s 2s,opacity 2s}.shiftButton{-webkit-appearance:none;-webkit-text-stroke:2px rgba(104,104,104,.412);appearance:auto;width:16%;height:6rem;background:#03254c;color:#fff;cursor:pointer;font-weight:700;font-size:calc(1vw + 1vh)}.shiftBox{background:#2a9df4;color:#fff;font-weight:700;font-size:calc(1vw + 1vh);width:4%;text-align:center}body{display:block;margin:0 auto;background-color:#1167b1;opacity:1;transition:.5s;height:100%;width:100%}fieldset{border:10px solid #1e3252;box-sizing:border-box;grid-area:1/1;padding:5px;margin:0 auto;z-index:-1}.confirmation-dialog{display:flex;align-items:center;justify-content:center;position:fixed;inset:0;background-color:rgba(201,201,201,.7);z-index:100}.confirmation-dialog>.confirmation-panel{background-color:#03245c;color:#fff;padding:20px;border-radius:10px}.confirmation-panel>.confirmation-buttongroup{padding-top:10px}.confirmation-buttongroup>input[type=button]{width:75px;height:40px;font-size:1em;border-radius:10px} From 9b213a3adb8b8c6d1d4726832c63973e8fce6c2a Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Mon, 18 Nov 2024 00:12:01 -0600 Subject: [PATCH 07/25] Modernized UX --- data/bluetoothscanner.html | 422 ++++++++++----------- data/btsimulator.html | 525 ++++++++++++-------------- data/develop.html | 143 ++++--- data/hrtowatts.html | 278 ++++++++------ data/index.html | 139 ++++--- data/settings.html | 736 +++++++++++++++++-------------------- data/shift.html | 159 ++++---- data/status.html | 460 +++++++++++------------ data/streamfit.html | 118 ++++-- data/style.css | 645 +++++++++++++++++++++++++++++++- 10 files changed, 2164 insertions(+), 1461 deletions(-) diff --git a/data/bluetoothscanner.html b/data/bluetoothscanner.html index 575663f3..d026f4dd 100644 --- a/data/bluetoothscanner.html +++ b/data/bluetoothscanner.html @@ -1,238 +1,214 @@ - - + - + + SmartSpin2k Bluetooth Scanner - - + - -
- http://github.com/doudar/SmartSpin2k -

Main Index

-

-
Loading
-

-

Select Bluetooth Devices

-

-

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

-
-

-
-

-
-

-
-

-
-

-
-
Power Correction FactorIncrease or decrease - to correct the power transmitted from your bike.
-
-
- 1x +
+
+ +
+ +
+ +

Bluetooth Devices

+ +
+
+

Connected Devices

+
+
+ Power Meter + loading +
+
+ Heart Monitor + loading +
+
+ Remote + loading +
+
+
+ + +
+

Device Selection

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Power Settings

+
+ +
+ +
+
+ 1x +
+
- - - -


-
- - -

-
- - -

Page Help

-
-

- - - - - \ No newline at end of file + function loadCss() { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'style.css'; + document.head.appendChild(link); + } + + window.addEventListener('load', () => { + setTimeout(loadCss, 100); + requestConfigValues(); + + // Retry loading if values are still default + setInterval(() => { + if (document.getElementById('connectedPowerMeter').textContent === 'loading') { + requestConfigValues(); + } + }, 1000); + }); + + + diff --git a/data/btsimulator.html b/data/btsimulator.html index c362cfad..39812e3e 100644 --- a/data/btsimulator.html +++ b/data/btsimulator.html @@ -1,327 +1,284 @@ - - - + - - SmartSpin2k Web Server - + + + SmartSpin2k BLE Simulator + - -
-

-
Loading
-

- http://github.com/doudar/SmartSpin2k -

Main Index

-

BLE Device Simulator

+
+
+ +
-

Sim Heart Rate

-

-

-

+
+ +

BLE Simulator

-

Sim Power Output

-
-

- -   - - - - -

-

- -

-
-

- -

+
+
+ +
+
+

Heart Rate

+ +
+
+
+ -- BPM +
+
+ +
+
+
-

Sim CAD Output

-
-

- -   - - - - -

-

- -

-
-

- -

+ +
+
+

Power Output

+ +
+
+
+ -- W + +
+
+ +
+
+
-

Trainer Simulator

-

Enable ERG

-

- -

-

ERG Target Watts

-

-

-

- -

- - + function loadCss() { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'style.css'; + document.head.appendChild(link); + } - \ No newline at end of file + window.addEventListener('load', () => { + setTimeout(loadCss, 100); + setTimeout(requestConfigValues, 500); + }); + + + diff --git a/data/develop.html b/data/develop.html index da2d3955..36e617cf 100644 --- a/data/develop.html +++ b/data/develop.html @@ -1,48 +1,103 @@ - - + - + + + + + - - \ No newline at end of file + window.addEventListener('load', () => { + setTimeout(loadCss, 100); + }); + + + diff --git a/data/hrtowatts.html b/data/hrtowatts.html index bcf94fe3..c0d7ae26 100644 --- a/data/hrtowatts.html +++ b/data/hrtowatts.html @@ -1,126 +1,170 @@ - - - + - - SmartSpin2k Web Server - + + + SmartSpin2k Heart Rate to Power + - -
- http://github.com/doudar/SmartSpin2k -

Main Index

-

-
Loading
-

-

Physical Working Capacity

-

-

For the most accurate power estimation when not using a power meter, please submit the following information. -
Note: You can get estimated watts from any outdoor ride recorded in Strava with heart rate information. -

-
- - - - - - - - - - - - - - - - - - - - - - -
-

Easy Session Average HRSession 1 HR

Average - Heartrate over an easy 1 Hour course.

-
-

Easy Session Average PowerAverage Power over an easy 1 hour - course in watts.

-
-
-

Hard Session Average HRAverage HR over a hard 1 hour - course.

-
-
-

Hard Session Average PowerAverage Power over a hard 1 hour - course in watts.

-
-
-

HR->PWRAutomatically calculate watts using - heart rate when power meter not connected

-
-

- -

Page Help

-
-

- +
+
+ +
- - - \ No newline at end of file + + fetch('/send_settings?' + params.toString(), { method: 'GET' }) + .then(() => { + showSaveStatus('Settings saved successfully', 'success'); + }) + .catch(error => { + showSaveStatus('Failed to save settings', 'error'); + console.error('Error:', error); + }); + }); + + function showSaveStatus(message, type = 'info') { + saveStatus.textContent = message; + saveStatus.className = `status-message ${type}`; + setTimeout(() => { + saveStatus.textContent = ''; + saveStatus.className = 'status-message'; + }, 3000); + } + + function requestConfigValues() { + fetch('/PWCJSON') + .then(response => response.json()) + .then(data => { + document.getElementById('session1HR').value = data.session1HR; + document.getElementById('session1Pwr').value = data.session1Pwr; + document.getElementById('session2HR').value = data.session2HR; + document.getElementById('session2Pwr').value = data.session2Pwr; + document.getElementById('hr2Pwr').checked = data.hr2Pwr; + document.getElementById('loadingWatermark')?.remove(); + }) + .catch(error => console.error('Error:', error)); + } + + function loadCss() { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'style.css'; + document.head.appendChild(link); + } + + window.addEventListener('load', () => { + setTimeout(loadCss, 100); + requestConfigValues(); + + // Retry loading if values are still default + setInterval(() => { + if (document.getElementById('session1HR').value === '0') { + requestConfigValues(); + } + }, 1000); + }); + + + diff --git a/data/index.html b/data/index.html index 516e25c6..df4c60b3 100644 --- a/data/index.html +++ b/data/index.html @@ -1,67 +1,96 @@ - - + - - - SmartSpin2k Web Server - + + + SmartSpin2k + -
http://github.com/doudar/SmartSpin2k -

SmartSpin2k

-

-

Web Shifter

-

Heartrate to Watts Setup

-

Settings

-

Bluetooth Scanner

-

Developer Tools

-

SS2K Help

-

-
+
+
+ +
-

- +
+

SmartSpin2k

+ + + + + +
+ +
SS2K Help
diff --git a/data/settings.html b/data/settings.html index 8f167117..b50f82df 100644 --- a/data/settings.html +++ b/data/settings.html @@ -211,6 +211,7 @@

System Settings

+
diff --git a/data/style.css b/data/style.css index 971059f4..9d6e02e8 100644 --- a/data/style.css +++ b/data/style.css @@ -247,86 +247,82 @@ a:hover{color:#2a9df4} display: grid; gap: 2rem; padding: 1rem 0; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + max-width: 1200px; + margin: 0 auto; } .settings-section { background: rgba(255,255,255,.1); border-radius: 8px; padding: 1.5rem; - margin-bottom: 1rem; + margin-bottom: .5rem; } .setting-group { margin-bottom: 1.5rem; background: rgba(3,37,76,.6); border-radius: 6px; - padding: 1.5rem; + padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,.1); border: 1px solid rgba(255,255,255,.1); } -.setting-group:hover { - background: rgba(3,37,76,.8); - border-color: rgba(255,255,255,.2); -} - -.setting-group h2 { - margin: 0 0 1.5rem; - font-size: 1.2rem; - color: #2a9df4; - border-bottom: 1px solid rgba(42,157,244,.3); - padding-bottom: .5rem; -} - -.setting-label { - display: block; - margin-bottom: .75rem; - font-weight: 500; +/* Text Input Styles */ +.setting-group input[type="text"], +.setting-group input[type="password"] { + width: 100%; + padding: .75rem; + background: rgba(255,255,255,.1); + border: 1px solid rgba(255,255,255,.2); + border-radius: 4px; color: #fff; - font-size: 1.1em; - text-shadow: 0 1px 2px rgba(0,0,0,.2); -} - -.setting-help { - font-size: .875rem; - color: rgba(255,255,255,.7); - margin-top: .25rem; + font-size: 1rem; + box-sizing: border-box; } +/* Slider Styles */ .slider-group { display: flex; align-items: center; - gap: .5rem; + gap: 1rem; min-height: 60px; - padding: .5rem; + padding: 1rem; background: rgba(0,0,0,.2); border-radius: 4px; + justify-content: center; } .slider-container { flex: 1; - position: relative; display: flex; flex-direction: column; align-items: center; + gap: .5rem; + max-width: 400px; + margin: 0 auto; +} + +.slider-container input[type="range"] { + width: 100%; } .value-display { display: flex; align-items: center; justify-content: center; - margin-bottom: .5rem; + margin-bottom: 5.5rem; font-weight: 500; color: #2a9df4; } .value-display span { - margin-left: 2px; + margin-left: 20px; } .adjust-button { - width: 32px; - height: 32px; + width: 50px; + height: 50px; border: none; background: #03254c; color: #fff; @@ -399,25 +395,6 @@ a:hover{color:#2a9df4} display: flex; flex-direction: column; gap: .5rem; - padding: 1rem; - background: rgba(0,0,0,.2); - border-radius: 4px; - transition: background .2s; -} - -.status-item:hover { - background: rgba(0,0,0,.3); -} - -.status-label { - font-size: .9rem; - color: rgba(255,255,255,.7); - text-transform: uppercase; - letter-spacing: .5px; -} - -.status-value { - font-size: 1.1rem; font-weight: 500; color: #2a9df4; } @@ -518,14 +495,14 @@ a:hover{color:#2a9df4} /* StreamFit Styles */ .upload-container{text-align:center;padding:2rem;background:rgba(3,37,76,.6);border-radius:12px;margin-bottom:2rem} -.file-upload{display:flex;flex-direction:column;align-items:center;gap:1rem;padding:2rem;border:2px dashed rgba(255,255,255,.2);border-radius:8px;cursor:pointer;transition:all .3s} +.file-upload{display:flex;flex-direction:column;align-items:center;gap:1rem;padding:1rem;border:2px dashed rgba(255,255,255,.2);border-radius:8px;cursor:pointer;transition:all .3s} .file-upload:hover{border-color:#2a9df4;background:rgba(42,157,244,.1)} .upload-icon{font-size:2.5rem;color:#2a9df4} .file-input{display:none} /* Metrics Display */ -.metrics-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));margin:2rem 0} -.metric-card{background:rgba(3,37,76,.6);border-radius:8px;padding:1.5rem;text-align:center;border:1px solid rgba(255,255,255,.1)} +.metrics-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(100px,.5fr));margin:.5rem} +.metric-card{background:rgba(3,37,76,.6);border-radius:8px;padding:.5rem;text-align:center;border:1px solid rgba(255,255,255,.1)} .metric-icon{font-size:2rem;margin-bottom:.5rem} .metric-label{font-size:.9rem;color:rgba(255,255,255,.7);text-transform:uppercase;letter-spacing:.5px;margin-bottom:.5rem} .metric-value{font-size:2rem;font-weight:600;color:#2a9df4;margin-bottom:.25rem} From 124d50222d956104f761d29f883428d20b538bce Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Mon, 18 Nov 2024 15:19:21 -0600 Subject: [PATCH 09/25] Icons --- data/index.html | 64 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/data/index.html b/data/index.html index 3cbaa0c5..aaee8a58 100644 --- a/data/index.html +++ b/data/index.html @@ -17,52 +17,86 @@

SmartSpin2k

-
- + + \ No newline at end of file diff --git a/data/style.css b/data/style.css index 66c18778..0abc98f6 100644 --- a/data/style.css +++ b/data/style.css @@ -97,9 +97,7 @@ nav a:hover { } .shift-button, .shifter-container, -.simulator-card { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} + .dev-tool-card:hover, .menu-item:hover { background: rgba(42, 157, 244, 0.15); @@ -229,81 +227,7 @@ a { overflow: auto; text-shadow: 0 0 4px rgba(200, 200, 200, 0.5); } -.simulator-grid { - display: grid; - gap: 1.5rem; - padding: 1rem 0; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -} -.simulator-card { - background: rgba(3, 37, 76, 0.6); - border-radius: 12px; - padding: 1.5rem; - border: 1px solid rgba(255, 255, 255, 0.1); -} -.simulator-card:hover { - background: rgba(3, 37, 76, 0.8); - border-color: rgba(255, 255, 255, 0.2); -} -.card-header { - display: flex; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid rgba(42, 157, 244, 0.3); -} -.simulator-content { - padding: 1rem; - background: rgba(0, 0, 0, 0.2); - border-radius: 8px; -} -.value-display { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 1.2rem; -} -.auto-update { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.7); -} -.simulator-slider { - -webkit-appearance: none; - width: 100%; - height: 6px; - background: #03254c; - border-radius: 3px; - outline: 0; - margin: 1rem 0; -} -.simulator-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - background: #2a9df4; - border-radius: 50%; - cursor: pointer; - border: 2px solid #fff; - transition: background 0.2s; -} -.simulator-slider::-moz-range-thumb { - width: 20px; - height: 20px; - background: #2a9df4; - border-radius: 50%; - cursor: pointer; - border: 2px solid #fff; - transition: background 0.2s; -} -.simulator-slider:focus::-webkit-slider-thumb { - box-shadow: 0 0 0 3px rgba(42, 157, 244, 0.4); -} -.simulator-slider:focus::-moz-range-thumb { - box-shadow: 0 0 0 3px rgba(42, 157, 244, 0.4); -} + .shifter-container { max-width: 600px; margin: 2rem auto; @@ -916,7 +840,6 @@ footer { .dev-tools-grid, .menu-grid, .metrics-grid, - .simulator-grid, .status-grid { grid-template-columns: 1fr; } @@ -944,7 +867,6 @@ footer { } .menu-item, .metric-card, - .simulator-card, .status-group { padding: 1.25rem; } From 718f402fa621b22363d5d3780b4484763bf2006c Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 15 Dec 2024 20:13:54 -0600 Subject: [PATCH 20/25] added a save button to the debug log --- data/status.html | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/data/status.html b/data/status.html index 1871db11..03e615e4 100644 --- a/data/status.html +++ b/data/status.html @@ -1,11 +1,17 @@ + SmartSpin2k Status - + +
@@ -105,6 +111,9 @@

Debug Console

Loading
+
+ +
@@ -148,7 +157,7 @@

Debug Console

function requestConfigValues() { if (updatePending) return; - + updatePending = true; fetch('/configJSON') .then(response => response.json()) @@ -166,7 +175,7 @@

Debug Console

function requestRuntimeValues() { if (updatePending) return; - + updatePending = true; fetch('/runtimeConfigJSON') .then(response => response.json()) @@ -202,16 +211,16 @@

Debug Console

websocket.onmessage = (evt) => { if (!evt?.data) return; - + lastLogMessageTime = Date.now(); logEntries.push(evt.data); - + if (logEntries.length > maxLogentries) { logEntries = logEntries.slice(1, logEntries.length - 10); } debugElement.innerHTML = logEntries.join("
"); - + if (followElement.checked) { debugElement.scrollTop = debugElement.scrollHeight - debugElement.clientHeight; } @@ -239,6 +248,22 @@

Debug Console

setTimeout(requestRuntimeValues, 200); startUpdate(); }); + + document.getElementById('saveLogButton').addEventListener('click', () => { + const debugElement = document.getElementById('debug'); + const logContent = debugElement.textContent; + const blob = new Blob([logContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'debug_log.txt'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }); + - + + \ No newline at end of file From 1ae3e0f5f973828caf64e720d6462b9a8ba1e413 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 15 Dec 2024 20:33:59 -0600 Subject: [PATCH 21/25] added save button to debug log --- data/status.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/status.html b/data/status.html index 03e615e4..c127db7d 100644 --- a/data/status.html +++ b/data/status.html @@ -251,7 +251,8 @@

Debug Console

document.getElementById('saveLogButton').addEventListener('click', () => { const debugElement = document.getElementById('debug'); - const logContent = debugElement.textContent; + logContent = debugElement.textContent; + logContent = logContent.replace(/(\[\s*\d+\])/g, '\n$1'); const blob = new Blob([logContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); From a5581966778c6c8413590a9bd53cbfe2ba1a6f5c Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Sun, 15 Dec 2024 21:42:00 -0600 Subject: [PATCH 22/25] Update btsimulator.html --- data/btsimulator.html | 481 ++++++++++++++++++++++++++---------------- 1 file changed, 303 insertions(+), 178 deletions(-) diff --git a/data/btsimulator.html b/data/btsimulator.html index a7215d4a..c65db1c2 100644 --- a/data/btsimulator.html +++ b/data/btsimulator.html @@ -9,6 +9,113 @@ html { background-color: #03245c } + + .setting-group .value-display, + .setting-group .slider-group { + display: none; + } + + .setting-group { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background: rgba(3, 37, 76, 0.6); + padding: 1rem; + border-radius: 12px; + } + + .slider-group { + background: rgba(0, 0, 0, 0.3); + padding: 0.5rem; + border-radius: 8px; + width: 100%; + height: 1rem; + max-width: 300px; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .slider-container { + width: 100%; + margin: 0; + height: 20px; + -webkit-appearance: none; + appearance: none; + background: transparent; + } + + .slider-container::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: #2196F3; + cursor: pointer; + margin-top: -6px; + } + + .slider-container::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + background: #fff; + border-radius: 2px; + } + + .slider-container::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: #2196F3; + cursor: pointer; + border: none; + } + + .slider-container::-moz-range-track { + width: 100%; + height: 4px; + background: #fff; + border-radius: 2px; + } + + .value-display { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + gap: 1rem; + margin-top: 0.25rem; + min-height: 24px; + flex-wrap: wrap; + } + + .setting-group:last-child .value-display { + padding: 0.25rem; + box-sizing: border-box; + } + + .setting-group:last-child .value-display .switch { + margin: 0; + } + + .settings-section { + display: grid; + gap: 2rem; + grid-template-columns: 1fr; + } + + @media (min-width: 768px) { + .settings-section { + grid-template-columns: repeat(2, 1fr); + } + } + + h2 { + color: #fff; + margin-top: 0; + } @@ -27,248 +134,266 @@

BLE Simulator

-

Heart Rate

+

Simulate Heart Rate

-
- -- BPM -
+
+ -- BPM +
-

Power Output

+

Simulate Power Output

-
- -- W - -
+
+ -- W + +
-
-

Cadence

+

Simulate Cadence

-
- -- RPM - -
+
+ -- RPM + +
-

ERG Mode

+

Simulate ERG Mode

-
- -- W - -
+
+ -- W +
+ +
+
- + - - - + } + + function loadCss() { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'style.css'; + document.head.appendChild(link); + } + + window.addEventListener('load', () => { + setTimeout(loadCss, 100); + setTimeout(requestConfigValues, 500); + }); + \ No newline at end of file From d0cd20a51c2ae68bad07f43731b358978fa304cd Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Mon, 16 Dec 2024 11:56:42 -0600 Subject: [PATCH 23/25] added homingSensitivy to the web settings --- data/settings.html | 20 +++++++++++--- src/SmartSpin_parameters.cpp | 52 +++++------------------------------- src/http/HTTPSettings.cpp | 7 +++++ 3 files changed, 31 insertions(+), 48 deletions(-) diff --git a/data/settings.html b/data/settings.html index 85c382e1..c9bd5368 100644 --- a/data/settings.html +++ b/data/settings.html @@ -121,6 +121,22 @@

Main Settings

+
+ +
+ +
+ 0 + +
+ +
+
+