diff --git a/README.md b/README.md index 29cef7b..56520ae 100644 --- a/README.md +++ b/README.md @@ -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(); - spore.addService(sensorService); + auto sensorService = std::make_shared(spore.getContext(), spore.getTaskManager()); + spore.registerService(sensorService); // Start the API server and complete initialization spore.begin(); diff --git a/docs/Architecture.md b/docs/Architecture.md index dfbb4ba..35cbc35 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -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 diff --git a/docs/Development.md b/docs/Development.md index 9885c78..bcfaa33 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -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) diff --git a/docs/TaskManagement.md b/docs/TaskManagement.md index b583463..07ada60 100644 --- a/docs/TaskManagement.md +++ b/docs/TaskManagement.md @@ -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 diff --git a/examples/multimatrix/MultiMatrixService.cpp b/examples/multimatrix/MultiMatrixService.cpp new file mode 100644 index 0000000..0d8e974 --- /dev/null +++ b/examples/multimatrix/MultiMatrixService.cpp @@ -0,0 +1,248 @@ +#include "MultiMatrixService.h" +#include "spore/core/ApiServer.h" +#include "spore/util/Logging.h" +#include +#include + +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(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{}); + + api.registerEndpoint(API_CONTROL_ENDPOINT, HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + 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(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(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(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((static_cast(invertedValue) * MAX_VOLUME) / 1023U); + return std::min(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(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(std::max(0, std::min(static_cast(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(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(m_volume); + doc["loop"] = m_loopEnabled; + String payload; + serializeJson(doc, payload); + m_ctx.fire(EVENT_TOPIC, &payload); +} diff --git a/examples/multimatrix/MultiMatrixService.h b/examples/multimatrix/MultiMatrixService.h new file mode 100644 index 0000000..01bebca --- /dev/null +++ b/examples/multimatrix/MultiMatrixService.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include +#include +#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 m_serial; + DFRobotDFPlayerMini m_player; + uint8_t m_potentiometerPin; + uint8_t m_volume; + bool m_playerReady; + bool m_loopEnabled; +}; diff --git a/examples/multimatrix/README.md b/examples/multimatrix/README.md new file mode 100644 index 0000000..6a00f40 --- /dev/null +++ b/examples/multimatrix/README.md @@ -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 +``` diff --git a/examples/multimatrix/data/public/index.html b/examples/multimatrix/data/public/index.html new file mode 100644 index 0000000..baf25c5 --- /dev/null +++ b/examples/multimatrix/data/public/index.html @@ -0,0 +1,508 @@ + + + + + + Multi-Matrix Audio Player + + + +
+
+

Multi-Matrix Audio

