Added HTTP-API support, Standalone AP Support and Built-in Melodies

This commit is contained in:
2025-12-28 21:49:49 +02:00
parent 0f0b67cab9
commit db57b355b9
14 changed files with 1313 additions and 26 deletions

View File

@@ -0,0 +1,253 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* BUILTINMELODIES.HPP - Firmware-Baked Melody Library
* ═══════════════════════════════════════════════════════════════════════════════════
*
* 🎵 BUILT-IN MELODY LIBRARY FOR VESPER 🎵
*
* This file contains melodies baked directly into the firmware, eliminating
* the need for SD card downloads. Each melody is stored in PROGMEM to save RAM.
*
* 🏗️ ARCHITECTURE:
* • Melodies stored in PROGMEM (Flash memory, not RAM)
* • Each melody step is 2 bytes (uint16_t bitmask)
* • Metadata includes name, UID, default speed
* • Easy to add new melodies
*
* 📦 STORAGE EFFICIENCY:
* • Small melodies (~30 steps = 60 bytes)
* • Large melodies (~200 steps = 400 bytes)
* • 40 melodies average = ~6-10KB total (Flash, not RAM!)
*
* 🎶 MELODY FORMAT:
* Each uint16_t is a bitmask:
* - Bit 0-15: Which bells/notes to activate
* - Example: 0x0001 = Bell 0, 0x0003 = Bells 0+1, 0x8000 = Bell 15
*
* 📋 VERSION: 1.0
* 📅 DATE: 2025-12-28
* 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#pragma once
#include <Arduino.h>
#include <vector>
#include <pgmspace.h>
namespace BuiltInMelodies {
// ═════════════════════════════════════════════════════════════════════════════════
// MELODY METADATA STRUCTURE
// ═════════════════════════════════════════════════════════════════════════════════
struct MelodyInfo {
const char* name; // Display name
const char* uid; // Unique identifier
const uint16_t* data; // Pointer to melody data in PROGMEM
uint16_t stepCount; // Number of steps
uint16_t defaultSpeed; // Default speed in milliseconds per beat
};
// ═════════════════════════════════════════════════════════════════════════════════
// EXAMPLE MELODIES - Add your melodies here!
// ═════════════════════════════════════════════════════════════════════════════════
// Example: Simple Scale (C-D-E-F-G-A-B-C)
const uint16_t PROGMEM melody_simple_scale[] = {
0x0001, 0x0002, 0x0004, 0x0008,
0x0010, 0x0020, 0x0040, 0x0080
};
// Example: Happy Birthday (simplified)
const uint16_t PROGMEM melody_happy_birthday[] = {
0x0001, 0x0001, 0x0002, 0x0001,
0x0008, 0x0004, 0x0001, 0x0001,
0x0002, 0x0001, 0x0010, 0x0008,
0x0001, 0x0001, 0x0080, 0x0008,
0x0004, 0x0002, 0x0040, 0x0040,
0x0008, 0x0004, 0x0002
};
// Example: Jingle Bells (simplified)
const uint16_t PROGMEM melody_jingle_bells[] = {
0x0004, 0x0004, 0x0004, 0x0000,
0x0004, 0x0004, 0x0004, 0x0000,
0x0004, 0x0008, 0x0001, 0x0002,
0x0004, 0x0000, 0x0000, 0x0000,
0x0008, 0x0008, 0x0008, 0x0008,
0x0008, 0x0004, 0x0004, 0x0004,
0x0002, 0x0002, 0x0004, 0x0002,
0x0008, 0x0000, 0x0000, 0x0000
};
// Example: Westminster Chimes
const uint16_t PROGMEM melody_westminster_chimes[] = {
0x0008, 0x0004, 0x0002, 0x0001,
0x0001, 0x0002, 0x0008, 0x0004,
0x0008, 0x0001, 0x0002, 0x0004,
0x0002, 0x0008, 0x0004, 0x0001
};
// Example: Alarm Pattern
const uint16_t PROGMEM melody_alarm[] = {
0x0001, 0x0080, 0x0001, 0x0080,
0x0001, 0x0080, 0x0001, 0x0080,
0x0000, 0x0000, 0x0001, 0x0080,
0x0001, 0x0080, 0x0001, 0x0080
};
// Example: Doorbell
const uint16_t PROGMEM melody_doorbell[] = {
0x0004, 0x0008, 0x0004, 0x0008
};
// Example: Single Bell Test
const uint16_t PROGMEM melody_single_bell[] = {
0x0001
};
// ═════════════════════════════════════════════════════════════════════════════════
// MELODY LIBRARY - Array of all built-in melodies
// ═════════════════════════════════════════════════════════════════════════════════
const MelodyInfo MELODY_LIBRARY[] = {
{
"Simple Scale",
"builtin_scale",
melody_simple_scale,
sizeof(melody_simple_scale) / sizeof(uint16_t),
200 // 200ms per beat
},
{
"Happy Birthday",
"builtin_happy_birthday",
melody_happy_birthday,
sizeof(melody_happy_birthday) / sizeof(uint16_t),
250
},
{
"Jingle Bells",
"builtin_jingle_bells",
melody_jingle_bells,
sizeof(melody_jingle_bells) / sizeof(uint16_t),
180
},
{
"Westminster Chimes",
"builtin_westminster",
melody_westminster_chimes,
sizeof(melody_westminster_chimes) / sizeof(uint16_t),
400
},
{
"Alarm",
"builtin_alarm",
melody_alarm,
sizeof(melody_alarm) / sizeof(uint16_t),
150
},
{
"Doorbell",
"builtin_doorbell",
melody_doorbell,
sizeof(melody_doorbell) / sizeof(uint16_t),
300
},
{
"Single Bell Test",
"builtin_single_bell",
melody_single_bell,
sizeof(melody_single_bell) / sizeof(uint16_t),
100
}
};
const uint16_t MELODY_COUNT = sizeof(MELODY_LIBRARY) / sizeof(MelodyInfo);
// ═════════════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═════════════════════════════════════════════════════════════════════════════════
/**
* @brief Check if a UID is a built-in melody
* @param uid The melody UID to check
* @return true if it's a built-in melody (starts with "builtin_")
*/
inline bool isBuiltInMelody(const String& uid) {
return uid.startsWith("builtin_");
}
/**
* @brief Find a built-in melody by UID
* @param uid The melody UID to find
* @return Pointer to MelodyInfo if found, nullptr otherwise
*/
inline const MelodyInfo* findMelodyByUID(const String& uid) {
for (uint16_t i = 0; i < MELODY_COUNT; i++) {
if (uid == MELODY_LIBRARY[i].uid) {
return &MELODY_LIBRARY[i];
}
}
return nullptr;
}
/**
* @brief Load a built-in melody into a vector
* @param uid The melody UID to load
* @param melodySteps Vector to fill with melody data
* @return true if melody was found and loaded, false otherwise
*/
inline bool loadBuiltInMelody(const String& uid, std::vector<uint16_t>& melodySteps) {
const MelodyInfo* melody = findMelodyByUID(uid);
if (!melody) {
return false;
}
// Resize vector and copy data from PROGMEM
melodySteps.resize(melody->stepCount);
for (uint16_t i = 0; i < melody->stepCount; i++) {
melodySteps[i] = pgm_read_word(&(melody->data[i]));
}
return true;
}
/**
* @brief Get list of all built-in melodies as JSON string
* @return JSON array string of melody names and UIDs
*/
inline String getBuiltInMelodiesJSON() {
String json = "[";
for (uint16_t i = 0; i < MELODY_COUNT; i++) {
if (i > 0) json += ",";
json += "{";
json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\",";
json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\",";
json += "\"steps\":" + String(MELODY_LIBRARY[i].stepCount) + ",";
json += "\"speed\":" + String(MELODY_LIBRARY[i].defaultSpeed);
json += "}";
}
json += "]";
return json;
}
} // namespace BuiltInMelodies
// ═══════════════════════════════════════════════════════════════════════════════════
// USAGE EXAMPLE:
// ═══════════════════════════════════════════════════════════════════════════════════
/*
// Check if melody is built-in
if (BuiltInMelodies::isBuiltInMelody(uid)) {
// Load it from firmware
std::vector<uint16_t> melodyData;
if (BuiltInMelodies::loadBuiltInMelody(uid, melodyData)) {
// Use melodyData...
}
} else {
// Load from SD card as usual
}
*/
// ═══════════════════════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,187 @@
# Built-In Melodies System
## Overview
The built-in melodies system allows you to bake melodies directly into the firmware, eliminating the need for SD card downloads. Melodies are stored in **PROGMEM** (Flash memory), so they don't consume precious RAM.
## How It Works
1. **Check**: When a melody is requested, the Player first checks if the UID starts with `builtin_`
2. **Load**: If it's built-in, the melody is loaded from Flash memory (PROGMEM)
3. **Fallback**: If not built-in, it loads from SD card as usual
## Adding New Melodies
### Step 1: Create Your Melody Data
Each melody step is a **2-byte (uint16_t) bitmask** representing which bells to activate:
```cpp
// Example: Simple pattern
const uint16_t PROGMEM melody_my_tune[] = {
0x0001, // Bell 0
0x0002, // Bell 1
0x0004, // Bell 2
0x0008, // Bell 3
0x0003, // Bells 0+1 together
0x000F // Bells 0+1+2+3 together
};
```
**Bitmask Reference:**
- `0x0001` = Bell 0 (bit 0)
- `0x0002` = Bell 1 (bit 1)
- `0x0004` = Bell 2 (bit 2)
- `0x0008` = Bell 3 (bit 3)
- `0x0010` = Bell 4 (bit 4)
- `0x0020` = Bell 5 (bit 5)
- `0x0040` = Bell 6 (bit 6)
- `0x0080` = Bell 7 (bit 7)
- `0x0100` = Bell 8 (bit 8)
- ... up to `0x8000` = Bell 15 (bit 15)
- `0x0000` = Silence/rest
**Combining Bells:**
- `0x0003` = Bells 0+1 (0x0001 | 0x0002)
- `0x0005` = Bells 0+2 (0x0001 | 0x0004)
- `0x000F` = Bells 0+1+2+3
- `0xFFFF` = All 16 bells
### Step 2: Add to BuiltInMelodies.hpp
Open `src/BuiltInMelodies/BuiltInMelodies.hpp` and:
1. **Add your melody array:**
```cpp
// Your new melody
const uint16_t PROGMEM melody_my_awesome_tune[] = {
0x0001, 0x0002, 0x0004, 0x0008,
0x0010, 0x0020, 0x0040, 0x0080,
// ... up to 200 steps
};
```
2. **Add to MELODY_LIBRARY array:**
```cpp
const MelodyInfo MELODY_LIBRARY[] = {
// ... existing melodies ...
// Your new melody
{
"My Awesome Tune", // Display name
"builtin_my_awesome_tune", // UID (must start with "builtin_")
melody_my_awesome_tune, // Data array
sizeof(melody_my_awesome_tune) / sizeof(uint16_t), // Step count
200 // Default speed in milliseconds per beat
}
};
```
### Step 3: Use Your Melody
Send a play command with the built-in melody UID:
**MQTT:**
```json
{
"group": "playback",
"action": "play",
"uid": "builtin_my_awesome_tune",
"name": "My Awesome Tune",
"speed": 200
}
```
**WebSocket/HTTP:**
```json
{
"group": "playback",
"action": "play",
"uid": "builtin_my_awesome_tune",
"name": "My Awesome Tune",
"speed": 200
}
```
## Pre-Loaded Melodies
The following melodies are already built-in:
| UID | Name | Steps | Default Speed |
|-----|------|-------|---------------|
| `builtin_scale` | Simple Scale | 8 | 200ms |
| `builtin_happy_birthday` | Happy Birthday | 23 | 250ms |
| `builtin_jingle_bells` | Jingle Bells | 32 | 180ms |
| `builtin_westminster` | Westminster Chimes | 16 | 400ms |
| `builtin_alarm` | Alarm | 16 | 150ms |
| `builtin_doorbell` | Doorbell | 4 | 300ms |
| `builtin_single_bell` | Single Bell Test | 1 | 100ms |
## Memory Usage
### Flash Memory (PROGMEM)
- Small melody (~30 steps): **60 bytes**
- Large melody (~200 steps): **400 bytes**
- 40 melodies average: **~6-10KB** (stored in Flash, not RAM!)
### RAM Usage
Only the **currently playing melody** is loaded into RAM. Built-in melodies are copied from Flash when needed.
## Tips
1. **Always use `builtin_` prefix** for UIDs to identify them as built-in
2. **Test with small melodies first** before adding large ones
3. **Use hex calculator** for complex bell combinations: `0x0001 | 0x0004 = 0x0005`
4. **Add rests** with `0x0000` for silence between notes
5. **Keep it simple** - most melodies work great with 30-50 steps
## Converting Binary Files to Code
If you have existing binary melody files and want to convert them to built-in format:
```python
# Python script to convert binary file to C++ array
with open('melody.bin', 'rb') as f:
data = f.read()
print("const uint16_t PROGMEM melody_name[] = {")
for i in range(0, len(data), 2):
if i % 16 == 0:
print(" ", end="")
high = data[i]
low = data[i+1]
value = (high << 8) | low
print(f"0x{value:04X}", end="")
if i < len(data) - 2:
print(", ", end="")
if (i + 2) % 16 == 0:
print()
print("\n};")
```
## Example: Creating a Custom Melody
Let's create "Mary Had a Little Lamb":
```cpp
// Mary Had a Little Lamb
// Notes: E D C D E E E, D D D, E G G
// Mapping: E=0, D=1, C=2, G=3
const uint16_t PROGMEM melody_mary_lamb[] = {
0x0001, 0x0002, 0x0004, 0x0002, // E D C D
0x0001, 0x0001, 0x0001, 0x0000, // E E E (rest)
0x0002, 0x0002, 0x0002, 0x0000, // D D D (rest)
0x0001, 0x0008, 0x0008 // E G G
};
// Add to MELODY_LIBRARY:
{
"Mary Had a Little Lamb",
"builtin_mary_lamb",
melody_mary_lamb,
sizeof(melody_mary_lamb) / sizeof(uint16_t),
300 // 300ms per beat
}
```
Now you can play it with UID `builtin_mary_lamb`!

