diff --git a/CMakeLists.txt b/CMakeLists.txt index b17005cd..6428423b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ set(ATOMVM_NEOPIXEL_COMPONENT_SRCS "nifs/atomvm_neopixel.c" - "nifs/led_strip_rmt_ws2812.c" + "nifs/led_strip_driver.c" ) # WHOLE_ARCHIVE option is supported only with esp-idf 5.x @@ -34,7 +34,7 @@ endif() idf_component_register( SRCS ${ATOMVM_NEOPIXEL_COMPONENT_SRCS} INCLUDE_DIRS "nifs/include" - PRIV_REQUIRES "libatomvm" "avm_sys" "driver" + PRIV_REQUIRES "libatomvm" "avm_sys" "driver" "led_strip" ${OPTIONAL_WHOLE_ARCHIVE} ) diff --git a/Kconfig b/Kconfig index b99037d7..38105b18 100644 --- a/Kconfig +++ b/Kconfig @@ -5,5 +5,41 @@ config AVM_NEOPIXEL_ENABLE default y help Use this parameter to enable or disable the AtomVM NEOPIXEL driver. + + This driver uses the ESP-IDF led_strip component which provides: + - Automatic DMA support on ESP32-S3/C6 + - SPI backend fallback for better WiFi coexistence + - Maintained by Espressif for optimal performance + +choice AVM_NEOPIXEL_BACKEND + prompt "LED strip backend" + default AVM_NEOPIXEL_BACKEND_AUTO + depends on AVM_NEOPIXEL_ENABLE + help + Select the backend peripheral for driving the LED strip. + +config AVM_NEOPIXEL_BACKEND_AUTO + bool "Auto (recommended)" + help + Automatically select the best backend for your chip: + - ESP32: SPI with DMA (best WiFi coexistence) + - ESP32-S3/C6/P4: RMT with DMA + - Others: RMT, fallback to SPI + +config AVM_NEOPIXEL_BACKEND_SPI + bool "Force SPI" + help + Always use SPI backend with DMA. + Best for WiFi coexistence on all chips. + Note: Uses entire SPI bus (SPI2_HOST). + +config AVM_NEOPIXEL_BACKEND_RMT + bool "Force RMT" + help + Always use RMT backend. + May cause flickering with WiFi on ESP32 (no DMA). + Works well on ESP32-S3/C6/P4 with DMA. + +endchoice endmenu diff --git a/README.md b/README.md index 7947a18b..78f7055f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # AtomVM NeoPixel Library -This AtomVM Erlang library and Nif can be used to control WS2812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. +This AtomVM Erlang library and Nif can be used to control WS2812 and SK6812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. + +## Features + +- **RGB LED support** (WS2812, WS2812B) - 24-bit color +- **RGBW LED support** (SK6812) - 32-bit color with dedicated white channel +- **Global brightness control** - Hardware-efficient brightness scaling (0-255) +- **Multiple color spaces** - RGB, RGBW, HSV, and HSVW +- **Batch operations** - Fill entire strip or set multiple pixels in a single call +- **ESP-IDF 5.x compatible** - Uses the new RMT driver API This Nif is included as an add-on to the AtomVM base image. In order to use this Nif in your AtomVM program, you must be able to build the AtomVM virtual machine, which in turn requires installation of the Espressif IDF SDK and tool chain. diff --git a/examples/neopixel_example/src/neopixel_example.erl b/examples/neopixel_example/src/neopixel_example.erl index 10784d2c..6eed188c 100644 --- a/examples/neopixel_example/src/neopixel_example.erl +++ b/examples/neopixel_example/src/neopixel_example.erl @@ -16,7 +16,7 @@ %% -module(neopixel_example). --export([start/0]). +-export([start/0, demo_fill/0, demo_pattern/0]). -define(NEOPIXEL_PIN, 18). -define(NUM_PIXELS, 4). @@ -24,6 +24,7 @@ -define(SATURATION, 100). -define(VALUE, 15). +%% @doc Main example - rainbow cycle on each pixel start() -> {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), ok = neopixel:clear(NeoPixel), @@ -35,6 +36,40 @@ start() -> ), timer:sleep(infinity). +%% @doc Demo: Fill entire strip with solid colors +demo_fill() -> + {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), + ok = neopixel:clear(NeoPixel), + %% Cycle through red, green, blue + fill_loop(NeoPixel, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]). + +fill_loop(NeoPixel, []) -> + fill_loop(NeoPixel, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]); +fill_loop(NeoPixel, [{R, G, B} | Rest]) -> + ok = neopixel:fill_rgb(NeoPixel, R, G, B), + ok = neopixel:refresh(NeoPixel), + timer:sleep(1000), + fill_loop(NeoPixel, Rest). + +%% @doc Demo: Set multiple pixels with a pattern using set_pixels_rgb +demo_pattern() -> + {ok, NeoPixel} = neopixel:start(?NEOPIXEL_PIN, ?NUM_PIXELS), + ok = neopixel:clear(NeoPixel), + %% Create a rainbow pattern + Pattern = [{255, 0, 0}, {255, 127, 0}, {0, 255, 0}, {0, 0, 255}], + pattern_loop(NeoPixel, Pattern, 0). + +pattern_loop(NeoPixel, Pattern, Offset) -> + %% Rotate the pattern by Offset positions + RotatedPattern = rotate_list(Pattern, Offset), + ok = neopixel:set_pixels_rgb(NeoPixel, RotatedPattern), + ok = neopixel:refresh(NeoPixel), + timer:sleep(200), + pattern_loop(NeoPixel, Pattern, (Offset + 1) rem length(Pattern)). + +rotate_list(List, 0) -> List; +rotate_list([H | T], N) -> rotate_list(T ++ [H], N - 1). + loop(NeoPixel, I, Hue, SleepMs) -> ok = neopixel:set_pixel_hsv(NeoPixel, I, Hue, ?SATURATION, ?VALUE), ok = neopixel:refresh(NeoPixel), diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 00000000..4f144007 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/led_strip: "^3.0.2" + idf: + version: ">=5.0" diff --git a/markdown/neopixel.md b/markdown/neopixel.md index 98316bfc..5b8dba07 100644 --- a/markdown/neopixel.md +++ b/markdown/neopixel.md @@ -1,9 +1,16 @@ # Neopixel -This AtomVM Erlang library and Nif can be used to control WS2812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. +This AtomVM Erlang library and Nif can be used to control WS2812 and SK6812 LED strips using the ESP32 SoC for any Erlang/Elixir programs targeted for AtomVM on the ESP32 platform. The AtomVM NeoPixel library is only supported on the ESP32 platform. +## Features + +- **RGB LED support** (WS2812, WS2812B) - 24-bit color (3 bytes per pixel) +- **RGBW LED support** (SK6812) - 32-bit color with dedicated white channel (4 bytes per pixel) +- **Global brightness control** - Efficient brightness scaling applied at refresh time +- **Multiple color spaces** - RGB, RGBW, and HSV + ## Build Instructions The AtomVM NeoPixel library is implemented as an AtomVM component, which includes some native C code that must be linked into the ESP32 AtomVM image. In order to build and deploy this client code, you must build an AtomVM binary image with this component compiled and linked into the image. @@ -16,9 +23,14 @@ Once the AtomVM image including this component has been flashed to your ESP32 de ## Programmer's Guide -The `atomvm_neopixel` library can be used to drive a strip of [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) "Neopixel" LEDs. +The `atomvm_neopixel` library can be used to drive a strip of [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) "Neopixel" LEDs or [SK6812](https://cdn-shop.adafruit.com/product-files/2757/p2757_SK6812RGBW_REV01.pdf) RGBW LEDs. + +Neopixel LED strips are individually addressable sets of "pixels". RGB strips (WS2812) contain 3 LEDs per pixel (red, green, blue), while RGBW strips (SK6812) contain 4 LEDs per pixel (red, green, blue, white). Each color channel can be configured using an 8-bit value (`0..255`). -Neopixel LED strips are individually addressable sets of "pixels", where each pixel contains 3 LEDs (red, blue, green). Each pixel can be configured using three 8-bit (`0..255`) values for red, green, and blue respectively, using an RGB color space. Alternatively, users may specify a hue (`0..359`), saturation (`0..100`), and value (`0..100`), using an HSV color space. +Colors can be specified using: +- **RGB** - Red, Green, Blue values (`0..255` each) +- **RGBW** - Red, Green, Blue, White values (`0..255` each) - for SK6812 strips only +- **HSV** - Hue (`0..359`), Saturation (`0..100`), Value (`0..100`) AtomVM programmers interface with the `atomvm_neopixel` API via the `neopixel` module, which provides operations for starting and stopping an Erlang process associated with a specified LED strip, and for setting values on each pixel in the strip. @@ -34,21 +46,27 @@ Use the `neopixel:start/2` function to initialize a neopixel instance. Specify {ok NeoPixel} = neopixel:start(Pin, NumPixels), ... -Use the `neopixel:start/3` function to initialize a neopixel instance with non-default options. Options are represented in an Erlang map. The permissible entries are encapsulated in the following table: +Use the `neopixel:start/3` function to initialize a neopixel instance with non-default options. Options can be specified as an Erlang map or a proplist (keyword list in Elixir). The permissible entries are encapsulated in the following table: | Key | Type | Default | Description | | ----- | ----- | ------| ----| +| `led_type` | `rgb \| rgbw` | `rgb` | LED strip type. Use `rgbw` for SK6812 RGBW strips. | | `timeout` | `non_neg_integer()` | 100 | Timeout (in milliseconds) used internally when communicating with the LED strip | -| `channel` | `channel_0\|channel_1\|channel_2\|channel_3` | `channel_0` | ESP RTC transmit channel to use. Use a different channel for each `neopixel` instance created. | +| `channel` | `channel_0 \| channel_1 \| channel_2 \| channel_3` | `channel_0` | Legacy option, ignored in ESP-IDF 5.x | -For example, +For example, to use an RGBW strip: %% erlang Pin = 18, NumPixels = 4, - {ok NeoPixel} = neopixel:start(Pin, NumPixels, #{channel => channel_1}), + {ok, NeoPixel} = neopixel:start(Pin, NumPixels, #{led_type => rgbw}), ... +Or in Elixir with a keyword list: + + # elixir + {:ok, neo_pixel} = :neopixel.start(18, 4, led_type: :rgbw) + The returned `NeoPixel` instance should be used for subsequent operations. Use the `neopixel:stop/1` function to stop a neopixel instance and free any resources in use by it. @@ -56,6 +74,45 @@ Use the `neopixel:stop/1` function to stop a neopixel instance and free any reso %% erlang neopixel:stop(NeoPixel). +### Multiple Strips + +You can drive multiple LED strips simultaneously by starting multiple neopixel instances on different GPIO pins: + + %% erlang + %% Start three independent strips + {ok, Strip1} = neopixel:start(18, 30), %% 30-pixel RGB strip on pin 18 + {ok, Strip2} = neopixel:start(19, 60), %% 60-pixel RGB strip on pin 19 + {ok, Strip3} = neopixel:start(21, 16, #{led_type => rgbw}), %% 16-pixel RGBW strip on pin 21 + +Or in Elixir: + + # elixir + {:ok, strip1} = :neopixel.start(18, 30) + {:ok, strip2} = :neopixel.start(19, 60) + {:ok, strip3} = :neopixel.start(21, 16, led_type: :rgbw) + +Each strip is completely independent: +- Separate gen_server process +- Separate pixel buffer +- Separate RMT hardware channel +- Can be controlled from different processes concurrently + +**Hardware Limits:** The number of simultaneous strips is limited by available RMT TX channels: + +| ESP32 Variant | Max TX Channels | Max Strips | +|---------------|-----------------|------------| +| ESP32 | 4-8* | 4-8 | +| ESP32-S2 | 4 | 4 | +| ESP32-S3 | 4 | 4 | +| ESP32-C3 | 2 | 2 | +| ESP32-C6 | 2 | 2 | + +*ESP32 has 8 RMT channels that can be configured as TX or RX. Default is 4 TX + 4 RX. + +RMT channels are automatically allocated by ESP-IDF when you call `neopixel:start`. If no channels are available, start will return an error. + +> **Note:** The `channel` option in `neopixel:start/3` is legacy and ignored in ESP-IDF 5.x. Channel allocation is now automatic. + ### Clearing pixels Use the `neopixel:clear/1` function to clear all the pixels in the LED strip to an "off" value. @@ -88,6 +145,33 @@ RGB values and their ranges are summarized in the following table: | green | `0..255` | Value of green LED | | blue | `0..255` | Value of blue LED | +#### RGBW Color Space (SK6812 only) + +For RGBW strips (SK6812), pixel colors can be set using the `neopixel:set_pixel_rgbw/6` function. This function is only available when the strip was initialized with `led_type => rgbw`. + +For example, to set the second pixel to red with 50% white: + + %% erlang + ok = neopixel:set_pixel_rgbw(NeoPixel, 1, 255, 0, 0, 128). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixel_rgbw(neo_pixel, 1, 255, 0, 0, 128) + +RGBW values and their ranges are summarized in the following table: + +| Parameter | Range | Description | +| ----- | ----- | ------| +| red | `0..255` | Value of red LED | +| green | `0..255` | Value of green LED | +| blue | `0..255` | Value of blue LED | +| white | `0..255` | Value of white LED | + +> **Note:** Calling `set_pixel_rgbw/6` on an RGB strip will return `{error, not_supported}`. + +> **Note:** You can use `set_pixel_rgb/5` on RGBW strips - the white channel will be set to 0. + #### HSV color space Pixel colors can be set using the HSV color space via the `neopixel:set_pixel_hsv/5` function. Pixel indices are in the range `[0..NumPixels-1]`. @@ -107,6 +191,112 @@ HSV values and their ranges are summarized in the following table: | saturation | `0..100` | Color saturation, as a percentage, with 0 being all white, and 100 maximum color saturation. | | value | `0..100` | Value, as a percentage, with 0 being all dark, and 100 maximum brightness. | +#### HSVW color space (SK6812 only) + +For RGBW strips, use `neopixel:set_pixel_hsvw/6` to set a pixel using HSV values plus an independent white channel. The HSV values control the RGB LEDs while the white parameter controls the dedicated white LED. + + %% erlang + %% Set pixel 0 to orange (H=30) at full saturation and value, with 50% white + ok = neopixel:set_pixel_hsvw(NeoPixel, 0, 30, 100, 100, 128). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixel_hsvw(neo_pixel, 0, 30, 100, 100, 128) + +| Parameter | Range | Description | +| ----- | ----- | ------| +| hue | `0..259` | Pixel hue (same as HSV) | +| saturation | `0..100` | Color saturation (same as HSV) | +| value | `0..100` | Value/brightness of RGB LEDs (same as HSV) | +| white | `0..255` | Value of the dedicated white LED | + +> **Note:** Calling `set_pixel_hsvw/6` on an RGB strip will return `{error, not_supported}`. + +### Filling the Strip + +Use the `neopixel:fill_rgb/4` function to set all pixels in the strip to the same color with a single call. + + %% erlang + %% Fill entire strip with red + ok = neopixel:fill_rgb(NeoPixel, 255, 0, 0). + +Or in Elixir: + + # elixir + :ok = :neopixel.fill_rgb(neo_pixel, 255, 0, 0) + +For RGBW strips, use `neopixel:fill_rgbw/5`: + + %% erlang + %% Fill entire strip with pure white (using white LED only) + ok = neopixel:fill_rgbw(NeoPixel, 0, 0, 0, 255). + +> **Note:** Calling `fill_rgbw/5` on an RGB strip will return `{error, not_supported}`. + +#### Filling with HSV Colors + +Use `neopixel:fill_hsv/4` to fill the strip using the HSV color space. This is convenient for color cycling effects like rainbows: + + %% erlang + %% Fill entire strip with red (hue=0) + ok = neopixel:fill_hsv(NeoPixel, 0, 100, 100). + + %% Fill with cyan (hue=180) at 50% brightness + ok = neopixel:fill_hsv(NeoPixel, 180, 100, 50). + +Or in Elixir: + + # elixir + # Rainbow cycle - just increment hue each frame + :ok = :neopixel.fill_hsv(neo_pixel, hue, 100, 50) + +For RGBW strips, use `neopixel:fill_hsvw/5` to combine HSV color with the white channel: + + %% erlang + %% Warm white: orange tint (H=30) plus white LED + ok = neopixel:fill_hsvw(NeoPixel, 30, 50, 50, 200). + +> **Note:** Calling `fill_hsvw/5` on an RGB strip will return `{error, not_supported}`. + +### Setting Multiple Pixels + +Use `neopixel:set_pixels_rgb/2` to set multiple pixels at once from a list of `{R, G, B}` tuples. Pixels are set starting at index 0. + + %% erlang + %% Set first 3 pixels to red, green, blue + Colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}], + ok = neopixel:set_pixels_rgb(NeoPixel, Colors). + +Or in Elixir: + + # elixir + colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}] + :ok = :neopixel.set_pixels_rgb(neo_pixel, colors) + +Use `neopixel:set_pixels_rgb/3` to set pixels starting at a specific offset: + + %% erlang + %% Set pixels 5, 6, 7 to red, green, blue + Colors = [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}], + ok = neopixel:set_pixels_rgb(NeoPixel, 5, Colors). + +Or in Elixir: + + # elixir + :ok = :neopixel.set_pixels_rgb(neo_pixel, 5, colors) + +For RGBW strips, use `neopixel:set_pixels_rgbw/2` or `neopixel:set_pixels_rgbw/3` with `{R, G, B, W}` tuples: + + %% erlang + %% Set pixels 2, 3, 4 with different white levels + Colors = [{255, 0, 0, 0}, {0, 255, 0, 128}, {0, 0, 255, 255}], + ok = neopixel:set_pixels_rgbw(NeoPixel, 2, Colors). + +The list can be shorter than the strip length - only the specified pixels will be updated. + +> **Note:** Calling `set_pixels_rgbw/2` or `set_pixels_rgbw/3` on an RGB strip will return `{error, not_supported}`. + ### Refreshing pixels Use the `neopixel:refresh/1` function to ref refresh all the pixels in the LED strip. @@ -116,6 +306,99 @@ Use the `neopixel:refresh/1` function to ref refresh all the pixels in the LED s Refreshing the LED strip will manifest any changes made via any previous `set_pixel_*` operations (see above). +### Brightness Control + +Use the `neopixel:set_brightness/2` function to set the global brightness for the strip. Brightness is a value from 0 (off) to 255 (full brightness). + + %% erlang + ok = neopixel:set_brightness(NeoPixel, 128). %% 50% brightness + +Or in Elixir: + + # elixir + :ok = :neopixel.set_brightness(neo_pixel, 128) + +Brightness scaling is applied efficiently at refresh time, so: +- Original color values are preserved in memory +- Changing brightness and calling `refresh/1` immediately shows the effect +- No precision loss from repeated brightness changes + +Use the `neopixel:get_brightness/1` function to get the current brightness: + + %% erlang + Brightness = neopixel:get_brightness(NeoPixel). + +### Concurrency + +The neopixel driver is implemented as a `gen_server` process, which has important implications for concurrent access. + +#### Safety + +**Yes, you can safely call neopixel functions from multiple processes.** All API functions (`set_pixel_*`, `fill_*`, `set_pixels_*`, `refresh`, etc.) use synchronous `gen_server:call/2`, which means: + +- Requests are serialized through the gen_server's mailbox +- Only one operation executes at a time +- Each call blocks until the operation completes +- No race conditions on the underlying hardware + +Example with multiple processes: + + %% erlang + {ok, NeoPixel} = neopixel:start(18, 30), + + %% Process 1: animate first half of strip + spawn(fun() -> animate_section(NeoPixel, 0, 14) end), + + %% Process 2: animate second half of strip + spawn(fun() -> animate_section(NeoPixel, 15, 29) end), + + %% Process 3: periodically refresh + spawn(fun() -> refresh_loop(NeoPixel, 16) end). %% 60 FPS + +#### Mailbox Considerations + +Since the gen_server processes requests sequentially, a few things to keep in mind: + +1. **Backpressure is automatic**: Because `gen_server:call` is synchronous, a calling process blocks until its request is handled. This naturally prevents any single process from flooding the mailbox. + +2. **Many concurrent callers**: If many processes call simultaneously, requests queue in the mailbox. Each caller blocks until their specific request completes. This is generally fine for typical LED animation patterns. + +3. **Blocking NIFs**: The NIF operations (especially `refresh`) block the gen_server while communicating with hardware. For a 30-LED strip, `refresh` typically takes ~1ms. During this time, other requests wait in the mailbox. + +4. **No mailbox overflow risk**: Under normal usage, the mailbox won't overflow because: + - Synchronous calls provide natural backpressure + - NIF operations are fast (microseconds to low milliseconds) + - Callers block while waiting, limiting request rate + +#### Performance Tips + +For best performance with multiple processes: + +1. **Batch updates**: Use `set_pixels_rgb/2,3` instead of multiple `set_pixel_rgb/5` calls - one NIF call vs N calls: + + %% Slow: 30 gen_server calls + [neopixel:set_pixel_rgb(NP, I, R, G, B) || I <- lists:seq(0, 29)]. + + %% Fast: 1 gen_server call + neopixel:set_pixels_rgb(NP, Colors). + +2. **Coordinate refresh**: If multiple processes set pixels, consider having a single process handle `refresh` at a fixed rate rather than each process refreshing after every update. + +3. **Partition the strip**: Assign different pixel ranges to different processes to avoid visual conflicts, then have a coordinator refresh. + +#### Timeout Handling + +All gen_server calls use the default 5-second timeout. If a call times out (extremely unlikely under normal conditions), the calling process crashes with a timeout error. The gen_server continues running. + +For custom timeout handling: + + %% erlang + try + neopixel:set_pixel_rgb(NeoPixel, 0, 255, 0, 0) + catch + exit:{timeout, _} -> handle_timeout() + end. + ### API Reference To generate Reference API documentation in HTML, issue the rebar3 target @@ -123,3 +406,43 @@ To generate Reference API documentation in HTML, issue the rebar3 target shell$ rebar3 edoc from the top level of the `atomvm_neopixel` source tree. Output is written to the `doc` directory. + +### Future Optimizations + +The following optimizations have been identified but not yet implemented. They could further improve performance on resource-constrained hardware: + +#### Binary Input for `set_pixels` + +Accept a binary `<>` instead of a list of tuples. This would avoid the overhead of unpacking Erlang tuples in the NIF and reduce memory allocations on the Erlang side. + +```erlang +%% Current (tuple list) +neopixel:set_pixels_rgb(Strip, [{255,0,0}, {0,255,0}, {0,0,255}]). + +%% Potential future API (binary) +neopixel:set_pixels_rgb_bin(Strip, <<255,0,0, 0,255,0, 0,0,255>>). +``` + +#### Asynchronous Operations + +Add `gen_server:cast` versions of operations for fire-and-forget scenarios where the caller doesn't need to wait for completion: + +```erlang +%% Current (blocking) +ok = neopixel:refresh(Strip). + +%% Potential future API (non-blocking) +ok = neopixel:refresh_async(Strip). +``` + +#### Dirty NIF for Refresh + +Mark the `refresh` NIF as a dirty NIF so it runs on a separate scheduler and doesn't block the main BEAM scheduler during RMT transmission. Most beneficial for very long strips (100+ LEDs). + +#### DMA Double-Buffering + +Prepare the next frame in a second buffer while the RMT peripheral transmits the current frame. This would allow true zero-copy animation at high frame rates, but adds memory overhead and complexity. + +--- + +Contributions implementing any of these optimizations are welcome! diff --git a/nifs/atomvm_neopixel.c b/nifs/atomvm_neopixel.c index 08f8460b..5a369da1 100644 --- a/nifs/atomvm_neopixel.c +++ b/nifs/atomvm_neopixel.c @@ -19,30 +19,21 @@ #include #include -#include #include #include #include #include #include -#include "led_strip.h" +#include "atomvm_led_strip.h" // #define ENABLE_TRACE #include "trace.h" #define TAG "atomvm_neopixel" -#define NO_ALLOC_FLAGS 0 -// References -// https://docs.espressif.com/projects/esp-idf/en/v3.3.4/api-reference/peripherals/rmt.html -// - -static const char *const led_strip_atom = "\x9" "led_strip"; -static const char *const channel_0_atom = "\x9" "channel_0"; -static const char *const channel_1_atom = "\x9" "channel_1"; -static const char *const channel_2_atom = "\x9" "channel_2"; -static const char *const channel_3_atom = "\x9" "channel_3"; -// 123456789ABCDEF01 +static const char *const led_strip_atom = "\x9" "led_strip"; +static const char *const rgbw_atom = "\x4" "rgbw"; +static const char *const not_supported_atom = "\xD" "not_supported"; static inline term ptr_to_binary(void *ptr, Context* ctx) @@ -61,25 +52,6 @@ static inline void *binary_to_ptr(term binary) } -static rmt_channel_t get_rmt_channel(Context *ctx, term channel) -{ - if (channel == globalcontext_make_atom(ctx->global, channel_0_atom)) { - return RMT_CHANNEL_1; - } else if (channel == globalcontext_make_atom(ctx->global, channel_1_atom)) { - return RMT_CHANNEL_2; - } else if (channel == globalcontext_make_atom(ctx->global, channel_2_atom)) { - return RMT_CHANNEL_2; - } else if (channel == globalcontext_make_atom(ctx->global, channel_3_atom)) { - return RMT_CHANNEL_3; -#if SOC_RMT_CHANNELS_PER_GROUP > 4 - // TODO -#endif - } else { - return RMT_CHANNEL_MAX; - } -} - - static term nif_init(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -90,44 +62,37 @@ static term nif_init(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(num_pixels, term_is_integer); term channel = argv[2]; VALIDATE_VALUE(channel, term_is_atom); - - rmt_channel_t rmt_channel = get_rmt_channel(ctx, channel); + term led_type_term = argv[3]; + VALIDATE_VALUE(led_type_term, term_is_atom); + // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - rmt_config_t config = RMT_DEFAULT_CONFIG_TX(term_to_int(pin), rmt_channel); - // set counter clock to 40MHz - config.clk_div = 2; + } - esp_err_t err = rmt_config(&config); - if (err != ESP_OK) { - TRACE("Failed to initialize rmt config. err=%i\n", err); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; - } - err = rmt_driver_install(config.channel, 0, NO_ALLOC_FLAGS); - if (err != ESP_OK) { - TRACE("Failed to install rmt driver. err=%i\n", err); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; - } - led_strip_config_t strip_config = LED_STRIP_DEFAULT_CONFIG(term_to_int(num_pixels), (led_strip_dev_t) config.channel); - led_strip_t *strip = led_strip_new_rmt_ws2812(&strip_config); - if (!strip) { - TRACE("Failed to install WS2812 driver.\n"); - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, led_strip_atom)); - return error_tuple; - } - ESP_LOGI(TAG, "Installed WS2812 driver."); - return ptr_to_binary(strip, ctx); + // Determine LED type from atom + avm_led_strip_type_t led_type = AVM_LED_STRIP_RGB; + if (globalcontext_is_term_equal_to_atom_string(ctx->global, led_type_term, rgbw_atom)) { + led_type = AVM_LED_STRIP_RGBW; + } + + avm_led_strip_config_t strip_config = { + .max_leds = term_to_int(num_pixels), + .gpio_num = term_to_int(pin), + .led_type = led_type + }; + + avm_led_strip_t *strip = avm_led_strip_new(&strip_config); + if (!strip) { + TRACE("Failed to install WS2812 driver.\n"); + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, led_strip_atom)); + return error_tuple; } + + ESP_LOGI(TAG, "Installed WS2812 driver."); + return ptr_to_binary(strip, ctx); } @@ -140,19 +105,18 @@ static term nif_clear(Context *ctx, int argc, term argv[]) term timeout = argv[1]; VALIDATE_VALUE(timeout, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); esp_err_t err = strip->clear(strip, term_to_int(timeout)); if (err != ESP_OK) { TRACE("Failed to clear led strip. err=%i\n", err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Cleared led strip.\n"); return OK_ATOM; @@ -168,19 +132,18 @@ static term nif_refresh(Context *ctx, int argc, term argv[]) term timeout = argv[1]; VALIDATE_VALUE(timeout, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); esp_err_t err = strip->refresh(strip, term_to_int(timeout)); if (err != ESP_OK) { TRACE("Failed to refresh led strip. err=%i\n", err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Refreshed led strip.\n"); return OK_ATOM; @@ -202,7 +165,7 @@ static term nif_set_pixel_rgb(Context *ctx, int argc, term argv[]) term blue = argv[4]; VALIDATE_VALUE(blue, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel(strip, i, term_to_int(red), term_to_int(green), term_to_int(blue)); @@ -210,12 +173,11 @@ static term nif_set_pixel_rgb(Context *ctx, int argc, term argv[]) TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i). err=%i\n", i, red, green, blue, err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue)); return OK_ATOM; @@ -237,12 +199,12 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) term value = argv[4]; VALIDATE_VALUE(value, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); uint32_t red = 0; uint32_t green = 0; uint32_t blue = 0; - led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); avm_int_t i = term_to_int(index); esp_err_t err = strip->set_pixel(strip, i, red, green, blue); @@ -250,56 +212,500 @@ static term nif_set_pixel_hsv(Context *ctx, int argc, term argv[]) TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i). err=%i\n", i, red, green, blue, err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { - term error_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(error_tuple, 0, ERROR_ATOM); - term_put_tuple_element(error_tuple, 1, term_from_int(err)); - return error_tuple; } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } - TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, red, green, blue); + TRACE("Set pixel %i to r=%i g=%i b=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue)); return OK_ATOM; } -static term nif_tini(Context *ctx, int argc, term argv[]) +static term nif_set_pixel_hsvw(Context *ctx, int argc, term argv[]) { UNUSED(argc); term handle = argv[0]; VALIDATE_VALUE(handle, term_is_binary); - term channel = argv[1]; - VALIDATE_VALUE(channel, term_is_atom); + term index = argv[1]; + VALIDATE_VALUE(index, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); - led_strip_t *strip = (led_strip_t *) binary_to_ptr(handle); + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); - esp_err_t err = strip->del(strip); + uint32_t red = 0; + uint32_t green = 0; + uint32_t blue = 0; + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + avm_int_t i = term_to_int(index); + esp_err_t err = strip->set_pixel_rgbw(strip, i, red, green, blue, term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixel_hsvw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } if (err != ESP_OK) { - TRACE("Failed to delete led strip. err=%i\n", err); + TRACE("Failed to set pixel value on index %i. err=%i\n", i, err); if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set pixel %i via HSVW\n", i); + return OK_ATOM; +} + + +static term nif_set_pixel_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term index = argv[1]; + VALIDATE_VALUE(index, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + avm_int_t i = term_to_int(index); + esp_err_t err = strip->set_pixel_rgbw(strip, i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixel_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to set pixel value on index %i (r=%i g=%i b=%i w=%i). err=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white), err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set pixel %i to r=%i g=%i b=%i w=%i\n", i, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + return OK_ATOM; +} + + +static term nif_set_brightness(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term brightness = argv[1]; + VALIDATE_VALUE(brightness, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + avm_int_t br = term_to_int(brightness); + if (br < 0 || br > 255) { + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_brightness(strip, (uint8_t)br); + if (err != ESP_OK) { + TRACE("Failed to set brightness to %i. err=%i\n", br, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Set brightness to %i\n", br); + return OK_ATOM; +} + + +static term nif_get_brightness(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + uint8_t brightness = strip->get_brightness(strip); + return term_from_int(brightness); +} + + +static term nif_fill_rgb(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + // Use optimized direct buffer fill + esp_err_t err = strip->fill(strip, term_to_int(red), term_to_int(green), term_to_int(blue)); + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with r=%i g=%i b=%i\n", term_to_int(red), term_to_int(green), term_to_int(blue)); + return OK_ATOM; +} + + +static term nif_fill_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term red = argv[2]; + VALIDATE_VALUE(red, term_is_integer); + term green = argv[3]; + VALIDATE_VALUE(green, term_is_integer); + term blue = argv[4]; + VALIDATE_VALUE(blue, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + // Use optimized direct buffer fill + esp_err_t err = strip->fill_rgbw(strip, term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("fill_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with r=%i g=%i b=%i w=%i\n", term_to_int(red), term_to_int(green), term_to_int(blue), term_to_int(white)); + return OK_ATOM; +} + + +static term nif_fill_hsv(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + // Convert HSV to RGB + uint32_t red = 0, green = 0, blue = 0; + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + // Use optimized direct buffer fill with converted RGB values + esp_err_t err = strip->fill(strip, red, green, blue); + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with h=%i s=%i v=%i (r=%i g=%i b=%i)\n", + term_to_int(hue), term_to_int(saturation), term_to_int(value), red, green, blue); + return OK_ATOM; +} + + +static term nif_fill_hsvw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term num_pixels = argv[1]; + VALIDATE_VALUE(num_pixels, term_is_integer); + term hue = argv[2]; + VALIDATE_VALUE(hue, term_is_integer); + term saturation = argv[3]; + VALIDATE_VALUE(saturation, term_is_integer); + term value = argv[4]; + VALIDATE_VALUE(value, term_is_integer); + term white = argv[5]; + VALIDATE_VALUE(white, term_is_integer); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + // Convert HSV to RGB + uint32_t red = 0, green = 0, blue = 0; + avm_led_strip_hsv2rgb(term_to_int(hue), term_to_int(saturation), term_to_int(value), &red, &green, &blue); + + // Use optimized direct buffer fill with converted RGB + white values + esp_err_t err = strip->fill_rgbw(strip, red, green, blue, term_to_int(white)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("fill_hsvw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to fill pixels. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; + } + TRACE("Filled pixels with h=%i s=%i v=%i w=%i\n", + term_to_int(hue), term_to_int(saturation), term_to_int(value), term_to_int(white)); + return OK_ATOM; +} + + +static term nif_set_pixels_rgb(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term offset_term = argv[1]; + VALIDATE_VALUE(offset_term, term_is_integer); + term pixel_list = argv[2]; + VALIDATE_VALUE(pixel_list, term_is_list); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + avm_int_t offset = term_to_int(offset_term); + + avm_int_t index = 0; + term current = pixel_list; + while (!term_is_nil(current)) { + term color = term_get_list_head(current); + if (!term_is_tuple(color) || term_get_tuple_arity(color) != 3) { + TRACE("Invalid color tuple at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + term r_term = term_get_tuple_element(color, 0); + term g_term = term_get_tuple_element(color, 1); + term b_term = term_get_tuple_element(color, 2); + + if (!term_is_integer(r_term) || !term_is_integer(g_term) || !term_is_integer(b_term)) { + TRACE("Invalid color values at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_pixel(strip, offset + index, term_to_int(r_term), term_to_int(g_term), term_to_int(b_term)); + if (err != ESP_OK) { + TRACE("Failed to set pixel %i. err=%i\n", offset + index, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } term error_tuple = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, term_from_int(err)); return error_tuple; } + + current = term_get_list_tail(current); + index++; } + TRACE("Set %i pixels from list starting at offset %i\n", index, offset); + return OK_ATOM; +} - rmt_channel_t rmt_channel = get_rmt_channel(ctx, channel); - err = rmt_driver_uninstall(term_to_int(rmt_channel)); - if (err != ESP_OK) { - TRACE("Failed to uninstall rmt driver. err=%i\n", err); - if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } else { + +static term nif_set_pixels_rgbw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term offset_term = argv[1]; + VALIDATE_VALUE(offset_term, term_is_integer); + term pixel_list = argv[2]; + VALIDATE_VALUE(pixel_list, term_is_list); + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + avm_int_t offset = term_to_int(offset_term); + + avm_int_t index = 0; + term current = pixel_list; + while (!term_is_nil(current)) { + term color = term_get_list_head(current); + if (!term_is_tuple(color) || term_get_tuple_arity(color) != 4) { + TRACE("Invalid color tuple at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + term r_term = term_get_tuple_element(color, 0); + term g_term = term_get_tuple_element(color, 1); + term b_term = term_get_tuple_element(color, 2); + term w_term = term_get_tuple_element(color, 3); + + if (!term_is_integer(r_term) || !term_is_integer(g_term) || !term_is_integer(b_term) || !term_is_integer(w_term)) { + TRACE("Invalid color values at index %i\n", index); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, BADARG_ATOM); + return error_tuple; + } + + esp_err_t err = strip->set_pixel_rgbw(strip, offset + index, term_to_int(r_term), term_to_int(g_term), term_to_int(b_term), term_to_int(w_term)); + if (err == ESP_ERR_NOT_SUPPORTED) { + TRACE("set_pixels_rgbw called on non-RGBW strip\n"); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(ctx->global, not_supported_atom)); + return error_tuple; + } + if (err != ESP_OK) { + TRACE("Failed to set pixel %i. err=%i\n", offset + index, err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } term error_tuple = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, term_from_int(err)); return error_tuple; } + + current = term_get_list_tail(current); + index++; + } + TRACE("Set %i RGBW pixels from list starting at offset %i\n", index, offset); + return OK_ATOM; +} + + +static term nif_tini(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term handle = argv[0]; + VALIDATE_VALUE(handle, term_is_binary); + term channel = argv[1]; + VALIDATE_VALUE(channel, term_is_atom); + // Note: channel argument is kept for API compatibility but ignored in ESP-IDF 5.x + + avm_led_strip_t *strip = (avm_led_strip_t *) binary_to_ptr(handle); + + esp_err_t err = strip->del(strip); + if (err != ESP_OK) { + TRACE("Failed to delete led strip. err=%i\n", err); + if (UNLIKELY(memory_ensure_free(ctx, 3) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, term_from_int(err)); + return error_tuple; } - TRACE("LED strip niti'd\n"); + + TRACE("LED strip tini'd\n"); return OK_ATOM; } @@ -324,11 +730,61 @@ static const struct Nif set_pixel_hsv_nif = .base.type = NIFFunctionType, .nif_ptr = nif_set_pixel_hsv }; +static const struct Nif set_pixel_hsvw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixel_hsvw +}; static const struct Nif set_pixel_rgb_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_set_pixel_rgb }; +static const struct Nif set_pixel_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixel_rgbw +}; +static const struct Nif set_brightness_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_brightness +}; +static const struct Nif get_brightness_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_get_brightness +}; +static const struct Nif fill_rgb_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_rgb +}; +static const struct Nif fill_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_rgbw +}; +static const struct Nif fill_hsv_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_hsv +}; +static const struct Nif fill_hsvw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_fill_hsvw +}; +static const struct Nif set_pixels_rgb_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixels_rgb +}; +static const struct Nif set_pixels_rgbw_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_set_pixels_rgbw +}; static const struct Nif tini_nif = { .base.type = NIFFunctionType, @@ -348,7 +804,7 @@ void atomvm_neopixel_init(GlobalContext *global) const struct Nif *atomvm_neopixel_get_nif(const char *nifname) { TRACE("Locating nif %s ...", nifname); - if (strcmp("neopixel:nif_init/3", nifname) == 0) { + if (strcmp("neopixel:nif_init/4", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &init_nif; } @@ -364,10 +820,50 @@ const struct Nif *atomvm_neopixel_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &set_pixel_rgb_nif; } + if (strcmp("neopixel:nif_set_pixel_rgbw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixel_rgbw_nif; + } if (strcmp("neopixel:nif_set_pixel_hsv/5", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &set_pixel_hsv_nif; } + if (strcmp("neopixel:nif_set_pixel_hsvw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixel_hsvw_nif; + } + if (strcmp("neopixel:nif_set_brightness/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_brightness_nif; + } + if (strcmp("neopixel:nif_get_brightness/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &get_brightness_nif; + } + if (strcmp("neopixel:nif_fill_rgb/5", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_rgb_nif; + } + if (strcmp("neopixel:nif_fill_rgbw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_rgbw_nif; + } + if (strcmp("neopixel:nif_fill_hsv/5", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_hsv_nif; + } + if (strcmp("neopixel:nif_fill_hsvw/6", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &fill_hsvw_nif; + } + if (strcmp("neopixel:nif_set_pixels_rgb/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixels_rgb_nif; + } + if (strcmp("neopixel:nif_set_pixels_rgbw/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &set_pixels_rgbw_nif; + } if (strcmp("neopixel:nif_tini/2", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &tini_nif; diff --git a/nifs/include/atomvm_led_strip.h b/nifs/include/atomvm_led_strip.h new file mode 100644 index 00000000..ff1005ef --- /dev/null +++ b/nifs/include/atomvm_led_strip.h @@ -0,0 +1,88 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// Copyright 2024 dushin.net (modifications for AtomVM) +// +// 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. + +// Internal AtomVM LED strip interface +// This wraps the ESP-IDF led_strip component with our interface + +#ifndef ATOMVM_LED_STRIP_H +#define ATOMVM_LED_STRIP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" +#include + +/** + * @brief AtomVM LED Strip Type (forward declaration) + */ +typedef struct avm_led_strip_s avm_led_strip_t; + +/** + * @brief AtomVM LED Strip interface structure + */ +struct avm_led_strip_s { + esp_err_t (*set_pixel)(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); + esp_err_t (*set_pixel_rgbw)(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); + esp_err_t (*refresh)(avm_led_strip_t *strip, uint32_t timeout_ms); + esp_err_t (*clear)(avm_led_strip_t *strip, uint32_t timeout_ms); + esp_err_t (*del)(avm_led_strip_t *strip); + esp_err_t (*set_brightness)(avm_led_strip_t *strip, uint8_t brightness); + uint8_t (*get_brightness)(avm_led_strip_t *strip); + esp_err_t (*fill)(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); + esp_err_t (*fill_rgbw)(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); +}; + +/** + * @brief LED Strip Type (RGB vs RGBW) + */ +typedef enum { + AVM_LED_STRIP_RGB = 0, /*!< RGB LEDs (WS2812, WS2812B) - 3 bytes per pixel */ + AVM_LED_STRIP_RGBW = 1, /*!< RGBW LEDs (SK6812) - 4 bytes per pixel */ +} avm_led_strip_type_t; + +/** + * @brief AtomVM LED Strip Configuration + */ +typedef struct { + uint32_t max_leds; /*!< Maximum LEDs in a single strip */ + int gpio_num; /*!< GPIO number */ + uint8_t brightness; /*!< Global brightness (0-255), default 255 */ + avm_led_strip_type_t led_type; /*!< LED type (RGB or RGBW), default RGB */ +} avm_led_strip_config_t; + +/** + * @brief Create a new LED strip driver + * + * Uses ESP-IDF led_strip component internally with automatic backend selection: + * - ESP32-S3/C6: RMT with DMA (best performance) + * - ESP32/C3: RMT without DMA, falls back to SPI if needed + * + * @param config LED strip configuration + * @return LED strip instance or NULL on failure + */ +avm_led_strip_t *avm_led_strip_new(const avm_led_strip_config_t *config); + +/** + * @brief Convert HSV to RGB color space + */ +void avm_led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b); + +#ifdef __cplusplus +} +#endif + +#endif // ATOMVM_LED_STRIP_H diff --git a/nifs/include/led_strip.h b/nifs/include/led_strip.h deleted file mode 100644 index 64e86324..00000000 --- a/nifs/include/led_strip.h +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// 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. -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_err.h" - -/** -* @brief LED Strip Type -* -*/ -typedef struct led_strip_s led_strip_t; - -/** -* @brief LED Strip Device Type -* -*/ -typedef void *led_strip_dev_t; - -/** -* @brief Declare of LED Strip Type -* -*/ -struct led_strip_s { - /** - * @brief Set RGB for a specific pixel - * - * @param strip: LED strip - * @param index: index of pixel to set - * @param red: red part of color - * @param green: green part of color - * @param blue: blue part of color - * - * @return - * - ESP_OK: Set RGB for a specific pixel successfully - * - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters - * - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred - */ - esp_err_t (*set_pixel)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); - - /** - * @brief Refresh memory colors to LEDs - * - * @param strip: LED strip - * @param timeout_ms: timeout value for refreshing task - * - * @return - * - ESP_OK: Refresh successfully - * - ESP_ERR_TIMEOUT: Refresh failed because of timeout - * - ESP_FAIL: Refresh failed because some other error occurred - * - * @note: - * After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip. - */ - esp_err_t (*refresh)(led_strip_t *strip, uint32_t timeout_ms); - - /** - * @brief Clear LED strip (turn off all LEDs) - * - * @param strip: LED strip - * @param timeout_ms: timeout value for clearing task - * - * @return - * - ESP_OK: Clear LEDs successfully - * - ESP_ERR_TIMEOUT: Clear LEDs failed because of timeout - * - ESP_FAIL: Clear LEDs failed because some other error occurred - */ - esp_err_t (*clear)(led_strip_t *strip, uint32_t timeout_ms); - - /** - * @brief Free LED strip resources - * - * @param strip: LED strip - * - * @return - * - ESP_OK: Free resources successfully - * - ESP_FAIL: Free resources failed because error occurred - */ - esp_err_t (*del)(led_strip_t *strip); -}; - -/** -* @brief LED Strip Configuration Type -* -*/ -typedef struct { - uint32_t max_leds; /*!< Maximum LEDs in a single strip */ - led_strip_dev_t dev; /*!< LED strip device (e.g. RMT channel, PWM channel, etc) */ -} led_strip_config_t; - -/** - * @brief Default configuration for LED strip - * - */ -#define LED_STRIP_DEFAULT_CONFIG(number, dev_hdl) \ - { \ - .max_leds = number, \ - .dev = dev_hdl, \ - } - -/** -* @brief Install a new ws2812 driver (based on RMT peripheral) -* -* @param config: LED strip configuration -* @return -* LED strip instance or NULL -*/ -led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config); - -void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b); - -#ifdef __cplusplus -} -#endif diff --git a/nifs/led_strip_driver.c b/nifs/led_strip_driver.c new file mode 100644 index 00000000..d7fc358f --- /dev/null +++ b/nifs/led_strip_driver.c @@ -0,0 +1,455 @@ +// +// Copyright (c) 2021-2024 dushin.net +// All rights reserved. +// +// 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. +// +// This implementation uses the ESP-IDF led_strip component for better +// WiFi coexistence and simpler maintenance. +// + +#include +#include +#include "sdkconfig.h" +#include "esp_log.h" + +// ESP-IDF led_strip component +#include "led_strip.h" + +// Our internal interface +#include "atomvm_led_strip.h" + +static const char *TAG = "avm_led_strip"; + +// Internal wrapper structure +typedef struct { + avm_led_strip_t parent; // Our interface (function pointers) + led_strip_handle_t idf_strip; // ESP-IDF led_strip handle + uint32_t strip_len; + uint8_t brightness; + uint8_t bytes_per_pixel; + avm_led_strip_type_t led_type; + uint8_t *pixel_buf; // Raw pixel values (before brightness) +} strip_wrapper_t; + +// Forward declarations +static esp_err_t wrapper_set_pixel(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue); +static esp_err_t wrapper_set_pixel_rgbw(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); +static esp_err_t wrapper_refresh(avm_led_strip_t *strip, uint32_t timeout_ms); +static esp_err_t wrapper_clear(avm_led_strip_t *strip, uint32_t timeout_ms); +static esp_err_t wrapper_del(avm_led_strip_t *strip); +static esp_err_t wrapper_set_brightness(avm_led_strip_t *strip, uint8_t brightness); +static uint8_t wrapper_get_brightness(avm_led_strip_t *strip); +static esp_err_t wrapper_fill(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue); +static esp_err_t wrapper_fill_rgbw(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white); + +// Apply brightness to a color value +static inline uint8_t apply_brightness(uint8_t value, uint8_t brightness) +{ + if (brightness == 255) return value; + return (uint8_t)(((uint16_t)value * (brightness + 1)) >> 8); +} + +static esp_err_t wrapper_set_pixel(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (index >= wrapper->strip_len) { + return ESP_ERR_INVALID_ARG; + } + + // Store raw values in our buffer + uint32_t offset = index * wrapper->bytes_per_pixel; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + if (wrapper->bytes_per_pixel == 4) { + wrapper->pixel_buf[offset + 3] = 0; + } + + // Apply brightness and send to ESP-IDF driver + uint8_t br = wrapper->brightness; + return led_strip_set_pixel(wrapper->idf_strip, index, + apply_brightness(red, br), + apply_brightness(green, br), + apply_brightness(blue, br)); +} + +static esp_err_t wrapper_set_pixel_rgbw(avm_led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (wrapper->led_type != AVM_LED_STRIP_RGBW) { + return ESP_ERR_NOT_SUPPORTED; + } + + if (index >= wrapper->strip_len) { + return ESP_ERR_INVALID_ARG; + } + + // Store raw values + uint32_t offset = index * 4; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + wrapper->pixel_buf[offset + 3] = white & 0xFF; + + // Apply brightness and send to ESP-IDF driver + uint8_t br = wrapper->brightness; + return led_strip_set_pixel_rgbw(wrapper->idf_strip, index, + apply_brightness(red, br), + apply_brightness(green, br), + apply_brightness(blue, br), + apply_brightness(white, br)); +} + +static esp_err_t wrapper_refresh(avm_led_strip_t *strip, uint32_t timeout_ms) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + (void)timeout_ms; // ESP-IDF driver doesn't use timeout + + return led_strip_refresh(wrapper->idf_strip); +} + +static esp_err_t wrapper_clear(avm_led_strip_t *strip, uint32_t timeout_ms) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + (void)timeout_ms; + + // Clear our buffer + memset(wrapper->pixel_buf, 0, wrapper->strip_len * wrapper->bytes_per_pixel); + + return led_strip_clear(wrapper->idf_strip); +} + +static esp_err_t wrapper_del(avm_led_strip_t *strip) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + esp_err_t ret = led_strip_del(wrapper->idf_strip); + + if (wrapper->pixel_buf) { + free(wrapper->pixel_buf); + } + free(wrapper); + + return ret; +} + +static esp_err_t wrapper_set_brightness(avm_led_strip_t *strip, uint8_t brightness) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + wrapper->brightness = brightness; + + // Re-apply brightness to all pixels + uint8_t br = brightness; + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * wrapper->bytes_per_pixel; + uint8_t r = wrapper->pixel_buf[offset + 0]; + uint8_t g = wrapper->pixel_buf[offset + 1]; + uint8_t b = wrapper->pixel_buf[offset + 2]; + + if (wrapper->bytes_per_pixel == 4) { + uint8_t w = wrapper->pixel_buf[offset + 3]; + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, + apply_brightness(r, br), + apply_brightness(g, br), + apply_brightness(b, br), + apply_brightness(w, br)); + } else { + led_strip_set_pixel(wrapper->idf_strip, i, + apply_brightness(r, br), + apply_brightness(g, br), + apply_brightness(b, br)); + } + } + + return ESP_OK; +} + +static uint8_t wrapper_get_brightness(avm_led_strip_t *strip) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + return wrapper->brightness; +} + +static esp_err_t wrapper_fill(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + uint8_t br = wrapper->brightness; + uint8_t r = apply_brightness(red, br); + uint8_t g = apply_brightness(green, br); + uint8_t b = apply_brightness(blue, br); + + // Store in our buffer and set in ESP-IDF driver + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * wrapper->bytes_per_pixel; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + if (wrapper->bytes_per_pixel == 4) { + wrapper->pixel_buf[offset + 3] = 0; + } + + if (wrapper->bytes_per_pixel == 4) { + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, r, g, b, 0); + } else { + led_strip_set_pixel(wrapper->idf_strip, i, r, g, b); + } + } + + return ESP_OK; +} + +static esp_err_t wrapper_fill_rgbw(avm_led_strip_t *strip, uint32_t red, uint32_t green, uint32_t blue, uint32_t white) +{ + strip_wrapper_t *wrapper = __containerof(strip, strip_wrapper_t, parent); + + if (wrapper->led_type != AVM_LED_STRIP_RGBW) { + return ESP_ERR_NOT_SUPPORTED; + } + + uint8_t br = wrapper->brightness; + uint8_t r = apply_brightness(red, br); + uint8_t g = apply_brightness(green, br); + uint8_t b = apply_brightness(blue, br); + uint8_t w = apply_brightness(white, br); + + for (uint32_t i = 0; i < wrapper->strip_len; i++) { + uint32_t offset = i * 4; + wrapper->pixel_buf[offset + 0] = red & 0xFF; + wrapper->pixel_buf[offset + 1] = green & 0xFF; + wrapper->pixel_buf[offset + 2] = blue & 0xFF; + wrapper->pixel_buf[offset + 3] = white & 0xFF; + + led_strip_set_pixel_rgbw(wrapper->idf_strip, i, r, g, b, w); + } + + return ESP_OK; +} + +avm_led_strip_t *avm_led_strip_new(const avm_led_strip_config_t *config) +{ + if (!config) { + ESP_LOGE(TAG, "Configuration cannot be null"); + return NULL; + } + + uint8_t bytes_per_pixel = (config->led_type == AVM_LED_STRIP_RGBW) ? 4 : 3; + + // Allocate wrapper structure + strip_wrapper_t *wrapper = calloc(1, sizeof(strip_wrapper_t)); + if (!wrapper) { + ESP_LOGE(TAG, "Failed to allocate wrapper"); + return NULL; + } + + // Allocate pixel buffer + wrapper->pixel_buf = calloc(config->max_leds, bytes_per_pixel); + if (!wrapper->pixel_buf) { + ESP_LOGE(TAG, "Failed to allocate pixel buffer"); + free(wrapper); + return NULL; + } + + // Configure ESP-IDF led_strip + led_strip_config_t strip_config = { + .strip_gpio_num = config->gpio_num, + .max_leds = config->max_leds, + .led_model = LED_MODEL_WS2812, + .color_component_format = (config->led_type == AVM_LED_STRIP_RGBW) ? LED_STRIP_COLOR_COMPONENT_FMT_GRBW : LED_STRIP_COLOR_COMPONENT_FMT_GRB, + .flags.invert_out = false, + }; + + esp_err_t ret = ESP_FAIL; + +// Helper macros for backend selection +#if defined(CONFIG_AVM_NEOPIXEL_BACKEND_SPI) + #define TRY_SPI_FIRST 1 + #define TRY_RMT_FIRST 0 +#elif defined(CONFIG_AVM_NEOPIXEL_BACKEND_RMT) + #define TRY_SPI_FIRST 0 + #define TRY_RMT_FIRST 1 +#else + // Auto mode: prefer SPI on ESP32 (original), RMT elsewhere + #if CONFIG_IDF_TARGET_ESP32 + #define TRY_SPI_FIRST 1 + #define TRY_RMT_FIRST 0 + #else + #define TRY_SPI_FIRST 0 + #define TRY_RMT_FIRST 1 + #endif +#endif + +#if TRY_SPI_FIRST + // Try SPI backend first + { + led_strip_spi_config_t spi_config = { + .spi_bus = SPI2_HOST, + .flags.with_dma = true, + }; + + ret = led_strip_new_spi_device(&strip_config, &spi_config, &wrapper->idf_strip); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Using SPI backend with DMA"); + } else { + ESP_LOGW(TAG, "SPI backend failed (err=%d), trying RMT", ret); + } + } + + // Fallback to RMT + if (ret != ESP_OK) { + led_strip_rmt_config_t rmt_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10 * 1000 * 1000, +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + .mem_block_symbols = 64, + .flags.with_dma = true, +#elif CONFIG_IDF_TARGET_ESP32 + .mem_block_symbols = 192, + .flags.with_dma = false, +#else + .mem_block_symbols = 64, + .flags.with_dma = false, +#endif + }; + ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &wrapper->idf_strip); + if (ret == ESP_OK) { +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + ESP_LOGI(TAG, "Using RMT with DMA"); +#else + ESP_LOGW(TAG, "Using RMT without DMA (may flicker with WiFi)"); +#endif + } + } +#else // TRY_RMT_FIRST + // Try RMT backend first + { + led_strip_rmt_config_t rmt_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10 * 1000 * 1000, +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + .mem_block_symbols = 64, + .flags.with_dma = true, +#elif CONFIG_IDF_TARGET_ESP32 + .mem_block_symbols = 192, + .flags.with_dma = false, +#else + .mem_block_symbols = 64, + .flags.with_dma = false, +#endif + }; + ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &wrapper->idf_strip); + if (ret == ESP_OK) { +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4 + ESP_LOGI(TAG, "Using RMT with DMA"); +#else + ESP_LOGI(TAG, "Using RMT without DMA"); +#endif + } else { + ESP_LOGW(TAG, "RMT backend failed (err=%d), trying SPI", ret); + } + } + + // Fallback to SPI + if (ret != ESP_OK) { + led_strip_spi_config_t spi_config = { + .spi_bus = SPI2_HOST, + .flags.with_dma = true, + }; + ret = led_strip_new_spi_device(&strip_config, &spi_config, &wrapper->idf_strip); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Using SPI backend with DMA"); + } + } +#endif + +#undef TRY_SPI_FIRST +#undef TRY_RMT_FIRST + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to create LED strip: %d", ret); + free(wrapper->pixel_buf); + free(wrapper); + return NULL; + } + + // Initialize wrapper + wrapper->strip_len = config->max_leds; + wrapper->brightness = config->brightness ? config->brightness : 255; + wrapper->bytes_per_pixel = bytes_per_pixel; + wrapper->led_type = config->led_type; + + // Set up function pointers + wrapper->parent.set_pixel = wrapper_set_pixel; + wrapper->parent.set_pixel_rgbw = wrapper_set_pixel_rgbw; + wrapper->parent.refresh = wrapper_refresh; + wrapper->parent.clear = wrapper_clear; + wrapper->parent.del = wrapper_del; + wrapper->parent.set_brightness = wrapper_set_brightness; + wrapper->parent.get_brightness = wrapper_get_brightness; + wrapper->parent.fill = wrapper_fill; + wrapper->parent.fill_rgbw = wrapper_fill_rgbw; + + ESP_LOGI(TAG, "LED strip initialized: %lu LEDs, %s", + (unsigned long)config->max_leds, + config->led_type == AVM_LED_STRIP_RGBW ? "RGBW" : "RGB"); + + return &wrapper->parent; +} + +void avm_led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) +{ + h %= 360; + uint32_t rgb_max = (v * 255 + 50) / 100; + uint32_t rgb_min = rgb_max * (100 - s) / 100; + + uint32_t i = h / 60; + uint32_t diff = h % 60; + uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; + + switch (i) { + case 0: + *r = rgb_max; + *g = rgb_min + rgb_adj; + *b = rgb_min; + break; + case 1: + *r = rgb_max - rgb_adj; + *g = rgb_max; + *b = rgb_min; + break; + case 2: + *r = rgb_min; + *g = rgb_max; + *b = rgb_min + rgb_adj; + break; + case 3: + *r = rgb_min; + *g = rgb_max - rgb_adj; + *b = rgb_max; + break; + case 4: + *r = rgb_min + rgb_adj; + *g = rgb_min; + *b = rgb_max; + break; + default: + *r = rgb_max; + *g = rgb_min; + *b = rgb_max - rgb_adj; + break; + } +} diff --git a/nifs/led_strip_rmt_ws2812.c b/nifs/led_strip_rmt_ws2812.c deleted file mode 100644 index 9985f4f7..00000000 --- a/nifs/led_strip_rmt_ws2812.c +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// 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. -#include -#include -#include -#include "esp_log.h" -#include "esp_attr.h" -#include "led_strip.h" -#include "driver/rmt.h" - -static const char *TAG = "ws2812"; -#define STRIP_CHECK(a, str, goto_tag, ret_value, ...) \ - do \ - { \ - if (!(a)) \ - { \ - ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ - ret = ret_value; \ - goto goto_tag; \ - } \ - } while (0) - -#define WS2812_T0H_NS (350) -#define WS2812_T0L_NS (1000) -#define WS2812_T1H_NS (1000) -#define WS2812_T1L_NS (350) -#define WS2812_RESET_US (280) - -static uint32_t ws2812_t0h_ticks = 0; -static uint32_t ws2812_t1h_ticks = 0; -static uint32_t ws2812_t0l_ticks = 0; -static uint32_t ws2812_t1l_ticks = 0; - -typedef struct { - led_strip_t parent; - rmt_channel_t rmt_channel; - uint32_t strip_len; - uint8_t buffer[0]; -} ws2812_t; - -/** - * @brief Conver RGB data to RMT format. - * - * @note For WS2812, R,G,B each contains 256 different choices (i.e. uint8_t) - * - * @param[in] src: source data, to converted to RMT format - * @param[in] dest: place where to store the convert result - * @param[in] src_size: size of source data - * @param[in] wanted_num: number of RMT items that want to get - * @param[out] translated_size: number of source data that got converted - * @param[out] item_num: number of RMT items which are converted from source data - */ -static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size, - size_t wanted_num, size_t *translated_size, size_t *item_num) -{ - if (src == NULL || dest == NULL) { - *translated_size = 0; - *item_num = 0; - return; - } - const rmt_item32_t bit0 = {{{ ws2812_t0h_ticks, 1, ws2812_t0l_ticks, 0 }}}; //Logical 0 - const rmt_item32_t bit1 = {{{ ws2812_t1h_ticks, 1, ws2812_t1l_ticks, 0 }}}; //Logical 1 - size_t size = 0; - size_t num = 0; - uint8_t *psrc = (uint8_t *)src; - rmt_item32_t *pdest = dest; - while (size < src_size && num < wanted_num) { - for (int i = 0; i < 8; i++) { - // MSB first - if (*psrc & (1 << (7 - i))) { - pdest->val = bit1.val; - } else { - pdest->val = bit0.val; - } - num++; - pdest++; - } - size++; - psrc++; - } - *translated_size = size; - *item_num = num; -} - -static esp_err_t ws2812_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue) -{ - esp_err_t ret = ESP_OK; - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - STRIP_CHECK(index < ws2812->strip_len, "index out of the maximum number of leds", err, ESP_ERR_INVALID_ARG); - uint32_t start = index * 3; - // In thr order of GRB - ws2812->buffer[start + 0] = green & 0xFF; - ws2812->buffer[start + 1] = red & 0xFF; - ws2812->buffer[start + 2] = blue & 0xFF; - return ESP_OK; -err: - return ret; -} - -static esp_err_t ws2812_refresh(led_strip_t *strip, uint32_t timeout_ms) -{ - esp_err_t ret = ESP_OK; - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - STRIP_CHECK(rmt_write_sample(ws2812->rmt_channel, ws2812->buffer, ws2812->strip_len * 3, true) == ESP_OK, - "transmit RMT samples failed", err, ESP_FAIL); - return rmt_wait_tx_done(ws2812->rmt_channel, pdMS_TO_TICKS(timeout_ms)); -err: - return ret; -} - -static esp_err_t ws2812_clear(led_strip_t *strip, uint32_t timeout_ms) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - // Write zero to turn off all leds - memset(ws2812->buffer, 0, ws2812->strip_len * 3); - return ws2812_refresh(strip, timeout_ms); -} - -static esp_err_t ws2812_del(led_strip_t *strip) -{ - ws2812_t *ws2812 = __containerof(strip, ws2812_t, parent); - free(ws2812); - return ESP_OK; -} - -led_strip_t *led_strip_new_rmt_ws2812(const led_strip_config_t *config) -{ - led_strip_t *ret = NULL; - STRIP_CHECK(config, "configuration can't be null", err, NULL); - - // 24 bits per led - uint32_t ws2812_size = sizeof(ws2812_t) + config->max_leds * 3; - ws2812_t *ws2812 = calloc(1, ws2812_size); - STRIP_CHECK(ws2812, "request memory for ws2812 failed", err, NULL); - - uint32_t counter_clk_hz = 0; - STRIP_CHECK(rmt_get_counter_clock((rmt_channel_t)config->dev, &counter_clk_hz) == ESP_OK, - "get rmt counter clock failed", err, NULL); - // ns -> ticks - float ratio = (float)counter_clk_hz / 1e9; - ws2812_t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS); - ws2812_t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS); - ws2812_t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS); - ws2812_t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS); - - // set ws2812 to rmt adapter - rmt_translator_init((rmt_channel_t)config->dev, ws2812_rmt_adapter); - - ws2812->rmt_channel = (rmt_channel_t)config->dev; - ws2812->strip_len = config->max_leds; - - ws2812->parent.set_pixel = ws2812_set_pixel; - ws2812->parent.refresh = ws2812_refresh; - ws2812->parent.clear = ws2812_clear; - ws2812->parent.del = ws2812_del; - - return &ws2812->parent; -err: - return ret; -} - -void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) -{ - h %= 360; // h -> [0,360] - uint32_t rgb_max = v * 2.55f; - uint32_t rgb_min = rgb_max * (100 - s) / 100.0f; - - uint32_t i = h / 60; - uint32_t diff = h % 60; - - // RGB adjustment amount by hue - uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; - - switch (i) { - case 0: - *r = rgb_max; - *g = rgb_min + rgb_adj; - *b = rgb_min; - break; - case 1: - *r = rgb_max - rgb_adj; - *g = rgb_max; - *b = rgb_min; - break; - case 2: - *r = rgb_min; - *g = rgb_max; - *b = rgb_min + rgb_adj; - break; - case 3: - *r = rgb_min; - *g = rgb_max - rgb_adj; - *b = rgb_max; - break; - case 4: - *r = rgb_min + rgb_adj; - *g = rgb_min; - *b = rgb_max; - break; - default: - *r = rgb_max; - *g = rgb_min; - *b = rgb_max - rgb_adj; - break; - } -} diff --git a/src/neopixel.erl b/src/neopixel.erl index 21f60dbd..84c19330 100644 --- a/src/neopixel.erl +++ b/src/neopixel.erl @@ -15,37 +15,56 @@ %% limitations under the License. %% %%----------------------------------------------------------------------------- -%% @doc WS2812 ("Neopixel") support. +%% @doc WS2812/SK6812 ("Neopixel") support. %% -%% Use this module to drive a strip of WS2812 "noepixel" LED strips. +%% Use this module to drive a strip of WS2812 or SK6812 "NeoPixel" LED strips. %% %% Each LED in a strip is individually addressable and can be configured in -%% 24-bit color, using either a Red-Green-Blue (RGB) or Hue-Saturation-Value (HSV) -%% color space. +%% 24-bit color (RGB) or 32-bit color (RGBW for SK6812), using either a +%% Red-Green-Blue (RGB/RGBW) or Hue-Saturation-Value (HSV) color space. +%% +%% Global brightness control is supported (0-255). +%% +%% Options: +%%
    +%%
  • `led_type' - `rgb' (default) or `rgbw' for SK6812 RGBW strips
  • +%%
  • `brightness' - Global brightness 0-255 (default 255)
  • +%%
  • `timeout' - Refresh timeout in ms (default 100)
  • +%%
  • `channel' - RMT channel (legacy, ignored in ESP-IDF 5.x)
  • +%%
%% @end %%----------------------------------------------------------------------------- -module(neopixel). -export([ - start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_hsv/5, refresh/1 + start/2, start/3, stop/1, clear/1, set_pixel_rgb/5, set_pixel_rgbw/6, set_pixel_hsv/5, + set_pixel_hsvw/6, refresh/1, set_brightness/2, get_brightness/1, + fill_rgb/4, fill_rgbw/5, fill_hsv/4, fill_hsvw/5, + set_pixels_rgb/2, set_pixels_rgb/3, set_pixels_rgbw/2, set_pixels_rgbw/3 ]). --export([nif_init/3, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_rgb/5, nif_tini/2]). %% internal nif APIs +-export([nif_init/4, nif_clear/2, nif_refresh/2, nif_set_pixel_hsv/5, nif_set_pixel_hsvw/6, + nif_set_pixel_rgb/5, nif_set_pixel_rgbw/6, nif_tini/2, nif_set_brightness/2, + nif_get_brightness/1, nif_fill_rgb/5, nif_fill_rgbw/6, nif_fill_hsv/5, nif_fill_hsvw/6, + nif_set_pixels_rgb/3, nif_set_pixels_rgbw/3]). %% internal nif APIs -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -behaviour(gen_server). -type neopixel() :: term(). -type pin() :: non_neg_integer(). --type options() :: [option()]. --type option() :: #{timeout => non_neg_integer(), channel => channel()}. +-type options() :: map() | proplists:proplist(). -type channel() :: channel_0 | channel_1 | channel_2 | channel_3. +-type led_type() :: rgb | rgbw. -type color() :: 0..255. +-type brightness() :: 0..255. -type hue() :: 0..359. -type saturation() :: 0..100. -type value() :: 0..100. +-type rgb_color() :: {color(), color(), color()}. +-type rgbw_color() :: {color(), color(), color(), color()}. --define(DEFAULT_OPTIONS, #{timeout => 100, channel => channel_0}). +-define(DEFAULT_OPTIONS, #{timeout => 100, channel => channel_0, led_type => rgb}). -record(state, { pin :: pin(), @@ -76,7 +95,9 @@ start(Pin, NumPixels) -> %%----------------------------------------------------------------------------- -spec start(Pin::pin(), NumPixels::non_neg_integer(), Options::options()) -> {ok, neopixel()} | {error, Reason::term()}. start(Pin, NumPixels, Options) -> - gen_server:start(?MODULE, [Pin, NumPixels, validate_options(maps:merge(Options, ?DEFAULT_OPTIONS))], []). + NormalizedOpts = normalize_options(Options), + MergedOpts = maps:merge(?DEFAULT_OPTIONS, NormalizedOpts), + gen_server:start(?MODULE, [Pin, NumPixels, validate_options(MergedOpts)], []). %%----------------------------------------------------------------------------- %% @returns ok @@ -126,6 +147,25 @@ set_pixel_rgb(Neopixel, I, R, G, B) when is_pid(Neopixel), 0 =< R, R =< 255, 0 = set_pixel_rgb(_Neopixel, _I, _R, _G, _B) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param I pixel index (`0..NumPixels - 1') +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set a pixel value in the RGBW color space (for SK6812 RGBW strips). +%% +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixel_rgbw(Neopixel::neopixel(), I::non_neg_integer(), R::color(), G::color(), B::color(), W::color()) -> ok | {error, Reason::term()}. +set_pixel_rgbw(Neopixel, I, R, G, B, W) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {set_pixel_rgbw, I, R, G, B, W}); +set_pixel_rgbw(_Neopixel, _I, _R, _G, _B, _W) -> + throw(badarg). + %%----------------------------------------------------------------------------- %% @param Neopixel Neopixel instance %% @param I pixel index (`0..NumPixels - 1') @@ -143,13 +183,204 @@ set_pixel_hsv(Neopixel, I, H, S, V) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< set_pixel_hsv(_Neopixel, _I, _R, _G, _B) -> throw(badarg). +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param I pixel index (`0..NumPixels - 1') +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set a pixel value in the HSV color space with white channel (for SK6812 RGBW strips). +%% +%% The H, S, V values are converted to RGB, then combined with the white channel. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixel_hsvw(Neopixel::neopixel(), I::non_neg_integer(), H::hue(), S::saturation(), V::value(), W::color()) -> ok | {error, Reason::term()}. +set_pixel_hsvw(Neopixel, I, H, S, V, W) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {set_pixel_hsvw, I, H, S, V, W}); +set_pixel_hsvw(_Neopixel, _I, _H, _S, _V, _W) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Brightness Brightness value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Set global brightness for the strip. +%% +%% Brightness is applied when pixels are set. A value of 255 means full +%% brightness (no scaling), 128 means 50% brightness, 0 means off. +%% Note: You need to call refresh/1 and re-set pixels to see the effect. +%% @end +%%----------------------------------------------------------------------------- +-spec set_brightness(Neopixel::neopixel(), Brightness::brightness()) -> ok | {error, Reason::term()}. +set_brightness(Neopixel, Brightness) when is_pid(Neopixel), 0 =< Brightness, Brightness =< 255 -> + gen_server:call(Neopixel, {set_brightness, Brightness}); +set_brightness(_Neopixel, _Brightness) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @returns Brightness value (`0..255') +%% @doc Get current global brightness for the strip. +%% @end +%%----------------------------------------------------------------------------- +-spec get_brightness(Neopixel::neopixel()) -> brightness(). +get_brightness(Neopixel) when is_pid(Neopixel) -> + gen_server:call(Neopixel, get_brightness); +get_brightness(_Neopixel) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single RGB color. +%% +%% Sets all pixels to the same color. Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_rgb(Neopixel::neopixel(), R::color(), G::color(), B::color()) -> ok | {error, Reason::term()}. +fill_rgb(Neopixel, R, G, B) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255 -> + gen_server:call(Neopixel, {fill_rgb, R, G, B}); +fill_rgb(_Neopixel, _R, _G, _B) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param R Red value (`0..255') +%% @param G Green value (`0..255') +%% @param B Blue value (`0..255') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single RGBW color (for SK6812 RGBW strips). +%% +%% Sets all pixels to the same color. Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_rgbw(Neopixel::neopixel(), R::color(), G::color(), B::color(), W::color()) -> ok | {error, Reason::term()}. +fill_rgbw(Neopixel, R, G, B, W) when is_pid(Neopixel), 0 =< R, R =< 255, 0 =< G, G =< 255, 0 =< B, B =< 255, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {fill_rgbw, R, G, B, W}); +fill_rgbw(_Neopixel, _R, _G, _B, _W) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single HSV color. +%% +%% HSV is converted to RGB in the NIF. Sets all pixels to the same color. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_hsv(Neopixel::neopixel(), H::hue(), S::saturation(), V::value()) -> ok | {error, Reason::term()}. +fill_hsv(Neopixel, H, S, V) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100 -> + gen_server:call(Neopixel, {fill_hsv, H, S, V}); +fill_hsv(_Neopixel, _H, _S, _V) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param H Hue value (`0..359') +%% @param S Saturation value (`0..100') +%% @param V Value (`0..100') +%% @param W White value (`0..255') +%% @returns ok | {error, Reason} +%% @doc Fill entire strip with a single HSVW color (for SK6812 RGBW strips). +%% +%% HSV is converted to RGB in the NIF, combined with the white channel. +%% Sets all pixels to the same color. Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec fill_hsvw(Neopixel::neopixel(), H::hue(), S::saturation(), V::value(), W::color()) -> ok | {error, Reason::term()}. +fill_hsvw(Neopixel, H, S, V, W) when is_pid(Neopixel), 0 =< H, H < 360, 0 =< S, S =< 100, 0 =< V, V =< 100, 0 =< W, W =< 255 -> + gen_server:call(Neopixel, {fill_hsvw, H, S, V, W}); +fill_hsvw(_Neopixel, _H, _S, _V, _W) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Colors List of `{R, G, B}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGB colors. +%% +%% Each element in the list sets the corresponding pixel starting at index 0. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgb(Neopixel::neopixel(), Colors::[rgb_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgb(Neopixel, Colors) -> + set_pixels_rgb(Neopixel, 0, Colors). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Offset Starting pixel index (`0..NumPixels - 1') +%% @param Colors List of `{R, G, B}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGB colors starting at offset. +%% +%% Each element in the list sets the corresponding pixel starting at the given offset. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgb(Neopixel::neopixel(), Offset::non_neg_integer(), Colors::[rgb_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgb(Neopixel, Offset, Colors) when is_pid(Neopixel), is_integer(Offset), Offset >= 0, is_list(Colors) -> + gen_server:call(Neopixel, {set_pixels_rgb, Offset, Colors}); +set_pixels_rgb(_Neopixel, _Offset, _Colors) -> + throw(badarg). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Colors List of `{R, G, B, W}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGBW colors (for SK6812 RGBW strips). +%% +%% Each element in the list sets the corresponding pixel starting at index 0. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgbw(Neopixel::neopixel(), Colors::[rgbw_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgbw(Neopixel, Colors) -> + set_pixels_rgbw(Neopixel, 0, Colors). + +%%----------------------------------------------------------------------------- +%% @param Neopixel Neopixel instance +%% @param Offset Starting pixel index (`0..NumPixels - 1') +%% @param Colors List of `{R, G, B, W}' tuples +%% @returns ok | {error, Reason} +%% @doc Set multiple pixels from a list of RGBW colors starting at offset. +%% +%% Each element in the list sets the corresponding pixel starting at the given offset. +%% The list can be shorter than the strip length. +%% Call refresh/1 to display. +%% Returns `{error, not_supported}' if called on an RGB strip. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pixels_rgbw(Neopixel::neopixel(), Offset::non_neg_integer(), Colors::[rgbw_color()]) -> ok | {error, Reason::term()}. +set_pixels_rgbw(Neopixel, Offset, Colors) when is_pid(Neopixel), is_integer(Offset), Offset >= 0, is_list(Colors) -> + gen_server:call(Neopixel, {set_pixels_rgbw, Offset, Colors}); +set_pixels_rgbw(_Neopixel, _Offset, _Colors) -> + throw(badarg). + %% %% gen_server API %% %% @hidden init([Pin, NumPixels, Options]) -> - Handle = ?MODULE:nif_init(Pin, NumPixels, maps:get(channel, Options)), + Handle = ?MODULE:nif_init(Pin, NumPixels, maps:get(channel, Options), maps:get(led_type, Options)), {ok, #state{ pin=Pin, num_pixels=NumPixels, @@ -166,8 +397,28 @@ handle_call(refresh, _From, State) -> {reply, ?MODULE:nif_refresh(State#state.nif_handle, maps:get(timeout, State#state.options)), State}; handle_call({set_pixel_rgb, I, R, G, B}, _From, State) -> {reply, ?MODULE:nif_set_pixel_rgb(State#state.nif_handle, I, R, G, B), State}; +handle_call({set_pixel_rgbw, I, R, G, B, W}, _From, State) -> + {reply, ?MODULE:nif_set_pixel_rgbw(State#state.nif_handle, I, R, G, B, W), State}; handle_call({set_pixel_hsv, I, H, S, V}, _From, State) -> {reply, ?MODULE:nif_set_pixel_hsv(State#state.nif_handle, I, H, S, V), State}; +handle_call({set_pixel_hsvw, I, H, S, V, W}, _From, State) -> + {reply, ?MODULE:nif_set_pixel_hsvw(State#state.nif_handle, I, H, S, V, W), State}; +handle_call({set_brightness, Brightness}, _From, State) -> + {reply, ?MODULE:nif_set_brightness(State#state.nif_handle, Brightness), State}; +handle_call(get_brightness, _From, State) -> + {reply, ?MODULE:nif_get_brightness(State#state.nif_handle), State}; +handle_call({fill_rgb, R, G, B}, _From, State) -> + {reply, ?MODULE:nif_fill_rgb(State#state.nif_handle, State#state.num_pixels, R, G, B), State}; +handle_call({fill_rgbw, R, G, B, W}, _From, State) -> + {reply, ?MODULE:nif_fill_rgbw(State#state.nif_handle, State#state.num_pixels, R, G, B, W), State}; +handle_call({fill_hsv, H, S, V}, _From, State) -> + {reply, ?MODULE:nif_fill_hsv(State#state.nif_handle, State#state.num_pixels, H, S, V), State}; +handle_call({fill_hsvw, H, S, V, W}, _From, State) -> + {reply, ?MODULE:nif_fill_hsvw(State#state.nif_handle, State#state.num_pixels, H, S, V, W), State}; +handle_call({set_pixels_rgb, Offset, Colors}, _From, State) -> + {reply, ?MODULE:nif_set_pixels_rgb(State#state.nif_handle, Offset, Colors), State}; +handle_call({set_pixels_rgbw, Offset, Colors}, _From, State) -> + {reply, ?MODULE:nif_set_pixels_rgbw(State#state.nif_handle, Offset, Colors), State}; handle_call(Request, _From, State) -> {reply, {error, {unknown_request, Request}}, State}. @@ -191,10 +442,20 @@ code_change(_OldVsn, State, _Extra) -> %% internal operations %% +%% @private +%% @doc Convert options to map format, supporting both maps and proplists +normalize_options(Options) when is_map(Options) -> + Options; +normalize_options(Options) when is_list(Options) -> + maps:from_list(Options); +normalize_options(_) -> + throw(badarg). + %% @private validate_options(Options) -> validate_timeout_option(maps:get(timeout, Options, undefined)), validate_channel_option(maps:get(channel, Options, undefined)), + validate_led_type_option(maps:get(led_type, Options, undefined)), Options. %% @private @@ -215,13 +476,21 @@ validate_channel_option(channel_3) -> validate_channel_option(_Timeout) -> throw(badarg). +%% @private +validate_led_type_option(rgb) -> + ok; +validate_led_type_option(rgbw) -> + ok; +validate_led_type_option(_LedType) -> + throw(badarg). + %% %% Nifs %% %% @hidden -nif_init(_Pin, _NumPixels, _Channel) -> +nif_init(_Pin, _NumPixels, _Channel, _LedType) -> throw(nif_error). %% @hidden @@ -236,10 +505,50 @@ nif_refresh(_NifHandle, _Timeout) -> nif_set_pixel_rgb(_NifHandle, _Index, _Red, _Green, _Blue) -> throw(nif_error). +%% @hidden +nif_set_pixel_rgbw(_NifHandle, _Index, _Red, _Green, _Blue, _White) -> + throw(nif_error). + %% @hidden nif_set_pixel_hsv(_NifHandle, _Index, _Hue, _Saturation, _Value) -> throw(nif_error). +%% @hidden +nif_set_pixel_hsvw(_NifHandle, _Index, _Hue, _Saturation, _Value, _White) -> + throw(nif_error). + +%% @hidden +nif_set_brightness(_NifHandle, _Brightness) -> + throw(nif_error). + +%% @hidden +nif_get_brightness(_NifHandle) -> + throw(nif_error). + +%% @hidden +nif_fill_rgb(_NifHandle, _NumPixels, _Red, _Green, _Blue) -> + throw(nif_error). + +%% @hidden +nif_fill_rgbw(_NifHandle, _NumPixels, _Red, _Green, _Blue, _White) -> + throw(nif_error). + +%% @hidden +nif_fill_hsv(_NifHandle, _NumPixels, _Hue, _Saturation, _Value) -> + throw(nif_error). + +%% @hidden +nif_fill_hsvw(_NifHandle, _NumPixels, _Hue, _Saturation, _Value, _White) -> + throw(nif_error). + +%% @hidden +nif_set_pixels_rgb(_NifHandle, _Offset, _Colors) -> + throw(nif_error). + +%% @hidden +nif_set_pixels_rgbw(_NifHandle, _Offset, _Colors) -> + throw(nif_error). + %% @hidden nif_tini(_NifHandle, _Channel) -> throw(nif_error).