feat: configurable PixelStream

This commit is contained in:
2025-10-28 21:05:02 +01:00
parent ad879bfe7b
commit 633957c95c
3 changed files with 255 additions and 10 deletions

View File

@@ -0,0 +1,202 @@
#include "PixelStreamService.h"
#include "spore/util/Logging.h"
#include <Adafruit_NeoPixel.h>
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>{
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<ParamSpec>{});
}
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<uint8_t>()) config.pin = doc["pin"].as<uint8_t>();
if (doc["pixel_count"].is<uint16_t>()) config.pixelCount = doc["pixel_count"].as<uint16_t>();
if (doc["brightness"].is<uint8_t>()) config.brightness = doc["brightness"].as<uint8_t>();
if (doc["matrix_width"].is<uint16_t>()) config.matrixWidth = doc["matrix_width"].as<uint16_t>();
if (doc["matrix_serpentine"].is<bool>()) config.matrixSerpentine = doc["matrix_serpentine"].as<bool>();
if (doc["pixel_type"].is<uint8_t>()) config.pixelType = static_cast<neoPixelType>(doc["pixel_type"].as<uint8_t>());
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<uint8_t>(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<uint8_t>(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<uint16_t>(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<uint8_t>(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<uint16_t>(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<neoPixelType>(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<uint8_t>(config.pixelType);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/NodeContext.h"
#include "PixelStreamController.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#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"; }
};

View File

@@ -2,7 +2,11 @@
#include "spore/Spore.h"
#include "spore/util/Logging.h"
#include "PixelStreamController.h"
#include "PixelStreamService.h"
#include <Adafruit_NeoPixel.h>
// 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<uint8_t>(PIXEL_PIN),
static_cast<uint16_t>(PIXEL_COUNT),
static_cast<uint8_t>(PIXEL_BRIGHTNESS),
static_cast<uint16_t>(PIXEL_MATRIX_WIDTH),
static_cast<bool>(PIXEL_MATRIX_SERPENTINE),
static_cast<neoPixelType>(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();
}