feat: configurable PixelStream
This commit is contained in:
202
examples/pixelstream/PixelStreamService.cpp
Normal file
202
examples/pixelstream/PixelStreamService.cpp
Normal 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);
|
||||
}
|
||||
|
||||
33
examples/pixelstream/PixelStreamService.h
Normal file
33
examples/pixelstream/PixelStreamService.h
Normal 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"; }
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user