+
+ + Connecting... +
+
+ +
+ + + +
+ +
+ + +
+ +
+
+ 🔊 Volume + 15 +
+ +
+ +
+
+ 🔁 Loop Mode +
+
+
+
+
+
+ + + + diff --git a/examples/multimatrix/main.cpp b/examples/multimatrix/main.cpp new file mode 100644 index 0000000..fdb7482 --- /dev/null +++ b/examples/multimatrix/main.cpp @@ -0,0 +1,54 @@ +#include +#include +#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 pixelController; +std::shared_ptr 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(spore.getContext(), config); + pixelController->begin(); + + audioService = std::make_shared(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(); +} diff --git a/examples/neopattern/NeoPatternService.cpp b/examples/neopattern/NeoPatternService.cpp index 2d17d0c..aa32fd9 100644 --- a/examples/neopattern/NeoPatternService.cpp +++ b/examples/neopattern/NeoPatternService.cpp @@ -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{}); // Patterns list endpoint - api.addEndpoint("/api/neopattern/patterns", HTTP_GET, + api.registerEndpoint("/api/neopattern/patterns", HTTP_GET, [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, std::vector{}); // Control endpoint - api.addEndpoint("/api/neopattern", HTTP_POST, + api.registerEndpoint("/api/neopattern", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ 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{}); } @@ -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"); diff --git a/examples/neopattern/NeoPatternService.h b/examples/neopattern/NeoPatternService.h index b4e1a1b..2dca54e 100644 --- a/examples/neopattern/NeoPatternService.h +++ b/examples/neopattern/NeoPatternService.h @@ -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(); diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp index b942b11..6932562 100644 --- a/examples/neopattern/main.cpp +++ b/examples/neopattern/main.cpp @@ -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(); diff --git a/examples/relay/README.md b/examples/relay/README.md index 6fc8ace..56cd2b1 100644 --- a/examples/relay/README.md +++ b/examples/relay/README.md @@ -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(); } diff --git a/examples/relay/RelayService.cpp b/examples/relay/RelayService.cpp index 7d5890c..1c0605f 100644 --- a/examples/relay/RelayService.cpp +++ b/examples/relay/RelayService.cpp @@ -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{}); - api.addEndpoint("/api/relay", HTTP_POST, + api.registerEndpoint("/api/relay", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ 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")); }); diff --git a/examples/relay/RelayService.h b/examples/relay/RelayService.h index 4b62848..7f5fcba 100644 --- a/examples/relay/RelayService.h +++ b/examples/relay/RelayService.h @@ -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; diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 27f3a75..5c00bd5 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -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(); diff --git a/include/spore/Service.h b/include/spore/Service.h index 735f592..6911403 100644 --- a/include/spore/Service.h +++ b/include/spore/Service.h @@ -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; }; diff --git a/include/spore/Spore.h b/include/spore/Spore.h index ea0f89e..55a732f 100644 --- a/include/spore/Spore.h +++ b/include/spore/Spore.h @@ -25,8 +25,8 @@ public: void loop(); // Service management - void addService(std::shared_ptr service); - void addService(Service* service); + void registerService(std::shared_ptr service); + void registerService(Service* service); // Access to core components NodeContext& getContext() { return ctx; } diff --git a/include/spore/core/ApiServer.h b/include/spore/core/ApiServer.h index dcf2eb0..f54c78c 100644 --- a/include/spore/core/ApiServer.h +++ b/include/spore/core/ApiServer.h @@ -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 requestHandler); - void addEndpoint(const String& uri, int method, std::function requestHandler, + void registerService(Service& service); + void registerEndpoint(const String& uri, int method, std::function requestHandler); + void registerEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler); - void addEndpoint(const String& uri, int method, std::function requestHandler, + void registerEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params); - void addEndpoint(const String& uri, int method, std::function requestHandler, + void registerEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params); diff --git a/include/spore/core/NodeContext.h b/include/spore/core/NodeContext.h index 822d6aa..49dc455 100644 --- a/include/spore/core/NodeContext.h +++ b/include/spore/core/NodeContext.h @@ -19,7 +19,7 @@ public: IPAddress localIP; NodeInfo self; std::map* memberList; - Config config; + ::Config config; using EventCallback = std::function; std::map> eventRegistry; diff --git a/include/spore/services/ClusterService.h b/include/spore/services/ClusterService.h index 21b8a9e..249dd1e 100644 --- a/include/spore/services/ClusterService.h +++ b/include/spore/services/ClusterService.h @@ -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: diff --git a/include/spore/services/MonitoringService.h b/include/spore/services/MonitoringService.h index 9da968f..8c2901a 100644 --- a/include/spore/services/MonitoringService.h +++ b/include/spore/services/MonitoringService.h @@ -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 diff --git a/include/spore/services/NetworkService.h b/include/spore/services/NetworkService.h index 4c8973b..e5a2af1 100644 --- a/include/spore/services/NetworkService.h +++ b/include/spore/services/NetworkService.h @@ -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: diff --git a/include/spore/services/NodeService.h b/include/spore/services/NodeService.h index daae61c..7829d6e 100644 --- a/include/spore/services/NodeService.h +++ b/include/spore/services/NodeService.h @@ -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: diff --git a/include/spore/services/StaticFileService.h b/include/spore/services/StaticFileService.h index ba77128..882c14c 100644 --- a/include/spore/services/StaticFileService.h +++ b/include/spore/services/StaticFileService.h @@ -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: diff --git a/include/spore/services/TaskService.h b/include/spore/services/TaskService.h index d5b6edb..9196e71 100644 --- a/include/spore/services/TaskService.h +++ b/include/spore/services/TaskService.h @@ -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: diff --git a/platformio.ini b/platformio.ini index 6fa4b77..fba0ac4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 = + + + + +[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 = + + + + + + + + + + + + + + + + diff --git a/src/spore/Spore.cpp b/src/spore/Spore.cpp index 85618d9..9fbfed1 100644 --- a/src/spore/Spore.cpp +++ b/src/spore/Spore.cpp @@ -85,7 +85,7 @@ void Spore::loop() { yield(); } -void Spore::addService(std::shared_ptr service) { +void Spore::registerService(std::shared_ptr service) { if (!service) { LOG_WARN("Spore", "Attempted to add null service"); return; @@ -95,21 +95,22 @@ void Spore::addService(std::shared_ptr 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*){})); + registerService(std::shared_ptr(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"); } } diff --git a/src/spore/core/ApiServer.cpp b/src/spore/core/ApiServer.cpp index ae95a4e..58148de 100644 --- a/src/spore/core/ApiServer.cpp +++ b/src/spore/core/ApiServer.cpp @@ -31,7 +31,7 @@ void ApiServer::registerEndpoint(const String& uri, int method, } } -void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler) { +void ApiServer::registerEndpoint(const String& uri, int method, std::function 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 requestHandler, +void ApiServer::registerEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler) { // Get current service name if available String serviceName = "unknown"; @@ -53,7 +53,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, +void ApiServer::registerEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params) { // Get current service name if available String serviceName = "unknown"; @@ -64,7 +64,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, +void ApiServer::registerEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params) { // Get current service name if available @@ -76,7 +76,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function{}); // 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(); diff --git a/src/spore/services/MonitoringService.cpp b/src/spore/services/MonitoringService.cpp index a611456..53a62c2 100644 --- a/src/spore/services/MonitoringService.cpp +++ b/src/spore/services/MonitoringService.cpp @@ -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{}); } +void MonitoringService::registerTasks(TaskManager& taskManager) { + // MonitoringService doesn't register any tasks itself +} + MonitoringService::SystemResources MonitoringService::getSystemResources() const { SystemResources resources; diff --git a/src/spore/services/NetworkService.cpp b/src/spore/services/NetworkService.cpp index 1009f4f..333abc1 100644 --- a/src/spore/services/NetworkService.cpp +++ b/src/spore/services/NetworkService.cpp @@ -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{}); - api.addEndpoint("/api/network/wifi/scan", HTTP_GET, + api.registerEndpoint("/api/network/wifi/scan", HTTP_GET, [this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); }, std::vector{}); // 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{}); - api.addEndpoint("/api/network/wifi/config", HTTP_POST, + api.registerEndpoint("/api/network/wifi/config", HTTP_POST, [this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); }, std::vector{ 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(); diff --git a/src/spore/services/NodeService.cpp b/src/spore/services/NodeService.cpp index ffbebc4..6af2650 100644 --- a/src/spore/services/NodeService.cpp +++ b/src/spore/services/NodeService.cpp @@ -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{}); // 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{}); // Endpoints endpoint - api.addEndpoint("/api/node/endpoints", HTTP_GET, + api.registerEndpoint("/api/node/endpoints", HTTP_GET, [this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); }, std::vector{}); // 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(); diff --git a/src/spore/services/StaticFileService.cpp b/src/spore/services/StaticFileService.cpp index dbb5298..5011b16 100644 --- a/src/spore/services/StaticFileService.cpp +++ b/src/spore/services/StaticFileService.cpp @@ -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 +} + diff --git a/src/spore/services/TaskService.cpp b/src/spore/services/TaskService.cpp index c6db68e..55b8ba1 100644 --- a/src/spore/services/TaskService.cpp +++ b/src/spore/services/TaskService.cpp @@ -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{}); - api.addEndpoint("/api/tasks/control", HTTP_POST, + api.registerEndpoint("/api/tasks/control", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ 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); diff --git a/test/pixelstream/snek/server.js b/test/pixelstream/snek/server.js index fe17b5e..32a2aab 100644 --- a/test/pixelstream/snek/server.js +++ b/test/pixelstream/snek/server.js @@ -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; diff --git a/test/pixelstream/tetris/server.js b/test/pixelstream/tetris/server.js index 6b2526f..f609481 100644 --- a/test/pixelstream/tetris/server.js +++ b/test/pixelstream/tetris/server.js @@ -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;