Merge pull request 'feature/refactoring' (#13) from feature/multimatrix-example into main

Reviewed-on: #13
This commit is contained in:
2025-10-14 18:17:23 +02:00
37 changed files with 1177 additions and 76 deletions

View File

@@ -93,12 +93,12 @@ void setup() {
spore.setup();
// Create and register custom services
RelayService* relayService = new RelayService(spore.getTaskManager(), 2);
spore.addService(relayService);
RelayService* relayService = new RelayService(spore.getContext(), spore.getTaskManager(), 2);
spore.registerService(relayService);
// Or using smart pointers
auto sensorService = std::make_shared<SensorService>();
spore.addService(sensorService);
auto sensorService = std::make_shared<SensorService>(spore.getContext(), spore.getTaskManager());
spore.registerService(sensorService);
// Start the API server and complete initialization
spore.begin();

View File

@@ -21,11 +21,13 @@ The system architecture consists of several key components working together:
### API Server
- **HTTP API Server**: RESTful API for cluster management
- **Dynamic Endpoint Registration**: Automatic API endpoint discovery
- **Dynamic Endpoint Registration**: Services register endpoints via `registerEndpoints(ApiServer&)`
- **Service Registry**: Track available services across the cluster
- **Service Lifecycle**: Services register both endpoints and tasks through unified interface
### Task Scheduler
- **Cooperative Multitasking**: Background task management system (`TaskManager`)
- **Service Task Registration**: Services register tasks via `registerTasks(TaskManager&)`
- **Task Lifecycle Management**: Enable/disable tasks and set intervals at runtime
- **Execution Model**: Tasks run in `Spore::loop()` when their interval elapses

View File

@@ -29,13 +29,13 @@ spore/
│ │ ├── NetworkManager.cpp # WiFi and network handling
│ │ ├── TaskManager.cpp # Background task management
│ │ └── NodeContext.cpp # Central context and events
│ ├── services/ # Built-in services
│ │ ├── NodeService.cpp
│ │ ├── NetworkService.cpp
│ │ ├── ClusterService.cpp
│ │ ├── TaskService.cpp
│ │ ├── StaticFileService.cpp
│ │ └── MonitoringService.cpp
│ ├── services/ # Built-in services (implement Service interface)
│ │ ├── NodeService.cpp # registerEndpoints() + registerTasks()
│ │ ├── NetworkService.cpp # registerEndpoints() + registerTasks()
│ │ ├── ClusterService.cpp # registerEndpoints() + registerTasks()
│ │ ├── TaskService.cpp # registerEndpoints() + registerTasks()
│ │ ├── StaticFileService.cpp # registerEndpoints() + registerTasks()
│ │ └── MonitoringService.cpp # registerEndpoints() + registerTasks()
│ └── types/ # Shared types
├── include/ # Header files
├── examples/ # Example apps per env (base, relay, neopattern)

View File

@@ -11,6 +11,31 @@ The TaskManager system provides:
- **Status Monitoring**: View task status and configuration
- **Automatic Lifecycle**: Tasks are automatically managed and executed
## Service Interface Integration
Services now implement a unified interface for both endpoint and task registration:
```cpp
class MyService : public Service {
public:
void registerEndpoints(ApiServer& api) override {
// Register HTTP endpoints
api.registerEndpoint("/api/my/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatus(request); });
}
void registerTasks(TaskManager& taskManager) override {
// Register background tasks
taskManager.registerTask("my_heartbeat", 2000,
[this]() { sendHeartbeat(); });
taskManager.registerTask("my_maintenance", 30000,
[this]() { performMaintenance(); });
}
const char* getName() const override { return "MyService"; }
};
```
## Basic Usage
```cpp
@@ -27,6 +52,46 @@ taskManager.registerTask("maintenance", 30000, maintenanceFunction);
taskManager.initialize();
```
## Service Lifecycle
The Spore framework automatically manages service registration and task lifecycle:
### Service Registration Process
1. **Service Creation**: Services are created with required dependencies (NodeContext, TaskManager, etc.)
2. **Service Registration**: Services are registered with the Spore framework via `spore.registerService()`
3. **Endpoint Registration**: When `spore.begin()` is called, `registerEndpoints()` is called for each service
4. **Task Registration**: Simultaneously, `registerTasks()` is called for each service
5. **Task Initialization**: The TaskManager initializes all registered tasks
6. **Execution**: Tasks run in the main loop when their intervals elapse
### Framework Integration
```cpp
void setup() {
spore.setup();
// Create service with dependencies
MyService* service = new MyService(spore.getContext(), spore.getTaskManager());
// Register service (endpoints and tasks will be registered when begin() is called)
spore.registerService(service);
// This triggers registerEndpoints() and registerTasks() for all services
spore.begin();
}
```
### Dynamic Service Addition
Services can be added after the framework has started:
```cpp
// Add service to running framework
MyService* newService = new MyService(spore.getContext(), spore.getTaskManager());
spore.registerService(newService); // Immediately registers endpoints and tasks
```
## Task Registration Methods
### Using std::bind with Member Functions (Recommended)
@@ -153,7 +218,62 @@ taskManager.registerTask("lambda_task", 2000,
## Adding Custom Tasks
### Method 1: Using std::bind (Recommended)
### Method 1: Service Interface (Recommended)
1. **Create your service class implementing the Service interface**:
```cpp
class SensorService : public Service {
public:
SensorService(NodeContext& ctx, TaskManager& taskManager)
: ctx(ctx), taskManager(taskManager) {}
void registerEndpoints(ApiServer& api) override {
api.registerEndpoint("/api/sensor/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatus(request); });
}
void registerTasks(TaskManager& taskManager) override {
taskManager.registerTask("temp_read", 1000,
[this]() { readTemperature(); });
taskManager.registerTask("calibrate", 60000,
[this]() { calibrateSensors(); });
}
const char* getName() const override { return "SensorService"; }
private:
NodeContext& ctx;
TaskManager& taskManager;
void readTemperature() {
// Read sensor logic
Serial.println("Reading temperature");
}
void calibrateSensors() {
// Calibration logic
Serial.println("Calibrating sensors");
}
void handleStatus(AsyncWebServerRequest* request) {
// Handle status request
}
};
```
2. **Register with Spore framework**:
```cpp
void setup() {
spore.setup();
SensorService* sensorService = new SensorService(spore.getContext(), spore.getTaskManager());
spore.registerService(sensorService);
spore.begin(); // This will call registerTasks() automatically
}
```
### Method 2: Direct TaskManager Registration
1. **Create your service class**:
```cpp
@@ -181,7 +301,7 @@ taskManager.registerTask("lambda_task", 2000,
std::bind(&SensorService::calibrateSensors, &sensors));
```
### Method 2: Traditional Functions
### Method 3: Traditional Functions
1. **Define your task function**:
```cpp
@@ -308,13 +428,55 @@ curl -X POST http://192.168.1.100/api/tasks/control \
## Best Practices
1. **Use std::bind for member functions**: Cleaner than wrapper functions
2. **Group related tasks**: Register multiple related operations in a single task
1. **Use Service Interface**: Implement the Service interface for clean integration with the framework
2. **Group related tasks**: Register multiple related operations in a single service
3. **Monitor task health**: Use the status API to monitor task performance
4. **Plan intervals carefully**: Balance responsiveness with system resources
5. **Use descriptive names**: Make task names clear and meaningful
6. **Separate concerns**: Use registerEndpoints() for HTTP API and registerTasks() for background work
7. **Dependency injection**: Pass required dependencies (NodeContext, TaskManager) to service constructors
## Migration from Wrapper Functions
## Migration to Service Interface
### Before (manual task registration in constructor):
```cpp
class MyService : public Service {
public:
MyService(TaskManager& taskManager) : taskManager(taskManager) {
// Tasks registered in constructor
taskManager.registerTask("heartbeat", 2000, [this]() { sendHeartbeat(); });
}
void registerEndpoints(ApiServer& api) override {
// Only endpoints registered here
}
};
```
### After (using Service interface):
```cpp
class MyService : public Service {
public:
MyService(TaskManager& taskManager) : taskManager(taskManager) {
// No task registration in constructor
}
void registerEndpoints(ApiServer& api) override {
// Register HTTP endpoints
api.registerEndpoint("/api/my/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatus(request); });
}
void registerTasks(TaskManager& taskManager) override {
// Register background tasks
taskManager.registerTask("heartbeat", 2000, [this]() { sendHeartbeat(); });
}
const char* getName() const override { return "MyService"; }
};
```
### Migration from Wrapper Functions
### Before (with wrapper functions):
```cpp
@@ -335,11 +497,11 @@ taskManager.registerTask("cluster_listen", interval,
## Compatibility
- The new `std::bind` support is fully backward compatible
- Existing code using function pointers will continue to work
- You can mix both approaches in the same project
- The new Service interface is fully backward compatible
- Existing code using direct TaskManager registration will continue to work
- You can mix Service interface and direct registration in the same project
- All existing TaskManager methods remain unchanged
- New status monitoring methods are additive and don't break existing functionality
- The Service interface provides a cleaner, more organized approach for framework integration
## Related Documentation

View File

@@ -0,0 +1,248 @@
#include "MultiMatrixService.h"
#include "spore/core/ApiServer.h"
#include "spore/util/Logging.h"
#include <ArduinoJson.h>
#include <SoftwareSerial.h>
namespace {
constexpr uint8_t DEFAULT_VOLUME = 15;
constexpr char API_STATUS_ENDPOINT[] = "/api/audio/status";
constexpr char API_CONTROL_ENDPOINT[] = "/api/audio";
constexpr char EVENT_TOPIC[] = "audio/player";
}
MultiMatrixService::MultiMatrixService(NodeContext& ctx, TaskManager& taskManager, uint8_t rxPin, uint8_t txPin, uint8_t potentiometerPin)
: m_ctx(ctx),
m_taskManager(taskManager),
m_serial(std::make_unique<SoftwareSerial>(rxPin, txPin)),
m_potentiometerPin(potentiometerPin),
m_volume(DEFAULT_VOLUME),
m_playerReady(false),
m_loopEnabled(false) {
pinMode(m_potentiometerPin, INPUT);
m_serial->begin(9600);
// DFPlayer Mini requires time to initialize after power-on
delay(1000);
if (m_player.begin(*m_serial)) {
m_playerReady = true;
m_player.setTimeOut(500);
m_player.EQ(DFPLAYER_EQ_NORMAL);
m_player.volume(m_volume);
LOG_INFO("MultiMatrixService", "DFPlayer initialized successfully");
publishEvent("ready");
} else {
LOG_ERROR("MultiMatrixService", "Failed to initialize DFPlayer");
}
}
void MultiMatrixService::registerEndpoints(ApiServer& api) {
api.registerEndpoint(API_STATUS_ENDPOINT, HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.registerEndpoint(API_CONTROL_ENDPOINT, HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("action"), true, String("body"), String("string"),
{String("play"), String("stop"), String("pause"), String("resume"), String("next"), String("previous"), String("volume"), String("loop")}},
ParamSpec{String("volume"), false, String("body"), String("numberRange"), {}, String("15")},
ParamSpec{String("loop"), false, String("body"), String("boolean"), {}}
});
}
bool MultiMatrixService::isReady() const {
return m_playerReady;
}
uint8_t MultiMatrixService::getVolume() const {
return m_volume;
}
bool MultiMatrixService::isLoopEnabled() const {
return m_loopEnabled;
}
void MultiMatrixService::play() {
if (!m_playerReady) {
return;
}
m_player.play();
publishEvent("play");
LOG_INFO("MultiMatrixService", "Playback started");
}
void MultiMatrixService::stop() {
if (!m_playerReady) {
return;
}
m_player.stop();
publishEvent("stop");
LOG_INFO("MultiMatrixService", "Playback stopped");
}
void MultiMatrixService::pause() {
if (!m_playerReady) {
return;
}
m_player.pause();
publishEvent("pause");
LOG_INFO("MultiMatrixService", "Playback paused");
}
void MultiMatrixService::resume() {
if (!m_playerReady) {
return;
}
m_player.start();
publishEvent("resume");
LOG_INFO("MultiMatrixService", "Playback resumed");
}
void MultiMatrixService::next() {
if (!m_playerReady) {
return;
}
m_player.next();
publishEvent("next");
LOG_INFO("MultiMatrixService", "Next track");
}
void MultiMatrixService::previous() {
if (!m_playerReady) {
return;
}
m_player.previous();
publishEvent("previous");
LOG_INFO("MultiMatrixService", "Previous track");
}
void MultiMatrixService::setVolume(uint8_t volume) {
if (!m_playerReady) {
return;
}
const uint8_t clampedVolume = std::min<uint8_t>(volume, MAX_VOLUME);
if (clampedVolume == m_volume) {
return;
}
applyVolume(clampedVolume);
}
void MultiMatrixService::setLoop(bool enabled) {
if (!m_playerReady) {
return;
}
m_loopEnabled = enabled;
if (enabled) {
m_player.enableLoop();
} else {
m_player.disableLoop();
}
publishEvent("loop");
LOG_INFO("MultiMatrixService", String("Loop ") + (enabled ? "enabled" : "disabled"));
}
void MultiMatrixService::registerTasks(TaskManager& taskManager) {
taskManager.registerTask("multimatrix_potentiometer", POTENTIOMETER_SAMPLE_INTERVAL_MS,
[this]() { pollPotentiometer(); });
}
void MultiMatrixService::pollPotentiometer() {
if (!m_playerReady) {
return;
}
const uint16_t rawValue = analogRead(static_cast<uint8_t>(m_potentiometerPin));
const uint8_t targetVolume = calculateVolumeFromPotentiometer(rawValue);
if (targetVolume > m_volume + POT_VOLUME_EPSILON || targetVolume + POT_VOLUME_EPSILON < m_volume) {
applyVolume(targetVolume);
}
}
uint8_t MultiMatrixService::calculateVolumeFromPotentiometer(uint16_t rawValue) const {
// Clamp raw value to prevent underflow (analogRead can return 1024)
const uint16_t clampedValue = std::min<uint16_t>(rawValue, 1023U);
// Invert: all down (0) = max volume, all up (1023) = min volume
const uint16_t invertedValue = 1023U - clampedValue;
const uint8_t scaledVolume = static_cast<uint8_t>((static_cast<uint32_t>(invertedValue) * MAX_VOLUME) / 1023U);
return std::min<uint8_t>(scaledVolume, MAX_VOLUME);
}
void MultiMatrixService::applyVolume(uint8_t targetVolume) {
m_volume = targetVolume;
m_player.volume(m_volume);
publishEvent("volume");
LOG_INFO("MultiMatrixService", String("Volume set to ") + String(m_volume));
}
void MultiMatrixService::handleStatusRequest(AsyncWebServerRequest* request) {
StaticJsonDocument<192> doc;
doc["ready"] = m_playerReady;
doc["volume"] = static_cast<int>(m_volume);
doc["loop"] = m_loopEnabled;
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void MultiMatrixService::handleControlRequest(AsyncWebServerRequest* request) {
String action = request->hasParam("action", true) ? request->getParam("action", true)->value() : "";
bool ok = true;
if (action.equalsIgnoreCase("play")) {
play();
} else if (action.equalsIgnoreCase("stop")) {
stop();
} else if (action.equalsIgnoreCase("pause")) {
pause();
} else if (action.equalsIgnoreCase("resume")) {
resume();
} else if (action.equalsIgnoreCase("next")) {
next();
} else if (action.equalsIgnoreCase("previous")) {
previous();
} else if (action.equalsIgnoreCase("volume")) {
if (request->hasParam("volume", true)) {
int volumeValue = request->getParam("volume", true)->value().toInt();
setVolume(static_cast<uint8_t>(std::max(0, std::min(static_cast<int>(MAX_VOLUME), volumeValue))));
} else {
ok = false;
}
} else if (action.equalsIgnoreCase("loop")) {
if (request->hasParam("loop", true)) {
String loopValue = request->getParam("loop", true)->value();
bool enabled = loopValue.equalsIgnoreCase("true") || loopValue == "1";
setLoop(enabled);
} else {
ok = false;
}
} else {
ok = false;
}
StaticJsonDocument<256> resp;
resp["success"] = ok;
resp["ready"] = m_playerReady;
resp["volume"] = static_cast<int>(m_volume);
resp["loop"] = m_loopEnabled;
if (!ok) {
resp["message"] = "Invalid action";
}
String json;
serializeJson(resp, json);
request->send(ok ? 200 : 400, "application/json", json);
}
void MultiMatrixService::publishEvent(const char* action) {
StaticJsonDocument<192> doc;
doc["action"] = action;
doc["volume"] = static_cast<int>(m_volume);
doc["loop"] = m_loopEnabled;
String payload;
serializeJson(doc, payload);
m_ctx.fire(EVENT_TOPIC, &payload);
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <Arduino.h>
#include <algorithm>
#include <memory>
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include "spore/Service.h"
#include "spore/core/TaskManager.h"
#include "spore/core/NodeContext.h"
class ApiServer;
class AsyncWebServerRequest;
class MultiMatrixService : public Service {
public:
MultiMatrixService(NodeContext& ctx, TaskManager& taskManager, uint8_t rxPin, uint8_t txPin, uint8_t potentiometerPin);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "MultiMatrixAudio"; }
bool isReady() const;
uint8_t getVolume() const;
bool isLoopEnabled() const;
void play();
void stop();
void pause();
void resume();
void next();
void previous();
void setVolume(uint8_t volume);
void setLoop(bool enabled);
private:
static constexpr uint16_t POTENTIOMETER_SAMPLE_INTERVAL_MS = 200;
static constexpr uint8_t MAX_VOLUME = 30;
static constexpr uint8_t POT_VOLUME_EPSILON = 2;
void pollPotentiometer();
void handleStatusRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
uint8_t calculateVolumeFromPotentiometer(uint16_t rawValue) const;
void applyVolume(uint8_t targetVolume);
void publishEvent(const char* action);
NodeContext& m_ctx;
TaskManager& m_taskManager;
std::unique_ptr<SoftwareSerial> m_serial;
DFRobotDFPlayerMini m_player;
uint8_t m_potentiometerPin;
uint8_t m_volume;
bool m_playerReady;
bool m_loopEnabled;
};

View File

@@ -0,0 +1,19 @@
# Multi-Matrix
This example combines different capabilities:
- Spore base stack
- use PixelStreamController in Matrix mode 16x16
- DFRobotDFPlayerMini audio playback
- analog potentiometer to controll audio volume
- API and web interface to control the audio player (start, stop, pause, next / previous track, volume)
## MCU
- Wemos D1 Mini
## Pin Configuration
```
#define MP3PLAYER_PIN_RX D3
#define MP3PLAYER_PIN_TX D4
#define MATRIX_PIN D2
#define POTI_PIN A0
```

View File

@@ -0,0 +1,508 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Matrix Audio Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.player-container {
background: rgba(255, 255, 255, 0.98);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 3rem 2.5rem;
max-width: 420px;
width: 100%;
backdrop-filter: blur(10px);
}
.player-header {
text-align: center;
margin-bottom: 2rem;
}
.player-title {
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 1rem;
background: rgba(102, 126, 234, 0.1);
border-radius: 20px;
font-size: 0.875rem;
color: #667eea;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
animation: pulse 2s infinite;
}
.status-dot.inactive {
background: #ef4444;
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.main-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin: 2.5rem 0;
}
.control-btn {
width: 52px;
height: 52px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
font-size: 1.2rem;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.control-btn:active {
transform: translateY(0);
}
.play-btn {
width: 72px;
height: 72px;
font-size: 1.5rem;
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
}
.secondary-controls {
display: flex;
gap: 0.75rem;
margin-bottom: 2rem;
justify-content: center;
}
.secondary-btn {
padding: 0.6rem 1.25rem;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 12px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.3s ease;
}
.secondary-btn:hover {
background: #667eea;
color: white;
transform: translateY(-1px);
}
.secondary-btn:active {
transform: translateY(0);
}
.volume-section {
margin: 2rem 0;
}
.volume-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: #64748b;
font-weight: 600;
}
.volume-value {
color: #667eea;
font-weight: 700;
font-size: 1rem;
}
.volume-slider {
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
outline: none;
opacity: 0.7;
transition: opacity 0.2s;
}
.volume-slider:hover {
opacity: 1;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.5);
border: 2px solid #667eea;
}
.volume-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.5);
border: 2px solid #667eea;
}
.loop-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
margin-top: 1.5rem;
}
.loop-label {
font-size: 0.95rem;
font-weight: 600;
color: #475569;
display: flex;
align-items: center;
gap: 0.5rem;
}
.toggle-switch {
position: relative;
width: 52px;
height: 28px;
background: #cbd5e1;
border-radius: 14px;
cursor: pointer;
transition: background 0.3s ease;
}
.toggle-switch.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.toggle-slider {
position: absolute;
top: 3px;
left: 3px;
width: 22px;
height: 22px;
background: white;
border-radius: 50%;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-switch.active .toggle-slider {
transform: translateX(24px);
}
.icon {
display: inline-block;
}
@media (max-width: 480px) {
.player-container {
padding: 2rem 1.5rem;
}
.player-title {
font-size: 1.5rem;
}
.control-btn {
width: 48px;
height: 48px;
}
.play-btn {
width: 64px;
height: 64px;
}
}
</style>
</head>
<body>
<div class="player-container">
<div class="player-header">
<h1 class="player-title">Multi-Matrix Audio</h1>
<div class="status-badge">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Connecting...</span>
</div>
</div>
<div class="main-controls">
<button class="control-btn" onclick="sendAction('previous')" title="Previous">
<span class="icon"></span>
</button>
<button class="control-btn play-btn" id="playBtn" onclick="togglePlayPause()" title="Play/Pause">
<span class="icon" id="playIcon"></span>
</button>
<button class="control-btn" onclick="sendAction('next')" title="Next">
<span class="icon"></span>
</button>
</div>
<div class="secondary-controls">
<button class="secondary-btn" onclick="sendAction('stop')">⏹ Stop</button>
<button class="secondary-btn" onclick="sendAction('resume')">▶ Resume</button>
</div>
<div class="volume-section">
<div class="volume-header">
<span>🔊 Volume</span>
<span class="volume-value" id="volumeDisplay">15</span>
</div>
<input type="range"
class="volume-slider"
id="volumeSlider"
min="0"
max="30"
value="15"
oninput="updateVolumeDisplay(this.value)"
onchange="setVolume(this.value)">
</div>
<div class="loop-toggle">
<div class="loop-label">
<span>🔁 Loop Mode</span>
</div>
<div class="toggle-switch" id="loopToggle" onclick="toggleLoop()">
<div class="toggle-slider"></div>
</div>
</div>
</div>
<script>
let isPlaying = false;
let loopEnabled = false;
async function fetchStatus() {
try {
const response = await fetch('/api/audio/status');
if (!response.ok) {
throw new Error('Status request failed');
}
const status = await response.json();
updateUIStatus(status);
} catch (error) {
document.getElementById('statusText').textContent = 'Error';
document.getElementById('statusDot').className = 'status-dot inactive';
console.error(error);
}
}
function updateUIStatus(status) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (status.ready) {
statusDot.className = 'status-dot';
statusText.textContent = 'Ready';
} else {
statusDot.className = 'status-dot inactive';
statusText.textContent = 'Not Ready';
}
if ('volume' in status) {
document.getElementById('volumeSlider').value = status.volume;
document.getElementById('volumeDisplay').textContent = status.volume;
}
if ('loop' in status) {
loopEnabled = status.loop;
const loopToggle = document.getElementById('loopToggle');
if (loopEnabled) {
loopToggle.classList.add('active');
} else {
loopToggle.classList.remove('active');
}
}
}
async function sendAction(action, params = {}) {
try {
const formData = new URLSearchParams({ action, ...params });
const response = await fetch('/api/audio', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData
});
const result = await response.json();
updateUIStatus(result);
if (action === 'play' || action === 'resume') {
isPlaying = true;
updatePlayButton();
} else if (action === 'pause' || action === 'stop') {
isPlaying = false;
updatePlayButton();
}
if (!result.success) {
alert(result.message || 'Action failed');
}
} catch (error) {
alert('Request failed: ' + error.message);
}
}
function togglePlayPause() {
if (isPlaying) {
sendAction('pause');
} else {
sendAction('play');
}
}
function updatePlayButton() {
const playIcon = document.getElementById('playIcon');
playIcon.textContent = isPlaying ? '⏸' : '▶';
}
function updateVolumeDisplay(value) {
document.getElementById('volumeDisplay').textContent = value;
}
function setVolume(value) {
sendAction('volume', { volume: value });
}
function toggleLoop() {
loopEnabled = !loopEnabled;
const loopToggle = document.getElementById('loopToggle');
if (loopEnabled) {
loopToggle.classList.add('active');
} else {
loopToggle.classList.remove('active');
}
sendAction('loop', { loop: loopEnabled });
}
fetchStatus();
setInterval(fetchStatus, 5000);
function setupWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
ws.onmessage = (event) => {
try {
const payload = JSON.parse(event.data);
if (payload.event === 'audio/player' && payload.payload) {
const data = JSON.parse(payload.payload);
// Skip debug messages
if (data.action === 'pot_debug') {
return;
}
if ('volume' in data) {
document.getElementById('volumeSlider').value = data.volume;
document.getElementById('volumeDisplay').textContent = data.volume;
}
if ('loop' in data) {
loopEnabled = data.loop;
const loopToggle = document.getElementById('loopToggle');
if (loopEnabled) {
loopToggle.classList.add('active');
} else {
loopToggle.classList.remove('active');
}
}
if (data.action) {
const statusText = document.getElementById('statusText');
const statusDot = document.getElementById('statusDot');
if (data.action === 'play' || data.action === 'resume' || data.action === 'next' || data.action === 'previous') {
statusText.textContent = 'Playing';
statusDot.className = 'status-dot';
isPlaying = true;
updatePlayButton();
} else if (data.action === 'pause') {
statusText.textContent = 'Paused';
statusDot.className = 'status-dot';
isPlaying = false;
updatePlayButton();
} else if (data.action === 'stop') {
statusText.textContent = 'Stopped';
statusDot.className = 'status-dot inactive';
isPlaying = false;
updatePlayButton();
} else if (data.action === 'ready') {
statusText.textContent = 'Ready';
statusDot.className = 'status-dot';
}
}
}
} catch (err) {
console.error('WebSocket parse error', err);
}
};
ws.onclose = () => {
setTimeout(setupWebSocket, 3000);
};
}
setupWebSocket();
</script>
</body>
</html>

