mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:07:33 +00:00
[pixoo] Add Divoom Pixoo display component
Add a display platform for the Divoom Pixoo 64 LED matrix over SPI, with a light platform to control panel brightness.
This commit is contained in:
@@ -385,6 +385,7 @@ esphome/components/pcm5122/* @remcom
|
||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
esphome/components/pixoo/* @jesserockz
|
||||
esphome/components/pm1006/* @habbie
|
||||
esphome/components/pm2005/* @andrewjswan
|
||||
esphome/components/pmsa003i/* @sjtrny
|
||||
|
||||
1
esphome/components/pixoo/__init__.py
Normal file
1
esphome/components/pixoo/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
43
esphome/components/pixoo/display.py
Normal file
43
esphome/components/pixoo/display.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_MODEL
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
|
||||
CONF_PIXOO_ID = "pixoo_id"
|
||||
|
||||
pixoo_ns = cg.esphome_ns.namespace("pixoo")
|
||||
Pixoo = pixoo_ns.class_("Pixoo", cg.PollingComponent, display.Display, spi.SPIDevice)
|
||||
PixooModel = pixoo_ns.enum("PixooModel")
|
||||
|
||||
# Only the 64x64 panel is hardware-verified. Smaller Pixoo panels are assumed to share the
|
||||
# same protocol; add them here once confirmed.
|
||||
MODELS = {
|
||||
"64X64": PixooModel.PIXOO_64,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Pixoo),
|
||||
cv.Optional(CONF_MODEL, default="64X64"): cv.enum(MODELS, upper=True),
|
||||
}
|
||||
).extend(spi.spi_device_schema(cs_pin_required=True, default_data_rate=8e6))
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
"pixoo", require_miso=False, require_mosi=True
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_MODEL])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
if (lambda_config := config.get(CONF_LAMBDA)) is not None:
|
||||
lambda_ = await cg.process_lambda(
|
||||
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
24
esphome/components/pixoo/light/__init__.py
Normal file
24
esphome/components/pixoo/light/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import light
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..display import CONF_PIXOO_ID, Pixoo, pixoo_ns
|
||||
|
||||
PixooLight = pixoo_ns.class_("PixooLight", light.LightOutput)
|
||||
|
||||
CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(PixooLight),
|
||||
cv.GenerateID(CONF_PIXOO_ID): cv.use_id(Pixoo),
|
||||
# The LED board applies its own gamma, so default to no gamma correction here.
|
||||
cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await light.register_light(var, config)
|
||||
await cg.register_parented(var, config[CONF_PIXOO_ID])
|
||||
26
esphome/components/pixoo/light/pixoo_light.h
Normal file
26
esphome/components/pixoo/light/pixoo_light.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
#include "esphome/components/pixoo/pixoo.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::pixoo {
|
||||
|
||||
// Brightness-only light that drives the Pixoo panel's LIGHT command.
|
||||
class PixooLight : public light::LightOutput, public Parented<Pixoo> {
|
||||
public:
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
|
||||
void write_state(light::LightState *state) override {
|
||||
float brightness;
|
||||
state->current_values_as_brightness(&brightness);
|
||||
this->parent_->set_panel_brightness(brightness);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::pixoo
|
||||
183
esphome/components/pixoo/pixoo.cpp
Normal file
183
esphome/components/pixoo/pixoo.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "pixoo.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::pixoo {
|
||||
|
||||
static const char *const TAG = "pixoo";
|
||||
|
||||
float Pixoo::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||
|
||||
size_t Pixoo::build_packet_(uint8_t *buf, uint8_t cmd, const uint8_t *data, uint16_t len) {
|
||||
buf[0] = PACKET_HEAD;
|
||||
buf[1] = static_cast<uint8_t>(len & 0xFF);
|
||||
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
|
||||
buf[3] = cmd;
|
||||
if (data != nullptr && len > 0)
|
||||
std::memcpy(buf + PACKET_HEADER_LEN, data, len);
|
||||
buf[PACKET_HEADER_LEN + len] = PACKET_TAIL;
|
||||
return len + PACKET_STATIC_LEN;
|
||||
}
|
||||
|
||||
void Pixoo::pad_unused_(uint8_t *buf, size_t total) {
|
||||
const uint16_t len = static_cast<uint16_t>(total - PACKET_STATIC_LEN);
|
||||
buf[0] = PACKET_HEAD;
|
||||
buf[1] = static_cast<uint8_t>(len & 0xFF);
|
||||
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
|
||||
buf[3] = CMD_UNUSED;
|
||||
buf[total - 1] = PACKET_TAIL;
|
||||
}
|
||||
|
||||
void Pixoo::setup() {
|
||||
const uint32_t num_pixels = static_cast<uint32_t>(this->model_) * this->model_;
|
||||
this->data_size_ = num_pixels * 3;
|
||||
// The frame is a DATA packet (header + RGB888 + tail) followed by a DMA-chunk-sized UNUSED
|
||||
// packet, so the LED board completes its final DMA block.
|
||||
this->frame_size_ = this->data_size_ + PACKET_STATIC_LEN + DMA_CHUNK;
|
||||
|
||||
if (!this->buffer_.init(this->data_size_)) {
|
||||
this->mark_failed(LOG_STR("Failed to allocate draw buffer"));
|
||||
return;
|
||||
}
|
||||
|
||||
// The frame is shipped in one SPI transfer, so keep it in DMA-capable internal RAM.
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
this->frame_buffer_ = allocator.allocate(this->frame_size_);
|
||||
if (this->frame_buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
|
||||
return;
|
||||
}
|
||||
std::memset(this->frame_buffer_, 0, this->frame_size_);
|
||||
// Pre-build the constant DATA-packet framing; only the RGB888 payload changes per frame.
|
||||
this->frame_buffer_[0] = PACKET_HEAD;
|
||||
this->frame_buffer_[1] = static_cast<uint8_t>(this->data_size_ & 0xFF);
|
||||
this->frame_buffer_[2] = static_cast<uint8_t>((this->data_size_ >> 8) & 0xFF);
|
||||
this->frame_buffer_[3] = CMD_DATA;
|
||||
this->frame_buffer_[PACKET_HEADER_LEN + this->data_size_] = PACKET_TAIL;
|
||||
this->pad_unused_(this->frame_buffer_ + this->data_size_ + PACKET_STATIC_LEN, DMA_CHUNK);
|
||||
|
||||
this->spi_setup();
|
||||
|
||||
this->buffer_.fill(0x00);
|
||||
|
||||
// Set the per-channel LED current. Brightness is controlled separately via the light platform.
|
||||
const uint8_t iout[3] = {DEFAULT_IOUT, DEFAULT_IOUT, DEFAULT_IOUT};
|
||||
this->send_command_(CMD_SET_RGB_IOUT, iout, 3);
|
||||
|
||||
// Frames are pushed synchronously inside update(), so there is no loop() work to do and the
|
||||
// component is idle between updates. Marking it done (LOOP_DONE) lets LVGL's
|
||||
// update_when_display_idle option treat the panel as idle and drive frames on demand.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void Pixoo::send_command_(uint8_t cmd, const uint8_t *data, uint16_t len) {
|
||||
std::memset(this->cmd_buffer_, 0, DMA_CHUNK);
|
||||
const size_t used = build_packet_(this->cmd_buffer_, cmd, data, len);
|
||||
if (DMA_CHUNK - used > PACKET_STATIC_LEN)
|
||||
pad_unused_(this->cmd_buffer_ + used, DMA_CHUNK - used);
|
||||
this->enable();
|
||||
this->write_array(this->cmd_buffer_, DMA_CHUNK);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void Pixoo::set_panel_brightness(float brightness) {
|
||||
const uint8_t pct = static_cast<uint8_t>(clamp(brightness, 0.0f, 1.0f) * 100.0f + 0.5f);
|
||||
this->send_command_(CMD_LIGHT, &pct, 1);
|
||||
}
|
||||
|
||||
void Pixoo::update() {
|
||||
this->do_update_();
|
||||
for (size_t i = 0; i < this->data_size_; i++)
|
||||
this->frame_buffer_[PACKET_HEADER_LEN + i] = this->buffer_[i];
|
||||
this->enable();
|
||||
this->write_array(this->frame_buffer_, this->frame_size_);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void Pixoo::set_pixel_(uint32_t index, Color color) {
|
||||
const size_t off = static_cast<size_t>(index) * 3;
|
||||
this->buffer_[off] = color.r;
|
||||
this->buffer_[off + 1] = color.g;
|
||||
this->buffer_[off + 2] = color.b;
|
||||
}
|
||||
|
||||
void HOT Pixoo::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
const int side = static_cast<int>(this->model_);
|
||||
switch (this->rotation_) {
|
||||
case DISPLAY_ROTATION_0_DEGREES:
|
||||
break;
|
||||
case DISPLAY_ROTATION_90_DEGREES:
|
||||
std::swap(x, y);
|
||||
x = side - x - 1;
|
||||
break;
|
||||
case DISPLAY_ROTATION_180_DEGREES:
|
||||
x = side - x - 1;
|
||||
y = side - y - 1;
|
||||
break;
|
||||
case DISPLAY_ROTATION_270_DEGREES:
|
||||
std::swap(x, y);
|
||||
y = side - y - 1;
|
||||
break;
|
||||
}
|
||||
if (x < 0 || x >= side || y < 0 || y >= side)
|
||||
return;
|
||||
this->set_pixel_(static_cast<uint32_t>(y) * side + x, color);
|
||||
}
|
||||
|
||||
void Pixoo::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
// Fast path for the common LVGL/image blit: RGB565, RGB order, no rotation, no active clipping.
|
||||
// Anything else defers to the base implementation, which decodes per pixel and routes through
|
||||
// draw_pixel_at() so rotation, clipping and other color formats stay correct.
|
||||
if (bitness != COLOR_BITNESS_565 || order != COLOR_ORDER_RGB || this->rotation_ != DISPLAY_ROTATION_0_DEGREES ||
|
||||
this->is_clipping()) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
const int side = static_cast<int>(this->model_);
|
||||
const size_t line_stride = static_cast<size_t>(x_offset) + w + x_pad;
|
||||
for (int y = 0; y != h; y++) {
|
||||
const int dst_y = y_start + y;
|
||||
if (dst_y < 0 || dst_y >= side)
|
||||
continue;
|
||||
size_t source_idx = (static_cast<size_t>(y_offset) + y) * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
const int dst_x = x_start + x;
|
||||
if (dst_x < 0 || dst_x >= side)
|
||||
continue;
|
||||
const size_t byte_idx = source_idx * 2;
|
||||
const uint16_t rgb565 =
|
||||
big_endian ? (ptr[byte_idx] << 8) | ptr[byte_idx + 1] : ptr[byte_idx] | (ptr[byte_idx + 1] << 8);
|
||||
const uint8_t r5 = (rgb565 >> 11) & 0x1F;
|
||||
const uint8_t g6 = (rgb565 >> 5) & 0x3F;
|
||||
const uint8_t b5 = rgb565 & 0x1F;
|
||||
this->set_pixel_(static_cast<uint32_t>(dst_y) * side + dst_x,
|
||||
Color((r5 << 3) | (r5 >> 2), (g6 << 2) | (g6 >> 4), (b5 << 3) | (b5 >> 2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Pixoo::fill(Color color) {
|
||||
if (this->is_clipping()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < this->data_size_; i += 3) {
|
||||
this->buffer_[i] = color.r;
|
||||
this->buffer_[i + 1] = color.g;
|
||||
this->buffer_[i + 2] = color.b;
|
||||
}
|
||||
}
|
||||
|
||||
void Pixoo::dump_config() {
|
||||
LOG_DISPLAY("", "Divoom Pixoo", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: %ux%u", (unsigned) this->model_, (unsigned) this->model_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::pixoo
|
||||
79
esphome/components/pixoo/pixoo.h
Normal file
79
esphome/components/pixoo/pixoo.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/split_buffer/split_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::pixoo {
|
||||
|
||||
using namespace display;
|
||||
|
||||
// The Pixoo's main board (where ESPHome runs) talks to a separate LED-driver board (a GD32/AT32
|
||||
// MCU) over SPI using Divoom's packet protocol:
|
||||
// 0xAA, len_lo, len_hi, cmd, <data...>, 0xBB
|
||||
// The image is sent as a DATA (0x00) packet carrying width*height*3 bytes of RGB888; brightness is
|
||||
// a separate LIGHT (0x01) command; the LED current is set once via SET_RGB_IOUT (0x22). Command
|
||||
// packets are padded out to the LED board's 240-byte DMA chunk with an UNUSED (0x21) packet.
|
||||
// The model selects the (square) panel side length.
|
||||
enum PixooModel : uint8_t {
|
||||
PIXOO_64 = 64,
|
||||
};
|
||||
|
||||
class Pixoo : public Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_8MHZ> {
|
||||
public:
|
||||
explicit Pixoo(PixooModel model) : model_(model) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
// Brightness is controlled exclusively via the light platform: send a LIGHT command to the LED
|
||||
// board (brightness 0..1 -> 0..100%).
|
||||
void set_panel_brightness(float brightness);
|
||||
|
||||
DisplayType get_display_type() override { return DISPLAY_TYPE_COLOR; }
|
||||
|
||||
void fill(Color color) override;
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
protected:
|
||||
int get_width_internal() override { return static_cast<int>(this->model_); }
|
||||
int get_height_internal() override { return static_cast<int>(this->model_); }
|
||||
|
||||
void set_pixel_(uint32_t index, Color color);
|
||||
void send_command_(uint8_t cmd, const uint8_t *data, uint16_t len);
|
||||
// Pack a `0xAA len cmd data 0xBB` packet into buf; returns the packet length.
|
||||
static size_t build_packet_(uint8_t *buf, uint8_t cmd, const uint8_t *data, uint16_t len);
|
||||
// Fill `total` bytes at buf with a single UNUSED padding packet.
|
||||
static void pad_unused_(uint8_t *buf, size_t total);
|
||||
|
||||
// Divoom LED-board packet protocol constants.
|
||||
static constexpr uint8_t PACKET_HEAD = 0xAA;
|
||||
static constexpr uint8_t PACKET_TAIL = 0xBB;
|
||||
static constexpr uint8_t CMD_DATA = 0x00;
|
||||
static constexpr uint8_t CMD_LIGHT = 0x01;
|
||||
static constexpr uint8_t CMD_UNUSED = 0x21;
|
||||
static constexpr uint8_t CMD_SET_RGB_IOUT = 0x22;
|
||||
static constexpr size_t PACKET_HEADER_LEN = 4; // head + len(2) + cmd
|
||||
static constexpr size_t PACKET_STATIC_LEN = 5; // header + tail
|
||||
static constexpr size_t DMA_CHUNK = 240; // LED board DMA granularity
|
||||
static constexpr uint8_t DEFAULT_IOUT = 75; // per-channel LED current / white balance default
|
||||
|
||||
PixooModel model_;
|
||||
|
||||
size_t data_size_{0}; // RGB888 image bytes: model^2 * 3
|
||||
size_t frame_size_{0}; // full SPI frame: DATA packet + trailing UNUSED packet
|
||||
|
||||
split_buffer::SplitBuffer buffer_{};
|
||||
uint8_t *frame_buffer_{nullptr};
|
||||
uint8_t cmd_buffer_[DMA_CHUNK]{};
|
||||
};
|
||||
|
||||
} // namespace esphome::pixoo
|
||||
26
tests/components/pixoo/common.yaml
Normal file
26
tests/components/pixoo/common.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
display:
|
||||
- platform: pixoo
|
||||
id: pixoo_display
|
||||
model: 64x64
|
||||
cs_pin: GPIO5
|
||||
data_rate: 10MHz
|
||||
update_interval: 1s
|
||||
lambda: |-
|
||||
it.fill(Color(0, 0, 0));
|
||||
it.filled_rectangle(0, 0, 16, 16, Color(255, 0, 0));
|
||||
it.line(0, 0, 63, 63, Color(0, 255, 0));
|
||||
|
||||
- platform: pixoo
|
||||
id: pixoo_display_pages
|
||||
model: 64x64
|
||||
cs_pin: GPIO21
|
||||
rotation: 90
|
||||
pages:
|
||||
- id: pixoo_page
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height(), Color(0, 0, 255));
|
||||
|
||||
light:
|
||||
- platform: pixoo
|
||||
pixoo_id: pixoo_display
|
||||
name: Pixoo Brightness
|
||||
4
tests/components/pixoo/test.esp32-idf.yaml
Normal file
4
tests/components/pixoo/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user