diff --git a/examples/pixelstream/PixelStreamService.cpp b/examples/pixelstream/PixelStreamService.cpp new file mode 100644 index 0000000..4c8f306 --- /dev/null +++ b/examples/pixelstream/PixelStreamService.cpp @@ -0,0 +1,202 @@ +#include "PixelStreamService.h" +#include "spore/util/Logging.h" +#include + +PixelStreamService::PixelStreamService(NodeContext& ctx, ApiServer& apiServer, PixelStreamController* controller) + : ctx(ctx), apiServer(apiServer), controller(controller) {} + +void PixelStreamService::registerEndpoints(ApiServer& api) { + // Config endpoint for setting pixelstream configuration + api.registerEndpoint("/api/pixelstream/config", HTTP_PUT, + [this](AsyncWebServerRequest* request) { handleConfigRequest(request); }, + std::vector{ + ParamSpec{String("pin"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("pixel_count"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("matrix_width"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("matrix_serpentine"), false, String("body"), String("boolean"), {}, String("")}, + ParamSpec{String("pixel_type"), false, String("body"), String("number"), {}, String("")} + }); + + // Config endpoint for getting pixelstream configuration + api.registerEndpoint("/api/pixelstream/config", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetConfigRequest(request); }, + std::vector{}); +} + +void PixelStreamService::registerTasks(TaskManager& taskManager) { + // PixelStreamService doesn't register any tasks itself +} + +PixelStreamConfig PixelStreamService::loadConfig() { + // Initialize with proper defaults + PixelStreamConfig config; + config.pin = 2; + config.pixelCount = 16; + config.brightness = 80; + config.matrixWidth = 16; + config.matrixSerpentine = false; + config.pixelType = NEO_GRB + NEO_KHZ800; + + if (!LittleFS.begin()) { + LOG_WARN("PixelStream", "Failed to initialize LittleFS, using defaults"); + return config; + } + + if (!LittleFS.exists(CONFIG_FILE())) { + LOG_INFO("PixelStream", "No pixelstream config file found, using defaults"); + // Save defaults + saveConfig(config); + return config; + } + + File file = LittleFS.open(CONFIG_FILE(), "r"); + if (!file) { + LOG_ERROR("PixelStream", "Failed to open config file for reading"); + return config; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + LOG_ERROR("PixelStream", "Failed to parse config file: " + String(error.c_str())); + return config; + } + + if (doc["pin"].is()) config.pin = doc["pin"].as(); + if (doc["pixel_count"].is()) config.pixelCount = doc["pixel_count"].as(); + if (doc["brightness"].is()) config.brightness = doc["brightness"].as(); + if (doc["matrix_width"].is()) config.matrixWidth = doc["matrix_width"].as(); + if (doc["matrix_serpentine"].is()) config.matrixSerpentine = doc["matrix_serpentine"].as(); + if (doc["pixel_type"].is()) config.pixelType = static_cast(doc["pixel_type"].as()); + + LOG_INFO("PixelStream", "Configuration loaded from " + String(CONFIG_FILE())); + return config; +} + +bool PixelStreamService::saveConfig(const PixelStreamConfig& config) { + if (!LittleFS.begin()) { + LOG_ERROR("PixelStream", "LittleFS not initialized, cannot save config"); + return false; + } + + File file = LittleFS.open(CONFIG_FILE(), "w"); + if (!file) { + LOG_ERROR("PixelStream", "Failed to open config file for writing"); + return false; + } + + JsonDocument doc; + doc["pin"] = config.pin; + doc["pixel_count"] = config.pixelCount; + doc["brightness"] = config.brightness; + doc["matrix_width"] = config.matrixWidth; + doc["matrix_serpentine"] = config.matrixSerpentine; + doc["pixel_type"] = static_cast(config.pixelType); + + size_t bytesWritten = serializeJson(doc, file); + file.close(); + + if (bytesWritten > 0) { + LOG_INFO("PixelStream", "Configuration saved to " + String(CONFIG_FILE()) + " (" + String(bytesWritten) + " bytes)"); + return true; + } else { + LOG_ERROR("PixelStream", "Failed to write configuration to file"); + return false; + } +} + +void PixelStreamService::handleConfigRequest(AsyncWebServerRequest* request) { + // Load current config from file + PixelStreamConfig config = loadConfig(); + + bool updated = false; + + // Handle individual form parameters + if (request->hasParam("pin", true)) { + String pinStr = request->getParam("pin", true)->value(); + if (pinStr.length() > 0) { + int pinValue = pinStr.toInt(); + if (pinValue >= 0 && pinValue <= 255) { + config.pin = static_cast(pinValue); + updated = true; + } + } + } + if (request->hasParam("pixel_count", true)) { + String countStr = request->getParam("pixel_count", true)->value(); + if (countStr.length() > 0) { + int countValue = countStr.toInt(); + if (countValue > 0 && countValue <= 65535) { + config.pixelCount = static_cast(countValue); + updated = true; + } + } + } + if (request->hasParam("brightness", true)) { + String brightnessStr = request->getParam("brightness", true)->value(); + if (brightnessStr.length() > 0) { + int brightnessValue = brightnessStr.toInt(); + if (brightnessValue >= 0 && brightnessValue <= 255) { + config.brightness = static_cast(brightnessValue); + updated = true; + } + } + } + if (request->hasParam("matrix_width", true)) { + String widthStr = request->getParam("matrix_width", true)->value(); + if (widthStr.length() > 0) { + int widthValue = widthStr.toInt(); + if (widthValue > 0 && widthValue <= 65535) { + config.matrixWidth = static_cast(widthValue); + updated = true; + } + } + } + if (request->hasParam("matrix_serpentine", true)) { + String serpentineStr = request->getParam("matrix_serpentine", true)->value(); + config.matrixSerpentine = (serpentineStr.equalsIgnoreCase("true") || serpentineStr == "1"); + updated = true; + } + if (request->hasParam("pixel_type", true)) { + String typeStr = request->getParam("pixel_type", true)->value(); + if (typeStr.length() > 0) { + int typeValue = typeStr.toInt(); + config.pixelType = static_cast(typeValue); + updated = true; + } + } + + if (!updated) { + request->send(400, "application/json", "{\"error\":\"No valid configuration fields provided\"}"); + return; + } + + // Save config to file + if (saveConfig(config)) { + LOG_INFO("PixelStreamService", "Configuration updated and saved to pixelstream.json"); + request->send(200, "application/json", "{\"status\":\"success\",\"message\":\"Configuration updated and saved\"}"); + } else { + LOG_ERROR("PixelStreamService", "Failed to save configuration to file"); + request->send(500, "application/json", "{\"error\":\"Failed to save configuration\"}"); + } +} + +void PixelStreamService::handleGetConfigRequest(AsyncWebServerRequest* request) { + PixelStreamConfig config = loadConfig(); + + JsonDocument doc; + doc["pin"] = config.pin; + doc["pixel_count"] = config.pixelCount; + doc["brightness"] = config.brightness; + doc["matrix_width"] = config.matrixWidth; + doc["matrix_serpentine"] = config.matrixSerpentine; + doc["pixel_type"] = static_cast(config.pixelType); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + diff --git a/examples/pixelstream/PixelStreamService.h b/examples/pixelstream/PixelStreamService.h new file mode 100644 index 0000000..e0925ae --- /dev/null +++ b/examples/pixelstream/PixelStreamService.h @@ -0,0 +1,33 @@ +#pragma once +#include "spore/Service.h" +#include "spore/core/NodeContext.h" +#include "PixelStreamController.h" +#include +#include +#include "spore/util/Logging.h" + +// PixelStreamConfig is defined in PixelStreamController.h + +class PixelStreamService : public Service { +public: + PixelStreamService(NodeContext& ctx, ApiServer& apiServer, PixelStreamController* controller); + void registerEndpoints(ApiServer& api) override; + void registerTasks(TaskManager& taskManager) override; + const char* getName() const override { return "PixelStream"; } + + // Config management + PixelStreamConfig loadConfig(); + bool saveConfig(const PixelStreamConfig& config); + void setController(PixelStreamController* ctrl) { controller = ctrl; } + +private: + NodeContext& ctx; + ApiServer& apiServer; + PixelStreamController* controller; + + void handleConfigRequest(AsyncWebServerRequest* request); + void handleGetConfigRequest(AsyncWebServerRequest* request); + + static const char* CONFIG_FILE() { return "/pixelstream.json"; } +}; + diff --git a/examples/pixelstream/main.cpp b/examples/pixelstream/main.cpp index f053938..2b705af 100644 --- a/examples/pixelstream/main.cpp +++ b/examples/pixelstream/main.cpp @@ -2,7 +2,11 @@ #include "spore/Spore.h" #include "spore/util/Logging.h" #include "PixelStreamController.h" +#include "PixelStreamService.h" +#include +// Defaults are now loaded from config.json on LittleFS +// Can still be overridden with preprocessor defines if needed #ifndef PIXEL_PIN #define PIXEL_PIN 2 #endif @@ -34,22 +38,28 @@ Spore spore({ }); PixelStreamController* controller = nullptr; +PixelStreamService* service = nullptr; void setup() { spore.setup(); - PixelStreamConfig config{ - static_cast(PIXEL_PIN), - static_cast(PIXEL_COUNT), - static_cast(PIXEL_BRIGHTNESS), - static_cast(PIXEL_MATRIX_WIDTH), - static_cast(PIXEL_MATRIX_SERPENTINE), - static_cast(PIXEL_TYPE) - }; - + // Create service first (need it to load config) + service = new PixelStreamService(spore.getContext(), spore.getApiServer(), nullptr); + + // Load pixelstream config from LittleFS (pixelstream.json) or use defaults + PixelStreamConfig config = service->loadConfig(); + + // Create controller with loaded config controller = new PixelStreamController(spore.getContext(), config); controller->begin(); - + + // Update service with the actual controller + service->setController(controller); + + // Register service + spore.registerService(service); + + // Start the API server spore.begin(); }