View File

@@ -0,0 +1,54 @@
#include <Arduino.h>
#include <memory>
#include "spore/Spore.h"
#include "spore/util/Logging.h"
#include "../pixelstream/PixelStreamController.h"
#include "MultiMatrixService.h"
namespace {
constexpr uint8_t MATRIX_PIN = D2;
constexpr uint16_t MATRIX_PIXEL_COUNT = 256;
constexpr uint8_t MATRIX_BRIGHTNESS = 40;
constexpr uint16_t MATRIX_WIDTH = 16;
constexpr bool MATRIX_SERPENTINE = false;
constexpr neoPixelType MATRIX_PIXEL_TYPE = NEO_GRB + NEO_KHZ800;
constexpr uint8_t MP3PLAYER_PIN_RX = D3;
constexpr uint8_t MP3PLAYER_PIN_TX = D4;
constexpr uint8_t POTENTIOMETER_PIN = A0;
}
Spore spore({
{"app", "multimatrix"},
{"role", "media"},
{"matrix", String(MATRIX_PIXEL_COUNT)}
});
std::unique_ptr<PixelStreamController> pixelController;
std::shared_ptr<MultiMatrixService> audioService;
void setup() {
spore.setup();
PixelStreamConfig config{
MATRIX_PIN,
MATRIX_PIXEL_COUNT,
MATRIX_BRIGHTNESS,
MATRIX_WIDTH,
MATRIX_SERPENTINE,
MATRIX_PIXEL_TYPE
};
pixelController = std::make_unique<PixelStreamController>(spore.getContext(), config);
pixelController->begin();
audioService = std::make_shared<MultiMatrixService>(spore.getContext(), spore.getTaskManager(), MP3PLAYER_PIN_RX, MP3PLAYER_PIN_TX, POTENTIOMETER_PIN);
spore.registerService(audioService);
spore.begin();
LOG_INFO("MultiMatrix", "Setup complete");
}
void loop() {
spore.loop();
}

