#include "PixelStreamController.h" namespace { constexpr int COMPONENTS_PER_PIXEL = 3; } PixelStreamController::PixelStreamController(NodeContext& ctxRef, const PixelStreamConfig& cfg) : ctx(ctxRef), config(cfg), pixels(cfg.pixelCount, cfg.pin, cfg.pixelType) { } void PixelStreamController::begin() { pixels.begin(); pixels.setBrightness(config.brightness); // Default all pixels to green so we can verify hardware before streaming frames for (uint16_t i = 0; i < config.pixelCount; ++i) { pixels.setPixelColor(i, pixels.Color(0, 255, 0)); } pixels.show(); ctx.on("udp/raw", [this](void* data) { this->handleEvent(data); }); LOG_INFO("PixelStream", String("PixelStreamController ready on pin ") + String(config.pin) + " with " + String(config.pixelCount) + " pixels"); } void PixelStreamController::handleEvent(void* data) { if (data == nullptr) { return; } String* payload = static_cast(data); if (!payload) { return; } if (!applyFrame(*payload)) { LOG_WARN("PixelStream", String("Ignoring RAW payload with invalid length (") + String(payload->length()) + ")"); } } bool PixelStreamController::applyFrame(const String& payload) { static constexpr std::size_t frameWidth = COMPONENTS_PER_PIXEL * 2; const std::size_t payloadLength = static_cast(payload.length()); if (payloadLength == 0 || (payloadLength % frameWidth) != 0) { LOG_WARN("PixelStream", String("Payload size ") + String(payloadLength) + " is not a multiple of " + String(frameWidth)); return false; } const uint16_t framesProvided = static_cast(payloadLength / frameWidth); const uint16_t pixelsToUpdate = std::min(config.pixelCount, framesProvided); for (uint16_t index = 0; index < pixelsToUpdate; ++index) { const std::size_t base = static_cast(index) * frameWidth; FrameComponents components{}; if (!tryParsePixel(payload, base, components)) { LOG_WARN("PixelStream", String("Invalid hex data at pixel index ") + String(index)); return false; } const uint16_t hardwareIndex = mapPixelIndex(index); pixels.setPixelColor(hardwareIndex, pixels.Color(components.red, components.green, components.blue)); } // Clear any remaining pixels so stale data is removed when fewer frames are provided for (uint16_t index = pixelsToUpdate; index < config.pixelCount; ++index) { const uint16_t hardwareIndex = mapPixelIndex(index); pixels.setPixelColor(hardwareIndex, 0); } pixels.show(); return true; } uint16_t PixelStreamController::mapPixelIndex(uint16_t logicalIndex) const { if (config.matrixWidth == 0) { return logicalIndex; } const uint16_t row = logicalIndex / config.matrixWidth; const uint16_t col = logicalIndex % config.matrixWidth; if (!config.matrixSerpentine || (row % 2 == 0)) { return row * config.matrixWidth + col; } const uint16_t reversedCol = (config.matrixWidth - 1) - col; return row * config.matrixWidth + reversedCol; } int PixelStreamController::hexToNibble(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'a' && c <= 'f') { return 10 + c - 'a'; } if (c >= 'A' && c <= 'F') { return 10 + c - 'A'; } return -1; } bool PixelStreamController::tryParsePixel(const String& payload, std::size_t startIndex, FrameComponents& components) const { static constexpr std::size_t frameWidth = COMPONENTS_PER_PIXEL * 2; if (startIndex + frameWidth > static_cast(payload.length())) { return false; } const int rHi = hexToNibble(payload[startIndex]); const int rLo = hexToNibble(payload[startIndex + 1]); const int gHi = hexToNibble(payload[startIndex + 2]); const int gLo = hexToNibble(payload[startIndex + 3]); const int bHi = hexToNibble(payload[startIndex + 4]); const int bLo = hexToNibble(payload[startIndex + 5]); if (rHi < 0 || rLo < 0 || gHi < 0 || gLo < 0 || bHi < 0 || bLo < 0) { return false; } components.red = static_cast((rHi << 4) | rLo); components.green = static_cast((gHi << 4) | gLo); components.blue = static_cast((bHi << 4) | bLo); return true; }