diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fe14cb..685d15b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ idf_component_register(SRCS "epaper_color.c" "epaper_commands.c" "epaper_draw.c" + "epaper_program.c" "mono_draw.c" "epaper_display_driver.c" "font_data.c" diff --git a/README.Md b/README.Md index f129067..106cb50 100644 --- a/README.Md +++ b/README.Md @@ -83,7 +83,9 @@ that would otherwise be impossible to support with traditional frame buffer appr * `ilitek,ili9341` / `ilitek,ili9342c`: ILI9341 / ILI9342C - 240x320, 16-bit colors * `ilitek,ili9486`: ILI9486 - 320x480, 16-bit colors (RGB565) * `ilitek,ili9488`: ILI9488 - 320x480, 18-bit colors (RGB666 over SPI; 3 bytes/pixel transfer) -* `waveshare,5in65-acep-7c`: Waveshare 7-color 5.65" ACeP display module - 600x480, 7 colors + +* `waveshare,5in65-acep-7c`: Waveshare 7-color 5.65" ACeP display module - 600x448, 7 colors + +software dithering +* `good-display/gdep073e01`: Good Display 7.3" 7-color e-paper module - 800x480, 7 colors + software dithering * `sharp,memory-lcd`: Sharp Memory LCDs - 400x240, 1-bit monochrome * `solomon-systech,ssd1306`: Solomon Systech SSD1306 - 128x64, 1-bit monochrome @@ -93,6 +95,39 @@ software dithering See also [display drivers](docs/display-drivers.md) documentation page for additional information. +### E-paper panel descriptors + +Ready-made e-paper panel descriptors are provided as a separate, +dependency-free Erlang/OTP library in [`atomgl_epaper/`](atomgl_epaper/). It +can be consumed as a sparse / `subdir` dependency directly from this +repository — see [`atomgl_epaper/README.md`](atomgl_epaper/README.md) for +details. + +For Erlang (`rebar3`): + +```erlang +%% rebar.config +{deps, [ + {atomgl_epaper, + {git, "https://github.com/atomvm/atomgl.git", {branch, "main"}}, + [{subdir, "atomgl_epaper"}]} +]}. +``` + +For Elixir (Mix), add the dependency to `mix.exs`: + +```elixir +# mix.exs +defp deps do + [ + {:atomgl_epaper, + github: "atomvm/atomgl", + branch: "main", + sparse: "atomgl_epaper"} + ] +end +``` + ## Platform Support AtomGL is currently compatible with **ESP32** microcontrollers. Linux with SDL is also supported for diff --git a/atomgl_epaper/.gitignore b/atomgl_epaper/.gitignore new file mode 100644 index 0000000..27e2e34 --- /dev/null +++ b/atomgl_epaper/.gitignore @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 Peter M + +_build/ +.rebar3/ +rebar.lock +ebin/ +*.beam +.eunit/ diff --git a/atomgl_epaper/ADD_PANEL.md b/atomgl_epaper/ADD_PANEL.md new file mode 100644 index 0000000..e557bda --- /dev/null +++ b/atomgl_epaper/ADD_PANEL.md @@ -0,0 +1,173 @@ + + + +# Adding E-Paper Panels + +This guide is for humans and LLM agents adding a new E-Paper panel descriptor to +AtomGL. The descriptor library now lives in this `atomgl_epaper` package; the +low-level C display driver still lives in the parent AtomGL component. Keep +panel additions small, traceable, and license-clean. + +## Source Policy + +Prefer primary, permissively licensed sources: + +- Vendor datasheets, product specs, and command tables. +- Vendor driver examples with compatible licenses, such as + [waveshareteam/e-Paper](https://github.com/waveshareteam/e-Paper). +- Existing descriptors in `atomgl_epaper/src/atomgl_epaper_panels.erl` and driver + helpers in the parent AtomGL tree. + +The Waveshare repository is a useful starting point for many panel init +sequences, refresh commands, LUT payloads, reset timing, BUSY polarity, and +native dimensions. When copying bytes or command sequences from permissively +licensed source, preserve the relevant attribution or license notice near the +copied data. + +Treat GPL code as reference only: + +- [bitbank2/bb_epaper](https://github.com/bitbank2/bb_epaper) and similar GPL + projects are useful for identifying controller families, panel aliases, and + behavioral clues. +- Do not copy GPL code, LUT tables, command byte arrays, comments, names chosen + only by that project, or implementation structure into AtomGL. +- If GPL code helped orient the work, describe it as corroborating reference + material in the commit or PR notes, not as the source for copied content. + +If the license of a source is unclear, do not copy from it. Use it only to form +questions, then verify details from a primary source or hardware testing. + +## Before Editing + +Collect these facts for the target panel: + +- Panel marketing name and likely compatible lookup strings. +- Native width and height, not just the desired display orientation. +- Controller IC or controller family. If uncertain, name the family rather than + inventing a precise controller. +- BUSY idle level, reset pulse timing, and whether GPIO pullups are needed. +- RAM data commands for old/current planes or color planes. +- Frame layout: row-major or column-major, MSB or LSB first. +- Pixel polarity: whether `1` means white or black. +- Refresh modes: full, fast, partial, 4-gray, and color; sleep modes are + described separately via `sleep_modes`. +- LUT format and whether extra register bytes accompany the LUT payload. +- Any controller-specific plane mapping, palette, or ghosting constraints. + +Use `rg` to search this package, the parent driver, and source trees for similar +panels first. Match the existing descriptor style instead of inventing a new +shape. + +Important local paths: + +- `atomgl_epaper/src/atomgl_epaper.erl` - public `panel/1`, `panel/2`, and + descriptor validation facade. +- `atomgl_epaper/src/atomgl_epaper_panels.erl` - panel lookup strings, + controller command sequences, and LUTs. +- `atomgl_epaper/src/atomgl_epaper_descriptor.erl` - descriptor builders, + orientation handling, timing defaults, and validation. +- `atomgl_epaper/src/atomgl_epaper_program.erl` - bytecode helpers for + commands, delays, BUSY waits, plane insertion, and LUT insertion. +- `atomgl_epaper/test/atomgl_epaper_tests.erl` - descriptor and validation + tests. +- `atomgl_epaper/README.md` - package usage and public package notes. +- `atomgl_epaper/CUSTOM_PANEL.md` - userland descriptor prototyping guide. +- `epaper_commands.h` and `epaper_display_driver.c` - parent AtomGL C driver + files, needed only when controller behavior changes. + +## Implementation Checklist + +1. Decide whether this is descriptor-only + + Most panels should be descriptor-only changes in + `atomgl_epaper/src/atomgl_epaper_panels.erl`. Reuse an existing controller + when the command set and driver behavior match. + + Add a new controller only when parsing, LUT insertion, plane writes, palette + mapping, refresh flow, or BUSY behavior needs distinct C-side handling. + + Controller changes usually touch: + + - `epaper_commands.h` + - `epaper_display_driver.c` + +2. Add a panel descriptor + + Panel lookup clauses live in `atomgl_epaper/src/atomgl_epaper_panels.erl`. + Add: + + - A primary lookup string, usually vendor-qualified. + - Optional aliases for mode defaults, such as `-fast`, `-partial`, or + `-4gray`. + - Native geometry, palette size, refresh modes, and default refresh mode. + - Init, refresh, partial, 4-gray, LUT, and `sleep_modes` programs as needed. + - Helper functions only when they reduce duplication or make the sequence + easier to audit. + +3. Keep driver behavior generic + + The C driver should know about controller behavior, not product names. Avoid + hardcoding a specific panel lookup string in the driver. Product-specific + command values should usually live in the descriptor bytecode. + +4. Document the lookup strings + + Update package docs when a new public lookup string is added. If the parent + AtomGL docs include a supported-panel list, update that too. + +5. Add descriptor tests + + Add focused EUnit coverage in `atomgl_epaper/test/atomgl_epaper_tests.erl` + for: + + - Controller atom. + - Native and view dimensions. + - Refresh mode list and default mode aliases. + - LUT byte size or palette size when relevant. + +## Verification + +Run the lightweight descriptor checks: + +```sh +cd atomgl_epaper +rebar3 eunit +rebar3 xref +cd .. +git diff --check +``` + +For descriptor-only changes, `rebar3 eunit` is the main package test. For +controller or C driver changes, also run the broader parent AtomGL tests +available in your environment. If hardware is available, verify: + +- Full refresh clears and draws correctly. +- Fast or partial refresh updates the expected window without stale-plane + artifacts. +- Standby/sleep/deep-sleep and wake work repeatedly for every exposed + `sleep_modes` entry. +- 4-gray or color palettes map white, gray/color levels, and black correctly. +- Timeout behavior matches the panel BUSY polarity. + +Record which hardware revision was tested. Some Waveshare panels changed +controller behavior across V1/V2/V3 or after a production date. + +## LLM Agent Rules + +When an LLM agent adds a panel: + +- State which source files or datasheets were used and their licenses. +- Use GPL sources only as reference; never copy bytes, arrays, or comments from + them. +- Prefer Waveshare or other permissive vendor sources for command bytes and LUTs + when available. +- Do not claim an exact controller when the evidence only says `UC81xx`, + `SSD16xx`, or another family name. +- Keep the change in a separate commit for each controller or panel family. +- Keep descriptor tests in `atomgl_epaper/test`, not inline in the descriptor + module. +- Do not refactor unrelated display code while adding a panel. +- Run the verification commands above and report failures honestly. + +When in doubt, leave a narrowly worded note in the descriptor name or docs, such +as `UC81xx-compatible`, instead of encoding a guessed controller identity. diff --git a/atomgl_epaper/CUSTOM_PANEL.md b/atomgl_epaper/CUSTOM_PANEL.md new file mode 100644 index 0000000..8a8e20d --- /dev/null +++ b/atomgl_epaper/CUSTOM_PANEL.md @@ -0,0 +1,255 @@ + + + +# Custom E-Paper Panels + +You can prototype a panel descriptor in your own Erlang or Elixir project +without adding a lookup string to AtomGL. Use `atomgl_epaper_descriptor` to +build and validate the descriptor, and use `atomgl_epaper_program` to build the +bytecode programs consumed by the C display driver. + +Descriptors are keyword/proplists all the way down. The validator checks scalar +ranges, controller/layout compatibility, bytecode structure, sleep mode +programs, and ACeP raw init sequences so malformed custom panels fail before the +display port opens. + +Once the descriptor works on hardware, it can be copied into +`atomgl_epaper/src/atomgl_epaper_panels.erl` with a public lookup string and a +focused descriptor test. + +## Example: Waveshare 1.54 V2 200x200 + +The example below is a userland descriptor module for the Waveshare 1.54 inch +V2 200x200 monochrome panel. The command sequence and waveform bytes are +adapted from Waveshare's MIT-licensed `EPD_1in54_V2` driver: + +- `Arduino_R4/src/e-Paper/EPD_1in54_V2.cpp` +- `Arduino_R4/src/e-Paper/EPD_1in54_V2.h` + +The AtomGL controller atom is `ssd16xx`; this covers SSD1680-compatible +program-driven monochrome panels. + +```erlang +%% SPDX-License-Identifier: MIT +%% SPDX-SnippetCopyrightText: Waveshare team + +-module(my_epaper_200x200). + +-export([ + descriptor/0, + descriptor/1 +]). + +-import(atomgl_epaper_program, [ + program/1, + cmd/1, + cmd/2, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + delay_ms/1 +]). + +-define(LUT_FULL, 0). +-define(LUT_PARTIAL, 1). + +descriptor() -> + descriptor(#{}). + +descriptor(Opts) -> + atomgl_epaper_descriptor:ssd16xx_panel( + "Waveshare 1.54 V2 200x200 e-paper SSD16xx-compatible", + maps:merge(#{ + native_width => 200, + native_height => 200, + refresh_modes => [full, partial], + default_refresh => full, + init => init(), + full => full_refresh(), + partial => partial_refresh(), + sleep_modes => [ + {sleep, [ + {enter, cmd(16#10, <<16#01>>)}, + {wake, reset_init}, + {controller_ram, unknown}, + {host_prev_frame, preserve}, + {after_wake_refresh, allow_if_program_reseeds} + ]} + ], + lut_full => wf_full_1in54(), + lut_partial => wf_partial_1in54() + }, Opts)). + +%% `allow_if_program_reseeds` allows fast/partial after wake only when the +%% selected refresh program includes insert_prev_frame/1. + +init() -> + program([ + reset(200, 2, 200), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#01, <<16#C7, 16#00, 16#01>>), + cmd(16#11, <<16#01>>), + full_window(), + cmd(16#3C, <<16#01>>), + cmd(16#18, <<16#80>>), + cmd(16#22, <<16#B1>>), + cmd(16#20), + set_ram_cursor(), + wait_busy(0, 5000), + insert_lut(?LUT_FULL) + ]). + +full_refresh() -> + program([ + insert_lut(?LUT_FULL), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +partial_refresh() -> + program([ + insert_lut(?LUT_PARTIAL), + cmd(16#37, <<16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#00, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + cmd(16#22, <<16#C0>>), + cmd(16#20), + wait_busy(0, 5000), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#CF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +full_window() -> + [ + cmd(16#44, <<16#00, 16#18>>), + cmd(16#45, <<16#C7, 16#00, 16#00, 16#00>>) + ]. + +set_ram_cursor() -> + [ + cmd(16#4E, <<16#00>>), + cmd(16#4F, <<16#C7, 16#00>>) + ]. + +wf_full_1in54() -> + << + 16#80, 16#48, 16#40, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#48, 16#80, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#48, 16#40, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#48, 16#80, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0A, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#08, 16#01, 16#00, 16#08, 16#01, + 16#00, 16#02, 16#0A, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, + 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#00, 16#32, 16#20 + >>. + +wf_partial_1in54() -> + << + 16#00, 16#40, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#80, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#40, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#80, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0F, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#01, 16#01, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, + 16#00, 16#00, 16#00, 16#02, 16#17, 16#41, + 16#B0, 16#32, 16#28 + >>. +``` + +Use the descriptor exactly like a built-in one: + +```erlang +{ok, Descriptor} = my_epaper_200x200:descriptor(), + +Port = open_port({spawn, "display"}, [ + {descriptor, Descriptor}, + {spi_host, spi2}, + {cs, 5}, + {dc, 17}, + {reset, 16}, + {busy, 4} +]). +``` + +The GPIO numbers above are placeholders. Use the pins from your board wiring. + +For ACeP color panels, build `init_seq` and `frame_preamble_seq` with +`atomgl_epaper_program:init_seq/1`, `init_cmd/1,2`, and `init_cmd_delay/3`. +Those raw command sequences are separate from interpreted refresh/sleep +bytecode and do not support `wait_busy/2`, `reset/3`, or render operations. + +## Moving A Working Panel Into AtomGL + +After hardware testing, move the descriptor into +`atomgl_epaper/src/atomgl_epaper_panels.erl`: + +- Add a primary lookup string, such as `"waveshare,epd1in54_V2"`. +- Keep aliases or mode-default variants close to the primary clause. +- Keep copied vendor LUTs under an SPDX snippet notice. +- Add descriptor tests for controller, dimensions, refresh modes, and LUT byte + sizes. +- Update `README.md` and `docs/display-drivers.md` with the new lookup string. diff --git a/atomgl_epaper/LICENSE b/atomgl_epaper/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/atomgl_epaper/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + 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/atomgl_epaper/README.md b/atomgl_epaper/README.md new file mode 100644 index 0000000..4a34b43 --- /dev/null +++ b/atomgl_epaper/README.md @@ -0,0 +1,75 @@ + + +# atomgl_epaper + +A small Erlang/OTP library that provides ready-made panel descriptors and a +bytecode builder for e-paper displays driven by [AtomGL](../README.Md). + +The main public entry point is `atomgl_epaper`, whose `panel/1` and +`panel/2` functions return a validated descriptor (`{ok, Descriptor}`) that +can be passed straight to the AtomGL e-paper display driver. + +For custom out-of-tree panels, `atomgl_epaper_descriptor` and +`atomgl_epaper_program` can be used directly from your own project. See +[`CUSTOM_PANEL.md`](CUSTOM_PANEL.md). + +## Usage + +```erlang +{ok, Descriptor} = atomgl_epaper:panel("waveshare,epd2in9_V2"), +%% Descriptor can now be used to open the AtomGL display port. +``` + +An optional options map may override defaults such as `orientation`, +`rotation`, `view_width`, `view_height`, `spi_clock_hz`, etc.: + +```erlang +{ok, Descriptor} = + atomgl_epaper:panel("waveshare,epd2in9_V2", + #{orientation => portrait}). +``` + +## Adding this library to your project + +This library lives inside the [AtomGL](https://github.com/atomvm/atomgl) +mono-repo. Both `rebar3` and `mix` can pull just this subdirectory. + +### rebar3 + +```erlang +%% rebar.config +{deps, [ + {atomgl_epaper, + {git, "https://github.com/atomvm/atomgl.git", {branch, "main"}}, + [{subdir, "atomgl_epaper"}]} +]}. +``` + +### Elixir (Mix) + +```elixir +# mix.exs +defp deps do + [ + {:atomgl_epaper, + github: "atomvm/atomgl", + branch: "main", + sparse: "atomgl_epaper"} + ] +end +``` + +## Tests + +```bash +cd atomgl_epaper +rebar3 eunit +``` + +## License + +Apache-2.0. See [`LICENSE`](LICENSE). diff --git a/atomgl_epaper/rebar.config b/atomgl_epaper/rebar.config new file mode 100644 index 0000000..637c005 --- /dev/null +++ b/atomgl_epaper/rebar.config @@ -0,0 +1,16 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +{erl_opts, [debug_info]}. + +{deps, []}. + +{eunit_opts, [verbose]}. + +{xref_checks, [ + undefined_function_calls, + undefined_functions, + locals_not_used, + deprecated_function_calls, + deprecated_functions +]}. diff --git a/atomgl_epaper/src/atomgl_epaper.app.src b/atomgl_epaper/src/atomgl_epaper.app.src new file mode 100644 index 0000000..3ee48a7 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper.app.src @@ -0,0 +1,13 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +{application, atomgl_epaper, [ + {description, "AtomGL e-paper panel descriptors and bytecode builders"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [kernel, stdlib]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, [{"GitHub", "https://github.com/atomvm/atomgl"}]} +]}. diff --git a/atomgl_epaper/src/atomgl_epaper.erl b/atomgl_epaper/src/atomgl_epaper.erl new file mode 100644 index 0000000..901202c --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper.erl @@ -0,0 +1,23 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper). + +-export([ + panel/1, + panel/2, + validate_descriptor/1 +]). + +panel(Compatible) when is_binary(Compatible) -> + panel(binary_to_list(Compatible), #{}); +panel(Compatible) -> + panel(Compatible, #{}). + +panel(Compatible, Opts) when is_binary(Compatible) -> + panel(binary_to_list(Compatible), Opts); +panel(Compatible, Opts) -> + atomgl_epaper_panels:panel(Compatible, Opts). + +validate_descriptor(Desc) -> + atomgl_epaper_descriptor:validate_descriptor(Desc). diff --git a/atomgl_epaper/src/atomgl_epaper_descriptor.erl b/atomgl_epaper/src/atomgl_epaper_descriptor.erl new file mode 100644 index 0000000..28316f8 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_descriptor.erl @@ -0,0 +1,556 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_descriptor). + +-define(MAX_SLEEP_MODES, 8). +-define(MAX_DESCRIPTOR_BINARY_LEN, 4096). +-define(MAX_INT, 16#7FFFFFFF). +-define(MAX_DIMENSION, (?MAX_INT - 7)). +-define(MAX_FRAME_BYTES, 16#FFFFFFFF). +-define(PROGRAM_DELAY, 16#80). +-define(PROGRAM_META, 16#40). +-define(PROGRAM_LEN_MASK, 16#3F). +-define(INIT_SEQ_DELAY, 16#80). +-define(INIT_SEQ_LEN_MASK, 16#7F). + +-define(OP_WAIT_BUSY, 16#01). +-define(OP_RESET, 16#02). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_LUT, 16#04). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). +-define(OP_MARK_PREV_VALID, 16#07). +-define(OP_LABEL, 16#10). + +-export([ + ssd16xx_panel/2, + jd79656_panel/2, + uc8151_panel/2, + uc8276_panel/2, + acep7_panel/2, + validate_descriptor/1 +]). + +ssd16xx_panel(Name, Config) -> + build_panel(ssd16xx, Name, Config). + +jd79656_panel(Name, Config) -> + build_panel(jd79656, Name, Config). + +uc8151_panel(Name, Config) -> + build_panel(uc8151, Name, Config). + +uc8276_panel(Name, Config) -> + build_panel(uc8276, Name, Config). + +acep7_panel(Name, Config) -> + NativeW = maps:get(native_width, Config), + NativeH = maps:get(native_height, Config), + Rotation = maps:get(rotation, Config, 0), + ViewW = maps:get(view_width, Config, NativeW), + ViewH = maps:get(view_height, Config, NativeH), + + Descriptor = [ + {descriptor_version, 3}, + {name, Name}, + {controller, acep7}, + {native_width, NativeW}, + {native_height, NativeH}, + {view_width, ViewW}, + {view_height, ViewH}, + {rotation, Rotation}, + {spi_clock_hz, maps:get(spi_clock_hz, Config, 4000000)}, + {busy_idle_level, maps:get(busy_idle_level, Config, 1)}, + {use_gpio_pullups, maps:get(use_gpio_pullups, Config, true)}, + {frame_layout, maps:get(frame_layout, Config, row_msb)}, + {polarity, maps:get(polarity, Config, white_1)}, + {refresh_modes, maps:get(refresh_modes, Config, [full])}, + {default_refresh, maps:get(default_refresh, Config, full)}, + {palette, maps:get(palette, Config)}, + {palette_size, maps:get(palette_size, Config, 7)}, + {init_seq, maps:get(init_seq, Config)}, + {init_wait_busy_between_cmds, maps:get(init_wait_busy_between_cmds, Config, false)}, + {refresh_has_data, maps:get(refresh_has_data, Config, false)}, + {refresh_data_byte, maps:get(refresh_data_byte, Config, 0)}, + {post_power_off_busy_level, maps:get(post_power_off_busy_level, Config, 0)}, + {periodic_refresh_interval, maps:get(periodic_refresh_interval, Config, 0)}, + {sleep_modes, maps:get(sleep_modes, Config, [])} + ] ++ case maps:find(frame_preamble_seq, Config) of + {ok, FramePreambleSeq} -> [{frame_preamble_seq, FramePreambleSeq}]; + error -> [] + end, + + validate_descriptor(Descriptor). + +build_panel(Controller, Name, Config) -> + NativeW = maps:get(native_width, Config), + NativeH = maps:get(native_height, Config), + OrientationRotation = orientation_rotation(maps:get(orientation, Config, landscape)), + Rotation = maps:get(rotation, Config, OrientationRotation), + {DefaultViewW, DefaultViewH} = default_view_geometry(NativeW, NativeH, Rotation), + ViewW = maps:get(view_width, Config, DefaultViewW), + ViewH = maps:get(view_height, Config, DefaultViewH), + + DefaultRefresh = maps:get(default_refresh, Config, full), + RefreshModes = maps:get(refresh_modes, Config, [full]), + PaletteSize = maps:get(palette_size, Config, 2), + + Luts = [ + {lut_full, maps:get(lut_full, Config, <<>>)}, + {lut_partial, maps:get(lut_partial, Config, <<>>)}, + {lut_4gray, maps:get(lut_4gray, Config, <<>>)}, + {lut_fast, maps:get(lut_fast, Config, <<>>)} + ], + + Programs = mode_programs(Config), + Timing = timing(Config), + GhostingConfig = maps:get(ghosting, Config, #{}), + Ghosting = [ + {max_fast_refreshes, maps:get(max_fast_refreshes, GhostingConfig, maps:get(max_fast_refreshes, Config, 10))}, + {reseed_on_timeout, maps:get(reseed_on_timeout, GhostingConfig, maps:get(reseed_on_timeout, Config, true))} + ], + + {Layout, Polarity} = case Controller of + ssd16xx -> {row_msb, white_1}; + jd79656 -> {column_msb, black_1}; + uc8151 -> {row_msb, white_1}; + uc8276 -> {row_msb, white_1}; + uc8175 -> {row_msb, white_1} + end, + + Descriptor = [ + {descriptor_version, 3}, + {name, Name}, + {controller, Controller}, + {native_width, NativeW}, + {native_height, NativeH}, + {view_width, ViewW}, + {view_height, ViewH}, + {rotation, Rotation}, + {spi_clock_hz, maps:get(spi_clock_hz, Config, 4000000)}, + {busy_idle_level, maps:get(busy_idle_level, Config, 0)}, + {use_gpio_pullups, maps:get(use_gpio_pullups, Config, false)}, + {frame_layout, Layout}, + {polarity, Polarity}, + {refresh_modes, RefreshModes}, + {default_refresh, DefaultRefresh}, + {palette_size, PaletteSize}, + {programs, Programs}, + {sleep_modes, maps:get(sleep_modes, Config, [])}, + {timing, Timing}, + {ghosting, Ghosting} + ] ++ [L || L = {_, Bin} <- Luts, Bin =/= <<>>], + + validate_descriptor(Descriptor). + +orientation_rotation(landscape) -> + 90; +orientation_rotation(landscape_left) -> + 90; +orientation_rotation(landscape_right) -> + 270; +orientation_rotation(portrait) -> + 0; +orientation_rotation(portrait_flipped) -> + 180; +orientation_rotation(Orientation) -> + error({bad_orientation, Orientation}). + +default_view_geometry(NativeW, NativeH, Rotation) -> + case Rotation of + 0 -> {NativeW, NativeH}; + 180 -> {NativeW, NativeH}; + 90 -> {NativeH, NativeW}; + 270 -> {NativeH, NativeW}; + _ -> {NativeW, NativeH} + end. + +mode_programs(Config) -> + P0 = [], + P1 = case maps:find(init, Config) of + {ok, Init} -> [{init, Init} | P0]; + error -> P0 + end, + P2 = case maps:find(full, Config) of + {ok, Full} -> [{full, Full} | P1]; + error -> P1 + end, + P3 = case maps:find(fast, Config) of + {ok, Fast} -> [{fast, Fast} | P2]; + error -> P2 + end, + P4 = case maps:find(partial, Config) of + {ok, Partial} -> [{partial, Partial} | P3]; + error -> P3 + end, + P5 = case maps:find('4gray', Config) of + {ok, FourGray} -> [{'4gray', FourGray} | P4]; + error -> P4 + end, + P5. + +timing(Config) -> + TimingConfig = maps:get(timing, Config, #{}), + [ + {full_expected_ms, config_value(full_expected_ms, TimingConfig, Config, 2000)}, + {fast_expected_ms, config_value(fast_expected_ms, TimingConfig, Config, 500)}, + {poll_interval_ms, config_value(poll_interval_ms, TimingConfig, Config, 50)}, + {timeout_ms, config_value(timeout_ms, TimingConfig, Config, 5000)} + ]. + +config_value(Key, Nested, Config, Default) when is_map(Nested) -> + maps:get(Key, Nested, maps:get(Key, Config, Default)); +config_value(Key, _Nested, Config, Default) -> + maps:get(Key, Config, Default). + +validate_descriptor(Desc) when is_list(Desc) -> + Version = get_value(descriptor_version, Desc), + validate_int(descriptor_version, Version, 3, 3), + + NativeW = get_value(native_width, Desc), + NativeH = get_value(native_height, Desc), + ViewW = get_value(view_width, Desc), + ViewH = get_value(view_height, Desc), + Rotation = get_value(rotation, Desc), + validate_int(native_width, NativeW, 1, ?MAX_DIMENSION), + validate_int(native_height, NativeH, 1, ?MAX_DIMENSION), + validate_int(view_width, ViewW, 1, ?MAX_DIMENSION), + validate_int(view_height, ViewH, 1, ?MAX_DIMENSION), + validate_int(rotation, Rotation, 0, 270), + + case Rotation of + 0 -> + require_true(view_width, ViewW == NativeW, bad_geometry), + require_true(view_height, ViewH == NativeH, bad_geometry); + 180 -> + require_true(view_width, ViewW == NativeW, bad_geometry), + require_true(view_height, ViewH == NativeH, bad_geometry); + 90 -> + require_true(view_width, ViewW == NativeH, bad_geometry), + require_true(view_height, ViewH == NativeW, bad_geometry); + 270 -> + require_true(view_width, ViewW == NativeH, bad_geometry), + require_true(view_height, ViewH == NativeW, bad_geometry); + _ -> + error({bad_rotation, Rotation}) + end, + + Controller = get_value(controller, Desc), + validate_member(controller, Controller, [ssd16xx, jd79656, uc8151, uc8276, uc8175, acep7]), + + Layout = get_value(frame_layout, Desc), + require_true(frame_layout, case Controller of + ssd16xx -> lists:member(Layout, [row_msb, row_lsb]); + uc8151 -> lists:member(Layout, [row_msb, row_lsb]); + uc8276 -> lists:member(Layout, [row_msb, row_lsb]); + uc8175 -> lists:member(Layout, [row_msb, row_lsb]); + jd79656 -> lists:member(Layout, [column_msb, column_lsb]); + _ -> true + end, incompatible_with_controller), + case Controller of + acep7 -> ok; + _ -> + FrameBytes = case Layout of + column_msb -> NativeW * ((NativeH + 7) div 8); + column_lsb -> NativeW * ((NativeH + 7) div 8); + _ -> ((NativeW + 7) div 8) * NativeH + end, + require_true(frame_layout, FrameBytes =< ?MAX_FRAME_BYTES, + frame_size_overflow) + end, + validate_member(polarity, get_value(polarity, Desc), [white_1, black_1]), + validate_int(spi_clock_hz, get_value(spi_clock_hz, Desc), 1, 80000000), + validate_member(busy_idle_level, get_value(busy_idle_level, Desc), [0, 1]), + validate_bool(use_gpio_pullups, get_value(use_gpio_pullups, Desc)), + + DefaultRefresh = get_value(default_refresh, Desc), + RefreshModes = get_value(refresh_modes, Desc), + validate_refresh_modes(RefreshModes), + validate_member(default_refresh, DefaultRefresh, RefreshModes), + validate_sleep_modes(get_value(sleep_modes, Desc, [])), + PaletteSize = get_value(palette_size, Desc), + validate_member(palette_size, PaletteSize, [2, 4, 7]), + validate_timing(get_value(timing, Desc, [])), + validate_ghosting(get_value(ghosting, Desc, [])), + + case Controller of + acep7 -> + require_true(rotation, Rotation == 0, acep7_native_orientation_only), + require_true(native_width, (NativeW rem 2) == 0, acep7_width_must_be_even), + require_true(view_width, (ViewW rem 2) == 0, acep7_width_must_be_even), + validate_member(palette_size, PaletteSize, [7]), + Palette = get_value(palette, Desc), + validate_member(palette, Palette, [acep7, acep7c, gdep073e01]), + InitSeq = get_value(init_seq, Desc), + validate_required_init_seq(init_seq, InitSeq), + FramePreambleSeq = get_value(frame_preamble_seq, Desc, <<>>), + validate_init_seq(frame_preamble_seq, FramePreambleSeq), + validate_bool(init_wait_busy_between_cmds, get_value(init_wait_busy_between_cmds, Desc)), + validate_bool(refresh_has_data, get_value(refresh_has_data, Desc)), + validate_int(refresh_data_byte, get_value(refresh_data_byte, Desc), 0, 255), + validate_member(post_power_off_busy_level, + get_value(post_power_off_busy_level, Desc), [0, 1]), + validate_int(periodic_refresh_interval, + get_value(periodic_refresh_interval, Desc), 0, ?MAX_INT), + validate_member(refresh_modes, RefreshModes, [[full]]); + _ -> + require_true(palette_size, PaletteSize =/= 7, acep7_only), + require_true(palette_size, + PaletteSize =/= 4 orelse lists:member(Controller, [ssd16xx, uc8276]), + unsupported_4gray_controller), + require_true(palette_size, + not lists:member('4gray', RefreshModes) orelse PaletteSize == 4, + required_for_4gray), + Programs = get_value(programs, Desc), + validate_kv_list(programs, Programs), + require_true(programs, is_defined(init, Programs), missing_init), + + lists:foreach(fun(Mode) -> + require_true(programs, is_defined(Mode, Programs), + {missing_refresh_program, Mode}) + end, RefreshModes), + + validate_required_program(init, get_value(init, Programs), false), + lists:foreach(fun(Mode) -> + validate_required_program(Mode, get_value(Mode, Programs), true) + end, RefreshModes), + lists:foreach(fun({Mode, Program}) -> + validate_program(Mode, Program, + lists:member(Mode, [full, fast, partial, '4gray'])) + end, Programs), + validate_optional_binary(lut_full, get_value(lut_full, Desc, <<>>)), + validate_optional_binary(lut_partial, get_value(lut_partial, Desc, <<>>)), + validate_optional_binary(lut_4gray, get_value(lut_4gray, Desc, <<>>)), + validate_optional_binary(lut_fast, get_value(lut_fast, Desc, <<>>)) + end, + + {ok, Desc}; +validate_descriptor(_) -> + error({bad_descriptor, expected_keyword_list}). + +bad_field(Key, Reason) -> + error({bad_field, Key, Reason}). + +require_true(_Key, true, _Reason) -> + ok; +require_true(Key, false, Reason) -> + bad_field(Key, Reason). + +validate_int(Key, Value, Min, Max) when is_integer(Value), Value >= Min -> + case Max of + infinity -> ok; + _ when Value =< Max -> ok; + _ -> bad_field(Key, {out_of_range, Min, Max}) + end; +validate_int(Key, _Value, Min, Max) -> + bad_field(Key, {expected_integer, Min, Max}). + +validate_bool(_Key, Value) when is_boolean(Value) -> + ok; +validate_bool(Key, _Value) -> + bad_field(Key, expected_boolean). + +validate_member(_Key, Value, Allowed) -> + case lists:member(Value, Allowed) of + true -> ok; + false -> bad_field(_Key, {expected_one_of, Allowed}) + end. + +validate_kv_list(Key, Value) when is_list(Value) -> + lists:foreach(fun + ({K, _V}) when is_atom(K) -> ok; + (_) -> bad_field(Key, expected_keyword_list) + end, Value); +validate_kv_list(Key, _Value) -> + bad_field(Key, expected_keyword_list). + +validate_refresh_modes(RefreshModes) when is_list(RefreshModes) -> + require_true(refresh_modes, RefreshModes =/= [], empty), + require_true(refresh_modes, lists:member(full, RefreshModes), missing_full), + require_true(refresh_modes, + length(RefreshModes) == length(lists:usort(RefreshModes)), duplicate), + lists:foreach(fun(Mode) -> + validate_member(refresh_modes, Mode, [full, fast, partial, '4gray']) + end, RefreshModes); +validate_refresh_modes(_) -> + bad_field(refresh_modes, expected_list). + +validate_timing(Timing) -> + validate_kv_list(timing, Timing), + validate_int(full_expected_ms, get_value(full_expected_ms, Timing, 2000), 0, ?MAX_INT), + validate_int(fast_expected_ms, get_value(fast_expected_ms, Timing, 500), 0, ?MAX_INT), + validate_int(poll_interval_ms, get_value(poll_interval_ms, Timing, 50), 1, ?MAX_INT), + validate_int(timeout_ms, get_value(timeout_ms, Timing, 5000), 1, ?MAX_INT). + +validate_ghosting(Ghosting) -> + validate_kv_list(ghosting, Ghosting), + validate_int(max_fast_refreshes, get_value(max_fast_refreshes, Ghosting, 10), 0, ?MAX_INT), + validate_bool(reseed_on_timeout, get_value(reseed_on_timeout, Ghosting, true)). + +validate_optional_binary(_Key, <<>>) -> + ok; +validate_optional_binary(_Key, Bin) when is_binary(Bin), byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + ok; +validate_optional_binary(Key, _Value) -> + bad_field(Key, expected_binary). + +validate_program(Key, Bin, AllowRenderOps) when is_binary(Bin), + byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + validate_program_bytes(Key, Bin, AllowRenderOps), + validate_program_flow(Key, Bin, false, false); +validate_program(Key, _Value, _AllowRenderOps) -> + bad_field(Key, expected_program_binary). + +validate_required_program(Key, <<>>, _AllowRenderOps) -> + bad_field(Key, empty_program); +validate_required_program(Key, Bin, AllowRenderOps) -> + validate_program(Key, Bin, AllowRenderOps). + +validate_program_bytes(_Key, <<>>, _AllowRenderOps) -> + ok; +validate_program_bytes(Key, <>, AllowRenderOps) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + case Rest of + <> -> + case IsMeta of + true -> validate_meta(Key, Opcode, byte_size(Data), AllowRenderOps); + false -> ok + end, + case {HasDelay, Tail} of + {true, <<_Delay, Next/binary>>} -> + validate_program_bytes(Key, Next, AllowRenderOps); + {true, _} -> + bad_field(Key, truncated_delay); + {false, _} -> + validate_program_bytes(Key, Tail, AllowRenderOps) + end; + _ -> + bad_field(Key, truncated_record) + end; +validate_program_bytes(Key, _Bad, _AllowRenderOps) -> + bad_field(Key, truncated_record). + +validate_program_flow(Key, <<>>, true, _CaptureCompleted) -> + bad_field(Key, capture_without_plane); +validate_program_flow(_Key, <<>>, false, _CaptureCompleted) -> + ok; +validate_program_flow(Key, <>, + CapturePending, CaptureCompleted) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + <<_Data:Len/binary, Tail0/binary>> = Rest, + Tail = case HasDelay of + true -> + <<_Delay, Next/binary>> = Tail0, + Next; + false -> + Tail0 + end, + {NextPending, NextCompleted} = case {IsMeta, Opcode, CapturePending} of + {true, ?OP_CAPTURE_FRAME, true} -> + bad_field(Key, capture_already_pending); + {true, ?OP_CAPTURE_FRAME, false} -> + {true, CaptureCompleted}; + {true, ?OP_INSERT_PLANE, true} -> + {false, true}; + {true, ?OP_MARK_PREV_VALID, _} when not CaptureCompleted -> + bad_field(Key, mark_without_capture); + _ -> + {CapturePending, CaptureCompleted} + end, + validate_program_flow(Key, Tail, NextPending, NextCompleted). + +validate_meta(_Key, ?OP_WAIT_BUSY, 3, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_RESET, 3, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_INSERT_LUT, 1, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_LABEL, 1, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_INSERT_PLANE, 1, true) -> ok; +validate_meta(_Key, ?OP_INSERT_PREV_FRAME, 1, true) -> ok; +validate_meta(_Key, ?OP_CAPTURE_FRAME, 0, true) -> ok; +validate_meta(_Key, ?OP_MARK_PREV_VALID, 0, true) -> ok; +validate_meta(Key, Opcode, Len, _AllowRenderOps) -> + bad_field(Key, {bad_meta_opcode, Opcode, Len}). + +validate_init_seq(Key, Bin) when is_binary(Bin), + byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + validate_init_seq_bytes(Key, Bin); +validate_init_seq(Key, _Value) -> + bad_field(Key, expected_init_sequence_binary). + +validate_required_init_seq(Key, <<>>) -> + bad_field(Key, empty_sequence); +validate_required_init_seq(Key, Bin) -> + validate_init_seq(Key, Bin). + +validate_init_seq_bytes(_Key, <<>>) -> + ok; +validate_init_seq_bytes(Key, <<_Cmd, FlagsLen, Rest/binary>>) -> + Len = FlagsLen band ?INIT_SEQ_LEN_MASK, + HasDelay = (FlagsLen band ?INIT_SEQ_DELAY) =/= 0, + case Rest of + <<_Data:Len/binary, Tail/binary>> -> + case {HasDelay, Tail} of + {true, <<_Delay, Next/binary>>} -> + validate_init_seq_bytes(Key, Next); + {true, _} -> + bad_field(Key, truncated_delay); + {false, _} -> + validate_init_seq_bytes(Key, Tail) + end; + _ -> + bad_field(Key, truncated_record) + end; +validate_init_seq_bytes(Key, _Bad) -> + bad_field(Key, truncated_record). + +validate_sleep_modes(SleepModes) when is_list(SleepModes) -> + require_true(sleep_modes, length(SleepModes) =< ?MAX_SLEEP_MODES, too_many), + Modes = lists:map(fun + ({Mode, _Props}) when is_atom(Mode) -> Mode; + (_) -> bad_field(sleep_modes, bad_sleep_mode) + end, SleepModes), + require_true(sleep_modes, length(Modes) == length(lists:usort(Modes)), duplicate), + lists:foreach(fun validate_sleep_mode/1, SleepModes); +validate_sleep_modes(_) -> + bad_field(sleep_modes, expected_list). + +validate_sleep_mode({Mode, Props}) when is_atom(Mode), is_list(Props) -> + validate_kv_list(Mode, Props), + Enter = get_value(enter, Props), + validate_required_program(Mode, Enter, false), + Wake = get_value(wake, Props, reset_init), + case Wake of + Bin when is_binary(Bin) -> + validate_required_program(Mode, Bin, false); + _ -> + validate_member(Mode, Wake, [init, reset_init]) + end, + validate_member(Mode, get_value(controller_ram, Props, unknown), + [unknown, retained, lost]), + validate_member(Mode, get_value(host_prev_frame, Props, invalidate), + [preserve, invalidate]), + validate_member(Mode, get_value(after_wake_refresh, Props, full), + [allow, full, allow_if_program_reseeds]); +validate_sleep_mode(_) -> + bad_field(sleep_modes, bad_sleep_mode). + +get_value(Key, List) -> + get_value(Key, List, undefined). + +get_value(Key, [{Key, Val} | _], _Default) -> + Val; +get_value(Key, [_ | Tail], Default) -> + get_value(Key, Tail, Default); +get_value(_Key, [], Default) -> + Default. + +is_defined(Key, [{Key, _} | _]) -> + true; +is_defined(Key, [_ | Tail]) -> + is_defined(Key, Tail); +is_defined(_Key, []) -> + false. diff --git a/atomgl_epaper/src/atomgl_epaper_panels.erl b/atomgl_epaper/src/atomgl_epaper_panels.erl new file mode 100644 index 0000000..3ff2eb0 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_panels.erl @@ -0,0 +1,841 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_panels). + +-export([ + panel/2 +]). + +-import(atomgl_epaper_descriptor, [ + acep7_panel/2, + jd79656_panel/2, + ssd16xx_panel/2, + uc8151_panel/2, + uc8276_panel/2 +]). + +-import(atomgl_epaper_program, [ + program/1, + init_seq/1, + cmd/1, + cmd/2, + init_cmd/1, + init_cmd/2, + init_cmd_delay/3, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + label_delay/2, + delay_ms/1 +]). + +-define(LUT_FULL, 0). +-define(LUT_PARTIAL, 1). +-define(LUT_4GRAY, 2). +-define(LUT_FAST, 3). + +panel("waveshare,epaper-2in9", Opts) -> + panel("waveshare,epd2in9", Opts); +panel("waveshare,epd2in9", Opts) -> + uc8151_panel("Waveshare epd2in9 2.9\" e-paper UC8151 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + refresh_modes => [full, partial], + default_refresh => full, + init => uc8151_init(296, 16#03, ?LUT_FULL), + full => uc8151_refresh(128, 296, ?LUT_FULL), + partial => uc8151_refresh(128, 296, ?LUT_PARTIAL), + sleep_modes => controller_sleep_modes(sleep_program(0)), + lut_full => uc8151_2in9_full_lut(), + lut_partial => uc8151_2in9_partial_lut() + }, Opts)); +panel("waveshare,epd2in9_V2", Opts) -> + ssd16xx_panel("Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + palette_size => 4, + refresh_modes => [full, fast, partial, '4gray'], + default_refresh => full, + init => epd2in9_v2_init(none), + full => epd2in9_v2_refresh(?LUT_FULL, 16#C7), + fast => epd2in9_v2_fast_refresh(), + partial => epd2in9_v2_partial_refresh(), + '4gray' => epd2in9_v2_4gray_refresh(), + sleep_modes => ssd16xx_sleep_modes(100), + lut_full => epd2in9_v2_default_lut(), + lut_fast => epd2in9_v2_fast_lut(), + lut_partial => epd2in9_v2_partial_lut(), + lut_4gray => epd2in9_v2_4gray_lut() + }, Opts)); +panel("waveshare,epd2in9_V2-fast", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd2in9_V2-partial", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd2in9_V2-4gray", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => '4gray'}, Opts)); +panel("dke,depg0290bns800f6", Opts) -> + ssd16xx_panel("DKE DEPG0290BNS800F6 2.9\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + refresh_modes => [full], + default_refresh => full, + init => ssd1680_2in9_init(), + full => ssd1680_2in9_refresh(), + sleep_modes => ssd16xx_sleep_modes(0) + }, Opts)); +panel("waveshare,epaper-2in13", Opts) -> + panel("waveshare,epd2in13_V4", Opts); +panel("waveshare,epd2in13", Opts) -> + uc8151_panel("Waveshare epd2in13 2.13\" e-paper UC8151 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + refresh_modes => [full, partial], + default_refresh => full, + init => uc8151_init(250, 16#63, ?LUT_FULL), + full => uc8151_refresh(122, 250, ?LUT_FULL), + partial => uc8151_refresh(122, 250, ?LUT_PARTIAL), + sleep_modes => controller_sleep_modes(sleep_program(0)), + lut_full => uc8151_2in13_full_lut(), + lut_partial => uc8151_2in13_partial_lut() + }, Opts)); +panel("waveshare,epd2in13_V4", Opts) -> + ssd16xx_panel("Waveshare epd2in13_V4 2.13\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + refresh_modes => [full, fast, partial], + default_refresh => full, + init => epd2in13_v4_init(), + full => epd2in13_v4_refresh(16#F7), + fast => epd2in13_v4_fast_refresh(), + partial => epd2in13_v4_partial_refresh(), + sleep_modes => ssd16xx_sleep_modes(100) + }, Opts)); +panel("waveshare,epd2in13_V4-fast", Opts) -> + panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd2in13_V4-partial", Opts) -> + panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd4in2_V2", Opts) -> + uc8276_panel("Waveshare epd4in2_V2 4.2\" e-paper UC8276 v1", maps:merge(#{ + native_width => 400, + native_height => 300, + palette_size => 4, + refresh_modes => [full, fast, partial, '4gray'], + default_refresh => full, + init => uc8276_init(), + full => uc8276_refresh(uc8276_mono_prelude(), 16#F7), + fast => uc8276_fast_refresh(16#6E), + partial => uc8276_partial_refresh(), + '4gray' => uc8276_4gray_refresh(), + sleep_modes => controller_sleep_modes(sleep_program(200)), + lut_4gray => uc8276_4gray_lut() + }, Opts)); +panel("waveshare,epd4in2_V2-fast", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd4in2_V2-partial", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd4in2_V2-4gray", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => '4gray'}, Opts)); +panel("heltec,lcmen2r13efc1", Opts) -> + jd79656_panel("Heltec LCMEN2R13EFC1 2.13\" e-paper JD79656 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + busy_idle_level => 1, + refresh_modes => [full], + default_refresh => full, + init => heltec_lcmen2r13efc1_init(), + full => heltec_lcmen2r13efc1_refresh(), + sleep_modes => standby_sleep_modes(program([cmd(16#02), wait_busy(1, 5000)])) + }, Opts)); +panel("heltec,icmen2r13efc1", Opts) -> + panel("heltec,lcmen2r13efc1", Opts); +panel("heltec,ht-vme213", Opts) -> + panel("heltec,lcmen2r13efc1", Opts); +panel("waveshare,5in65-acep-7c", Opts) -> + acep7_panel("Waveshare 5.65\" ACeP 7-color", maps:merge(#{ + native_width => 600, + native_height => 448, + spi_clock_hz => 1000000, + busy_idle_level => 1, + use_gpio_pullups => true, + palette => acep7, + init_seq => acep7c_init_seq(), + init_wait_busy_between_cmds => false, + frame_preamble_seq => acep7c_frame_preamble_seq(), + refresh_has_data => false, + refresh_data_byte => 0, + post_power_off_busy_level => 0, + periodic_refresh_interval => 5, + sleep_modes => acep7_sleep_modes(0, none) + }, Opts)); +panel("good-display/gdep073e01", Opts) -> + acep7_panel("Good Display GDEP073E01 7.3\" 7-color", maps:merge(#{ + native_width => 800, + native_height => 480, + spi_clock_hz => 4000000, + busy_idle_level => 1, + use_gpio_pullups => true, + palette => gdep073e01, + init_seq => gdep073e01_init_seq(), + init_wait_busy_between_cmds => true, + refresh_has_data => true, + refresh_data_byte => 0, + post_power_off_busy_level => 1, + periodic_refresh_interval => 0, + sleep_modes => acep7_sleep_modes(1, <<16#00>>) + }, Opts)); +panel(Compatible, _Opts) -> + {error, {unsupported_panel, Compatible}}. + +ssd16xx_sleep_modes(DelayMs) -> + [ + {sleep, sleep_mode(sleep_program(DelayMs), reset_init, + retained, preserve, allow_if_program_reseeds)}, + {deep_sleep, sleep_mode(deep_sleep_program(DelayMs), reset_init, + lost, preserve, allow_if_program_reseeds)} + ]. + +controller_sleep_modes(EnterProgram) -> + [ + {sleep, sleep_mode(EnterProgram, reset_init, + unknown, preserve, allow_if_program_reseeds)} + ]. + +standby_sleep_modes(EnterProgram) -> + [ + {standby, sleep_mode(EnterProgram, reset_init, + unknown, invalidate, full)} + ]. + +acep7_sleep_modes(BusyLevel, PowerOffData) -> + [ + {standby, sleep_mode(acep7_power_off_program(BusyLevel, PowerOffData), + init, unknown, invalidate, full)}, + {deep_sleep, sleep_mode(acep7_deep_sleep_program(BusyLevel, PowerOffData), + reset_init, lost, invalidate, full)} + ]. + +sleep_mode(Enter, Wake, ControllerRam, HostPrevFrame, AfterWakeRefresh) -> + [ + {enter, Enter}, + {wake, Wake}, + {controller_ram, ControllerRam}, + {host_prev_frame, HostPrevFrame}, + {after_wake_refresh, AfterWakeRefresh} + ]. + +sleep_program(0) -> + cmd(16#10, <<16#01>>); +sleep_program(DelayMs) -> + program([cmd(16#10, <<16#01>>), delay_ms(DelayMs)]). + +deep_sleep_program(0) -> + cmd(16#10, <<16#03>>); +deep_sleep_program(DelayMs) -> + program([cmd(16#10, <<16#03>>), delay_ms(DelayMs)]). + +acep7_power_off_program(BusyLevel, PowerOffData) -> + program(acep7_power_off_steps(BusyLevel, PowerOffData)). + +acep7_deep_sleep_program(BusyLevel, PowerOffData) -> + program([acep7_power_off_steps(BusyLevel, PowerOffData), cmd(16#07, <<16#A5>>)]). + +acep7_power_off_steps(BusyLevel, none) -> + [cmd(16#02), wait_busy(BusyLevel, 5000)]; +acep7_power_off_steps(BusyLevel, PowerOffData) -> + [cmd(16#02, PowerOffData), wait_busy(BusyLevel, 5000)]. + +uc8151_init(NativeHeight, BorderWaveform, LutSlot) -> + DriverOutput = NativeHeight - 1, + Border = case BorderWaveform of + none -> []; + _ -> [cmd(16#3C, <>)] + end, + program([ + reset(200, 2, 200), + cmd(16#01, <>), + cmd(16#0C, <<16#D7, 16#D6, 16#9D>>), + cmd(16#2C, <<16#A8>>), + cmd(16#3A, <<16#1A>>), + cmd(16#3B, <<16#08>>), + Border, + cmd(16#11, <<16#03>>), + insert_lut(LutSlot) + ]). + +uc8151_refresh(NativeWidth, NativeHeight, LutSlot) -> + program([ + insert_lut(LutSlot), + uc8151_window(NativeWidth, NativeHeight), + uc8151_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C4>>), + cmd(16#20), + cmd(16#FF), + delay_ms(100), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8151_window(NativeWidth, NativeHeight) -> + XEnd = NativeWidth - 1, + YEnd = NativeHeight - 1, + [ + cmd(16#44, <<16#00, ((XEnd bsr 3) band 16#FF)>>), + cmd(16#45, <<16#00, 16#00, YEnd:16/little-unsigned-integer>>) + ]. + +uc8151_cursor() -> + [ + cmd(16#4E, <<16#00>>), + cmd(16#4F, <<16#00, 16#00>>) + ]. + +uc8276_init() -> + program([ + reset(100, 2, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + uc8276_mono_prelude(), + wait_busy(0, 5000) + ]). + +uc8276_mono_prelude() -> + [ + cmd(16#21, <<16#40, 16#00>>), + cmd(16#3C, <<16#05>>), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor() + ]. + +uc8276_fast_prelude(Temperature) -> + [ + cmd(16#21, <<16#40, 16#00>>), + cmd(16#3C, <<16#05>>), + cmd(16#1A, <>), + cmd(16#22, <<16#91>>), + cmd(16#20), + wait_busy(0, 5000), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor() + ]. + +uc8276_refresh(Prelude, UpdateMode) -> + program([ + Prelude, + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8276_fast_refresh(Temperature) -> + program([ + uc8276_fast_prelude(Temperature), + cmd(16#26), + insert_prev_frame(0), + uc8276_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8276_partial_refresh() -> + program([ + cmd(16#3C, <<16#80>>), + cmd(16#21, <<16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + uc8276_window(), + uc8276_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#FF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8276_4gray_refresh() -> + program([ + reset(100, 2, 100), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#21, <<16#00, 16#00>>), + cmd(16#3C, <<16#03>>), + cmd(16#0C, <<16#8B, 16#9C, 16#A4, 16#0F>>), + insert_lut(?LUT_4GRAY), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor(), + cmd(16#24), + insert_plane(0), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#CF>>), + cmd(16#20), + wait_busy(0, 5000) + ]). + +uc8276_window() -> + [cmd(16#44, <<16#00, 16#31>>), cmd(16#45, <<16#00, 16#00, 16#2B, 16#01>>)]. + +uc8276_cursor() -> + [cmd(16#4E, <<16#00>>), cmd(16#4F, <<16#00, 16#00>>)]. + +ssd1680_2in9_init() -> + program([ + reset(10, 10, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + label_delay(0, 20), + cmd(16#3C, <<16#05>>), + cmd(16#2C, <<16#36>>), + cmd(16#03, <<16#17>>), + cmd(16#04, <<16#41, 16#00, 16#32>>), + cmd(16#11, <<16#03>>), + cmd(16#44, <<16#01, 16#10>>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + set_ram_cursor(1), + cmd(16#01, <<16#27, 16#01, 16#00>>) + ]). + +ssd1680_2in9_refresh() -> + program([ + set_ram_cursor(1), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(1), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#F4>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_init() -> + program([ + reset(20, 2, 20), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#01, <<16#F9, 16#00, 16#00>>), + cmd(16#11, <<16#03>>), + epd2in13_v4_window(), + epd2in13_v4_cursor(), + cmd(16#3C, <<16#05>>), + cmd(16#21, <<16#00, 16#80>>), + cmd(16#18, <<16#80>>), + wait_busy(0, 5000) + ]). + +epd2in13_v4_refresh(UpdateMode) -> + program([ + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + epd2in13_v4_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_fast_refresh() -> + program([ + epd2in13_v4_cursor(), + cmd(16#26), + insert_prev_frame(0), + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_partial_refresh() -> + program([ + reset(0, 1, 0), + cmd(16#3C, <<16#80>>), + cmd(16#01, <<16#F9, 16#00, 16#00>>), + cmd(16#11, <<16#03>>), + epd2in13_v4_window(), + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#FF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_window() -> + [cmd(16#44, <<16#00, 16#0F>>), cmd(16#45, <<16#00, 16#00, 16#F9, 16#00>>)]. + +epd2in13_v4_cursor() -> + [cmd(16#4E, <<16#00>>), cmd(16#4F, <<16#00, 16#00>>)]. + +%% SPDX-SnippetBegin +%% SPDX-SnippetCopyrightText: Copyright (c) 2019 Heltec Automation +%% SPDX-License-Identifier: MIT +heltec_lcmen2r13efc1_init() -> + program([ + reset(100, 100, 100), + wait_busy(1, 5000), + cmd(16#12), + wait_busy(1, 5000), + cmd(16#4D, <<16#55, 16#00, 16#00>>), + cmd(16#A9, <<16#25, 16#00, 16#00>>), + cmd(16#F3, <<16#0A, 16#00, 16#00>>), + cmd(16#44, <<16#01, 16#0F>>), + cmd(16#45, <<16#F9, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#01>>), + cmd(16#18, <<16#80>>), + cmd(16#4E, <<16#01>>), + cmd(16#4F, <<16#F9, 16#00>>), + wait_busy(1, 5000) + ]). + +heltec_lcmen2r13efc1_refresh() -> + program([ + cmd(16#13), + insert_plane(0), + cmd(16#12), + cmd(16#04), + wait_busy(1, 5000), + delay_ms(10), + cmd(16#12), + delay_ms(10), + wait_busy(1, 5000), + cmd(16#02), + wait_busy(1, 5000) + ]). +%% SPDX-SnippetEnd + +acep7c_init_seq() -> + init_seq([ + init_cmd(16#00, <<16#EF, 16#08>>), + init_cmd(16#01, <<16#37, 16#00, 16#23, 16#23>>), + init_cmd(16#03, <<16#00>>), + init_cmd(16#06, <<16#C7, 16#C7, 16#1D>>), + init_cmd(16#30, <<16#3C>>), + init_cmd(16#40, <<16#00>>), + init_cmd(16#50, <<16#3F>>), + init_cmd(16#60, <<16#22>>), + init_cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>), + init_cmd(16#E3, <<16#AA>>), + init_cmd_delay(16#82, <<16#80>>, 100), + init_cmd(16#50, <<16#37>>) + ]). + +acep7c_frame_preamble_seq() -> + init_seq([ + init_cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>) + ]). + +gdep073e01_init_seq() -> + init_seq([ + init_cmd(16#AA, <<16#49, 16#55, 16#20, 16#08, 16#09, 16#18>>), + init_cmd(16#01, <<16#3F>>), + init_cmd(16#00, <<16#5F, 16#69>>), + init_cmd(16#03, <<16#00, 16#54, 16#00, 16#44>>), + init_cmd(16#05, <<16#40, 16#1F, 16#1F, 16#2C>>), + init_cmd(16#06, <<16#6F, 16#1F, 16#17, 16#49>>), + init_cmd(16#08, <<16#6F, 16#1F, 16#1F, 16#22>>), + init_cmd(16#30, <<16#00>>), + init_cmd(16#50, <<16#3F>>), + init_cmd(16#60, <<16#02, 16#00>>), + init_cmd(16#61, <<16#03, 16#20, 16#01, 16#E0>>), + init_cmd(16#84, <<16#01>>), + init_cmd(16#E3, <<16#2F>>), + init_cmd(16#04) + ]). + +epd2in9_v2_init(BorderWaveform) -> + epd2in9_v2_init(BorderWaveform, 0). + +epd2in9_v2_init(BorderWaveform, RamXOffset) -> + RamXEnd = RamXOffset + 15, + Base = [ + reset(10, 10, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + label_delay(0, 20), + cmd(16#01, <<16#27, 16#01, 16#00>>), + cmd(16#11, <<16#03>>), + cmd(16#44, <>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + cmd(16#4E, <>), + cmd(16#4F, <<16#00, 16#00>>), + cmd(16#21, <<16#00, 16#80>>) + ], + program(Base ++ border_waveform(BorderWaveform)). + +epd2in9_v2_refresh(LutSlot, UpdateMode) -> + program([ + insert_lut(LutSlot), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in9_v2_fast_refresh() -> + program([ + insert_lut(?LUT_FAST), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in9_v2_partial_refresh() -> + program([ + epd2in9_v2_partial_prelude(), + cmd(16#11, <<16#03>>), + cmd(16#44, <<16#00, 16#0F>>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#0F>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in9_v2_partial_prelude() -> + [ + reset(0, 1, 2), + insert_lut(?LUT_PARTIAL), + cmd(16#37, <<16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + cmd(16#22, <<16#C0>>), + cmd(16#20), + wait_busy(0, 5000) + ]. + +epd2in9_v2_4gray_refresh() -> + program([ + insert_lut(?LUT_4GRAY), + set_ram_cursor(), + cmd(16#24), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000) + ]). + +border_waveform(none) -> + []; +border_waveform(Waveform) -> + [cmd(16#3C, <>)]. + +set_ram_cursor() -> + set_ram_cursor(0). + +set_ram_cursor(RamXOffset) -> + [cmd(16#4E, <>), cmd(16#4F, <<16#00, 16#00>>)]. + +%% SPDX-SnippetBegin +%% SPDX-SnippetCopyrightText: Waveshare team +%% SPDX-License-Identifier: MIT +uc8151_2in9_full_lut() -> + << + 16#50, 16#AA, 16#55, 16#AA, 16#11, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#FF, 16#FF, 16#1F, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in9_partial_lut() -> + << + 16#10, 16#18, 16#18, 16#08, 16#18, 16#18, + 16#08, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#13, 16#14, 16#44, 16#12, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in13_full_lut() -> + << + 16#22, 16#55, 16#AA, 16#55, 16#AA, 16#55, 16#AA, 16#11, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in13_partial_lut() -> + << + 16#18, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0F, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8276_4gray_lut() -> + << + 16#01, 16#0A, 16#1B, 16#0F, 16#03, 16#01, 16#01, + 16#05, 16#0A, 16#01, 16#0A, 16#01, 16#01, 16#01, + 16#05, 16#08, 16#03, 16#02, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#0A, 16#1B, 16#0F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#03, 16#82, 16#84, 16#01, 16#01, + 16#01, 16#84, 16#84, 16#82, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#0A, 16#1B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#83, 16#82, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#8A, 16#1B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#83, 16#02, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#8A, 16#9B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#03, 16#42, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#42, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#02, 16#00, 16#00, 16#07, 16#17, 16#41, 16#A8, + 16#32, 16#30 + >>. + +epd2in9_v2_partial_lut() -> + << + 16#00, 16#40, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#80, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#40, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#80, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0A, 16#00, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#B0, 16#32, 16#36 + >>. + +epd2in9_v2_default_lut() -> + << + 16#80, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, + 16#10, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#20, 16#00, 16#00, 16#00, + 16#80, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, + 16#10, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#20, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#14, 16#08, 16#00, 16#00, 16#00, 16#00, 16#02, 16#0A, 16#0A, 16#00, 16#0A, 16#0A, + 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#14, 16#08, 16#00, 16#01, + 16#00, 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#01, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#44, 16#44, 16#44, 16#44, 16#44, 16#44, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#00, 16#32, 16#36 + >>. + +epd2in9_v2_4gray_lut() -> + << + 16#00, 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#20, 16#60, 16#10, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#28, 16#60, 16#14, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#2A, 16#60, 16#15, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#02, 16#00, 16#05, 16#14, 16#00, 16#00, 16#1E, 16#1E, 16#00, 16#00, 16#00, + 16#00, 16#01, 16#00, 16#02, 16#00, 16#05, 16#14, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#24, 16#22, 16#22, 16#22, 16#23, 16#32, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#AE, 16#32, 16#28 + >>. + +epd2in9_v2_fast_lut() -> + << + 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#19, 16#19, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#24, 16#42, 16#22, 16#22, 16#23, 16#32, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#AE, 16#32, 16#38 + >>. +%% SPDX-SnippetEnd diff --git a/atomgl_epaper/src/atomgl_epaper_program.erl b/atomgl_epaper/src/atomgl_epaper_program.erl new file mode 100644 index 0000000..496c033 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_program.erl @@ -0,0 +1,101 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_program). + +-export([ + program/1, + init_seq/1, + cmd/1, + cmd/2, + cmd_delay/3, + init_cmd/1, + init_cmd/2, + init_cmd_delay/3, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + label_delay/2, + delay_ms/1 +]). + +-define(DELAY, 16#80). +-define(META, 16#40). +-define(LEN_MASK, 16#3F). +-define(INIT_LEN_MASK, 16#7F). + +-define(OP_WAIT_BUSY, 16#01). +-define(OP_RESET, 16#02). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_LUT, 16#04). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). +-define(OP_MARK_PREV_VALID, 16#07). +-define(OP_LABEL, 16#10). + +program(Parts) -> + iolist_to_binary(Parts). + +init_seq(Parts) -> + iolist_to_binary(Parts). + +cmd(Cmd) -> + cmd(Cmd, <<>>). + +cmd(Cmd, Data) when is_list(Data) -> + cmd(Cmd, list_to_binary(Data)); +cmd(Cmd, Data) when byte_size(Data) =< ?LEN_MASK -> + <>. + +cmd_delay(Cmd, Data, DelayMs) when byte_size(Data) =< ?LEN_MASK -> + <>. + +init_cmd(Cmd) -> + init_cmd(Cmd, <<>>). + +init_cmd(Cmd, Data) when is_list(Data) -> + init_cmd(Cmd, list_to_binary(Data)); +init_cmd(Cmd, Data) when byte_size(Data) =< ?INIT_LEN_MASK -> + <>. + +init_cmd_delay(Cmd, Data, DelayMs) when is_list(Data) -> + init_cmd_delay(Cmd, list_to_binary(Data), DelayMs); +init_cmd_delay(Cmd, Data, DelayMs) when byte_size(Data) =< ?INIT_LEN_MASK -> + <>. + +wait_busy(Level, TimeoutMs) -> + meta(?OP_WAIT_BUSY, <>). + +reset(HighMs, LowMs, SettleMs) -> + meta(?OP_RESET, <>). + +insert_plane(PlaneId) -> + meta(?OP_INSERT_PLANE, <>). + +insert_lut(SlotId) -> + meta(?OP_INSERT_LUT, <>). + +insert_prev_frame(PlaneId) -> + meta(?OP_INSERT_PREV_FRAME, <>). + +capture_frame() -> + meta(?OP_CAPTURE_FRAME, <<>>). + +mark_prev_valid() -> + meta(?OP_MARK_PREV_VALID, <<>>). + +label_delay(LabelId, DelayMs) -> + meta_delay(?OP_LABEL, <>, DelayMs). + +delay_ms(DelayMs) -> + label_delay(0, DelayMs). + +meta(Opcode, Operands) when byte_size(Operands) =< ?LEN_MASK -> + <>. + +meta_delay(Opcode, Operands, DelayMs) when byte_size(Operands) =< ?LEN_MASK -> + <>. diff --git a/atomgl_epaper/test/atomgl_epaper_tests.erl b/atomgl_epaper/test/atomgl_epaper_tests.erl new file mode 100644 index 0000000..85c5c98 --- /dev/null +++ b/atomgl_epaper/test/atomgl_epaper_tests.erl @@ -0,0 +1,468 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-import(atomgl_epaper, [panel/1, panel/2, validate_descriptor/1]). + +-define(PROGRAM_DELAY, 16#80). +-define(PROGRAM_META, 16#40). +-define(PROGRAM_LEN_MASK, 16#3F). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). + +get_value(Key, List) -> + proplists:get_value(Key, List). + +orientation_geometry_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(90, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => landscape_left}), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(90, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => landscape_right}), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(270, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => portrait}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => portrait_flipped}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(180, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{rotation => 0}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", + #{rotation => 0, view_width => 128, view_height => 296}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) + end) + ]. + +invalid_rotation_descriptor_test() -> + Desc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 45}, + {frame_layout, row_msb}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<>>}, {full, <<>>}]} + ], + ?assertError({bad_rotation, 45}, validate_descriptor(Desc)). + +invalid_rotated_view_geometry_test() -> + Desc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 90}, + {frame_layout, row_msb}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<>>}, {full, <<>>}]} + ], + ?assertError({bad_field, view_width, bad_geometry}, validate_descriptor(Desc)). + +frame_size_limits_test_() -> + Config = #{rotation => 90, init => <<0, 0>>, full => <<0, 0>>}, + [ + ?_test(begin + {ok, Row} = atomgl_epaper_descriptor:ssd16xx_panel("row boundary", + Config#{native_width => 32768, native_height => 1048575}), + ?assertEqual(1048575, get_value(view_width, Row)), + {ok, Column} = atomgl_epaper_descriptor:jd79656_panel("column boundary", + Config#{native_width => 1048575, native_height => 32768}), + ?assertEqual(1048575, get_value(view_height, Column)) + end), + ?_test(begin + %% 3 bytes per row times 1431655765 rows is exactly UINT32_MAX. + {ok, _} = atomgl_epaper_descriptor:ssd16xx_panel("exact frame boundary", + Config#{native_width => 24, native_height => 1431655765}) + end), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("one row beyond frame boundary", + Config#{native_width => 24, native_height => 1431655766})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("row overflow", + Config#{native_width => 32768, native_height => 1048576})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:jd79656_panel("column overflow", + Config#{native_width => 1048576, native_height => 32768})), + %% Floor division would incorrectly accept these non-byte-aligned frames. + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("row rounding", + Config#{native_width => 32769, native_height => 1048575})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:jd79656_panel("column rounding", + Config#{native_width => 1048575, native_height => 32769})), + ?_test(begin + {ok, _} = atomgl_epaper_descriptor:ssd16xx_panel("dimension boundary", + Config#{native_width => 2147483640, native_height => 1}) + end), + ?_assertError({bad_field, native_width, {out_of_range, 1, 2147483640}}, + atomgl_epaper_descriptor:ssd16xx_panel("dimension rounding overflow", + Config#{native_width => 2147483641, native_height => 1})), + ?_assertError({bad_field, native_height, {out_of_range, 1, 2147483640}}, + atomgl_epaper_descriptor:jd79656_panel("dimension narrowing overflow", + Config#{native_width => 1, native_height => 4294967297})) + ]. + +invalid_refresh_modes_descriptor_test_() -> + BaseDesc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 0}, + {frame_layout, row_msb}, + {polarity, white_1}, + {spi_clock_hz, 4000000}, + {busy_idle_level, 0}, + {use_gpio_pullups, false}, + {palette_size, 2}, + {default_refresh, full}, + {programs, [{init, <<0, 0>>}, {full, <<0, 0>>}, {partial, <<0, 0>>}]}, + {timing, []}, + {ghosting, []} + ], + [ + ?_test(begin + ?assertError({bad_field, refresh_modes, duplicate}, + validate_descriptor([{refresh_modes, [full, partial, partial]} | BaseDesc])) + end), + ?_test(begin + ?assertError({bad_field, refresh_modes, missing_full}, + validate_descriptor([{refresh_modes, [partial]} | BaseDesc])) + end), + ?_test(begin + ?assertError({bad_field, refresh_modes, {expected_one_of, [full, fast, partial, '4gray']}}, + validate_descriptor([{refresh_modes, [full, turbo]} | BaseDesc])) + end) + ]. + +empty_required_program_test_() -> + BaseDesc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 0}, + {frame_layout, row_msb}, + {polarity, white_1}, + {spi_clock_hz, 4000000}, + {busy_idle_level, 0}, + {use_gpio_pullups, false}, + {palette_size, 2}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<0, 0>>}, {full, <<0, 0>>}]}, + {timing, []}, + {ghosting, []} + ], + [ + ?_assertError({bad_field, init, empty_program}, + validate_descriptor([{programs, [{init, <<>>}, {full, <<0, 0>>}]} | BaseDesc])), + ?_assertError({bad_field, full, empty_program}, + validate_descriptor([{programs, [{init, <<0, 0>>}, {full, <<>>}]} | BaseDesc])), + ?_assertError({bad_field, sleep, empty_program}, + validate_descriptor([{sleep_modes, [{sleep, [{enter, <<>>}]}]} | BaseDesc])), + ?_assertError({bad_field, sleep, empty_program}, + validate_descriptor([{sleep_modes, [{sleep, [ + {enter, <<0, 0>>}, {wake, <<>>} + ]}]} | BaseDesc])), + ?_assertError({bad_field, init_seq, empty_sequence}, + panel("waveshare,5in65-acep-7c", #{init_seq => <<>>})) + ]. + +invalid_previous_frame_flow_test_() -> + {ok, Descriptor} = panel("waveshare,epd2in9_V2"), + Programs = get_value(programs, Descriptor), + [ + ?_assertError({bad_field, full, mark_without_capture}, + validate_descriptor(replace_value(programs, + replace_value(full, <<16#07, 16#40>>, Programs), Descriptor))), + ?_assertError({bad_field, full, capture_without_plane}, + validate_descriptor(replace_value(programs, + replace_value(full, <<16#06, 16#40>>, Programs), Descriptor))) + ]. + +fast_previous_frame_order_test_() -> + [ + ?_test(assert_previous_frame_order("waveshare,epd2in9_V2")), + ?_test(assert_previous_frame_order("waveshare,epd2in13_V4")), + ?_test(assert_previous_frame_order("waveshare,epd4in2_V2")) + ]. + +restricted_refresh_modes_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", #{refresh_modes => [full]}), + ?assertEqual([full], get_value(refresh_modes, Desc)), + ?assertEqual(full, get_value(default_refresh, Desc)), + Programs = get_value(programs, Desc), + ?assert(is_binary(get_value('4gray', Programs))), + ?assertError({bad_field, fast, truncated_record}, + validate_descriptor(replace_value(programs, + replace_value(fast, <<0>>, Programs), Desc))), + ?assertError({bad_field, init, {bad_meta_opcode, 3, 1}}, + validate_descriptor(replace_value(programs, + replace_value(init, <<3, 16#41, 0>>, Programs), Desc))). + +heltec_busy_polarity_test() -> + {ok, Desc} = panel("heltec,lcmen2r13efc1"), + ?assertEqual(1, get_value(busy_idle_level, Desc)), + Standby = get_value(standby, get_value(sleep_modes, Desc)), + ?assertEqual(reset_init, get_value(wake, Standby)), + {ok, LowBusyDesc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(0, get_value(busy_idle_level, LowBusyDesc)). + +timing_options_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", + #{timing => #{timeout_ms => 1234, poll_interval_ms => 25}}), + Timing = get_value(timing, Desc), + ?assertEqual(1234, get_value(timeout_ms, Timing)), + ?assertEqual(25, get_value(poll_interval_ms, Timing)). + +integer_field_limits_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", #{timeout_ms => 70000}), + ?assertEqual(70000, get_value(timeout_ms, get_value(timing, Desc))), + ?assertError({bad_field, timeout_ms, {out_of_range, 1, 2147483647}}, + panel("waveshare,epd2in9_V2", #{timeout_ms => 2147483648})), + ?assertError({bad_field, max_fast_refreshes, {out_of_range, 0, 2147483647}}, + panel("waveshare,epd2in9_V2", #{max_fast_refreshes => 4294967296})). + +uc8151_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9"), + ?assertEqual(uc8151, get_value(controller, Desc)), + ?assertEqual(128, get_value(native_width, Desc)), + ?assertEqual(296, get_value(native_height, Desc)), + ?assertEqual([full, partial], get_value(refresh_modes, Desc)), + ?assertEqual(30, byte_size(get_value(lut_full, Desc))), + ?assertEqual(30, byte_size(get_value(lut_partial, Desc))) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in13"), + ?assertEqual(uc8151, get_value(controller, Desc)), + ?assertEqual(122, get_value(native_width, Desc)), + ?assertEqual(250, get_value(native_height, Desc)), + ?assertEqual(250, get_value(view_width, Desc)), + ?assertEqual(122, get_value(view_height, Desc)), + ?assertEqual(30, byte_size(get_value(lut_full, Desc))), + ?assertEqual(30, byte_size(get_value(lut_partial, Desc))) + end) + ]. + +uc8276_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2"), + ?assertEqual(uc8276, get_value(controller, Desc)), + ?assertEqual(400, get_value(native_width, Desc)), + ?assertEqual(300, get_value(native_height, Desc)), + ?assertEqual(300, get_value(view_width, Desc)), + ?assertEqual(400, get_value(view_height, Desc)), + ?assertEqual([full, fast, partial, '4gray'], get_value(refresh_modes, Desc)), + ?assertEqual(233, byte_size(get_value(lut_4gray, Desc))) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2-4gray"), + ?assertEqual(uc8276, get_value(controller, Desc)), + ?assertEqual('4gray', get_value(default_refresh, Desc)) + end) + ]. + +sleep_modes_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(3, get_value(descriptor_version, Desc)), + SleepModes = get_value(sleep_modes, Desc), + Sleep = get_value(sleep, SleepModes), + DeepSleep = get_value(deep_sleep, SleepModes), + ?assertEqual(<<16#10, 1, 16#01, 16#10, 16#C1, 0, 100>>, + get_value(enter, Sleep)), + ?assertEqual(retained, get_value(controller_ram, Sleep)), + ?assertEqual(preserve, get_value(host_prev_frame, Sleep)), + ?assertEqual(allow_if_program_reseeds, get_value(after_wake_refresh, Sleep)), + ?assertEqual(<<16#10, 1, 16#03, 16#10, 16#C1, 0, 100>>, + get_value(enter, DeepSleep)), + ?assertEqual(lost, get_value(controller_ram, DeepSleep)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2"), + SleepModes = get_value(sleep_modes, Desc), + Sleep = get_value(sleep, SleepModes), + ?assertEqual(<<16#10, 1, 16#01, 16#10, 16#C1, 0, 200>>, + get_value(enter, Sleep)) + end), + ?_test(begin + {ok, Desc} = panel("good-display/gdep073e01"), + SleepModes = get_value(sleep_modes, Desc), + DeepSleep = get_value(deep_sleep, SleepModes), + ?assertEqual( + <<16#02, 1, 16#00, 16#01, 16#43, 1, 136, 19, 16#07, 1, 16#A5>>, + get_value(enter, DeepSleep)), + ?assertEqual(reset_init, get_value(wake, DeepSleep)), + ?assertEqual(full, get_value(after_wake_refresh, DeepSleep)) + end), + ?_test(begin + CustomModes = [ + {lab, [ + {enter, <<1, 2, 2, 3>>}, + {wake, init}, + {controller_ram, unknown}, + {host_prev_frame, invalidate}, + {after_wake_refresh, full} + ]} + ], + {ok, Desc} = panel("waveshare,epd2in9_V2", #{sleep_modes => CustomModes}), + ?assertEqual(CustomModes, get_value(sleep_modes, Desc)) + end), + ?_test(begin + DuplicateModes = [ + {sleep, [{enter, <<1>>}]}, + {sleep, [{enter, <<2>>}]} + ], + ?assertError({bad_field, sleep_modes, duplicate}, + panel("waveshare,epd2in9_V2", #{sleep_modes => DuplicateModes})) + end), + ?_test(begin + TooManyModes = [ + {mode1, [{enter, <<1>>}]}, + {mode2, [{enter, <<2>>}]}, + {mode3, [{enter, <<3>>}]}, + {mode4, [{enter, <<4>>}]}, + {mode5, [{enter, <<5>>}]}, + {mode6, [{enter, <<6>>}]}, + {mode7, [{enter, <<7>>}]}, + {mode8, [{enter, <<8>>}]}, + {mode9, [{enter, <<9>>}]} + ], + ?assertError({bad_field, sleep_modes, too_many}, + panel("waveshare,epd2in9_V2", #{sleep_modes => TooManyModes})) + end) + ]. + +acep7_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,5in65-acep-7c"), + ?assertEqual(acep7, get_value(controller, Desc)), + ?assertEqual(600, get_value(native_width, Desc)), + ?assertEqual(448, get_value(native_height, Desc)), + ?assertEqual(acep7, get_value(palette, Desc)), + ?assertEqual(46, byte_size(get_value(init_seq, Desc))), + ?assertEqual(6, byte_size(get_value(frame_preamble_seq, Desc))), + ?assertEqual(false, get_value(refresh_has_data, Desc)), + ?assertEqual(5, get_value(periodic_refresh_interval, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("good-display/gdep073e01"), + ?assertEqual(acep7, get_value(controller, Desc)), + ?assertEqual(800, get_value(native_width, Desc)), + ?assertEqual(480, get_value(native_height, Desc)), + ?assertEqual(gdep073e01, get_value(palette, Desc)), + ?assertEqual(63, byte_size(get_value(init_seq, Desc))), + ?assertEqual(true, get_value(init_wait_busy_between_cmds, Desc)), + ?assertEqual(true, get_value(refresh_has_data, Desc)), + ?assertEqual(1, get_value(post_power_off_busy_level, Desc)) + end), + ?_test(begin + ?assertError({bad_field, rotation, acep7_native_orientation_only}, + panel("waveshare,5in65-acep-7c", + #{rotation => 90, view_width => 448, view_height => 600})) + end), + ?_test(begin + ?assertError({bad_field, init_seq, truncated_record}, + panel("waveshare,5in65-acep-7c", #{init_seq => <<0>>})) + end), + ?_test(begin + ?assertError({bad_field, native_width, acep7_width_must_be_even}, + panel("waveshare,5in65-acep-7c", #{native_width => 601})) + end) + ]. + +unknown_panel_test() -> + ?assertEqual({error, {unsupported_panel, "missing,panel"}}, + panel("missing,panel")). + +assert_previous_frame_order(Compatible) -> + {ok, Descriptor} = panel(Compatible), + Programs = get_value(programs, Descriptor), + FastOps = program_meta_opcodes(get_value(fast, Programs)), + ?assert(opcode_index(?OP_INSERT_PREV_FRAME, FastOps) + < opcode_index(?OP_CAPTURE_FRAME, FastOps)), + ?assert(opcode_index(?OP_CAPTURE_FRAME, FastOps) + < opcode_index(?OP_INSERT_PLANE, FastOps)), + FullOps = program_meta_opcodes(get_value(full, Programs)), + ?assert(opcode_index(?OP_CAPTURE_FRAME, FullOps) + < opcode_index(?OP_INSERT_PLANE, FullOps)), + ?assert(opcode_index(?OP_INSERT_PLANE, FullOps) + < opcode_index(?OP_INSERT_PREV_FRAME, FullOps)). + +program_meta_opcodes(<<>>) -> + []; +program_meta_opcodes(<>) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + <<_Data:Len/binary, Tail0/binary>> = Rest, + Tail = case HasDelay of + true -> + <<_Delay, Next/binary>> = Tail0, + Next; + false -> + Tail0 + end, + case IsMeta of + true -> [Opcode | program_meta_opcodes(Tail)]; + false -> program_meta_opcodes(Tail) + end. + +opcode_index(Opcode, Opcodes) -> + opcode_index(Opcode, Opcodes, 1). + +opcode_index(Opcode, [Opcode | _], Index) -> + Index; +opcode_index(Opcode, [_ | Rest], Index) -> + opcode_index(Opcode, Rest, Index + 1). + +replace_value(Key, Value, Proplist) -> + [{Key, Value} | proplists:delete(Key, Proplist)]. diff --git a/display_driver.c b/display_driver.c index dce82db..3e64d33 100644 --- a/display_driver.c +++ b/display_driver.c @@ -37,6 +37,12 @@ Context *oled_display_create_port(GlobalContext *global, term opts); Context *display_create_port(GlobalContext *global, term opts) { term compat_atom = globalcontext_make_atom(global, ATOM_STR("\xA", "compatible")); + term descriptor_atom = globalcontext_make_atom(global, ATOM_STR("\xA", "descriptor")); + + term descriptor_term = interop_proplist_get_value(opts, descriptor_atom); + if (descriptor_term != term_nil()) { + return epaper_display_create_port(global, opts); + } term compat_value_term = interop_proplist_get_value(opts, compat_atom); if (compat_value_term == term_nil()) { @@ -51,10 +57,7 @@ Context *display_create_port(GlobalContext *global, term opts) } Context *ctx = NULL; - if (!strcmp(compat_string, "waveshare,5in65-acep-7c") - || !strcmp(compat_string, "good-display/gdep073e01")) { - ctx = epaper_display_create_port(global, opts); - } else if (!strcmp(compat_string, "sharp,memory-lcd")) { + if (!strcmp(compat_string, "sharp,memory-lcd")) { ctx = memory_lcd_display_create_port(global, opts); } else if (!strcmp(compat_string, "ilitek,ili9341") || !strcmp(compat_string, "ilitek,ili9342c") diff --git a/docs/display-drivers.md b/docs/display-drivers.md index f48c3cb..8305c1a 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -11,7 +11,7 @@ This document describes how to configure and use the various display drivers sup To use a display with AtomGL, you need: 1. A communication interface (either SPI or I²C) that must be opened and configured -2. A display driver selected by providing a `compatible` string that matches your display model +2. A display driver selected by providing either a `compatible` string or an E-Paper `descriptor` The display driver will handle all the low-level communication and rendering, while you provide display lists describing what should be shown. @@ -272,17 +272,15 @@ sharp_lcd_opts = [ ] ``` -### Waveshare 5.65" ACeP 7-Color (waveshare,5in65-acep-7c) +### ACeP 7-Color E-Paper -600×480 7-color E-Paper display. Driver has software dithering support. - -**Compatible string:** `"waveshare,5in65-acep-7c"` +ACeP 7-color E-Paper displays use Erlang descriptors generated by +`atomgl_epaper`. Driver support includes software dithering. | Option | Type | Description | Default | |--------|------|-------------|---------| | `spi_host` | term | SPI host reference | Required | -| `width` | integer | Display width in pixels | 600 | -| `height` | integer | Display height in pixels | 480 | +| `descriptor` | keyword list | Descriptor returned by `atomgl_epaper` | Required | | `cs` | integer | Chip select GPIO pin | Required | | `dc` | integer | Data/Command GPIO pin | Required | | `reset` | integer | Reset GPIO pin | Required | @@ -291,17 +289,249 @@ sharp_lcd_opts = [ **Note:** E-Paper displays have slow refresh rates due to their technology. **Example:** +```elixir +{:ok, descriptor} = :atomgl_epaper.panel("waveshare,5in65-acep-7c") + +epaper_opts = [ + spi_host: spi_host, + descriptor: descriptor, + cs: 22, + dc: 21, + reset: 18, + busy: 19 +] +``` + +### E-Paper Display API + +AtomGL provides a data-driven descriptor framework to drive monochrome, 4-gray, and ACeP 7-color E-Paper displays. Panel configurations are represented as bytecode descriptors, meaning the same driver binary can adapt to different controllers, native geometry vs. user-facing orientation, byte/bit layouts, and waveform program payloads. + +#### Panel Constructors + +To obtain a panel descriptor, call `atomgl_epaper:panel/1` or `atomgl_epaper:panel/2`. + +```elixir +# Default panel configuration (landscape) +{:ok, descriptor} = :atomgl_epaper.panel("waveshare,epd2in9_V2") + +# Custom panel configuration with options +{:ok, descriptor_portrait} = :atomgl_epaper.panel("waveshare,epd2in9_V2", %{ + orientation: :portrait, + default_refresh: :partial +}) +``` + +Supported panel lookup strings: +- **UC8151 2.9" / 2.13"**: `"waveshare,epaper-2in9"`, `"waveshare,epd2in9"`, `"waveshare,epd2in13"` +- **UC8276 4.2"**: `"waveshare,epd4in2_V2"`, `"waveshare,epd4in2_V2-fast"`, `"waveshare,epd4in2_V2-partial"`, `"waveshare,epd4in2_V2-4gray"` +- **SSD1680 2.9"**: `"waveshare,epd2in9_V2"`, `"waveshare,epd2in9_V2-fast"`, `"waveshare,epd2in9_V2-partial"`, `"waveshare,epd2in9_V2-4gray"`, `"dke,depg0290bns800f6"` +- **SSD1680 2.13"**: `"waveshare,epaper-2in13"`, `"waveshare,epd2in13_V4"`, `"waveshare,epd2in13_V4-fast"`, `"waveshare,epd2in13_V4-partial"` +- **JD79656 2.13"**: `"heltec,lcmen2r13efc1"`, `"heltec,icmen2r13efc1"`, `"heltec,ht-vme213"` +- **ACeP 7-color**: `"waveshare,5in65-acep-7c"`, `"good-display/gdep073e01"` + +##### Constructor Options + +When calling `panel/2`, you can customize: +- `orientation` - `:landscape`, `:landscape_left`, `:landscape_right`, `:portrait`, or `:portrait_flipped` (automatically maps native coordinates to view width/height and sets rotation). `:landscape` is a compatibility alias for `:landscape_left`. +- `default_refresh` - The refresh mode used when no frame option is specified. +- `refresh_modes` - List of allowed refresh modes (e.g., `[:full, :fast, :partial]`). +- `sleep_modes` - Override the panel's low-power mode descriptors for experiments or panel-specific tuning. +- `ghosting` - Map with ghosting settings: + - `max_fast_refreshes` - Maximum successive fast updates before a full refresh is forced. + - `reseed_on_timeout` - Reseed previous/current planes with full refresh after BUSY timeouts. +- `timing` - Map to adjust driver timeout parameters (`full_expected_ms`, `fast_expected_ms`, `poll_interval_ms`, `timeout_ms`). + +ACeP 7-color panel constructors currently support native orientation only. + +#### E-Paper Descriptor Schema + +A completed descriptor is a keyword list with the following shape: + +```elixir +[ + descriptor_version: 3, + name: "Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", + controller: :ssd16xx, # :ssd16xx, :jd79656, :uc8151, :uc8276, :uc8175, or :acep7 + native_width: 128, + native_height: 296, + view_width: 296, + view_height: 128, + rotation: 90, + spi_clock_hz: 4000000, + busy_idle_level: 0, + use_gpio_pullups: false, + frame_layout: :row_msb, # :row_msb, :row_lsb, :column_msb, :column_lsb + polarity: :white_1, # :white_1 or :black_1 + refresh_modes: [:full, :fast, :partial, :4gray], + default_refresh: :full, + palette: :acep7, # ACeP color descriptors use :acep7, :acep7c, or :gdep073e01 + palette_size: 7, + programs: [ + init: <<...>>, # Init bytecode + full: <<...>>, # Full refresh waveform/cmd sequence + fast: <<...>>, # Fast refresh sequence (optional) + partial: <<...>>, # Partial refresh sequence (optional) + "4gray": <<...>> # 4-gray refresh sequence (optional) + ], + sleep_modes: [ + sleep: [ + enter: <<...>>, # Bytecode program to enter the mode + wake: :reset_init, # :init, :reset_init, or custom bytecode + controller_ram: :retained, # :retained, :lost, or :unknown + host_prev_frame: :preserve, # :preserve or :invalidate + after_wake_refresh: :allow_if_program_reseeds + ], + deep_sleep: [ + enter: <<...>>, + wake: :reset_init, + controller_ram: :lost, + host_prev_frame: :preserve, + after_wake_refresh: :allow_if_program_reseeds + ] + ], + init_seq: <<...>>, # ACeP raw init sequence + init_wait_busy_between_cmds: false, # ACeP color init behavior + frame_preamble_seq: <<...>>, # ACeP color per-frame preamble (optional) + refresh_has_data: false, # ACeP color refresh command behavior + refresh_data_byte: 0, + post_power_off_busy_level: 0, + periodic_refresh_interval: 0, + timing: [ + full_expected_ms: 2000, + fast_expected_ms: 500, + poll_interval_ms: 50, + timeout_ms: 5000 + ], + ghosting: [ + max_fast_refreshes: 10, + reseed_on_timeout: true + ], + lut_full: <<...>>, # Waveform LUT (optional) + lut_partial: <<...>>, # Waveform LUT (optional) + lut_4gray: <<...>>, # Waveform LUT (optional) + lut_fast: <<...>> # Waveform LUT (optional) +] +``` + +Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP raw sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. Both styles can expose `sleep_modes`. Descriptor nested fields are keyword lists/proplists; `panel/2` constructor options are maps. + +Use `atomgl_epaper_program:program/1` with `cmd`, `wait_busy`, `reset`, and render op helpers for interpreted descriptor bytecode. Use `init_seq/1`, `init_cmd/1,2`, and `init_cmd_delay/3` only for ACeP raw init and frame preamble sequences; raw sequences do not support bytecode meta operations. + +#### Ghosting Policy + +E-Paper controllers are highly susceptible to image burn-in and visual ghosting. AtomGL implements a conservative driver-level ghosting policy: +1. **Reseeding**: When the previous frame state is unknown or invalid, a partial/fast update is promoted to a full refresh to seed the controller's current/previous memory. +2. **Promotion Count**: After `max_fast_refreshes` successive fast/partial updates, the next update is automatically promoted to a full refresh to clean the screen. +3. **BUSY Timeout**: If the controller fails to finish in time (timing out on wait), the driver invalidates the previous frame and schedules a full refresh on the next update. + +#### Opening the Port + +Configure the SPI display port with the descriptor returned by the constructor: + ```elixir epaper_opts = [ spi_host: spi_host, - compatible: "waveshare,5in65-acep-7c", + descriptor: descriptor, cs: 22, dc: 21, reset: 18, busy: 19 ] + +display = :erlang.open_port({:spawn, "display"}, epaper_opts) ``` +#### Panel Power Switch + +Boards that switch the panel's supply (Heltec's Vext rail, Waveshare HAT power +pins and similar) can hand the controlling GPIO to the port: + +| Option | Type | Description | Default | +|--------|------|-------------|---------| +| `panel_power` | integer | GPIO that switches the panel supply | Optional; without it the rail is assumed to be always on | +| `panel_power_active` | atom | `:high` or `:low` active level | `:high` | +| `panel_power_delay_ms` | integer | Settle time after the rail comes up, before reset/init is sent | 100 | + +The driver raises the rail before it resets or initializes the controller, and +again before any wake sequence, then drops it when a sleep mode declares +`controller_ram: :lost` — the descriptor has already given up on the +controller's RAM in that mode, so `{:sleep, :deep_sleep}` becomes the lowest +power state the board can reach. Modes that declare `:retained` or `:unknown` +keep the rail up, so the switch can never contradict a descriptor that promises +retained state. + +```elixir +# Heltec Vision Master E290: the panel is fed from the Vext rail (GPIO 18). +epaper_opts = [ + spi_host: spi_host, + descriptor: descriptor, + cs: 3, + dc: 4, + reset: 5, + busy: 6, + panel_power: 18 +] +``` + +#### Runtime Refresh Options + +When sending updates via the port, you can dynamically choose the refresh mode per frame (rather than being locked to one mode at port creation time): + +```elixir +# Standard/Default update +:port.call(display, {:update, items}, 5000) + +# Request a fast refresh mode for this frame +:port.call(display, {:update, items, [refresh: :fast]}, 5000) +``` + +If the requested refresh mode is not listed in the descriptor's `refresh_modes`, the driver defaults back to `default_refresh`. Only modes defined and present in the descriptor are permitted. + +For slow displays, use `wait_idle` after an asynchronous `update` when the caller needs a queue barrier. It returns after earlier retained display messages have been processed, with `:ok` when the most recent executed update completed successfully and `:error` if the driver detected a failure. If the display queue overflows, older pre-acked updates may be dropped before execution, so `wait_idle` reports the last update that actually ran. + +```elixir +:port.call(display, {:update, items, [refresh: :partial]}, 5000) +:port.call(display, {:wait_idle}, 30_000) +``` + +The e-paper port also exposes descriptor capabilities at runtime via `{:info}` or the `{:capabilities}` alias: + +```elixir +:port.call(display, {:info}, 5000) +# [ +# controller: :ssd16xx, +# width: 296, +# height: 128, +# native_width: 128, +# native_height: 296, +# rotation: 90, +# palette_size: 4, +# refresh_modes: [:full, :fast, :partial, :"4gray"], +# default_refresh: :full, +# sleep_modes: [:sleep, :deep_sleep], +# asleep: false +# ] +``` + +#### Low Power Sleep + +Descriptors can expose up to 8 unique named low-power modes under `sleep_modes`. The built-in names are: + +- `:standby` - controller power-off/standby when the panel supports it. +- `:sleep` - controller sleep mode intended to retain more internal state. +- `:deep_sleep` - lowest-power controller mode; wake generally requires hardware reset. + +Use the mode explicitly: + +```elixir +:port.call(display, {:sleep, :standby}, 5000) +:port.call(display, {:sleep, :sleep}, 5000) +:port.call(display, {:sleep, :deep_sleep}, 5000) +:port.call(display, {:wake}, 5000) +``` + +If an update arrives while the controller is asleep, the driver wakes it first. `host_prev_frame` controls whether AtomGL keeps its own previous-frame buffer; `after_wake_refresh` controls whether a fast/partial update is allowed, forced to full, or allowed only when the selected bytecode program writes the saved previous frame back to controller RAM. + ## Custom Initialization Sequences Many displays require specific initialization sequences with carefully tuned values for voltages, diff --git a/docs/epaper-low-power-specs.md b/docs/epaper-low-power-specs.md new file mode 100644 index 0000000..4629574 --- /dev/null +++ b/docs/epaper-low-power-specs.md @@ -0,0 +1,80 @@ + + + +# E-paper Low Power Spec Notes + +Internal working notes for mapping panel data sheets and vendor drivers onto +AtomGL `standby`, `sleep`, `deep_sleep`, and `wake` behavior. + +## Summary Table + +| AtomGL panel key | Panel / controller | Source | Power-off / standby | Sleep | Deep sleep | Wake requirement | Partial after wake | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `waveshare,epd2in9_V2` | 2.9 inch SSD1680 | SSD1680 command table / Waveshare command use | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `dke,depg0290bns800f6` | 2.9 inch SSD1680 | DEPG0290BNS800F6 controller sheet | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | No differential mode in AtomGL descriptor | +| `waveshare,epd2in13_V4` | 2.13 inch SSD1680-style | Waveshare command use | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `waveshare,epd2in9`, `waveshare,epd2in13` | UC8151-era mono panels | Existing AtomGL command table | Not exposed separately | `0x10,0x01` | Not exposed by default | Reset + init | Partial promoted unless the selected program reseeds previous RAM | +| `waveshare,epd4in2_V2` | UC8276 4.2 inch | Existing AtomGL command table | Not exposed separately | `0x10,0x01` | Not exposed by default | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `heltec,lcmen2r13efc1` | JD79656 2.13 inch | Existing AtomGL command table | `0x02`, wait BUSY high | Not exposed by default | Not exposed by default | Reset + init | No differential mode in AtomGL descriptor | +| `waveshare,5in65-acep-7c` | 5.65 inch ACeP 7-color | Waveshare command use / existing AtomGL refresh flow | `0x02`, wait BUSY low | Not exposed by default | `0x02`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | +| `good-display/gdep073e01` | 7.3 inch Spectra 6, 800x480, GDEP073E01-style | `Waveshare_7.3inch-e-Paper-(E)-user-manual.pdf` | `0x02,0x00`; BUSY low while active, high when idle | Not documented | `0x02,0x00`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | + +## Board power rails + +The table above describes what each controller can do on its own. Boards that +also switch the panel's supply (Heltec's Vext rail, Waveshare HAT power pins) +report that GPIO to the port as `panel_power`, with `panel_power_active` +(`:high`/`:low`) and `panel_power_delay_ms` (settle time, 100 ms default). + +- The rail is raised before reset/init and before every wake sequence, so the + controller is always powered before it is addressed. +- The rail is dropped when a sleep mode declares `controller_ram: :lost`, since + the descriptor already gives up the controller RAM in that mode. Combined, + `deep_sleep` + `panel_power` is the lowest power state the board offers. +- `:retained` and `:unknown` modes keep the rail up, so the switch never + contradicts a descriptor that promises retained state. +- Ports without `panel_power` behave exactly as before (rail assumed on). + +## `good-display/gdep073e01` + +Local spec sheet: + +- `/Users/petermm/Downloads/Waveshare_7.3inch-e-Paper-(E)-user-manual.pdf` + +Extracted from the command table: + +| Function | Command | Data | Notes | +| --- | --- | --- | --- | +| Power OFF | `0x02` | `0x00` | Driver enters power-off/standby state. | +| Power ON | `0x04` | none | Used before refresh. | +| Deep Sleep | `0x07` | `0xA5` | Check code; exiting deep sleep requires HWRESET. | +| Data Start Transmission | `0x10` | frame bytes | Writes display RAM. | +| Data Refresh | `0x12` | `0x01` in spec | Local Waveshare sample code sends `0x00`; AtomGL currently follows sample code. | + +Power/current table notes: + +- The sheet includes rows for module standby current and module deep sleep current, + but the extracted table does not provide numeric values. +- The sheet advertises low-current deep sleep and on-chip display RAM, but does not + describe a separate RAM-retaining sleep mode analogous to SSD1680 `0x10,0x01`. + +Cross-checks: + +- Waveshare Arduino/STM32/C drivers for `EPD_7IN3E_Sleep()` send: + `0x02, 0x00`, wait busy, then `0x07, 0xA5`. +- Waveshare Python `epd7in3e.py` sleep sends only `0x07, 0xA5`. +- `bb_epaper` sends data refresh as `0x12, 0x00` for UC81xx 4-gray, + 4-color, and 7-color panels, including Spectra 6. +- `bb_epaper` currently treats `BBEP_7COLOR` sleep as power-off only and does not + send the `0x07,0xA5` deep-sleep command in its 7-color path. +- AtomGL currently does power-off after each ACEP7/Spectra 6 refresh: + `0x04`, `0x12`, optional refresh data, `0x02`. + +Implementation implication: + +- For this panel family, `standby` means the already-supported power-off state + (`0x02,0x00`) when exposed as an explicit command. +- `deep_sleep` should mean `0x02,0x00`, wait for idle, then `0x07,0xA5`. +- `wake` from deep sleep must use hardware reset and full init. +- There is no basis in this sheet for preserving controller RAM across + `deep_sleep`. diff --git a/epaper_color.c b/epaper_color.c index ddd27b7..e6f5667 100644 --- a/epaper_color.c +++ b/epaper_color.c @@ -20,6 +20,7 @@ #include "epaper_color.h" +#include #include #include #include @@ -44,27 +45,34 @@ const uint8_t epaper_gdep073e01_palette[7][3] = { { 0x12, 0x5F, 0x20 } }; +// App-facing SSD1680 4-gray palette. The plane writer maps these palette +// indices onto the SSD1680/Waveshare two-plane gray classes. +const uint8_t epaper_ssd1680_4gray_palette[4][3] = { + { 0xFF, 0xFF, 0xFF }, + { 0xAA, 0xAA, 0xAA }, + { 0x55, 0x55, 0x55 }, + { 0x00, 0x00, 0x00 } +}; + static inline float square(float p) { return p * p; } -uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, - const uint8_t palette[][3], int palette_size) +static int clamp_u8(int v) { - const uint8_t m[4][4] = { - { 0, 8, 2, 10 }, - { 12, 4, 14, 6 }, - { 3, 11, 1, 9 }, - { 15, 7, 13, 5 } - }; - - // following r parameters have been found using standard deviation - // that gives a decent result - int r1 = r + roundf(92.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); - int g1 = g + roundf(85.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); - int b1 = b + roundf(65.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + if (v < 0) { + return 0; + } + if (v > 255) { + return 255; + } + return v; +} +static uint8_t nearest_palette_index(int r, int g, int b, + const uint8_t palette[][3], int palette_size) +{ float min = INT_MAX; int min_index = 0; @@ -74,9 +82,9 @@ uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, int b2 = palette[i][2]; #ifdef NO_WEIGHTS - float d = square((r2 - r1)) + square((g2 - g1)) + square((b2 - b1)); + float d = square((r2 - r)) + square((g2 - g)) + square((b2 - b)); #else - float d = square((r2 - r1) * 0.30) + square((g2 - g1) * 0.59) + square((b2 - b1) * 0.11); + float d = square((r2 - r) * 0.30) + square((g2 - g) * 0.59) + square((b2 - b) * 0.11); #endif if (d < min) { @@ -87,3 +95,60 @@ uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, return min_index; } + +static bool exact_palette_index(uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size, uint8_t *out) +{ + for (int i = 0; i < palette_size; i++) { + if (palette[i][0] == r && palette[i][1] == g && palette[i][2] == b) { + *out = i; + return true; + } + } + return false; +} + +uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size) +{ + uint8_t exact_index; + if (exact_palette_index(r, g, b, palette, palette_size, &exact_index)) { + return exact_index; + } + + const uint8_t m[4][4] = { + { 0, 8, 2, 10 }, + { 12, 4, 14, 6 }, + { 3, 11, 1, 9 }, + { 15, 7, 13, 5 } + }; + + // following r parameters have been found using standard deviation + // that gives a decent result + int r1 = r + roundf(92.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + int g1 = g + roundf(85.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + int b1 = b + roundf(65.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + + return nearest_palette_index(r1, g1, b1, palette, palette_size); +} + +uint8_t epaper_dither_4gray(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size) +{ + uint8_t exact_index; + if (exact_palette_index(r, g, b, palette, palette_size, &exact_index)) { + return exact_index; + } + + const uint8_t m[4][4] = { + { 0, 8, 2, 10 }, + { 12, 4, 14, 6 }, + { 3, 11, 1, 9 }, + { 15, 7, 13, 5 } + }; + + int yval = ((r << 1) + r + (g << 2) + b) >> 3; + yval = clamp_u8(yval + ((2 * m[x % 4][y % 4]) - 15) * 3); + + return 3 - ((yval * 3 + 127) / 255); +} diff --git a/epaper_color.h b/epaper_color.h index 57c24a9..a528bcd 100644 --- a/epaper_color.h +++ b/epaper_color.h @@ -25,8 +25,12 @@ extern const uint8_t epaper_acep_palette[7][3]; extern const uint8_t epaper_gdep073e01_palette[7][3]; +extern const uint8_t epaper_ssd1680_4gray_palette[4][3]; uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, const uint8_t palette[][3], int palette_size); +uint8_t epaper_dither_4gray(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size); + #endif diff --git a/epaper_commands.c b/epaper_commands.c index 9a67aa2..469a765 100644 --- a/epaper_commands.c +++ b/epaper_commands.c @@ -25,8 +25,6 @@ #include -#include "epaper_color.h" - static void wait_busy_high(int busy_gpio) { while (gpio_get_level(busy_gpio) != 1) { @@ -34,141 +32,71 @@ static void wait_busy_high(int busy_gpio) } } -void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, - const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds) +static TickType_t delay_ms_to_ticks(uint32_t delay_ms) { - const uint8_t *end = seq + seq_len; - while (seq < end) { - uint8_t cmd = *seq++; - uint8_t flags_len = *seq++; - uint8_t len = flags_len & 0x7F; + return (delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS; +} - spi_dc_write_cmd_data(bus, cmd, seq, len); - seq += len; +bool epaper_validate_init_seq(const uint8_t *seq, size_t seq_len) +{ + if (seq == NULL) { + return seq_len == 0; + } + if (seq_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } - if (flags_len & EPAPER_INIT_SEQ_DELAY) { - vTaskDelay(*seq++ / portTICK_PERIOD_MS); + size_t pos = 0; + while (pos < seq_len) { + if (seq_len - pos < 2) { + return false; } + pos++; + uint8_t flags_len = seq[pos++]; + uint8_t len = flags_len & 0x7F; - if (wait_busy_between_cmds) { - wait_busy_high(busy_gpio); + if (seq_len - pos < len) { + return false; + } + pos += len; + + if (flags_len & EPAPER_INIT_SEQ_DELAY) { + if (seq_len - pos < 1) { + return false; + } + pos++; } } + return true; } -// --- Built-in init sequences --- -// -// Transcribed mechanically from the original display_spi_init() -// bodies in 5in65_acep_7c_display_driver.c and -// gdep073e01_display_driver.c. _Static_assert on sizeof guards -// against miscounted data bytes. - -// clang-format off - -// Waveshare 5.65" ACeP 7-color. No BUSY polling between commands; a -// single 100 ms delay between VDCS (0x82) and the second CDI (0x50) -// replicates the vTaskDelay(10) at tick rate 100 Hz in the original -// driver. -const uint8_t epaper_init_seq_acep7c[] = { - 0x00, 2, 0xEF, 0x08, // PSR - 0x01, 4, 0x37, 0x00, 0x23, 0x23, // PWRR - 0x03, 1, 0x00, // POFS - 0x06, 3, 0xC7, 0xC7, 0x1D, // BTST - 0x30, 1, 0x3C, // PLL - 0x40, 1, 0x00, // TSE - 0x50, 1, 0x3F, // CDI - 0x60, 1, 0x22, // TCON - 0x61, 4, 0x02, 0x58, 0x01, 0xC0, // TRES (600x448) - 0xE3, 1, 0xAA, // PWS - 0x82, EPAPER_INIT_SEQ_DELAY | 1, 0x80, 100, // VDCS + 100 ms - 0x50, 1, 0x37, // CDI (second) -}; -_Static_assert(sizeof(epaper_init_seq_acep7c) == 46, - "epaper_init_seq_acep7c: miscounted bytes"); -const size_t epaper_init_seq_acep7c_len = sizeof(epaper_init_seq_acep7c); - -// Good Display GDEP073E01 7.3" 7-color. BUSY polling interleaved -// after each command by the executor (wait_busy_between_cmds = true); -// no per-command delay bytes are required. The leading 0xAA entry is -// an undocumented vendor preamble preserved verbatim. -const uint8_t epaper_init_seq_gdep073e01[] = { - 0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, // vendor preamble - 0x01, 1, 0x3F, // PWRR - 0x00, 2, 0x5F, 0x69, // PSR - 0x03, 4, 0x00, 0x54, 0x00, 0x44, // POFS - 0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, // BTST1 - 0x06, 4, 0x6F, 0x1F, 0x17, 0x49, // BTST2 - 0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, // BTST3 - 0x30, 1, 0x00, // PLL - 0x50, 1, 0x3F, // CDI - 0x60, 2, 0x02, 0x00, // TCON - 0x61, 4, 0x03, 0x20, 0x01, 0xE0, // TRES (800x480) - 0x84, 1, 0x01, // T_VDCS - 0xE3, 1, 0x2F, // PWS - 0x04, 0, // PON -}; -_Static_assert(sizeof(epaper_init_seq_gdep073e01) == 63, - "epaper_init_seq_gdep073e01: miscounted bytes"); -const size_t epaper_init_seq_gdep073e01_len = sizeof(epaper_init_seq_gdep073e01); - -// --- Per-frame preambles --- -// -// ACeP 5.65" retransmits the resolution command (0x61 + 600x448) -// before every DTM; GoodDisplay's GDEP073E01 does not. The preamble -// uses the same byte-array format as init_seq. - -static const uint8_t epaper_preamble_acep7c[] = { - 0x61, 4, 0x02, 0x58, 0x01, 0xC0, // TRES (600x448) -}; -_Static_assert(sizeof(epaper_preamble_acep7c) == 6, - "epaper_preamble_acep7c: miscounted bytes"); - -// --- Per-panel descriptors --- - -const struct EPaperDesc epaper_desc_acep7c = { - .name = "Waveshare 5.65\" ACeP 7-color", - .native_width = 600, - .native_height = 448, - .spi_clock_hz = 1000000, - - .palette = epaper_acep_palette, - .palette_size = 7, - - .init_seq = epaper_init_seq_acep7c, - .init_seq_len = sizeof(epaper_init_seq_acep7c), - .init_wait_busy_between_cmds = false, - - .frame_preamble_seq = epaper_preamble_acep7c, - .frame_preamble_seq_len = sizeof(epaper_preamble_acep7c), - - .refresh_has_data = false, - .refresh_data_byte = 0x00, - .post_power_off_busy_level = 0, - - .periodic_refresh_interval = 5, -}; - -const struct EPaperDesc epaper_desc_gdep073e01 = { - .name = "Good Display GDEP073E01 7.3\" 7-color", - .native_width = 800, - .native_height = 480, - .spi_clock_hz = 4000000, - - .palette = epaper_gdep073e01_palette, - .palette_size = 7, +bool epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, + const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds) +{ + if (!epaper_validate_init_seq(seq, seq_len)) { + return false; + } - .init_seq = epaper_init_seq_gdep073e01, - .init_seq_len = sizeof(epaper_init_seq_gdep073e01), - .init_wait_busy_between_cmds = true, + size_t pos = 0; + while (pos < seq_len) { + uint8_t cmd = seq[pos++]; + uint8_t flags_len = seq[pos++]; + uint8_t len = flags_len & 0x7F; - .frame_preamble_seq = NULL, - .frame_preamble_seq_len = 0, + spi_dc_write_cmd_data(bus, cmd, seq + pos, len); + pos += len; - .refresh_has_data = true, - .refresh_data_byte = 0x00, - .post_power_off_busy_level = 1, + if (flags_len & EPAPER_INIT_SEQ_DELAY) { + uint8_t delay_ms = seq[pos++]; + if (delay_ms > 0) { + vTaskDelay(delay_ms_to_ticks(delay_ms)); + } + } - .periodic_refresh_interval = 0, -}; + if (wait_busy_between_cmds) { + wait_busy_high(busy_gpio); + } + } -// clang-format on + return true; +} diff --git a/epaper_commands.h b/epaper_commands.h index c90faf0..2926f86 100644 --- a/epaper_commands.h +++ b/epaper_commands.h @@ -25,6 +25,7 @@ #include #include +#include "epaper_program.h" #include "spi_dc_driver.h" // --- Init sequence byte-array format --- @@ -42,47 +43,135 @@ // 0x00, which is PSR on most Waveshare / GoodDisplay controllers. #define EPAPER_INIT_SEQ_DELAY 0x80 - // Execute an init sequence over an SPI+DC bus. seq/seq_len describe // a byte array in the format documented above. When // wait_busy_between_cmds is true, a busy-high poll is inserted after // each command (mirroring the Good Display / Waveshare convention of // "command completes when BUSY rises"). busy_gpio is ignored when // the flag is false. -void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, - const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds); +bool epaper_validate_init_seq(const uint8_t *seq, size_t seq_len); -// Built-in init sequences. Each array is paired with a size_t -// constant giving its length; callers pass both to -// epaper_execute_init_seq(). -extern const uint8_t epaper_init_seq_acep7c[]; -extern const size_t epaper_init_seq_acep7c_len; -extern const uint8_t epaper_init_seq_gdep073e01[]; -extern const size_t epaper_init_seq_gdep073e01_len; +bool epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, + const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds); // --- Per-panel descriptor --- // +enum EPaperController +{ + EPAPER_CONTROLLER_ACEP7, + EPAPER_CONTROLLER_SSD16XX, + EPAPER_CONTROLLER_JD79656, + EPAPER_CONTROLLER_UC8151, + EPAPER_CONTROLLER_UC8276, + EPAPER_CONTROLLER_UC8175 +}; + +enum EPaperByteOrder +{ + EPAPER_BYTE_ORDER_ROW_MAJOR, + EPAPER_BYTE_ORDER_COLUMN_MAJOR +}; + +enum EPaperBitOrder +{ + EPAPER_BIT_ORDER_MSB_LEFT, + EPAPER_BIT_ORDER_LSB_LEFT +}; + +enum EPaperPolarity +{ + EPAPER_POLARITY_WHITE_IS_1, + EPAPER_POLARITY_BLACK_IS_1 +}; + +enum EPaperRefreshMode +{ + EPAPER_REFRESH_FULL, + EPAPER_REFRESH_FAST, + EPAPER_REFRESH_PARTIAL, + EPAPER_REFRESH_4GRAY, + EPAPER_REFRESH_MODE_COUNT +}; + +#define EPAPER_REFRESH_MODE_FULL (1U << EPAPER_REFRESH_FULL) +#define EPAPER_REFRESH_MODE_FAST (1U << EPAPER_REFRESH_FAST) +#define EPAPER_REFRESH_MODE_PARTIAL (1U << EPAPER_REFRESH_PARTIAL) +#define EPAPER_REFRESH_MODE_4GRAY (1U << EPAPER_REFRESH_4GRAY) + +#define EPAPER_MAX_SLEEP_MODES 8 + +enum EPaperControllerRamPolicy +{ + EPAPER_CONTROLLER_RAM_UNKNOWN, + EPAPER_CONTROLLER_RAM_RETAINED, + EPAPER_CONTROLLER_RAM_LOST +}; + +enum EPaperHostPrevFramePolicy +{ + EPAPER_HOST_PREV_FRAME_INVALIDATE, + EPAPER_HOST_PREV_FRAME_PRESERVE +}; + +enum EPaperAfterWakeRefreshPolicy +{ + EPAPER_AFTER_WAKE_REFRESH_FULL, + EPAPER_AFTER_WAKE_REFRESH_ALLOW, + EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS +}; + +enum EPaperWakePolicy +{ + EPAPER_WAKE_INIT, + EPAPER_WAKE_RESET_INIT, + EPAPER_WAKE_PROGRAM +}; + +struct EPaperFrameLayout +{ + enum EPaperByteOrder byte_order; + enum EPaperBitOrder bit_order; + enum EPaperPolarity polarity; +}; + +struct EPaperSleepMode +{ + uint32_t atom_index; + struct EPaperProgram enter; + struct EPaperProgram wake; + enum EPaperWakePolicy wake_policy; + enum EPaperControllerRamPolicy controller_ram; + enum EPaperHostPrevFramePolicy host_prev_frame; + enum EPaperAfterWakeRefreshPolicy after_wake_refresh; +}; + // Captures every panel-specific knob so a single unified driver can -// drive multiple controllers by compatible-string dispatch. The -// struct carries no function pointers: the current variation across -// ACeP 5.65" and GDEP073E01 is entirely data. +// drive multiple controllers by descriptor dispatch. struct EPaperDesc { const char *name; int native_width; int native_height; + int view_width; + int view_height; + int rotation; int spi_clock_hz; // Color palette (RGB triplets) and its entry count. const uint8_t (*palette)[3]; int palette_size; + enum EPaperController controller; + struct EPaperFrameLayout layout; + // One-time init sequence (format documented above). const uint8_t *init_seq; size_t init_seq_len; bool init_wait_busy_between_cmds; + bool use_gpio_pullups; + // Optional per-frame preamble sent before DTM (0x10) on every // do_update and clear_screen. NULL when unused. Uses the same // byte-array format as init_seq and is executed without inter- @@ -97,13 +186,39 @@ struct EPaperDesc uint8_t refresh_data_byte; int post_power_off_busy_level; + // BUSY pin level when the controller is idle / ready to receive + // commands. Used after reset (in display_spi_init) before + // executing any init sequence. Most Waveshare/Good Display PSR- + // style controllers idle BUSY high (1); SSD1680 idles BUSY low + // (0). Get this wrong and the post-reset busy wait deadlocks. + int busy_idle_level; + // Periodic full-screen white-out. Zero = disabled. N > 0 means // "call clear_screen(7) once every N do_update() invocations" to // prevent ghosting on panels that need it (ACeP 5.65"). int periodic_refresh_interval; -}; -extern const struct EPaperDesc epaper_desc_acep7c; -extern const struct EPaperDesc epaper_desc_gdep073e01; + // Data-driven descriptor programs. + uint8_t descriptor_version; + struct EPaperProgram init; + struct EPaperProgram program_full; + struct EPaperProgram program_fast; + struct EPaperProgram program_partial; + struct EPaperProgram program_4gray; + struct EPaperSleepMode sleep_modes[EPAPER_MAX_SLEEP_MODES]; + int sleep_mode_count; + uint8_t refresh_mode_mask; + struct EPaperLut lut_slots[EPAPER_MAX_LUT_SLOTS]; + + // Timing & ghosting fields + int full_expected_ms; + int fast_expected_ms; + int poll_interval_ms; + int timeout_ms; + int max_fast_refreshes; + bool reseed_on_timeout; + + enum EPaperRefreshMode default_refresh; +}; #endif diff --git a/epaper_display_driver.c b/epaper_display_driver.c index 12f9152..affaa94 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -20,10 +20,17 @@ #include #include +#include #include +#include +#include #include +#include #include +#include +#include +#include #include #include @@ -32,6 +39,7 @@ #include #include #include +#include #include #include @@ -43,8 +51,10 @@ #include "epaper_color.h" #include "epaper_commands.h" #include "epaper_draw.h" +#include "epaper_program.h" #include "epaper_screen.h" #include "image_helpers.h" +#include "mono_draw.h" #include "spi_dc_driver.h" #include "spi_display.h" @@ -53,7 +63,31 @@ static const char *TAG = "epaper_display_driver"; -static void clear_screen(Context *ctx, int color); +#define SSD1680_LUT_PAYLOAD_LEN 153 +#define SSD1680_LUT_WITH_REGS_LEN 159 +#define UC8151_LUT_PAYLOAD_LEN 30 +#define UC8276_LUT_PAYLOAD_LEN 227 +#define UC8276_LUT_WITH_REGS_LEN 233 +#define EPAPER_TERM_PROGRAM_COUNT 5 +#define EPAPER_MAX_SPI_CLOCK_HZ 80000000 + +struct EPaperState +{ + bool prev_valid; + int fast_refresh_count; + enum EPaperRefreshMode last_refresh_mode; + uint64_t last_refresh_ms; + bool needs_reseed; + bool last_op_ok; + bool asleep; + const struct EPaperSleepMode *asleep_mode; + bool after_wake_pending; + bool after_wake_can_reseed_prev; + enum EPaperAfterWakeRefreshPolicy after_wake_refresh; +}; + +static void clear_screen_internal(Context *ctx, int color, + bool clear_after_wake_pending); struct EpaperDriver { @@ -62,13 +96,34 @@ struct EpaperDriver int busy_gpio; int reset_gpio; + // Optional board-level panel power switch (e.g. Heltec Vext, Waveshare HAT + // power pins): -1 when the panel is permanently powered. + int panel_power_gpio; + bool panel_power_active_high; + bool panel_power_on; + uint32_t panel_power_delay_ms; + const struct EPaperDesc *desc; + struct EPaperDesc term_desc; + char *term_desc_name; + uint8_t *term_init_seq_bytes; + uint8_t *term_frame_preamble_seq_bytes; + uint8_t *term_program_bytes[EPAPER_TERM_PROGRAM_COUNT]; + uint8_t *term_sleep_enter_bytes[EPAPER_MAX_SLEEP_MODES]; + uint8_t *term_sleep_wake_bytes[EPAPER_MAX_SLEEP_MODES]; + uint8_t *term_lut_bytes[EPAPER_MAX_LUT_SLOTS]; + struct EpaperScreen screen; + struct MonoScreen mono_screen; + + uint8_t *prev_frame; + size_t prev_frame_len; + + struct EPaperState state; Context *ctx; int count_to_refresh; - uint64_t last_refresh; struct DisplayTaskArgs display_args; }; @@ -76,25 +131,48 @@ struct EpaperDriver #define EPAPER_DRIVER_FROM_CTX(ctx) \ CONTAINER_OF((struct DisplayTaskArgs *) (ctx)->platform_data, struct EpaperDriver, display_args) -static const struct { - const char *compat; - const struct EPaperDesc *desc; -} epaper_compat_table[] = { - { "waveshare,5in65-acep-7c", &epaper_desc_acep7c }, - { "good-display/gdep073e01", &epaper_desc_gdep073e01 }, +static const struct EPaperDesc epaper_desc_term_default = { + .name = "Erlang e-paper descriptor", + .native_width = 128, + .native_height = 296, + .view_width = 128, + .view_height = 296, + .rotation = 0, + .spi_clock_hz = 4000000, + .palette = NULL, + .palette_size = 2, + .controller = EPAPER_CONTROLLER_SSD16XX, + .layout = { + .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, + .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, + .polarity = EPAPER_POLARITY_WHITE_IS_1 + }, + .use_gpio_pullups = false, + .busy_idle_level = 0, + .refresh_mode_mask = EPAPER_REFRESH_MODE_FULL, + .descriptor_version = 3, + .full_expected_ms = 2000, + .fast_expected_ms = 500, + .poll_interval_ms = 50, + .timeout_ms = 5000, + .max_fast_refreshes = 10, + .reseed_on_timeout = true, + .default_refresh = EPAPER_REFRESH_FULL, }; -static const struct EPaperDesc *epaper_desc_for_compatible(const char *compat) +static bool epaper_desc_requires_program(const struct EPaperDesc *desc) { - for (size_t i = 0; i < sizeof(epaper_compat_table) / sizeof(epaper_compat_table[0]); i++) { - if (!strcmp(compat, epaper_compat_table[i].compat)) { - return epaper_compat_table[i].desc; - } - } - return NULL; + return desc->controller != EPAPER_CONTROLLER_ACEP7; } static void display_init_using_list(struct EpaperDriver *driver, term init_list); +static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, + term opts, Context *ctx); +static void epaper_free_init_failed_driver(struct EpaperDriver *driver, + bool spi_device_added); +static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefreshMode *out); +static bool epaper_parse_palette(term val, Context *ctx, + const uint8_t (**palette)[3], int *palette_size); static void display_reset(struct EpaperDriver *driver) { @@ -103,6 +181,100 @@ static void display_reset(struct EpaperDriver *driver) gpio_set_level(driver->reset_gpio, 1); } +static void epaper_delay_ms(uint32_t delay_ms) +{ + if (delay_ms == 0) { + return; + } + if (delay_ms < portTICK_PERIOD_MS) { + esp_rom_delay_us(delay_ms * 1000); + return; + } + vTaskDelay((delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS); +} + +// Board-level panel power switch. Ports may pass `panel_power` (GPIO number), +// `panel_power_active` (:high by default, :low for inverted switches) and +// `panel_power_delay_ms` (settle time before the controller is touched, 100 ms +// by default). Without the option the panel is assumed to be always powered, +// which is what the desktop/bench panels do. +#define EPAPER_PANEL_POWER_DEFAULT_DELAY_MS 100 + +static bool epaper_parse_panel_power(struct EpaperDriver *driver, term opts, Context *ctx) +{ + GlobalContext *global = ctx->global; + + term gpio_term = interop_kv_get_value_default( + opts, ATOM_STR("\xB", "panel_power"), term_invalid_term(), global); + if (gpio_term == term_invalid_term()) { + return true; + } + if (!term_is_integer(gpio_term)) { + ESP_LOGE(TAG, "Invalid panel_power option: expected a GPIO number."); + return false; + } + avm_int_t gpio = term_to_int(gpio_term); + if (gpio < 0 || gpio > INT_MAX || !GPIO_IS_VALID_OUTPUT_GPIO((int) gpio)) { + ESP_LOGE(TAG, "Invalid panel_power option: expected an output-capable GPIO."); + return false; + } + driver->panel_power_gpio = (int) gpio; + driver->panel_power_active_high = true; + driver->panel_power_delay_ms = EPAPER_PANEL_POWER_DEFAULT_DELAY_MS; + + term active_term = interop_kv_get_value_default( + opts, ATOM_STR("\x12", "panel_power_active"), term_invalid_term(), global); + if (active_term != term_invalid_term()) { + if (active_term == context_make_atom(ctx, ATOM_STR("\x4", "high"))) { + driver->panel_power_active_high = true; + } else if (active_term == context_make_atom(ctx, ATOM_STR("\x3", "low"))) { + driver->panel_power_active_high = false; + } else { + ESP_LOGE(TAG, "Invalid panel_power_active option: expected high or low."); + return false; + } + } + + term delay_term = interop_kv_get_value_default( + opts, ATOM_STR("\x14", "panel_power_delay_ms"), term_invalid_term(), global); + if (delay_term != term_invalid_term()) { + avm_int_t delay_ms = term_is_integer(delay_term) ? term_to_int(delay_term) : -1; + if (delay_ms < 0) { + ESP_LOGE(TAG, "Invalid panel_power_delay_ms option: expected milliseconds >= 0."); + return false; + } + driver->panel_power_delay_ms = (uint32_t) delay_ms; + } + + ESP_LOGI(TAG, "Panel power switch: GPIO%d, %s, %ums settle delay.", + driver->panel_power_gpio, driver->panel_power_active_high ? "active high" : "active low", + (unsigned) driver->panel_power_delay_ms); + return true; +} + +// Raises or drops the panel rail. Idempotent, so callers may assert power on +// every path that is about to talk to the controller. +static bool epaper_set_panel_power(struct EpaperDriver *driver, bool on) +{ + if (driver->panel_power_gpio < 0 || on == driver->panel_power_on) { + return true; + } + + int on_level = driver->panel_power_active_high ? 1 : 0; + if (gpio_set_direction(driver->panel_power_gpio, GPIO_MODE_OUTPUT) != ESP_OK + || gpio_set_level(driver->panel_power_gpio, on ? on_level : !on_level) != ESP_OK) { + ESP_LOGE(TAG, "Failed to switch panel power GPIO%d %s.", + driver->panel_power_gpio, on ? "on" : "off"); + return false; + } + driver->panel_power_on = on; + if (on) { + // The controller must be powered before it accepts reset and commands. + epaper_delay_ms(driver->panel_power_delay_ms); + } + return true; +} + static void wait_busy_level(struct EpaperDriver *driver, int level) { while (gpio_get_level(driver->busy_gpio) != level) { @@ -110,6 +282,117 @@ static void wait_busy_level(struct EpaperDriver *driver, int level) } } +// Wait for BUSY pin to go to target level. Returns true on +// success, false on timeout. On timeout, invalidates the cached +// partial-refresh previous frame so the next partial update re-seeds +// via a full base refresh. +static bool wait_busy_timeout(struct EpaperDriver *driver, int level, + uint32_t timeout_ms) +{ + TickType_t start = xTaskGetTickCount(); + // Descriptor timeouts can exceed 16 bits; avoid overflow in tick conversion. + TickType_t timeout = (TickType_t) (((uint64_t) timeout_ms * configTICK_RATE_HZ + 999) / 1000); + + while (gpio_get_level(driver->busy_gpio) != level) { + if ((TickType_t) (xTaskGetTickCount() - start) >= timeout) { + if (driver->state.prev_valid) { + ESP_LOGW(TAG, + "BUSY wait for level %d timed out for %s; invalidating partial prev frame.", + level, driver->desc->name); + } else { + ESP_LOGW(TAG, "BUSY wait for level %d timed out for %s.", + level, driver->desc->name); + } + driver->state.prev_valid = false; + if (driver->desc->reseed_on_timeout) { + driver->state.needs_reseed = true; + } + return false; + } + epaper_delay_ms(10); + } + return true; +} + +static bool wait_busy_low_timeout(struct EpaperDriver *driver) +{ + int timeout = (driver->desc->timeout_ms > 0) ? driver->desc->timeout_ms : 5000; + return wait_busy_timeout(driver, 0, timeout); +} + +static bool epaper_write_lut_bytes(struct EpaperDriver *driver, + const uint8_t *lut, size_t lut_len) +{ + if (lut == NULL || lut_len == 0) { + return false; + } + + if (driver->desc->controller == EPAPER_CONTROLLER_UC8151) { + if (lut_len < UC8151_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "UC8151 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, UC8151_LUT_PAYLOAD_LEN); + return true; + } + + if (driver->desc->controller == EPAPER_CONTROLLER_UC8276) { + if (lut_len < UC8276_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "UC8276 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, UC8276_LUT_PAYLOAD_LEN); + + if (lut_len < UC8276_LUT_WITH_REGS_LEN) { + return true; + } + + spi_dc_write_command(&driver->bus, 0x3F); + spi_dc_write_data(&driver->bus, lut[227]); + spi_dc_write_command(&driver->bus, 0x03); + spi_dc_write_data(&driver->bus, lut[228]); + spi_dc_write_command(&driver->bus, 0x04); + spi_dc_write_data(&driver->bus, lut[229]); + spi_dc_write_data(&driver->bus, lut[230]); + spi_dc_write_data(&driver->bus, lut[231]); + spi_dc_write_command(&driver->bus, 0x2C); + spi_dc_write_data(&driver->bus, lut[232]); + return true; + } + + if (driver->desc->controller != EPAPER_CONTROLLER_SSD16XX) { + ESP_LOGE(TAG, "Controller %d does not support LUT insertion.", + driver->desc->controller); + return false; + } + + if (lut_len < SSD1680_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "SSD1680 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, SSD1680_LUT_PAYLOAD_LEN); + if (!wait_busy_low_timeout(driver)) { + return false; + } + + if (lut_len < SSD1680_LUT_WITH_REGS_LEN) { + return true; + } + + spi_dc_write_command(&driver->bus, 0x3F); // WRITE_EOPQ + spi_dc_write_data(&driver->bus, lut[153]); + spi_dc_write_command(&driver->bus, 0x03); // GATE_VOLTAGE + spi_dc_write_data(&driver->bus, lut[154]); + spi_dc_write_command(&driver->bus, 0x04); // SOURCE_VOLTAGE + spi_dc_write_data(&driver->bus, lut[155]); + spi_dc_write_data(&driver->bus, lut[156]); + spi_dc_write_data(&driver->bus, lut[157]); + spi_dc_write_command(&driver->bus, 0x2C); // WRITE_VCOM + spi_dc_write_data(&driver->bus, lut[158]); + return true; +} + static void wait_some_time(Context *ctx) { struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); @@ -117,11 +400,11 @@ static void wait_some_time(Context *ctx) struct timeval tv; gettimeofday(&tv, NULL); uint64_t now = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); - uint64_t delta = now - driver->last_refresh; + uint64_t delta = now - driver->state.last_refresh_ms; if (delta < 2000) { // Wait 2 seconds before allowing a new refresh; undocumented but // empirically required or the panel drops updates. - vTaskDelay((2000 - delta) / portTICK_PERIOD_MS); + epaper_delay_ms(2000 - delta); } } @@ -131,7 +414,7 @@ static void update_last_refresh_ts(Context *ctx) struct timeval tv; gettimeofday(&tv, NULL); - driver->last_refresh = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); + driver->state.last_refresh_ms = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); } static void maybe_refresh(Context *ctx) @@ -144,19 +427,20 @@ static void maybe_refresh(Context *ctx) driver->count_to_refresh--; if (driver->count_to_refresh <= 0) { // 7 is the panel's white entry on both current palettes. - clear_screen(ctx, 7); + clear_screen_internal(ctx, 7, false); update_last_refresh_ts(ctx); driver->count_to_refresh = driver->desc->periodic_refresh_interval; } } -static void send_frame_preamble(struct EpaperDriver *driver) +static bool send_frame_preamble(struct EpaperDriver *driver) { if (driver->desc->frame_preamble_seq != NULL) { - epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + return epaper_execute_init_seq(&driver->bus, driver->busy_gpio, driver->desc->frame_preamble_seq, driver->desc->frame_preamble_seq_len, false); } + return true; } static void send_post_frame_refresh(struct EpaperDriver *driver) @@ -177,52 +461,126 @@ static void send_post_frame_refresh(struct EpaperDriver *driver) wait_busy_level(driver, driver->desc->post_power_off_busy_level); } -static void do_update(Context *ctx, term display_list) +static int view_mono_line_bytes(struct EpaperDriver *driver) { - maybe_refresh(ctx); - wait_some_time(ctx); + return (driver->mono_screen.w + 7) / 8; +} - BaseDisplayItem *items; - size_t len; - if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { - return; +static int native_row_bytes(struct EpaperDriver *driver) +{ + return (driver->desc->native_width + 7) / 8; +} + +static int native_column_bytes(struct EpaperDriver *driver) +{ + return (driver->desc->native_height + 7) / 8; +} + +static int native_frame_chunk_bytes(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return native_column_bytes(driver); } + return native_row_bytes(driver); +} - struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); - int screen_width = driver->screen.w; - int screen_height = driver->screen.h; +static int native_frame_chunk_count(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return driver->desc->native_width; + } + return driver->desc->native_height; +} + +static size_t mono_frame_bytes(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return (size_t) driver->desc->native_width * native_column_bytes(driver); + } + return (size_t) native_row_bytes(driver) * driver->desc->native_height; +} - send_frame_preamble(driver); +static bool epaper_view_to_native(const struct EPaperDesc *desc, + int vx, int vy, int *nx, int *ny) +{ + switch (desc->rotation) { + case 0: + *nx = vx; + *ny = vy; + break; + case 90: + *nx = vy; + *ny = desc->native_height - 1 - vx; + break; + case 180: + *nx = desc->native_width - 1 - vx; + *ny = desc->native_height - 1 - vy; + break; + case 270: + *nx = desc->native_width - 1 - vy; + *ny = vx; + break; + default: + return false; + } - // DTM — data transfer to panel memory. - spi_dc_write_command(&driver->bus, 0x10); + return *nx >= 0 && *nx < desc->native_width + && *ny >= 0 && *ny < desc->native_height; +} - uint8_t *buf = heap_caps_malloc(screen_width / 2, MALLOC_CAP_DMA); - if (UNLIKELY(!buf)) { - fprintf(stderr, "do_update: failed to alloc buf\n"); - display_items_delete(items, len); - return; +static void render_mono_line_raw(struct EpaperDriver *driver, + uint8_t *buf, int ypos, BaseDisplayItem *items, int items_len) +{ + int plane_bytes = view_mono_line_bytes(driver); + memset(buf, 0xFF, plane_bytes); + + BaseDisplayItem *row = display_items_row(items, items_len, ypos); + int xpos = 0; + while (xpos < driver->mono_screen.w) { + int drawn_pixels = mono_draw_x(&driver->mono_screen, buf, xpos, ypos, row); + xpos += drawn_pixels; } - memset(buf, 0x11, screen_width / 2); +} + +static bool ensure_prev_frame(struct EpaperDriver *driver) +{ + size_t frame_len = mono_frame_bytes(driver); + if (driver->prev_frame != NULL + && driver->prev_frame_len == frame_len) { + return true; + } + + free(driver->prev_frame); + driver->prev_frame = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + driver->prev_frame_len = frame_len; + driver->state.prev_valid = false; + if (UNLIKELY(!driver->prev_frame)) { + driver->prev_frame_len = 0; + ESP_LOGE(TAG, "Failed to allocate e-paper previous-frame buffer."); + return false; + } + + return true; +} + +static void write_mono_frame(struct EpaperDriver *driver, + const uint8_t *frame) +{ bool transaction_in_progress = false; + int chunk_bytes = native_frame_chunk_bytes(driver); + int chunk_count = native_frame_chunk_count(driver); spi_device_acquire_bus(driver->bus.spi_disp.handle, portMAX_DELAY); - for (int ypos = 0; ypos < screen_height; ypos++) { + for (int chunk = 0; chunk < chunk_count; chunk++) { if (transaction_in_progress) { spi_transaction_t *trans = NULL; spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); } - BaseDisplayItem *row = display_items_row(items, len, ypos); - int xpos = 0; - while (xpos < screen_width) { - int drawn_pixels = epaper_draw_x(&driver->screen, buf, xpos, ypos, row); - xpos += drawn_pixels; - } - - spi_display_dma_write(&driver->bus.spi_disp, screen_width / 2, buf); + spi_display_dma_write(&driver->bus.spi_disp, chunk_bytes, + frame + ((size_t) chunk * chunk_bytes)); transaction_in_progress = true; } @@ -232,172 +590,2225 @@ static void do_update(Context *ctx, term display_list) } spi_device_release_bus(driver->bus.spi_disp.handle); +} - free(buf); +static void set_native_frame_bit(struct EpaperDriver *driver, uint8_t *frame, + int nx, int ny, bool bit_value) +{ + size_t byte_index; + int bit_pos; - send_post_frame_refresh(driver); + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + byte_index = (size_t) nx * native_column_bytes(driver) + (ny / 8); + bit_pos = ny % 8; + } else { + byte_index = (size_t) ny * native_row_bytes(driver) + (nx / 8); + bit_pos = nx % 8; + } - display_items_delete(items, len); + if (driver->desc->layout.bit_order == EPAPER_BIT_ORDER_MSB_LEFT) { + bit_pos = 7 - bit_pos; + } - update_last_refresh_ts(ctx); + uint8_t mask = (uint8_t) (1U << bit_pos); + if (bit_value) { + frame[byte_index] |= mask; + } else { + frame[byte_index] &= (uint8_t) ~mask; + } } -static void process_message(Message *message, Context *ctx) +static void set_native_mono_pixel(struct EpaperDriver *driver, uint8_t *frame, + int nx, int ny, bool white) { - GenMessage gen_message; - if (UNLIKELY(port_parse_gen_message(message->message, &gen_message) != GenCallMessage)) { - fprintf(stderr, "Received invalid message."); - AVM_ABORT(); - } - - term req = gen_message.req; - if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 1)) { - AVM_ABORT(); + bool bit_value = white; + if (driver->desc->layout.polarity == EPAPER_POLARITY_BLACK_IS_1) { + bit_value = !white; } - term cmd = term_get_tuple_element(req, 0); - - if (cmd == context_make_atom(ctx, "\x6" - "update")) { + set_native_frame_bit(driver, frame, nx, ny, bit_value); +} - term display_list = term_get_tuple_element(req, 1); - do_update(ctx, display_list); - return; +static bool build_native_mono_frame(struct EpaperDriver *driver, uint8_t *frame, + uint8_t *line_buf, BaseDisplayItem *items, int items_len, bool draw_bw_plane) +{ + size_t frame_len = mono_frame_bytes(driver); + memset(frame, driver->desc->layout.polarity == EPAPER_POLARITY_WHITE_IS_1 ? 0xFF : 0x00, + frame_len); - } else if (cmd == globalcontext_make_atom(ctx->global, "\xA" "load_image")) { - handle_load_image(req, gen_message.ref, gen_message.pid, ctx); - return; + for (int vy = 0; vy < driver->mono_screen.h; vy++) { + if (draw_bw_plane) { + render_mono_line_raw(driver, line_buf, vy, items, items_len); + } else { + memset(line_buf, 0xFF, view_mono_line_bytes(driver)); + } - } else { -#if REPORT_UNEXPECTED_MSGS - fprintf(stderr, "display: "); - term_display(stderr, req, ctx); - fprintf(stderr, "\n"); -#endif + for (int vx = 0; vx < driver->mono_screen.w; vx++) { + bool white = ((line_buf[vx / 8] >> (vx % 8)) & 0x01) != 0; + int nx; + int ny; + if (!epaper_view_to_native(driver->desc, vx, vy, &nx, &ny)) { + return false; + } + set_native_mono_pixel(driver, frame, nx, ny, white); + } } - BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE, heap); - term return_tuple = term_alloc_tuple(2, &heap); - term_put_tuple_element(return_tuple, 0, gen_message.ref); - term_put_tuple_element(return_tuple, 1, OK_ATOM); - - display_message_send(gen_message.pid, return_tuple, ctx->global); - END_WITH_STACK_HEAP(heap, ctx->global); + return true; } -static void clear_screen(Context *ctx, int color) +static bool write_mono_plane(struct EpaperDriver *driver, uint8_t *frame_buf, + uint8_t *line_buf, + BaseDisplayItem *items, int items_len, bool draw_bw_plane, + uint8_t *capture_frame) { - struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); - int screen_width = driver->screen.w; - int screen_height = driver->screen.h; + if (!build_native_mono_frame(driver, frame_buf, line_buf, + items, items_len, draw_bw_plane)) { + ESP_LOGE(TAG, "Failed to transform logical e-paper frame into native RAM."); + return false; + } - send_frame_preamble(driver); + if (capture_frame != NULL) { + memcpy(capture_frame, frame_buf, mono_frame_bytes(driver)); + } - spi_dc_write_command(&driver->bus, 0x10); + write_mono_frame(driver, frame_buf); + return true; +} - uint8_t *buf = heap_caps_malloc(screen_width / 2, MALLOC_CAP_DMA); - if (UNLIKELY(!buf)) { - fprintf(stderr, "clear_screen: failed to alloc buf\n"); - return; +static uint8_t get_epaper_packed_pixel(uint8_t *line_buf, int xpos) +{ + uint8_t packed = line_buf[xpos / 2]; + if ((xpos & 1) == 0) { + return packed >> 4; } + return packed & 0x0F; +} - bool transaction_in_progress = false; - - spi_device_acquire_bus(driver->bus.spi_disp.handle, portMAX_DELAY); +static bool build_native_4gray_plane(struct EpaperDriver *driver, + uint8_t *gray_buf, uint8_t *plane_buf, BaseDisplayItem *items, + int items_len, int plane_bit) +{ + int gray_bytes = (driver->mono_screen.w + 1) / 2; + memset(plane_buf, 0x00, mono_frame_bytes(driver)); - for (int i = 0; i < screen_height; i++) { - if (transaction_in_progress) { - spi_transaction_t *trans = NULL; - spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + for (int vy = 0; vy < driver->mono_screen.h; vy++) { + memset(gray_buf, 0x00, gray_bytes); + BaseDisplayItem *row = display_items_row(items, items_len, vy); + int xpos = 0; + while (xpos < driver->mono_screen.w) { + int drawn_pixels = epaper_draw_4gray_x(&driver->screen, gray_buf, xpos, vy, row); + xpos += drawn_pixels; } - // memset inside the loop so every scanline carries fresh data, - // avoiding artefacts if a prior scanline left stale bytes. - memset(buf, color | (color << 4), screen_width / 2); - spi_display_dma_write(&driver->bus.spi_disp, screen_width / 2, buf); - transaction_in_progress = true; + for (xpos = 0; xpos < driver->mono_screen.w; xpos++) { + uint8_t gray = get_epaper_packed_pixel(gray_buf, xpos); + bool bit_value; + if (driver->desc->controller == EPAPER_CONTROLLER_UC8276) { + // UC8276 4-gray expects inverted two-bit gray planes: + // plane 0 carries the high bit, plane 1 carries the low bit. + bit_value = (gray & (plane_bit == 0 ? 0x02 : 0x01)) == 0; + } else { + bit_value = (gray & (1 << plane_bit)) != 0; + } + if (bit_value) { + int nx; + int ny; + if (!epaper_view_to_native(driver->desc, xpos, vy, &nx, &ny)) { + return false; + } + set_native_frame_bit(driver, plane_buf, nx, ny, true); + } + } } - if (transaction_in_progress) { - spi_transaction_t *trans = NULL; - spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + return true; +} + +static bool write_4gray_plane(struct EpaperDriver *driver, + uint8_t *gray_buf, uint8_t *plane_buf, BaseDisplayItem *items, + int items_len, int plane_bit) +{ + if (!build_native_4gray_plane(driver, gray_buf, plane_buf, + items, items_len, plane_bit)) { + ESP_LOGE(TAG, "Failed to transform logical 4-gray e-paper frame into native RAM."); + return false; } - spi_device_release_bus(driver->bus.spi_disp.handle); + write_mono_frame(driver, plane_buf); + return true; +} - free(buf); +struct EPaperProgramRun +{ + uint8_t *plane_buf; + uint8_t *line_buf; + uint8_t *gray_buf; + BaseDisplayItem *items; + int items_len; + bool capture_next_plane; + bool capture_succeeded; + bool mark_prev_valid_requested; + bool is_4gray; +}; - send_post_frame_refresh(driver); +struct EPaperProgramDriverContext +{ + struct EpaperDriver *driver; + struct EPaperProgramRun *run; +}; + +static void epaper_program_command(void *ctx, uint8_t command, + const uint8_t *data, size_t len) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + spi_dc_write_cmd_data(&program_ctx->driver->bus, command, data, len); } -static void display_spi_init(Context *ctx, term opts) +static void epaper_program_delay_ms(void *ctx, uint8_t delay_ms) { - // Resolve compatible string -> per-panel descriptor. - term compat_term = interop_kv_get_value_default( - opts, ATOM_STR("\xA", "compatible"), term_nil(), ctx->global); - int str_ok; - char *compat_string = interop_term_to_string(compat_term, &str_ok); - const struct EPaperDesc *desc = NULL; - if (str_ok && compat_string) { - desc = epaper_desc_for_compatible(compat_string); - } - if (!desc) { - ESP_LOGE(TAG, "Failed init: unknown or missing compatible '%s'.", - compat_string ? compat_string : "(null)"); - free(compat_string); - return; - } - free(compat_string); + (void) ctx; + epaper_delay_ms(delay_ms); +} - struct EpaperDriver *driver = malloc(sizeof(struct EpaperDriver)); - // TODO check here +static void epaper_program_reset(void *ctx, uint8_t high_ms, + uint8_t low_ms, uint8_t settle_ms) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + gpio_set_level(driver->reset_gpio, 1); + epaper_delay_ms(high_ms); + gpio_set_level(driver->reset_gpio, 0); + epaper_delay_ms(low_ms); + gpio_set_level(driver->reset_gpio, 1); + epaper_delay_ms(settle_ms); +} - driver->desc = desc; - driver->ctx = ctx; - driver->screen.w = desc->native_width; - driver->screen.h = desc->native_height; - driver->screen.palette = desc->palette; - driver->screen.palette_size = desc->palette_size; +static bool epaper_program_wait_busy(void *ctx, uint8_t level, + uint16_t timeout_ms) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + return wait_busy_timeout(program_ctx->driver, level, timeout_ms); +} - driver->display_args.messages_queue = xQueueCreate(32, sizeof(Message *)); - driver->display_args.process_message_fn = process_message; - driver->display_args.ctx = ctx; - ctx->platform_data = &driver->display_args; +static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + struct EPaperProgramRun *run = program_ctx->run; - struct SPIDisplayConfig spi_config; - spi_display_init_config(&spi_config); - spi_config.clock_speed_hz = desc->spi_clock_hz; - spi_display_parse_config(&spi_config, opts, ctx->global); - spi_display_init(&driver->bus.spi_disp, &spi_config); + if (run == NULL || run->plane_buf == NULL) { + ESP_LOGE(TAG, "INSERT_PLANE used without render state."); + return false; + } - bool ok = display_common_gpio_from_opts(opts, ATOM_STR("\x4", "busy"), &driver->busy_gpio, ctx->global); + uint8_t *capture_frame = NULL; + if (run->capture_next_plane) { + if (run->is_4gray) { + ESP_LOGE(TAG, "CAPTURE_FRAME is not supported for 4-gray frames."); + return false; + } + if (!ensure_prev_frame(driver)) { + return false; + } + capture_frame = driver->prev_frame; + run->capture_next_plane = false; + } + + switch (plane_id) { + case 0: + if ((driver->desc->controller == EPAPER_CONTROLLER_SSD16XX + || driver->desc->controller == EPAPER_CONTROLLER_UC8276) + && run->is_4gray) { + if (run->gray_buf == NULL) { + return false; + } + return write_4gray_plane(driver, run->gray_buf, run->plane_buf, + run->items, run->items_len, 0); + } + if (run->line_buf == NULL) { + return false; + } + if (!write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, true, capture_frame)) { + return false; + } + if (capture_frame != NULL) { + run->capture_succeeded = true; + driver->state.prev_valid = false; + } + return true; + case 1: + if ((driver->desc->controller == EPAPER_CONTROLLER_SSD16XX + || driver->desc->controller == EPAPER_CONTROLLER_UC8276) + && run->is_4gray) { + if (run->gray_buf == NULL) { + return false; + } + return write_4gray_plane(driver, run->gray_buf, run->plane_buf, + run->items, run->items_len, 1); + } + if (run->line_buf == NULL) { + return false; + } + if (!write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, false, capture_frame)) { + return false; + } + if (capture_frame != NULL) { + run->capture_succeeded = true; + driver->state.prev_valid = false; + } + return true; + default: + ESP_LOGE(TAG, "Unsupported INSERT_PLANE id %d.", plane_id); + return false; + } +} + +static bool epaper_program_insert_lut(void *ctx, uint8_t slot_id) +{ + if (slot_id >= EPAPER_MAX_LUT_SLOTS) { + return false; + } + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + if (driver->desc->lut_slots[slot_id].bytes == NULL + || driver->desc->lut_slots[slot_id].len == 0) { + ESP_LOGE(TAG, "INSERT_LUT references missing LUT slot %d.", slot_id); + return false; + } + return epaper_write_lut_bytes(driver, + driver->desc->lut_slots[slot_id].bytes, + driver->desc->lut_slots[slot_id].len); +} + +static bool epaper_program_insert_prev_frame(void *ctx, uint8_t plane_id) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + struct EPaperProgramRun *run = program_ctx->run; + bool cached_frame_available = driver->state.prev_valid + || driver->state.after_wake_can_reseed_prev + || (run != NULL && run->capture_succeeded); + if (plane_id != 0 || !cached_frame_available + || driver->prev_frame == NULL + || driver->prev_frame_len != mono_frame_bytes(driver)) { + ESP_LOGE(TAG, "INSERT_PREV_FRAME used without a valid plane 0 cache."); + return false; + } + write_mono_frame(driver, driver->prev_frame); + return true; +} + +static bool epaper_program_capture_frame(void *ctx) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + if (program_ctx->run == NULL) { + return false; + } + program_ctx->run->capture_next_plane = true; + return true; +} + +static bool epaper_program_mark_prev_valid(void *ctx) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + if (program_ctx->run == NULL || !program_ctx->run->capture_succeeded) { + ESP_LOGE(TAG, "MARK_PREV_VALID used without a successful capture."); + return false; + } + program_ctx->run->mark_prev_valid_requested = true; + return true; +} + +static bool epaper_run_driver_program(struct EpaperDriver *driver, + const struct EPaperProgram *program, struct EPaperProgramRun *run) +{ + struct EPaperProgramDriverContext program_ctx = { + .driver = driver, + .run = run + }; + const struct EPaperProgramOps ops = { + .ctx = &program_ctx, + .command = epaper_program_command, + .delay_ms = epaper_program_delay_ms, + .reset = epaper_program_reset, + .wait_busy = epaper_program_wait_busy, + .insert_plane = epaper_program_insert_plane, + .insert_lut = epaper_program_insert_lut, + .insert_prev_frame = epaper_program_insert_prev_frame, + .capture_frame = epaper_program_capture_frame, + .mark_prev_valid = epaper_program_mark_prev_valid + }; + bool ok = epaper_run_program(program, &ops); + if (run != NULL) { + if (run->capture_next_plane) { + ok = false; + } + if (run->capture_succeeded) { + driver->state.prev_valid = ok && run->mark_prev_valid_requested; + } + } + return ok; +} + +static void epaper_clear_after_wake_policy(struct EpaperDriver *driver) +{ + driver->state.after_wake_pending = false; + driver->state.after_wake_can_reseed_prev = false; + driver->state.after_wake_refresh = EPAPER_AFTER_WAKE_REFRESH_ALLOW; +} + +static const struct EPaperSleepMode *epaper_find_sleep_mode(struct EpaperDriver *driver, + term mode_term) +{ + if (!term_is_atom(mode_term)) { + return NULL; + } + uint32_t atom_index = (uint32_t) term_to_atom_index(mode_term); + for (int i = 0; i < driver->desc->sleep_mode_count; i++) { + const struct EPaperSleepMode *mode = &driver->desc->sleep_modes[i]; + if (mode->atom_index == atom_index) { + return mode; + } + } + return NULL; +} + +static bool epaper_run_init_sequence(struct EpaperDriver *driver, bool reset_first) +{ + if (reset_first) { + display_reset(driver); + if (!wait_busy_timeout(driver, driver->desc->busy_idle_level, + driver->desc->timeout_ms)) { + return false; + } + } + if (driver->desc->init.bytes != NULL) { + return epaper_run_driver_program(driver, &driver->desc->init, NULL); + } + if (driver->desc->init_seq != NULL) { + return epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + driver->desc->init_seq, driver->desc->init_seq_len, + driver->desc->init_wait_busy_between_cmds); + } + + if (!reset_first) { + display_reset(driver); + if (!wait_busy_timeout(driver, driver->desc->busy_idle_level, + driver->desc->timeout_ms)) { + return false; + } + } + return true; +} + +static bool epaper_run_wake_sequence(struct EpaperDriver *driver, + const struct EPaperSleepMode *mode) +{ + if (mode != NULL && mode->wake_policy == EPAPER_WAKE_PROGRAM) { + return epaper_run_driver_program(driver, &mode->wake, NULL); + } + if (mode != NULL && mode->wake_policy == EPAPER_WAKE_INIT) { + return epaper_run_init_sequence(driver, false); + } + return epaper_run_init_sequence(driver, true); +} + +static bool epaper_wake_if_asleep(struct EpaperDriver *driver) +{ + if (!driver->state.asleep) { + return true; + } + + const struct EPaperSleepMode *mode = driver->state.asleep_mode; + // A sleep mode with lost controller RAM also drops the panel rail, so it + // has to be raised (and allowed to settle) before the wake sequence runs. + if (!epaper_set_panel_power(driver, true)) { + return false; + } + if (!epaper_run_wake_sequence(driver, mode)) { + ESP_LOGW(TAG, "E-paper wake sequence failed for %s.", driver->desc->name); + // Keep the port asleep and restore the rail-off state of lost-RAM modes. + if (mode != NULL && mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + epaper_set_panel_power(driver, false); + } + return false; + } + + driver->state.asleep = false; + driver->state.asleep_mode = NULL; + bool prev_valid_before_wake = driver->state.prev_valid; + bool host_prev_preserved = mode != NULL + && mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_PRESERVE; + bool controller_ram_retained = mode != NULL + && mode->controller_ram == EPAPER_CONTROLLER_RAM_RETAINED; + if (!(host_prev_preserved && controller_ram_retained)) { + driver->state.prev_valid = false; + } + // Reset/init does not constitute a full optical refresh. + driver->state.after_wake_pending = true; + driver->state.after_wake_can_reseed_prev = + host_prev_preserved && prev_valid_before_wake; + driver->state.after_wake_refresh = mode != NULL + ? mode->after_wake_refresh + : EPAPER_AFTER_WAKE_REFRESH_FULL; + return true; +} + +static bool do_sleep(Context *ctx, term mode_term) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + + if (driver->state.asleep) { + // Retry power-off if a previous sleep or failed wake could not drop it. + if (driver->state.asleep_mode != NULL + && driver->state.asleep_mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + return epaper_set_panel_power(driver, false); + } + return true; + } + + const struct EPaperSleepMode *mode = epaper_find_sleep_mode(driver, mode_term); + if (mode == NULL) { + ESP_LOGI(TAG, "E-paper descriptor for %s has no requested sleep mode.", + driver->desc->name); + return false; + } + + if (!epaper_run_driver_program(driver, &mode->enter, NULL)) { + ESP_LOGW(TAG, "E-paper sleep program failed for %s.", driver->desc->name); + return false; + } + + driver->state.asleep = true; + driver->state.asleep_mode = mode; + epaper_clear_after_wake_policy(driver); + if (mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_INVALIDATE) { + driver->state.prev_valid = false; + } + if (mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + // The descriptor says the controller keeps nothing across this mode, so + // the panel rail can stay down until something wakes it again. + return epaper_set_panel_power(driver, false); + } + return true; +} + +static bool do_wake(Context *ctx) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + + if (driver->state.asleep) { + return epaper_wake_if_asleep(driver); + } + return true; +} + +static const struct EPaperProgram *epaper_program_for_refresh_mode( + struct EpaperDriver *driver, enum EPaperRefreshMode mode) +{ + switch (mode) { + case EPAPER_REFRESH_FULL: + return &driver->desc->program_full; + case EPAPER_REFRESH_FAST: + return &driver->desc->program_fast; + case EPAPER_REFRESH_PARTIAL: + return &driver->desc->program_partial; + case EPAPER_REFRESH_4GRAY: + return &driver->desc->program_4gray; + default: + return &driver->desc->program_full; + } +} + +static uint8_t epaper_refresh_mode_bit(enum EPaperRefreshMode mode) +{ + return (uint8_t) (1U << mode); +} + +static bool epaper_refresh_mode_allowed(const struct EPaperDesc *desc, + enum EPaperRefreshMode mode) +{ + return (desc->refresh_mode_mask & epaper_refresh_mode_bit(mode)) != 0; +} + +static term epaper_refresh_mode_atom(Context *ctx, enum EPaperRefreshMode mode) +{ + switch (mode) { + case EPAPER_REFRESH_FULL: + return context_make_atom(ctx, ATOM_STR("\x4", "full")); + case EPAPER_REFRESH_FAST: + return context_make_atom(ctx, ATOM_STR("\x4", "fast")); + case EPAPER_REFRESH_PARTIAL: + return context_make_atom(ctx, ATOM_STR("\x7", "partial")); + case EPAPER_REFRESH_4GRAY: + return context_make_atom(ctx, ATOM_STR("\x5", "4gray")); + default: + return context_make_atom(ctx, ATOM_STR("\x7", "unknown")); + } +} + +static term epaper_controller_atom(Context *ctx, enum EPaperController controller) +{ + switch (controller) { + case EPAPER_CONTROLLER_SSD16XX: + return context_make_atom(ctx, ATOM_STR("\x7", "ssd16xx")); + case EPAPER_CONTROLLER_JD79656: + return context_make_atom(ctx, ATOM_STR("\x7", "jd79656")); + case EPAPER_CONTROLLER_UC8151: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8151")); + case EPAPER_CONTROLLER_UC8276: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8276")); + case EPAPER_CONTROLLER_UC8175: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8175")); + case EPAPER_CONTROLLER_ACEP7: + return context_make_atom(ctx, ATOM_STR("\x5", "acep7")); + default: + return context_make_atom(ctx, ATOM_STR("\x7", "unknown")); + } +} + +static bool epaper_program_inserts_prev_frame(const struct EPaperProgram *program) +{ + if (program == NULL || program->bytes == NULL) { + return false; + } + + size_t pos = 0; + while (pos + 2 <= program->len) { + uint8_t opcode = program->bytes[pos++]; + uint8_t flags_len = program->bytes[pos++]; + size_t data_len = flags_len & EPAPER_PROGRAM_LEN_MASK; + bool is_meta = (flags_len & EPAPER_PROGRAM_META) != 0; + bool has_delay = (flags_len & EPAPER_PROGRAM_DELAY) != 0; + size_t next = pos + data_len + (has_delay ? 1 : 0); + if (next > program->len) { + return false; + } + if (is_meta && opcode == EPAPER_PROGRAM_INSERT_PREV_FRAME) { + return true; + } + pos = next; + } + + return false; +} + +static void epaper_mark_refresh_success(struct EpaperDriver *driver, + enum EPaperRefreshMode resolved_mode) +{ + epaper_clear_after_wake_policy(driver); + driver->state.last_refresh_mode = resolved_mode; + if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; + } else { + driver->state.fast_refresh_count++; + } +} + +static const struct EPaperProgram *epaper_resolve_refresh_program(struct EpaperDriver *driver, + enum EPaperRefreshMode requested_mode, enum EPaperRefreshMode *resolved_mode) +{ + enum EPaperRefreshMode mode = requested_mode; + const struct EPaperProgram *after_wake_program = epaper_program_for_refresh_mode(driver, mode); + bool after_wake_reseed_allowed = + driver->state.after_wake_pending + && driver->state.after_wake_refresh + == EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS + && driver->state.after_wake_can_reseed_prev + && epaper_program_inserts_prev_frame(after_wake_program); + + // Promote differential refreshes to full when the previous-frame state is not usable. + bool differential_mode = mode == EPAPER_REFRESH_FAST || mode == EPAPER_REFRESH_PARTIAL; + if (differential_mode) { + if ((!driver->state.prev_valid && !after_wake_reseed_allowed) + || driver->state.needs_reseed) { + ESP_LOGI(TAG, "Promoting refresh to FULL: prev_valid=%d, needs_reseed=%d", + driver->state.prev_valid, driver->state.needs_reseed); + mode = EPAPER_REFRESH_FULL; + } else if (driver->state.last_refresh_mode != EPAPER_REFRESH_FULL + && driver->state.last_refresh_mode != mode) { + ESP_LOGI(TAG, "Promoting refresh to FULL: mode transition from %d to %d", + driver->state.last_refresh_mode, mode); + mode = EPAPER_REFRESH_FULL; + } else if (driver->desc->max_fast_refreshes > 0 + && driver->state.fast_refresh_count >= driver->desc->max_fast_refreshes) { + ESP_LOGI(TAG, "Promoting refresh to FULL: fast refresh count %d reached limit %d", + driver->state.fast_refresh_count, driver->desc->max_fast_refreshes); + mode = EPAPER_REFRESH_FULL; + } + } + + if ((mode == EPAPER_REFRESH_FAST || mode == EPAPER_REFRESH_PARTIAL) + && driver->state.after_wake_pending) { + if (driver->state.after_wake_refresh == EPAPER_AFTER_WAKE_REFRESH_FULL) { + ESP_LOGI(TAG, "Promoting refresh to FULL: after-wake policy requires full refresh."); + mode = EPAPER_REFRESH_FULL; + } else if (driver->state.after_wake_refresh + == EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS + && !after_wake_reseed_allowed) { + ESP_LOGI(TAG, + "Promoting refresh to FULL: after-wake program does not reseed previous frame."); + mode = EPAPER_REFRESH_FULL; + } + } + + ESP_LOGI(TAG, "Resolved refresh program: requested=%d, last=%d, resolved=%d", + requested_mode, driver->state.last_refresh_mode, mode); + + *resolved_mode = mode; + return epaper_program_for_refresh_mode(driver, mode); +} + +static bool do_update_descriptor_mono(struct EpaperDriver *driver, + BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) +{ + enum EPaperRefreshMode resolved_mode; + const struct EPaperProgram *program = epaper_resolve_refresh_program(driver, requested_mode, &resolved_mode); + + if (program->bytes == NULL) { + ESP_LOGE(TAG, "E-paper descriptor for %s has no refresh program for mode %d.", + driver->desc->name, resolved_mode); + return false; + } + + const size_t frame_len = mono_frame_bytes(driver); + const int line_bytes = view_mono_line_bytes(driver); + uint8_t *frame_buf = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + uint8_t *line_buf = heap_caps_malloc(line_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!frame_buf || !line_buf)) { + fprintf(stderr, "do_update: failed to alloc e-paper frame buffers\n"); + free(frame_buf); + free(line_buf); + return false; + } + + struct EPaperProgramRun run = { + .plane_buf = frame_buf, + .line_buf = line_buf, + .gray_buf = NULL, + .items = items, + .items_len = items_len, + .capture_next_plane = false, + .capture_succeeded = false, + .mark_prev_valid_requested = false, + .is_4gray = false + }; + + bool ok = epaper_run_driver_program(driver, program, &run); + if (!ok) { + ESP_LOGW(TAG, "E-paper refresh program failed for %s.", driver->desc->name); + } else { + epaper_mark_refresh_success(driver, resolved_mode); + } + + free(frame_buf); + free(line_buf); + return ok; +} + +static bool do_update_descriptor_4gray(struct EpaperDriver *driver, + BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) +{ + enum EPaperRefreshMode resolved_mode; + const struct EPaperProgram *program = epaper_resolve_refresh_program(driver, requested_mode, &resolved_mode); + + if (program->bytes == NULL) { + ESP_LOGE(TAG, "E-paper descriptor for %s has no 4-gray refresh program.", + driver->desc->name); + return false; + } + + const int gray_bytes = (driver->mono_screen.w + 1) / 2; + const size_t frame_len = mono_frame_bytes(driver); + const int line_bytes = view_mono_line_bytes(driver); + bool is_4gray = (resolved_mode == EPAPER_REFRESH_4GRAY); + uint8_t *gray_buf = NULL; + if (is_4gray) { + gray_buf = heap_caps_malloc(gray_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!gray_buf)) { + fprintf(stderr, "do_update: failed to alloc 4-gray buffer\n"); + return false; + } + } + uint8_t *plane_buf = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + uint8_t *line_buf = heap_caps_malloc(line_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!plane_buf || !line_buf)) { + fprintf(stderr, "do_update: failed to alloc plane buffers\n"); + free(gray_buf); + free(plane_buf); + free(line_buf); + return false; + } + + struct EPaperProgramRun run = { + .plane_buf = plane_buf, + .line_buf = line_buf, + .gray_buf = gray_buf, + .items = items, + .items_len = items_len, + .capture_next_plane = false, + .capture_succeeded = false, + .mark_prev_valid_requested = false, + .is_4gray = is_4gray + }; + + if (is_4gray) { + // Neither gray plane represents a cached monochrome optical frame. + // Invalidate before execution, since a failed program may still refresh. + driver->state.prev_valid = false; + driver->state.after_wake_can_reseed_prev = false; + } + + bool ok = epaper_run_driver_program(driver, program, &run); + if (!ok) { + ESP_LOGW(TAG, "E-paper 4-gray refresh program failed for %s.", + driver->desc->name); + } else { + epaper_mark_refresh_success(driver, resolved_mode); + } + + free(gray_buf); + free(plane_buf); + free(line_buf); + return ok; +} + +static bool do_update(Context *ctx, term display_list, term update_opts) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + if (!epaper_wake_if_asleep(driver)) { + return false; + } + + if (driver->desc->controller == EPAPER_CONTROLLER_ACEP7) { + maybe_refresh(ctx); + wait_some_time(ctx); + } + + BaseDisplayItem *items; + size_t len; + if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { + return false; + } + + // Resolve update refresh mode + enum EPaperRefreshMode refresh_mode = driver->desc->default_refresh; + if (update_opts != term_nil()) { + term mode_term = interop_kv_get_value_default(update_opts, ATOM_STR("\x7", "refresh"), term_nil(), ctx->global); + if (mode_term != term_nil()) { + enum EPaperRefreshMode parsed_mode; + if (epaper_parse_refresh_mode(mode_term, ctx, &parsed_mode)) { + if (epaper_refresh_mode_allowed(driver->desc, parsed_mode)) { + refresh_mode = parsed_mode; + } else { + ESP_LOGW(TAG, + "Refresh mode is not declared for %s; using default.", + driver->desc->name); + } + } else { + ESP_LOGE(TAG, "Invalid refresh mode option specified in update."); + } + } + } + + if (driver->desc->controller != EPAPER_CONTROLLER_ACEP7) { + bool ok; + if (driver->desc->palette_size == 4) { + ok = do_update_descriptor_4gray(driver, items, len, refresh_mode); + } else { + ok = do_update_descriptor_mono(driver, items, len, refresh_mode); + } + display_items_delete(items, len); + if (ok) { + update_last_refresh_ts(ctx); + } + return ok; + } + + if (!send_frame_preamble(driver)) { + display_items_delete(items, len); + return false; + } + + // DTM — data transfer to panel memory. + spi_dc_write_command(&driver->bus, 0x10); + + int screen_width = driver->screen.w; + int screen_height = driver->screen.h; + + uint8_t *buf = heap_caps_malloc(screen_width / 2, MALLOC_CAP_DMA); + if (UNLIKELY(!buf)) { + fprintf(stderr, "do_update: failed to alloc buf\n"); + display_items_delete(items, len); + return false; + } + memset(buf, 0x11, screen_width / 2); + + bool transaction_in_progress = false; + + spi_device_acquire_bus(driver->bus.spi_disp.handle, portMAX_DELAY); + + for (int ypos = 0; ypos < screen_height; ypos++) { + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + BaseDisplayItem *row = display_items_row(items, len, ypos); + int xpos = 0; + while (xpos < screen_width) { + int drawn_pixels = epaper_draw_x(&driver->screen, buf, xpos, ypos, row); + xpos += drawn_pixels; + } + + spi_display_dma_write(&driver->bus.spi_disp, screen_width / 2, buf); + transaction_in_progress = true; + } + + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + spi_device_release_bus(driver->bus.spi_disp.handle); + + free(buf); + + send_post_frame_refresh(driver); + epaper_clear_after_wake_policy(driver); + + display_items_delete(items, len); + + update_last_refresh_ts(ctx); + return true; +} + +static term epaper_info_kv(Context *ctx, Heap *heap, AtomString key, + term value, term tail) +{ + term item = term_alloc_tuple(2, heap); + term_put_tuple_element(item, 0, context_make_atom(ctx, key)); + term_put_tuple_element(item, 1, value); + return term_list_prepend(item, tail, heap); +} + +static term epaper_refresh_modes_term(Context *ctx, Heap *heap, + const struct EPaperDesc *desc) +{ + term modes = term_nil(); + for (int mode = EPAPER_REFRESH_MODE_COUNT - 1; mode >= 0; mode--) { + if (epaper_refresh_mode_allowed(desc, (enum EPaperRefreshMode) mode)) { + modes = term_list_prepend( + epaper_refresh_mode_atom(ctx, (enum EPaperRefreshMode) mode), + modes, heap); + } + } + return modes; +} + +static term epaper_sleep_modes_term(Heap *heap, const struct EPaperDesc *desc) +{ + term modes = term_nil(); + for (int i = desc->sleep_mode_count - 1; i >= 0; i--) { + modes = term_list_prepend( + term_from_atom_index((atom_index_t) desc->sleep_modes[i].atom_index), + modes, heap); + } + return modes; +} + +static term epaper_build_info(Context *ctx, struct EpaperDriver *driver, + Heap *heap) +{ + const struct EPaperDesc *desc = driver->desc; + term info = term_nil(); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x6", "asleep"), + driver->state.asleep ? TRUE_ATOM : FALSE_ATOM, info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xB", "sleep_modes"), + epaper_sleep_modes_term(heap, desc), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xF", "default_refresh"), + epaper_refresh_mode_atom(ctx, desc->default_refresh), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xD", "refresh_modes"), + epaper_refresh_modes_term(ctx, heap, desc), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xC", "palette_size"), + term_from_int(desc->palette_size), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x8", "rotation"), + term_from_int(desc->rotation), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xD", "native_height"), + term_from_int(desc->native_height), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xC", "native_width"), + term_from_int(desc->native_width), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x6", "height"), + term_from_int(desc->view_height), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x5", "width"), + term_from_int(desc->view_width), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xA", "controller"), + epaper_controller_atom(ctx, desc->controller), info); + return info; +} + +static void process_message(Message *message, Context *ctx) +{ + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(message->message, &gen_message) != GenCallMessage)) { + fprintf(stderr, "Received invalid message."); + AVM_ABORT(); + } + + term req = gen_message.req; + if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 1)) { + AVM_ABORT(); + } + int req_arity = term_get_tuple_arity(req); + term cmd = term_get_tuple_element(req, 0); + term reply_status = OK_ATOM; + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + + if (cmd == context_make_atom(ctx, "\x6" + "update")) { + + bool update_ok = false; + if (req_arity < 2) { + ESP_LOGE(TAG, "Invalid update request: expected {update, Items} or {update, Items, Opts}."); + } else { + term display_list = term_get_tuple_element(req, 1); + term update_opts = term_nil(); + if (req_arity >= 3) { + update_opts = term_get_tuple_element(req, 2); + } + update_ok = do_update(ctx, display_list, update_opts); + } + driver->state.last_op_ok = update_ok; + + // Reply already sent at enqueue time by + // try_pre_ack_render_cmd in display_task.c. Sending another + // reply here would leak a stray `{Ref, ok}' into the caller's + // mailbox after port:call has already returned. + return; + + } else if (cmd == context_make_atom(ctx, "\x5" "sleep")) { + if (req_arity != 2) { + ESP_LOGE(TAG, "Invalid sleep request: expected {sleep, Mode}."); + reply_status = ERROR_ATOM; + } else { + reply_status = do_sleep(ctx, term_get_tuple_element(req, 1)) + ? OK_ATOM + : ERROR_ATOM; + } + + } else if (cmd == context_make_atom(ctx, "\x4" "wake")) { + reply_status = do_wake(ctx) ? OK_ATOM : ERROR_ATOM; + + } else if (cmd == context_make_atom(ctx, "\x9" "wait_idle")) { + reply_status = driver->state.last_op_ok ? OK_ATOM : ERROR_ATOM; + + } else if (cmd == context_make_atom(ctx, "\x4" "info") + || cmd == context_make_atom(ctx, "\xC" "capabilities")) { + BEGIN_WITH_STACK_HEAP( + TUPLE_SIZE(2) + REF_SIZE + + 11 * (TUPLE_SIZE(2) + CONS_SIZE) + + (EPAPER_REFRESH_MODE_COUNT + EPAPER_MAX_SLEEP_MODES) * CONS_SIZE + + 8, + heap); + term return_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(return_tuple, 0, gen_message.ref); + term_put_tuple_element(return_tuple, 1, epaper_build_info(ctx, driver, &heap)); + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); + return; + + } else if (cmd == globalcontext_make_atom(ctx->global, "\xA" "load_image")) { + handle_load_image(req, gen_message.ref, gen_message.pid, ctx); + return; + + } else { +#if REPORT_UNEXPECTED_MSGS + fprintf(stderr, "display: "); + term_display(stderr, req, ctx); + fprintf(stderr, "\n"); +#endif + } + + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE, heap); + term return_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(return_tuple, 0, gen_message.ref); + term_put_tuple_element(return_tuple, 1, reply_status); + + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); +} + +static void clear_screen_internal(Context *ctx, int color, + bool clear_after_wake_pending) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + if (!epaper_wake_if_asleep(driver)) { + return; + } + + int screen_width = driver->screen.w; + int screen_height = driver->screen.h; + + if (!send_frame_preamble(driver)) { + return; + } + + spi_dc_write_command(&driver->bus, 0x10); + + uint8_t *buf = heap_caps_malloc(screen_width / 2, MALLOC_CAP_DMA); + if (UNLIKELY(!buf)) { + fprintf(stderr, "clear_screen: failed to alloc buf\n"); + return; + } + + bool transaction_in_progress = false; + + spi_device_acquire_bus(driver->bus.spi_disp.handle, portMAX_DELAY); + + for (int i = 0; i < screen_height; i++) { + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + // memset inside the loop so every scanline carries fresh data, + // avoiding artefacts if a prior scanline left stale bytes. + memset(buf, color | (color << 4), screen_width / 2); + spi_display_dma_write(&driver->bus.spi_disp, screen_width / 2, buf); + transaction_in_progress = true; + } + + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + spi_device_release_bus(driver->bus.spi_disp.handle); + + free(buf); + + send_post_frame_refresh(driver); + if (clear_after_wake_pending) { + epaper_clear_after_wake_policy(driver); + } +} + +static bool epaper_term_bool(term value, bool *out) +{ + if (value == TRUE_ATOM) { + *out = true; + return true; + } + if (value == FALSE_ATOM) { + *out = false; + return true; + } + return false; +} + +static bool epaper_parse_int_field(term container, Context *ctx, + AtomString key, int *out) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + return true; + } + if (term_is_integer(value)) { + avm_int_t parsed = term_to_int(value); + if (parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + *out = (int) parsed; + return true; + } + return false; +} + +static bool epaper_parse_bool_field(term container, Context *ctx, + AtomString key, bool *out) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + return true; + } + bool parsed; + if (epaper_term_bool(value, &parsed)) { + *out = parsed; + return true; + } + return false; +} + +static bool epaper_copy_binary_field(term container, Context *ctx, + AtomString key, uint8_t **owned_bytes, const uint8_t **bytes, size_t *len) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + *owned_bytes = NULL; + *bytes = NULL; + *len = 0; + return true; + } + + if (!term_is_binary(value)) { + return false; + } + + size_t value_len = term_binary_size(value); + if (value_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } + uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); + if (copy == NULL) { + return false; + } + + memcpy(copy, term_binary_data(value), value_len); + *owned_bytes = copy; + *bytes = copy; + *len = value_len; + return true; +} + +static bool epaper_copy_binary_term(term value, + uint8_t **owned_bytes, const uint8_t **bytes, size_t *len) +{ + if (!term_is_binary(value)) { + return false; + } + + size_t value_len = term_binary_size(value); + if (value_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } + uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); + if (copy == NULL) { + return false; + } + + memcpy(copy, term_binary_data(value), value_len); + *owned_bytes = copy; + *bytes = copy; + *len = value_len; + return true; +} + +static void epaper_free_descriptor_override(struct EpaperDriver *driver) +{ + free(driver->term_desc_name); + driver->term_desc_name = NULL; + free(driver->term_init_seq_bytes); + driver->term_init_seq_bytes = NULL; + free(driver->term_frame_preamble_seq_bytes); + driver->term_frame_preamble_seq_bytes = NULL; + + for (int i = 0; i < EPAPER_TERM_PROGRAM_COUNT; i++) { + free(driver->term_program_bytes[i]); + driver->term_program_bytes[i] = NULL; + } + + for (int i = 0; i < EPAPER_MAX_SLEEP_MODES; i++) { + free(driver->term_sleep_enter_bytes[i]); + driver->term_sleep_enter_bytes[i] = NULL; + free(driver->term_sleep_wake_bytes[i]); + driver->term_sleep_wake_bytes[i] = NULL; + } + + for (int i = 0; i < EPAPER_MAX_LUT_SLOTS; i++) { + free(driver->term_lut_bytes[i]); + driver->term_lut_bytes[i] = NULL; + } +} + +static void epaper_free_init_failed_driver(struct EpaperDriver *driver, + bool spi_device_added) +{ + if (driver == NULL) { + return; + } + + if (driver->ctx != NULL && driver->ctx->platform_data == &driver->display_args) { + driver->ctx->platform_data = NULL; + } + + if (driver->display_args.messages_queue != NULL) { + vQueueDelete(driver->display_args.messages_queue); + driver->display_args.messages_queue = NULL; + } + + if (spi_device_added && driver->bus.spi_disp.handle != NULL) { + spi_bus_remove_device(driver->bus.spi_disp.handle); + driver->bus.spi_disp.handle = NULL; + } + + free(driver->prev_frame); + driver->prev_frame = NULL; + epaper_free_descriptor_override(driver); + // Nobody can talk to the panel through this port any more, so leave the + // rail off instead of powering a controller that is not being driven. + epaper_set_panel_power(driver, false); + free(driver); +} + +static bool epaper_parse_controller(term val, Context *ctx, enum EPaperController *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "ssd16xx"))) { + *out = EPAPER_CONTROLLER_SSD16XX; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "jd79656"))) { + *out = EPAPER_CONTROLLER_JD79656; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8151"))) { + *out = EPAPER_CONTROLLER_UC8151; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8276"))) { + *out = EPAPER_CONTROLLER_UC8276; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8175"))) { + *out = EPAPER_CONTROLLER_UC8175; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x5", "acep7"))) { + *out = EPAPER_CONTROLLER_ACEP7; + return true; + } + return false; +} + +static bool epaper_parse_layout(term val, Context *ctx, struct EPaperFrameLayout *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "row_msb"))) { + out->byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_MSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "row_lsb"))) { + out->byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_LSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "column_msb"))) { + out->byte_order = EPAPER_BYTE_ORDER_COLUMN_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_MSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "column_lsb"))) { + out->byte_order = EPAPER_BYTE_ORDER_COLUMN_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_LSB_LEFT; + return true; + } + return false; +} + +static bool epaper_parse_polarity(term val, Context *ctx, enum EPaperPolarity *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "white_1"))) { + *out = EPAPER_POLARITY_WHITE_IS_1; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "black_1"))) { + *out = EPAPER_POLARITY_BLACK_IS_1; + return true; + } + return false; +} + +static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefreshMode *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x4", "full"))) { + *out = EPAPER_REFRESH_FULL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "fast"))) { + *out = EPAPER_REFRESH_FAST; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "partial"))) { + *out = EPAPER_REFRESH_PARTIAL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x5", "4gray"))) { + *out = EPAPER_REFRESH_4GRAY; + return true; + } + return false; +} + +static bool epaper_parse_refresh_modes(term modes, Context *ctx, uint8_t *out) +{ + if (!term_is_nonempty_list(modes)) { + return false; + } + + uint8_t mask = 0; + term t = modes; + while (term_is_nonempty_list(t)) { + enum EPaperRefreshMode mode; + if (!epaper_parse_refresh_mode(term_get_list_head(t), ctx, &mode)) { + return false; + } + uint8_t bit = epaper_refresh_mode_bit(mode); + if ((mask & bit) != 0) { + return false; + } + mask |= bit; + t = term_get_list_tail(t); + } + + if (t != term_nil() || (mask & EPAPER_REFRESH_MODE_FULL) == 0) { + return false; + } + + *out = mask; + return true; +} + +static bool epaper_parse_controller_ram_policy(term val, Context *ctx, + enum EPaperControllerRamPolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "unknown"))) { + *out = EPAPER_CONTROLLER_RAM_UNKNOWN; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x8", "retained"))) { + *out = EPAPER_CONTROLLER_RAM_RETAINED; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "lost"))) { + *out = EPAPER_CONTROLLER_RAM_LOST; + return true; + } + return false; +} + +static bool epaper_parse_host_prev_frame_policy(term val, Context *ctx, + enum EPaperHostPrevFramePolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x8", "preserve"))) { + *out = EPAPER_HOST_PREV_FRAME_PRESERVE; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "invalidate"))) { + *out = EPAPER_HOST_PREV_FRAME_INVALIDATE; + return true; + } + return false; +} + +static bool epaper_parse_after_wake_refresh_policy(term val, Context *ctx, + enum EPaperAfterWakeRefreshPolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x5", "allow"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_ALLOW; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "full"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_FULL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x18", "allow_if_program_reseeds"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS; + return true; + } + return false; +} + +static bool epaper_parse_palette(term val, Context *ctx, + const uint8_t (**palette)[3], int *palette_size) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x5", "acep7")) + || val == context_make_atom(ctx, ATOM_STR("\x6", "acep7c"))) { + *palette = epaper_acep_palette; + *palette_size = 7; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "gdep073e01"))) { + *palette = epaper_gdep073e01_palette; + *palette_size = 7; + return true; + } + return false; +} + +static bool epaper_parse_sleep_modes(struct EpaperDriver *driver, + term descriptor, Context *ctx) +{ + term sleep_modes = interop_kv_get_value_default( + descriptor, ATOM_STR("\xB", "sleep_modes"), term_nil(), ctx->global); + if (sleep_modes == term_nil()) { + driver->term_desc.sleep_mode_count = 0; + return true; + } + + int mode_count = 0; + term t = sleep_modes; + while (term_is_nonempty_list(t)) { + if (mode_count >= EPAPER_MAX_SLEEP_MODES) { + ESP_LOGE(TAG, "Too many e-paper sleep modes; maximum is %d.", + EPAPER_MAX_SLEEP_MODES); + return false; + } + + term entry = term_get_list_head(t); + if (!term_is_tuple(entry) || term_get_tuple_arity(entry) != 2) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode entry."); + return false; + } + + term mode_atom = term_get_tuple_element(entry, 0); + term props = term_get_tuple_element(entry, 1); + if (!term_is_atom(mode_atom) + || (props != term_nil() && !term_is_nonempty_list(props))) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode name or properties."); + return false; + } + uint32_t mode_atom_index = (uint32_t) term_to_atom_index(mode_atom); + for (int i = 0; i < mode_count; i++) { + if (driver->term_desc.sleep_modes[i].atom_index == mode_atom_index) { + ESP_LOGE(TAG, "Duplicate e-paper sleep mode."); + return false; + } + } + + struct EPaperSleepMode *mode = &driver->term_desc.sleep_modes[mode_count]; + memset(mode, 0, sizeof(*mode)); + mode->atom_index = mode_atom_index; + mode->wake_policy = EPAPER_WAKE_RESET_INIT; + mode->controller_ram = EPAPER_CONTROLLER_RAM_UNKNOWN; + mode->host_prev_frame = EPAPER_HOST_PREV_FRAME_INVALIDATE; + mode->after_wake_refresh = EPAPER_AFTER_WAKE_REFRESH_FULL; + + term enter = interop_kv_get_value_default( + props, ATOM_STR("\x5", "enter"), term_nil(), ctx->global); + if (enter == term_nil() + || !epaper_copy_binary_term(enter, + &driver->term_sleep_enter_bytes[mode_count], + &mode->enter.bytes, &mode->enter.len)) { + ESP_LOGE(TAG, "Invalid or missing e-paper sleep mode enter program."); + return false; + } + + term wake = interop_kv_get_value_default( + props, ATOM_STR("\x4", "wake"), term_nil(), ctx->global); + if (wake != term_nil()) { + if (term_is_binary(wake)) { + if (!epaper_copy_binary_term(wake, + &driver->term_sleep_wake_bytes[mode_count], + &mode->wake.bytes, &mode->wake.len)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode wake program."); + return false; + } + mode->wake_policy = EPAPER_WAKE_PROGRAM; + } else if (wake == context_make_atom(ctx, ATOM_STR("\x4", "init"))) { + mode->wake_policy = EPAPER_WAKE_INIT; + } else if (wake == context_make_atom(ctx, ATOM_STR("\xA", "reset_init"))) { + mode->wake_policy = EPAPER_WAKE_RESET_INIT; + } else { + ESP_LOGE(TAG, "Invalid e-paper sleep mode wake policy."); + return false; + } + } + + term controller_ram = interop_kv_get_value_default( + props, ATOM_STR("\xE", "controller_ram"), term_nil(), ctx->global); + if (controller_ram != term_nil() + && !epaper_parse_controller_ram_policy(controller_ram, ctx, &mode->controller_ram)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode controller_ram policy."); + return false; + } + + term host_prev_frame = interop_kv_get_value_default( + props, ATOM_STR("\xF", "host_prev_frame"), term_nil(), ctx->global); + if (host_prev_frame != term_nil() + && !epaper_parse_host_prev_frame_policy(host_prev_frame, ctx, + &mode->host_prev_frame)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode host_prev_frame policy."); + return false; + } + + term after_wake_refresh = interop_kv_get_value_default( + props, ATOM_STR("\x12", "after_wake_refresh"), term_nil(), ctx->global); + if (after_wake_refresh != term_nil() + && !epaper_parse_after_wake_refresh_policy(after_wake_refresh, ctx, + &mode->after_wake_refresh)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode after_wake_refresh policy."); + return false; + } + + mode_count++; + t = term_get_list_tail(t); + } + + if (t != term_nil()) { + ESP_LOGE(TAG, "Invalid e-paper sleep_modes list."); + return false; + } + + driver->term_desc.sleep_mode_count = mode_count; + return true; +} + +static bool epaper_validate_descriptor_refresh_modes(const struct EPaperDesc *desc) +{ + if ((desc->refresh_mode_mask & epaper_refresh_mode_bit(desc->default_refresh)) == 0) { + ESP_LOGE(TAG, "default_refresh is not listed in refresh_modes."); + return false; + } + + if (desc->controller == EPAPER_CONTROLLER_ACEP7) { + if (desc->refresh_mode_mask != EPAPER_REFRESH_MODE_FULL) { + ESP_LOGE(TAG, "ACeP e-paper descriptors currently support only full refresh."); + return false; + } + return true; + } + + for (int mode = 0; mode < EPAPER_REFRESH_MODE_COUNT; mode++) { + if (!epaper_refresh_mode_allowed(desc, (enum EPaperRefreshMode) mode)) { + continue; + } + const struct EPaperProgram *program; + switch ((enum EPaperRefreshMode) mode) { + case EPAPER_REFRESH_FULL: + program = &desc->program_full; + break; + case EPAPER_REFRESH_FAST: + program = &desc->program_fast; + break; + case EPAPER_REFRESH_PARTIAL: + program = &desc->program_partial; + break; + case EPAPER_REFRESH_4GRAY: + program = &desc->program_4gray; + break; + default: + program = NULL; + break; + } + if (program->bytes == NULL || program->len == 0) { + ESP_LOGE(TAG, "refresh_modes declares a mode without a program."); + return false; + } + } + + return true; +} + +static bool epaper_validate_descriptor_programs(const struct EPaperDesc *desc) +{ + if (!epaper_validate_program(&desc->init, false) + || !epaper_validate_program(&desc->program_full, true) + || !epaper_validate_program(&desc->program_fast, true) + || !epaper_validate_program(&desc->program_partial, true) + || !epaper_validate_program(&desc->program_4gray, true)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor bytecode program."); + return false; + } + + for (int i = 0; i < desc->sleep_mode_count; i++) { + const struct EPaperSleepMode *mode = &desc->sleep_modes[i]; + if (mode->enter.bytes == NULL || mode->enter.len == 0 + || !epaper_validate_program(&mode->enter, false) + || (mode->wake_policy == EPAPER_WAKE_PROGRAM + && (mode->wake.bytes == NULL || mode->wake.len == 0 + || !epaper_validate_program(&mode->wake, false)))) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode bytecode program."); + return false; + } + } + + return true; +} + +static bool epaper_validate_descriptor_sequences(const struct EPaperDesc *desc) +{ + if (!epaper_validate_init_seq(desc->init_seq, desc->init_seq_len) + || !epaper_validate_init_seq(desc->frame_preamble_seq, + desc->frame_preamble_seq_len)) { + ESP_LOGE(TAG, "Invalid e-paper ACeP raw command sequence."); + return false; + } + return true; +} + +static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) +{ + if (desc->native_width <= 0 || desc->native_height <= 0 + || desc->view_width <= 0 || desc->view_height <= 0) { + ESP_LOGE(TAG, "Invalid e-paper geometry: native=%dx%d view=%dx%d.", + desc->native_width, desc->native_height, + desc->view_width, desc->view_height); + return false; + } + + if (desc->native_width > INT_MAX - 7 || desc->native_height > INT_MAX - 7 + || desc->view_width > INT_MAX - 7 || desc->view_height > INT_MAX - 7) { + ESP_LOGE(TAG, "E-paper dimensions exceed safe rounding limits."); + return false; + } + if (desc->controller != EPAPER_CONTROLLER_ACEP7) { + bool column_major = desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR; + size_t chunk_bytes = column_major + ? ((size_t) desc->native_height + 7) / 8 + : ((size_t) desc->native_width + 7) / 8; + size_t chunk_count = column_major + ? (size_t) desc->native_width + : (size_t) desc->native_height; + // Keep the ESP32 allocation contract even when validated on a 64-bit host. + if (chunk_count > UINT32_MAX / chunk_bytes) { + ESP_LOGE(TAG, "E-paper frame size exceeds 32-bit allocation limits."); + return false; + } + } + + if (desc->controller == EPAPER_CONTROLLER_ACEP7 + && ((desc->native_width & 1) != 0 || (desc->view_width & 1) != 0)) { + ESP_LOGE(TAG, "ACeP e-paper width must be even: native=%d view=%d.", + desc->native_width, desc->view_width); + return false; + } + + if (desc->controller == EPAPER_CONTROLLER_ACEP7 + && (desc->rotation != 0 + || desc->view_width != desc->native_width + || desc->view_height != desc->native_height)) { + ESP_LOGE(TAG, + "ACeP e-paper descriptors only support native orientation: native=%dx%d view=%dx%d rotation=%d.", + desc->native_width, desc->native_height, + desc->view_width, desc->view_height, desc->rotation); + return false; + } + + switch (desc->rotation) { + case 0: + case 180: + if (desc->view_width == desc->native_width + && desc->view_height == desc->native_height) { + return true; + } + break; + case 90: + case 270: + if (desc->view_width == desc->native_height + && desc->view_height == desc->native_width) { + return true; + } + break; + default: + ESP_LOGE(TAG, "Invalid e-paper rotation %d; expected 0, 90, 180, or 270.", + desc->rotation); + return false; + } + + ESP_LOGE(TAG, "Invalid e-paper geometry for rotation %d: native=%dx%d view=%dx%d.", + desc->rotation, desc->native_width, desc->native_height, + desc->view_width, desc->view_height); + return false; +} + +static bool epaper_validate_descriptor_layout(const struct EPaperDesc *desc) +{ + switch (desc->controller) { + case EPAPER_CONTROLLER_JD79656: + return desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR; + case EPAPER_CONTROLLER_SSD16XX: + case EPAPER_CONTROLLER_UC8151: + case EPAPER_CONTROLLER_UC8276: + case EPAPER_CONTROLLER_UC8175: + return desc->layout.byte_order == EPAPER_BYTE_ORDER_ROW_MAJOR; + case EPAPER_CONTROLLER_ACEP7: + return true; + default: + return false; + } +} + +static bool epaper_validate_descriptor_semantics(const struct EPaperDesc *desc) +{ + if (!epaper_validate_descriptor_geometry(desc) + || !epaper_validate_descriptor_refresh_modes(desc) + || !epaper_validate_descriptor_programs(desc) + || !epaper_validate_descriptor_sequences(desc)) { + return false; + } + + if (!epaper_validate_descriptor_layout(desc)) { + ESP_LOGE(TAG, "Invalid frame_layout for e-paper controller."); + return false; + } + if (desc->spi_clock_hz <= 0 || desc->spi_clock_hz > EPAPER_MAX_SPI_CLOCK_HZ) { + ESP_LOGE(TAG, "Invalid e-paper spi_clock_hz: %d.", desc->spi_clock_hz); + return false; + } + if ((desc->busy_idle_level != 0 && desc->busy_idle_level != 1) + || (desc->post_power_off_busy_level != 0 + && desc->post_power_off_busy_level != 1)) { + ESP_LOGE(TAG, "Invalid e-paper BUSY level in descriptor."); + return false; + } + if (desc->palette_size != 2 && desc->palette_size != 4 + && desc->palette_size != 7) { + ESP_LOGE(TAG, "Invalid e-paper palette_size: %d.", desc->palette_size); + return false; + } + if (desc->controller == EPAPER_CONTROLLER_ACEP7) { + if (desc->palette == NULL || desc->palette_size != 7) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires a known 7-color palette."); + return false; + } + if ((desc->init.bytes == NULL || desc->init.len == 0) + && (desc->init_seq == NULL || desc->init_seq_len == 0)) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires an init program or init_seq."); + return false; + } + } else { + if (desc->init.bytes == NULL || desc->init.len == 0) { + ESP_LOGE(TAG, "E-paper descriptor requires an init program."); + return false; + } + if (desc->palette_size == 7) { + ESP_LOGE(TAG, "7-color palette_size is only valid for ACeP descriptors."); + return false; + } + if (desc->palette_size == 4 + && desc->controller != EPAPER_CONTROLLER_SSD16XX + && desc->controller != EPAPER_CONTROLLER_UC8276) { + ESP_LOGE(TAG, "4-gray palette_size is not supported by this controller."); + return false; + } + } + if (epaper_refresh_mode_allowed(desc, EPAPER_REFRESH_4GRAY) + && desc->palette_size != 4) { + ESP_LOGE(TAG, "4gray refresh mode requires palette_size 4."); + return false; + } + if (desc->full_expected_ms < 0 || desc->fast_expected_ms < 0 + || desc->poll_interval_ms <= 0 || desc->timeout_ms <= 0 + || desc->max_fast_refreshes < 0 + || desc->periodic_refresh_interval < 0) { + ESP_LOGE(TAG, "Invalid e-paper timing or ghosting value."); + return false; + } + return true; +} + +static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, + term opts, Context *ctx) +{ + term descriptor = interop_kv_get_value_default( + opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); + if (descriptor == term_nil()) { + return true; + } + if (!term_is_nonempty_list(descriptor)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor: expected keyword list."); + return false; + } + + memset(driver->term_program_bytes, 0, sizeof(driver->term_program_bytes)); + memset(driver->term_sleep_enter_bytes, 0, sizeof(driver->term_sleep_enter_bytes)); + memset(driver->term_sleep_wake_bytes, 0, sizeof(driver->term_sleep_wake_bytes)); + memset(driver->term_lut_bytes, 0, sizeof(driver->term_lut_bytes)); + driver->term_init_seq_bytes = NULL; + driver->term_frame_preamble_seq_bytes = NULL; + driver->term_desc_name = NULL; + driver->term_desc = *driver->desc; + memset(driver->term_desc.sleep_modes, 0, sizeof(driver->term_desc.sleep_modes)); + driver->term_desc.sleep_mode_count = 0; + + // Check version first + int descriptor_version = 0; + if (!epaper_parse_int_field(descriptor, ctx, + ATOM_STR("\x12", "descriptor_version"), + &descriptor_version)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor_version."); + return false; + } + driver->term_desc.descriptor_version = descriptor_version; + if (driver->term_desc.descriptor_version != 3) { + ESP_LOGE(TAG, "Unsupported e-paper descriptor version %d. Only version 3 is supported.", + driver->term_desc.descriptor_version); + return false; + } + + term name = interop_kv_get_value_default( + descriptor, ATOM_STR("\x4", "name"), term_nil(), ctx->global); + if (name != term_nil()) { + int name_ok; + char *parsed_name = interop_term_to_string(name, &name_ok); + if (name_ok && parsed_name != NULL) { + driver->term_desc_name = parsed_name; + driver->term_desc.name = parsed_name; + } else { + free(parsed_name); + } + } + + // Geometry + if (!epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "native_width"), + &driver->term_desc.native_width) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xD", "native_height"), + &driver->term_desc.native_height) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xA", "view_width"), + &driver->term_desc.view_width) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xB", "view_height"), + &driver->term_desc.view_height) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x8", "rotation"), + &driver->term_desc.rotation) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "spi_clock_hz"), + &driver->term_desc.spi_clock_hz) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xF", "busy_idle_level"), + &driver->term_desc.busy_idle_level)) { + ESP_LOGE(TAG, "Invalid integer field in e-paper descriptor."); + return false; + } + + if (!epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x10", "use_gpio_pullups"), + &driver->term_desc.use_gpio_pullups)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor use_gpio_pullups."); + return false; + } + + if (!epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "palette_size"), + &driver->term_desc.palette_size)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor palette_size."); + return false; + } + + term palette_term = interop_kv_get_value_default( + descriptor, ATOM_STR("\x7", "palette"), term_nil(), ctx->global); + if (palette_term != term_nil()) { + const uint8_t (*palette)[3]; + int palette_size; + if (!epaper_parse_palette(palette_term, ctx, &palette, &palette_size)) { + ESP_LOGE(TAG, "Invalid palette in e-paper descriptor."); + return false; + } + driver->term_desc.palette = palette; + driver->term_desc.palette_size = palette_size; + } + + // Parse Controller + term controller_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xA", "controller"), term_nil(), ctx->global); + if (controller_term != term_nil()) { + enum EPaperController ctrl; + if (!epaper_parse_controller(controller_term, ctx, &ctrl)) { + ESP_LOGE(TAG, "Invalid controller in descriptor."); + return false; + } + driver->term_desc.controller = ctrl; + } + + // Parse Frame Layout + term layout_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xC", "frame_layout"), term_nil(), ctx->global); + if (layout_term != term_nil()) { + struct EPaperFrameLayout layout; + if (!epaper_parse_layout(layout_term, ctx, &layout)) { + ESP_LOGE(TAG, "Invalid frame_layout in descriptor."); + return false; + } + driver->term_desc.layout = layout; + } + + // Parse Polarity + term polarity_term = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "polarity"), term_nil(), ctx->global); + if (polarity_term != term_nil()) { + enum EPaperPolarity polarity; + if (!epaper_parse_polarity(polarity_term, ctx, &polarity)) { + ESP_LOGE(TAG, "Invalid polarity in descriptor."); + return false; + } + driver->term_desc.layout.polarity = polarity; + } + + // Parse Refresh Modes + term refresh_modes_term = interop_kv_get_value_default( + descriptor, ATOM_STR("\xD", "refresh_modes"), term_nil(), ctx->global); + if (refresh_modes_term == term_nil() + || !epaper_parse_refresh_modes(refresh_modes_term, ctx, + &driver->term_desc.refresh_mode_mask)) { + ESP_LOGE(TAG, "Invalid refresh_modes in descriptor."); + return false; + } + + // Parse Default Refresh + term default_refresh_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xF", "default_refresh"), term_nil(), ctx->global); + if (default_refresh_term != term_nil()) { + enum EPaperRefreshMode default_refresh; + if (!epaper_parse_refresh_mode(default_refresh_term, ctx, &default_refresh)) { + ESP_LOGE(TAG, "Invalid default_refresh mode in descriptor."); + return false; + } + driver->term_desc.default_refresh = default_refresh; + } + if (!epaper_refresh_mode_allowed(&driver->term_desc, + driver->term_desc.default_refresh)) { + ESP_LOGE(TAG, "default_refresh is not listed in refresh_modes."); + return false; + } + + // Parse Programs Map + term programs_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "programs"), term_nil(), ctx->global); + if (programs_map != term_nil()) { + if (!epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "init"), + &driver->term_program_bytes[0], + &driver->term_desc.init.bytes, &driver->term_desc.init.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "full"), + &driver->term_program_bytes[1], + &driver->term_desc.program_full.bytes, &driver->term_desc.program_full.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "fast"), + &driver->term_program_bytes[2], + &driver->term_desc.program_fast.bytes, &driver->term_desc.program_fast.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x7", "partial"), + &driver->term_program_bytes[3], + &driver->term_desc.program_partial.bytes, &driver->term_desc.program_partial.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x5", "4gray"), + &driver->term_program_bytes[4], + &driver->term_desc.program_4gray.bytes, &driver->term_desc.program_4gray.len)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor program binary."); + return false; + } + } + + if (!epaper_parse_sleep_modes(driver, descriptor, ctx)) { + return false; + } + + if (!epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "init_seq"), + &driver->term_init_seq_bytes, + &driver->term_desc.init_seq, &driver->term_desc.init_seq_len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x12", "frame_preamble_seq"), + &driver->term_frame_preamble_seq_bytes, + &driver->term_desc.frame_preamble_seq, &driver->term_desc.frame_preamble_seq_len)) { + ESP_LOGE(TAG, "Invalid e-paper ACeP sequence binary."); + return false; + } + + int refresh_data_byte = driver->term_desc.refresh_data_byte; + if (!epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x1B", "init_wait_busy_between_cmds"), + &driver->term_desc.init_wait_busy_between_cmds) + || !epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x10", "refresh_has_data"), + &driver->term_desc.refresh_has_data) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x11", "refresh_data_byte"), + &refresh_data_byte) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x19", "post_power_off_busy_level"), + &driver->term_desc.post_power_off_busy_level) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x19", "periodic_refresh_interval"), + &driver->term_desc.periodic_refresh_interval)) { + ESP_LOGE(TAG, "Invalid ACeP e-paper descriptor field."); + return false; + } + if (refresh_data_byte < 0 || refresh_data_byte > UINT8_MAX) { + ESP_LOGE(TAG, "Invalid refresh_data_byte in e-paper descriptor."); + return false; + } + driver->term_desc.refresh_data_byte = (uint8_t) refresh_data_byte; + + // Parse Timing Map + term timing_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x6", "timing"), term_nil(), ctx->global); + if (timing_map != term_nil()) { + if (!epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "full_expected_ms"), &driver->term_desc.full_expected_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "fast_expected_ms"), &driver->term_desc.fast_expected_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "poll_interval_ms"), &driver->term_desc.poll_interval_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\n", "timeout_ms"), &driver->term_desc.timeout_ms)) { + ESP_LOGE(TAG, "Invalid field in e-paper descriptor timing."); + return false; + } + } + + // Parse Ghosting Map + term ghosting_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "ghosting"), term_nil(), ctx->global); + if (ghosting_map != term_nil()) { + if (!epaper_parse_int_field(ghosting_map, ctx, ATOM_STR("\x12", "max_fast_refreshes"), &driver->term_desc.max_fast_refreshes) + || !epaper_parse_bool_field(ghosting_map, ctx, ATOM_STR("\x11", "reseed_on_timeout"), &driver->term_desc.reseed_on_timeout)) { + ESP_LOGE(TAG, "Invalid field in e-paper descriptor ghosting."); + return false; + } + } + + // Parse LUTs + if (!epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "lut_full"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_FULL], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FULL].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FULL].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\xB", "lut_partial"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_PARTIAL], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_PARTIAL].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_PARTIAL].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x9", "lut_4gray"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_4GRAY], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_4GRAY].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_4GRAY].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "lut_fast"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_FAST], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FAST].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FAST].len)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor LUT binary."); + return false; + } + + if (driver->term_desc.palette_size <= 0) { + driver->term_desc.palette_size = 2; + } + + // Setup 4-gray palette if relevant + if ((driver->term_desc.controller == EPAPER_CONTROLLER_SSD16XX + || driver->term_desc.controller == EPAPER_CONTROLLER_UC8276) + && driver->term_desc.palette_size == 4) { + driver->term_desc.palette = epaper_ssd1680_4gray_palette; + } + if (!epaper_validate_descriptor_semantics(&driver->term_desc)) { + return false; + } + + driver->desc = &driver->term_desc; + ESP_LOGI(TAG, "Using Erlang e-paper descriptor: %s", driver->desc->name); + return true; +} + +static bool display_spi_init(Context *ctx, term opts) +{ + term descriptor_term = interop_kv_get_value_default( + opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); + + if (descriptor_term == term_nil()) { + ESP_LOGE(TAG, "Failed init: missing e-paper descriptor."); + return false; + } + + const struct EPaperDesc *desc = &epaper_desc_term_default; + + struct EpaperDriver *driver = calloc(1, sizeof(struct EpaperDriver)); + if (UNLIKELY(!driver)) { + ESP_LOGE(TAG, "Failed init: unable to allocate driver state."); + return false; + } + // No panel power switch unless the port asks for one: GPIO 0 must never be + // driven by an unconfigured rail. + driver->panel_power_gpio = -1; + + driver->desc = desc; + driver->ctx = ctx; + if (!epaper_parse_descriptor_override(driver, opts, ctx)) { + ESP_LOGE(TAG, "Failed init: invalid descriptor override."); + epaper_free_init_failed_driver(driver, false); + return false; + } + desc = driver->desc; + if (!epaper_validate_descriptor_geometry(desc)) { + ESP_LOGE(TAG, "Failed init: invalid descriptor geometry for '%s'.", desc->name); + epaper_free_init_failed_driver(driver, false); + return false; + } + if (epaper_desc_requires_program(desc) && desc->program_full.bytes == NULL) { + ESP_LOGE(TAG, + "Failed init: e-paper descriptor '%s' requires a full refresh program.", + desc->name); + epaper_free_init_failed_driver(driver, false); + return false; + } + + driver->screen.w = desc->view_width; + driver->screen.h = desc->view_height; + driver->screen.palette = desc->palette; + driver->screen.palette_size = desc->palette_size; + driver->mono_screen.w = desc->view_width; + driver->mono_screen.h = desc->view_height; + driver->display_args.messages_queue = xQueueCreate(32, sizeof(Message *)); + if (driver->display_args.messages_queue == NULL) { + ESP_LOGE(TAG, "Failed init: unable to allocate display queue."); + epaper_free_init_failed_driver(driver, false); + return false; + } + driver->display_args.process_message_fn = process_message; + driver->display_args.ctx = ctx; + ctx->platform_data = &driver->display_args; + + struct SPIDisplayConfig spi_config; + spi_display_init_config(&spi_config); + spi_config.clock_speed_hz = desc->spi_clock_hz; + if (!spi_display_parse_config(&spi_config, opts, ctx->global)) { + ESP_LOGE(TAG, "Failed init: invalid SPI display configuration."); + epaper_free_init_failed_driver(driver, false); + return false; + } + spi_display_init(&driver->bus.spi_disp, &spi_config); + bool spi_device_added = true; + + bool ok = display_common_gpio_from_opts(opts, ATOM_STR("\x4", "busy"), &driver->busy_gpio, ctx->global); ok = ok && display_common_gpio_from_opts(opts, ATOM_STR("\x2", "dc"), &driver->bus.dc_gpio, ctx->global); ok = ok && display_common_gpio_from_opts(opts, ATOM_STR("\x5", "reset"), &driver->reset_gpio, ctx->global); if (UNLIKELY(!ok)) { ESP_LOGE(TAG, "Failed init: invalid display GPIOs."); - return; + epaper_free_init_failed_driver(driver, spi_device_added); + return false; } gpio_set_direction(driver->reset_gpio, GPIO_MODE_OUTPUT); gpio_set_level(driver->reset_gpio, 1); gpio_set_direction(driver->bus.dc_gpio, GPIO_MODE_OUTPUT); - gpio_set_pull_mode(driver->bus.dc_gpio, GPIO_PULLUP_ENABLE); gpio_set_direction(driver->busy_gpio, GPIO_MODE_INPUT); - gpio_set_pull_mode(driver->busy_gpio, GPIO_PULLUP_ENABLE); - gpio_set_level(driver->bus.dc_gpio, 0); + if (desc->use_gpio_pullups) { + gpio_set_pull_mode(driver->bus.dc_gpio, GPIO_PULLUP_ENABLE); + gpio_set_pull_mode(driver->busy_gpio, GPIO_PULLUP_ENABLE); + gpio_set_level(driver->bus.dc_gpio, 0); + } - display_reset(driver); + // Raise the panel rail (when the board has one) before reset or any + // controller command leaves the port. + if (!epaper_parse_panel_power(driver, opts, ctx)) { + ESP_LOGE(TAG, "Failed init: invalid panel power configuration."); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } + if (!epaper_set_panel_power(driver, true)) { + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } - wait_busy_level(driver, 1); + driver->state.prev_valid = false; + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; + driver->state.last_op_ok = true; + driver->state.asleep = false; + driver->state.asleep_mode = NULL; + epaper_clear_after_wake_policy(driver); // Init sequence: init_list opt overrides the descriptor default. term init_list = interop_kv_get_value_default( opts, ATOM_STR("\x9", "init_list"), term_nil(), ctx->global); if (init_list != term_nil()) { + display_reset(driver); + wait_busy_level(driver, desc->busy_idle_level); display_init_using_list(driver, init_list); - } else { - epaper_execute_init_seq(&driver->bus, driver->busy_gpio, - desc->init_seq, desc->init_seq_len, - desc->init_wait_busy_between_cmds); + } else if (desc->init.bytes != NULL) { + if (!epaper_run_driver_program(driver, &desc->init, NULL)) { + ESP_LOGE(TAG, "E-paper descriptor init program failed for %s.", + desc->name); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } + } else if (desc->init_seq != NULL) { + display_reset(driver); + wait_busy_level(driver, desc->busy_idle_level); + if (!epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + desc->init_seq, desc->init_seq_len, + desc->init_wait_busy_between_cmds)) { + ESP_LOGE(TAG, "E-paper descriptor init_seq failed for %s.", + desc->name); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } } update_last_refresh_ts(ctx); @@ -406,33 +2817,40 @@ static void display_spi_init(Context *ctx, term opts) #if SELF_TEST for (int i = 0; i < 8; i++) { fprintf(stderr, "color: %i\n", i); - clear_screen(ctx, i); + clear_screen_internal(ctx, i, true); vTaskDelay(30000 / portTICK_PERIOD_MS); } - clear_screen(ctx, 1); + clear_screen_internal(ctx, 1, true); while (1) ; #else - xTaskCreate(display_task_process_messages, "display", 10000, &driver->display_args, 1, NULL); + if (xTaskCreate(display_task_process_messages, "display", 10000, + &driver->display_args, 1, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed init: unable to start display task."); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } #endif + + return true; } Context *epaper_display_create_port(GlobalContext *global, term opts) { Context *ctx = context_new(global); + if (ctx == NULL) { + return NULL; + } ctx->native_handler = display_task_consume_mailbox; - display_spi_init(ctx, opts); + if (!display_spi_init(ctx, opts)) { + scheduler_terminate(ctx); + return NULL; + } return ctx; } -// Erlang-side init override: accepts a list of -// {CmdByte :: 0..255, Binary :: binary()} -// {sleep_ms, Ms :: 0..255} -// {wait_busy_level, Level :: 0 | 1} -// tuples and applies them in order. Mirrors dcs_lcd's display_init_using_list -// with an added wait_busy_level clause, since e-paper controllers typically -// require BUSY-pin polling between commands that DCS LCDs do not. +// Erlang-side init override static void display_init_using_list(struct EpaperDriver *driver, term init_list) { term t = init_list; @@ -447,7 +2865,7 @@ static void display_init_using_list(struct EpaperDriver *driver, term init_list) spi_dc_write_cmd_data(&driver->bus, cmd, data, term_binary_size(data_term)); } else if ((cmd_term == context_make_atom(driver->ctx, ATOM_STR("\x8", "sleep_ms"))) && term_is_integer(data_term)) { - vTaskDelay(term_to_int(data_term) / portTICK_PERIOD_MS); + epaper_delay_ms(term_to_int(data_term)); } else if ((cmd_term == context_make_atom(driver->ctx, ATOM_STR("\xF", "wait_busy_level"))) && term_is_integer(data_term)) { wait_busy_level(driver, term_to_int(data_term)); diff --git a/epaper_draw.c b/epaper_draw.c index ef46e80..e4547c7 100644 --- a/epaper_draw.c +++ b/epaper_draw.c @@ -32,6 +32,17 @@ #include "epaper_screen.h" #include "font_data.h" +static uint8_t epaper_dither_screen(const struct EpaperScreen *screen, + int x, int y, uint8_t r, uint8_t g, uint8_t b, bool four_gray) +{ + if (four_gray) { + return epaper_dither_4gray(x, y, r, g, b, + screen->palette, screen->palette_size); + } + return epaper_dither_acep7(x, y, r, g, b, + screen->palette, screen->palette_size); +} + void epaper_draw_pixel_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, uint8_t c) { @@ -47,9 +58,9 @@ void epaper_draw_pixel_x(const struct EpaperScreen *screen, } } -int epaper_draw_image_x(const struct EpaperScreen *screen, +static int epaper_draw_image_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -88,13 +99,13 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (visible_bg) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (drawn_pixels > 0) { @@ -113,9 +124,9 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_rect_x(const struct EpaperScreen *screen, +static int epaper_draw_rect_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int width = item->width; @@ -131,8 +142,8 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, } for (int j = xpos - x; j < width; j++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } @@ -140,10 +151,10 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_shape_x(const struct EpaperScreen *screen, +static int epaper_draw_shape_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item, - int *outside_run) + int *outside_run, bool four_gray) { if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { return 0; @@ -158,12 +169,12 @@ int epaper_draw_shape_x(const struct EpaperScreen *screen, if (run > max_line_len) { run = max_line_len; } - return epaper_draw_rect_x(screen, line_buf, xpos, ypos, run, item); + return epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, run, item, four_gray); } -int epaper_draw_text_x(const struct EpaperScreen *screen, +static int epaper_draw_text_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -215,13 +226,13 @@ int epaper_draw_text_x(const struct EpaperScreen *screen, } if (opaque) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, fgcolor_r, fgcolor_g, fgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, fgcolor_r, fgcolor_g, fgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (visible_bg) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (drawn_pixels > 0) { @@ -239,9 +250,9 @@ int epaper_draw_text_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, +static int epaper_draw_scaled_cropped_img_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -282,15 +293,15 @@ int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; for (int k = 0; k < run; k++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } } else if (visible_bg) { for (int k = 0; k < run; k++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } @@ -312,9 +323,9 @@ int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_x(const struct EpaperScreen *screen, +static int epaper_draw_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, - BaseDisplayItem *row) + BaseDisplayItem *row, bool four_gray) { int line_len = screen->w - xpos; int transparent_run = INT_MAX; @@ -337,23 +348,28 @@ int epaper_draw_x(const struct EpaperScreen *screen, int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: - drawn_pixels = epaper_draw_image_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_image_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveScaledCroppedImage: - drawn_pixels = epaper_draw_scaled_cropped_img_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_scaled_cropped_img_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveRect: - drawn_pixels = epaper_draw_rect_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveText: - drawn_pixels = epaper_draw_text_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_text_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveShape: - drawn_pixels = epaper_draw_shape_x(screen, line_buf, xpos, ypos, max_line_len, item, &run); + drawn_pixels = epaper_draw_shape_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, &run, four_gray); break; default: { @@ -376,3 +392,57 @@ int epaper_draw_x(const struct EpaperScreen *screen, return 1; } + +int epaper_draw_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row) +{ + return epaper_draw_dither_x(screen, line_buf, xpos, ypos, row, false); +} + +int epaper_draw_4gray_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row) +{ + return epaper_draw_dither_x(screen, line_buf, xpos, ypos, row, true); +} + +int epaper_draw_image_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_image_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_rect_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_shape_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, int *outside_run) +{ + return epaper_draw_shape_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, outside_run, false); +} + +int epaper_draw_text_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_text_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_scaled_cropped_img_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} diff --git a/epaper_draw.h b/epaper_draw.h index 5b9918c..97133ce 100644 --- a/epaper_draw.h +++ b/epaper_draw.h @@ -51,4 +51,8 @@ int epaper_draw_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, BaseDisplayItem *row); +int epaper_draw_4gray_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row); + #endif diff --git a/epaper_program.c b/epaper_program.c new file mode 100644 index 0000000..da6c759 --- /dev/null +++ b/epaper_program.c @@ -0,0 +1,203 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 AtomGL contributors + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "epaper_program.h" + +static bool epaper_run_meta_opcode(const struct EPaperProgramOps *ops, + uint8_t opcode, const uint8_t *operands, uint8_t len) +{ + switch (opcode) { + case EPAPER_PROGRAM_WAIT_BUSY: + if (len != 3 || ops->wait_busy == NULL) { + return false; + } + return ops->wait_busy(ops->ctx, operands[0], + (uint16_t) operands[1] | ((uint16_t) operands[2] << 8)); + + case EPAPER_PROGRAM_RESET: + if (len != 3 || ops->reset == NULL) { + return false; + } + ops->reset(ops->ctx, operands[0], operands[1], operands[2]); + return true; + + case EPAPER_PROGRAM_INSERT_PLANE: + if (len != 1 || ops->insert_plane == NULL) { + return false; + } + return ops->insert_plane(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_INSERT_LUT: + if (len != 1 || ops->insert_lut == NULL) { + return false; + } + return ops->insert_lut(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_INSERT_PREV_FRAME: + if (len != 1 || ops->insert_prev_frame == NULL) { + return false; + } + return ops->insert_prev_frame(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_CAPTURE_FRAME: + if (len != 0 || ops->capture_frame == NULL) { + return false; + } + return ops->capture_frame(ops->ctx); + + case EPAPER_PROGRAM_MARK_PREV_VALID: + if (len != 0 || ops->mark_prev_valid == NULL) { + return false; + } + return ops->mark_prev_valid(ops->ctx); + + case EPAPER_PROGRAM_LABEL: + return len == 1; + + default: + return false; + } +} + +static bool epaper_validate_meta_opcode(uint8_t opcode, uint8_t len, + bool allow_render_ops) +{ + switch (opcode) { + case EPAPER_PROGRAM_WAIT_BUSY: + case EPAPER_PROGRAM_RESET: + return len == 3; + + case EPAPER_PROGRAM_INSERT_LUT: + return len == 1; + + case EPAPER_PROGRAM_INSERT_PLANE: + case EPAPER_PROGRAM_INSERT_PREV_FRAME: + return allow_render_ops && len == 1; + + case EPAPER_PROGRAM_CAPTURE_FRAME: + case EPAPER_PROGRAM_MARK_PREV_VALID: + return allow_render_ops && len == 0; + + case EPAPER_PROGRAM_LABEL: + return len == 1; + + default: + return false; + } +} + +bool epaper_validate_program(const struct EPaperProgram *program, + bool allow_render_ops) +{ + if (program == NULL || program->bytes == NULL) { + return true; + } + if (program->len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } + + const uint8_t *pc = program->bytes; + const uint8_t *end = program->bytes + program->len; + bool capture_pending = false; + bool capture_completed = false; + while (pc < end) { + if ((size_t) (end - pc) < 2) { + return false; + } + + uint8_t opcode = *pc++; + uint8_t flags_len = *pc++; + uint8_t len = flags_len & EPAPER_PROGRAM_LEN_MASK; + if ((size_t) (end - pc) < len) { + return false; + } + + if (flags_len & EPAPER_PROGRAM_META) { + if (!epaper_validate_meta_opcode(opcode, len, allow_render_ops)) { + return false; + } + if (opcode == EPAPER_PROGRAM_CAPTURE_FRAME) { + if (capture_pending) { + return false; + } + capture_pending = true; + } else if (opcode == EPAPER_PROGRAM_INSERT_PLANE && capture_pending) { + capture_pending = false; + capture_completed = true; + } else if (opcode == EPAPER_PROGRAM_MARK_PREV_VALID && !capture_completed) { + return false; + } + } + pc += len; + + if (flags_len & EPAPER_PROGRAM_DELAY) { + if (pc >= end) { + return false; + } + pc++; + } + } + + return !capture_pending; +} + +bool epaper_run_program(const struct EPaperProgram *program, + const struct EPaperProgramOps *ops) +{ + if (program == NULL || program->bytes == NULL) { + return true; + } + if (ops == NULL || ops->command == NULL) { + return false; + } + + const uint8_t *pc = program->bytes; + const uint8_t *end = program->bytes + program->len; + while (pc < end) { + if ((size_t) (end - pc) < 2) { + return false; + } + + uint8_t opcode = *pc++; + uint8_t flags_len = *pc++; + uint8_t len = flags_len & EPAPER_PROGRAM_LEN_MASK; + if ((size_t) (end - pc) < len) { + return false; + } + + if (flags_len & EPAPER_PROGRAM_META) { + if (!epaper_run_meta_opcode(ops, opcode, pc, len)) { + return false; + } + } else { + ops->command(ops->ctx, opcode, pc, len); + } + pc += len; + + if (flags_len & EPAPER_PROGRAM_DELAY) { + if (pc >= end || ops->delay_ms == NULL) { + return false; + } + ops->delay_ms(ops->ctx, *pc++); + } + } + + return true; +} diff --git a/epaper_program.h b/epaper_program.h new file mode 100644 index 0000000..0ed7ab4 --- /dev/null +++ b/epaper_program.h @@ -0,0 +1,88 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 AtomGL contributors + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _EPAPER_PROGRAM_H_ +#define _EPAPER_PROGRAM_H_ + +#include +#include +#include + +#define EPAPER_MAX_LUT_SLOTS 4 + +#define EPAPER_PROGRAM_DELAY 0x80 +#define EPAPER_PROGRAM_META 0x40 +#define EPAPER_PROGRAM_LEN_MASK 0x3F +#define EPAPER_MAX_DESCRIPTOR_BINARY_LEN 4096 + +enum EPaperProgramOpcode +{ + EPAPER_PROGRAM_WAIT_BUSY = 0x01, + EPAPER_PROGRAM_RESET = 0x02, + EPAPER_PROGRAM_INSERT_PLANE = 0x03, + EPAPER_PROGRAM_INSERT_LUT = 0x04, + EPAPER_PROGRAM_INSERT_PREV_FRAME = 0x05, + EPAPER_PROGRAM_CAPTURE_FRAME = 0x06, + EPAPER_PROGRAM_MARK_PREV_VALID = 0x07, + EPAPER_PROGRAM_LABEL = 0x10 +}; + +enum EPaperLutSlot +{ + EPAPER_LUT_SLOT_FULL = 0, + EPAPER_LUT_SLOT_PARTIAL = 1, + EPAPER_LUT_SLOT_4GRAY = 2, + EPAPER_LUT_SLOT_FAST = 3 +}; + +struct EPaperProgram +{ + const uint8_t *bytes; + size_t len; +}; + +struct EPaperLut +{ + const uint8_t *bytes; + size_t len; +}; + +struct EPaperProgramOps +{ + void *ctx; + + void (*command)(void *ctx, uint8_t command, const uint8_t *data, size_t len); + void (*delay_ms)(void *ctx, uint8_t delay_ms); + void (*reset)(void *ctx, uint8_t high_ms, uint8_t low_ms, uint8_t settle_ms); + + bool (*wait_busy)(void *ctx, uint8_t level, uint16_t timeout_ms); + bool (*insert_plane)(void *ctx, uint8_t plane_id); + bool (*insert_lut)(void *ctx, uint8_t slot_id); + bool (*insert_prev_frame)(void *ctx, uint8_t plane_id); + bool (*capture_frame)(void *ctx); + bool (*mark_prev_valid)(void *ctx); +}; + +bool epaper_run_program(const struct EPaperProgram *program, + const struct EPaperProgramOps *ops); +bool epaper_validate_program(const struct EPaperProgram *program, + bool allow_render_ops); + +#endif diff --git a/tests/items/test_items.c b/tests/items/test_items.c index 46d08d2..565a0e2 100644 --- a/tests/items/test_items.c +++ b/tests/items/test_items.c @@ -1233,10 +1233,11 @@ typedef enum { RendererDcsLcd, RendererMono, - RendererEpaper + RendererEpaper, + RendererEpaper4gray } renderer_t; -static const char *const renderer_names[] = { "dcs_lcd", "mono", "epaper" }; +static const char *const renderer_names[] = { "dcs_lcd", "mono", "epaper", "epaper_4gray" }; static int renderer_line_bytes(renderer_t renderer) { @@ -1244,6 +1245,7 @@ static int renderer_line_bytes(renderer_t renderer) case RendererMono: return SCREEN_W / 8; case RendererEpaper: + case RendererEpaper4gray: return SCREEN_W / 2; default: return SCREEN_W * 2; @@ -1261,6 +1263,12 @@ static int renderer_draw_x(renderer_t renderer, int w, uint16_t *line, int xpos, struct EpaperScreen screen = { .w = w, .h = SCREEN_H, .palette = epaper_acep_palette, .palette_size = 7 }; return epaper_draw_x(&screen, (uint8_t *) line, xpos, ypos, row); } + case RendererEpaper4gray: { + struct EpaperScreen screen = { + .w = w, .h = SCREEN_H, .palette = epaper_ssd1680_4gray_palette, .palette_size = 4 + }; + return epaper_draw_4gray_x(&screen, (uint8_t *) line, xpos, ypos, row); + } default: { struct DCSLCDScreen screen; memset(&screen, 0, sizeof(screen)); @@ -1410,7 +1418,7 @@ static void test_runs_match_pixels(void) } bool same = true; - for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper && same; renderer++) { + for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper4gray && same; renderer++) { render_items(renderer, items, len, expected, true); render_items(renderer, items, len, frame, false); same = memcmp(expected, frame, renderer_line_bytes(renderer) * SCREEN_H) == 0;