View File

@@ -34,7 +34,6 @@ NeoPatternService::NeoPatternService(NodeContext& ctx, TaskManager& taskMgr, con
neoPattern->Direction = static_cast<::direction>(direction);
registerPatterns();
registerTasks();
registerEventHandlers();
initialized = true;
@@ -49,17 +48,17 @@ NeoPatternService::~NeoPatternService() {
void NeoPatternService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/neopattern/status", HTTP_GET,
api.registerEndpoint("/api/neopattern/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
// Patterns list endpoint
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
api.registerEndpoint("/api/neopattern/patterns", HTTP_GET,
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
std::vector<ParamSpec>{});
// Control endpoint
api.addEndpoint("/api/neopattern", HTTP_POST,
api.registerEndpoint("/api/neopattern", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
@@ -73,7 +72,7 @@ void NeoPatternService::registerEndpoints(ApiServer& api) {
});
// State endpoint for complex state updates
api.addEndpoint("/api/neopattern/state", HTTP_POST,
api.registerEndpoint("/api/neopattern/state", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleStateRequest(request); },
std::vector<ParamSpec>{});
}
@@ -403,7 +402,7 @@ NeoPatternState NeoPatternService::getState() const {
return currentState;
}
void NeoPatternService::registerTasks() {
void NeoPatternService::registerTasks(TaskManager& taskManager) {
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
LOG_INFO("NeoPattern", "Status update");

View File

@@ -30,6 +30,7 @@ public:
~NeoPatternService();
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "NeoPattern"; }
// Pattern control methods
@@ -47,7 +48,6 @@ public:
NeoPatternState getState() const;
private:
void registerTasks();
void registerPatterns();
void update();
void registerEventHandlers();

View File

@@ -46,7 +46,7 @@ void setup() {
// Create and add custom service
neoPatternService = new NeoPatternService(spore.getContext(), spore.getTaskManager(), config);
spore.addService(neoPatternService);
spore.registerService(neoPatternService);
// Start the API server and complete initialization
spore.begin();

View File

@@ -33,7 +33,7 @@ void setup() {
spore.setup();
relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN);
spore.addService(relayService);
spore.registerService(relayService);
spore.begin();
}

View File

@@ -7,15 +7,14 @@ RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
pinMode(relayPin, OUTPUT);
// Many relay modules are active LOW. Start in OFF state (relay de-energized).
digitalWrite(relayPin, HIGH);
registerTasks();
}
void RelayService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/relay/status", HTTP_GET,
api.registerEndpoint("/api/relay/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/relay", HTTP_POST,
api.registerEndpoint("/api/relay", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("state"), true, String("body"), String("string"),
@@ -82,7 +81,7 @@ void RelayService::toggle() {
}
}
void RelayService::registerTasks() {
void RelayService::registerTasks(TaskManager& taskManager) {
taskManager.registerTask("relay_status_print", 5000, [this]() {
LOG_INFO("RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF"));
});

View File

@@ -8,6 +8,7 @@ class RelayService : public Service {
public:
RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Relay"; }
void turnOn();
@@ -15,8 +16,6 @@ public:
void toggle();
private:
void registerTasks();
NodeContext& ctx;
TaskManager& taskManager;
int relayPin;

View File

@@ -23,7 +23,7 @@ void setup() {
// Create and add custom service
relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN);
spore.addService(relayService);
spore.registerService(relayService);
// Start the API server and complete initialization
spore.begin();

View File

@@ -1,9 +1,12 @@
#pragma once
#include "spore/core/ApiServer.h"
class TaskManager;
class Service {
public:
virtual ~Service() = default;
virtual void registerEndpoints(ApiServer& api) = 0;
virtual void registerTasks(TaskManager& taskManager) = 0;
virtual const char* getName() const = 0;
};

View File

@@ -25,8 +25,8 @@ public:
void loop();
// Service management
void addService(std::shared_ptr<Service> service);
void addService(Service* service);
void registerService(std::shared_ptr<Service> service);
void registerService(Service* service);
// Access to core components
NodeContext& getContext() { return ctx; }

View File

@@ -19,14 +19,14 @@ class ApiServer {
public:
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
void begin();
void addService(Service& service);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void registerService(Service& service);
void registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
void registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params);

View File

@@ -19,7 +19,7 @@ public:
IPAddress localIP;
NodeInfo self;
std::map<String, NodeInfo>* memberList;
Config config;
::Config config;
using EventCallback = std::function<void(void*)>;
std::map<std::string, std::vector<EventCallback>> eventRegistry;

View File

@@ -7,6 +7,7 @@ class ClusterService : public Service {
public:
ClusterService(NodeContext& ctx);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Cluster"; }
private:

View File

@@ -7,6 +7,7 @@ class MonitoringService : public Service {
public:
MonitoringService(CpuUsage& cpuUsage);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Monitoring"; }
// System resource information

View File

@@ -7,6 +7,7 @@ class NetworkService : public Service {
public:
NetworkService(NetworkManager& networkManager);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Network"; }
private:

View File

@@ -8,6 +8,7 @@ class NodeService : public Service {
public:
NodeService(NodeContext& ctx, ApiServer& apiServer);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Node"; }
private:

View File

@@ -9,6 +9,7 @@ class StaticFileService : public Service {
public:
StaticFileService(NodeContext& ctx, ApiServer& apiServer);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return name.c_str(); }
private:

View File

@@ -7,6 +7,7 @@ class TaskService : public Service {
public:
TaskService(TaskManager& taskManager);
void registerEndpoints(ApiServer& api) override;
void registerTasks(TaskManager& taskManager) override;
const char* getName() const override { return "Task"; }
private:

View File

@@ -9,7 +9,7 @@
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = base
;default_envs = base
src_dir = .
data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data
@@ -130,7 +130,7 @@ upload_speed = 115200
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m256.ld
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_flags = -DPIXEL_PIN=TX -DPIXEL_COUNT=256 -DMATRIX_WIDTH=16
@@ -142,3 +142,26 @@ build_src_filter =
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:multimatrix]
platform = platformio/espressif8266@^4.2.1
board = d1_mini
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.flash_mode = dio
board_build.flash_size = 4M
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
dfrobot/DFRobotDFPlayerMini@^1.0.6
build_src_filter =
+<examples/multimatrix/*.cpp>
+<examples/pixelstream/PixelStreamController.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>

View File

@@ -85,7 +85,7 @@ void Spore::loop() {
yield();
}
void Spore::addService(std::shared_ptr<Service> service) {
void Spore::registerService(std::shared_ptr<Service> service) {
if (!service) {
LOG_WARN("Spore", "Attempted to add null service");
return;
@@ -95,21 +95,22 @@ void Spore::addService(std::shared_ptr<Service> service) {
if (apiServerStarted) {
// If API server is already started, register the service immediately
apiServer.addService(*service);
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to running API server");
apiServer.registerService(*service);
service->registerTasks(taskManager);
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to running API server and task manager");
} else {
LOG_INFO("Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)");
}
}
void Spore::addService(Service* service) {
void Spore::registerService(Service* service) {
if (!service) {
LOG_WARN("Spore", "Attempted to add null service");
return;
}
// Wrap raw pointer in shared_ptr with no-op deleter to avoid double-delete
addService(std::shared_ptr<Service>(service, [](Service*){}));
registerService(std::shared_ptr<Service>(service, [](Service*){}));
}
@@ -155,11 +156,12 @@ void Spore::startApiServer() {
LOG_INFO("Spore", "Starting API server...");
// Register all services with API server
// Register all services with API server and task manager
for (auto& service : services) {
if (service) {
apiServer.addService(*service);
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to API server");
apiServer.registerService(*service);
service->registerTasks(taskManager);
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to API server and task manager");
}
}

View File

@@ -31,7 +31,7 @@ void ApiServer::registerEndpoint(const String& uri, int method,
}
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
void ApiServer::registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
@@ -41,7 +41,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
server.on(uri.c_str(), method, requestHandler);
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void ApiServer::registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler) {
// Get current service name if available
String serviceName = "unknown";
@@ -53,7 +53,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
}
// Overloads that also record minimal capability specs
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void ApiServer::registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params) {
// Get current service name if available
String serviceName = "unknown";
@@ -64,7 +64,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
server.on(uri.c_str(), method, requestHandler);
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
void ApiServer::registerEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params) {
// Get current service name if available
@@ -76,7 +76,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}
void ApiServer::addService(Service& service) {
void ApiServer::registerService(Service& service) {
services.push_back(service);
LOG_INFO("API", "Added service: " + String(service.getName()));
}

View File

@@ -4,12 +4,12 @@
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
void ClusterService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/cluster/members", HTTP_GET,
api.registerEndpoint("/api/cluster/members", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
std::vector<ParamSpec>{});
// Generic cluster broadcast endpoint
api.addEndpoint("/api/cluster/event", HTTP_POST,
api.registerEndpoint("/api/cluster/event", HTTP_POST,
[this](AsyncWebServerRequest* request) {
if (!request->hasParam("event", true) || !request->hasParam("payload", true)) {
request->send(400, "application/json", "{\"error\":\"Missing 'event' or 'payload'\"}");
@@ -32,6 +32,10 @@ void ClusterService::registerEndpoints(ApiServer& api) {
});
}
void ClusterService::registerTasks(TaskManager& taskManager) {
// ClusterService doesn't register any tasks itself
}
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();

View File

@@ -10,11 +10,15 @@ MonitoringService::MonitoringService(CpuUsage& cpuUsage)
}
void MonitoringService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/monitoring/resources", HTTP_GET,
api.registerEndpoint("/api/monitoring/resources", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleResourcesRequest(request); },
std::vector<ParamSpec>{});
}
void MonitoringService::registerTasks(TaskManager& taskManager) {
// MonitoringService doesn't register any tasks itself
}
MonitoringService::SystemResources MonitoringService::getSystemResources() const {
SystemResources resources;

View File

@@ -6,20 +6,20 @@ NetworkService::NetworkService(NetworkManager& networkManager)
void NetworkService::registerEndpoints(ApiServer& api) {
// WiFi scanning endpoints
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
api.registerEndpoint("/api/network/wifi/scan", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
api.registerEndpoint("/api/network/wifi/scan", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
std::vector<ParamSpec>{});
// Network status and configuration endpoints
api.addEndpoint("/api/network/status", HTTP_GET,
api.registerEndpoint("/api/network/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
api.registerEndpoint("/api/network/wifi/config", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
std::vector<ParamSpec>{
ParamSpec{String("ssid"), true, String("body"), String("string"), {}, String("")},
@@ -29,6 +29,10 @@ void NetworkService::registerEndpoints(ApiServer& api) {
});
}
void NetworkService::registerTasks(TaskManager& taskManager) {
// NetworkService doesn't register any tasks itself
}
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {
networkManager.scanWifi();

View File

@@ -6,12 +6,12 @@ NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), api
void NodeService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/node/status", HTTP_GET,
api.registerEndpoint("/api/node/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
// Update endpoint with file upload
api.addEndpoint("/api/node/update", HTTP_POST,
api.registerEndpoint("/api/node/update", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleUpdateRequest(request); },
[this](AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) {
handleUpdateUpload(request, filename, index, data, len, final);
@@ -21,17 +21,17 @@ void NodeService::registerEndpoints(ApiServer& api) {
});
// Restart endpoint
api.addEndpoint("/api/node/restart", HTTP_POST,
api.registerEndpoint("/api/node/restart", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
std::vector<ParamSpec>{});
// Endpoints endpoint
api.addEndpoint("/api/node/endpoints", HTTP_GET,
api.registerEndpoint("/api/node/endpoints", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
std::vector<ParamSpec>{});
// Generic local event endpoint
api.addEndpoint("/api/node/event", HTTP_POST,
api.registerEndpoint("/api/node/event", HTTP_POST,
[this](AsyncWebServerRequest* request) {
if (!request->hasParam("event", true) || !request->hasParam("payload", true)) {
request->send(400, "application/json", "{\"error\":\"Missing 'event' or 'payload'\"}");
@@ -49,6 +49,10 @@ void NodeService::registerEndpoints(ApiServer& api) {
});
}
void NodeService::registerTasks(TaskManager& taskManager) {
// NodeService doesn't register any tasks itself
}
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["freeHeap"] = ESP.getFreeHeap();

View File

@@ -20,3 +20,7 @@ void StaticFileService::registerEndpoints(ApiServer& api) {
api.serveStatic("/", LittleFS, "/public", "max-age=3600");
}
void StaticFileService::registerTasks(TaskManager& taskManager) {
// StaticFileService doesn't register any tasks itself
}

View File

@@ -5,11 +5,11 @@
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
void TaskService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/tasks/status", HTTP_GET,
api.registerEndpoint("/api/tasks/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/tasks/control", HTTP_POST,
api.registerEndpoint("/api/tasks/control", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{
@@ -31,6 +31,10 @@ void TaskService::registerEndpoints(ApiServer& api) {
});
}
void TaskService::registerTasks(TaskManager& taskManager) {
// TaskService doesn't register any tasks itself - it manages other tasks
}
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument scratch;
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);

View File

@@ -8,7 +8,7 @@ const HTTP_PORT = process.env.PORT || 3000;
// UDP settings
// Default broadcast; override with unicast address via UDP_ADDR if you have a single receiver
const UDP_BROADCAST_PORT = Number(process.env.UDP_PORT) || 4210;
const UDP_BROADCAST_ADDR = process.env.UDP_ADDR || '10.0.1.144';
const UDP_BROADCAST_ADDR = process.env.UDP_ADDR || '255.255.255.255';
// NeoPixel frame properties for basic validation/logging (no transformation here)
const MATRIX_WIDTH = Number(process.env.MATRIX_WIDTH) || 16;

View File

@@ -6,7 +6,7 @@ const { WebSocketServer } = require('ws');
const HTTP_PORT = process.env.PORT || 3000;
const UDP_BROADCAST_PORT = Number(process.env.UDP_PORT) || 4210;
const UDP_BROADCAST_ADDR = process.env.UDP_ADDR || '10.0.1.144';
const UDP_BROADCAST_ADDR = process.env.UDP_ADDR || '255.255.255.255';
const MATRIX_WIDTH = Number(process.env.MATRIX_WIDTH) || 16;
const MATRIX_HEIGHT = Number(process.env.MATRIX_HEIGHT) || 16;