feat: udp stream
This commit is contained in:
127
examples/pixelstream/PixelStreamController.cpp
Normal file
127
examples/pixelstream/PixelStreamController.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#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<String*>(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<std::size_t>(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<uint16_t>(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<std::size_t>(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<std::size_t>(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<uint8_t>((rHi << 4) | rLo);
|
||||
components.green = static_cast<uint8_t>((gHi << 4) | gLo);
|
||||
components.blue = static_cast<uint8_t>((bHi << 4) | bLo);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
42
examples/pixelstream/PixelStreamController.h
Normal file
42
examples/pixelstream/PixelStreamController.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include "spore/core/NodeContext.h"
|
||||
#include "spore/util/Logging.h"
|
||||
|
||||
struct PixelStreamConfig {
|
||||
uint8_t pin;
|
||||
uint16_t pixelCount;
|
||||
uint8_t brightness;
|
||||
uint16_t matrixWidth;
|
||||
bool matrixSerpentine;
|
||||
neoPixelType pixelType;
|
||||
};
|
||||
|
||||
class PixelStreamController {
|
||||
public:
|
||||
PixelStreamController(NodeContext& ctx, const PixelStreamConfig& config);
|
||||
void begin();
|
||||
|
||||
private:
|
||||
struct FrameComponents {
|
||||
uint8_t red;
|
||||
uint8_t green;
|
||||
uint8_t blue;
|
||||
};
|
||||
|
||||
bool tryParsePixel(const String& payload, std::size_t startIndex, FrameComponents& components) const;
|
||||
void handleEvent(void* data);
|
||||
bool applyFrame(const String& payload);
|
||||
uint16_t mapPixelIndex(uint16_t logicalIndex) const;
|
||||
static int hexToNibble(char c);
|
||||
|
||||
NodeContext& ctx;
|
||||
PixelStreamConfig config;
|
||||
Adafruit_NeoPixel pixels;
|
||||
};
|
||||
|
||||
|
||||
33
examples/pixelstream/README.md
Normal file
33
examples/pixelstream/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# PixelStream Example
|
||||
|
||||
This example demonstrates how to consume the `udp/raw` cluster event and drive a NeoPixel strip or matrix directly from streamed RGB data. Frames are provided as hex encoded byte triplets (`RRGGBB` per pixel).
|
||||
|
||||
## Features
|
||||
|
||||
- Subscribes to `udp/raw` via `NodeContext::on`.
|
||||
- Converts incoming frames into pixel colors for strips or matrices.
|
||||
- Supports serpentine (zig-zag) matrix wiring.
|
||||
|
||||
## Payload Format
|
||||
|
||||
Each packet is expected to be `RAW:` followed by `pixelCount * 3 * 2` hexadecimal characters. For example, for 8 pixels:
|
||||
|
||||
```
|
||||
RAW:FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Strip Mode
|
||||
|
||||
Upload the example with `PIXEL_MATRIX_WIDTH` set to 0 (default). Send frames containing `PIXEL_COUNT * 3` bytes as hex.
|
||||
|
||||
### Matrix Mode
|
||||
|
||||
Set `PIXEL_MATRIX_WIDTH` to the number of columns. The controller remaps even/odd rows to support serpentine wiring.
|
||||
|
||||
## Configuration
|
||||
|
||||
Adjust `PIXEL_PIN`, `PIXEL_COUNT`, `PIXEL_BRIGHTNESS`, `PIXEL_MATRIX_WIDTH`, `PIXEL_MATRIX_SERPENTINE`, and `PIXEL_TYPE` through build defines or editing `main.cpp`.
|
||||
|
||||
|
||||
60
examples/pixelstream/main.cpp
Normal file
60
examples/pixelstream/main.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include <Arduino.h>
|
||||
#include "spore/Spore.h"
|
||||
#include "spore/util/Logging.h"
|
||||
#include "PixelStreamController.h"
|
||||
|
||||
#ifndef PIXEL_PIN
|
||||
#define PIXEL_PIN 2
|
||||
#endif
|
||||
|
||||
#ifndef PIXEL_COUNT
|
||||
#define PIXEL_COUNT 64
|
||||
#endif
|
||||
|
||||
#ifndef PIXEL_BRIGHTNESS
|
||||
#define PIXEL_BRIGHTNESS 80
|
||||
#endif
|
||||
|
||||
#ifndef PIXEL_MATRIX_WIDTH
|
||||
#define PIXEL_MATRIX_WIDTH 0
|
||||
#endif
|
||||
|
||||
#ifndef PIXEL_MATRIX_SERPENTINE
|
||||
#define PIXEL_MATRIX_SERPENTINE 1
|
||||
#endif
|
||||
|
||||
#ifndef PIXEL_TYPE
|
||||
#define PIXEL_TYPE NEO_GRB + NEO_KHZ800
|
||||
#endif
|
||||
|
||||
Spore spore({
|
||||
{"app", "pixelstream"},
|
||||
{"role", "led"},
|
||||
{"pixels", String(PIXEL_COUNT)}
|
||||
});
|
||||
|
||||
PixelStreamController* controller = 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)
|
||||
};
|
||||
|
||||
controller = new PixelStreamController(spore.getContext(), config);
|
||||
controller->begin();
|
||||
|
||||
spore.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
spore.loop();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user