#include "hub75_component.h" #include "esphome/core/application.h" #include #ifdef USE_ESP32 namespace esphome::hub75 { static const char *const TAG = "hub75"; // ======================================== // Constructor // ======================================== HUB75Display::HUB75Display(const Hub75Config &config) : config_(config) { // Initialize runtime state from config this->brightness_ = config.brightness; this->enabled_ = (config.brightness > 0); } // ======================================== // Core Component methods // ======================================== void HUB75Display::setup() { ESP_LOGCONFIG(TAG, "Setting up HUB75Display..."); // Create driver with pre-configured config driver_ = new Hub75Driver(config_); if (!driver_->begin()) { ESP_LOGE(TAG, "Failed to initialize HUB75 driver!"); return; } this->enabled_ = true; } void HUB75Display::dump_config() { LOG_DISPLAY("", "HUB75", this); ESP_LOGCONFIG(TAG, " Panel: %dx%d pixels\n" " Layout: %dx%d panels\n" " Virtual Display: %dx%d pixels", config_.panel_width, config_.panel_height, config_.layout_cols, config_.layout_rows, config_.panel_width * config_.layout_cols, config_.panel_height * config_.layout_rows); ESP_LOGCONFIG(TAG, " Scan Wiring: %d\n" " Shift Driver: %d", static_cast(config_.scan_wiring), static_cast(config_.shift_driver)); ESP_LOGCONFIG(TAG, " Pins: R1:%i, G1:%i, B1:%i, R2:%i, G2:%i, B2:%i\n" " Pins: A:%i, B:%i, C:%i, D:%i, E:%i\n" " Pins: LAT:%i, OE:%i, CLK:%i", config_.pins.r1, config_.pins.g1, config_.pins.b1, config_.pins.r2, config_.pins.g2, config_.pins.b2, config_.pins.a, config_.pins.b, config_.pins.c, config_.pins.d, config_.pins.e, config_.pins.lat, config_.pins.oe, config_.pins.clk); ESP_LOGCONFIG(TAG, " Clock Speed: %" PRIu32 " MHz\n" " Latch Blanking: %i\n" " Clock Phase: %s\n" " Min Refresh Rate: %i Hz\n" " Bit Depth: %i\n" " Double Buffer: %s", static_cast(config_.output_clock_speed) / 1000000, config_.latch_blanking, TRUEFALSE(config_.clk_phase_inverted), config_.min_refresh_rate, HUB75_BIT_DEPTH, YESNO(config_.double_buffer)); } // ======================================== // Display/PollingComponent methods // ======================================== void HUB75Display::update() { if (!driver_) [[unlikely]] return; if (!this->enabled_) [[unlikely]] return; this->do_update_(); if (config_.double_buffer) { driver_->flip_buffer(); } } void HUB75Display::fill(Color color) { if (!driver_) [[unlikely]] return; if (!this->enabled_) [[unlikely]] return; // 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; } 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) { if (!driver_) [[unlikely]] return; if (!this->enabled_) [[unlikely]] return; if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]] return; if (!this->get_clipping().inside(x, y)) return; driver_->set_pixel(x, y, color.r, color.g, color.b); App.feed_wdt(); } void HOT HUB75Display::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) { if (!driver_) [[unlikely]] return; if (!this->enabled_) [[unlikely]] return; // Map ESPHome enums to hub75 enums Hub75PixelFormat format; Hub75ColorOrder color_order = Hub75ColorOrder::RGB; int bytes_per_pixel; // Determine format based on bitness if (bitness == ColorBitness::COLOR_BITNESS_565) { format = Hub75PixelFormat::RGB565; bytes_per_pixel = 2; } else if (bitness == ColorBitness::COLOR_BITNESS_888) { #ifdef USE_LVGL #if LV_COLOR_DEPTH == 32 // 32-bit: 4 bytes per pixel with padding byte (LVGL mode) format = Hub75PixelFormat::RGB888_32; bytes_per_pixel = 4; // Map ESPHome ColorOrder to Hub75ColorOrder // ESPHome ColorOrder is typically BGR for little-endian 32-bit color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR; #elif LV_COLOR_DEPTH == 24 // 24-bit: 3 bytes per pixel, tightly packed format = Hub75PixelFormat::RGB888; bytes_per_pixel = 3; // Note: 24-bit is always RGB order in LVGL #else ESP_LOGE(TAG, "Unsupported LV_COLOR_DEPTH: %d", LV_COLOR_DEPTH); return; #endif #else // Non-LVGL mode: standard 24-bit RGB888 format = Hub75PixelFormat::RGB888; bytes_per_pixel = 3; color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR; #endif } else { ESP_LOGE(TAG, "Unsupported bitness: %d", static_cast(bitness)); return; } // Check if buffer is tightly packed (no stride) const int stride_px = x_offset + w + x_pad; const bool is_packed = (x_offset == 0 && x_pad == 0 && y_offset == 0); if (is_packed) { // Tightly packed buffer - single bulk call for best performance driver_->draw_pixels(x_start, y_start, w, h, ptr, format, color_order, big_endian); } else { // Buffer has stride (padding between rows) - draw row by row for (int yy = 0; yy < h; ++yy) { const size_t row_offset = ((y_offset + yy) * stride_px + x_offset) * bytes_per_pixel; const uint8_t *row_ptr = ptr + row_offset; driver_->draw_pixels(x_start, y_start + yy, w, 1, row_ptr, format, color_order, big_endian); } } } void HUB75Display::set_brightness(uint8_t brightness) { this->brightness_ = brightness; this->enabled_ = (brightness > 0); if (this->driver_ != nullptr) { this->driver_->set_brightness(brightness); } } } // namespace esphome::hub75 #endif