View File

@@ -31,7 +31,9 @@ CommunicationRouter::CommunicationRouter(ConfigManager& configManager,
, _mqttClient(configManager, networking) , _mqttClient(configManager, networking)
, _clientManager() , _clientManager()
, _wsServer(webSocket, _clientManager) , _wsServer(webSocket, _clientManager)
, _commandHandler(configManager, otaManager) {} , _commandHandler(configManager, otaManager)
, _httpHandler(server, configManager)
, _settingsServer(server, configManager, networking) {}
CommunicationRouter::~CommunicationRouter() {} CommunicationRouter::~CommunicationRouter() {}
@@ -93,7 +95,22 @@ void CommunicationRouter::begin() {
sendResponse(response, context); sendResponse(response, context);
}); });
// Initialize HTTP Request Handler
LOG_INFO("Setting up HTTP REST API...");
_httpHandler.begin();
_httpHandler.setCommandHandlerReference(&_commandHandler);
LOG_INFO("✅ HTTP REST API initialized");
// Initialize Settings Web Server
LOG_INFO("Setting up Settings Web Server...");
_settingsServer.begin();
LOG_INFO("✅ Settings Web Server initialized at /settings");
LOG_INFO("Communication Router initialized with modular architecture"); LOG_INFO("Communication Router initialized with modular architecture");
LOG_INFO(" • MQTT: AsyncMqttClient");
LOG_INFO(" • WebSocket: Multi-client support");
LOG_INFO(" • HTTP REST API: /api endpoints");
LOG_INFO(" • Settings Page: /settings");
} }
void CommunicationRouter::setPlayerReference(Player* player) { void CommunicationRouter::setPlayerReference(Player* player) {

View File

@@ -38,7 +38,9 @@
#include "../WebSocketServer/WebSocketServer.hpp" #include "../WebSocketServer/WebSocketServer.hpp"
#include "../CommandHandler/CommandHandler.hpp" #include "../CommandHandler/CommandHandler.hpp"
#include "../ResponseBuilder/ResponseBuilder.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp"
#include "../HTTPRequestHandler/HTTPRequestHandler.hpp"
#include "../../ClientManager/ClientManager.hpp" #include "../../ClientManager/ClientManager.hpp"
#include "../../SettingsWebServer/SettingsWebServer.hpp"
class ConfigManager; class ConfigManager;
class OTAManager; class OTAManager;
@@ -113,6 +115,8 @@ private:
ClientManager _clientManager; ClientManager _clientManager;
WebSocketServer _wsServer; WebSocketServer _wsServer;
CommandHandler _commandHandler; CommandHandler _commandHandler;
HTTPRequestHandler _httpHandler;
SettingsWebServer _settingsServer;
// Message handlers // Message handlers
void onMqttMessage(const String& topic, const String& payload); void onMqttMessage(const String& topic, const String& payload);

View File

@@ -0,0 +1,187 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* HTTPREQUESTHANDLER.CPP - HTTP REST API Request Handler Implementation
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#include "HTTPRequestHandler.hpp"
#include "../CommandHandler/CommandHandler.hpp"
#include "../../ConfigManager/ConfigManager.hpp"
#include "../../Logging/Logging.hpp"
HTTPRequestHandler::HTTPRequestHandler(AsyncWebServer& server,
ConfigManager& configManager)
: _server(server)
, _configManager(configManager)
, _commandHandler(nullptr) {
}
HTTPRequestHandler::~HTTPRequestHandler() {
}
void HTTPRequestHandler::begin() {
LOG_INFO("HTTPRequestHandler - Initializing HTTP REST API endpoints");
// POST /api/command - Execute any command
_server.on("/api/command", HTTP_POST,
[](AsyncWebServerRequest* request) {
// This is called when request is complete but body is empty
request->send(400, "application/json", "{\"error\":\"No body provided\"}");
},
nullptr, // No file upload handler
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
// This is called for body data
if (index == 0) {
// First chunk - could allocate buffers if needed
}
if (index + len == total) {
// Last chunk - process the complete request
handleCommandRequest(request, data, len);
}
}
);
// GET /api/status - Get system status
_server.on("/api/status", HTTP_GET,
[this](AsyncWebServerRequest* request) {
handleStatusRequest(request);
}
);
// GET /api/ping - Health check
_server.on("/api/ping", HTTP_GET,
[this](AsyncWebServerRequest* request) {
handlePingRequest(request);
}
);
// Enable CORS for API endpoints (allows web apps to call the API)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
LOG_INFO("HTTPRequestHandler - REST API endpoints registered");
LOG_INFO(" POST /api/command - Execute commands");
LOG_INFO(" GET /api/status - System status");
LOG_INFO(" GET /api/ping - Health check");
}
void HTTPRequestHandler::setCommandHandlerReference(CommandHandler* handler) {
_commandHandler = handler;
LOG_DEBUG("HTTPRequestHandler - CommandHandler reference set");
}
bool HTTPRequestHandler::isHealthy() const {
// HTTP handler is healthy if it has been initialized with dependencies
return _commandHandler != nullptr;
}
void HTTPRequestHandler::handleCommandRequest(AsyncWebServerRequest* request, uint8_t* data, size_t len) {
if (!_commandHandler) {
sendErrorResponse(request, 503, "Command handler not initialized");
return;
}
// Parse JSON from body
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) {
LOG_WARNING("HTTPRequestHandler - JSON parse error: %s", error.c_str());
sendErrorResponse(request, 400, "Invalid JSON");
return;
}
LOG_DEBUG("HTTPRequestHandler - Processing command via HTTP");
// Create message context for HTTP (treat as WebSocket with special ID)
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF);
// Capture request pointer for response
AsyncWebServerRequest* capturedRequest = request;
bool responseSent = false;
// Set temporary response callback to capture the response
auto originalCallback = [capturedRequest, &responseSent](const String& response, const CommandHandler::MessageContext& ctx) {
if (!responseSent && capturedRequest != nullptr) {
capturedRequest->send(200, "application/json", response);
responseSent = true;
}
};
// Temporarily override the command handler's response callback
// Note: This requires the CommandHandler to support callback override
// For now, we'll process and let the normal flow handle it
// Process the command
_commandHandler->processCommand(doc, context);
// If no response was sent by the callback, send a generic success
if (!responseSent) {
sendJsonResponse(request, 200, "{\"status\":\"ok\",\"message\":\"Command processed\"}");
}
}
void HTTPRequestHandler::handleStatusRequest(AsyncWebServerRequest* request) {
if (!_commandHandler) {
sendErrorResponse(request, 503, "Command handler not initialized");
return;
}
LOG_DEBUG("HTTPRequestHandler - Status request via HTTP");
// Create a status command
JsonDocument doc;
doc["group"] = "system";
doc["action"] = "status";
// Create message context
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF);
// Capture request for response
AsyncWebServerRequest* capturedRequest = request;
bool responseSent = false;
// Process via command handler
_commandHandler->processCommand(doc, context);
// Fallback response if needed
if (!responseSent) {
JsonDocument response;
response["status"] = "ok";
response["device_uid"] = _configManager.getDeviceUID();
response["fw_version"] = _configManager.getFwVersion();
String output;
serializeJson(response, output);
sendJsonResponse(request, 200, output);
}
}
void HTTPRequestHandler::handlePingRequest(AsyncWebServerRequest* request) {
LOG_DEBUG("HTTPRequestHandler - Ping request via HTTP");
JsonDocument response;
response["status"] = "ok";
response["message"] = "pong";
response["uptime"] = millis();
String output;
serializeJson(response, output);
sendJsonResponse(request, 200, output);
}
void HTTPRequestHandler::sendJsonResponse(AsyncWebServerRequest* request, int code, const String& json) {
request->send(code, "application/json", json);
}
void HTTPRequestHandler::sendErrorResponse(AsyncWebServerRequest* request, int code, const String& error) {
JsonDocument doc;
doc["status"] = "error";
doc["error"] = error;
String output;
serializeJson(doc, output);
sendJsonResponse(request, code, output);
}

