diff --git a/.gitmodules b/.gitmodules
index 0d18c27c6..695474766 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,6 @@
[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 02965b4be..195f180a2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -45,6 +45,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
"TactilityC"
"TactilityFreeRtos"
"Libraries/elf_loader"
+ "Libraries/esp_epaper"
"Libraries/lv_screenshot"
"Libraries/minitar"
"Libraries/minmea"
diff --git a/Devices/cl32/CMakeLists.txt b/Devices/cl32/CMakeLists.txt
new file mode 100644
index 000000000..09a2fdee3
--- /dev/null
+++ b/Devices/cl32/CMakeLists.txt
@@ -0,0 +1,7 @@
+file(GLOB_RECURSE SOURCE_FILES source/*.c*)
+
+idf_component_register(
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS "source"
+ REQUIRES TactilityKernel
+)
diff --git a/Devices/cl32/LICENSE-Apache-2.0.md b/Devices/cl32/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Devices/cl32/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Devices/cl32/cl32.dts b/Devices/cl32/cl32.dts
new file mode 100644
index 000000000..9027c8e99
--- /dev/null
+++ b/Devices/cl32/cl32.dts
@@ -0,0 +1,121 @@
+/dts-v1/;
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+/ {
+ compatible = "root";
+ model = "CL-32";
+
+ gpio0 {
+ compatible = "espressif,esp32-gpio";
+ gpio-count = <49>;
+ };
+
+ wifi0 {
+ compatible = "espressif,esp32-wifi";
+ 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.
+ i2c0 {
+ compatible = "espressif,esp32-i2c";
+ port = ;
+ clock-frequency = <100000>;
+ 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 10 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 10 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 10 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 10 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 10 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 10 0 // LEFT DOWN RIGHT < > / \ = RUN
+ ];
+ shift-row = <4>;
+ shift-col = <0>;
+ sym-row = <5>;
+ sym-col = <8>;
+ };
+
+ rtc: bm8563 {
+ compatible = "belling,bm8563";
+ reg = <0x51>;
+ };
+ };
+
+ port_b: grove1 {
+ compatible = "espressif,esp32-grove";
+ defaultMode = ;
+ pinSdaTx = <&gpio0 47 GPIO_FLAG_NONE>;
+ pinSclRx = <&gpio0 48 GPIO_FLAG_NONE>;
+ uartPort = ;
+ i2cPort = ;
+ i2cClockFrequency = <100000>;
+ };
+
+ spi1 {
+ compatible = "espressif,esp32-spi";
+ host = ;
+ cs-gpios = <&gpio0 7 GPIO_FLAG_NONE>;
+ pin-mosi = <&gpio0 10 GPIO_FLAG_NONE>;
+ pin-miso = <&gpio0 11 GPIO_FLAG_NONE>;
+ pin-sclk = <&gpio0 9 GPIO_FLAG_NONE>;
+ max-transfer-size = <4096>;
+
+ sdcard@0 {
+ compatible = "espressif,esp32-sdspi";
+ frequency-khz = <20000>;
+ };
+
+ epd@1 {
+ compatible = "tuanpmt,esp-epaper";
+ pin-dc = <&gpio0 13 GPIO_FLAG_NONE>;
+ pin-reset = <&gpio0 12 GPIO_FLAG_NONE>;
+ pin-busy = <&gpio0 14 GPIO_FLAG_NONE>;
+ pin-cs = <&gpio0 6 GPIO_FLAG_NONE>;
+ clock-speed-hz = <4000000>;
+ panel-type = "gdey029t71h";
+ update-mode = <2>;
+ rotation = <1>;
+ };
+ };
+};
diff --git a/Devices/cl32/device.properties b/Devices/cl32/device.properties
new file mode 100644
index 000000000..f59540f82
--- /dev/null
+++ b/Devices/cl32/device.properties
@@ -0,0 +1,26 @@
+general.vendor=CL-32
+general.name=CL-32
+
+apps.launcherAppId=Launcher
+
+hardware.target=ESP32S3
+hardware.flashSize=8MB
+hardware.spiRam=true
+hardware.spiRamMode=QUAD
+hardware.spiRamSpeed=80M
+hardware.esptoolFlashFreq=80M
+hardware.bluetooth=true
+
+storage.userDataLocation=SD
+
+display.size=2.9"
+display.shape=rectangle
+display.dpi=139
+
+lvgl.colorDepth=8
+lvgl.theme=Mono
+
+# Fix error "PSRAM space not enough for the Flash instructions" on boot:
+sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
+sdkconfig.CONFIG_SPIRAM_RODATA=n
+sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
diff --git a/Devices/cl32/devicetree.yaml b/Devices/cl32/devicetree.yaml
new file mode 100644
index 000000000..e2bc7d3bb
--- /dev/null
+++ b/Devices/cl32/devicetree.yaml
@@ -0,0 +1,6 @@
+dependencies:
+ - Platforms/platform-esp32
+ - Drivers/tca8418-module
+ - Drivers/esp-epaper-module
+ - Drivers/bm8563-module
+dts: cl32.dts
diff --git a/Devices/cl32/source/module.cpp b/Devices/cl32/source/module.cpp
new file mode 100644
index 000000000..5b88b4dd5
--- /dev/null
+++ b/Devices/cl32/source/module.cpp
@@ -0,0 +1,9 @@
+#include
+
+extern "C" {
+
+struct Module cl32_module = {
+ .name = "cl32"
+};
+
+}
diff --git a/Drivers/esp-epaper-module/CMakeLists.txt b/Drivers/esp-epaper-module/CMakeLists.txt
new file mode 100644
index 000000000..7bf6aa3a5
--- /dev/null
+++ b/Drivers/esp-epaper-module/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+tactility_add_module(esp-epaper-module
+ SRCS source/esp_epaper.cpp source/module.cpp
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel platform-esp32 driver esp_epaper
+)
diff --git a/Drivers/esp-epaper-module/LICENSE-Apache-2.0.md b/Drivers/esp-epaper-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/esp-epaper-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml
new file mode 100644
index 000000000..8b44b5e4a
--- /dev/null
+++ b/Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml
@@ -0,0 +1,65 @@
+description: >
+ E-paper display panels driven by the tuanpmt/esp_epaper ESP-IDF component
+ (SSD1680/1681 BW, GDEY0154D67 and ACeP/BWRY color controllers). The node must
+ be a child of an SPI controller: the display shares the parent's SPI host,
+ clock and MOSI pins, and manages its DC/RESET/BUSY/CS pins itself via the
+ component. Only 1bpp black/white panels are supported by the kernel display
+ bridge (DISPLAY_COLOR_FORMAT_MONOCHROME); color panels fail to start.
+
+compatible: "tuanpmt,esp-epaper"
+
+bus: spi
+
+properties:
+ pin-dc:
+ type: phandles
+ required: true
+ description: Data/Command GPIO pin
+ pin-reset:
+ type: phandles
+ required: true
+ description: Reset GPIO pin (the esp_epaper component drives it unconditionally)
+ pin-busy:
+ type: phandles
+ required: true
+ description: Busy GPIO pin (active high)
+ pin-cs:
+ type: phandles
+ required: true
+ description: Chip-select GPIO pin (driven manually by the component)
+ clock-speed-hz:
+ type: int
+ default: 4000000
+ description: SPI clock frequency in Hz (must be > 0)
+ panel-type:
+ type: text
+ required: true
+ description: >
+ esp_epaper panel registry name. One of "gdey0154d67", "gdep073e01",
+ "gdey037f51", "gdey029t71h", "ssd16xx-154", "ssd16xx-213", "ssd16xx-266",
+ "ssd16xx-270", "ssd16xx-290", "ssd16xx-370", "ssd16xx-420".
+ width:
+ type: int
+ default: 0
+ description: Horizontal resolution override in pixels (0 = the panel's default, maximum 2048)
+ height:
+ type: int
+ default: 0
+ description: Vertical resolution override in pixels (0 = the panel's default, maximum 2048)
+ update-mode:
+ type: int
+ default: 0
+ description: >
+ epd_update_mode_t used for frame updates: 0 = full, 1 = fast, 2 = partial.
+ Full is the safest default; fast and partial refresh quickly but ghost.
+ Values above 2 are rejected at driver start.
+ rotation:
+ type: int
+ default: 0
+ description: >
+ Fixed display rotation applied at start. 0 = 0 degrees, 1 = 90 degrees,
+ 2 = 180 degrees, 3 = 270 degrees counter-clockwise (matching
+ LV_DISPLAY_ROTATION_*). The kernel bridge reports the rotated resolution,
+ 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.
diff --git a/Drivers/esp-epaper-module/devicetree.yaml b/Drivers/esp-epaper-module/devicetree.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/esp-epaper-module/devicetree.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/esp-epaper-module/include/bindings/esp_epaper.h b/Drivers/esp-epaper-module/include/bindings/esp_epaper.h
new file mode 100644
index 000000000..0c6057f61
--- /dev/null
+++ b/Drivers/esp-epaper-module/include/bindings/esp_epaper.h
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+DEFINE_DEVICETREE(esp_epaper, struct EspEpaperConfig)
diff --git a/Drivers/esp-epaper-module/include/drivers/esp_epaper.h b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h
new file mode 100644
index 000000000..cba3a7706
--- /dev/null
+++ b/Drivers/esp-epaper-module/include/drivers/esp_epaper.h
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include
+
+#include
+#include
+
+/**
+ * @brief Panel configuration for an esp_epaper-driven e-paper display.
+ *
+ * The struct field order matches the binding's property order and must stay in
+ * sync with it: the devicetree compiler emits struct initializers in binding
+ * order, not declaration order.
+ */
+struct EspEpaperConfig {
+ struct GpioPinSpec pin_dc;
+ struct GpioPinSpec pin_reset;
+ struct GpioPinSpec pin_busy;
+ struct GpioPinSpec pin_cs;
+ /** SPI clock frequency in Hz */
+ int clock_speed_hz;
+ /** Panel registry name, e.g. "ssd16xx-290" (see the binding's panel-type) */
+ const char* panel_type;
+ /** Horizontal resolution override; 0 = the panel's default */
+ uint16_t width;
+ /** Vertical resolution override; 0 = the panel's default */
+ uint16_t height;
+ /** epd_update_mode_t used for frame updates */
+ epd_update_mode_t update_mode;
+ /**
+ * Fixed display rotation, 0 = 0 degrees, 1 = 90, 2 = 180, 3 = 270 degrees
+ * counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime.
+ */
+ uint8_t rotation;
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp-epaper-module/include/esp_epaper_module.h b/Drivers/esp-epaper-module/include/esp_epaper_module.h
new file mode 100644
index 000000000..fb3d5f3de
--- /dev/null
+++ b/Drivers/esp-epaper-module/include/esp_epaper_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module esp_epaper_module;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp-epaper-module/source/esp_epaper.cpp b/Drivers/esp-epaper-module/source/esp_epaper.cpp
new file mode 100644
index 000000000..8bff9eea6
--- /dev/null
+++ b/Drivers/esp-epaper-module/source/esp_epaper.cpp
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include "esp_epaper_rotate.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+constexpr auto* TAG = "esp_epaper";
+#define GET_CONFIG(device) (static_cast((device)->config))
+
+/** Width/height overrides above this are rejected as nonsense. */
+constexpr uint16_t MAX_PANEL_DIMENSION = 2048;
+
+struct EspEpaperInternal {
+ /** Opaque esp_epaper device, owns the panel's pins and SPI device. */
+ epd_handle_t epd;
+ epd_panel_info_t info;
+ /** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
+ uint8_t* rotate_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. */
+ bool display_on;
+};
+
+static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) {
+ return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width;
+}
+
+static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) {
+ return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height;
+}
+
+static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) {
+ struct PanelMapping {
+ const char* name;
+ epd_panel_type_t type;
+ };
+ static constexpr PanelMapping kPanelMappings[] = {
+ { "gdey0154d67", EPD_PANEL_GDEY0154D67 },
+ { "gdep073e01", EPD_PANEL_GDEP073E01 },
+ { "gdey037f51", EPD_PANEL_GDEY037F51 },
+ { "gdey029t71h", EPD_PANEL_GDEY029T71H },
+ { "ssd16xx-154", EPD_PANEL_SSD16XX_154 },
+ { "ssd16xx-213", EPD_PANEL_SSD16XX_213 },
+ { "ssd16xx-266", EPD_PANEL_SSD16XX_266 },
+ { "ssd16xx-270", EPD_PANEL_SSD16XX_270 },
+ { "ssd16xx-290", EPD_PANEL_SSD16XX_290 },
+ { "ssd16xx-370", EPD_PANEL_SSD16XX_370 },
+ { "ssd16xx-420", EPD_PANEL_SSD16XX_420 },
+ };
+ for (const auto& mapping : kPanelMappings) {
+ if (std::strcmp(name, mapping.name) == 0) {
+ *out_type = mapping.type;
+ return true;
+ }
+ }
+ return false;
+}
+
+static error_t esp_epaper_reset(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
+ // epd_wake() toggles the reset pin and re-runs the full init sequence.
+ const esp_err_t ret = epd_wake(internal->epd);
+ // epd_wake() re-inits the panel, so it is awake (and drawable) again.
+ if (ret == ESP_OK) {
+ internal->display_on = true;
+ }
+ xSemaphoreGive(internal->panel_mutex);
+ return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+static error_t esp_epaper_init(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
+ const esp_err_t ret = epd_wake(internal->epd);
+ if (ret == ESP_OK) {
+ internal->display_on = true;
+ }
+ xSemaphoreGive(internal->panel_mutex);
+ return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
+// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
+// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
+// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is
+// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by
+// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity.
+static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ const auto* config = GET_CONFIG(device);
+
+ const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation);
+ const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation);
+ if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) {
+ LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
+
+ if (!internal->display_on) {
+ // Display is off (deep sleep). Drop the frame; the mismatch is fine because the next
+ // power-on triggers a full refresh of the current render anyway.
+ xSemaphoreGive(internal->panel_mutex);
+ return ERROR_NONE;
+ }
+
+ const uint8_t* source;
+ if (config->rotation == 0) {
+ source = static_cast(color_data);
+ } else {
+ memset(internal->rotate_buffer, 0, internal->info.buffer_size);
+ esp_epaper_rotate_frame(static_cast(color_data), internal->rotate_buffer,
+ internal->info.width, internal->info.height, config->rotation);
+ source = internal->rotate_buffer;
+ }
+
+ const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
+ xSemaphoreGive(internal->panel_mutex);
+ if (ret != ESP_OK) {
+ LOG_E(TAG, "epd_update 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));
+
+ xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
+ if (on_off == internal->display_on) {
+ xSemaphoreGive(internal->panel_mutex);
+ return ERROR_NONE;
+ }
+
+ bool ok = true;
+ if (on_off) {
+ if (epd_wake(internal->epd) != ESP_OK) {
+ LOG_E(TAG, "epd_wake failed");
+ ok = false;
+ }
+ } else {
+ if (epd_sleep(internal->epd) != ESP_OK) {
+ LOG_E(TAG, "epd_sleep failed");
+ ok = false;
+ }
+ }
+
+ if (ok) {
+ internal->display_on = on_off;
+ }
+ xSemaphoreGive(internal->panel_mutex);
+ return ok ? ERROR_NONE : ERROR_RESOURCE;
+}
+
+static DisplayColorFormat esp_epaper_get_color_format(Device*) {
+ return DISPLAY_COLOR_FORMAT_MONOCHROME;
+}
+
+static uint16_t esp_epaper_get_resolution_x(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation);
+}
+
+static uint16_t esp_epaper_get_resolution_y(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+ return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation);
+}
+
+static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
+ *out_buffer = nullptr;
+}
+
+static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
+ return 0;
+}
+
+static const DisplayApi esp_epaper_display_api = {
+ .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
+ .reset = esp_epaper_reset,
+ .init = esp_epaper_init,
+ .draw_bitmap = esp_epaper_draw_bitmap,
+ .mirror = nullptr,
+ .swap_xy = nullptr,
+ .get_swap_xy = nullptr,
+ .get_mirror_x = nullptr,
+ .get_mirror_y = nullptr,
+ .set_gap = nullptr,
+ .get_gap_x = nullptr,
+ .get_gap_y = nullptr,
+ .invert_color = nullptr,
+ .disp_on_off = esp_epaper_disp_on_off,
+ .disp_sleep = nullptr,
+ .get_color_format = esp_epaper_get_color_format,
+ .get_resolution_x = esp_epaper_get_resolution_x,
+ .get_resolution_y = esp_epaper_get_resolution_y,
+ .get_frame_buffer = esp_epaper_get_frame_buffer,
+ .get_frame_buffer_count = esp_epaper_get_frame_buffer_count,
+ .get_backlight = nullptr,
+ .has_capability = nullptr,
+};
+
+static void free_internal(EspEpaperInternal* internal) {
+ if (internal->epd != nullptr) {
+ epd_deinit(internal->epd);
+ }
+ if (internal->rotate_buffer != nullptr) {
+ free(internal->rotate_buffer);
+ }
+ if (internal->panel_mutex != nullptr) {
+ vSemaphoreDelete(internal->panel_mutex);
+ }
+ free(internal);
+}
+
+static error_t start(Device* device) {
+ auto* parent = device_get_parent(device);
+ check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
+
+ const auto* spi_config = static_cast(parent->config);
+ const auto* config = GET_CONFIG(device);
+
+ epd_panel_type_t panel_type;
+ if (!resolve_panel_type(config->panel_type, &panel_type)) {
+ LOG_E(TAG, "Unknown panel type: %s", config->panel_type);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ if (config->clock_speed_hz == 0) {
+ LOG_E(TAG, "Invalid clock_speed_hz (0)");
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ if (config->update_mode > EPD_UPDATE_PARTIAL) {
+ LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) {
+ LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ epd_config_t epd_config = EPD_CONFIG_DEFAULT();
+ epd_config.pins.busy = config->pin_busy.pin;
+ epd_config.pins.rst = config->pin_reset.pin;
+ epd_config.pins.dc = config->pin_dc.pin;
+ epd_config.pins.cs = config->pin_cs.pin;
+ epd_config.pins.sck = spi_config->pin_sclk.pin;
+ epd_config.pins.mosi = spi_config->pin_mosi.pin;
+ epd_config.spi.host = spi_config->host;
+ epd_config.spi.speed_hz = config->clock_speed_hz;
+ epd_config.panel.type = panel_type;
+ epd_config.panel.width = config->width;
+ epd_config.panel.height = config->height;
+
+ epd_handle_t epd = nullptr;
+ if (epd_init(&epd_config, &epd) != ESP_OK) {
+ LOG_E(TAG, "epd_init failed");
+ return ERROR_RESOURCE;
+ }
+
+ auto* internal = static_cast(calloc(1, sizeof(EspEpaperInternal)));
+ if (internal == nullptr) {
+ epd_deinit(epd);
+ return ERROR_OUT_OF_MEMORY;
+ }
+
+ internal->epd = epd;
+ internal->panel_mutex = xSemaphoreCreateMutex();
+ if (internal->panel_mutex == nullptr) {
+ free_internal(internal);
+ return ERROR_OUT_OF_MEMORY;
+ }
+
+ if (epd_get_info(epd, &internal->info) != ESP_OK) {
+ LOG_E(TAG, "epd_get_info failed");
+ free_internal(internal);
+ return ERROR_RESOURCE;
+ }
+
+ if (internal->info.color_mode != EPD_COLOR_BW) {
+ LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode);
+ free_internal(internal);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ if (config->rotation > 3) {
+ LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation);
+ free_internal(internal);
+ return ERROR_NOT_SUPPORTED;
+ }
+
+ if (config->rotation != 0) {
+ internal->rotate_buffer = static_cast(malloc(internal->info.buffer_size));
+ if (internal->rotate_buffer == nullptr) {
+ LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size);
+ free_internal(internal);
+ return ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ internal->display_on = true;
+ device_set_driver_data(device, internal);
+
+ LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation);
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ auto* internal = static_cast(device_get_driver_data(device));
+
+ xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
+ if (internal->display_on) {
+ // Leave the panel in deep sleep.
+ epd_sleep(internal->epd);
+ internal->display_on = false;
+ }
+ xSemaphoreGive(internal->panel_mutex);
+
+ free_internal(internal);
+ device_set_driver_data(device, nullptr);
+ return ERROR_NONE;
+}
+
+Driver esp_epaper_driver = {
+ .name = "esp_epaper",
+ .compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .api = &esp_epaper_display_api,
+ .device_type = &DISPLAY_TYPE,
+ .owner = &esp_epaper_module,
+ .internal = nullptr
+};
diff --git a/Drivers/esp-epaper-module/source/esp_epaper_rotate.h b/Drivers/esp-epaper-module/source/esp_epaper_rotate.h
new file mode 100644
index 000000000..1cd6ae48f
--- /dev/null
+++ b/Drivers/esp-epaper-module/source/esp_epaper_rotate.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Rotates a full 1bpp frame (row-major, MSB-first) from the display's rotated
+ * space into the panel's native space. The display is the panel's native
+ * width x height turned counter-clockwise by `rotation` (0 = 0, 1 = 90, 2 = 180,
+ * 3 = 270 degrees), so its own dimensions are the swapped native ones for
+ * 90/270. Each mapping is the inverse of LVGL's lv_display_rotate_area()
+ * (Libraries/lvgl/src/display/lv_display.c), i.e. the exact transform LVGL
+ * applies when displaying on the panel.
+ *
+ * src points to the display frame (stride = ceil(display_width / 8) bytes,
+ * MSB-first bits), dst to the native frame (stride = ceil(width / 8) bytes).
+ * dst is overwritten; it may not alias src.
+ */
+static inline bool esp_epaper_rotation_swaps_axes(uint8_t rotation) {
+ return rotation == 1 || rotation == 3;
+}
+
+static inline void esp_epaper_rotate_frame(const uint8_t* src, uint8_t* dst, uint16_t width, uint16_t height, uint8_t rotation) {
+ const uint16_t display_width = esp_epaper_rotation_swaps_axes(rotation) ? height : width;
+ const uint32_t src_stride = (display_width + 7) / 8;
+ const uint32_t dst_stride = (width + 7) / 8;
+
+ memset(dst, 0, dst_stride * height);
+
+ for (uint16_t y = 0; y < height; y++) {
+ for (uint16_t x = 0; x < width; x++) {
+ // Display pixel (u, v) that lands on native pixel (x, y).
+ uint16_t u;
+ uint16_t v;
+ switch (rotation) {
+ case 0:
+ u = x;
+ v = y;
+ break;
+ case 1:
+ u = height - 1 - y;
+ v = x;
+ break;
+ case 2:
+ u = width - 1 - x;
+ v = height - 1 - y;
+ break;
+ case 3:
+ default:
+ u = y;
+ v = width - 1 - x;
+ break;
+ }
+ if (src[(uint32_t)v * src_stride + u / 8] & (0x80 >> (u % 8))) {
+ dst[(uint32_t)y * dst_stride + x / 8] |= (0x80 >> (x % 8));
+ }
+ }
+ }
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp-epaper-module/source/module.cpp b/Drivers/esp-epaper-module/source/module.cpp
new file mode 100644
index 000000000..9a24dde3f
--- /dev/null
+++ b/Drivers/esp-epaper-module/source/module.cpp
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern Driver esp_epaper_driver;
+
+extern "C" {
+
+static Driver* const esp_epaper_drivers[] = {
+ &esp_epaper_driver,
+ nullptr
+};
+
+Module esp_epaper_module = {
+ .name = "esp_epaper",
+ .drivers = esp_epaper_drivers
+};
+
+} // extern "C"
diff --git a/Libraries/esp_epaper b/Libraries/esp_epaper
new file mode 160000
index 000000000..0bf4f144b
--- /dev/null
+++ b/Libraries/esp_epaper
@@ -0,0 +1 @@
+Subproject commit 0bf4f144b543048f10bdadd49f0578858bdc2a39
diff --git a/Tactility/Source/lvgl/UsbHidInput.cpp b/Tactility/Source/lvgl/UsbHidInput.cpp
index 731046a19..87f3f8449 100644
--- a/Tactility/Source/lvgl/UsbHidInput.cpp
+++ b/Tactility/Source/lvgl/UsbHidInput.cpp
@@ -155,16 +155,24 @@ static void usbHidInputTask(void* arg) {
lvgl_lock();
- ctx->mouse_cursor = lv_image_create(lv_layer_sys());
- lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE);
- lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR);
- lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
+ // Without a registered display, lv_layer_sys() is NULL: creating the cursor image on it trips
+ // an LVGL assert whose default handler is an infinite loop (while(1);), hanging this task while
+ // it holds the LVGL lock. Only create the cursor when a system layer actually exists.
+ lv_obj_t* sys_layer = lv_layer_sys();
+ if (sys_layer != nullptr) {
+ ctx->mouse_cursor = lv_image_create(sys_layer);
+ lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE);
+ lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR);
+ lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
+ }
ctx->mouse_indev = lv_indev_create();
lv_indev_set_type(ctx->mouse_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(ctx->mouse_indev, mouse_read_cb);
lv_indev_set_user_data(ctx->mouse_indev, ctx);
- lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor);
+ if (ctx->mouse_cursor != nullptr) {
+ lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor);
+ }
ctx->kb_indev = lv_indev_create();
lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD);
diff --git a/device.py b/device.py
index f97a51999..448908258 100644
--- a/device.py
+++ b/device.py
@@ -254,6 +254,11 @@ def write_lvgl_variables(output_file, device_properties: dict):
output_file.write("CONFIG_LV_THEME_DEFAULT_LIGHT=y\n")
elif theme == "Mono":
output_file.write("CONFIG_LV_USE_THEME_MONO=y\n")
+ # LVGL selects the theme with #if LV_USE_THEME_DEFAULT / #elif LV_USE_THEME_SIMPLE /
+ # #elif LV_USE_THEME_MONO, and the Kconfig defaults enable DEFAULT+SIMPLE for any
+ # non-1bpp color depth so MONO only takes effect once those are disabled.
+ output_file.write("CONFIG_LV_USE_THEME_DEFAULT=n\n")
+ output_file.write("CONFIG_LV_USE_THEME_SIMPLE=n\n")
else:
exit_with_error(f"Unknown theme: {theme}")
font_height_text = get_property_or_default(device_properties, "lvgl.fontSize", "14")
diff --git a/partitions-16mb-no-sd-dev.csv b/partitions-16mb-no-sd-dev.csv
index 497ffa1ef..5d69f3878 100644
--- a/partitions-16mb-no-sd-dev.csv
+++ b/partitions-16mb-no-sd-dev.csv
@@ -3,5 +3,5 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
data, data, fat, , 1000k,
diff --git a/partitions-16mb-no-sd.csv b/partitions-16mb-no-sd.csv
index daeee992d..65eeb5a8a 100644
--- a/partitions-16mb-no-sd.csv
+++ b/partitions-16mb-no-sd.csv
@@ -3,5 +3,5 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
data, data, fat, , 11800k,
diff --git a/partitions-16mb-with-sd.csv b/partitions-16mb-with-sd.csv
index 262186df8..6e011704c 100644
--- a/partitions-16mb-with-sd.csv
+++ b/partitions-16mb-with-sd.csv
@@ -3,4 +3,4 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
diff --git a/partitions-4mb-with-sd.csv b/partitions-4mb-with-sd.csv
index 59d1243b4..bf83015c4 100644
--- a/partitions-4mb-with-sd.csv
+++ b/partitions-4mb-with-sd.csv
@@ -3,4 +3,4 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 3800k,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
diff --git a/partitions-8mb-no-sd-dev.csv b/partitions-8mb-no-sd-dev.csv
index 497ffa1ef..5d69f3878 100644
--- a/partitions-8mb-no-sd-dev.csv
+++ b/partitions-8mb-no-sd-dev.csv
@@ -3,5 +3,5 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
data, data, fat, , 1000k,
diff --git a/partitions-8mb-no-sd.csv b/partitions-8mb-no-sd.csv
index 79544b13d..bdc40599e 100644
--- a/partitions-8mb-no-sd.csv
+++ b/partitions-8mb-no-sd.csv
@@ -3,5 +3,5 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,
data, data, fat, , 3800k,
diff --git a/partitions-8mb-with-sd.csv b/partitions-8mb-with-sd.csv
index 262186df8..6e011704c 100644
--- a/partitions-8mb-with-sd.csv
+++ b/partitions-8mb-with-sd.csv
@@ -3,4 +3,4 @@
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M,
-system, data, fat, , 100k,
+system, data, fat, , 128k,