Added HTTP-API support, Standalone AP Support and Built-in Melodies
This commit is contained in:
253
vesper/src/BuiltInMelodies/BuiltInMelodies.hpp
Normal file
253
vesper/src/BuiltInMelodies/BuiltInMelodies.hpp
Normal 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
|
||||
}
|
||||
*/
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
187
vesper/src/BuiltInMelodies/README.md
Normal file
187
vesper/src/BuiltInMelodies/README.md
Normal 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`!
|
||||
@@ -31,7 +31,9 @@ CommunicationRouter::CommunicationRouter(ConfigManager& configManager,
|
||||
, _mqttClient(configManager, networking)
|
||||
, _clientManager()
|
||||
, _wsServer(webSocket, _clientManager)
|
||||
, _commandHandler(configManager, otaManager) {}
|
||||
, _commandHandler(configManager, otaManager)
|
||||
, _httpHandler(server, configManager)
|
||||
, _settingsServer(server, configManager, networking) {}
|
||||
|
||||
CommunicationRouter::~CommunicationRouter() {}
|
||||
|
||||
@@ -93,7 +95,22 @@ void CommunicationRouter::begin() {
|
||||
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(" • 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) {
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
#include "../WebSocketServer/WebSocketServer.hpp"
|
||||
#include "../CommandHandler/CommandHandler.hpp"
|
||||
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
||||
#include "../HTTPRequestHandler/HTTPRequestHandler.hpp"
|
||||
#include "../../ClientManager/ClientManager.hpp"
|
||||
#include "../../SettingsWebServer/SettingsWebServer.hpp"
|
||||
|
||||
class ConfigManager;
|
||||
class OTAManager;
|
||||
@@ -113,6 +115,8 @@ private:
|
||||
ClientManager _clientManager;
|
||||
WebSocketServer _wsServer;
|
||||
CommandHandler _commandHandler;
|
||||
HTTPRequestHandler _httpHandler;
|
||||
SettingsWebServer _settingsServer;
|
||||
|
||||
// Message handlers
|
||||
void onMqttMessage(const String& topic, const String& payload);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -790,6 +790,12 @@ bool ConfigManager::loadNetworkConfig() {
|
||||
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")) {
|
||||
String ipStr = doc["ip"].as<String>();
|
||||
if (!ipStr.isEmpty() && ipStr != "0.0.0.0") {
|
||||
@@ -848,6 +854,9 @@ bool ConfigManager::saveNetworkConfig() {
|
||||
doc["dns1"] = networkConfig.dns1.toString();
|
||||
doc["dns2"] = networkConfig.dns2.toString();
|
||||
|
||||
// Save permanent AP mode setting
|
||||
doc["permanentAPMode"] = networkConfig.permanentAPMode;
|
||||
|
||||
char buffer[512];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ public:
|
||||
String apSsid; // 📡 Auto-generated AP name
|
||||
String apPass; // 🔐 AP is Open. No Password
|
||||
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,
|
||||
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
|
||||
bool loadNetworkConfig();
|
||||
bool saveNetworkConfig();
|
||||
|
||||
@@ -64,6 +64,13 @@ void Networking::begin() {
|
||||
// Clear Previous Settings, USE once to test.
|
||||
//_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
|
||||
auto& hwConfig = _configManager.getHardwareConfig();
|
||||
ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
|
||||
@@ -134,6 +141,18 @@ void Networking::startWiFiPortal() {
|
||||
|
||||
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())) {
|
||||
LOG_INFO("WiFi configured successfully via portal");
|
||||
onWiFiConnected();
|
||||
@@ -397,6 +416,8 @@ String Networking::getLocalIP() const {
|
||||
return ETH.localIP().toString();
|
||||
case ConnectionType::WIFI:
|
||||
return WiFi.localIP().toString();
|
||||
case ConnectionType::AP:
|
||||
return WiFi.softAPIP().toString();
|
||||
default:
|
||||
return "0.0.0.0";
|
||||
}
|
||||
@@ -486,3 +507,62 @@ void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,13 +66,15 @@ enum class NetworkState {
|
||||
WIFI_PORTAL_MODE,
|
||||
CONNECTED_ETHERNET,
|
||||
CONNECTED_WIFI,
|
||||
RECONNECTING
|
||||
RECONNECTING,
|
||||
AP_MODE_PERMANENT
|
||||
};
|
||||
|
||||
enum class ConnectionType {
|
||||
NONE,
|
||||
ETHERNET,
|
||||
WIFI
|
||||
WIFI,
|
||||
AP
|
||||
};
|
||||
|
||||
class Networking {
|
||||
@@ -98,6 +100,10 @@ public:
|
||||
// Manual connection control (for testing/debugging)
|
||||
void forceReconnect();
|
||||
|
||||
// AP Mode control
|
||||
void startPermanentAPMode();
|
||||
bool isInAPMode() const { return _state == NetworkState::AP_MODE_PERMANENT; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||
#include "../BellEngine/BellEngine.hpp"
|
||||
#include "../Telemetry/Telemetry.hpp"
|
||||
#include "../BuiltInMelodies/BuiltInMelodies.hpp"
|
||||
|
||||
// Note: Removed global melody_steps dependency for cleaner architecture
|
||||
|
||||
@@ -245,7 +246,30 @@ void Player::setMelodyAttributes(JsonVariant doc) {
|
||||
}
|
||||
|
||||
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);
|
||||
if (!bin_file) {
|
||||
@@ -286,7 +310,7 @@ void Player::loadMelodyInRAM() {
|
||||
_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();
|
||||
}
|
||||
|
||||
|
||||
365
vesper/src/SettingsWebServer/SettingsWebServer.cpp
Normal file
365
vesper/src/SettingsWebServer/SettingsWebServer.cpp
Normal 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;
|
||||
}
|
||||
66
vesper/src/SettingsWebServer/SettingsWebServer.hpp
Normal file
66
vesper/src/SettingsWebServer/SettingsWebServer.hpp
Normal 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();
|
||||
};
|
||||
@@ -24,8 +24,9 @@
|
||||
* 🎯 KEY FEATURES:
|
||||
* ✅ Microsecond-precision bell timing (BellEngine)
|
||||
* ✅ Multi-hardware support (PCF8574, GPIO, Mock)
|
||||
* ✅ Dual network connectivity (Ethernet + WiFi)
|
||||
* ✅ Dual Communication Support (MQTT + WebSocket)
|
||||
* ✅ Dual network connectivity (Ethernet + WiFi + Permanent AP Mode)
|
||||
* ✅ Multi-protocol communication (MQTT + WebSocket + HTTP REST API)
|
||||
* ✅ Web settings interface for network mode switching
|
||||
* ✅ Real-time telemetry and load monitoring
|
||||
* ✅ Over-the-air firmware updates
|
||||
* ✅ SD card configuration and file management
|
||||
@@ -33,8 +34,9 @@
|
||||
* ✅ Comprehensive logging system
|
||||
*
|
||||
* 📡 COMMUNICATION PROTOCOLS:
|
||||
* • MQTT (SSL/TLS via PubSubClient on Core 0)
|
||||
* • MQTT (SSL/TLS via AsyncMqttClient on Core 0)
|
||||
* • WebSocket (Real-time web interface)
|
||||
* • HTTP REST API (Command execution via HTTP)
|
||||
* • UDP Discovery (Auto-discovery service)
|
||||
* • HTTP/HTTPS (OTA updates)
|
||||
*
|
||||
@@ -351,7 +353,10 @@ void setup()
|
||||
communication.onNetworkConnected();
|
||||
|
||||
// Non-blocking NTP sync (graceful without internet)
|
||||
// Skip NTP sync in AP mode (no internet connection)
|
||||
if (!networking.isInAPMode()) {
|
||||
timekeeper.syncTimeWithNTP();
|
||||
}
|
||||
|
||||
// Start AsyncWebServer when network becomes available (only once!)
|
||||
if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
||||
@@ -370,7 +375,10 @@ void setup()
|
||||
communication.onNetworkConnected();
|
||||
|
||||
// Non-blocking NTP sync (graceful without internet)
|
||||
// Skip NTP sync in AP mode (no internet connection)
|
||||
if (!networking.isInAPMode()) {
|
||||
timekeeper.syncTimeWithNTP();
|
||||
}
|
||||
|
||||
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
|
||||
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
||||
|
||||
Reference in New Issue
Block a user