From a60c9840ca0ba68ffc86085e464aaa0aa15095a2 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Thu, 23 Jul 2026 23:16:58 -0400 Subject: [PATCH] feat(rlcd): add Waveshare ST7305 board support --- .../waveshare-esp32-s3-rlcd/CMakeLists.txt | 7 + .../waveshare-esp32-s3-rlcd/device.properties | 27 + .../waveshare-esp32-s3-rlcd/devicetree.yaml | 5 + .../waveshare-esp32-s3-rlcd/source/module.cpp | 10 + .../waveshare,esp32-s3-rlcd.dts | 96 ++++ Drivers/st7305-module/CMakeLists.txt | 11 + .../bindings/sitronix,st7305.yaml | 35 ++ Drivers/st7305-module/devicetree.yaml | 3 + .../st7305-module/include/bindings/st7305.h | 7 + .../st7305-module/include/drivers/st7305.h | 25 + Drivers/st7305-module/include/st7305_module.h | 14 + Drivers/st7305-module/source/module.cpp | 19 + Drivers/st7305-module/source/st7305.cpp | 509 ++++++++++++++++++ 13 files changed, 768 insertions(+) create mode 100644 Devices/waveshare-esp32-s3-rlcd/CMakeLists.txt create mode 100644 Devices/waveshare-esp32-s3-rlcd/device.properties create mode 100644 Devices/waveshare-esp32-s3-rlcd/devicetree.yaml create mode 100644 Devices/waveshare-esp32-s3-rlcd/source/module.cpp create mode 100644 Devices/waveshare-esp32-s3-rlcd/waveshare,esp32-s3-rlcd.dts create mode 100644 Drivers/st7305-module/CMakeLists.txt create mode 100644 Drivers/st7305-module/bindings/sitronix,st7305.yaml create mode 100644 Drivers/st7305-module/devicetree.yaml create mode 100644 Drivers/st7305-module/include/bindings/st7305.h create mode 100644 Drivers/st7305-module/include/drivers/st7305.h create mode 100644 Drivers/st7305-module/include/st7305_module.h create mode 100644 Drivers/st7305-module/source/module.cpp create mode 100644 Drivers/st7305-module/source/st7305.cpp diff --git a/Devices/waveshare-esp32-s3-rlcd/CMakeLists.txt b/Devices/waveshare-esp32-s3-rlcd/CMakeLists.txt new file mode 100644 index 00000000..38c00783 --- /dev/null +++ b/Devices/waveshare-esp32-s3-rlcd/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "source" + REQUIRES TactilityKernel driver +) diff --git a/Devices/waveshare-esp32-s3-rlcd/device.properties b/Devices/waveshare-esp32-s3-rlcd/device.properties new file mode 100644 index 00000000..64ab7388 --- /dev/null +++ b/Devices/waveshare-esp32-s3-rlcd/device.properties @@ -0,0 +1,27 @@ +general.vendor=WaveShare +general.name=ESP32-S3-RLCD-4.2 +general.incubating=true + +apps.launcherAppId=Launcher + +hardware.target=ESP32S3 +hardware.flashSize=16MB +hardware.spiRam=true +hardware.spiRamMode=OCT +hardware.spiRamSpeed=120M +hardware.tinyUsb=true +hardware.esptoolFlashFreq=120M +hardware.bluetooth=true + +dependencies.useDeprecatedHal=false + +storage.userDataLocation=SD + +display.size=4.2" +display.shape=rectangle +display.dpi=120 + +lvgl.colorDepth=16 +lvgl.uiDensity=compact +lvgl.fontSize=20 +lvgl.theme=DefaultDark diff --git a/Devices/waveshare-esp32-s3-rlcd/devicetree.yaml b/Devices/waveshare-esp32-s3-rlcd/devicetree.yaml new file mode 100644 index 00000000..84f05454 --- /dev/null +++ b/Devices/waveshare-esp32-s3-rlcd/devicetree.yaml @@ -0,0 +1,5 @@ +dependencies: + - Platforms/platform-esp32 + - Drivers/st7305-module + - Drivers/button-control-module +dts: waveshare,esp32-s3-rlcd.dts diff --git a/Devices/waveshare-esp32-s3-rlcd/source/module.cpp b/Devices/waveshare-esp32-s3-rlcd/source/module.cpp new file mode 100644 index 00000000..163815ba --- /dev/null +++ b/Devices/waveshare-esp32-s3-rlcd/source/module.cpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" { + +Module waveshare_esp32_s3_rlcd_module = { + .name = "waveshare-esp32-s3-rlcd" +}; + +} // extern "C" diff --git a/Devices/waveshare-esp32-s3-rlcd/waveshare,esp32-s3-rlcd.dts b/Devices/waveshare-esp32-s3-rlcd/waveshare,esp32-s3-rlcd.dts new file mode 100644 index 00000000..fa31b614 --- /dev/null +++ b/Devices/waveshare-esp32-s3-rlcd/waveshare,esp32-s3-rlcd.dts @@ -0,0 +1,96 @@ +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/ { + compatible = "root"; + model = "Waveshare ESP32-S3-RLCD-4.2"; + + wifi0 { + compatible = "espressif,esp32-wifi-pinned"; + status = "disabled"; + }; + + ble0 { + compatible = "espressif,esp32-ble"; + status = "disabled"; + }; + + gpio0 { + compatible = "espressif,esp32-gpio"; + gpio-count = <49>; + }; + + // Audio bus pins from the RLCD-specific MicroPython configuration. + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 45 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>; + }; + + i2c0 { + compatible = "espressif,esp32-i2c-master"; + port = ; + clock-frequency = <400000>; + pin-sda = <&gpio0 13 GPIO_FLAG_NONE>; + pin-scl = <&gpio0 14 GPIO_FLAG_NONE>; + }; + + spi0 { + compatible = "espressif,esp32-spi"; + host = ; + cs-gpios = <&gpio0 40 GPIO_FLAG_NONE>; + pin-mosi = <&gpio0 12 GPIO_FLAG_NONE>; + pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>; + max-transfer-size = <15000>; + + display@0 { + compatible = "sitronix,st7305"; + horizontal-resolution = <400>; + vertical-resolution = <300>; + pixel-clock-hz = <20000000>; + pin-dc = <&gpio0 5 GPIO_FLAG_NONE>; + pin-reset = <&gpio0 41 GPIO_FLAG_NONE>; + invert-color; + }; + }; + + buttons { + compatible = "tactility,button-control"; + pin-primary = <&gpio0 18 GPIO_FLAG_NONE>; + pin-secondary = <&gpio0 0 GPIO_FLAG_NONE>; + }; + + sdmmc0 { + compatible = "espressif,esp32-sdmmc"; + pin-clk = <&gpio0 38 GPIO_FLAG_NONE>; + pin-cmd = <&gpio0 21 GPIO_FLAG_NONE>; + pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>; + pin-d3 = <&gpio0 17 GPIO_FLAG_NONE>; + slot = ; + bus-width = <1>; + pullups; + }; + + uart0 { + compatible = "espressif,esp32-uart"; + port = ; + pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; + pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; + }; +}; diff --git a/Drivers/st7305-module/CMakeLists.txt b/Drivers/st7305-module/CMakeLists.txt new file mode 100644 index 00000000..03f9f9da --- /dev/null +++ b/Drivers/st7305-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(st7305-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel platform-esp32 esp_lcd driver +) diff --git a/Drivers/st7305-module/bindings/sitronix,st7305.yaml b/Drivers/st7305-module/bindings/sitronix,st7305.yaml new file mode 100644 index 00000000..00441914 --- /dev/null +++ b/Drivers/st7305-module/bindings/sitronix,st7305.yaml @@ -0,0 +1,35 @@ +description: Sitronix ST7305 monochrome reflective LCD panel + +compatible: "sitronix,st7305" + +bus: spi + +properties: + horizontal-resolution: + type: int + required: true + description: Horizontal resolution in pixels + vertical-resolution: + type: int + required: true + description: Vertical resolution in pixels + pixel-clock-hz: + type: int + default: 20000000 + description: SPI pixel clock frequency in Hz + transaction-queue-depth: + type: int + default: 10 + description: Size of the internal SPI transaction queue + pin-dc: + type: phandles + required: true + description: Data/Command GPIO pin + pin-reset: + type: phandles + required: true + description: Active-low reset GPIO pin + invert-color: + type: boolean + default: false + description: Enable the ST7305 display inversion used by the Waveshare RLCD panel diff --git a/Drivers/st7305-module/devicetree.yaml b/Drivers/st7305-module/devicetree.yaml new file mode 100644 index 00000000..a07d6f33 --- /dev/null +++ b/Drivers/st7305-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/st7305-module/include/bindings/st7305.h b/Drivers/st7305-module/include/bindings/st7305.h new file mode 100644 index 00000000..4acde6fd --- /dev/null +++ b/Drivers/st7305-module/include/bindings/st7305.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(st7305, struct St7305Config) diff --git a/Drivers/st7305-module/include/drivers/st7305.h b/Drivers/st7305-module/include/drivers/st7305.h new file mode 100644 index 00000000..ca548ba0 --- /dev/null +++ b/Drivers/st7305-module/include/drivers/st7305.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +struct St7305Config { + uint16_t horizontal_resolution; + uint16_t vertical_resolution; + uint32_t pixel_clock_hz; + uint8_t transaction_queue_depth; + struct GpioPinSpec pin_dc; + struct GpioPinSpec pin_reset; + bool invert_color; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7305-module/include/st7305_module.h b/Drivers/st7305-module/include/st7305_module.h new file mode 100644 index 00000000..dffd85c1 --- /dev/null +++ b/Drivers/st7305-module/include/st7305_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module st7305_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/st7305-module/source/module.cpp b/Drivers/st7305-module/source/module.cpp new file mode 100644 index 00000000..32a3150c --- /dev/null +++ b/Drivers/st7305-module/source/module.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern "C" { + +extern Driver st7305_driver; + +static Driver* const st7305_drivers[] = { + &st7305_driver, + nullptr +}; + +Module st7305_module = { + .name = "st7305", + .drivers = st7305_drivers +}; + +} // extern "C" diff --git a/Drivers/st7305-module/source/st7305.cpp b/Drivers/st7305-module/source/st7305.cpp new file mode 100644 index 00000000..d1d16c0e --- /dev/null +++ b/Drivers/st7305-module/source/st7305.cpp @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "ST7305" +#define GET_CONFIG(device) (static_cast((device)->config)) + +namespace { + +constexpr uint16_t PANEL_WIDTH = 400; +constexpr uint16_t PANEL_HEIGHT = 300; +constexpr size_t PANEL_BUFFER_SIZE = PANEL_WIDTH * PANEL_HEIGHT / 8; +constexpr uint8_t PANEL_COLUMN_START = 0x12; +constexpr uint8_t PANEL_COLUMN_END = 0x2A; +constexpr uint8_t PANEL_ROW_START = 0x00; +constexpr uint8_t PANEL_ROW_END = 0xC7; + +constexpr uint8_t CMD_SLEEP_IN = 0x10; +constexpr uint8_t CMD_SLEEP_OUT = 0x11; +constexpr uint8_t CMD_INVERSION_OFF = 0x20; +constexpr uint8_t CMD_INVERSION_ON = 0x21; +constexpr uint8_t CMD_DISPLAY_OFF = 0x28; +constexpr uint8_t CMD_DISPLAY_ON = 0x29; +constexpr uint8_t CMD_COLUMN_ADDRESS = 0x2A; +constexpr uint8_t CMD_ROW_ADDRESS = 0x2B; +constexpr uint8_t CMD_MEMORY_WRITE = 0x2C; + +struct InitCommand { + uint8_t command; + const uint8_t* data; + size_t data_size; + uint16_t delay_ms; +}; + +static const uint8_t INIT_D6[] = { 0x17, 0x02 }; +static const uint8_t INIT_D1[] = { 0x01 }; +static const uint8_t INIT_C0[] = { 0x11, 0x04 }; +static const uint8_t INIT_C1[] = { 0x41, 0x41, 0x41, 0x41 }; +static const uint8_t INIT_C2[] = { 0x19, 0x19, 0x19, 0x19 }; +static const uint8_t INIT_C4[] = { 0x41, 0x41, 0x41, 0x41 }; +// Keep the second value exactly as the proven MicroPython RLCD sequence: decimal 19 (0x13). +static const uint8_t INIT_C5[] = { 0x19, 0x13, 0x19, 0x19 }; +static const uint8_t INIT_D8[] = { 0xA6, 0xE9 }; +static const uint8_t INIT_B2[] = { 0x05 }; +static const uint8_t INIT_B3[] = { 0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45 }; +static const uint8_t INIT_B4[] = { 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45 }; +static const uint8_t INIT_62[] = { 0x32, 0x03, 0x1F }; +static const uint8_t INIT_B7[] = { 0x13 }; +static const uint8_t INIT_B0[] = { 0x64 }; +static const uint8_t INIT_C9[] = { 0x00 }; +static const uint8_t INIT_36[] = { 0x48 }; +static const uint8_t INIT_3A[] = { 0x11 }; +static const uint8_t INIT_B9[] = { 0x20 }; +static const uint8_t INIT_B8[] = { 0x29 }; +static const uint8_t INIT_2A[] = { PANEL_COLUMN_START, PANEL_COLUMN_END }; +static const uint8_t INIT_2B[] = { PANEL_ROW_START, PANEL_ROW_END }; +static const uint8_t INIT_35[] = { 0x00 }; +static const uint8_t INIT_D0[] = { 0xFF }; + +// Command order and voltage values are intentionally identical to lib/rlcd.py in the supplied +// MicroPython project. The inversion command sits between these two groups and display-on follows. +static const InitCommand INIT_SEQUENCE_BEFORE_INVERSION[] = { + { 0xD6, INIT_D6, sizeof(INIT_D6), 0 }, + { 0xD1, INIT_D1, sizeof(INIT_D1), 0 }, + { 0xC0, INIT_C0, sizeof(INIT_C0), 0 }, + { 0xC1, INIT_C1, sizeof(INIT_C1), 0 }, + { 0xC2, INIT_C2, sizeof(INIT_C2), 0 }, + { 0xC4, INIT_C4, sizeof(INIT_C4), 0 }, + { 0xC5, INIT_C5, sizeof(INIT_C5), 0 }, + { 0xD8, INIT_D8, sizeof(INIT_D8), 0 }, + { 0xB2, INIT_B2, sizeof(INIT_B2), 0 }, + { 0xB3, INIT_B3, sizeof(INIT_B3), 0 }, + { 0xB4, INIT_B4, sizeof(INIT_B4), 0 }, + { 0x62, INIT_62, sizeof(INIT_62), 0 }, + { 0xB7, INIT_B7, sizeof(INIT_B7), 0 }, + { 0xB0, INIT_B0, sizeof(INIT_B0), 0 }, + { CMD_SLEEP_OUT, nullptr, 0, 200 }, + { 0xC9, INIT_C9, sizeof(INIT_C9), 0 }, + { 0x36, INIT_36, sizeof(INIT_36), 0 }, + { 0x3A, INIT_3A, sizeof(INIT_3A), 0 }, + { 0xB9, INIT_B9, sizeof(INIT_B9), 0 }, + { 0xB8, INIT_B8, sizeof(INIT_B8), 0 }, +}; + +static const InitCommand INIT_SEQUENCE_AFTER_INVERSION[] = { + { CMD_COLUMN_ADDRESS, INIT_2A, sizeof(INIT_2A), 0 }, + { CMD_ROW_ADDRESS, INIT_2B, sizeof(INIT_2B), 0 }, + { 0x35, INIT_35, sizeof(INIT_35), 0 }, + { 0xD0, INIT_D0, sizeof(INIT_D0), 0 }, + { 0x38, nullptr, 0, 0 }, +}; + +struct St7305Internal { + esp_lcd_panel_io_handle_t io_handle; + SemaphoreHandle_t transfer_done; + uint8_t* panel_buffer; +}; + +static int pin_or_unused(const GpioPinSpec& pin) { + return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); +} + +static bool IRAM_ATTR on_color_transfer_done( + esp_lcd_panel_io_handle_t, + esp_lcd_panel_io_event_data_t*, + void* user_context +) { + auto* internal = static_cast(user_context); + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->transfer_done, &high_task_woken); + return high_task_woken == pdTRUE; +} + +static bool send_command( + esp_lcd_panel_io_handle_t io_handle, + uint8_t command, + const void* data = nullptr, + size_t data_size = 0 +) { + esp_err_t result = esp_lcd_panel_io_tx_param(io_handle, command, data, data_size); + if (result != ESP_OK) { + LOG_E(TAG, "Command 0x%02X failed: %s", command, esp_err_to_name(result)); + return false; + } + return true; +} + +template +static bool send_init_sequence(esp_lcd_panel_io_handle_t io_handle, const InitCommand (&sequence)[Size]) { + for (const auto& item : sequence) { + if (!send_command(io_handle, item.command, item.data, item.data_size)) { + return false; + } + if (item.delay_ms > 0) { + vTaskDelay(pdMS_TO_TICKS(item.delay_ms)); + } + } + return true; +} + +static bool perform_hardware_reset(const St7305Config* config) { + const int reset_pin = pin_or_unused(config->pin_reset); + if (reset_pin < 0) { + LOG_E(TAG, "Reset pin is required"); + return false; + } + + gpio_config_t io_config = { + .pin_bit_mask = 1ULL << reset_pin, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t result = gpio_config(&io_config); + if (result != ESP_OK) { + LOG_E(TAG, "Failed to configure reset pin: %s", esp_err_to_name(result)); + return false; + } + + // Active-low reset, matching RLCD.reset(): released -> asserted -> released. + if (gpio_set_level(static_cast(reset_pin), 1) != ESP_OK) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(50)); + if (gpio_set_level(static_cast(reset_pin), 0) != ESP_OK) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(20)); + if (gpio_set_level(static_cast(reset_pin), 1) != ESP_OK) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(50)); + return true; +} + +static bool set_address_window(esp_lcd_panel_io_handle_t io_handle) { + return send_command(io_handle, CMD_COLUMN_ADDRESS, INIT_2A, sizeof(INIT_2A)) && + send_command(io_handle, CMD_ROW_ADDRESS, INIT_2B, sizeof(INIT_2B)); +} + +static bool send_panel_buffer(St7305Internal* internal) { + if (!set_address_window(internal->io_handle)) { + return false; + } + + // Parameter transactions can leave a callback signal on some ESP-IDF versions. Drain it so + // this wait can only be completed by the color transaction queued immediately below. + xSemaphoreTake(internal->transfer_done, 0); + esp_err_t result = esp_lcd_panel_io_tx_color( + internal->io_handle, + CMD_MEMORY_WRITE, + internal->panel_buffer, + PANEL_BUFFER_SIZE + ); + if (result != ESP_OK) { + LOG_E(TAG, "Panel transfer failed: %s", esp_err_to_name(result)); + return false; + } + + xSemaphoreTake(internal->transfer_done, portMAX_DELAY); + return true; +} + +// Converts Tactility's DISPLAY_COLOR_FORMAT_MONOCHROME (row-major, MSB-first, bit 1 = white) +// into the ST7305's column-major 2x4 packing. The working MicroPython canvas uses bit 1 = black +// and enables panel inversion, so each LVGL source pair is inverted before it is packed. +static void pack_monochrome_frame(const uint8_t* source, uint8_t* destination) { + constexpr size_t source_stride = PANEL_WIDTH / 8; + constexpr size_t destination_column_stride = PANEL_HEIGHT / 4; + + for (uint16_t byte_x = 0; byte_x < PANEL_WIDTH / 2; byte_x++) { + const uint16_t x = byte_x * 2; + const uint8_t source_pair_shift = static_cast(6 - (x & 0x07)); + + for (uint16_t block_y = 0; block_y < PANEL_HEIGHT / 4; block_y++) { + uint8_t packed = 0; + for (uint8_t local_y = 0; local_y < 4; local_y++) { + const uint16_t y = PANEL_HEIGHT - 1 - (block_y * 4 + local_y); + const uint8_t white_pair = + (source[y * source_stride + x / 8] >> source_pair_shift) & 0x03; + const uint8_t black_pair = white_pair ^ 0x03; + packed |= static_cast(black_pair << (6 - local_y * 2)); + } + destination[byte_x * destination_column_stride + block_y] = packed; + } + } +} + +static bool initialize_panel(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + const auto* config = GET_CONFIG(device); + + if (!send_init_sequence(internal->io_handle, INIT_SEQUENCE_BEFORE_INVERSION)) { + return false; + } + if (!send_command( + internal->io_handle, + config->invert_color ? CMD_INVERSION_ON : CMD_INVERSION_OFF + )) { + return false; + } + if (!send_init_sequence(internal->io_handle, INIT_SEQUENCE_AFTER_INVERSION)) { + return false; + } + if (!send_command(internal->io_handle, CMD_DISPLAY_ON)) { + return false; + } + + // The Python reference starts with an all-zero hardware buffer. With inversion enabled this + // clears retained GRAM to reflective white before LVGL submits its first full frame. + memset(internal->panel_buffer, 0, PANEL_BUFFER_SIZE); + return send_panel_buffer(internal); +} + +} // namespace + +// region DisplayApi + +static error_t st7305_reset(Device* device) { + return perform_hardware_reset(GET_CONFIG(device)) ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7305_init(Device* device) { + return initialize_panel(device) ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7305_draw_bitmap( + Device* device, + int32_t x_start, + int32_t y_start, + int32_t x_end, + int32_t y_end, + const void* color_data +) { + if (x_start != 0 || y_start != 0 || x_end != PANEL_WIDTH || y_end != PANEL_HEIGHT || + color_data == nullptr) { + LOG_E(TAG, "ST7305 requires a complete %ux%u frame", PANEL_WIDTH, PANEL_HEIGHT); + return ERROR_INVALID_ARGUMENT; + } + + auto* internal = static_cast(device_get_driver_data(device)); + pack_monochrome_frame(static_cast(color_data), internal->panel_buffer); + return send_panel_buffer(internal) ? ERROR_NONE : ERROR_RESOURCE; +} + +static error_t st7305_invert_color(Device* device, bool invert) { + auto* internal = static_cast(device_get_driver_data(device)); + return send_command(internal->io_handle, invert ? CMD_INVERSION_ON : CMD_INVERSION_OFF) + ? ERROR_NONE + : ERROR_RESOURCE; +} + +static error_t st7305_disp_on_off(Device* device, bool on) { + auto* internal = static_cast(device_get_driver_data(device)); + return send_command(internal->io_handle, on ? CMD_DISPLAY_ON : CMD_DISPLAY_OFF) + ? ERROR_NONE + : ERROR_RESOURCE; +} + +static error_t st7305_disp_sleep(Device* device, bool sleep) { + auto* internal = static_cast(device_get_driver_data(device)); + if (!send_command(internal->io_handle, sleep ? CMD_SLEEP_IN : CMD_SLEEP_OUT)) { + return ERROR_RESOURCE; + } + vTaskDelay(pdMS_TO_TICKS(sleep ? 10 : 120)); + return ERROR_NONE; +} + +static DisplayColorFormat st7305_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_MONOCHROME; +} + +static uint16_t st7305_get_resolution_x(Device*) { + return PANEL_WIDTH; +} + +static uint16_t st7305_get_resolution_y(Device*) { + return PANEL_HEIGHT; +} + +static void st7305_get_frame_buffer(Device*, uint8_t, void** out_buffer) { + *out_buffer = nullptr; +} + +static uint8_t st7305_get_frame_buffer_count(Device*) { + return 0; +} + +static const DisplayApi st7305_display_api = { + .capabilities = DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | + DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME, + .reset = st7305_reset, + .init = st7305_init, + .draw_bitmap = st7305_draw_bitmap, + .mirror = nullptr, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = nullptr, + .get_mirror_y = nullptr, + .set_gap = nullptr, + .get_gap_x = nullptr, + .get_gap_y = nullptr, + .invert_color = st7305_invert_color, + .disp_on_off = st7305_disp_on_off, + .disp_sleep = st7305_disp_sleep, + .get_color_format = st7305_get_color_format, + .get_resolution_x = st7305_get_resolution_x, + .get_resolution_y = st7305_get_resolution_y, + .get_frame_buffer = st7305_get_frame_buffer, + .get_frame_buffer_count = st7305_get_frame_buffer_count, + .get_backlight = nullptr, + .has_capability = nullptr, +}; + +// endregion + +// region Driver lifecycle + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + check(device_get_type(parent) == &SPI_CONTROLLER_TYPE); + + const auto* config = GET_CONFIG(device); + if (config->horizontal_resolution != PANEL_WIDTH || + config->vertical_resolution != PANEL_HEIGHT) { + LOG_E(TAG, "Unsupported resolution %ux%u", config->horizontal_resolution, config->vertical_resolution); + return ERROR_NOT_SUPPORTED; + } + + GpioPinSpec cs_pin; + if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) { + LOG_E(TAG, "Failed to resolve CS pin"); + return ERROR_RESOURCE; + } + + auto* internal = static_cast(calloc(1, sizeof(St7305Internal))); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->transfer_done = xSemaphoreCreateBinary(); + if (internal->transfer_done == nullptr) { + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + internal->panel_buffer = static_cast( + heap_caps_malloc(PANEL_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_8BIT) + ); + if (internal->panel_buffer == nullptr) { + vSemaphoreDelete(internal->transfer_done); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + + const auto* spi_config = static_cast(parent->config); + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = pin_or_unused(cs_pin), + .dc_gpio_num = pin_or_unused(config->pin_dc), + .spi_mode = 0, + .pclk_hz = config->pixel_clock_hz, + .trans_queue_depth = config->transaction_queue_depth, + .on_color_trans_done = on_color_transfer_done, + .user_ctx = internal, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 1, + .lsb_first = 0, + .cs_high_active = 0, + }, + }; + + esp_err_t result = esp_lcd_new_panel_io_spi( + static_cast(spi_config->host), + &io_config, + &internal->io_handle + ); + if (result != ESP_OK) { + LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(result)); + heap_caps_free(internal->panel_buffer); + vSemaphoreDelete(internal->transfer_done); + free(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + if (!perform_hardware_reset(config) || !initialize_panel(device)) { + LOG_E(TAG, "Failed to initialize panel"); + device_set_driver_data(device, nullptr); + esp_lcd_panel_io_del(internal->io_handle); + heap_caps_free(internal->panel_buffer); + vSemaphoreDelete(internal->transfer_done); + free(internal); + return ERROR_RESOURCE; + } + + LOG_I(TAG, "Initialized %ux%u reflective LCD at %lu Hz", + PANEL_WIDTH, + PANEL_HEIGHT, + static_cast(config->pixel_clock_hz)); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + if (internal == nullptr) { + return ERROR_NONE; + } + + send_command(internal->io_handle, CMD_DISPLAY_OFF); + if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) { + LOG_E(TAG, "Failed to delete panel IO"); + return ERROR_RESOURCE; + } + + const int reset_pin = pin_or_unused(GET_CONFIG(device)->pin_reset); + if (reset_pin >= 0) { + gpio_reset_pin(static_cast(reset_pin)); + } + + heap_caps_free(internal->panel_buffer); + vSemaphoreDelete(internal->transfer_done); + free(internal); + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +Driver st7305_driver = { + .name = "st7305", + .compatible = (const char*[]) { "sitronix,st7305", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &st7305_display_api, + .device_type = &DISPLAY_TYPE, + .owner = &st7305_module, + .internal = nullptr +};