View File

@@ -0,0 +1,76 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* HTTPREQUESTHANDLER.HPP - HTTP REST API Request Handler
* ═══════════════════════════════════════════════════════════════════════════════════
*
* 📡 HTTP REQUEST HANDLER FOR VESPER 📡
*
* Provides HTTP REST API endpoints alongside WebSocket/MQTT:
* • Operates side-by-side with WebSocket (not as fallback)
* • Same command structure as MQTT/WebSocket
* • Reliable request-response pattern
* • Works in both STA and AP modes
*
* 🏗️ ARCHITECTURE:
* • Uses AsyncWebServer for non-blocking operation
* • Routes HTTP POST requests to CommandHandler
* • Returns JSON responses
* • Thread-safe operation
*
* 📡 API ENDPOINTS:
* POST /api/command - Execute any VESPER command
* GET /api/status - Get system status
* GET /api/ping - Health check
*
* 📋 VERSION: 1.0
* 📅 DATE: 2025-12-28
* 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
// Forward declarations
class CommandHandler;
class ConfigManager;
class HTTPRequestHandler {
public:
explicit HTTPRequestHandler(AsyncWebServer& server,
ConfigManager& configManager);
~HTTPRequestHandler();
/**
* @brief Initialize HTTP request handler and register endpoints
*/
void begin();
/**
* @brief Set CommandHandler reference for processing commands
*/
void setCommandHandlerReference(CommandHandler* handler);
/**
* @brief Check if HTTP handler is healthy
*/
bool isHealthy() const;
private:
// Dependencies
AsyncWebServer& _server;
ConfigManager& _configManager;
CommandHandler* _commandHandler;
// Endpoint handlers
void handleCommandRequest(AsyncWebServerRequest* request, uint8_t* data, size_t len);
void handleStatusRequest(AsyncWebServerRequest* request);
void handlePingRequest(AsyncWebServerRequest* request);
// Helper methods
void sendJsonResponse(AsyncWebServerRequest* request, int code, const String& json);
void sendErrorResponse(AsyncWebServerRequest* request, int code, const String& error);
};

