From 449e478becae951c293991e713c70ab7123db93d Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Sun, 4 Jan 2026 12:50:10 -0800 Subject: [PATCH] [hub75] Bump esp-hub75 version to 0.2.2 (#12674) --- esphome/components/hub75/display.py | 33 +++++++++++++--- esphome/components/hub75/hub75.cpp | 19 +++++++-- esphome/components/hub75/hub75_component.h | 4 +- esphome/idf_component.yml | 2 +- .../hub75/test.esp32-s3-idf-rotate.yaml | 39 +++++++++++++++++++ 5 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 tests/components/hub75/test.esp32-s3-idf-rotate.yaml diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 7736319330..40202e52ca 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_OE_PIN, + CONF_ROTATION, CONF_UPDATE_INTERVAL, ) from esphome.core import ID @@ -134,6 +135,14 @@ CLOCK_SPEEDS = { "20MHZ": Hub75ClockSpeed.HZ_20M, } +Hub75Rotation = cg.global_ns.enum("Hub75Rotation", is_class=True) +ROTATIONS = { + 0: Hub75Rotation.ROTATE_0, + 90: Hub75Rotation.ROTATE_90, + 180: Hub75Rotation.ROTATE_180, + 270: Hub75Rotation.ROTATE_270, +} + HUB75Display = hub75_ns.class_("HUB75Display", cg.PollingComponent, display.Display) Hub75Config = cg.global_ns.struct("Hub75Config") Hub75Pins = cg.global_ns.struct("Hub75Pins") @@ -361,6 +370,8 @@ CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(HUB75Display), + # Override rotation - store Hub75Rotation directly (driver handles rotation) + cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS, int=True), # Board preset (optional - provides default pin mappings) cv.Optional(CONF_BOARD): cv.one_of(*BOARDS.keys(), lower=True), # Panel dimensions @@ -378,7 +389,7 @@ CONFIG_SCHEMA = cv.All( # Display configuration cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, cv.Optional(CONF_BRIGHTNESS): cv.int_range(min=0, max=255), - cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=6, max=12), + cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=4, max=12), cv.Optional(CONF_GAMMA_CORRECT): cv.enum( {"LINEAR": 0, "CIE1931": 1, "GAMMA_2_2": 2}, upper=True ), @@ -490,10 +501,11 @@ def _build_config_struct( Fields must be added in declaration order (see hub75_types.h) to satisfy C++ designated initializer requirements. The order is: 1. fields_before_pins (panel_width through layout) - 2. pins - 3. output_clock_speed - 4. min_refresh_rate - 5. fields_after_min_refresh (latch_blanking through brightness) + 2. rotation + 3. pins + 4. output_clock_speed + 5. min_refresh_rate + 6. fields_after_min_refresh (latch_blanking through brightness) """ fields_before_pins = [ (CONF_PANEL_WIDTH, "panel_width"), @@ -516,6 +528,10 @@ def _build_config_struct( _append_config_fields(config, fields_before_pins, config_fields) + # Rotation - config already contains Hub75Rotation enum from cv.enum + if CONF_ROTATION in config: + config_fields.append(("rotation", config[CONF_ROTATION])) + config_fields.append(("pins", pins_struct)) if CONF_CLOCK_SPEED in config: @@ -531,7 +547,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.1.7", + ref="0.2.2", ) # Set compile-time configuration via defines @@ -570,6 +586,11 @@ async def to_code(config: ConfigType) -> None: pins_struct = _build_pins_struct(pin_expressions, e_pin_num) hub75_config = _build_config_struct(config, pins_struct, min_refresh) + # Rotation is handled by the hub75 driver (config_.rotation already set above). + # Force rotation to 0 for ESPHome's Display base class to avoid double-rotation. + if CONF_ROTATION in config: + config[CONF_ROTATION] = 0 + # Create display and register var = cg.new_Pvariable(config[CONF_ID], hub75_config) await display.register_display(var, config) diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index e29f1a898c..cf8661b2b3 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -92,14 +92,25 @@ void HUB75Display::fill(Color color) { if (!this->enabled_) [[unlikely]] return; - // Special case: black (off) - use fast hardware clear - if (!color.is_on()) { + // Start with full display rect + display::Rect fill_rect(0, 0, this->get_width_internal(), this->get_height_internal()); + + // Apply clipping using Rect::shrink() to intersect + display::Rect clip = this->get_clipping(); + if (clip.is_set()) { + fill_rect.shrink(clip); + if (!fill_rect.is_set()) + return; // Completely clipped + } + + // Fast path: black filling entire display + if (!color.is_on() && fill_rect.x == 0 && fill_rect.y == 0 && fill_rect.w == this->get_width_internal() && + fill_rect.h == this->get_height_internal()) { driver_->clear(); return; } - // For non-black colors, fall back to base class (pixel-by-pixel) - Display::fill(color); + driver_->fill(fill_rect.x, fill_rect.y, fill_rect.w, fill_rect.h, color.r, color.g, color.b); } void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) { diff --git a/esphome/components/hub75/hub75_component.h b/esphome/components/hub75/hub75_component.h index f0e7ea10d5..ab7e3fc5b1 100644 --- a/esphome/components/hub75/hub75_component.h +++ b/esphome/components/hub75/hub75_component.h @@ -39,8 +39,8 @@ class HUB75Display : public display::Display { protected: // Display internal methods - int get_width_internal() override { return config_.panel_width * config_.layout_cols; } - int get_height_internal() override { return config_.panel_height * config_.layout_rows; } + int get_width_internal() override { return this->driver_ != nullptr ? this->driver_->get_width() : 0; } + int get_height_internal() override { return this->driver_ != nullptr ? this->driver_->get_height() : 0; } // Member variables Hub75Driver *driver_{nullptr}; diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 4573391bc1..36aa77c524 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -28,6 +28,6 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.1.7 + version: 0.2.2 rules: - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" diff --git a/tests/components/hub75/test.esp32-s3-idf-rotate.yaml b/tests/components/hub75/test.esp32-s3-idf-rotate.yaml new file mode 100644 index 0000000000..9855fcb4e6 --- /dev/null +++ b/tests/components/hub75/test.esp32-s3-idf-rotate.yaml @@ -0,0 +1,39 @@ +display: + - platform: hub75 + id: my_hub75 + board: apollo-automation-rev6 + panel_width: 64 + panel_height: 64 + layout_rows: 1 + layout_cols: 2 + rotation: 90 + bit_depth: 4 + double_buffer: true + auto_clear_enabled: true + update_interval: 16ms + latch_blanking: 1 + clock_speed: 20MHz + lambda: |- + // Test clipping: 8 columns x 4 rows of 16x16 colored squares + Color colors[32] = { + Color(255, 0, 0), Color(0, 255, 0), Color(0, 0, 255), Color(255, 255, 0), + Color(255, 0, 255), Color(0, 255, 255), Color(255, 128, 0), Color(128, 0, 255), + Color(0, 128, 255), Color(255, 0, 128), Color(128, 255, 0), Color(0, 255, 128), + Color(255, 128, 128), Color(128, 255, 128), Color(128, 128, 255), Color(255, 255, 128), + Color(255, 128, 255), Color(128, 255, 255), Color(192, 64, 0), Color(64, 192, 0), + Color(0, 64, 192), Color(192, 0, 64), Color(64, 0, 192), Color(0, 192, 64), + Color(128, 64, 64), Color(64, 128, 64), Color(64, 64, 128), Color(128, 128, 64), + Color(128, 64, 128), Color(64, 128, 128), Color(255, 255, 255), Color(128, 128, 128) + }; + int idx = 0; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 8; col++) { + // Clipping mode: clip to square bounds, then fill "entire screen" + it.start_clipping(col * 16, row * 16, (col + 1) * 16, (row + 1) * 16); + it.fill(colors[idx]); + it.end_clipping(); + idx++; + } + } + +<<: !include common.yaml