diff --git a/.gitmodules b/.gitmodules index 695474766..0d18c27c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,6 +16,3 @@ [submodule "Libraries/cJSON/cJSON"] path = Libraries/cJSON/cJSON url = https://github.com/DaveGamble/cJSON.git -[submodule "Libraries/esp_epaper"] - path = Libraries/esp_epaper - url = https://github.com/NellowTCS/esp_epaper.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f29fc870..1f7fb145e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,8 @@ else () add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="tactility.launcher") add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="") add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL) + add_compile_definitions(CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=false) + endif () project(Tactility) diff --git a/Devices/cl32/CMakeLists.txt b/Devices/cl32/CMakeLists.txt index 09a2fdee3..7c10a752a 100644 --- a/Devices/cl32/CMakeLists.txt +++ b/Devices/cl32/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "source" - REQUIRES TactilityKernel + REQUIRES TactilityKernel tca8418-module ) diff --git a/Devices/cl32/cl32.dts b/Devices/cl32/cl32.dts index 26ea813fe..1fe1a5da9 100644 --- a/Devices/cl32/cl32.dts +++ b/Devices/cl32/cl32.dts @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -26,8 +25,10 @@ status = "disabled"; }; - // Top Stemma/Qwiic port 1 shares this bus with the keyboard and RTC (SDA: pin 1 <-> SCL: pin 2). - // External I2C devices on that port are reachable via i2c0. + // Top Stemma/Qwiic port 1 shares this bus with the RTC (SDA: pin 1 <-> SCL: pin 2). External + // I2C devices on that port are reachable via i2c0. The tca8418 keyboard (revision 2 hardware) + // and power-supply chip (revision 3 hardware) are mutually exclusive on this bus and are + // created dynamically based on hardware detection - see cl32_detect.cpp. i2c0 { compatible = "espressif,esp32-i2c-master"; port = ; @@ -35,47 +36,6 @@ pin-sda = <&gpio0 1 GPIO_FLAG_PULL_UP>; pin-scl = <&gpio0 2 GPIO_FLAG_PULL_UP>; - keyboard { - compatible = "ti,tca8418"; - reg = <0x34>; - rows = <8>; - columns = <10>; - keymap-lc = [ - 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 - 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT - 9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i - 111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP - 0 97 115 100 102 103 104 106 107 0 // a s d f g h j k - 108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU - 122 120 99 118 98 32 32 110 109 0 // z x c v b n m - 20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN - ]; - keymap-uc = [ - 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 - 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT - 9 81 87 69 82 84 89 85 73 0 // TAB Q W E R T Y U I - 79 80 13 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP - 0 65 83 68 70 71 72 74 75 0 // A S D F G H J K - 76 17 35 123 125 42 44 46 2 0 // L UP # { } * , . MENU - 90 88 67 86 66 32 32 78 77 0 // Z X C V B N M - 20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN - ]; - keymap-sy = [ - 37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8 - 57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT - 9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i - 111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP - 0 97 115 100 102 103 104 106 107 0 // a s d f g h j k - 108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU - 122 120 99 118 98 32 32 110 109 0 // z x c v b n m - 20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN - ]; - shift-row = <4>; - shift-col = <0>; - sym-row = <5>; - sym-col = <8>; - }; - rtc: bm8563 { compatible = "belling,bm8563"; reg = <0x51>; diff --git a/Devices/cl32/device.properties b/Devices/cl32/device.properties index 98ce693ff..2553a4ffe 100644 --- a/Devices/cl32/device.properties +++ b/Devices/cl32/device.properties @@ -20,3 +20,7 @@ display.dpi=139 lvgl.colorDepth=8 lvgl.theme=Mono +lvgl.uiDensity=compact +lvgl.statusbarColorsInverted=true + +cdn.warningMessage=Only hardware revision 0.2 and 0.3 are supported for now! diff --git a/Devices/cl32/source/cl32_detect.cpp b/Devices/cl32/source/cl32_detect.cpp new file mode 100644 index 000000000..82a8ab246 --- /dev/null +++ b/Devices/cl32/source/cl32_detect.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_detect.h" + +#include "cl32_v2_keyboard.h" +#include "cl32_v3_keyboard.h" +#include "cl32_v2.h" +#include "cl32_v3.h" +#include "cl32_v4.h" +#include "cl32_v4_keyboard.h" +#include "cl32_v4_power.h" + +#include +#include +#include +#include + +#include + +constexpr auto* TAG = "cl32-detect"; + +// Revision 2 and 3 boards are both plain tca8418 keyboards (different physical key layouts) with +// no core chip. Revision 4 boards have the core chip (power + keyboard) at CL32_V4_CORE_I2C_ADDRESS. +static Cl32HardwareRevision cl32_revision = Cl32HardwareRevision::Unknown; + +static Device cl32_v4_power_device {}; +static bool cl32_v4_power_created = false; + +// The probe-once latch for on_i2c0_started(). File-scope (not function-local) so +// cl32_teardown_devices() can reset it for a later start/probe cycle. +static bool did_probe = false; + +static void create_v2_devices(Device* i2c0) { + cl32_create_keyboard(i2c0); +} + +static void create_v3_devices(Device* i2c0) { + cl32_v3_create_keyboard(i2c0); +} + +static bool create_v4_power_device(Device* i2c0) { + cl32_v4_power_device = Device { .address = 0, .name = "cl32-v4-power", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr }; + + error_t error = device_construct(&cl32_v4_power_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct cl32-v4-power: %s", error_to_string(error)); + return false; + } + + device_set_parent(&cl32_v4_power_device, i2c0); + device_set_driver(&cl32_v4_power_device, &cl32_v4_power_driver); + + error = device_add(&cl32_v4_power_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add cl32-v4-power: %s", error_to_string(error)); + device_destruct(&cl32_v4_power_device); + return false; + } + + error = device_start(&cl32_v4_power_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start cl32-v4-power: %s", error_to_string(error)); + device_remove(&cl32_v4_power_device); + device_destruct(&cl32_v4_power_device); + return false; + } + + return true; +} + +static void create_v4_devices(Device* i2c0) { + cl32_v4_create_keyboard(i2c0); + cl32_v4_power_created = create_v4_power_device(i2c0); +} + +// Stops, removes and destructs cl32_v4_power_device if it was successfully created. +// cl32_v4_power_driver's own stop() tears down its power-supply child device. +static void destroy_v4_power_device() { + if (!cl32_v4_power_created) { + return; + } + device_stop(&cl32_v4_power_device); + device_remove(&cl32_v4_power_device); + device_destruct(&cl32_v4_power_device); + cl32_v4_power_created = false; +} + +static Cl32HardwareRevision cl32_detect(Device* i2c0) { + uint8_t probe_value = 0; + if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, CL32_V4_KEYBOARD_PROBE_REGISTER, &probe_value, CL32_V4_CORE_TIMEOUT) == ERROR_NONE) { + LOG_I(TAG, "Detected V4 hardware by V4 keyboard presence"); + return Cl32HardwareRevision::Revision4; + } + + if (i2c_controller_has_device_at_address(i2c0, CL32_V3_FUEL_GAUGE_I2C_ADDRESS, CL32_V3_TIMEOUT) == ERROR_NONE) { + LOG_I(TAG, "Detected V3 hardware by MAX17048G fuel gauge presence"); + return Cl32HardwareRevision::Revision3; + } + + LOG_I(TAG, "No core chip or fuel gauge detected, assuming revision 2 hardware"); + return Cl32HardwareRevision::Revision2; +} + +// Fires for every device's start/stop in the system. +static void on_i2c0_started(Device* device, DeviceEvent event, void* context) { + (void)context; + + if (did_probe || event != DEVICE_EVENT_STARTED || strcmp(device->name, "i2c0") != 0) { + return; + } + did_probe = true; + + cl32_revision = cl32_detect(device); + + switch (cl32_revision) { + case Cl32HardwareRevision::Revision2: + create_v2_devices(device); + break; + case Cl32HardwareRevision::Revision3: + create_v3_devices(device); + break; + case Cl32HardwareRevision::Revision4: + create_v4_devices(device); + break; + default: + LOG_W(TAG, "Unknown/unsupported hardware revision"); + break; + } +} + +Cl32HardwareRevision cl32_hardware_revision() { + return cl32_revision; +} + +void cl32_teardown_devices() { + // Revision 2/3 keyboards bind the shared ti,tca8418 driver (owned by tca8418-module), so they + // don't block cl32_module's own driver destruction - torn down anyway for symmetry and so a + // later start can recreate them cleanly. + cl32_destroy_keyboard(); + cl32_v3_destroy_keyboard(); + + // Revision 4 devices bind cl32-owned drivers (cl32_v4_keyboard_driver, cl32_v4_power_driver). + // Must be torn down before cl32_module's driver-removal loop runs, or driver_destruct() finds + // a started device still using the driver and fails. + cl32_v4_destroy_keyboard(); + destroy_v4_power_device(); + + did_probe = false; +} + +void cl32_power_detect_start() { + device_listener_add(on_i2c0_started, nullptr); +} + +void cl32_power_detect_stop() { + device_listener_remove(on_i2c0_started); +} diff --git a/Devices/cl32/source/cl32_detect.h b/Devices/cl32/source/cl32_detect.h new file mode 100644 index 000000000..f54f56854 --- /dev/null +++ b/Devices/cl32/source/cl32_detect.h @@ -0,0 +1,16 @@ +#pragma once + +enum class Cl32HardwareRevision { + Unknown, + Revision2, + Revision3, + Revision4, +}; + +Cl32HardwareRevision cl32_hardware_revision(); + +void cl32_power_detect_start(); +void cl32_power_detect_stop(); + +/** Stops, removes and destructs the devices for the relevant hardware revision. */ +void cl32_teardown_devices(); diff --git a/Devices/cl32/source/cl32_v2.h b/Devices/cl32/source/cl32_v2.h new file mode 100644 index 000000000..8df2c0768 --- /dev/null +++ b/Devices/cl32/source/cl32_v2.h @@ -0,0 +1,4 @@ +#pragma once + +#include + diff --git a/Devices/cl32/source/cl32_v2_keyboard.cpp b/Devices/cl32/source/cl32_v2_keyboard.cpp new file mode 100644 index 000000000..03f969d9a --- /dev/null +++ b/Devices/cl32/source/cl32_v2_keyboard.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_v2_keyboard.h" + +#include + +#include +#include +#include +#include +#include + +constexpr auto* TAG = "cl32-keyboard"; + +// Keymaps and matrix layout, copied from the original cl32.dts "keyboard" node (removed once +// keyboard creation became conditional on revision-2 hardware detection). +static constexpr uint32_t KEYMAP_LC[80] = { + 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 + 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT + 9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i + 111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP + 0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k + 108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU + 122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN +}; +static constexpr uint32_t KEYMAP_UC[80] = { + 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 + 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT + 9, 81, 87, 69, 82, 84, 89, 85, 73, 0, // TAB Q W E R T Y U I + 79, 80, 13, 40, 41, 45, 59, 58, 3, 0, // O P ENTER ( ) - ; : STOP + 0, 65, 83, 68, 70, 71, 72, 74, 75, 0, // A S D F G H J K + 76, 17, 35, 123, 125, 42, 44, 46, 2, 0, // L UP # { } * , . MENU + 90, 88, 67, 86, 66, 32, 32, 78, 77, 0, // Z X C V B N M + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN +}; +static constexpr uint32_t KEYMAP_SY[80] = { + 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 + 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT + 9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i + 111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP + 0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k + 108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU + 122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN +}; + +static Tca8418Config cl32_keyboard_config {}; +static Device cl32_keyboard_device {}; +static bool cl32_keyboard_created = false; + +bool cl32_create_keyboard(Device* i2c0) { + cl32_keyboard_config = Tca8418Config { + .address = 0x34, + .rows = 8, + .columns = 10, + .reverse_columns = false, + .keymap_lc = KEYMAP_LC, + .keymap_lc_length = sizeof(KEYMAP_LC) / sizeof(KEYMAP_LC[0]), + .keymap_uc = KEYMAP_UC, + .keymap_uc_length = sizeof(KEYMAP_UC) / sizeof(KEYMAP_UC[0]), + .keymap_sy = KEYMAP_SY, + .keymap_sy_length = sizeof(KEYMAP_SY) / sizeof(KEYMAP_SY[0]), + .shift_row = 4, + .shift_col = 0, + .sym_row = 5, + .sym_col = 8, + .pin_reset = GPIO_PIN_SPEC_NONE, + }; + + cl32_keyboard_device = Device { + .address = 0, + .name = "keyboard", + .config = &cl32_keyboard_config, + .parent = nullptr, + .flags = 0, + .internal = nullptr, + }; + + error_t error = device_construct(&cl32_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error)); + return false; + } + + device_set_parent(&cl32_keyboard_device, i2c0); + + Driver* driver = driver_find_compatible("ti,tca8418"); + if (driver == nullptr) { + LOG_E(TAG, "No driver registered for ti,tca8418"); + device_destruct(&cl32_keyboard_device); + return false; + } + device_set_driver(&cl32_keyboard_device, driver); + + error = device_add(&cl32_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error)); + device_destruct(&cl32_keyboard_device); + return false; + } + + error = device_start(&cl32_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error)); + device_remove(&cl32_keyboard_device); + device_destruct(&cl32_keyboard_device); + return false; + } + + cl32_keyboard_created = true; + return true; +} + +void cl32_destroy_keyboard() { + if (!cl32_keyboard_created) { + return; + } + device_stop(&cl32_keyboard_device); + device_remove(&cl32_keyboard_device); + device_destruct(&cl32_keyboard_device); + cl32_keyboard_created = false; +} diff --git a/Devices/cl32/source/cl32_v2_keyboard.h b/Devices/cl32/source/cl32_v2_keyboard.h new file mode 100644 index 000000000..14cf8d436 --- /dev/null +++ b/Devices/cl32/source/cl32_v2_keyboard.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +/** @return true if the device was successfully constructed, added and started. */ +bool cl32_create_keyboard(struct Device* i2c0); + +void cl32_destroy_keyboard(); diff --git a/Devices/cl32/source/cl32_v3.h b/Devices/cl32/source/cl32_v3.h new file mode 100644 index 000000000..9caede11f --- /dev/null +++ b/Devices/cl32/source/cl32_v3.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include + +constexpr uint8_t CL32_V3_FUEL_GAUGE_I2C_ADDRESS = 0x36; +constexpr TickType_t CL32_V3_TIMEOUT = pdMS_TO_TICKS(50); diff --git a/Devices/cl32/source/cl32_v3_keyboard.cpp b/Devices/cl32/source/cl32_v3_keyboard.cpp new file mode 100644 index 000000000..123f78862 --- /dev/null +++ b/Devices/cl32/source/cl32_v3_keyboard.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_v3_keyboard.h" + +#include + +#include +#include +#include +#include +#include + +constexpr auto* TAG = "cl32-v3-keyboard"; + +// Reference: https://github.com/CL-32/CL-32/blob/8618d991eb6d9cf291eccb8400e94f303420cece/Software/CL-32/lib/CL32/src/CL32_keyboard.cpp + +// Keymaps and matrix layout, transcribed from CL-32/CL-32's CL32_keyboard.cpp CL32_keyboard::_matrix +// (same tca8418 chip/wiring as cl32_v2_keyboard.cpp, different physical key layout). +static constexpr uint32_t KEYMAP_LC[70] = { + CODEPOINT_TAB, 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 0, // TAB q w e r t y u i + 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 0, // SHIFT a s d f g h j k + 0, 'z', 'x', 'c', ' ', ' ', 'v', 'b', 'n', 0, // FN z x c (space)(space) v b n + '=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, 'm', 0, // = . 0 + UP RIGHT ENTER ENTER m + '3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, 'l', 0, // 3 2 1 - LEFT DOWN ESC ENTER l + '6', '5', '4', '*', 67, 68, 8, 'p', 'o', 0, // 6 5 4 * FILE MENU BKSP p o + '9', '8', '7', '/', 0, 0, 0, 0, 0, 0, // 9 8 7 / +}; +static constexpr uint32_t KEYMAP_UC[70] = { + CODEPOINT_TAB, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 0, + 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 0, + 0, 'Z', 'X', 'C', ' ', ' ', 'V', 'B', 'N', 0, + '=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, 'M', 0, + '3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, 'L', 0, + '6', '5', '4', '*', 67, 68, 8, 'P', 'O', 0, + '9', '8', '7', '/', 0, 0, 0, 0, 0, 0, +}; +static constexpr uint32_t KEYMAP_SY[70] = { + CODEPOINT_TAB, '!', '"', '@', 0xA3 /* GBP sign */, '$', '%', '^', '&', 0, + 0, '|', '#', ',', '?', '\'', ':', ';', '{', 0, + 0, ' ', '\\', '~', ' ', ' ', '<', '>', '[', 0, + '=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, ']', 0, + '3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, '}', 0, + '6', '5', '4', '*', 67, 68, 8, ')', '(', 0, + '9', '8', '7', '/', 0, 0, 0, 0, 0, 0, +}; + +static Tca8418Config cl32_v3_keyboard_config {}; +static Device cl32_v3_keyboard_device {}; +static bool cl32_v3_keyboard_created = false; + +bool cl32_v3_create_keyboard(Device* i2c0) { + cl32_v3_keyboard_config = Tca8418Config { + .address = 0x34, + .rows = 7, + .columns = 10, + .reverse_columns = false, + .keymap_lc = KEYMAP_LC, + .keymap_lc_length = sizeof(KEYMAP_LC) / sizeof(KEYMAP_LC[0]), + .keymap_uc = KEYMAP_UC, + .keymap_uc_length = sizeof(KEYMAP_UC) / sizeof(KEYMAP_UC[0]), + .keymap_sy = KEYMAP_SY, + .keymap_sy_length = sizeof(KEYMAP_SY) / sizeof(KEYMAP_SY[0]), + .shift_row = 1, + .shift_col = 0, + .sym_row = 2, + .sym_col = 0, + .pin_reset = GPIO_PIN_SPEC_NONE, + }; + + cl32_v3_keyboard_device = Device { + .address = 0, + .name = "cl32-v3-keyboard", + .config = &cl32_v3_keyboard_config, + .parent = nullptr, + .flags = 0, + .internal = nullptr, + }; + + error_t error = device_construct(&cl32_v3_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error)); + return false; + } + + device_set_parent(&cl32_v3_keyboard_device, i2c0); + + Driver* driver = driver_find_compatible("ti,tca8418"); + if (driver == nullptr) { + LOG_E(TAG, "No driver registered for ti,tca8418"); + device_destruct(&cl32_v3_keyboard_device); + return false; + } + device_set_driver(&cl32_v3_keyboard_device, driver); + + error = device_add(&cl32_v3_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error)); + device_destruct(&cl32_v3_keyboard_device); + return false; + } + + error = device_start(&cl32_v3_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error)); + device_remove(&cl32_v3_keyboard_device); + device_destruct(&cl32_v3_keyboard_device); + return false; + } + + cl32_v3_keyboard_created = true; + return true; +} + +void cl32_v3_destroy_keyboard() { + if (!cl32_v3_keyboard_created) { + return; + } + device_stop(&cl32_v3_keyboard_device); + device_remove(&cl32_v3_keyboard_device); + device_destruct(&cl32_v3_keyboard_device); + cl32_v3_keyboard_created = false; +} diff --git a/Devices/cl32/source/cl32_v3_keyboard.h b/Devices/cl32/source/cl32_v3_keyboard.h new file mode 100644 index 000000000..65444fda3 --- /dev/null +++ b/Devices/cl32/source/cl32_v3_keyboard.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +/** @return true if the device was successfully constructed, added and started. */ +bool cl32_v3_create_keyboard(struct Device* i2c0); + +void cl32_v3_destroy_keyboard(); diff --git a/Devices/cl32/source/cl32_v4.h b/Devices/cl32/source/cl32_v4.h new file mode 100644 index 000000000..d65739f33 --- /dev/null +++ b/Devices/cl32/source/cl32_v4.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include + +constexpr uint8_t CL32_V4_KEYBOARD_PROBE_REGISTER = 0x02; +constexpr uint8_t CL32_V4_CORE_I2C_ADDRESS = 0x08; +constexpr TickType_t CL32_V4_CORE_TIMEOUT = pdMS_TO_TICKS(50); diff --git a/Devices/cl32/source/cl32_v4_keyboard.cpp b/Devices/cl32/source/cl32_v4_keyboard.cpp new file mode 100644 index 000000000..6cae65b5d --- /dev/null +++ b/Devices/cl32/source/cl32_v4_keyboard.cpp @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_v4.h" +#include "cl32_v4_keyboard.h" +#include +#include +#include +#include +#include + +#include + +extern Module cl32_module; + +constexpr auto* TAG = "cl32-v4-keyboard"; + +// v4's core chip reports keyboard events on the same I2C address as the power-supply function +// (see cl32_v4_power.h) - CL-32/CL-32's CL32_core class talks to both over one address. +// Registers below come from that project's regMap.h (CL32_INTERRUPT/EVENT_COUNT/EVENT1). +constexpr uint8_t REG_INTERRUPT = 0x02; // bit0: keyboard event pending +constexpr uint8_t REG_EVENT_COUNT = 0x03; +constexpr uint8_t REG_EVENT1 = 0x04; // reading this register pops one event off the chip's FIFO +constexpr uint8_t INTERRUPT_BIT_KEYBOARD = 0x01; + +// Upper bound on events drained per read_key() call, same purpose as tca8418.cpp's equivalent: +// caps the damage if the chip ever reports a count that never reaches 0. +constexpr uint8_t MAX_EVENTS_PER_DRAIN = 16; + +// HID Keyboard/Keypad usage codes (USB HID Usage Tables page 0x07) this chip's matrix produces, +// named after CL-32/CL-32's CL32_core::_matrix keymap (CL32_core.cpp). +constexpr uint8_t KEY_FN = 0x3A; // repurposed F1 position +constexpr uint8_t KEY_FILE = 0x43; // repurposed F10 position +constexpr uint8_t KEY_MENU = 0x44; // repurposed F11 position +constexpr uint8_t KEY_ENTER = 0x28; +constexpr uint8_t KEY_ESCAPE = 0x29; +constexpr uint8_t KEY_BACKSPACE = 0x2A; +constexpr uint8_t KEY_SPACE = 0x2C; +constexpr uint8_t KEY_EQUALS = 0x2E; +constexpr uint8_t KEY_PERIOD = 0x37; +constexpr uint8_t KEY_ARROW_RIGHT = 0x4F; +constexpr uint8_t KEY_ARROW_LEFT = 0x50; +constexpr uint8_t KEY_ARROW_DOWN = 0x51; +constexpr uint8_t KEY_ARROW_UP = 0x52; +constexpr uint8_t KEYPAD_SLASH = 0x54; +constexpr uint8_t KEYPAD_ASTERISK = 0x55; +constexpr uint8_t KEYPAD_MINUS = 0x56; +constexpr uint8_t KEYPAD_PLUS = 0x57; +constexpr uint8_t KEYPAD_ENTER = 0x58; +constexpr uint8_t KEYPAD_1 = 0x59; +constexpr uint8_t KEYPAD_9 = 0x61; +constexpr uint8_t KEYPAD_0 = 0x62; +constexpr uint8_t KEYPAD_PERIOD = 0x63; +constexpr uint8_t KEY_SHIFT = 0xE1; + +enum class ToggleState { Unpressed, OnePress, Locked }; + +static ToggleState next_toggle_state(ToggleState state) { + switch (state) { + case ToggleState::Unpressed: return ToggleState::OnePress; + case ToggleState::OnePress: return ToggleState::Locked; + case ToggleState::Locked: return ToggleState::Unpressed; + } + return ToggleState::Unpressed; +} + +static const char* toggle_state_name(ToggleState state) { + switch (state) { + case ToggleState::Unpressed: return "unpressed"; + case ToggleState::OnePress: return "one-press"; + case ToggleState::Locked: return "locked"; + } + return "?"; +} + +constexpr uint8_t PENDING_CAPACITY = 32; + +struct Cl32V4KeyEvent { + uint32_t key; + bool pressed; + uint8_t hid_keycode; + uint8_t hid_modifier; +}; + +struct Cl32V4KeyboardInternal { + ToggleState shift_state = ToggleState::Unpressed; + ToggleState fn_state = ToggleState::Unpressed; + Cl32V4KeyEvent pending[PENDING_CAPACITY]; + uint8_t pending_head = 0; + uint8_t pending_count = 0; +}; + +static void push_pending(Cl32V4KeyboardInternal* internal, uint32_t key, bool pressed, uint8_t hid_keycode, uint8_t hid_modifier) { + if (internal->pending_count >= PENDING_CAPACITY) { + LOG_W(TAG, "Pending event queue full, dropping event"); + return; + } + uint8_t tail = (internal->pending_head + internal->pending_count) % PENDING_CAPACITY; + internal->pending[tail] = { key, pressed, hid_keycode, hid_modifier }; + internal->pending_count++; +} + +static bool pop_pending(Cl32V4KeyboardInternal* internal, Cl32V4KeyEvent* out_event) { + if (internal->pending_count == 0) { + return false; + } + *out_event = internal->pending[internal->pending_head]; + internal->pending_head = (internal->pending_head + 1) % PENDING_CAPACITY; + internal->pending_count--; + return true; +} + +// Maps a HID code to a Unicode codepoint, mirroring CL32_core::_matrix. Fn selects the symbol +// layer (that table's third column); Shift selects uppercase. Returns false for a code with no +// Unicode/LVGL representation (Page Up/Down) - callers report KeyboardKeyData::hid_keycode +// instead, same as tab5_keyboard.cpp's F1-F12 handling. +static bool translate_key(uint8_t code, bool shift, bool fn, uint32_t* out_key) { + if (code >= 0x04 && code <= 0x1D) { + if (fn) { + static constexpr uint32_t FN_SYMBOLS[26] = { + '|', '>', '~', ',', '@', '?', '\'', ':', '&', ';', '{', '}', ']', '[', + '(', ')', '!', 0x00A3 /* GBP sign */, '#', '$', '^', '<', '"', '\\', '%', 0, + }; + uint32_t symbol = FN_SYMBOLS[code - 0x04]; + if (symbol != 0) { + *out_key = symbol; + return true; + } + // 'z' has no symbol layer in the reference matrix - fall through to the plain letter. + } + uint32_t c = 'a' + (code - 0x04); + *out_key = shift ? (c - 0x20) : c; + return true; + } + + if (fn) { + switch (code) { + case KEY_ARROW_RIGHT: *out_key = CODEPOINT_END; return true; + case KEY_ARROW_LEFT: *out_key = CODEPOINT_HOME; return true; + case KEY_ARROW_DOWN: return false; // Page Down - hid_keycode only + case KEY_ARROW_UP: return false; // Page Up - hid_keycode only + default: break; + } + } + + switch (code) { + case KEY_ENTER: *out_key = CODEPOINT_ENTER; return true; + case KEY_ESCAPE: *out_key = CODEPOINT_ESCAPE; return true; + case KEY_BACKSPACE: *out_key = CODEPOINT_BACKSPACE; return true; + case KEY_SPACE: *out_key = ' '; return true; + case KEY_EQUALS: *out_key = '='; return true; + case KEY_PERIOD: *out_key = '.'; return true; + case KEY_ARROW_RIGHT: *out_key = CODEPOINT_ARROW_RIGHT; return true; + case KEY_ARROW_LEFT: *out_key = CODEPOINT_ARROW_LEFT; return true; + case KEY_ARROW_DOWN: *out_key = CODEPOINT_ARROW_DOWN; return true; + case KEY_ARROW_UP: *out_key = CODEPOINT_ARROW_UP; return true; + case KEYPAD_SLASH: *out_key = '/'; return true; + case KEYPAD_ASTERISK: *out_key = '*'; return true; + case KEYPAD_MINUS: *out_key = '-'; return true; + case KEYPAD_PLUS: *out_key = '+'; return true; + case KEYPAD_ENTER: *out_key = CODEPOINT_ENTER; return true; + case KEYPAD_0: *out_key = '0'; return true; + case KEYPAD_PERIOD: *out_key = '.'; return true; + default: + if (code >= KEYPAD_1 && code <= KEYPAD_9) { + *out_key = static_cast('1' + (code - KEYPAD_1)); + return true; + } + return false; + } +} + +static void handle_key_down(Cl32V4KeyboardInternal* internal, uint8_t code) { + if (code == KEY_SHIFT) { + internal->shift_state = next_toggle_state(internal->shift_state); + LOG_I(TAG, "code=0x%02X shift -> %s", code, toggle_state_name(internal->shift_state)); + return; + } + if (code == KEY_FN) { + internal->fn_state = next_toggle_state(internal->fn_state); + LOG_I(TAG, "code=0x%02X fn -> %s", code, toggle_state_name(internal->fn_state)); + return; + } + + const bool shift = internal->shift_state != ToggleState::Unpressed; + const uint8_t hid_modifier = shift ? 0x02 : 0x00; // bit1 = LeftShift + + // No app-level menu system at the driver layer - surface the raw HID code only, same as + // tab5_keyboard.cpp's F1-F12 handling. + if (code == KEY_MENU || code == KEY_FILE) { + LOG_I(TAG, "code=0x%02X (menu/file) shift=%s fn=%s hid_modifier=0x%02X", + code, toggle_state_name(internal->shift_state), toggle_state_name(internal->fn_state), hid_modifier); + push_pending(internal, 0, true, code, hid_modifier); + push_pending(internal, 0, false, code, hid_modifier); + return; + } + + const bool fn = internal->fn_state != ToggleState::Unpressed; + uint32_t key = 0; + const bool has_key = translate_key(code, shift, fn, &key); + + const char printable = (has_key && key >= 0x20 && key < 0x7F) ? static_cast(key) : '.'; + LOG_I(TAG, "code=0x%02X key='%c' key_hex=0x%04X shift=%s fn=%s hid_modifier=0x%02X", + code, printable, static_cast(has_key ? key : 0), + toggle_state_name(internal->shift_state), toggle_state_name(internal->fn_state), hid_modifier); + + // LVGL only registers a key on a RELEASED->PRESSED edge, and this chip doesn't report + // reliable release events for character keys - queue an immediate press+release pair, same as + // tca8418.cpp. + push_pending(internal, has_key ? key : 0, true, code, hid_modifier); + push_pending(internal, has_key ? key : 0, false, code, hid_modifier); + + if (internal->shift_state == ToggleState::OnePress) { + internal->shift_state = ToggleState::Unpressed; + } + if (internal->fn_state == ToggleState::OnePress) { + internal->fn_state = ToggleState::Unpressed; + } +} + +static void drain_events(Device* i2c0, Cl32V4KeyboardInternal* internal) { + uint8_t interrupt_status = 0; + if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_INTERRUPT, &interrupt_status, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) { + return; + } + if ((interrupt_status & INTERRUPT_BIT_KEYBOARD) == 0) { + return; + } + + uint8_t count = 0; + if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT_COUNT, &count, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) { + return; + } + + for (uint8_t drained = 0; drained < MAX_EVENTS_PER_DRAIN && count > 0; drained++) { + uint8_t raw = 0; + if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT1, &raw, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) { + break; + } + + LOG_I(TAG, "raw event=0x%02X (%s)", raw, (raw & 0x80) != 0 ? "down" : "up"); + + if ((raw & 0x80) != 0) { + handle_key_down(internal, static_cast(raw & 0x7F)); + } + + if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT_COUNT, &count, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) { + break; + } + } + + // Only the keyboard bit is ours to clear - the power-supply function on this same chip owns + // the other status bits in this register. + i2c_controller_register8_reset_bits(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_INTERRUPT, INTERRUPT_BIT_KEYBOARD, CL32_V4_CORE_TIMEOUT); +} + +static error_t v3_read_key(Device* device, KeyboardKeyData* data) { + auto* internal = static_cast(device_get_driver_data(device)); + + Cl32V4KeyEvent event; + if (internal->pending_count == 0) { + drain_events(device_get_parent(device), internal); + } + + if (pop_pending(internal, &event)) { + data->key = event.key; + data->pressed = event.pressed; + data->continue_reading = internal->pending_count > 0; + data->ctrl = false; + data->alt = false; + data->hid_keycode = event.hid_keycode; + data->hid_modifier = event.hid_modifier; + } else { + data->key = 0; + data->pressed = false; + data->continue_reading = false; + data->ctrl = false; + data->alt = false; + data->hid_keycode = 0; + data->hid_modifier = 0; + } + + return ERROR_NONE; +} + +static constexpr KeyboardApi cl32_v4_keyboard_api = { + .read_key = v3_read_key, + .get_backlight = nullptr, + .is_present = nullptr, +}; + +static error_t start(Device* device) { + if (device_get_type(device_get_parent(device)) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* internal = new(std::nothrow) Cl32V4KeyboardInternal(); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + delete internal; + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +Driver cl32_v4_keyboard_driver = { + .name = "cl32-v4-keyboard", + .compatible = (const char*[]) { "cl32-v4-keyboard", nullptr }, + .start_device = start, + .stop_device = stop, + .api = &cl32_v4_keyboard_api, + .device_type = &KEYBOARD_TYPE, + .owner = &cl32_module, + .internal = nullptr +}; + +static Device cl32_v4_keyboard_device {}; +static bool cl32_v4_keyboard_created = false; + +bool cl32_v4_create_keyboard(Device* i2c0) { + cl32_v4_keyboard_device = Device { .address = 0, .name = "cl32-v4-keyboard", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr }; + + error_t error = device_construct(&cl32_v4_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error)); + return false; + } + + device_set_parent(&cl32_v4_keyboard_device, i2c0); + device_set_driver(&cl32_v4_keyboard_device, &cl32_v4_keyboard_driver); + + error = device_add(&cl32_v4_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error)); + device_destruct(&cl32_v4_keyboard_device); + return false; + } + + error = device_start(&cl32_v4_keyboard_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error)); + device_remove(&cl32_v4_keyboard_device); + device_destruct(&cl32_v4_keyboard_device); + return false; + } + + cl32_v4_keyboard_created = true; + return true; +} + +void cl32_v4_destroy_keyboard() { + if (!cl32_v4_keyboard_created) { + return; + } + device_stop(&cl32_v4_keyboard_device); + device_remove(&cl32_v4_keyboard_device); + device_destruct(&cl32_v4_keyboard_device); + cl32_v4_keyboard_created = false; +} diff --git a/Devices/cl32/source/cl32_v4_keyboard.h b/Devices/cl32/source/cl32_v4_keyboard.h new file mode 100644 index 000000000..5656342ea --- /dev/null +++ b/Devices/cl32/source/cl32_v4_keyboard.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +extern struct Driver cl32_v4_keyboard_driver; + +/** @return true if the device was successfully constructed, added and started. */ +bool cl32_v4_create_keyboard(struct Device* i2c0); + +void cl32_v4_destroy_keyboard(); diff --git a/Devices/cl32/source/cl32_v4_power.cpp b/Devices/cl32/source/cl32_v4_power.cpp new file mode 100644 index 000000000..8e7439bf8 --- /dev/null +++ b/Devices/cl32/source/cl32_v4_power.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_v4_power.h" +#include "cl32_v4.h" + +#include +#include +#include +#include +#include +#include + +#include + +extern Module cl32_module; + +constexpr auto* TAG = "cl32-v4-power"; + +constexpr uint8_t REG_VOLTAGE = 0x14; +constexpr int MV_PER_LSB = 25; +constexpr int MIN_MV = 3200; +constexpr int MAX_MV = 4200; + +// --------------------------------------------------------------------------- +// Power supply +// --------------------------------------------------------------------------- + +static bool ps_supports_property(Device*, PowerSupplyProperty property) { + return property == POWER_SUPPLY_PROP_VOLTAGE || property == POWER_SUPPLY_PROP_CAPACITY; +} + +static error_t read_battery_mv(Device* chip_device, int* out_mv) { + auto* i2c0 = device_get_parent(chip_device); + uint8_t raw = 0; + error_t error = i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_VOLTAGE, &raw, CL32_V4_CORE_TIMEOUT); + if (error != ERROR_NONE) { + return error; + } + *out_mv = raw * MV_PER_LSB; + return ERROR_NONE; +} + +static int estimate_capacity_from_mv(int battery_mv) { + if (battery_mv <= MIN_MV) return 0; + if (battery_mv >= MAX_MV) return 100; + return (battery_mv - MIN_MV) * 100 / (MAX_MV - MIN_MV); +} + +static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) { + if (property != POWER_SUPPLY_PROP_VOLTAGE && property != POWER_SUPPLY_PROP_CAPACITY) { + return ERROR_NOT_SUPPORTED; + } + + int battery_mv; + error_t error = read_battery_mv(device_get_parent(device), &battery_mv); + if (error != ERROR_NONE) { + return error; + } + + out_value->int_value = (property == POWER_SUPPLY_PROP_VOLTAGE) ? battery_mv : estimate_capacity_from_mv(battery_mv); + return ERROR_NONE; +} + +static bool ps_supports_charge_control(Device*) { return false; } +static bool ps_is_allowed_to_charge(Device*) { return false; } +static error_t ps_set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_quick_charge(Device*) { return false; } +static bool ps_is_quick_charge_enabled(Device*) { return false; } +static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; } +static bool ps_supports_power_off(Device*) { return false; } +static error_t ps_power_off(Device*) { return ERROR_NOT_SUPPORTED; } + +static constexpr PowerSupplyApi CL32_V4_POWER_SUPPLY_API = { + .supports_property = ps_supports_property, + .get_property = ps_get_property, + .supports_charge_control = ps_supports_charge_control, + .is_allowed_to_charge = ps_is_allowed_to_charge, + .set_allowed_to_charge = ps_set_allowed_to_charge, + .supports_quick_charge = ps_supports_quick_charge, + .is_quick_charge_enabled = ps_is_quick_charge_enabled, + .set_quick_charge_enabled = ps_set_quick_charge_enabled, + .supports_power_off = ps_supports_power_off, + .power_off = ps_power_off, +}; + +// Registered in cl32_module's driver list so driver_bind() has a valid ->internal, but never +// matched against a devicetree node: wired up directly by pointer from cl32_v4_power_driver's start(). +Driver cl32_v4_power_supply_driver = { + .name = "cl32-v4-power-supply", + .compatible = (const char*[]) { "cl32-v4-power-supply", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &CL32_V4_POWER_SUPPLY_API, + .device_type = &POWER_SUPPLY_TYPE, + .owner = &cl32_module, + .internal = nullptr +}; + +static error_t create_power_supply_child(Device* parent, Device*& out_child) { + auto* child = new(std::nothrow) Device { .address = 0, .name = "cl32-v4-power-supply", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr }; + if (child == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = device_construct(child); + if (error != ERROR_NONE) { + delete child; + return error; + } + + device_set_parent(child, parent); + device_set_driver(child, &cl32_v4_power_supply_driver); + + error = device_add(child); + if (error != ERROR_NONE) { + device_destruct(child); + delete child; + return error; + } + + error = device_start(child); + if (error != ERROR_NONE) { + device_remove(child); + device_destruct(child); + delete child; + return error; + } + + out_child = child; + return ERROR_NONE; +} + +static void destroy_power_supply_child(Device* child) { + check(device_stop(child) == ERROR_NONE); + check(device_remove(child) == ERROR_NONE); + check(device_destruct(child) == ERROR_NONE); + delete child; +} + +// --------------------------------------------------------------------------- +// Chip driver +// --------------------------------------------------------------------------- + +struct Cl32V4PowerInternal { + Device* power_supply_device = nullptr; +}; + +static error_t start(Device* device) { + if (device_get_type(device_get_parent(device)) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* internal = new(std::nothrow) Cl32V4PowerInternal(); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + error_t error = create_power_supply_child(device, internal->power_supply_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to create power-supply device"); + delete internal; + return error; + } + + device_set_driver_data(device, internal); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + destroy_power_supply_child(internal->power_supply_device); + device_set_driver_data(device, nullptr); + delete internal; + return ERROR_NONE; +} + +Driver cl32_v4_power_driver = { + .name = "cl32-v4-power", + .compatible = (const char*[]) { "cl32-v4-power", nullptr }, + .start_device = start, + .stop_device = stop, + .api = nullptr, + .device_type = nullptr, + .owner = &cl32_module, + .internal = nullptr +}; diff --git a/Devices/cl32/source/cl32_v4_power.h b/Devices/cl32/source/cl32_v4_power.h new file mode 100644 index 000000000..d5c00af36 --- /dev/null +++ b/Devices/cl32/source/cl32_v4_power.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +extern struct Driver cl32_v4_power_driver; +extern struct Driver cl32_v4_power_supply_driver; diff --git a/Devices/cl32/source/module.cpp b/Devices/cl32/source/module.cpp index 5b88b4dd5..89ace18a0 100644 --- a/Devices/cl32/source/module.cpp +++ b/Devices/cl32/source/module.cpp @@ -1,9 +1,36 @@ #include +#include "cl32_detect.h" +#include "cl32_v4_keyboard.h" +#include "cl32_v4_power.h" + extern "C" { -struct Module cl32_module = { - .name = "cl32" +static error_t start() { + cl32_power_detect_start(); + return ERROR_NONE; +} + +static error_t stop() { + cl32_power_detect_stop(); + cl32_teardown_devices(); + return ERROR_NONE; +} + +static Driver* const cl32_drivers[] = { + &cl32_v4_power_driver, + &cl32_v4_power_supply_driver, + &cl32_v4_keyboard_driver, + nullptr +}; + +Module cl32_module = { + .name = "cl32", + .start = start, + .stop = stop, + .drivers = cl32_drivers, + .symbols = nullptr, + .internal = nullptr }; } diff --git a/Devices/generic-esp32p4/generic,esp32p4.dts b/Devices/generic-esp32p4/generic,esp32p4.dts index 93d6280a9..457c1a373 100644 --- a/Devices/generic-esp32p4/generic,esp32p4.dts +++ b/Devices/generic-esp32p4/generic,esp32p4.dts @@ -9,6 +9,6 @@ gpio0 { compatible = "espressif,esp32-gpio"; - gpio-count = <57>; + gpio-count = <55>; }; }; diff --git a/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts b/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts index df86cdc79..6110b9047 100644 --- a/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts +++ b/Devices/guition-jc1060p470ciwy/guition,jc1060p470ciwy.dts @@ -38,7 +38,7 @@ gpio0 { compatible = "espressif,esp32-gpio"; - gpio-count = <57>; + gpio-count = <55>; }; i2c_internal { diff --git a/Devices/heltec-wifi-lora-32-v3/device.properties b/Devices/heltec-wifi-lora-32-v3/device.properties index aa55fb021..54e9cb053 100644 --- a/Devices/heltec-wifi-lora-32-v3/device.properties +++ b/Devices/heltec-wifi-lora-32-v3/device.properties @@ -22,5 +22,4 @@ cdn.infoMessage=Due to the small size of the screen, the icons don't render prop lvgl.theme=Mono lvgl.colorDepth=16 -lvgl.uiScale=70 lvgl.uiDensity=compact diff --git a/Devices/lilygo-tdeck-max/device.properties b/Devices/lilygo-tdeck-max/device.properties index 2df9188c0..20462171b 100644 --- a/Devices/lilygo-tdeck-max/device.properties +++ b/Devices/lilygo-tdeck-max/device.properties @@ -26,6 +26,7 @@ lvgl.colorDepth=8 # Monochrome theme: the default colour theme renders accent-coloured UI that # thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO). lvgl.theme=Mono +lvgl.statusbarColorsInverted=true cdn.warningMessage=Display not reliably working. Use at your own risk. diff --git a/Devices/lilygo-tdeck-pro/device.properties b/Devices/lilygo-tdeck-pro/device.properties index f7d383006..7ac27e12a 100644 --- a/Devices/lilygo-tdeck-pro/device.properties +++ b/Devices/lilygo-tdeck-pro/device.properties @@ -25,6 +25,7 @@ lvgl.colorDepth=8 # Monochrome theme: the default colour theme renders accent-coloured UI that # thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO). lvgl.theme=Mono +lvgl.statusbarColorsInverted=true cdn.warningMessage=Only for hardware revision 1.0! Display not reliably working. Use at your own risk. diff --git a/Devices/m5stack-papers3/device.properties b/Devices/m5stack-papers3/device.properties index 66f6a4035..69b644d3f 100644 --- a/Devices/m5stack-papers3/device.properties +++ b/Devices/m5stack-papers3/device.properties @@ -22,6 +22,7 @@ display.dpi=235 lvgl.colorDepth=8 lvgl.fontSize=24 lvgl.theme=Mono +lvgl.statusbarColorsInverted=true sdkconfig.CONFIG_EPD_DISPLAY_TYPE_ED047TC2=y sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0 \ No newline at end of file diff --git a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp index 5ba6025f3..b7d50b34a 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp @@ -54,6 +54,7 @@ static void create_gt911_touch(Device* i2c0) { .name = "touch0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; @@ -86,6 +87,7 @@ void tab5_create_devices_v1(Device* i2c0) { .name = "display0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/Source/devices/devices_v2.cpp b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp index 7c60a9bec..0db9ce0ab 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v2.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp @@ -25,6 +25,7 @@ void tab5_create_devices_v2(Device* i2c0) { .name = "display0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/Source/devices/devices_v2_v3_touch.cpp b/Devices/m5stack-tab5/Source/devices/devices_v2_v3_touch.cpp index 80c9d892d..e4fb212d4 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v2_v3_touch.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v2_v3_touch.cpp @@ -19,6 +19,7 @@ void create_st7123_touch(Device* i2c0) { .name = "touch0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/Source/devices/devices_v3.cpp b/Devices/m5stack-tab5/Source/devices/devices_v3.cpp index 108aa8e6b..cc4267b0b 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v3.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v3.cpp @@ -27,6 +27,7 @@ void tab5_create_devices_v3(Device* i2c0) { .name = "display0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/Source/devices/display_detect.cpp b/Devices/m5stack-tab5/Source/devices/display_detect.cpp index 282854b38..665ea09b5 100644 --- a/Devices/m5stack-tab5/Source/devices/display_detect.cpp +++ b/Devices/m5stack-tab5/Source/devices/display_detect.cpp @@ -25,6 +25,7 @@ static void tab5_create_power_control(Device* io_expander1) { .name = "power_control0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index a478e2999..09aee53b5 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -658,6 +658,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) { static const KeyboardApi tab5_keyboard_api = { .read_key = tab5_keyboard_read_key, + .get_backlight = nullptr, .is_present = tab5_keyboard_is_attached, }; @@ -693,6 +694,7 @@ void tab5_create_keyboard(Device* i2c2) { .name = "keyboard0", .config = nullptr, .parent = nullptr, + .flags = {}, .internal = nullptr, }; diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 644d2fb1b..ce78f0c5a 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -37,7 +37,7 @@ gpio0 { compatible = "espressif,esp32-gpio"; - gpio-count = <57>; + gpio-count = <55>; }; display_backlight_pwm { diff --git a/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml index 8b44b5e4a..d8b30bce3 100644 --- a/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml +++ b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml @@ -63,3 +63,13 @@ properties: so LVGL renders in rotated space and the driver rotates the 1bpp frame back to the panel's native layout before updating. Not changeable at runtime. + mirror-x: + type: boolean + default: false + description: > + Mirror the panel horizontally. Applied once at start via the underlying + esp_epaper component's epd_init(); not changeable at runtime. + mirror-y: + type: boolean + default: false + description: Mirror the panel vertically. Same fixed-at-start behavior as mirror-x. diff --git a/Drivers/esp-epaper-module/include/drivers/esp_epaper.h b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h index cba3a7706..f336f6f7c 100644 --- a/Drivers/esp-epaper-module/include/drivers/esp_epaper.h +++ b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h @@ -5,6 +5,7 @@ extern "C" { #endif +#include #include #include @@ -37,6 +38,11 @@ struct EspEpaperConfig { * counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime. */ uint8_t rotation; + /** Mirror the panel horizontally. Fixed at start; the underlying esp_epaper component applies + * it at epd_init() time with no runtime setter. */ + bool mirror_x; + /** Mirror the panel vertically. Fixed at start, same as mirror_x. */ + bool mirror_y; }; #ifdef __cplusplus diff --git a/Drivers/esp-epaper-module/source/esp_epaper.cpp b/Drivers/esp-epaper-module/source/esp_epaper.cpp index 7b2168887..05aa01dfd 100644 --- a/Drivers/esp-epaper-module/source/esp_epaper.cpp +++ b/Drivers/esp-epaper-module/source/esp_epaper.cpp @@ -33,6 +33,9 @@ struct EspEpaperInternal { epd_panel_info_t info; /** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */ uint8_t* rotate_buffer; + /** Copy of the last frame sent to epd_update(), in native panel layout - refresh() resends + * this at EPD_UPDATE_FULL to clear ghosting without new content from LVGL. */ + uint8_t* last_frame_buffer; /** Serializes panel/SPI access between draw_bitmap and power state changes. */ SemaphoreHandle_t panel_mutex; /** disp_on_off state; the panel is in deep sleep while false. */ @@ -138,6 +141,8 @@ static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y source = internal->rotate_buffer; } + memcpy(internal->last_frame_buffer, source, internal->info.buffer_size); + spi_controller_lock(internal->spi_controller); const esp_err_t ret = epd_update(internal->epd, source, config->update_mode); spi_controller_unlock(internal->spi_controller); @@ -149,6 +154,58 @@ static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y return ERROR_NONE; } +// Resends the last drawn frame at EPD_UPDATE_FULL (ignoring config->update_mode) to clear +// ghosting left by earlier fast/partial draw_bitmap() calls, without changing visible content. +// Called periodically by lvgl-module's idle-repaint mechanism for displays with +// DISPLAY_CAPABILITY_SLOW_REFRESH (see Modules/lvgl-module/source/devices/devices.cpp). +static error_t esp_epaper_refresh(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + if (!internal->display_on) { + xSemaphoreGive(internal->panel_mutex); + return ERROR_NONE; + } + + spi_controller_lock(internal->spi_controller); + const esp_err_t ret = epd_update(internal->epd, internal->last_frame_buffer, EPD_UPDATE_FULL); + spi_controller_unlock(internal->spi_controller); + xSemaphoreGive(internal->panel_mutex); + if (ret != ESP_OK) { + LOG_E(TAG, "epd_update (refresh) failed: %s", esp_err_to_name(ret)); + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +// White-fills the panel with a full-quality pass, via the library's epd_clear()/epd_fill() +// (fills its own internal framebuffer with 0xFF, then epd_update()s it at EPD_UPDATE_FULL). +// Keeps last_frame_buffer in sync so a later refresh() resends white rather than stale content. +static error_t esp_epaper_clear(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + + xSemaphoreTake(internal->panel_mutex, portMAX_DELAY); + if (!internal->display_on) { + xSemaphoreGive(internal->panel_mutex); + return ERROR_NONE; + } + + spi_controller_lock(internal->spi_controller); + const esp_err_t ret = epd_clear(internal->epd); + spi_controller_unlock(internal->spi_controller); + + if (ret == ESP_OK) { + memset(internal->last_frame_buffer, 0xFF, internal->info.buffer_size); + } + xSemaphoreGive(internal->panel_mutex); + + if (ret != ESP_OK) { + LOG_E(TAG, "epd_clear failed: %s", esp_err_to_name(ret)); + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + static error_t esp_epaper_disp_on_off(Device* device, bool on_off) { auto* internal = static_cast(device_get_driver_data(device)); @@ -207,8 +264,8 @@ static const DisplayApi esp_epaper_display_api = { .reset = esp_epaper_reset, .init = esp_epaper_init, .draw_bitmap = esp_epaper_draw_bitmap, - .clear = nullptr, - .refresh = nullptr, + .clear = esp_epaper_clear, + .refresh = esp_epaper_refresh, .mirror = nullptr, .swap_xy = nullptr, .get_swap_xy = nullptr, @@ -238,6 +295,9 @@ static void free_internal(EspEpaperInternal* internal) { if (internal->rotate_buffer != nullptr) { free(internal->rotate_buffer); } + if (internal->last_frame_buffer != nullptr) { + free(internal->last_frame_buffer); + } if (internal->panel_mutex != nullptr) { vSemaphoreDelete(internal->panel_mutex); } @@ -284,6 +344,8 @@ static error_t start(Device* device) { epd_config.panel.type = panel_type; epd_config.panel.width = config->width; epd_config.panel.height = config->height; + epd_config.panel.mirror_x = config->mirror_x; + epd_config.panel.mirror_y = config->mirror_y; epd_handle_t epd = nullptr; if (epd_init(&epd_config, &epd) != ESP_OK) { @@ -332,6 +394,16 @@ static error_t start(Device* device) { } } + // White-initialized (0xFF - see draw_bitmap()'s bit-polarity comment) so a refresh() before + // the first draw_bitmap() is a harmless all-white pass rather than all-black. + internal->last_frame_buffer = static_cast(malloc(internal->info.buffer_size)); + if (internal->last_frame_buffer == nullptr) { + LOG_E(TAG, "Failed to allocate %lu-byte last-frame buffer", internal->info.buffer_size); + free_internal(internal); + return ERROR_OUT_OF_MEMORY; + } + memset(internal->last_frame_buffer, 0xFF, internal->info.buffer_size); + internal->display_on = true; device_set_driver_data(device, internal); diff --git a/Drivers/gt911-module/source/gt911.cpp b/Drivers/gt911-module/source/gt911.cpp index d2254f906..621e44694 100644 --- a/Drivers/gt911-module/source/gt911.cpp +++ b/Drivers/gt911-module/source/gt911.cpp @@ -74,7 +74,23 @@ static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) { // GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not // devicetree-fixed), so both known addresses are probed regardless of the devicetree "reg" hint. static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) { - esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); + // Amended copy of ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG() to avoid compiler warnings + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, + .scl_speed_hz = 100000, + .control_phase_bytes = 1, + .dc_bit_offset = 0, + .lcd_cmd_bits = 16, + .lcd_param_bits = 0, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 1, + }, + .transaction_timeout_ms = 0, + }; + if (i2c_controller_has_device_at_address(parent, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, pdMS_TO_TICKS(10)) == ERROR_NONE) { io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS; diff --git a/Drivers/tca8418-module/bindings/ti,tca8418.yaml b/Drivers/tca8418-module/bindings/ti,tca8418.yaml index a6b217cef..e72eec11f 100644 --- a/Drivers/tca8418-module/bindings/ti,tca8418.yaml +++ b/Drivers/tca8418-module/bindings/ti,tca8418.yaml @@ -28,23 +28,24 @@ properties: before comparing against shift-row/shift-col/sym-row/sym-col. keymap-lc: type: array - element-type: uint8_t + element-type: uint32_t required: true description: > - Base (lowercase) layer keymap, rows*columns bytes in row-major order (already in + Base (lowercase) layer keymap, rows*columns values in row-major order (already in silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g. a blank matrix position, or a position handled as a modifier via shift-row/shift-col/ - sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (a Unicode - codepoint - byte range covers Latin-1 - for character and non-character keys). + sym-row/sym-col instead). Non-zero values are sent as-is via KeyboardKeyData::key (any + Unicode codepoint, including the CodePoint enum's non-character values) for character and + non-character keys. keymap-uc: type: array - element-type: uint8_t + element-type: uint32_t description: > Uppercase/shift layer keymap, same shape as keymap-lc. Omit if this keyboard has no separate uppercase layer (shift-row/shift-col should then also be left unset). keymap-sy: type: array - element-type: uint8_t + element-type: uint32_t description: > Symbol layer keymap, same shape as keymap-lc. Omit if this keyboard has no symbol layer (sym-row/sym-col should then also be left unset). diff --git a/Drivers/tca8418-module/include/drivers/tca8418.h b/Drivers/tca8418-module/include/drivers/tca8418.h index c1e343111..a5925f45d 100644 --- a/Drivers/tca8418-module/include/drivers/tca8418.h +++ b/Drivers/tca8418-module/include/drivers/tca8418.h @@ -16,11 +16,11 @@ struct Tca8418Config { uint8_t rows; uint8_t columns; bool reverse_columns; - const uint8_t* keymap_lc; + const uint32_t* keymap_lc; uint32_t keymap_lc_length; - const uint8_t* keymap_uc; + const uint32_t* keymap_uc; uint32_t keymap_uc_length; - const uint8_t* keymap_sy; + const uint32_t* keymap_sy; uint32_t keymap_sy_length; uint8_t shift_row; uint8_t shift_col; diff --git a/Drivers/tca8418-module/source/tca8418.cpp b/Drivers/tca8418-module/source/tca8418.cpp index cab59dc4e..55db1729d 100644 --- a/Drivers/tca8418-module/source/tca8418.cpp +++ b/Drivers/tca8418-module/source/tca8418.cpp @@ -177,7 +177,7 @@ static error_t stop(Device* device) { // region KeyboardApi -static uint8_t keymap_lookup(const uint8_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) { +static uint32_t keymap_lookup(const uint32_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) { uint32_t index = static_cast(row) * columns + vcol; if (keymap == nullptr || index >= keymap_length) { return 0; @@ -209,7 +209,7 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con } else if (is_sym) { internal->sym_pressed = true; } else { - const uint8_t* keymap = config->keymap_lc; + const uint32_t* keymap = config->keymap_lc; uint32_t keymap_length = config->keymap_lc_length; if (internal->sym_pressed) { keymap = config->keymap_sy; @@ -218,8 +218,12 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con keymap = config->keymap_uc; keymap_length = config->keymap_uc_length; } - uint8_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns); + uint32_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns); if (chr != 0) { + uint32_t index = static_cast(row) * config->columns + vcol; + LOG_D(TAG, "key index=%u value=0x%08X ('%c')", index, static_cast(chr), + (chr >= 0x20 && chr < 0x7F) ? static_cast(chr) : '?'); + // LVGL only registers a key on a RELEASED->PRESSED edge, so every keystroke is // queued as an immediate press+release pair. push_pending(internal, chr, true); @@ -275,6 +279,8 @@ static error_t tca8418_read_key(Device* device, KeyboardKeyData* data) { static constexpr KeyboardApi tca8418_api = { .read_key = tca8418_read_key, + .get_backlight = nullptr, + .is_present = nullptr, }; Driver tca8418_driver = { diff --git a/Libraries/esp_epaper b/Libraries/esp_epaper deleted file mode 160000 index 0bf4f144b..000000000 --- a/Libraries/esp_epaper +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bf4f144b543048f10bdadd49f0578858bdc2a39 diff --git a/Libraries/esp_epaper/.gitignore b/Libraries/esp_epaper/.gitignore new file mode 100644 index 000000000..2c16a99fb --- /dev/null +++ b/Libraries/esp_epaper/.gitignore @@ -0,0 +1,137 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### C++ ### +# Prerequisites + +# Compiled Object files +*.slo + +# Precompiled Headers + +# Compiled Dynamic libraries + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai + +# Executables + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,c,c++ + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +build/ +sdkconfig +sdkconfig.old +managed_components/ +ref/ +dist/ \ No newline at end of file diff --git a/Libraries/esp_epaper/ADDING_PANELS.md b/Libraries/esp_epaper/ADDING_PANELS.md new file mode 100644 index 000000000..6a75ca904 --- /dev/null +++ b/Libraries/esp_epaper/ADDING_PANELS.md @@ -0,0 +1,519 @@ +# Adding a New Panel + +This guide explains how to add support for a new e-paper panel using the data-driven architecture. + +## Architecture Overview + +``` +epaper_config.h epaper_registry.c controllers/ ++----------------+ +-------------------+ +----------------+ +| EPD_PANEL_xxx | ----> | panel_registry[] | ---> | ssd16xx.c | +| (enum) | | - name | | gdey0154_lut.c | ++----------------+ | - width/height | | acep_6color.c | + | - color_mode | | bwry_4color.c | + | - bits_per_pixel | +----------------+ + | - caps | + | - ctrl (enum) | ---> controller_ops[] + +-------------------+ (vtable lookup) +``` + +## Quick Start: Same Controller, Different Size + +**For BW panels using existing SSD16xx controller** (covers 90% of GoodDisplay panels), adding support requires only **1 line of code**: + +### Step 1: Add Panel Type Enum + +In `include/epaper_config.h`: + +```c +typedef enum { + // ... existing panels ... + EPD_PANEL_SSD16XX_290, // 2.9" BW 128x296 + EPD_PANEL_SSD16XX_YOUR_NEW, // <-- Add here + EPD_PANEL_SSD16XX_420, // 4.2" BW 400x300 + + EPD_PANEL_COUNT, // Must be BEFORE aliases! +} epd_panel_type_t; +``` + +### Step 2: Add to Panel Registry + +In `src/epaper_registry.c`, add one line to `panel_registry[]`: + +```c +static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = { + // ... existing panels ... + + [EPD_PANEL_SSD16XX_YOUR_NEW] = { + .name = "SSD16xx_YOUR", + .width = YOUR_WIDTH, + .height = YOUR_HEIGHT, + .color_mode = EPD_COLOR_BW, + .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, // Reuse existing controller + }, +}; +``` + +**That's it!** The existing `EPD_CTRL_SSD16XX` controller handles everything. + +### Currently Supported Generic Panels + +| Panel Type | Size | Resolution | Notes | +|------------|------|------------|-------| +| `EPD_PANEL_SSD16XX_154` | 1.54" | 200x200 | GDEM0154I61, generic 1.54" | +| `EPD_PANEL_SSD16XX_213` | 2.13" | 122x250 | GDEY0213B74, GDEM0213I61 | +| `EPD_PANEL_SSD16XX_266` | 2.66" | 152x296 | GDEY0266T90, GDEY0266T90H | +| `EPD_PANEL_SSD16XX_270` | 2.7" | 176x264 | GDEY027T91, GDEM027Q72 | +| `EPD_PANEL_SSD16XX_290` | 2.9" | 128x296 | GDEY029T94, GDEY029T71H | +| `EPD_PANEL_SSD16XX_370` | 3.7" | 280x480 | GDEY037T03 | +| `EPD_PANEL_SSD16XX_420` | 4.2" | 400x300 | GDEY042T81, GDEQ0426T82 | + +--- + +## Adding a New Controller + +For panels with **custom LUT tables**, **different controllers**, or **special features**, you need to add a new controller. + +### Step 1: Create Controller File + +Create `src/controllers/your_controller.c`: + +```c +/** + * @file your_controller.c + * @brief Driver for YOUR_CONTROLLER e-paper displays + */ + +#include "../epaper_common.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include + +static const char *TAG = "your_ctrl"; + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t your_controller_init(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + uint16_t w = epd_get_width(dev); + uint16_t h = epd_get_height(dev); + + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(50)); + WAIT(dev); // Uses polarity from panel caps + + CMD(spi, 0x12); // SWRESET + WAIT(dev); + + CMD(spi, 0x01); // Driver output control + DATA(spi, (h - 1) & 0xFF); + DATA(spi, ((h - 1) >> 8) & 0xFF); + DATA(spi, 0x00); + + CMD(spi, 0x11); // Data entry mode + DATA(spi, 0x03); + + // Set RAM address + CMD(spi, 0x44); + DATA(spi, 0x00); + DATA(spi, (w / 8) - 1); + + CMD(spi, 0x45); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, (h - 1) & 0xFF); + DATA(spi, ((h - 1) >> 8) & 0xFF); + + WAIT(dev); + + ESP_LOGI(TAG, "Panel initialized: %dx%d", w, h); + return ESP_OK; +} + +/*============================================================================= + * Update Functions + *============================================================================*/ + +esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode) +{ + epd_spi_t *spi = epd_get_spi(dev); + + switch (mode) { + case EPD_UPDATE_PARTIAL: + CMD(spi, 0x22); + DATA(spi, 0xFF); // Partial update sequence + break; + case EPD_UPDATE_FAST: + CMD(spi, 0x22); + DATA(spi, 0xC7); // Fast update + break; + case EPD_UPDATE_FULL: + default: + CMD(spi, 0x22); + DATA(spi, 0xF7); // Full update + break; + } + + CMD(spi, 0x20); // Master activation + WAIT(dev); + + return ESP_OK; +} + +/*============================================================================= + * RAM Write Functions + *============================================================================*/ + +esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + epd_spi_t *spi = epd_get_spi(dev); + bool partial_ready = epd_is_partial_ready(dev); + + // Reset RAM address + CMD(spi, 0x4E); + DATA(spi, 0x00); + CMD(spi, 0x4F); + DATA(spi, 0x00); + DATA(spi, 0x00); + + // Write to current RAM (0x24) + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, len); + + // Only write to base RAM (0x26) when setting up partial refresh + if (!partial_ready) { + CMD(spi, 0x4E); + DATA(spi, 0x00); + CMD(spi, 0x4F); + DATA(spi, 0x00); + DATA(spi, 0x00); + + CMD(spi, 0x26); + epd_spi_write_data_bulk(spi, data, len); + } + + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t your_controller_sleep(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x10); // Deep sleep + DATA(spi, 0x01); + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_LOGI(TAG, "Panel in deep sleep"); + return ESP_OK; +} + +esp_err_t your_controller_wake(epd_device_t *dev) +{ + return your_controller_init(dev); +} +``` + +### Step 2: Add Controller Type Enum + +In `include/epaper_panel.h`: + +```c +typedef enum { + EPD_CTRL_SSD16XX, + EPD_CTRL_GDEY0154_LUT, + EPD_CTRL_ACEP_6COLOR, + EPD_CTRL_BWRY_4COLOR, + EPD_CTRL_YOUR_CONTROLLER, // <-- Add here + EPD_CTRL_COUNT +} epd_controller_type_t; +``` + +### Step 3: Register Controller Operations + +In `src/epaper_registry.c`: + +```c +// Declare functions +extern esp_err_t your_controller_init(epd_device_t *dev); +extern esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode); +extern esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len); +extern esp_err_t your_controller_sleep(epd_device_t *dev); +extern esp_err_t your_controller_wake(epd_device_t *dev); + +// Add to controller_ops array +static const epd_controller_ops_t controller_ops[EPD_CTRL_COUNT] = { + // ... existing controllers ... + + [EPD_CTRL_YOUR_CONTROLLER] = { + .init = your_controller_init, + .update = your_controller_update, + .write_ram = your_controller_write_ram, + .write_ram_partial = NULL, // Optional + .sleep = your_controller_sleep, + .wake = your_controller_wake, + }, +}; +``` + +### Step 4: Add Panel to Registry + +In `src/epaper_registry.c`: + +```c +static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = { + // ... existing panels ... + + [EPD_PANEL_YOUR_PANEL] = { + .name = "YOUR_PANEL", + .width = 200, + .height = 200, + .color_mode = EPD_COLOR_BW, + .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_YOUR_CONTROLLER, + }, +}; +``` + +### Step 5: Update CMakeLists.txt + +```cmake +idf_component_register( + SRCS + "src/epaper.c" + "src/epaper_spi.c" + "src/epaper_lvgl.c" + "src/epaper_common.c" + "src/epaper_registry.c" + "src/controllers/ssd16xx.c" + "src/controllers/gdey0154_lut.c" + "src/controllers/acep_6color.c" + "src/controllers/bwry_4color.c" + "src/controllers/your_controller.c" # <-- Add here + INCLUDE_DIRS "include" + REQUIRES driver esp_driver_gpio esp_driver_spi esp_timer + PRIV_REQUIRES lvgl esp_psram +) +``` + +--- + +## Panel Descriptor Reference + +```c +typedef struct { + const char *name; // Panel name for logging + uint16_t width; // Width in pixels + uint16_t height; // Height in pixels + epd_color_mode_t color_mode; // EPD_COLOR_BW, EPD_COLOR_6COLOR, etc. + uint8_t bits_per_pixel; // 1, 2, or 4 + uint32_t caps; // EPD_CAP_* flags + epd_controller_type_t ctrl; // Controller type + const void *init_data; // Optional panel-specific data +} epd_panel_desc_t; +``` + +### Capability Flags + +| Flag | Description | +|------|-------------| +| `EPD_CAP_PARTIAL` | Supports partial refresh | +| `EPD_CAP_FAST` | Supports fast refresh mode | +| `EPD_CAP_GRAYSCALE` | Supports grayscale mode | +| `EPD_CAP_BUSY_INV` | Inverted busy signal (HIGH = ready) | + +### Color Modes + +| Mode | BPP | Buffer Size | Description | +|------|-----|-------------|-------------| +| `EPD_COLOR_BW` | 1 | W*H/8 | Black/White | +| `EPD_COLOR_BWR` | 2 | W*H/4 | Black/White/Red | +| `EPD_COLOR_BWY` | 2 | W*H/4 | Black/White/Yellow | +| `EPD_COLOR_4GRAY` | 2 | W*H/4 | 4 Gray levels | +| `EPD_COLOR_4COLOR` | 2 | W*H/4 | 4-color BWRY | +| `EPD_COLOR_6COLOR` | 4 | W*H/2 | 6 Colors | +| `EPD_COLOR_7COLOR` | 4 | W*H/2 | 7 Colors | + +--- + +## Controller Operations Interface + +```c +typedef struct { + esp_err_t (*init)(epd_device_t *dev); + esp_err_t (*update)(epd_device_t *dev, epd_update_mode_t mode); + esp_err_t (*write_ram)(epd_device_t *dev, const uint8_t *data, uint32_t len); + esp_err_t (*write_ram_partial)(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data); + esp_err_t (*sleep)(epd_device_t *dev); + esp_err_t (*wake)(epd_device_t *dev); +} epd_controller_ops_t; +``` + +| Function | Required | Description | +|----------|----------|-------------| +| `init` | Yes | Initialize panel hardware | +| `update` | Yes | Trigger display refresh (handles all modes) | +| `write_ram` | Yes | Write framebuffer to display RAM | +| `write_ram_partial` | No | Windowed RAM write for partial regions | +| `sleep` | No | Enter deep sleep mode | +| `wake` | No | Wake from sleep (usually calls init) | + +--- + +## Helper Functions & Macros + +Available in `epaper_common.h`: + +```c +// Device access +epd_spi_t* epd_get_spi(epd_device_t *dev); +uint16_t epd_get_width(epd_device_t *dev); +uint16_t epd_get_height(epd_device_t *dev); +const epd_panel_desc_t* epd_get_panel(epd_device_t *dev); +bool epd_is_partial_ready(epd_device_t *dev); + +// SPI macros +#define CMD(spi, c) epd_spi_write_cmd(spi, c) +#define DATA(spi, d) epd_spi_write_data(spi, d) +#define RESET(spi) epd_spi_reset(spi) + +// Busy wait with polarity support +#define WAIT(dev) epd_wait_busy_polarity(epd_get_spi(dev), epd_get_panel(dev)->caps, 0) +``` + +--- + +## Common Issues and Solutions + +### Busy Signal Polarity + +Some panels use inverted busy signal. Add `EPD_CAP_BUSY_INV` to capabilities: + +```c +// Standard: LOW = ready, HIGH = busy +.caps = EPD_CAP_PARTIAL, + +// Inverted: HIGH = ready, LOW = busy (e.g., GDEP073E01) +.caps = EPD_CAP_BUSY_INV, +``` + +The `WAIT(dev)` macro automatically handles polarity based on panel caps. + +### Partial Refresh RAM Management + +For partial refresh to work correctly: + +1. **First call** (`partial_ready = false`): Write to both RAM 0x24 and 0x26 +2. **Subsequent calls** (`partial_ready = true`): Write only to RAM 0x24 + +Use `epd_is_partial_ready(dev)` to check the state: + +```c +esp_err_t your_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + bool partial_ready = epd_is_partial_ready(dev); + + // Always write to current RAM + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, len); + + // Only write to base RAM when setting up partial mode + if (!partial_ready) { + CMD(spi, 0x26); + epd_spi_write_data_bulk(spi, data, len); + } + + return ESP_OK; +} +``` + +### Multi-Color Panel RAM Format + +6/7-color panels use 4-bit per pixel (2 pixels per byte): + +```c +// High nibble = even pixel, Low nibble = odd pixel +static void set_pixel_4bpp(uint8_t *fb, int x, int y, int width, uint8_t color) +{ + uint32_t addr = (y * width + x) / 2; + if (x % 2 == 0) { + fb[addr] = (fb[addr] & 0x0F) | (color << 4); + } else { + fb[addr] = (fb[addr] & 0xF0) | (color & 0x0F); + } +} +``` + +4-color BWRY panels use 2-bit per pixel (4 pixels per byte): + +```c +// Pixel order: [P0:7-6][P1:5-4][P2:3-2][P3:1-0] +static void set_pixel_2bpp(uint8_t *fb, int x, int y, int width, uint8_t color) +{ + uint32_t pixel_idx = y * width + x; + uint32_t byte_idx = pixel_idx / 4; + uint8_t bit_offset = (3 - (pixel_idx % 4)) * 2; // 6, 4, 2, 0 + uint8_t mask = 0x03 << bit_offset; + fb[byte_idx] = (fb[byte_idx] & ~mask) | ((color & 0x03) << bit_offset); +} +``` + +--- + +## Adding Board Preset (Optional) + +Create a configuration macro for easy setup in `include/epaper_config.h`: + +```c +#define EPD_CONFIG_YOUR_BOARD() { \ + .pins = { \ + .busy = GPIO_NUM_8, \ + .rst = GPIO_NUM_9, \ + .dc = GPIO_NUM_10, \ + .cs = GPIO_NUM_11, \ + .sck = GPIO_NUM_12, \ + .mosi = GPIO_NUM_13, \ + }, \ + .spi = { \ + .host = SPI2_HOST, \ + .speed_hz = 10000000, \ + }, \ + .panel = { \ + .type = EPD_PANEL_YOUR_PANEL, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} +``` + +--- + +## Testing + +1. **Build test**: `idf.py build` +2. **Basic test**: Fill screen with white, then black +3. **Pattern test**: Draw checkerboard or gradient +4. **Partial test**: Update small region repeatedly +5. **Sleep/wake test**: Verify low power mode + +--- + +## Resources + +- [Good Display Wiki](https://www.good-display.com/companyfile/101.html) - Datasheets and app notes +- [Waveshare E-Paper Wiki](https://www.waveshare.com/wiki/E-Paper) - Reference implementations +- [ESP-IDF SPI Master](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html) diff --git a/Libraries/esp_epaper/CMakeLists.txt b/Libraries/esp_epaper/CMakeLists.txt new file mode 100644 index 000000000..853b4eb68 --- /dev/null +++ b/Libraries/esp_epaper/CMakeLists.txt @@ -0,0 +1,22 @@ +idf_component_register( + SRCS + "src/epaper.c" + "src/epaper_spi.c" + "src/epaper_lvgl.c" + "src/epaper_common.c" + "src/epaper_registry.c" + "src/controllers/ssd16xx.c" + "src/controllers/gdey0154_lut.c" + "src/controllers/acep_6color.c" + "src/controllers/bwry_4color.c" + INCLUDE_DIRS + "include" + REQUIRES + driver + esp_driver_gpio + esp_driver_spi + esp_timer + PRIV_REQUIRES + lvgl + esp_psram +) diff --git a/Libraries/esp_epaper/LICENSE b/Libraries/esp_epaper/LICENSE new file mode 100644 index 000000000..b77bf2ab7 --- /dev/null +++ b/Libraries/esp_epaper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Libraries/esp_epaper/README.md b/Libraries/esp_epaper/README.md new file mode 100644 index 000000000..c5ccaea54 --- /dev/null +++ b/Libraries/esp_epaper/README.md @@ -0,0 +1,385 @@ +# ESP E-Paper Component + +Forked from https://github.com/NellowTCS/esp_epaper which is a fork of https://github.com/tuanpmt/esp_epaper + +[![Component Registry](https://components.espressif.com/components/tuanpmt/esp_epaper/badge.svg)](https://components.espressif.com/components/tuanpmt/esp_epaper) + +A flexible e-paper display driver component for ESP-IDF with LVGL 9 integration. Designed for IoT devices, electronic shelf labels, photo frames, and low-power display applications. + +## Key Features + +### Multi-Color Panel Support +- **Black/White (1-bit)**: Classic e-paper with fastest refresh +- **4-Color BWRY (2-bit)**: Black, White, Red, Yellow +- **6-Color (4-bit)**: Black, White, Yellow, Red, Blue, Green - ideal for photo frames + +### Floyd-Steinberg Dithering +Advanced error-diffusion dithering algorithm for photo-quality images: +- Smooth gradients using only available colors +- Automatic RGB565 to panel palette conversion +- PSRAM support for large displays (800x480) + +### LVGL 9 Partial Refresh +Smart partial refresh for BW panels: +- Base image tracking for differential updates +- Automatic mode switching (partial <-> full) +- Configurable ghosting prevention + +### Data-Driven Architecture +- **Panel Registry**: Add new panels with a single line of code +- **Controller Abstraction**: Reusable controller implementations +- **Capability Flags**: Runtime feature detection (partial, fast, grayscale) + +### Memory Efficient +- Automatic PSRAM allocation for large buffers +- Chunked SPI transfers for DMA compatibility +- Optional dithering buffer + +## Supported Panels + +### Specific Panels (Custom LUT/Features) + +| Panel | Size | Resolution | Colors | Partial Refresh | +|-------|------|------------|--------|-----------------| +| GDEY0154D67 | 1.54" | 200x200 | BW | Yes (custom LUT) | +| GDEP073E01 | 7.3" | 800x480 | 6-Color | No | +| GDEY037F51 | 3.7" | 240x416 | 4-Color BWRY | No | + +### Generic SSD16xx Panels + +Adding new BW panels requires only **1 line of code** - see [ADDING_PANELS.md](ADDING_PANELS.md). + +| Panel Type | Size | Resolution | Compatible Models | +|------------|------|------------|-------------------| +| SSD16XX_154 | 1.54" | 200x200 | GDEM0154I61, etc. | +| SSD16XX_213 | 2.13" | 122x250 | GDEY0213B74, GDEM0213I61 | +| SSD16XX_266 | 2.66" | 152x296 | GDEY0266T90, GDEY0266T90H | +| SSD16XX_270 | 2.7" | 176x264 | GDEY027T91, GDEM027Q72 | +| SSD16XX_290 | 2.9" | 128x296 | GDEY029T94, GDEY029T71H | +| SSD16XX_370 | 3.7" | 280x480 | GDEY037T03 | +| SSD16XX_420 | 4.2" | 400x300 | GDEY042T81, GDEQ0426T82 | + +## Installation + +### Using ESP-IDF Component Registry (Recommended) + +Add to your project's `idf_component.yml`: + +```yaml +dependencies: + tuanpmt/esp_epaper: "^1.0.4" + lvgl/lvgl: "^9.4.0" +``` + +Then run: +```bash +idf.py reconfigure +``` + +### Manual Installation + +Clone or copy this repository to your project's `components` folder: + +```bash +cd your_project/components +git clone https://github.com/tuanpmt/esp_epaper.git +``` + +## Quick Start + +### Basic Usage + +```c +#include "epaper.h" +#include "epaper_lvgl.h" +#include "lvgl.h" + +void app_main(void) +{ + lv_init(); + + // Use preset configuration for ESP32-S3-ePaper-1.54 + epd_config_t cfg = EPD_CONFIG_ESP32S3_154(); + + // Initialize e-paper + epd_handle_t epd; + ESP_ERROR_CHECK(epd_init(&cfg, &epd)); + + // Initialize LVGL display + epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT(); + lvgl_cfg.epd = epd; + lv_display_t *disp = epd_lvgl_init(&lvgl_cfg); + + // Create UI with LVGL... + lv_obj_t *label = lv_label_create(lv_screen_active()); + lv_label_set_text(label, "Hello E-Paper!"); + lv_obj_center(label); + + // Refresh display + epd_lvgl_refresh(disp); +} +``` + +### 6-Color Panel with Dithering (PhotoPainter) + +```c +epd_config_t cfg = EPD_CONFIG_73_6COLOR(); +epd_handle_t epd; +epd_init(&cfg, &epd); + +epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT(); +lvgl_cfg.epd = epd; +lvgl_cfg.update_mode = EPD_UPDATE_FULL; +lvgl_cfg.dither_mode = EPD_DITHER_FLOYD_STEINBERG; + +lv_display_t *disp = epd_lvgl_init(&lvgl_cfg); +``` + +### 4-Color BWRY Panel + +```c +epd_config_t cfg = EPD_CONFIG_ESP32_WROOM_4COLOR(); +epd_handle_t epd; +epd_init(&cfg, &epd); + +epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT(); +lvgl_cfg.epd = epd; +lvgl_cfg.dither_mode = EPD_DITHER_ORDERED; // Good for BWRY + +lv_display_t *disp = epd_lvgl_init(&lvgl_cfg); +``` + +## Configuration Presets + +```c +// Waveshare ESP32-S3-ePaper-1.54 (1.54" BW) +epd_config_t cfg = EPD_CONFIG_ESP32S3_154(); + +// Waveshare ESP32-S3-PhotoPainter (7.3" 6-Color) +epd_config_t cfg = EPD_CONFIG_73_6COLOR(); + +// Good Display ESP32-WROOM-32D (2.66" BW) +epd_config_t cfg = EPD_CONFIG_ESP32_WROOM(); + +// Good Display ESP32-WROOM-32D + 3.7" 4-Color BWRY +epd_config_t cfg = EPD_CONFIG_ESP32_WROOM_4COLOR(); +``` + +### Custom Configuration + +```c +epd_config_t cfg = { + .pins = { + .busy = GPIO_NUM_8, + .rst = GPIO_NUM_9, + .dc = GPIO_NUM_10, + .cs = GPIO_NUM_11, + .sck = GPIO_NUM_12, + .mosi = GPIO_NUM_13, + }, + .spi = { + .host = SPI2_HOST, + .speed_hz = 10000000, // 10MHz + }, + .panel = { + .type = EPD_PANEL_GDEY0154D67, + .width = 0, // 0 = use panel default + .height = 0, + }, +}; +``` + +## Update Modes + +| Mode | Description | Use Case | +|------|-------------|----------| +| `EPD_UPDATE_FULL` | Full refresh with flashing | Initial display, clearing ghosting | +| `EPD_UPDATE_PARTIAL` | Fast partial update | UI updates, counters | +| `EPD_UPDATE_FAST` | Fast mode (panel dependent) | Animations | + +## Architecture Overview + +``` ++------------------+ +-------------------+ +------------------+ +| Application |---->| epaper_lvgl.c |---->| epaper.c | +| (LVGL UI) | | (RGB565->EPD fmt) | | (Device Manager) | ++------------------+ +-------------------+ +------------------+ + | + +--------------------------------+ + | + v ++------------------+ +-------------------+ +| epaper_registry.c|---->| controllers/ | +| (Panel Database) | | - ssd16xx.c | +| | | - gdey0154_lut.c | +| - Panel specs | | - acep_6color.c | +| - Capabilities | | - bwry_4color.c | +| - Controller map | +-------------------+ ++------------------+ +``` + +### Panel Registry (Data-Driven) + +All panels are defined in a single registry table: + +```c +static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = { + [EPD_PANEL_GDEY0154D67] = { + .name = "GDEY0154D67", + .width = 200, .height = 200, + .color_mode = EPD_COLOR_BW, + .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_GDEY0154_LUT, + }, + // ... more panels +}; +``` + +### Capability Flags + +```c +#define EPD_CAP_PARTIAL (1 << 0) // Supports partial refresh +#define EPD_CAP_FAST (1 << 1) // Supports fast refresh +#define EPD_CAP_GRAYSCALE (1 << 2) // Supports grayscale mode +#define EPD_CAP_BUSY_INV (1 << 3) // Inverted busy signal +``` + +## Floyd-Steinberg Dithering + +The component implements Floyd-Steinberg error-diffusion dithering for photo-quality images on limited color e-paper displays. + +### How It Works + +1. **RGB565 -> RGB888 Conversion**: LVGL buffer is converted with proper bit expansion +2. **Palette Matching**: Each pixel is matched to the nearest e-paper color +3. **Error Diffusion**: Quantization error is distributed to neighbors (7/16 right, 3/16 bottom-left, 5/16 bottom, 1/16 bottom-right) + +### Dithering Modes + +| Mode | Description | Memory | Quality | +|------|-------------|--------|---------| +| `EPD_DITHER_NONE` | Direct color mapping | None | Basic | +| `EPD_DITHER_ORDERED` | Bayer 4x4 pattern | None | Good | +| `EPD_DITHER_FLOYD_STEINBERG` | Error diffusion | W*H*3 | Best | + +### Color Palette + +| Color | RGB Value | E-Paper Code | +|-------|-----------|--------------| +| Black | (0, 0, 0) | 0x00 | +| White | (255, 255, 255) | 0x01 | +| Yellow | (255, 255, 0) | 0x02 | +| Red | (255, 0, 0) | 0x03 | +| Blue | (0, 0, 255) | 0x05 | +| Green | (0, 255, 0) | 0x06 | + +## Partial Refresh + +BW panels support partial refresh for fast updates without full-screen flashing. + +### How It Works + +1. **Base Image**: First update writes to both current RAM (0x24) and base RAM (0x26) +2. **Partial Updates**: Subsequent updates only write to current RAM (0x24) +3. **Differential Update**: Display compares RAMs and only refreshes changed pixels + +### Usage + +```c +epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT(); +lvgl_cfg.epd = epd; +lvgl_cfg.update_mode = EPD_UPDATE_PARTIAL; +lvgl_cfg.use_partial_refresh = true; + +lv_display_t *disp = epd_lvgl_init(&lvgl_cfg); + +// First refresh sets base image (full refresh) +epd_lvgl_refresh(disp); + +// Subsequent refreshes use partial mode +lv_label_set_text(label, "Count: 1"); +epd_lvgl_refresh(disp); // Fast partial update + +// Force full refresh periodically to clear ghosting +epd_lvgl_force_full_refresh(disp); +epd_lvgl_refresh(disp); +``` + +**Note**: Multi-color panels (6-color, 4-color BWRY) do not support partial refresh. + +## Examples + +See the `examples/` folder for complete examples: + +| Example | Board | Description | +|---------|-------|-------------| +| [esp32s3_epaper_154](examples/esp32s3_epaper_154) | ESP32-S3-ePaper-1.54 | 1.54" BW with grayscale dithering | +| [esp32s3_photopainter](examples/esp32s3_photopainter) | ESP32-S3-PhotoPainter | 7.3" 6-Color with dithering | +| [esp32_wroom_generic](examples/esp32_wroom_generic) | ESP32-WROOM-32D | Generic SSD16xx, responsive UI | +| [esp32_wroom_4color](examples/esp32_wroom_4color) | ESP32-WROOM-32D | 3.7" 4-Color BWRY | + +### Building Examples + +```bash +cd examples/esp32s3_epaper_154 +idf.py set-target esp32s3 +idf.py build +idf.py -p PORT flash monitor +``` + +## API Reference + +### Core Functions + +```c +esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle); +esp_err_t epd_deinit(epd_handle_t handle); +esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info); +uint8_t* epd_get_framebuffer(epd_handle_t handle); +esp_err_t epd_update(epd_handle_t handle, const uint8_t *data, epd_update_mode_t mode); +esp_err_t epd_fill(epd_handle_t handle, uint8_t color); +esp_err_t epd_sleep(epd_handle_t handle); +esp_err_t epd_wake(epd_handle_t handle); +``` + +### LVGL Functions + +```c +lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config); +void epd_lvgl_deinit(lv_display_t *disp); +void epd_lvgl_refresh(lv_display_t *disp); +void epd_lvgl_force_full_refresh(lv_display_t *disp); +void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode); +void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode); +``` + +## Adding New Panels + +See [ADDING_PANELS.md](ADDING_PANELS.md) for detailed instructions on adding support for new e-paper panels. + +**Quick summary**: +- **Same controller, different size**: Add 1 line to panel registry +- **New controller**: Add controller ops + panel registry entry + +## Memory Requirements + +| Panel | Resolution | BPP | Framebuffer | RGB Buffer (dithering) | +|-------|------------|-----|-------------|------------------------| +| 1.54" BW | 200x200 | 1 | 5 KB | 120 KB | +| 2.13" BW | 122x250 | 1 | 3.8 KB | 92 KB | +| 2.66" BW | 152x296 | 1 | 5.6 KB | 135 KB | +| 3.7" BW | 280x480 | 1 | 16.8 KB | 403 KB | +| 4.2" BW | 400x300 | 1 | 15 KB | 360 KB | +| 3.7" BWRY | 240x416 | 2 | 25 KB | 300 KB | +| 7.3" 6-Color | 800x480 | 4 | 192 KB | 1.15 MB | + +**Note**: Large buffers (>32KB) are automatically allocated from PSRAM when available. + +## License + +MIT License - See [LICENSE](LICENSE) + +## Author + +- **tuanpmt** - [GitHub](https://github.com/tuanpmt) diff --git a/Libraries/esp_epaper/idf_component.yml b/Libraries/esp_epaper/idf_component.yml new file mode 100644 index 000000000..50c155d69 --- /dev/null +++ b/Libraries/esp_epaper/idf_component.yml @@ -0,0 +1,30 @@ +version: "2.0.0" +description: "E-Paper display driver for ESP-IDF with LVGL 9 integration. Supports BW, 3-color, and 6-color panels with Floyd-Steinberg dithering." +url: "https://github.com/tuanpmt/esp_epaper" +repository: "https://github.com/tuanpmt/esp_epaper.git" +documentation: "https://github.com/tuanpmt/esp_epaper/blob/master/README.md" +issues: "https://github.com/tuanpmt/esp_epaper/issues" +maintainers: + - "tuanpmt" + +dependencies: + idf: + version: ">=5.0.0" + lvgl/lvgl: + version: "^9.0.0" + public: true + +targets: + - esp32 + - esp32s2 + - esp32s3 + - esp32c3 + - esp32c6 + - esp32p4 + +license: "MIT" + +examples: + - path: examples/esp32s3_epaper_154 + - path: examples/esp32s3_photopainter + - path: examples/esp32_wroom_generic diff --git a/Libraries/esp_epaper/include/epaper.h b/Libraries/esp_epaper/include/epaper.h new file mode 100644 index 000000000..18158c08e --- /dev/null +++ b/Libraries/esp_epaper/include/epaper.h @@ -0,0 +1,129 @@ +#ifndef _EPAPER_H_ +#define _EPAPER_H_ + +#include +#include +#include "epaper_config.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Opaque handle +typedef struct epd_device* epd_handle_t; + +// Panel info (read-only after init) +typedef struct { + uint16_t width; + uint16_t height; + uint32_t buffer_size; // In bytes + epd_color_mode_t color_mode; +} epd_panel_info_t; + +/** + * @brief Initialize e-paper display + * @param config Device configuration + * @param handle Output handle + * @return ESP_OK on success + */ +esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle); + +/** + * @brief Deinitialize e-paper display + */ +esp_err_t epd_deinit(epd_handle_t handle); + +/** + * @brief Get panel information + */ +esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info); + +/** + * @brief Clear display (full white) + */ +esp_err_t epd_clear(epd_handle_t handle); + +/** + * @brief Fill display with color (0x00=black, 0xFF=white) + */ +esp_err_t epd_fill(epd_handle_t handle, uint8_t color); + +/** + * @brief Update full screen with buffer + * @param buffer Pixel data (1 bit per pixel, MSB first) + * @param mode Update mode (full/fast/partial) + */ +esp_err_t epd_update(epd_handle_t handle, const uint8_t *buffer, epd_update_mode_t mode); + +/** + * @brief Partial update + * @param x Start X (must be multiple of 8) + * @param y Start Y + * @param w Width (must be multiple of 8) + * @param h Height + * @param buffer Pixel data + */ +esp_err_t epd_update_partial(epd_handle_t handle, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *buffer); + +/** + * @brief Set base image for partial updates + */ +esp_err_t epd_set_base_image(epd_handle_t handle, const uint8_t *buffer); + +/** + * @brief Enter deep sleep mode + */ +esp_err_t epd_sleep(epd_handle_t handle); + +/** + * @brief Wake from deep sleep + */ +esp_err_t epd_wake(epd_handle_t handle); + +/** + * @brief Check if display is busy + */ +bool epd_is_busy(epd_handle_t handle); + +/** + * @brief Wait until display is ready + * @param timeout_ms Timeout in milliseconds (0 = infinite) + */ +esp_err_t epd_wait_busy(epd_handle_t handle, uint32_t timeout_ms); + +// ============ LVGL Integration Interface ============ + +/** + * @brief Flush callback for LVGL + * Call this from lv_display_set_flush_cb + * + * Usage in LVGL 9: + * lv_display_t *disp = lv_display_create(width, height); + * lv_display_set_flush_cb(disp, my_flush_cb); + * lv_display_set_user_data(disp, epd_handle); + * + * In my_flush_cb: + * epd_handle_t h = lv_display_get_user_data(disp); + * epd_lvgl_flush(h, area, px_map); + * lv_display_flush_ready(disp); + */ +esp_err_t epd_lvgl_flush(epd_handle_t handle, const void *area, const uint8_t *px_map); + +/** + * @brief Get framebuffer for direct LVGL rendering + * @return Pointer to internal framebuffer + */ +uint8_t* epd_get_framebuffer(epd_handle_t handle); + +/** + * @brief Flush framebuffer to display + */ +esp_err_t epd_flush_framebuffer(epd_handle_t handle, epd_update_mode_t mode); + +#ifdef __cplusplus +} +#endif + +#endif // _EPAPER_H_ diff --git a/Libraries/esp_epaper/include/epaper_config.h b/Libraries/esp_epaper/include/epaper_config.h new file mode 100644 index 000000000..d25cb23f3 --- /dev/null +++ b/Libraries/esp_epaper/include/epaper_config.h @@ -0,0 +1,243 @@ +#ifndef _EPAPER_CONFIG_H_ +#define _EPAPER_CONFIG_H_ + +#include +#include +#include "driver/spi_master.h" + +// Default pinout (ESP32-WROOM-32D) +#define EPD_DEFAULT_PIN_BUSY 14 +#define EPD_DEFAULT_PIN_RST 15 +#define EPD_DEFAULT_PIN_DC 16 +#define EPD_DEFAULT_PIN_CS 17 +#define EPD_DEFAULT_PIN_SCK 18 +#define EPD_DEFAULT_PIN_MOSI 23 + +#define EPD_DEFAULT_SPI_HOST SPI2_HOST +#define EPD_DEFAULT_SPI_SPEED 4000000 // 4MHz + +// Panel types +typedef enum { + // Specific panels (with custom LUT or special features) + EPD_PANEL_GDEY0154D67 = 0, // 1.54" BW 200x200 (custom LUT for better partial) + EPD_PANEL_GDEP073E01, // 7.3" 6-Color 800x480 + EPD_PANEL_GDEY037F51, // 3.7" 4-Color BWRY 240x416 + EPD_PANEL_GDEY029T71H, // 2.9" BW 168x384 (SSD1685, sources S8..S175) + + // Generic SSD16xx BW panels (same driver, different sizes) + EPD_PANEL_SSD16XX_154, // 1.54" BW 200x200 (generic, no custom LUT) + EPD_PANEL_SSD16XX_213, // 2.13" BW 122x250 (GDEY0213B74, etc.) + EPD_PANEL_SSD16XX_266, // 2.66" BW 152x296 (GDEY0266T90, etc.) + EPD_PANEL_SSD16XX_270, // 2.7" BW 176x264 (GDEY027T91, etc.) + EPD_PANEL_SSD16XX_290, // 2.9" BW 128x296 (GDEY029T94, etc.) + EPD_PANEL_SSD16XX_370, // 3.7" BW 280x480 (GDEY037T03, etc.) + EPD_PANEL_SSD16XX_420, // 4.2" BW 400x300 (GDEY042T81, etc.) + + EPD_PANEL_COUNT, // Must be BEFORE aliases! + + // Legacy aliases (for backward compatibility, don't affect count) + EPD_PANEL_GDEY0266T90 = EPD_PANEL_SSD16XX_266, +} epd_panel_type_t; + +// Board presets +typedef enum { + EPD_BOARD_CUSTOM = 0, + EPD_BOARD_ESP32_WROOM, // Default ESP32-WROOM-32D + EPD_BOARD_ESP32S3_EPAPER_154, // ESP32-S3-ePaper-1.54 board + EPD_BOARD_ESP32S3_EPAPER_73, // ESP32-S3 + 7.3" 6-Color board +} epd_board_preset_t; + +// Color mode +typedef enum { + EPD_COLOR_BW = 0, // Black/White (1-bit) + EPD_COLOR_BWR, // Black/White/Red (3-color) + EPD_COLOR_BWY, // Black/White/Yellow (3-color) + EPD_COLOR_4GRAY, // 4 Gray levels + EPD_COLOR_4COLOR, // 4-color BWRY (Black/White/Yellow/Red, 2-bit) + EPD_COLOR_7COLOR, // 7-color (B/W/R/Y/Orange/Green/Blue) + EPD_COLOR_6COLOR, // 6-color (B/W/R/Y/Green/Blue) +} epd_color_mode_t; + +// E-paper color indices (for multi-color panels) +#define EPD_PIXEL_BLACK 0x0 +#define EPD_PIXEL_WHITE 0x1 +#define EPD_PIXEL_YELLOW 0x2 +#define EPD_PIXEL_RED 0x3 +#define EPD_PIXEL_ORANGE 0x4 +#define EPD_PIXEL_BLUE 0x5 +#define EPD_PIXEL_GREEN 0x6 + +// Dithering mode +typedef enum { + EPD_DITHER_NONE = 0, // No dithering (direct color mapping) + EPD_DITHER_FLOYD_STEINBERG, // Floyd-Steinberg error diffusion + EPD_DITHER_ORDERED, // Ordered/Bayer dithering +} epd_dither_mode_t; + +// Update mode +typedef enum { + EPD_UPDATE_FULL = 0, // Full refresh + EPD_UPDATE_FAST, // Fast refresh + EPD_UPDATE_PARTIAL, // Partial refresh +} epd_update_mode_t; + +// Pin configuration +typedef struct { + int busy; + int rst; + int dc; + int cs; + int sck; + int mosi; +} epd_pin_config_t; + +// SPI configuration +typedef struct { + int host; // SPI host (SPI2_HOST, SPI3_HOST) + int speed_hz; // Clock speed +} epd_spi_config_t; + +// Panel configuration +typedef struct { + epd_panel_type_t type; + uint16_t width; // Override width (0 = use default) + uint16_t height; // Override height (0 = use default) + bool mirror_x; + bool mirror_y; + uint8_t rotation; // 0, 90, 180, 270 +} epd_panel_config_t; + +// Full device configuration +typedef struct { + epd_pin_config_t pins; + epd_spi_config_t spi; + epd_panel_config_t panel; +} epd_config_t; + +// Default configuration macro (ESP32-WROOM-32D) +#define EPD_CONFIG_DEFAULT() { \ + .pins = { \ + .busy = EPD_DEFAULT_PIN_BUSY, \ + .rst = EPD_DEFAULT_PIN_RST, \ + .dc = EPD_DEFAULT_PIN_DC, \ + .cs = EPD_DEFAULT_PIN_CS, \ + .sck = EPD_DEFAULT_PIN_SCK, \ + .mosi = EPD_DEFAULT_PIN_MOSI, \ + }, \ + .spi = { \ + .host = EPD_DEFAULT_SPI_HOST, \ + .speed_hz = EPD_DEFAULT_SPI_SPEED, \ + }, \ + .panel = { \ + .type = EPD_PANEL_GDEY0266T90, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} + +// ESP32-S3-ePaper-1.54 board configuration +// Pinout: DC=10, CS=11, SCK=12, MOSI=13, RST=9, BUSY=8 +#define EPD_CONFIG_ESP32S3_154() { \ + .pins = { \ + .busy = 8, \ + .rst = 9, \ + .dc = 10, \ + .cs = 11, \ + .sck = 12, \ + .mosi = 13, \ + }, \ + .spi = { \ + .host = SPI2_HOST, \ + .speed_hz = 40000000, \ + }, \ + .panel = { \ + .type = EPD_PANEL_GDEY0154D67, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} + +// 7.3" 6-Color E-Paper configuration (800x480) +// Adjust pins according to your board +#define EPD_CONFIG_73_6COLOR() { \ + .pins = { \ + .busy = 13, \ + .rst = 12, \ + .dc = 8, \ + .cs = 9, \ + .sck = 10, \ + .mosi = 11, \ + }, \ + .spi = { \ + .host = SPI2_HOST, \ + .speed_hz = 10000000, \ + }, \ + .panel = { \ + .type = EPD_PANEL_GDEP073E01, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} + +// Good Display ESP32-WROOM-32D development board +// Default panel: 2.66" BW (152x296) +// Pinout: BUSY=13, RST=12, DC=14, CS=27, SCK=18, MOSI=23 +#define EPD_CONFIG_ESP32_WROOM() { \ + .pins = { \ + .busy = 13, \ + .rst = 12, \ + .dc = 14, \ + .cs = 27, \ + .sck = 18, \ + .mosi = 23, \ + }, \ + .spi = { \ + .host = SPI2_HOST, \ + .speed_hz = 10000000, \ + }, \ + .panel = { \ + .type = EPD_PANEL_SSD16XX_266, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} + +// Good Display ESP32-WROOM-32D + 3.7" 4-Color BWRY (GDEY037F51) +// 240x416 resolution, no PSRAM required (streaming mode) +// Pinout: BUSY=13, RST=12, DC=14, CS=27, SCK=18, MOSI=23 +#define EPD_CONFIG_ESP32_WROOM_4COLOR() { \ + .pins = { \ + .busy = 13, \ + .rst = 12, \ + .dc = 14, \ + .cs = 27, \ + .sck = 18, \ + .mosi = 23, \ + }, \ + .spi = { \ + .host = SPI2_HOST, \ + .speed_hz = 10000000, \ + }, \ + .panel = { \ + .type = EPD_PANEL_GDEY037F51, \ + .width = 0, \ + .height = 0, \ + .mirror_x = false, \ + .mirror_y = false, \ + .rotation = 0, \ + }, \ +} + +#endif // _EPAPER_CONFIG_H_ diff --git a/Libraries/esp_epaper/include/epaper_lvgl.h b/Libraries/esp_epaper/include/epaper_lvgl.h new file mode 100644 index 000000000..f5e379f20 --- /dev/null +++ b/Libraries/esp_epaper/include/epaper_lvgl.h @@ -0,0 +1,111 @@ +#ifndef _EPAPER_LVGL_H_ +#define _EPAPER_LVGL_H_ + +#include "epaper.h" +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LVGL e-paper port configuration + */ +typedef struct { + epd_handle_t epd; // E-paper handle (required) + epd_update_mode_t update_mode; // Default update mode + epd_dither_mode_t dither_mode; // Dithering algorithm + bool use_partial_refresh; // Enable partial refresh optimization + uint32_t partial_threshold; // Min dirty area (pixels) for partial refresh +} epd_lvgl_config_t; + +#define EPD_LVGL_CONFIG_DEFAULT() { \ + .epd = NULL, \ + .update_mode = EPD_UPDATE_FULL, \ + .dither_mode = EPD_DITHER_NONE, \ + .use_partial_refresh = false, \ + .partial_threshold = 1000, \ +} + +/** + * @brief Initialize LVGL display driver for e-paper + * + * @param config LVGL port configuration + * @return lv_display_t* LVGL display object, NULL on failure + * + * Example: + * @code + * epd_config_t epd_cfg = EPD_CONFIG_DEFAULT(); + * epd_handle_t epd; + * epd_init(&epd_cfg, &epd); + * + * epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT(); + * lvgl_cfg.epd = epd; + * lvgl_cfg.use_partial_refresh = true; + * + * lv_display_t *disp = epd_lvgl_init(&lvgl_cfg); + * @endcode + */ +lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config); + +/** + * @brief Deinitialize LVGL e-paper driver + */ +void epd_lvgl_deinit(lv_display_t *disp); + +/** + * @brief Set update mode for next refresh + * @param disp LVGL display + * @param mode EPD_UPDATE_FULL, EPD_UPDATE_FAST, EPD_UPDATE_PARTIAL + */ +void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode); + +/** + * @brief Force full refresh on next update + * Call this periodically (e.g., every 10 partial updates) to prevent ghosting + */ +void epd_lvgl_force_full_refresh(lv_display_t *disp); + +/** + * @brief Manually trigger display refresh + * Use when lv_refr_now() doesn't work as expected + */ +void epd_lvgl_refresh(lv_display_t *disp); + +/** + * @brief Get e-paper handle from display + */ +epd_handle_t epd_lvgl_get_epd(lv_display_t *disp); + +/** + * @brief Set dithering mode + * @param disp LVGL display + * @param mode EPD_DITHER_NONE, EPD_DITHER_FLOYD_STEINBERG + * + * Floyd-Steinberg dithering provides better image quality for photos/gradients + * but requires additional memory (width * height * 3 bytes for RGB buffer) + */ +void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode); + +/** + * @brief Convert RGB888 to nearest e-paper color + * @param r Red component (0-255) + * @param g Green component (0-255) + * @param b Blue component (0-255) + * @param mode Color mode of the panel + * @return Nearest EPD_PIXEL_xxx value + */ +uint8_t epd_rgb_to_epaper_color(uint8_t r, uint8_t g, uint8_t b, epd_color_mode_t mode); + +/** + * @brief Create LVGL color from e-paper palette + * @param epaper_color EPD_PIXEL_xxx value + * @return lv_color_t LVGL color value + */ +lv_color_t epd_epaper_to_lv_color(uint8_t epaper_color); + +#ifdef __cplusplus +} +#endif + +#endif // _EPAPER_LVGL_H_ diff --git a/Libraries/esp_epaper/include/epaper_panel.h b/Libraries/esp_epaper/include/epaper_panel.h new file mode 100644 index 000000000..247c44985 --- /dev/null +++ b/Libraries/esp_epaper/include/epaper_panel.h @@ -0,0 +1,82 @@ +#ifndef _EPAPER_PANEL_H_ +#define _EPAPER_PANEL_H_ + +#include +#include +#include "esp_err.h" +#include "epaper_config.h" + +// Forward declaration +typedef struct epd_device epd_device_t; + +/*============================================================================= + * Capability Flags + *============================================================================*/ +#define EPD_CAP_PARTIAL (1 << 0) // Supports partial refresh +#define EPD_CAP_FAST (1 << 1) // Supports fast refresh +#define EPD_CAP_GRAYSCALE (1 << 2) // Supports grayscale mode +#define EPD_CAP_BUSY_INV (1 << 3) // Inverted busy signal (HIGH=ready) + +/*============================================================================= + * Controller Types + *============================================================================*/ +typedef enum { + EPD_CTRL_SSD16XX, // SSD1680/1681 BW panels (generic) + EPD_CTRL_GDEY0154_LUT, // GDEY0154D67 with custom LUT + EPD_CTRL_ACEP_6COLOR, // ACeP 6-color controller (GDEP073E01) + EPD_CTRL_BWRY_4COLOR, // BWRY 4-color controller (GDEY037F51) + EPD_CTRL_COUNT +} epd_controller_type_t; + +/*============================================================================= + * Panel Descriptor (Data-Driven) + *============================================================================*/ +typedef struct { + const char *name; // Panel name for logging + uint16_t width; // Default width in pixels + uint16_t height; // Default height in pixels + epd_color_mode_t color_mode; // Color mode + uint8_t bits_per_pixel; // 1, 2, or 4 + uint32_t caps; // Capability flags (EPD_CAP_*) + epd_controller_type_t ctrl; // Controller type + uint8_t source_shift_bytes; // GDDRAM X window offset in bytes (GDEY029T71H: 1) + bool fast_full_update; // SSD1685 fast full update: bypass RED + temp override + bool ssd1685_partial; // SSD1685 partial: 0x21=(0x00,0x00) + 0x22=0xDC + uint8_t full_refresh_interval; // Periodic full refresh after N partials (0 = disabled) + const void *init_data; // Panel-specific init data (LUT, etc.) +} epd_panel_desc_t; + +/*============================================================================= + * Controller Operations Interface + *============================================================================*/ +typedef struct { + esp_err_t (*init)(epd_device_t *dev); + esp_err_t (*update)(epd_device_t *dev, epd_update_mode_t mode); + esp_err_t (*write_ram)(epd_device_t *dev, const uint8_t *data, uint32_t len); + esp_err_t (*write_ram_partial)(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data); + esp_err_t (*sleep)(epd_device_t *dev); + esp_err_t (*wake)(epd_device_t *dev); +} epd_controller_ops_t; + +/*============================================================================= + * Registry Functions + *============================================================================*/ + +// Get panel descriptor by type +const epd_panel_desc_t* epd_get_panel_desc(epd_panel_type_t type); + +// Get controller operations by type +const epd_controller_ops_t* epd_get_controller_ops(epd_controller_type_t ctrl); + +// Check if panel supports a capability +static inline bool epd_panel_has_cap(const epd_panel_desc_t *panel, uint32_t cap) { + return panel && (panel->caps & cap); +} + +// Calculate buffer size for panel +static inline uint32_t epd_calc_buffer_size(uint16_t w, uint16_t h, uint8_t bpp) { + return ((uint32_t)w * h * bpp + 7) / 8; +} + +#endif // _EPAPER_PANEL_H_ diff --git a/Libraries/esp_epaper/src/controllers/acep_6color.c b/Libraries/esp_epaper/src/controllers/acep_6color.c new file mode 100644 index 000000000..e69499453 --- /dev/null +++ b/Libraries/esp_epaper/src/controllers/acep_6color.c @@ -0,0 +1,167 @@ +/** + * @file acep_6color.c + * @brief Driver for ACeP 6-Color E-Paper Displays (GDEP073E01, etc.) + * + * Colors: Black, White, Yellow, Red, Blue, Green + * Format: 4-bit per pixel (2 pixels per byte) + * + * Note: This panel does NOT support partial or fast refresh. + * Uses inverted busy signal (HIGH = ready, LOW = busy). + */ + +#include "../epaper_common.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "acep6c"; + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t acep6c_init(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + uint16_t w = epd_get_width(dev); + uint16_t h = epd_get_height(dev); + + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(50)); + WAIT(dev); + vTaskDelay(pdMS_TO_TICKS(50)); + + // Command sequence for 6-color e-paper + CMD(spi, 0xAA); // CMDH + DATA(spi, 0x49); + DATA(spi, 0x55); + DATA(spi, 0x20); + DATA(spi, 0x08); + DATA(spi, 0x09); + DATA(spi, 0x18); + + CMD(spi, 0x01); + DATA(spi, 0x3F); + + CMD(spi, 0x00); + DATA(spi, 0x5F); + DATA(spi, 0x69); + + CMD(spi, 0x03); + DATA(spi, 0x00); + DATA(spi, 0x54); + DATA(spi, 0x00); + DATA(spi, 0x44); + + CMD(spi, 0x05); + DATA(spi, 0x40); + DATA(spi, 0x1F); + DATA(spi, 0x1F); + DATA(spi, 0x2C); + + CMD(spi, 0x06); + DATA(spi, 0x6F); + DATA(spi, 0x1F); + DATA(spi, 0x17); + DATA(spi, 0x49); + + CMD(spi, 0x08); + DATA(spi, 0x6F); + DATA(spi, 0x1F); + DATA(spi, 0x1F); + DATA(spi, 0x22); + + CMD(spi, 0x30); + DATA(spi, 0x03); + + CMD(spi, 0x50); + DATA(spi, 0x3F); + + CMD(spi, 0x60); + DATA(spi, 0x02); + DATA(spi, 0x00); + + // Resolution + CMD(spi, 0x61); + DATA(spi, (w >> 8) & 0xFF); + DATA(spi, w & 0xFF); + DATA(spi, (h >> 8) & 0xFF); + DATA(spi, h & 0xFF); + + CMD(spi, 0x84); + DATA(spi, 0x01); + + CMD(spi, 0xE3); + DATA(spi, 0x2F); + + CMD(spi, 0x04); // Power on + WAIT(dev); + + ESP_LOGI(TAG, "Panel initialized (%dx%d 6-color)", w, h); + return ESP_OK; +} + +/*============================================================================= + * Update (only full refresh supported) + *============================================================================*/ + +esp_err_t acep6c_update(epd_device_t *dev, epd_update_mode_t mode) +{ + (void)mode; // Only full refresh supported + + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x04); // POWER_ON + WAIT(dev); + + CMD(spi, 0x06); + DATA(spi, 0x6F); + DATA(spi, 0x1F); + DATA(spi, 0x17); + DATA(spi, 0x49); + + CMD(spi, 0x12); // DISPLAY_REFRESH + DATA(spi, 0x00); + WAIT(dev); + + CMD(spi, 0x02); // POWER_OFF + DATA(spi, 0x00); + WAIT(dev); + + return ESP_OK; +} + +/*============================================================================= + * RAM Write + *============================================================================*/ + +esp_err_t acep6c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x10); // Write RAM + epd_spi_write_data_bulk(spi, data, len); + + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t acep6c_sleep(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x07); // DEEP_SLEEP + DATA(spi, 0xA5); + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_LOGI(TAG, "Panel entering deep sleep"); + return ESP_OK; +} + +esp_err_t acep6c_wake(epd_device_t *dev) +{ + return acep6c_init(dev); +} diff --git a/Libraries/esp_epaper/src/controllers/bwry_4color.c b/Libraries/esp_epaper/src/controllers/bwry_4color.c new file mode 100644 index 000000000..1c004a067 --- /dev/null +++ b/Libraries/esp_epaper/src/controllers/bwry_4color.c @@ -0,0 +1,230 @@ +/** + * @file bwry_4color.c + * @brief Driver for BWRY 4-Color E-Paper Displays (GDEY037F51, etc.) + * + * Colors: Black, White, Yellow, Red + * Format: 2-bit per pixel (4 pixels per byte) + * + * Note: This panel does NOT support partial or fast refresh. + * Uses inverted busy signal (HIGH = ready, LOW = busy). + */ + +#include "../epaper_common.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "bwry4c"; + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t bwry4c_init(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + uint16_t w = epd_get_width(dev); + uint16_t h = epd_get_height(dev); + + vTaskDelay(pdMS_TO_TICKS(100)); + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(50)); + WAIT(dev); + + // PSR - Panel Setting + CMD(spi, 0x00); + DATA(spi, 0x0F); + DATA(spi, 0x29); + + // PWR - Power Setting + CMD(spi, 0x01); + DATA(spi, 0x07); + DATA(spi, 0x00); + DATA(spi, 0x22); + DATA(spi, 0x78); + DATA(spi, 0x0A); + DATA(spi, 0x22); + + // PFS - Power off sequence setting + CMD(spi, 0x03); + DATA(spi, 0x10); + DATA(spi, 0x54); + DATA(spi, 0x44); + + // BTST - Booster soft start + CMD(spi, 0x06); + DATA(spi, 0xC0); + DATA(spi, 0xC0); + DATA(spi, 0xC0); + + // PLL control + CMD(spi, 0x30); + DATA(spi, 0x08); + + // TSE - Temperature sensor enable + CMD(spi, 0x41); + DATA(spi, 0x00); + + // CDI - VCOM and data interval setting + CMD(spi, 0x50); + DATA(spi, 0x37); + + // TCON setting + CMD(spi, 0x60); + DATA(spi, 0x02); + DATA(spi, 0x02); + + // Resolution setting + CMD(spi, 0x61); + DATA(spi, w >> 8); + DATA(spi, w & 0xFF); + DATA(spi, h >> 8); + DATA(spi, h & 0xFF); + + // FLG - Get flag status + CMD(spi, 0x65); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + + // PWM settings + CMD(spi, 0xE7); + DATA(spi, 0x1C); + + CMD(spi, 0xE3); + DATA(spi, 0x22); + + // Enter special register mode + CMD(spi, 0xFF); + DATA(spi, 0xA5); + + CMD(spi, 0xEF); + DATA(spi, 0x01); + DATA(spi, 0x1E); + DATA(spi, 0x0A); + DATA(spi, 0x1B); + DATA(spi, 0x0B); + DATA(spi, 0x17); + + CMD(spi, 0xC3); + DATA(spi, 0xFD); + + CMD(spi, 0xDC); + DATA(spi, 0x01); + + CMD(spi, 0xDD); + DATA(spi, 0x08); + + CMD(spi, 0xDE); + DATA(spi, 0x41); + + CMD(spi, 0xFD); + DATA(spi, 0x01); + + CMD(spi, 0xE8); + DATA(spi, 0x03); + + CMD(spi, 0xDA); + DATA(spi, 0x07); + + CMD(spi, 0xC9); + DATA(spi, 0x00); + + CMD(spi, 0xA8); + DATA(spi, 0x0F); + + // Exit special register mode + CMD(spi, 0xFF); + DATA(spi, 0xE3); + + CMD(spi, 0xE9); + DATA(spi, 0x01); + + // Power on + CMD(spi, 0x04); + WAIT(dev); + + // Additional PWM settings after power on + CMD(spi, 0xFF); + DATA(spi, 0xA5); + + CMD(spi, 0xEF); + DATA(spi, 0x03); + DATA(spi, 0x1E); + DATA(spi, 0x0A); + DATA(spi, 0x1B); + DATA(spi, 0x0E); + DATA(spi, 0x15); + + CMD(spi, 0xDC); + DATA(spi, 0x01); + + CMD(spi, 0xDD); + DATA(spi, 0x08); + + CMD(spi, 0xDE); + DATA(spi, 0x41); + + CMD(spi, 0xFF); + DATA(spi, 0xE3); + + ESP_LOGI(TAG, "Panel initialized (%dx%d 4-color BWRY)", w, h); + return ESP_OK; +} + +/*============================================================================= + * Update (only full refresh supported) + *============================================================================*/ + +esp_err_t bwry4c_update(epd_device_t *dev, epd_update_mode_t mode) +{ + (void)mode; // Only full refresh supported + + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x12); // Display Refresh + DATA(spi, 0x00); + WAIT(dev); + + return ESP_OK; +} + +/*============================================================================= + * RAM Write + *============================================================================*/ + +esp_err_t bwry4c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x10); // Write RAM + epd_spi_write_data_bulk(spi, data, len); + + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t bwry4c_sleep(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x02); // Power off + DATA(spi, 0x00); + WAIT(dev); + + CMD(spi, 0x07); // Deep sleep + DATA(spi, 0xA5); + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_LOGI(TAG, "Panel entering deep sleep"); + return ESP_OK; +} + +esp_err_t bwry4c_wake(epd_device_t *dev) +{ + return bwry4c_init(dev); +} diff --git a/Libraries/esp_epaper/src/controllers/gdey0154_lut.c b/Libraries/esp_epaper/src/controllers/gdey0154_lut.c new file mode 100644 index 000000000..2529a2734 --- /dev/null +++ b/Libraries/esp_epaper/src/controllers/gdey0154_lut.c @@ -0,0 +1,291 @@ +/** + * @file gdey0154_lut.c + * @brief Driver for GDEY0154D67 1.54" B/W e-paper display with custom LUT + * + * This panel uses custom waveform lookup tables for optimized + * full and partial refresh performance. + */ + +#include "../epaper_common.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include + +static const char *TAG = "gdey0154"; + +/*============================================================================= + * Waveform LUTs + *============================================================================*/ + +static const uint8_t LUT_FULL[159] = { + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x20 +}; + +static const uint8_t LUT_PARTIAL[159] = { + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x02, 0x17, 0x41, 0xB0, 0x32, 0x28, +}; + +/*============================================================================= + * LUT Loading + *============================================================================*/ + +static void load_lut(epd_spi_t *spi, const uint8_t *lut) +{ + CMD(spi, 0x32); + epd_spi_write_data_bulk(spi, lut, 153); + epd_spi_wait_busy(spi, 0); + + CMD(spi, 0x3F); + DATA(spi, lut[153]); + + CMD(spi, 0x03); + DATA(spi, lut[154]); + + CMD(spi, 0x04); + DATA(spi, lut[155]); + DATA(spi, lut[156]); + DATA(spi, lut[157]); + + CMD(spi, 0x2C); + DATA(spi, lut[158]); +} + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t gdey0154_init(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + uint16_t w = epd_get_width(dev); + uint16_t h = epd_get_height(dev); + + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(50)); + + epd_spi_wait_busy(spi, 0); + CMD(spi, 0x12); // SWRESET + epd_spi_wait_busy(spi, 0); + + CMD(spi, 0x01); // Driver output control + DATA(spi, 0xC7); + DATA(spi, 0x00); + DATA(spi, 0x01); + + CMD(spi, 0x11); // Data entry mode + DATA(spi, 0x01); + + // Set RAM window + CMD(spi, 0x44); + DATA(spi, 0); + DATA(spi, (w - 1) / 8); + + CMD(spi, 0x45); + DATA(spi, (h - 1) & 0xFF); + DATA(spi, ((h - 1) >> 8) & 0xFF); + DATA(spi, 0); + DATA(spi, 0); + + CMD(spi, 0x3C); // Border waveform + DATA(spi, 0x01); + + CMD(spi, 0x18); // Temperature sensor + DATA(spi, 0x80); + + CMD(spi, 0x22); // Load temperature + DATA(spi, 0xB1); + CMD(spi, 0x20); + + // Set cursor + CMD(spi, 0x4E); + DATA(spi, 0); + + CMD(spi, 0x4F); + DATA(spi, (h - 1) & 0xFF); + DATA(spi, ((h - 1) >> 8) & 0xFF); + + epd_spi_wait_busy(spi, 0); + load_lut(spi, LUT_FULL); + + ESP_LOGD(TAG, "Init complete (%dx%d)", w, h); + return ESP_OK; +} + +/*============================================================================= + * Update Functions + *============================================================================*/ + +static esp_err_t gdey0154_update_full(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + load_lut(spi, LUT_FULL); + + CMD(spi, 0x22); + DATA(spi, 0xC7); + CMD(spi, 0x20); + epd_spi_wait_busy(spi, 0); + + return ESP_OK; +} + +static esp_err_t gdey0154_update_partial(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(50)); + epd_spi_wait_busy(spi, 0); + + load_lut(spi, LUT_PARTIAL); + + CMD(spi, 0x37); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x40); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, 0x00); + + CMD(spi, 0x3C); // Border waveform + DATA(spi, 0x80); + + CMD(spi, 0x22); + DATA(spi, 0xCF); + CMD(spi, 0x20); + epd_spi_wait_busy(spi, 0); + + return ESP_OK; +} + +esp_err_t gdey0154_update(epd_device_t *dev, epd_update_mode_t mode) +{ + switch (mode) { + case EPD_UPDATE_PARTIAL: + case EPD_UPDATE_FAST: + return gdey0154_update_partial(dev); + case EPD_UPDATE_FULL: + default: + return gdey0154_update_full(dev); + } +} + +/*============================================================================= + * RAM Write Functions + *============================================================================*/ + +esp_err_t gdey0154_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + epd_spi_t *spi = epd_get_spi(dev); + bool partial_ready = epd_is_partial_ready(dev); + + // Write to current RAM (0x24) + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, len); + + // Only write to base image RAM (0x26) when setting up for partial refresh + // When partial_ready is true, we only update 0x24 so the display can + // compare old (0x26) vs new (0x24) for partial update + if (!partial_ready) { + CMD(spi, 0x26); + epd_spi_write_data_bulk(spi, data, len); + } + + return ESP_OK; +} + +esp_err_t gdey0154_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data) +{ + epd_spi_t *spi = epd_get_spi(dev); + uint16_t panel_h = epd_get_height(dev); + + uint16_t x_start = x / 8; + uint16_t x_end = x_start + w / 8 - 1; + uint16_t y_start = panel_h - 1 - y; + uint16_t y_end = y_start - h + 1; + + CMD(spi, 0x44); + DATA(spi, x_start); + DATA(spi, x_end); + + CMD(spi, 0x45); + DATA(spi, y_start & 0xFF); + DATA(spi, (y_start >> 8) & 0xFF); + DATA(spi, y_end & 0xFF); + DATA(spi, (y_end >> 8) & 0xFF); + + CMD(spi, 0x4E); + DATA(spi, x_start); + + CMD(spi, 0x4F); + DATA(spi, y_start & 0xFF); + DATA(spi, (y_start >> 8) & 0xFF); + + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, (w / 8) * h); + + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t gdey0154_sleep(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x10); + DATA(spi, 0x01); + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_LOGI(TAG, "Panel in deep sleep"); + return ESP_OK; +} + +esp_err_t gdey0154_wake(epd_device_t *dev) +{ + return gdey0154_init(dev); +} diff --git a/Libraries/esp_epaper/src/controllers/ssd16xx.c b/Libraries/esp_epaper/src/controllers/ssd16xx.c new file mode 100644 index 000000000..484d9b096 --- /dev/null +++ b/Libraries/esp_epaper/src/controllers/ssd16xx.c @@ -0,0 +1,291 @@ +/** + * @file ssd16xx.c + * @brief Generic driver for SSD1680/SSD1681 based B/W e-paper displays + * + * Supports panels: 1.54", 2.13", 2.66", 2.7", 2.9", 3.7", 4.2" + * All use same controller with different resolutions. + */ + +#include "../epaper_common.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include + +static const char *TAG = "ssd16xx"; + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t ssd16xx_init(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + const epd_panel_desc_t *panel = epd_get_panel(dev); + uint8_t source_shift = panel->source_shift_bytes; + uint16_t w = epd_get_width(dev); + uint16_t h = epd_get_height(dev); + + ESP_LOGI(TAG, "Init SSD16xx panel: %dx%d", w, h); + + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(10)); + WAIT(dev); + + CMD(spi, 0x12); // SWRESET + WAIT(dev); + + CMD(spi, 0x01); // Driver output control + DATA(spi, (h - 1) & 0xFF); + DATA(spi, (h - 1) >> 8); + DATA(spi, 0x00); + + CMD(spi, 0x11); // Data entry mode + DATA(spi, 0x03); // Y increment, X increment + + CMD(spi, 0x44); // Set RAM X start/end (offset by source_shift for shifted panels) + DATA(spi, source_shift); + DATA(spi, source_shift + (w / 8) - 1); + + CMD(spi, 0x45); // Set RAM Y start/end + DATA(spi, 0x00); + DATA(spi, 0x00); + DATA(spi, (h - 1) & 0xFF); + DATA(spi, (h - 1) >> 8); + + CMD(spi, 0x3C); // Border waveform + DATA(spi, 0x05); + + // Display update control. The second byte selects the OTP configuration: shifted + // panels (source_shift_bytes != 0, e.g. GDEY029T71H) need 0x00 here, others 0x80. + // GxEPD2: "OTP set for 168x384, but TFT shifted: S8..S175 connected to TFT sources". + CMD(spi, 0x21); // Display update control + DATA(spi, 0x00); + DATA(spi, panel->source_shift_bytes ? 0x00 : 0x80); + + CMD(spi, 0x18); // Temperature sensor + DATA(spi, 0x80); // Internal sensor + + CMD(spi, 0x4E); // Set RAM X counter (offset by source_shift) + DATA(spi, source_shift); + + CMD(spi, 0x4F); // Set RAM Y counter + DATA(spi, 0x00); + DATA(spi, 0x00); + + WAIT(dev); + + ESP_LOGI(TAG, "Panel initialized"); + return ESP_OK; +} + +/*============================================================================= + * Update Functions + *============================================================================*/ + +static esp_err_t ssd16xx_update_full(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + const epd_panel_desc_t *panel = epd_get_panel(dev); + + // Display update control: bypass RED as 0. GxEPD2 writes this before every full + // update. The fast variant additionally overrides the temperature register. + CMD(spi, 0x21); + DATA(spi, 0x40); + DATA(spi, 0x00); + + if (panel->fast_full_update) { + CMD(spi, 0x1A); // Write temperature register + DATA(spi, 0x6E); + DATA(spi, 0x00); + CMD(spi, 0x22); // Load temperature value + DATA(spi, 0x91); + CMD(spi, 0x20); + vTaskDelay(pdMS_TO_TICKS(2)); + + CMD(spi, 0x22); // Display update (reuse LUT) + DATA(spi, 0xC7); + } else { + CMD(spi, 0x22); // Display update (loads LUT, drives, powers down) + DATA(spi, 0xF7); + } + + CMD(spi, 0x20); // Master activation + WAIT(dev); + + return ESP_OK; +} + +static esp_err_t ssd16xx_update_fast(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + // Fast init sequence + RESET(spi); + vTaskDelay(pdMS_TO_TICKS(10)); + + CMD(spi, 0x12); // SWRESET + WAIT(dev); + + CMD(spi, 0x18); // Temperature sensor + DATA(spi, 0x80); + + CMD(spi, 0x22); // Load temperature + DATA(spi, 0xB1); + CMD(spi, 0x20); + WAIT(dev); + + CMD(spi, 0x1A); // Write temperature register + DATA(spi, 0x64); // 100°C for faster refresh + DATA(spi, 0x00); + + CMD(spi, 0x22); // Load temperature + DATA(spi, 0x91); + CMD(spi, 0x20); + WAIT(dev); + + // Fast update sequence + CMD(spi, 0x22); + DATA(spi, 0xC7); + CMD(spi, 0x20); + WAIT(dev); + + return ESP_OK; +} + +static esp_err_t ssd16xx_update_partial(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + const epd_panel_desc_t *panel = epd_get_panel(dev); + + if (panel->ssd1685_partial) { + // SSD1685 partial (GxEPD2 GDEY029T71H::_Update_Part): restore RED normal and + // run the powered fast partial LUT (0x22=0xDC), which keeps the panel powered + // so the next update starts without a full power-up cycle. + CMD(spi, 0x21); // Display update control + DATA(spi, 0x00); + DATA(spi, 0x00); + CMD(spi, 0x22); // Display update + DATA(spi, 0xDC); + } else { + CMD(spi, 0x3C); // Border waveform for partial + DATA(spi, 0x80); + CMD(spi, 0x22); // Display update + DATA(spi, 0xFF); // Partial update sequence + } + + CMD(spi, 0x20); // Master activation + WAIT(dev); + + return ESP_OK; +} + +esp_err_t ssd16xx_update(epd_device_t *dev, epd_update_mode_t mode) +{ + switch (mode) { + case EPD_UPDATE_FAST: + return ssd16xx_update_fast(dev); + case EPD_UPDATE_PARTIAL: + return ssd16xx_update_partial(dev); + case EPD_UPDATE_FULL: + default: + return ssd16xx_update_full(dev); + } +} + +/*============================================================================= + * RAM Write Functions + *============================================================================*/ + +esp_err_t ssd16xx_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len) +{ + epd_spi_t *spi = epd_get_spi(dev); + const epd_panel_desc_t *panel = epd_get_panel(dev); + bool partial_ready = epd_is_partial_ready(dev); + + // Reset RAM address to (source_shift, 0) + CMD(spi, 0x4E); + DATA(spi, panel->source_shift_bytes); + CMD(spi, 0x4F); + DATA(spi, 0x00); + DATA(spi, 0x00); + + // Write to current RAM (0x24) + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, len); + + // Only write to base image RAM (0x26) when setting up for partial refresh + // When partial_ready is true, we only update 0x24 so the display can + // compare old (0x26) vs new (0x24) for partial update + if (!partial_ready) { + CMD(spi, 0x4E); + DATA(spi, panel->source_shift_bytes); + CMD(spi, 0x4F); + DATA(spi, 0x00); + DATA(spi, 0x00); + + CMD(spi, 0x26); + epd_spi_write_data_bulk(spi, data, len); + } + + return ESP_OK; +} + +esp_err_t ssd16xx_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data) +{ + epd_spi_t *spi = epd_get_spi(dev); + const epd_panel_desc_t *panel = epd_get_panel(dev); + uint16_t panel_h = epd_get_height(dev); + + uint16_t x_start = x / 8 + panel->source_shift_bytes; + uint16_t x_end = x_start + w / 8 - 1; + uint16_t y_start = panel_h - 1 - y; + uint16_t y_end = y_start - h + 1; + + // Set window + CMD(spi, 0x44); + DATA(spi, x_start); + DATA(spi, x_end); + + CMD(spi, 0x45); + DATA(spi, y_start & 0xFF); + DATA(spi, (y_start >> 8) & 0xFF); + DATA(spi, y_end & 0xFF); + DATA(spi, (y_end >> 8) & 0xFF); + + // Set cursor + CMD(spi, 0x4E); + DATA(spi, x_start); + + CMD(spi, 0x4F); + DATA(spi, y_start & 0xFF); + DATA(spi, (y_start >> 8) & 0xFF); + + // Write RAM + CMD(spi, 0x24); + epd_spi_write_data_bulk(spi, data, (w / 8) * h); + + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t ssd16xx_sleep(epd_device_t *dev) +{ + epd_spi_t *spi = epd_get_spi(dev); + + CMD(spi, 0x10); // Deep sleep + DATA(spi, 0x01); + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_LOGI(TAG, "Panel in deep sleep"); + return ESP_OK; +} + +esp_err_t ssd16xx_wake(epd_device_t *dev) +{ + return ssd16xx_init(dev); +} diff --git a/Libraries/esp_epaper/src/epaper.c b/Libraries/esp_epaper/src/epaper.c new file mode 100644 index 000000000..417926db4 --- /dev/null +++ b/Libraries/esp_epaper/src/epaper.c @@ -0,0 +1,375 @@ +#include "epaper.h" +#include "epaper_panel.h" +#include "epaper_spi.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include +#include + +static const char *TAG = "epaper"; + +/*============================================================================= + * Device Structure + *============================================================================*/ + +struct epd_device { + epd_config_t config; + epd_spi_t spi; + const epd_panel_desc_t *panel; + const epd_controller_ops_t *ops; + uint16_t width; + uint16_t height; + uint32_t buffer_size; + uint8_t *framebuffer; + bool initialized; + bool partial_ready; + uint32_t partial_count; +}; + +/*============================================================================= + * Device Access Functions (used by controllers) + *============================================================================*/ + +epd_spi_t* epd_get_spi(epd_device_t *dev) +{ + return dev ? &dev->spi : NULL; +} + +uint16_t epd_get_width(epd_device_t *dev) +{ + return dev ? dev->width : 0; +} + +uint16_t epd_get_height(epd_device_t *dev) +{ + return dev ? dev->height : 0; +} + +const epd_panel_desc_t* epd_get_panel(epd_device_t *dev) +{ + return dev ? dev->panel : NULL; +} + +bool epd_is_partial_ready(epd_device_t *dev) +{ + return dev ? dev->partial_ready : false; +} + +/*============================================================================= + * Initialization + *============================================================================*/ + +esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle) +{ + if (!config || !handle) { + return ESP_ERR_INVALID_ARG; + } + + // Get panel descriptor from registry + const epd_panel_desc_t *panel = epd_get_panel_desc(config->panel.type); + if (!panel) { + ESP_LOGE(TAG, "Unknown panel type: %d", config->panel.type); + return ESP_ERR_NOT_SUPPORTED; + } + + // Get controller operations + const epd_controller_ops_t *ops = epd_get_controller_ops(panel->ctrl); + if (!ops) { + ESP_LOGE(TAG, "Unknown controller type: %d", panel->ctrl); + return ESP_ERR_NOT_SUPPORTED; + } + + // Allocate device + epd_device_t *dev = calloc(1, sizeof(epd_device_t)); + if (!dev) { + return ESP_ERR_NO_MEM; + } + + // Copy config and set references + memcpy(&dev->config, config, sizeof(epd_config_t)); + dev->panel = panel; + dev->ops = ops; + + // Set dimensions (allow override from config) + dev->width = config->panel.width > 0 ? config->panel.width : panel->width; + dev->height = config->panel.height > 0 ? config->panel.height : panel->height; + + // Calculate buffer size using panel's bits_per_pixel + dev->buffer_size = epd_calc_buffer_size(dev->width, dev->height, panel->bits_per_pixel); + + // Allocate framebuffer (prefer PSRAM for large buffers) + if (dev->buffer_size > 32768) { + dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + } else { + dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_DMA); + } + + if (!dev->framebuffer) { + ESP_LOGE(TAG, "Failed to allocate framebuffer (%lu bytes)", dev->buffer_size); + free(dev); + return ESP_ERR_NO_MEM; + } + + ESP_LOGI(TAG, "Framebuffer allocated: %lu bytes", dev->buffer_size); + memset(dev->framebuffer, 0xFF, dev->buffer_size); // White + + // Initialize SPI + esp_err_t ret = epd_spi_init(&dev->spi, &config->pins, &config->spi); + if (ret != ESP_OK) { + free(dev->framebuffer); + free(dev); + return ret; + } + + // Initialize panel via controller + ret = dev->ops->init(dev); + if (ret != ESP_OK) { + epd_spi_deinit(&dev->spi); + free(dev->framebuffer); + free(dev); + return ret; + } + + dev->initialized = true; + *handle = dev; + + ESP_LOGI(TAG, "Initialized %s (%dx%d)", panel->name, dev->width, dev->height); + return ESP_OK; +} + +esp_err_t epd_deinit(epd_handle_t handle) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + + if (dev->ops && dev->ops->sleep) { + dev->ops->sleep(dev); + } + + epd_spi_deinit(&dev->spi); + + if (dev->framebuffer) { + free(dev->framebuffer); + } + + free(dev); + return ESP_OK; +} + +/*============================================================================= + * Info & Status + *============================================================================*/ + +esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info) +{ + if (!handle || !info) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + info->width = dev->width; + info->height = dev->height; + info->buffer_size = dev->buffer_size; + info->color_mode = dev->panel->color_mode; + return ESP_OK; +} + +bool epd_is_busy(epd_handle_t handle) +{ + if (!handle) return false; + epd_device_t *dev = handle; + return epd_spi_is_busy(&dev->spi) != 0; +} + +esp_err_t epd_wait_busy(epd_handle_t handle, uint32_t timeout_ms) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + epd_device_t *dev = handle; + epd_spi_wait_busy(&dev->spi, timeout_ms); + return ESP_OK; +} + +/*============================================================================= + * Display Operations + *============================================================================*/ + +esp_err_t epd_clear(epd_handle_t handle) +{ + return epd_fill(handle, 0xFF); +} + +esp_err_t epd_fill(epd_handle_t handle, uint8_t color) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + memset(dev->framebuffer, color, dev->buffer_size); + + return epd_update(handle, dev->framebuffer, EPD_UPDATE_FULL); +} + +esp_err_t epd_update(epd_handle_t handle, const uint8_t *buffer, epd_update_mode_t mode) +{ + if (!handle || !buffer) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + esp_err_t ret; + + // Check capabilities and fallback if needed + if (mode == EPD_UPDATE_PARTIAL && !epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) { + ESP_LOGD(TAG, "Partial not supported, using full refresh"); + mode = EPD_UPDATE_FULL; + } + if (mode == EPD_UPDATE_FAST && !epd_panel_has_cap(dev->panel, EPD_CAP_FAST)) { + ESP_LOGD(TAG, "Fast not supported, using full refresh"); + mode = EPD_UPDATE_FULL; + } + + // Periodic full refresh: repeated partial writes on the same GDDRAM base + // image accumulate ghosting, so every Nth partial refresh the whole panel. + // The conversion redraws the identical current buffer, so the base image + // stays valid and subsequent partials can continue immediately. + bool converted_to_full = false; + if (mode == EPD_UPDATE_PARTIAL && dev->partial_ready && dev->panel->full_refresh_interval > 0) { + if (++dev->partial_count >= dev->panel->full_refresh_interval) { + dev->partial_count = 0; + mode = EPD_UPDATE_FULL; + converted_to_full = true; + ESP_LOGI(TAG, "Periodic full refresh after %d partial updates", dev->panel->full_refresh_interval); + } + } + + // Copy to framebuffer (callers may pass the internal framebuffer itself) + if (buffer != dev->framebuffer) { + memcpy(dev->framebuffer, buffer, dev->buffer_size); + } + + // For partial mode on first call, do a full refresh to set base image + if (mode == EPD_UPDATE_PARTIAL && !dev->partial_ready) { + if (!dev->initialized) { + ret = dev->ops->init(dev); + if (ret != ESP_OK) return ret; + dev->initialized = true; + } + + ret = dev->ops->write_ram(dev, buffer, dev->buffer_size); + if (ret != ESP_OK) return ret; + + ret = dev->ops->update(dev, EPD_UPDATE_FULL); + if (ret != ESP_OK) return ret; + + dev->partial_ready = true; + ESP_LOGI(TAG, "Partial mode ready"); + return ESP_OK; + } + + // Full refresh resets partial mode (unless it was the periodic conversion, + // which refreshed the same base image and keeps partial mode valid) + if (mode == EPD_UPDATE_FULL && !converted_to_full) { + dev->partial_ready = false; + if (!dev->initialized) { + ret = dev->ops->init(dev); + if (ret != ESP_OK) return ret; + dev->initialized = true; + } + } + + // Write RAM and update + ret = dev->ops->write_ram(dev, buffer, dev->buffer_size); + if (ret != ESP_OK) return ret; + + return dev->ops->update(dev, mode); +} + +esp_err_t epd_update_partial(epd_handle_t handle, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *buffer) +{ + if (!handle || !buffer) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + + if (!epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) { + return ESP_ERR_NOT_SUPPORTED; + } + + if (!dev->ops->write_ram_partial) { + return ESP_ERR_NOT_SUPPORTED; + } + + esp_err_t ret = dev->ops->write_ram_partial(dev, x, y, w, h, buffer); + if (ret != ESP_OK) return ret; + + return dev->ops->update(dev, EPD_UPDATE_PARTIAL); +} + +esp_err_t epd_set_base_image(epd_handle_t handle, const uint8_t *buffer) +{ + if (!handle || !buffer) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + memcpy(dev->framebuffer, buffer, dev->buffer_size); + return ESP_OK; +} + +/*============================================================================= + * Power Management + *============================================================================*/ + +esp_err_t epd_sleep(epd_handle_t handle) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + if (dev->ops->sleep) { + // Deep sleep clears GDDRAM, so the base image for partial updates is + // lost; the next partial update must re-establish it with a full refresh. + dev->partial_ready = false; + dev->partial_count = 0; + dev->initialized = false; + return dev->ops->sleep(dev); + } + return ESP_OK; +} + +esp_err_t epd_wake(epd_handle_t handle) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + if (dev->ops->wake) { + esp_err_t ret = dev->ops->wake(dev); + if (ret == ESP_OK) { + dev->initialized = true; + // Wake re-initializes the panel and clears GDDRAM, so the next + // partial update must re-establish the base with a full refresh. + dev->partial_ready = false; + dev->partial_count = 0; + } + return ret; + } + return dev->ops->init(dev); +} + +/*============================================================================= + * Framebuffer Access + *============================================================================*/ + +uint8_t* epd_get_framebuffer(epd_handle_t handle) +{ + if (!handle) return NULL; + return handle->framebuffer; +} + +esp_err_t epd_flush_framebuffer(epd_handle_t handle, epd_update_mode_t mode) +{ + if (!handle) return ESP_ERR_INVALID_ARG; + return epd_update(handle, handle->framebuffer, mode); +} + +esp_err_t epd_lvgl_flush(epd_handle_t handle, const void *area, const uint8_t *px_map) +{ + if (!handle || !px_map) return ESP_ERR_INVALID_ARG; + + epd_device_t *dev = handle; + memcpy(dev->framebuffer, px_map, dev->buffer_size); + return ESP_OK; +} diff --git a/Libraries/esp_epaper/src/epaper_common.c b/Libraries/esp_epaper/src/epaper_common.c new file mode 100644 index 000000000..5462bc0dc --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_common.c @@ -0,0 +1,28 @@ +#include "epaper_common.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" + +static const char *TAG = "epd_common"; + +void epd_wait_busy_polarity(epd_spi_t *spi, uint32_t caps, uint32_t timeout_ms) +{ + bool inverted = (caps & EPD_CAP_BUSY_INV); + uint32_t start = xTaskGetTickCount(); + + // For inverted: wait while LOW (busy), ready when HIGH + // For normal: wait while HIGH (busy), ready when LOW + int busy_level = inverted ? 0 : 1; + + while (gpio_get_level(spi->pin_busy) == busy_level) { + if (timeout_ms > 0) { + uint32_t elapsed = (xTaskGetTickCount() - start) * portTICK_PERIOD_MS; + if (elapsed >= timeout_ms) { + ESP_LOGW(TAG, "Wait busy timeout (%lu ms)", timeout_ms); + return; + } + } + vTaskDelay(pdMS_TO_TICKS(5)); + } +} diff --git a/Libraries/esp_epaper/src/epaper_common.h b/Libraries/esp_epaper/src/epaper_common.h new file mode 100644 index 000000000..a0d264fe4 --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_common.h @@ -0,0 +1,41 @@ +#ifndef _EPAPER_COMMON_H_ +#define _EPAPER_COMMON_H_ + +#include +#include +#include "esp_err.h" +#include "epaper_spi.h" +#include "epaper_panel.h" + +// Forward declaration +typedef struct epd_device epd_device_t; + +/*============================================================================= + * Device Access Functions (implemented in epaper.c) + *============================================================================*/ +epd_spi_t* epd_get_spi(epd_device_t *dev); +uint16_t epd_get_width(epd_device_t *dev); +uint16_t epd_get_height(epd_device_t *dev); +const epd_panel_desc_t* epd_get_panel(epd_device_t *dev); +bool epd_is_partial_ready(epd_device_t *dev); + +/*============================================================================= + * Common SPI Macros + *============================================================================*/ +#define CMD(spi, c) epd_spi_write_cmd(spi, c) +#define DATA(spi, d) epd_spi_write_data(spi, d) +#define RESET(spi) epd_spi_reset(spi) + +/*============================================================================= + * Busy Wait with Polarity Support + *============================================================================*/ + +// Wait for panel ready, respecting busy signal polarity +// caps: panel capability flags (use EPD_CAP_BUSY_INV for inverted) +// timeout_ms: timeout in ms, 0 = infinite +void epd_wait_busy_polarity(epd_spi_t *spi, uint32_t caps, uint32_t timeout_ms); + +// Convenience macro for controllers +#define WAIT(dev) epd_wait_busy_polarity(epd_get_spi(dev), epd_get_panel(dev)->caps, 0) + +#endif // _EPAPER_COMMON_H_ diff --git a/Libraries/esp_epaper/src/epaper_lvgl.c b/Libraries/esp_epaper/src/epaper_lvgl.c new file mode 100644 index 000000000..d02c033ed --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_lvgl.c @@ -0,0 +1,622 @@ +#include "epaper_lvgl.h" +#include "epaper_panel.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include + +static const char *TAG = "epd_lvgl"; + +// External function to get panel descriptor +extern const epd_panel_desc_t* epd_get_panel(void *dev); + +/*============================================================================= + * Color Palette for Multi-Color E-Paper + *============================================================================*/ + +// Pre-defined RGB values for e-paper colors +static const struct { + uint8_t r, g, b; + uint8_t epaper_color; +} color_palette[] = { + {0, 0, 0, EPD_PIXEL_BLACK}, // Black + {255, 255, 255, EPD_PIXEL_WHITE}, // White + {255, 255, 0, EPD_PIXEL_YELLOW}, // Yellow + {255, 0, 0, EPD_PIXEL_RED}, // Red + {0, 0, 255, EPD_PIXEL_BLUE}, // Blue + {0, 255, 0, EPD_PIXEL_GREEN}, // Green +}; + +#define PALETTE_SIZE (sizeof(color_palette) / sizeof(color_palette[0])) + +// BW-only palette +static const struct { + uint8_t r, g, b; + uint8_t epaper_color; +} bw_palette[] = { + {0, 0, 0, EPD_PIXEL_BLACK}, + {255, 255, 255, EPD_PIXEL_WHITE}, +}; + +#define BW_PALETTE_SIZE 2 + +/*============================================================================= + * Port Context + *============================================================================*/ + +typedef struct { + epd_handle_t epd; + epd_update_mode_t update_mode; + epd_dither_mode_t dither_mode; + epd_color_mode_t color_mode; + uint8_t bits_per_pixel; // From panel descriptor + bool use_partial_refresh; + uint32_t partial_threshold; + uint8_t *lvgl_buf; // RGB565 buffer for LVGL + uint8_t *rgb_buf; // RGB888 buffer for dithering + uint32_t lvgl_buf_size; + uint16_t width; + uint16_t height; + bool force_full; + uint32_t partial_count; + bool partial_render_mode; // True if using small buffer with partial rendering + bool fb_dirty; // Framebuffer has been modified +} epd_lvgl_ctx_t; + +/*============================================================================= + * Color Conversion Functions + *============================================================================*/ + +static inline int clamp_byte(int v) { + return v < 0 ? 0 : (v > 255 ? 255 : v); +} + +// 4-color BWRY palette +static const struct { + uint8_t r, g, b; + uint8_t epaper_color; +} bwry_palette[] = { + {0, 0, 0, EPD_PIXEL_BLACK}, // Black + {255, 255, 255, EPD_PIXEL_WHITE}, // White + {255, 255, 0, EPD_PIXEL_YELLOW}, // Yellow + {255, 0, 0, EPD_PIXEL_RED}, // Red +}; + +#define BWRY_PALETTE_SIZE 4 + +// Find nearest palette color (returns palette index) +static uint8_t find_nearest_color_idx(int r, int g, int b, epd_color_mode_t mode) +{ + uint32_t min_dist = UINT32_MAX; + uint8_t best = 0; + + if (mode == EPD_COLOR_BW) { + // BW mode: simple threshold + int gray = (r * 299 + g * 587 + b * 114) / 1000; + return (gray < 128) ? 0 : 1; // 0=black, 1=white + } + + if (mode == EPD_COLOR_4COLOR) { + // 4-color BWRY mode + for (int i = 0; i < BWRY_PALETTE_SIZE; i++) { + int dr = r - bwry_palette[i].r; + int dg = g - bwry_palette[i].g; + int db = b - bwry_palette[i].b; + uint32_t dist = dr * dr + dg * dg + db * db; + + if (dist < min_dist) { + min_dist = dist; + best = i; + } + } + return best; + } + + // Multi-color mode (6-color, 7-color) + for (int i = 0; i < PALETTE_SIZE; i++) { + int dr = r - color_palette[i].r; + int dg = g - color_palette[i].g; + int db = b - color_palette[i].b; + uint32_t dist = dr * dr + dg * dg + db * db; + + if (dist < min_dist) { + min_dist = dist; + best = i; + } + } + return best; +} + +// Convert RGB to e-paper color +uint8_t epd_rgb_to_epaper_color(uint8_t r, uint8_t g, uint8_t b, epd_color_mode_t mode) +{ + if (mode == EPD_COLOR_BW) { + int gray = (r * 299 + g * 587 + b * 114) / 1000; + return (gray < 128) ? EPD_PIXEL_BLACK : EPD_PIXEL_WHITE; + } + + if (mode == EPD_COLOR_4COLOR) { + uint8_t idx = find_nearest_color_idx(r, g, b, mode); + return bwry_palette[idx].epaper_color; + } + + uint8_t idx = find_nearest_color_idx(r, g, b, mode); + return color_palette[idx].epaper_color; +} + +// Convert e-paper color to LVGL color +lv_color_t epd_epaper_to_lv_color(uint8_t epaper_color) +{ + switch (epaper_color) { + case EPD_PIXEL_BLACK: return lv_color_make(0, 0, 0); + case EPD_PIXEL_WHITE: return lv_color_make(255, 255, 255); + case EPD_PIXEL_YELLOW: return lv_color_make(255, 255, 0); + case EPD_PIXEL_RED: return lv_color_make(255, 0, 0); + case EPD_PIXEL_BLUE: return lv_color_make(0, 0, 255); + case EPD_PIXEL_GREEN: return lv_color_make(0, 255, 0); + default: return lv_color_make(255, 255, 255); + } +} + +/*============================================================================= + * Dithering Algorithms + *============================================================================*/ + +// Bayer 4x4 ordered dithering matrix (values 0-15, scaled to 0-255) +static const uint8_t BAYER_4X4[4][4] = { + { 0, 128, 32, 160 }, + { 192, 64, 224, 96 }, + { 48, 176, 16, 144 }, + { 240, 112, 208, 80 } +}; + +// Set pixel in framebuffer based on color mode +static inline void set_fb_pixel(uint8_t *fb, int x, int y, int width, uint8_t color, epd_color_mode_t mode) +{ + if (mode == EPD_COLOR_6COLOR || mode == EPD_COLOR_7COLOR) { + // 4-bit per pixel (2 pixels per byte) + uint32_t addr = (y * width + x) / 2; + if (x % 2 == 0) { + // High nibble (left pixel) + fb[addr] = (fb[addr] & 0x0F) | (color << 4); + } else { + // Low nibble (right pixel) + fb[addr] = (fb[addr] & 0xF0) | (color & 0x0F); + } + } else if (mode == EPD_COLOR_4COLOR || mode == EPD_COLOR_4GRAY) { + // 2-bit per pixel (4 pixels per byte) + // Pixel order: [P0:7-6][P1:5-4][P2:3-2][P3:1-0] + uint32_t pixel_idx = y * width + x; + uint32_t byte_idx = pixel_idx / 4; + uint8_t bit_offset = (3 - (pixel_idx % 4)) * 2; // 6, 4, 2, 0 + uint8_t mask = 0x03 << bit_offset; + fb[byte_idx] = (fb[byte_idx] & ~mask) | ((color & 0x03) << bit_offset); + } else { + // 1-bit per pixel (BW) + uint16_t byte_idx = y * (width / 8) + (x / 8); + uint8_t bit = 7 - (x % 8); + if (color == EPD_PIXEL_WHITE) { + fb[byte_idx] |= (1 << bit); + } else { + fb[byte_idx] &= ~(1 << bit); + } + } +} + +static void apply_floyd_steinberg_dithering(epd_lvgl_ctx_t *ctx, uint8_t *fb) +{ + if (!ctx->rgb_buf || !fb) return; + + ESP_LOGI(TAG, "Applying Floyd-Steinberg dithering..."); + + for (int y = 0; y < ctx->height; y++) { + // Yield every 20 rows to prevent watchdog + if (y % 20 == 0) { + vTaskDelay(1); + } + + for (int x = 0; x < ctx->width; x++) { + int idx = (y * ctx->width + x) * 3; + + // Get current pixel RGB + int r = ctx->rgb_buf[idx + 0]; + int g = ctx->rgb_buf[idx + 1]; + int b = ctx->rgb_buf[idx + 2]; + + // Find nearest palette color + uint8_t pal_idx = find_nearest_color_idx(r, g, b, ctx->color_mode); + uint8_t epaper_color; + int pal_r, pal_g, pal_b; + + if (ctx->color_mode == EPD_COLOR_BW) { + epaper_color = (pal_idx == 0) ? EPD_PIXEL_BLACK : EPD_PIXEL_WHITE; + pal_r = bw_palette[pal_idx].r; + pal_g = bw_palette[pal_idx].g; + pal_b = bw_palette[pal_idx].b; + } else if (ctx->color_mode == EPD_COLOR_4COLOR) { + epaper_color = bwry_palette[pal_idx].epaper_color; + pal_r = bwry_palette[pal_idx].r; + pal_g = bwry_palette[pal_idx].g; + pal_b = bwry_palette[pal_idx].b; + } else { + epaper_color = color_palette[pal_idx].epaper_color; + pal_r = color_palette[pal_idx].r; + pal_g = color_palette[pal_idx].g; + pal_b = color_palette[pal_idx].b; + } + + // Set pixel in framebuffer + set_fb_pixel(fb, x, y, ctx->width, epaper_color, ctx->color_mode); + + // Calculate quantization error + int err_r = r - pal_r; + int err_g = g - pal_g; + int err_b = b - pal_b; + + // Distribute error to neighbors (Floyd-Steinberg) + // Right pixel: 7/16 + if (x + 1 < ctx->width) { + int ni = idx + 3; + ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 7 / 16); + ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 7 / 16); + ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 7 / 16); + } + // Bottom-left pixel: 3/16 + if (y + 1 < ctx->height && x > 0) { + int ni = ((y + 1) * ctx->width + x - 1) * 3; + ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 3 / 16); + ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 3 / 16); + ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 3 / 16); + } + // Bottom pixel: 5/16 + if (y + 1 < ctx->height) { + int ni = ((y + 1) * ctx->width + x) * 3; + ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 5 / 16); + ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 5 / 16); + ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 5 / 16); + } + // Bottom-right pixel: 1/16 + if (y + 1 < ctx->height && x + 1 < ctx->width) { + int ni = ((y + 1) * ctx->width + x + 1) * 3; + ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r / 16); + ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g / 16); + ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b / 16); + } + } + } + + ESP_LOGI(TAG, "Dithering complete"); +} + +/*============================================================================= + * Flush Callback - Convert RGB565 to E-Paper Format + *============================================================================*/ + +static void epd_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_p) +{ + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + if (!ctx || !ctx->epd) { + lv_display_flush_ready(disp); + return; + } + + uint16_t *buffer = (uint16_t *)color_p; + uint8_t *fb = epd_get_framebuffer(ctx->epd); + + if (!fb) { + lv_display_flush_ready(disp); + return; + } + + bool use_floyd_steinberg = (ctx->dither_mode == EPD_DITHER_FLOYD_STEINBERG) && ctx->rgb_buf; + // Use ordered dither when: explicitly set OR when Floyd-Steinberg requested but no RGB buffer + bool use_ordered_dither = (ctx->dither_mode == EPD_DITHER_ORDERED) || + ((ctx->dither_mode == EPD_DITHER_FLOYD_STEINBERG) && !ctx->rgb_buf); + + if (use_floyd_steinberg) { + // Store RGB888 in buffer for dithering + for (int y = area->y1; y <= area->y2; y++) { + for (int x = area->x1; x <= area->x2; x++) { + // RGB565 to RGB888 with proper bit expansion + // This ensures white (31,63,31) -> (255,255,255) + uint16_t c = *buffer++; + uint8_t r5 = (c >> 11) & 0x1F; + uint8_t g6 = (c >> 5) & 0x3F; + uint8_t b5 = c & 0x1F; + uint8_t r = (r5 << 3) | (r5 >> 2); // 5-bit to 8-bit + uint8_t g = (g6 << 2) | (g6 >> 4); // 6-bit to 8-bit + uint8_t b = (b5 << 3) | (b5 >> 2); // 5-bit to 8-bit + + int idx = (y * ctx->width + x) * 3; + ctx->rgb_buf[idx + 0] = r; + ctx->rgb_buf[idx + 1] = g; + ctx->rgb_buf[idx + 2] = b; + } + // Yield periodically + if ((y % 50) == 0) { + vTaskDelay(1); + } + } + + // Apply dithering after all pixels collected + bool is_last = (area->x2 == ctx->width - 1) && (area->y2 == ctx->height - 1); + if (is_last) { + // Clear framebuffer first using bits_per_pixel from panel + uint32_t fb_size = epd_calc_buffer_size(ctx->width, ctx->height, ctx->bits_per_pixel); + uint8_t fill_byte; + if (ctx->bits_per_pixel == 4) { + fill_byte = (EPD_PIXEL_WHITE << 4) | EPD_PIXEL_WHITE; + } else if (ctx->bits_per_pixel == 2) { + fill_byte = 0x55; // White (01) for all 4 pixels + } else { + fill_byte = 0xFF; // 1-bit white + } + memset(fb, fill_byte, fb_size); + + apply_floyd_steinberg_dithering(ctx, fb); + } + } else { + // Direct conversion with optional ordered (Bayer) dithering + // Clear framebuffer only on first chunk (top-left corner) + bool is_first = (area->x1 == 0 && area->y1 == 0); + if (is_first) { + uint32_t fb_size = epd_calc_buffer_size(ctx->width, ctx->height, ctx->bits_per_pixel); + uint8_t fill_byte; + if (ctx->bits_per_pixel == 4) { + fill_byte = (EPD_PIXEL_WHITE << 4) | EPD_PIXEL_WHITE; + } else if (ctx->bits_per_pixel == 2) { + fill_byte = 0x55; // White (01) for all 4 pixels + } else { + fill_byte = 0xFF; // 1-bit white + } + memset(fb, fill_byte, fb_size); + } + + for (int y = area->y1; y <= area->y2; y++) { + for (int x = area->x1; x <= area->x2; x++) { + // RGB565 to RGB888 with proper bit expansion + uint16_t c = *buffer++; + uint8_t r5 = (c >> 11) & 0x1F; + uint8_t g6 = (c >> 5) & 0x3F; + uint8_t b5 = c & 0x1F; + uint8_t r = (r5 << 3) | (r5 >> 2); + uint8_t g = (g6 << 2) | (g6 >> 4); + uint8_t b = (b5 << 3) | (b5 >> 2); + + uint8_t epaper_color; + + if (use_ordered_dither) { + // Bayer ordered dithering (no extra buffer needed) + uint8_t threshold = BAYER_4X4[y % 4][x % 4]; + + if (ctx->color_mode == EPD_COLOR_BW) { + int gray = (r * 299 + g * 587 + b * 114) / 1000; + epaper_color = (gray > threshold) ? EPD_PIXEL_WHITE : EPD_PIXEL_BLACK; + } else if (ctx->color_mode == EPD_COLOR_4COLOR) { + // Ordered dithering for 4-color BWRY + // Adjust RGB values with dither threshold for better color selection + int dr = r + (threshold - 128) / 2; + int dg = g + (threshold - 128) / 2; + int db = b + (threshold - 128) / 2; + dr = dr < 0 ? 0 : (dr > 255 ? 255 : dr); + dg = dg < 0 ? 0 : (dg > 255 ? 255 : dg); + db = db < 0 ? 0 : (db > 255 ? 255 : db); + epaper_color = epd_rgb_to_epaper_color(dr, dg, db, ctx->color_mode); + } else { + // Other multi-color modes + epaper_color = epd_rgb_to_epaper_color(r, g, b, ctx->color_mode); + } + } else { + // Simple threshold conversion (no dithering) + epaper_color = epd_rgb_to_epaper_color(r, g, b, ctx->color_mode); + } + + // Set pixel (format depends on color mode) + set_fb_pixel(fb, x, y, ctx->width, epaper_color, ctx->color_mode); + } + // Yield periodically + if ((y % 50) == 0) { + vTaskDelay(1); + } + } + } + + // Mark framebuffer as dirty + ctx->fb_dirty = true; + + // Check if this is the last chunk (bottom-right corner) + bool is_last = (area->x2 >= ctx->width - 1) && (area->y2 >= ctx->height - 1); + + // Only update display when: + // 1. Full render mode (not partial) - update every flush + // 2. Partial render mode - only update on last chunk + if (!ctx->partial_render_mode || is_last) { + // Determine update mode + epd_update_mode_t mode = ctx->update_mode; + if (ctx->force_full) { + mode = EPD_UPDATE_FULL; + ctx->force_full = false; + ctx->partial_count = 0; + } + + // Update display + epd_flush_framebuffer(ctx->epd, mode); + ctx->fb_dirty = false; + } + + lv_display_flush_ready(disp); +} + +/*============================================================================= + * Public API + *============================================================================*/ + +lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config) +{ + if (!config || !config->epd) { + ESP_LOGE(TAG, "Invalid config"); + return NULL; + } + + // Get panel info + epd_panel_info_t info; + if (epd_get_info(config->epd, &info) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get panel info"); + return NULL; + } + + // Allocate context + epd_lvgl_ctx_t *ctx = calloc(1, sizeof(epd_lvgl_ctx_t)); + if (!ctx) { + ESP_LOGE(TAG, "Failed to allocate context"); + return NULL; + } + + ctx->epd = config->epd; + ctx->update_mode = config->update_mode; + ctx->dither_mode = config->dither_mode; + ctx->color_mode = info.color_mode; + ctx->use_partial_refresh = config->use_partial_refresh; + ctx->partial_threshold = config->partial_threshold; + ctx->width = info.width; + ctx->height = info.height; + ctx->force_full = false; + ctx->partial_count = 0; + + // Get bits_per_pixel from panel descriptor + const epd_panel_desc_t *panel = epd_get_panel(config->epd); + ctx->bits_per_pixel = panel ? panel->bits_per_pixel : 1; + + // Calculate buffer size based on available memory + // Full screen buffer for PSRAM, partial buffer for internal RAM only + uint32_t full_buf_size = info.width * info.height * sizeof(uint16_t); + bool use_partial_render = false; + + // Try SPIRAM first for full buffer + ctx->lvgl_buf = heap_caps_malloc(full_buf_size, MALLOC_CAP_SPIRAM); + if (ctx->lvgl_buf) { + ctx->lvgl_buf_size = full_buf_size; + ESP_LOGI(TAG, "Using SPIRAM for full buffer: %lu bytes", ctx->lvgl_buf_size); + } else { + // No SPIRAM - use partial rendering with smaller buffer + // Buffer for 10 lines at a time (saves memory on no-PSRAM boards) + uint32_t partial_lines = 10; + uint32_t partial_buf_size = info.width * partial_lines * sizeof(uint16_t); + + ctx->lvgl_buf = heap_caps_malloc(partial_buf_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (!ctx->lvgl_buf) { + ctx->lvgl_buf = malloc(partial_buf_size); + } + + if (ctx->lvgl_buf) { + ctx->lvgl_buf_size = partial_buf_size; + ctx->partial_render_mode = true; + use_partial_render = true; + ESP_LOGI(TAG, "No SPIRAM, using partial render buffer: %lu bytes (%lu lines)", + ctx->lvgl_buf_size, partial_lines); + } + } + + if (!ctx->lvgl_buf) { + ESP_LOGE(TAG, "Failed to allocate LVGL buffer"); + free(ctx); + return NULL; + } + + // Allocate RGB buffer for dithering if enabled + if (config->dither_mode == EPD_DITHER_FLOYD_STEINBERG) { + size_t rgb_buf_size = info.width * info.height * 3; // RGB888 + ctx->rgb_buf = heap_caps_malloc(rgb_buf_size, MALLOC_CAP_SPIRAM); + if (!ctx->rgb_buf) { + ctx->rgb_buf = heap_caps_malloc(rgb_buf_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + } + if (ctx->rgb_buf) { + memset(ctx->rgb_buf, 255, rgb_buf_size); // White + ESP_LOGI(TAG, "Floyd-Steinberg dithering enabled, RGB buffer: %zu bytes", rgb_buf_size); + } else { + ESP_LOGW(TAG, "Not enough RAM for Floyd-Steinberg, using Bayer ordered dithering"); + } + } + + // Create LVGL display + lv_display_t *disp = lv_display_create(info.width, info.height); + if (!disp) { + ESP_LOGE(TAG, "Failed to create display"); + if (ctx->rgb_buf) free(ctx->rgb_buf); + free(ctx->lvgl_buf); + free(ctx); + return NULL; + } + + lv_display_set_user_data(disp, ctx); + lv_display_set_flush_cb(disp, epd_lvgl_flush_cb); + + // Use partial render mode if buffer is smaller than full screen + lv_display_render_mode_t render_mode = use_partial_render ? + LV_DISPLAY_RENDER_MODE_PARTIAL : LV_DISPLAY_RENDER_MODE_FULL; + lv_display_set_buffers(disp, ctx->lvgl_buf, NULL, ctx->lvgl_buf_size, render_mode); + + ESP_LOGI(TAG, "LVGL display initialized: %dx%d, dither=%d, render=%s", + info.width, info.height, config->dither_mode, + use_partial_render ? "partial" : "full"); + + return disp; +} + +void epd_lvgl_deinit(lv_display_t *disp) +{ + if (!disp) return; + + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + if (ctx) { + if (ctx->lvgl_buf) free(ctx->lvgl_buf); + if (ctx->rgb_buf) free(ctx->rgb_buf); + free(ctx); + } + + lv_display_delete(disp); +} + +void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode) +{ + if (!disp) return; + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + if (ctx) { + ctx->update_mode = mode; + } +} + +void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode) +{ + if (!disp) return; + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + if (ctx) { + ctx->dither_mode = mode; + ESP_LOGI(TAG, "Dither mode set to %d", mode); + } +} + +void epd_lvgl_force_full_refresh(lv_display_t *disp) +{ + if (!disp) return; + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + if (ctx) { + ctx->force_full = true; + } +} + +void epd_lvgl_refresh(lv_display_t *disp) +{ + if (!disp) return; + lv_obj_invalidate(lv_screen_active()); + lv_refr_now(disp); +} + +epd_handle_t epd_lvgl_get_epd(lv_display_t *disp) +{ + if (!disp) return NULL; + epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp); + return ctx ? ctx->epd : NULL; +} diff --git a/Libraries/esp_epaper/src/epaper_registry.c b/Libraries/esp_epaper/src/epaper_registry.c new file mode 100644 index 000000000..f6f182140 --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_registry.c @@ -0,0 +1,219 @@ +/** + * @file epaper_registry.c + * @brief Data-driven panel registry and controller lookup + * + * To add a new panel: + * 1. Add enum in epaper_config.h + * 2. Add entry in panel_registry[] below + * + * To add a new controller: + * 1. Add enum in epaper_panel.h (epd_controller_type_t) + * 2. Implement controller in src/controllers/ + * 3. Add entry in controller_ops[] below + */ + +#include "epaper_panel.h" +#include "epaper_config.h" + +/*============================================================================= + * Controller Operations (forward declarations) + *============================================================================*/ + +// SSD16xx generic BW controller +extern esp_err_t ssd16xx_init(epd_device_t *dev); +extern esp_err_t ssd16xx_update(epd_device_t *dev, epd_update_mode_t mode); +extern esp_err_t ssd16xx_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len); +extern esp_err_t ssd16xx_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data); +extern esp_err_t ssd16xx_sleep(epd_device_t *dev); +extern esp_err_t ssd16xx_wake(epd_device_t *dev); + +// GDEY0154D67 with custom LUT +extern esp_err_t gdey0154_init(epd_device_t *dev); +extern esp_err_t gdey0154_update(epd_device_t *dev, epd_update_mode_t mode); +extern esp_err_t gdey0154_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len); +extern esp_err_t gdey0154_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y, + uint16_t w, uint16_t h, const uint8_t *data); +extern esp_err_t gdey0154_sleep(epd_device_t *dev); +extern esp_err_t gdey0154_wake(epd_device_t *dev); + +// ACeP 6-color controller +extern esp_err_t acep6c_init(epd_device_t *dev); +extern esp_err_t acep6c_update(epd_device_t *dev, epd_update_mode_t mode); +extern esp_err_t acep6c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len); +extern esp_err_t acep6c_sleep(epd_device_t *dev); +extern esp_err_t acep6c_wake(epd_device_t *dev); + +// BWRY 4-color controller +extern esp_err_t bwry4c_init(epd_device_t *dev); +extern esp_err_t bwry4c_update(epd_device_t *dev, epd_update_mode_t mode); +extern esp_err_t bwry4c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len); +extern esp_err_t bwry4c_sleep(epd_device_t *dev); +extern esp_err_t bwry4c_wake(epd_device_t *dev); + +/*============================================================================= + * Controller Operations Table + *============================================================================*/ + +static const epd_controller_ops_t controller_ops[EPD_CTRL_COUNT] = { + [EPD_CTRL_SSD16XX] = { + .init = ssd16xx_init, + .update = ssd16xx_update, + .write_ram = ssd16xx_write_ram, + .write_ram_partial = ssd16xx_write_ram_partial, + .sleep = ssd16xx_sleep, + .wake = ssd16xx_wake, + }, + [EPD_CTRL_GDEY0154_LUT] = { + .init = gdey0154_init, + .update = gdey0154_update, + .write_ram = gdey0154_write_ram, + .write_ram_partial = gdey0154_write_ram_partial, + .sleep = gdey0154_sleep, + .wake = gdey0154_wake, + }, + [EPD_CTRL_ACEP_6COLOR] = { + .init = acep6c_init, + .update = acep6c_update, + .write_ram = acep6c_write_ram, + .write_ram_partial = NULL, // Not supported + .sleep = acep6c_sleep, + .wake = acep6c_wake, + }, + [EPD_CTRL_BWRY_4COLOR] = { + .init = bwry4c_init, + .update = bwry4c_update, + .write_ram = bwry4c_write_ram, + .write_ram_partial = NULL, // Not supported + .sleep = bwry4c_sleep, + .wake = bwry4c_wake, + }, +}; + +/*============================================================================= + * Panel Registry Table + * + * Format: { name, width, height, color_mode, bpp, caps, controller, init_data } + *============================================================================*/ + +static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = { + // Specific panels with custom features + [EPD_PANEL_GDEY0154D67] = { + .name = "GDEY0154D67", + .width = 200, .height = 200, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_GDEY0154_LUT, + .init_data = NULL, + }, + [EPD_PANEL_GDEP073E01] = { + .name = "GDEP073E01", + .width = 800, .height = 480, + .color_mode = EPD_COLOR_6COLOR, .bits_per_pixel = 4, + .caps = EPD_CAP_BUSY_INV, // Inverted busy, no partial/fast + .ctrl = EPD_CTRL_ACEP_6COLOR, + .init_data = NULL, + }, + [EPD_PANEL_GDEY037F51] = { + .name = "GDEY037F51", + .width = 240, .height = 416, + .color_mode = EPD_COLOR_4COLOR, .bits_per_pixel = 2, + .caps = EPD_CAP_BUSY_INV, // No partial/fast support + .ctrl = EPD_CTRL_BWRY_4COLOR, + .init_data = NULL, + }, + [EPD_PANEL_GDEY029T71H] = { + .name = "GDEY029T71H", + .width = 168, .height = 384, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .source_shift_bytes = 1, + .fast_full_update = false, + .ssd1685_partial = true, + .full_refresh_interval = 0, + .init_data = NULL, + }, + + // Generic SSD16xx BW panels (same controller, different sizes) + [EPD_PANEL_SSD16XX_154] = { + .name = "SSD16xx_154", + .width = 200, .height = 200, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_213] = { + .name = "SSD16xx_213", + .width = 122, .height = 250, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_266] = { + .name = "SSD16xx_266", + .width = 152, .height = 296, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_270] = { + .name = "SSD16xx_270", + .width = 176, .height = 264, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_290] = { + .name = "SSD16xx_290", + .width = 128, .height = 296, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_370] = { + .name = "SSD16xx_370", + .width = 280, .height = 480, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, + [EPD_PANEL_SSD16XX_420] = { + .name = "SSD16xx_420", + .width = 400, .height = 300, + .color_mode = EPD_COLOR_BW, .bits_per_pixel = 1, + .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST, + .ctrl = EPD_CTRL_SSD16XX, + .init_data = NULL, + }, +}; + +/*============================================================================= + * Registry Lookup Functions + *============================================================================*/ + +const epd_panel_desc_t* epd_get_panel_desc(epd_panel_type_t type) +{ + if (type >= EPD_PANEL_COUNT) { + return NULL; + } + // Check if panel entry is valid (has a name) + if (panel_registry[type].name == NULL) { + return NULL; + } + return &panel_registry[type]; +} + +const epd_controller_ops_t* epd_get_controller_ops(epd_controller_type_t ctrl) +{ + if (ctrl >= EPD_CTRL_COUNT) { + return NULL; + } + return &controller_ops[ctrl]; +} diff --git a/Libraries/esp_epaper/src/epaper_spi.c b/Libraries/esp_epaper/src/epaper_spi.c new file mode 100644 index 000000000..449b81cdf --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_spi.c @@ -0,0 +1,173 @@ +#include "epaper_spi.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "epd_spi"; + +#define SPI_MAX_CHUNK_SIZE 4096 // 4KB per transfer (safe for all ESP32 variants) + +esp_err_t epd_spi_init(epd_spi_t *spi, const epd_pin_config_t *pins, const epd_spi_config_t *spi_cfg) +{ + // Configure GPIO pins + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + // DC, RST, CS as output + io_conf.pin_bit_mask = (1ULL << pins->dc) | (1ULL << pins->rst) | (1ULL << pins->cs); + gpio_config(&io_conf); + + // BUSY as input + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << pins->busy); + gpio_config(&io_conf); + + // Store pins + spi->pin_dc = pins->dc; + spi->pin_rst = pins->rst; + spi->pin_busy = pins->busy; + spi->pin_cs = pins->cs; + + // Set initial state + gpio_set_level(pins->cs, 1); + gpio_set_level(pins->dc, 1); + gpio_set_level(pins->rst, 1); + + // Configure SPI bus (large buffer for big displays like 800x480) + spi_bus_config_t buscfg = { + .mosi_io_num = pins->mosi, + .miso_io_num = -1, // Not used + .sclk_io_num = pins->sck, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = SPI_MAX_CHUNK_SIZE, // Match chunk size + }; + + spi->host = spi_cfg->host; + + esp_err_t ret = spi_bus_initialize(spi_cfg->host, &buscfg, SPI_DMA_CH_AUTO); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret)); + return ret; + } + // ESP_ERR_INVALID_STATE means the host was already initialized by someone else - this + // component doesn't own it and must not free it on deinit. + spi->owns_bus = (ret == ESP_OK); + + // Add SPI device + spi_device_interface_config_t devcfg = { + .clock_speed_hz = spi_cfg->speed_hz, + .mode = 0, + .spics_io_num = -1, // Manual CS control + .queue_size = 1, + }; + + ret = spi_bus_add_device(spi_cfg->host, &devcfg, &spi->spi); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "SPI device add failed: %s", esp_err_to_name(ret)); + if (spi->owns_bus) { + spi_bus_free(spi->host); + spi->owns_bus = false; + } + return ret; + } + + ESP_LOGI(TAG, "SPI initialized"); + return ESP_OK; +} + +esp_err_t epd_spi_deinit(epd_spi_t *spi) +{ + if (spi->spi) { + spi_bus_remove_device(spi->spi); + spi->spi = NULL; + } + // spi_bus_free() requires all devices removed first, and only the owner of a host may free + // it - see spi_master.h's spi_bus_free() docs. + if (spi->owns_bus) { + spi_bus_free(spi->host); + spi->owns_bus = false; + } + return ESP_OK; +} + +void epd_spi_write_cmd(epd_spi_t *spi, uint8_t cmd) +{ + gpio_set_level(spi->pin_cs, 0); + gpio_set_level(spi->pin_dc, 0); // Command mode + + spi_transaction_t t = { + .length = 8, + .tx_buffer = &cmd, + }; + spi_device_polling_transmit(spi->spi, &t); + + gpio_set_level(spi->pin_cs, 1); +} + +void epd_spi_write_data(epd_spi_t *spi, uint8_t data) +{ + gpio_set_level(spi->pin_cs, 0); + gpio_set_level(spi->pin_dc, 1); // Data mode + + spi_transaction_t t = { + .length = 8, + .tx_buffer = &data, + }; + spi_device_polling_transmit(spi->spi, &t); + + gpio_set_level(spi->pin_cs, 1); +} + +void epd_spi_write_data_bulk(epd_spi_t *spi, const uint8_t *data, uint32_t len) +{ + gpio_set_level(spi->pin_cs, 0); + gpio_set_level(spi->pin_dc, 1); // Data mode + + // Send in chunks to avoid SPI transfer limit + uint32_t offset = 0; + while (offset < len) { + uint32_t chunk_size = (len - offset > SPI_MAX_CHUNK_SIZE) ? SPI_MAX_CHUNK_SIZE : (len - offset); + spi_transaction_t t = { + .length = chunk_size * 8, + .tx_buffer = data + offset, + }; + spi_device_polling_transmit(spi->spi, &t); + offset += chunk_size; + } + + gpio_set_level(spi->pin_cs, 1); +} + +void epd_spi_reset(epd_spi_t *spi) +{ + gpio_set_level(spi->pin_rst, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(spi->pin_rst, 1); + vTaskDelay(pdMS_TO_TICKS(10)); +} + +int epd_spi_is_busy(epd_spi_t *spi) +{ + return gpio_get_level(spi->pin_busy); +} + +void epd_spi_wait_busy(epd_spi_t *spi, uint32_t timeout_ms) +{ + uint32_t start = xTaskGetTickCount(); + while (gpio_get_level(spi->pin_busy)) { + if (timeout_ms > 0) { + uint32_t elapsed = (xTaskGetTickCount() - start) * portTICK_PERIOD_MS; + if (elapsed >= timeout_ms) { + ESP_LOGW(TAG, "Wait busy timeout"); + return; + } + } + vTaskDelay(pdMS_TO_TICKS(5)); // 5ms like reference + } +} diff --git a/Libraries/esp_epaper/src/epaper_spi.h b/Libraries/esp_epaper/src/epaper_spi.h new file mode 100644 index 000000000..6569355d1 --- /dev/null +++ b/Libraries/esp_epaper/src/epaper_spi.h @@ -0,0 +1,33 @@ +#ifndef _EPAPER_SPI_H_ +#define _EPAPER_SPI_H_ + +#include +#include +#include "esp_err.h" +#include "driver/spi_master.h" +#include "epaper_config.h" + +typedef struct { + spi_device_handle_t spi; + spi_host_device_t host; + /** True if epd_spi_init() called spi_bus_initialize() successfully (vs. the bus already + * being initialized by someone else) - only an owned bus gets freed on deinit. */ + bool owns_bus; + int pin_dc; + int pin_rst; + int pin_busy; + int pin_cs; +} epd_spi_t; + +esp_err_t epd_spi_init(epd_spi_t *spi, const epd_pin_config_t *pins, const epd_spi_config_t *spi_cfg); +esp_err_t epd_spi_deinit(epd_spi_t *spi); + +void epd_spi_write_cmd(epd_spi_t *spi, uint8_t cmd); +void epd_spi_write_data(epd_spi_t *spi, uint8_t data); +void epd_spi_write_data_bulk(epd_spi_t *spi, const uint8_t *data, uint32_t len); + +void epd_spi_reset(epd_spi_t *spi); +int epd_spi_is_busy(epd_spi_t *spi); +void epd_spi_wait_busy(epd_spi_t *spi, uint32_t timeout_ms); + +#endif // _EPAPER_SPI_H_ diff --git a/Modules/app-module/source/module.cpp b/Modules/app-module/source/module.cpp index 03544e532..91b100d7d 100644 --- a/Modules/app-module/source/module.cpp +++ b/Modules/app-module/source/module.cpp @@ -26,6 +26,7 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id), DEFINE_MODULE_SYMBOL(app_event_unsubscribe), DEFINE_MODULE_SYMBOL(app_event_poll), + DEFINE_MODULE_SYMBOL(app_event_emit_close), // app/execute DEFINE_MODULE_SYMBOL(app_execute), DEFINE_MODULE_SYMBOL(app_execute_for_result), diff --git a/Modules/cpp-symbols-module/README.md b/Modules/cpp-symbols-module/README.md index 252211a0a..3de9cffd6 100644 --- a/Modules/cpp-symbols-module/README.md +++ b/Modules/cpp-symbols-module/README.md @@ -16,6 +16,9 @@ instead of `_Znwj`), so this module doesn't build for the POSIX simulator. ### Compiler/runtime ABI support - `operator new(unsigned int)` / `operator delete(void*, unsigned int)` (`_Znwj` / `_ZdlPvj`) +- `operator new[](unsigned int)` / `operator delete[](void*, unsigned int)` (`_Znaj` / `_ZdaPvj`) +- `operator delete(void*)` / `operator delete[](void*)` (`_ZdlPv` / `_ZdaPv`) - the unsized forms, + used instead of the above when the compiler determines no size is needed - `std::nothrow` - `__cxa_pure_virtual` - called through a pure-virtual slot before a derived class's vtable is fully constructed; see [Bare metal C++](https://arobenko.github.io/bare_metal_cpp/). diff --git a/Modules/cpp-symbols-module/source/module.cpp b/Modules/cpp-symbols-module/source/module.cpp index 03e9b3bd6..859eb5cab 100644 --- a/Modules/cpp-symbols-module/source/module.cpp +++ b/Modules/cpp-symbols-module/source/module.cpp @@ -22,6 +22,12 @@ extern "C" { // libstdc++ exports these under different (m-suffixed) mangled names, so they don't apply there. extern void* _Znwj(uint32_t size); // operator new(unsigned int) extern void _ZdlPvj(void* p, uint64_t size); // operator delete(void*, unsigned int) + extern void* _Znaj(uint32_t size); // operator new[](unsigned int) + extern void _ZdaPvj(void* p, uint64_t size); // operator delete[](void*, unsigned int) + // Unsized forms: the compiler picks these over the sized ones above depending on context + // (e.g. trivially-destructible types needing no array cookie), so both must be exported. + extern void _ZdlPv(void* p); // operator delete(void*) + extern void _ZdaPv(void* p); // operator delete[](void*) #endif extern void __cxa_pure_virtual(); // cxx_guards.cpp @@ -99,6 +105,10 @@ static const ModuleSymbol SYMBOLS[] = { #ifdef ESP_PLATFORM DEFINE_MODULE_SYMBOL(_Znwj), // operator new(unsigned int) DEFINE_MODULE_SYMBOL(_ZdlPvj), // operator delete(void*, unsigned int) + DEFINE_MODULE_SYMBOL(_Znaj), // operator new[](unsigned int) + DEFINE_MODULE_SYMBOL(_ZdaPvj), // operator delete[](void*, unsigned int) + DEFINE_MODULE_SYMBOL(_ZdlPv), // operator delete(void*) + DEFINE_MODULE_SYMBOL(_ZdaPv), // operator delete[](void*) #endif { "_ZSt7nothrow", (void*)&std::nothrow }, DEFINE_MODULE_SYMBOL(__cxa_pure_virtual), // class-related, see https://arobenko.github.io/bare_metal_cpp/ diff --git a/Modules/http-module/source/download.cpp b/Modules/http-module/source/download.cpp index 956257492..0bf16a8c1 100644 --- a/Modules/http-module/source/download.cpp +++ b/Modules/http-module/source/download.cpp @@ -12,7 +12,7 @@ namespace { constexpr auto* TAG = "http-download"; -constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 4608 / sizeof(StackType_t); +constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 5120; struct DownloadContext { std::string url; diff --git a/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp b/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp index 76c8b36fb..96d20d6cf 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp @@ -66,13 +66,8 @@ static error_t read(Device* device, uint8_t address, uint8_t* data, size_t data_ lock(driver_data); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } else { + if (esp_error == ESP_OK) { esp_error = i2c_master_receive(driver_data->dev_handle, data, data_size, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "receive(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } } unlock(driver_data); return esp_err_to_error(esp_error); @@ -86,13 +81,8 @@ static error_t write(Device* device, uint8_t address, const uint8_t* data, uint1 lock(driver_data); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } else { + if (esp_error == ESP_OK) { esp_error = i2c_master_transmit(driver_data->dev_handle, data, data_size, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "transmit(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } } unlock(driver_data); return esp_err_to_error(esp_error); @@ -106,13 +96,8 @@ static error_t write_read(Device* device, uint8_t address, const uint8_t* write_ lock(driver_data); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } else { + if (esp_error == ESP_OK) { esp_error = i2c_master_transmit_receive(driver_data->dev_handle, write_data, write_data_size, read_data, read_data_size, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "transmit_receive(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } } unlock(driver_data); return esp_err_to_error(esp_error); @@ -126,13 +111,8 @@ static error_t read_register(Device* device, uint8_t address, uint8_t reg, uint8 lock(driver_data); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } else { + if (esp_error == ESP_OK) { esp_error = i2c_master_transmit_receive(driver_data->dev_handle, ®, 1, data, data_size, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "read_register(0x%02X, reg=0x%02X) failed: %s", address, reg, esp_err_to_name(esp_error)); - } } unlock(driver_data); return esp_err_to_error(esp_error); @@ -146,17 +126,12 @@ static error_t write_register(Device* device, uint8_t address, uint8_t reg, cons lock(driver_data); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } else { + if (esp_error == ESP_OK) { i2c_master_transmit_multi_buffer_info_t buffers[2] = { {.write_buffer = ®, .buffer_size = 1}, - {.write_buffer = data, .buffer_size = data_size} + {.write_buffer = data, .buffer_size = data_size}, }; esp_error = i2c_master_multi_buffer_transmit(driver_data->dev_handle, buffers, 2, timeout_ms); - if (esp_error != ESP_OK) { - LOG_E(TAG, "write_register(0x%02X, reg=0x%02X) failed: %s", address, reg, esp_err_to_name(esp_error)); - } } unlock(driver_data); return esp_err_to_error(esp_error); @@ -169,12 +144,6 @@ static error_t probe(Device* device, uint8_t address, TickType_t timeout) { lock(driver_data); esp_err_t esp_error = i2c_master_probe(driver_data->bus_handle, address, timeout_ms); - if (esp_error == ESP_ERR_NOT_FOUND) { - // Expected outcome when no device acks - e.g. hot-plug attach polling - LOG_D(TAG, "probe(0x%02X): not found", address); - } else if (esp_error != ESP_OK) { - LOG_E(TAG, "probe(0x%02X) failed: %s", address, esp_err_to_name(esp_error)); - } unlock(driver_data); return esp_err_to_error(esp_error); } @@ -286,11 +255,11 @@ static error_t stop(Device* device) { return ERROR_NONE; } -i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(struct Device* device) { +i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(Device* device) { return GET_DATA(device)->bus_handle; } -uint32_t esp32_i2c_master_get_clock_frequency(struct Device* device) { +uint32_t esp32_i2c_master_get_clock_frequency(Device* device) { return GET_CONFIG(device)->clockFrequency; } @@ -300,7 +269,7 @@ static constexpr I2cControllerApi ESP32_I2C_MASTER_API = { .write_read = write_read, .read_register = read_register, .write_register = write_register, - .probe = probe + .probe = probe, }; extern Module platform_esp32_module; @@ -313,7 +282,7 @@ Driver esp32_i2c_master_driver = { .api = &ESP32_I2C_MASTER_API, .device_type = &I2C_CONTROLLER_TYPE, .owner = &platform_esp32_module, - .internal = nullptr + .internal = nullptr, }; } // extern "C" diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index 29ede7667..bf97de672 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -503,6 +503,7 @@ static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) { .name = "usb_keyboard0", .config = nullptr, .parent = nullptr, + .flags = 0, .internal = nullptr, }; diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index cb06d773b..06d248bcb 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -84,8 +84,6 @@ extern "C" { // GCC integer arithmetic helpers (needed on 32-bit targets for 64-bit ops) long long __divdi3(long long a, long long b); long long __moddi3(long long a, long long b); - unsigned long long __udivdi3(unsigned long long a, unsigned long long b); - unsigned long long __umoddi3(unsigned long long a, unsigned long long b); #else extern double __adddf3(double a, double b); extern double __subdf3(double a, double b); @@ -120,6 +118,9 @@ extern "C" { // GCC 64-bit integer arithmetic helpers (needed for 64-bit div on 32-bit RISC-V) long long __divdi3(long long a, long long b); unsigned long long __udivdi3(unsigned long long a, unsigned long long b); + unsigned long long (__atomic_load_8)(const volatile void*, int); + void (__atomic_store_8)(volatile void*, unsigned long long, int); + unsigned long long (__atomic_exchange_8)(volatile void*, unsigned long long, int); #endif } @@ -284,8 +285,6 @@ static const ModuleSymbol platform_esp32_symbols[] = { DEFINE_MODULE_SYMBOL(__gtdf2), DEFINE_MODULE_SYMBOL(__divdi3), DEFINE_MODULE_SYMBOL(__moddi3), - DEFINE_MODULE_SYMBOL(__udivdi3), - DEFINE_MODULE_SYMBOL(__umoddi3), #else DEFINE_MODULE_SYMBOL(__adddf3), DEFINE_MODULE_SYMBOL(__subdf3), @@ -317,6 +316,9 @@ static const ModuleSymbol platform_esp32_symbols[] = { DEFINE_MODULE_SYMBOL(__clzsi2), DEFINE_MODULE_SYMBOL(__divdi3), DEFINE_MODULE_SYMBOL(__udivdi3), + DEFINE_MODULE_SYMBOL(__atomic_load_8), + DEFINE_MODULE_SYMBOL(__atomic_store_8), + DEFINE_MODULE_SYMBOL(__atomic_exchange_8), #endif MODULE_SYMBOL_TERMINATOR, }; diff --git a/Tactility/Kconfig b/Tactility/Kconfig index 813bbb54d..355305933 100644 --- a/Tactility/Kconfig +++ b/Tactility/Kconfig @@ -64,6 +64,11 @@ menu "Tactility App" range 0 200 help Shared icon size in pixels + config TT_LVGL_STATUSBAR_COLORS_INVERTED + bool "Statusbar colors inverted" + default n + help + Invert the statusbar's foreground/background colors config TT_AUTO_START_APP_ID string "Auto Start App ID" default "" diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index 716e85e42..9199e05d0 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -18,8 +17,18 @@ #include +#ifdef ESP_PLATFORM +#include +#endif + namespace tt::lvgl { +#if defined(CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED) && CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED +constexpr bool STATUSBAR_COLORS_INVERTED = true; +#else +constexpr bool STATUSBAR_COLORS_INVERTED = false; +#endif + constexpr auto* TAG = "statusbar"; static void onUpdateTime(); @@ -159,6 +168,23 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) { auto* statusbar = reinterpret_cast(obj); + const auto fg_color = STATUSBAR_COLORS_INVERTED ? lv_color_black() : lv_color_white(); + const auto bg_color = STATUSBAR_COLORS_INVERTED ? lv_color_white() : lv_color_black(); + + lv_obj_set_style_bg_color(obj, bg_color, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_STATE_DEFAULT); + + // Inverted statusbar creates a separator between statusbar and the rest of the UI below it + if (STATUSBAR_COLORS_INVERTED) { + // Show border as a horizontal line on the bottom + lv_obj_set_style_border_color(obj, fg_color, LV_STATE_DEFAULT); + lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(obj, 1, LV_STATE_DEFAULT); + } else { + // No border + lv_obj_set_style_border_color(obj, bg_color, LV_STATE_DEFAULT); + } + lv_obj_set_width(obj, LV_PCT(100)); lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT); @@ -171,21 +197,23 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) { lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT); statusbar->time = lv_label_create(obj); - lv_obj_set_style_text_color(statusbar->time, lv_color_white(), LV_STATE_DEFAULT); + lv_obj_set_style_text_color(statusbar->time, fg_color, LV_STATE_DEFAULT); lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT); update_time(statusbar); auto* left_spacer = lv_obj_create(obj); + lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(left_spacer, 1, 1); - obj_set_style_bg_invisible(left_spacer); lv_obj_set_flex_grow(left_spacer, 1); + lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT); statusbar_data.mutex.lock(MAX_TICKS); for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { auto* image = lv_image_create(obj); lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT); - lv_obj_set_style_text_color(image, lv_color_white(), LV_STATE_DEFAULT); + lv_obj_set_style_text_color(image, fg_color, LV_STATE_DEFAULT); lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT); statusbar->icons[i] = image; diff --git a/TactilityKernel/include/tactility/drivers/gpio_controller.h b/TactilityKernel/include/tactility/drivers/gpio_controller.h index 35223f0aa..1249fa7c4 100644 --- a/TactilityKernel/include/tactility/drivers/gpio_controller.h +++ b/TactilityKernel/include/tactility/drivers/gpio_controller.h @@ -201,6 +201,16 @@ error_t gpio_descriptor_disable_interrupt(struct GpioDescriptor* descriptor); */ error_t gpio_controller_get_pin_count(struct Device* device, uint32_t* count); +/** + * @brief Gets the logical level of a GPIO pin directly by pin number, without acquiring it first. + * @param[in] device the GPIO controller device + * @param[in] pin the pin number + * @param[out] high pointer to store the pin level + * @retval ERROR_OUT_OF_RANGE if pin is not a valid pin number for this controller + * @return ERROR_NONE if successful + */ +error_t gpio_controller_get_level(struct Device* device, gpio_pin_t pin, bool* high); + /** * @brief Initializes GPIO descriptors for a controller. * @param[in,out] device the GPIO controller device diff --git a/TactilityKernel/source/drivers/battery_sense.cpp b/TactilityKernel/source/drivers/battery_sense.cpp index 67f2813ca..c692f22c5 100644 --- a/TactilityKernel/source/drivers/battery_sense.cpp +++ b/TactilityKernel/source/drivers/battery_sense.cpp @@ -91,7 +91,7 @@ Driver battery_sense_power_supply_driver = { .api = &BATTERY_SENSE_POWER_SUPPLY_API, .device_type = &POWER_SUPPLY_TYPE, .owner = nullptr, - .internal = nullptr + .internal = nullptr, }; struct BatterySenseInternal { @@ -99,7 +99,15 @@ struct BatterySenseInternal { }; static error_t create_power_supply_child(Device* parent, Device*& out_child) { - auto* child = new(std::nothrow) Device { .address = 0, .name = "battery-sense-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr }; + auto* child = new(std::nothrow) Device { + .address = 0, + .name = "battery-sense-power-supply", + .config = nullptr, + .parent = nullptr, + .flags = 0, + .internal = nullptr, + }; + if (child == nullptr) { return ERROR_OUT_OF_MEMORY; } diff --git a/TactilityKernel/source/drivers/gpio_controller.cpp b/TactilityKernel/source/drivers/gpio_controller.cpp index ed3d5fc91..4311dbfbe 100644 --- a/TactilityKernel/source/drivers/gpio_controller.cpp +++ b/TactilityKernel/source/drivers/gpio_controller.cpp @@ -117,6 +117,21 @@ error_t gpio_controller_get_pin_count(Device* device, uint32_t* count) { return ERROR_NONE; } +error_t gpio_controller_get_level(Device* device, gpio_pin_t pin, bool* high) { + auto* data = static_cast(device_get_driver_data(device)); + + mutex_lock(&data->mutex); + if (pin >= data->pin_count) { + mutex_unlock(&data->mutex); + return ERROR_OUT_OF_RANGE; + } + GpioDescriptor* desc = &data->descriptors[pin]; + mutex_unlock(&data->mutex); + + const auto* driver = device_get_driver(device); + return GPIO_INTERNAL_API(driver)->get_level(desc, high); +} + error_t gpio_controller_init_descriptors(Device* device, uint32_t pin_count, void* controller_context) { auto* data = new(std::nothrow) GpioControllerData(pin_count, controller_context); if (!data) return ERROR_OUT_OF_MEMORY; diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 895ec4f24..39935d335 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -204,6 +204,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(gpio_controller_init_descriptors), DEFINE_MODULE_SYMBOL(gpio_controller_deinit_descriptors), DEFINE_MODULE_SYMBOL(gpio_controller_get_controller_context), + DEFINE_MODULE_SYMBOL(gpio_controller_get_level), DEFINE_MODULE_SYMBOL(GPIO_CONTROLLER_TYPE), // drivers/grove DEFINE_MODULE_SYMBOL(grove_set_mode), diff --git a/device.py b/device.py index 8bd34c47b..45fa43ae0 100644 --- a/device.py +++ b/device.py @@ -286,6 +286,9 @@ def write_lvgl_variables(output_file, device_properties: dict): output_file.write(f"CONFIG_LV_COLOR_DEPTH={color_depth}\n") output_file.write(f"CONFIG_LV_COLOR_DEPTH_{color_depth}=y\n") output_file.write("CONFIG_LV_DISP_DEF_REFR_PERIOD=10\n") + has_statusbar_colors_inverted = get_boolean_property_or_false(device_properties, "lvgl.statusbarColorsInverted") + if has_statusbar_colors_inverted: + output_file.write("CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=y\n") theme = get_property_or_default(device_properties, "lvgl.theme", "DefaultDark") if theme == "DefaultDark": output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n")