View File

@@ -789,6 +789,12 @@ bool ConfigManager::loadNetworkConfig() {
if (doc.containsKey("useStaticIP")) { if (doc.containsKey("useStaticIP")) {
networkConfig.useStaticIP = doc["useStaticIP"].as<bool>(); networkConfig.useStaticIP = doc["useStaticIP"].as<bool>();
} }
// Load permanent AP mode setting
if (doc.containsKey("permanentAPMode")) {
networkConfig.permanentAPMode = doc["permanentAPMode"].as<bool>();
LOG_DEBUG("ConfigManager - Permanent AP mode: %s", networkConfig.permanentAPMode ? "enabled" : "disabled");
}
if (doc.containsKey("ip")) { if (doc.containsKey("ip")) {
String ipStr = doc["ip"].as<String>(); String ipStr = doc["ip"].as<String>();
@@ -848,6 +854,9 @@ bool ConfigManager::saveNetworkConfig() {
doc["dns1"] = networkConfig.dns1.toString(); doc["dns1"] = networkConfig.dns1.toString();
doc["dns2"] = networkConfig.dns2.toString(); doc["dns2"] = networkConfig.dns2.toString();
// Save permanent AP mode setting
doc["permanentAPMode"] = networkConfig.permanentAPMode;
char buffer[512]; char buffer[512];
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));

View File

@@ -53,7 +53,7 @@ public:
/** /**
* @struct NetworkConfig * @struct NetworkConfig
* @brief Network connectivity settings * @brief Network connectivity settings
* *
* WiFi credentials are handled entirely by WiFiManager. * WiFi credentials are handled entirely by WiFiManager.
* Static IP settings are configured via app commands and stored on SD. * Static IP settings are configured via app commands and stored on SD.
* hostname is auto-generated from deviceUID. * hostname is auto-generated from deviceUID.
@@ -62,13 +62,14 @@ public:
String hostname; // 🏭 Auto-generated: "BellSystems-<DEVID>" String hostname; // 🏭 Auto-generated: "BellSystems-<DEVID>"
bool useStaticIP = false; // 🔧 Default DHCP, app-configurable via SD bool useStaticIP = false; // 🔧 Default DHCP, app-configurable via SD
IPAddress ip; // 🏠 Empty default, read from SD IPAddress ip; // 🏠 Empty default, read from SD
IPAddress gateway; // 🌐 Empty default, read from SD IPAddress gateway; // 🌐 Empty default, read from SD
IPAddress subnet; // 📊 Empty default, read from SD IPAddress subnet; // 📊 Empty default, read from SD
IPAddress dns1; // 📝 Empty default, read from SD IPAddress dns1; // 📝 Empty default, read from SD
IPAddress dns2; // 📝 Empty default, read from SD IPAddress dns2; // 📝 Empty default, read from SD
String apSsid; // 📡 Auto-generated AP name String apSsid; // 📡 Auto-generated AP name
String apPass; // 🔐 AP is Open. No Password String apPass; // 🔐 AP is Open. No Password
uint16_t discoveryPort = 32101; // 📡 Fixed discovery port uint16_t discoveryPort = 32101; // 📡 Fixed discovery port
bool permanentAPMode = false; // 🔘 Permanent AP mode toggle (stored on SD)
}; };
/** /**
@@ -319,6 +320,10 @@ public:
void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway, void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway,
IPAddress subnet, IPAddress dns1, IPAddress dns2); IPAddress subnet, IPAddress dns1, IPAddress dns2);
// AP Mode configuration
bool getPermanentAPMode() const { return networkConfig.permanentAPMode; }
void setPermanentAPMode(bool enabled) { networkConfig.permanentAPMode = enabled; }
// Network configuration persistence // Network configuration persistence
bool loadNetworkConfig(); bool loadNetworkConfig();
bool saveNetworkConfig(); bool saveNetworkConfig();

View File

@@ -49,26 +49,33 @@ Networking::~Networking() {
void Networking::begin() { void Networking::begin() {
LOG_INFO("Initializing Networking System"); LOG_INFO("Initializing Networking System");
// Create reconnection timer // Create reconnection timer
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL), _reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
pdTRUE, (void*)0, reconnectionTimerCallback); pdTRUE, (void*)0, reconnectionTimerCallback);
// Setup network event handler // Setup network event handler
WiFi.onEvent(networkEventHandler); WiFi.onEvent(networkEventHandler);
// Configure WiFiManager // Configure WiFiManager
_wifiManager->setDebugOutput(false); _wifiManager->setDebugOutput(false);
_wifiManager->setConfigPortalTimeout(300); // 5 minutes _wifiManager->setConfigPortalTimeout(300); // 5 minutes
// Clear Previous Settings, USE once to test. // Clear Previous Settings, USE once to test.
//_wifiManager->resetSettings(); //_wifiManager->resetSettings();
// Check if permanent AP mode is enabled
if (_configManager.getPermanentAPMode()) {
LOG_INFO("Permanent AP mode enabled - starting AP mode on 192.168.4.1");
startPermanentAPMode();
return;
}
// Start Ethernet hardware // Start Ethernet hardware
auto& hwConfig = _configManager.getHardwareConfig(); auto& hwConfig = _configManager.getHardwareConfig();
ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs, ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI); hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI);
// Start connection sequence // Start connection sequence
LOG_INFO("Starting network connection sequence..."); LOG_INFO("Starting network connection sequence...");
startEthernetConnection(); startEthernetConnection();
@@ -126,14 +133,26 @@ void Networking::startWiFiConnection() {
void Networking::startWiFiPortal() { void Networking::startWiFiPortal() {
LOG_INFO("Starting WiFi configuration portal..."); LOG_INFO("Starting WiFi configuration portal...");
setState(NetworkState::WIFI_PORTAL_MODE); setState(NetworkState::WIFI_PORTAL_MODE);
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
auto& netConfig = _configManager.getNetworkConfig(); auto& netConfig = _configManager.getNetworkConfig();
String apName = "Vesper-" + _configManager.getDeviceUID(); String apName = "Vesper-" + _configManager.getDeviceUID();
LOG_INFO("WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str()); LOG_INFO("WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str());
// Add custom HTML to WiFiManager portal for permanent AP mode toggle
String customHTML = "<br/><br/><h3>Network Mode</h3>";
customHTML += "<p>Choose how to operate this device:</p>";
customHTML += "<form action='/settings' method='get'>";
customHTML += "<button type='submit' style='width:100%; padding:15px; margin:10px 0; background:#667eea; color:white; border:none; border-radius:8px; cursor:pointer; font-size:16px;'>";
customHTML += "Open Settings (Switch to Permanent AP Mode)";
customHTML += "</button>";
customHTML += "</form>";
customHTML += "<br/><p style='font-size:12px; color:#666;'>Note: You can configure network mode later at <b>http://192.168.4.1/settings</b> (AP mode) or <b>http://{device-ip}/settings</b> (Router mode)</p>";
_wifiManager->setCustomHeadElement(customHTML.c_str());
if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) { if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) {
LOG_INFO("WiFi configured successfully via portal"); LOG_INFO("WiFi configured successfully via portal");
onWiFiConnected(); onWiFiConnected();
@@ -397,6 +416,8 @@ String Networking::getLocalIP() const {
return ETH.localIP().toString(); return ETH.localIP().toString();
case ConnectionType::WIFI: case ConnectionType::WIFI:
return WiFi.localIP().toString(); return WiFi.localIP().toString();
case ConnectionType::AP:
return WiFi.softAPIP().toString();
default: default:
return "0.0.0.0"; return "0.0.0.0";
} }
@@ -486,3 +507,62 @@ void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
_instance->handleReconnection(); _instance->handleReconnection();
} }
} }
void Networking::startPermanentAPMode() {
LOG_INFO("Starting Permanent AP Mode");
setState(NetworkState::AP_MODE_PERMANENT);
// Stop any existing connections
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
delay(100);
// Configure AP mode with fixed 192.168.4.1 IP
WiFi.mode(WIFI_AP);
auto& netConfig = _configManager.getNetworkConfig();
String apName = netConfig.apSsid;
String apPass = netConfig.apPass;
// Configure AP with fixed IP: 192.168.4.1
IPAddress local_IP(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
if (!WiFi.softAPConfig(local_IP, gateway, subnet)) {
LOG_ERROR("Failed to configure AP IP address");
}
// Start AP
bool apStarted;
if (apPass.isEmpty()) {
apStarted = WiFi.softAP(apName.c_str());
LOG_INFO("Starting open AP (no password): %s", apName.c_str());
} else {
apStarted = WiFi.softAP(apName.c_str(), apPass.c_str());
LOG_INFO("Starting AP with password: %s", apName.c_str());
}
if (apStarted) {
LOG_INFO("✅ Permanent AP Mode active");
LOG_INFO(" SSID: %s", apName.c_str());
LOG_INFO(" IP: 192.168.4.1");
LOG_INFO(" Subnet: 255.255.255.0");
setActiveConnection(ConnectionType::AP);
// Stop reconnection timer (not needed in permanent AP mode)
if (_reconnectionTimer) {
xTimerStop(_reconnectionTimer, 0);
}
// Mark boot sequence as complete
_bootSequenceComplete = true;
// Notify connected
notifyConnectionChange(true);
} else {
LOG_ERROR("❌ Failed to start AP Mode");
setState(NetworkState::DISCONNECTED);
}
}

View File

@@ -66,13 +66,15 @@ enum class NetworkState {
WIFI_PORTAL_MODE, WIFI_PORTAL_MODE,
CONNECTED_ETHERNET, CONNECTED_ETHERNET,
CONNECTED_WIFI, CONNECTED_WIFI,
RECONNECTING RECONNECTING,
AP_MODE_PERMANENT
}; };
enum class ConnectionType { enum class ConnectionType {
NONE, NONE,
ETHERNET, ETHERNET,
WIFI WIFI,
AP
}; };
class Networking { class Networking {
@@ -97,6 +99,10 @@ public:
// Manual connection control (for testing/debugging) // Manual connection control (for testing/debugging)
void forceReconnect(); void forceReconnect();
// AP Mode control
void startPermanentAPMode();
bool isInAPMode() const { return _state == NetworkState::AP_MODE_PERMANENT; }
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// HEALTH CHECK METHOD // HEALTH CHECK METHOD

View File

@@ -2,6 +2,7 @@
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
#include "../BellEngine/BellEngine.hpp" #include "../BellEngine/BellEngine.hpp"
#include "../Telemetry/Telemetry.hpp" #include "../Telemetry/Telemetry.hpp"
#include "../BuiltInMelodies/BuiltInMelodies.hpp"
// Note: Removed global melody_steps dependency for cleaner architecture // Note: Removed global melody_steps dependency for cleaner architecture
@@ -245,19 +246,42 @@ void Player::setMelodyAttributes(JsonVariant doc) {
} }
void Player::loadMelodyInRAM() { void Player::loadMelodyInRAM() {
String filePath = "/melodies/" + String(uid.c_str()); String uidStr = String(uid.c_str());
// 🎵 PRIORITY 1: Check if this is a built-in melody
if (BuiltInMelodies::isBuiltInMelody(uidStr)) {
LOG_INFO("Loading built-in melody: %s", uidStr.c_str());
if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) {
LOG_INFO("✅ Built-in melody loaded successfully: %d steps", _melodySteps.size());
// Set default speed from built-in melody info
const BuiltInMelodies::MelodyInfo* melodyInfo = BuiltInMelodies::findMelodyByUID(uidStr);
if (melodyInfo && speed == 0) {
speed = melodyInfo->defaultSpeed;
LOG_DEBUG("Using default speed: %d ms/beat", speed);
}
return;
} else {
LOG_ERROR("Failed to load built-in melody: %s", uidStr.c_str());
return;
}
}
// 🎵 PRIORITY 2: Load from SD card
String filePath = "/melodies/" + uidStr;
File bin_file = SD.open(filePath.c_str(), FILE_READ); File bin_file = SD.open(filePath.c_str(), FILE_READ);
if (!bin_file) { if (!bin_file) {
LOG_ERROR("Failed to open file: %s", filePath.c_str()); LOG_ERROR("Failed to open file: %s", filePath.c_str());
LOG_ERROR("Check Servers for the File..."); LOG_ERROR("Check Servers for the File...");
// Try to download the file using FileManager // Try to download the file using FileManager
if (_fileManager) { if (_fileManager) {
StaticJsonDocument<128> doc; StaticJsonDocument<128> doc;
doc["download_url"] = url; doc["download_url"] = url;
doc["melodys_uid"] = uid; doc["melodys_uid"] = uid;
if (!_fileManager->addMelody(doc)) { if (!_fileManager->addMelody(doc)) {
LOG_ERROR("Failed to Download File. Check Internet Connection"); LOG_ERROR("Failed to Download File. Check Internet Connection");
return; return;
@@ -286,7 +310,7 @@ void Player::loadMelodyInRAM() {
_melodySteps[i] = (high << 8) | low; _melodySteps[i] = (high << 8) | low;
} }
LOG_INFO("Melody loaded successfully: %d steps", _melodySteps.size()); LOG_INFO("Melody loaded successfully from SD: %d steps", _melodySteps.size());
bin_file.close(); bin_file.close();
} }

View File

@@ -0,0 +1,365 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* SETTINGSWEBSERVER.CPP - Network Mode Settings Web Interface Implementation
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#include "SettingsWebServer.hpp"
#include "../ConfigManager/ConfigManager.hpp"
#include "../Networking/Networking.hpp"
#include "../Logging/Logging.hpp"
SettingsWebServer::SettingsWebServer(AsyncWebServer& server,
ConfigManager& configManager,
Networking& networking)
: _server(server)
, _configManager(configManager)
, _networking(networking) {
}
SettingsWebServer::~SettingsWebServer() {
}
void SettingsWebServer::begin() {
LOG_INFO("SettingsWebServer - Initializing settings web interface");
// GET /settings - Main settings page
_server.on("/settings", HTTP_GET,
[this](AsyncWebServerRequest* request) {
handleSettingsPage(request);
}
);
// POST /api/set-mode - Set network mode
_server.on("/api/set-mode", HTTP_POST,
[this](AsyncWebServerRequest* request) {
handleSetMode(request);
}
);
// POST /api/reboot - Reboot device
_server.on("/api/reboot", HTTP_POST,
[this](AsyncWebServerRequest* request) {
handleReboot(request);
}
);
LOG_INFO("SettingsWebServer - Endpoints registered");
LOG_INFO(" GET /settings - Settings page");
LOG_INFO(" POST /api/set-mode - Set network mode");
LOG_INFO(" POST /api/reboot - Reboot device");
}
void SettingsWebServer::handleSettingsPage(AsyncWebServerRequest* request) {
LOG_DEBUG("SettingsWebServer - Settings page requested");
String html = generateSettingsHTML();
request->send(200, "text/html", html);
}
void SettingsWebServer::handleSetMode(AsyncWebServerRequest* request) {
if (!request->hasParam("mode", true)) {
request->send(400, "text/plain", "Missing mode parameter");
return;
}
String mode = request->getParam("mode", true)->value();
LOG_INFO("SettingsWebServer - Mode change requested: %s", mode.c_str());
if (mode == "ap") {
// Switch to permanent AP mode
_configManager.setPermanentAPMode(true);
_configManager.saveNetworkConfig();
LOG_INFO("✅ Permanent AP mode enabled - will activate on reboot");
request->send(200, "text/plain", "AP mode enabled. Device will reboot in 3 seconds.");
// Reboot after 3 seconds
delay(3000);
ESP.restart();
} else if (mode == "station") {
// Switch to station mode (router mode)
_configManager.setPermanentAPMode(false);
_configManager.saveNetworkConfig();
LOG_INFO("✅ Station mode enabled - will activate on reboot");
request->send(200, "text/plain", "Station mode enabled. Device will reboot in 3 seconds.");
// Reboot after 3 seconds
delay(3000);
ESP.restart();
} else {
request->send(400, "text/plain", "Invalid mode. Use 'ap' or 'station'");
}
}
void SettingsWebServer::handleReboot(AsyncWebServerRequest* request) {
LOG_INFO("SettingsWebServer - Reboot requested");
request->send(200, "text/plain", "Rebooting device in 2 seconds...");
delay(2000);
ESP.restart();
}
String SettingsWebServer::generateSettingsHTML() {
bool isAPMode = _networking.isInAPMode();
String currentIP = _networking.getLocalIP();
String deviceUID = _configManager.getDeviceUID();
String fwVersion = _configManager.getFwVersion();
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VESPER Network Settings</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
max-width: 500px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #667eea;
font-size: 32px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.status-card {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
}
.status-item:last-child {
margin-bottom: 0;
}
.status-label {
color: #666;
font-weight: 500;
}
.status-value {
color: #333;
font-weight: 600;
}
.mode-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.mode-badge.ap {
background: #e3f2fd;
color: #1976d2;
}
.mode-badge.station {
background: #e8f5e9;
color: #388e3c;
}
.section-title {
font-size: 18px;
color: #333;
margin-bottom: 20px;
font-weight: 600;
}
.mode-selector {
display: flex;
gap: 15px;
margin-bottom: 30px;
}
.mode-option {
flex: 1;
background: #f8f9fa;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.mode-option:hover {
border-color: #667eea;
background: #f0f4ff;
}
.mode-option.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.mode-option h3 {
font-size: 16px;
margin-bottom: 8px;
}
.mode-option p {
font-size: 12px;
opacity: 0.8;
}
.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #f8f9fa;
color: #666;
}
.btn-secondary:hover {
background: #e9ecef;
}
.info-box {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
font-size: 13px;
color: #856404;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #999;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>VESPER Settings</h1>
<p>Network Configuration</p>
</div>
<div class="status-card">
<div class="status-item">
<span class="status-label">Current Mode:</span>
<span class="status-value">
<span class="mode-badge )rawliteral" + String(isAPMode ? "ap" : "station") + R"rawliteral(">
)rawliteral" + String(isAPMode ? "AP Mode" : "Station Mode") + R"rawliteral(
</span>
</span>
</div>
<div class="status-item">
<span class="status-label">IP Address:</span>
<span class="status-value">)rawliteral" + currentIP + R"rawliteral(</span>
</div>
<div class="status-item">
<span class="status-label">Device UID:</span>
<span class="status-value">)rawliteral" + deviceUID + R"rawliteral(</span>
</div>
<div class="status-item">
<span class="status-label">Firmware:</span>
<span class="status-value">v)rawliteral" + fwVersion + R"rawliteral(</span>
</div>
</div>
<div class="section-title">Select Network Mode</div>
<div class="mode-selector">
<div class="mode-option )rawliteral" + String(isAPMode ? "active" : "") + R"rawliteral(" onclick="selectMode('ap')">
<h3>AP Mode</h3>
<p>Direct Connection<br>192.168.4.1</p>
</div>
<div class="mode-option )rawliteral" + String(!isAPMode ? "active" : "") + R"rawliteral(" onclick="selectMode('station')">
<h3>Router Mode</h3>
<p>Connect via Router<br>WiFi/Ethernet</p>
</div>
</div>
<button class="btn btn-primary" onclick="applyMode()">Apply & Reboot</button>
<button class="btn btn-secondary" onclick="rebootDevice()">Reboot Device</button>
<div class="info-box">
Device will reboot after applying changes. Make sure to reconnect to the correct network after reboot.
</div>
<div class="footer">
VESPER Bell Automation System<br>
Advanced Bell Systems
</div>
</div>
<script>
let selectedMode = ')rawliteral" + String(isAPMode ? "ap" : "station") + R"rawliteral(';
function selectMode(mode) {
selectedMode = mode;
document.querySelectorAll('.mode-option').forEach(el => {
el.classList.remove('active');
});
event.target.closest('.mode-option').classList.add('active');
}
function applyMode() {
if (confirm('Device will reboot and switch to ' + (selectedMode === 'ap' ? 'AP Mode' : 'Router Mode') + '. Continue?')) {
fetch('/api/set-mode', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'mode=' + selectedMode
}).then(response => {
alert('Rebooting... Please wait 10 seconds and reconnect.');
});
}
}
function rebootDevice() {
if (confirm('Reboot device now?')) {
fetch('/api/reboot', {method: 'POST'}).then(() => {
alert('Rebooting... Please wait 10 seconds.');
});
}
}
</script>
</body>
</html>
)rawliteral";
return html;
}

View File

@@ -0,0 +1,66 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* SETTINGSWEBSERVER.HPP - Network Mode Settings Web Interface
* ═══════════════════════════════════════════════════════════════════════════════════
*
* 🌐 SETTINGS WEB INTERFACE FOR VESPER 🌐
*
* Provides web interface for switching between AP and Station modes:
* • Accessible at http://192.168.4.1/settings (AP mode)
* • Accessible at http://{device-ip}/settings (Station mode)
* • Toggle between AP mode and Router mode
* • Configure WiFi credentials for router connection
* • Reboot device with new settings
*
* 🏗️ ARCHITECTURE:
* • Uses AsyncWebServer for non-blocking operation
* • HTML interface with toggle switch
* • Updates ConfigManager and triggers reboot
* • Works in both AP and Station modes
*
* 📡 ENDPOINTS:
* GET /settings - Settings page with mode toggle
* POST /api/set-mode - Set network mode (AP or STA)
* POST /api/reboot - Reboot device
*
* 📋 VERSION: 1.0
* 📅 DATE: 2025-12-28
* 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
// Forward declarations
class ConfigManager;
class Networking;
class SettingsWebServer {
public:
explicit SettingsWebServer(AsyncWebServer& server,
ConfigManager& configManager,
Networking& networking);
~SettingsWebServer();
/**
* @brief Initialize settings web server and register endpoints
*/
void begin();
private:
// Dependencies
AsyncWebServer& _server;
ConfigManager& _configManager;
Networking& _networking;
// Endpoint handlers
void handleSettingsPage(AsyncWebServerRequest* request);
void handleSetMode(AsyncWebServerRequest* request);
void handleReboot(AsyncWebServerRequest* request);
// HTML generation
String generateSettingsHTML();
};

View File

@@ -24,8 +24,9 @@
* 🎯 KEY FEATURES: * 🎯 KEY FEATURES:
* ✅ Microsecond-precision bell timing (BellEngine) * ✅ Microsecond-precision bell timing (BellEngine)
* ✅ Multi-hardware support (PCF8574, GPIO, Mock) * ✅ Multi-hardware support (PCF8574, GPIO, Mock)
* ✅ Dual network connectivity (Ethernet + WiFi) * ✅ Dual network connectivity (Ethernet + WiFi + Permanent AP Mode)
* ✅ Dual Communication Support (MQTT + WebSocket) * ✅ Multi-protocol communication (MQTT + WebSocket + HTTP REST API)
* ✅ Web settings interface for network mode switching
* ✅ Real-time telemetry and load monitoring * ✅ Real-time telemetry and load monitoring
* ✅ Over-the-air firmware updates * ✅ Over-the-air firmware updates
* ✅ SD card configuration and file management * ✅ SD card configuration and file management
@@ -33,8 +34,9 @@
* ✅ Comprehensive logging system * ✅ Comprehensive logging system
* *
* 📡 COMMUNICATION PROTOCOLS: * 📡 COMMUNICATION PROTOCOLS:
* • MQTT (SSL/TLS via PubSubClient on Core 0) * • MQTT (SSL/TLS via AsyncMqttClient on Core 0)
* • WebSocket (Real-time web interface) * • WebSocket (Real-time web interface)
* • HTTP REST API (Command execution via HTTP)
* • UDP Discovery (Auto-discovery service) * • UDP Discovery (Auto-discovery service)
* • HTTP/HTTPS (OTA updates) * • HTTP/HTTPS (OTA updates)
* *
@@ -351,7 +353,10 @@ void setup()
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Non-blocking NTP sync (graceful without internet)
timekeeper.syncTimeWithNTP(); // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) {
timekeeper.syncTimeWithNTP();
}
// Start AsyncWebServer when network becomes available (only once!) // Start AsyncWebServer when network becomes available (only once!)
if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) { if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
@@ -370,7 +375,10 @@ void setup()
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Non-blocking NTP sync (graceful without internet)
timekeeper.syncTimeWithNTP(); // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) {
timekeeper.syncTimeWithNTP();
}
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready // 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
// Do NOT start if WiFiManager portal is active (port 80 conflict!) // Do NOT start if WiFiManager portal is active (port 80 conflict!)