From c343efbdb32b4ccd39a8aad2c9b830f6e79c17c2 Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Sat, 20 Dec 2025 23:07:26 +0100 Subject: [PATCH 1/8] sanitize c string and data type change in AppFrame.to_c_string() --- examples/led-matrix-painter/python/app_frame.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/led-matrix-painter/python/app_frame.py b/examples/led-matrix-painter/python/app_frame.py index db66c9b..ecd0b35 100644 --- a/examples/led-matrix-painter/python/app_frame.py +++ b/examples/led-matrix-painter/python/app_frame.py @@ -146,10 +146,12 @@ def to_c_string(self) -> str: Returns: str: C source fragment containing a const array initializer. """ - c_type = "uint32_t" + c_type = "uint8_t" + # sanitize name into a safe C identifier + snake_name = re.sub(r'[^a-zA-Z0-9]', '_', self.name.lower()) scaled_arr = self.rescale_quantized_frame(scale_max=255) - parts = [f"const {c_type} {self.name}[] = {{"] + parts = [f"{c_type} {snake_name} [] = {{"] rows = scaled_arr.tolist() # Emit the array as row-major integer values, preserving row breaks for readability for r_idx, row in enumerate(rows): From cf025f258a0a3ec41eb398daa30c87b3d2159da7 Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Sat, 20 Dec 2025 23:07:47 +0100 Subject: [PATCH 2/8] add and improve AppFrame methods for animations --- .../led-matrix-painter/python/app_frame.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/examples/led-matrix-painter/python/app_frame.py b/examples/led-matrix-painter/python/app_frame.py index ecd0b35..47e470a 100644 --- a/examples/led-matrix-painter/python/app_frame.py +++ b/examples/led-matrix-painter/python/app_frame.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MPL-2.0 +import re import json from arduino.app_utils import Frame @@ -241,6 +242,53 @@ def to_animation_hex(self) -> list[str]: return hex_values + def to_animation_bytes(self) -> bytes: + """Return this frame encoded as 20 bytes (4 x uint32_t pixels + 1 x uint32_t duration) in little-endian. + """ + hex_values = self.to_animation_hex() + ba = bytearray() + for i in range(4): + value = int(hex_values[i], 16) + ba.extend(value.to_bytes(4, byteorder='little')) + duration = int(hex_values[4]) + ba.extend(duration.to_bytes(4, byteorder='little')) + return bytes(ba) + + @staticmethod + def frames_to_animation_bytes(frames: list) -> bytes: + """Aggregate multiple AppFrame instances into a bytes sequence ready for RPC. + + Each frame contributes 20 bytes (little-endian uint32 x5). + """ + ba = bytearray() + for f in frames: + ba.extend(f.to_animation_bytes()) + return bytes(ba) + + @staticmethod + def frames_to_c_animation_array(frames: list, name: str = 'Animation') -> str: + """Produce a C initializer for an animation sequence. + + Example output: + const uint32_t Animation[][5] = { + {0x..., 0x..., 0x..., 0x..., 1000}, + ... + }; + + This is suitable for inclusion in a .h and compatible with + `Arduino_LED_Matrix::loadWrapper(const uint32_t[][5], uint32_t)`. + """ + # sanitize name into a simple C identifier + snake = re.sub(r'[^a-zA-Z0-9]', '_', name) + parts = [f"const uint32_t {snake}[][5] = {{"] + for frame in frames: + hex_values = frame.to_animation_hex() + hex_str = ", ".join(hex_values) + parts.append(f" {{{hex_str}}}, // {getattr(frame, '_export_name', frame.name)}") + parts.append("};") + parts.append("") + return "\n".join(parts) + # -- Frame.from_rows override (for subclass construction only) --------------------------- @classmethod def from_rows( From 90799055fecebb4f85cee3bfcf553f732cd728e5 Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Sat, 20 Dec 2025 23:23:03 +0100 Subject: [PATCH 3/8] chore: use AppFrame exporters for animation export and playback --- examples/led-matrix-painter/python/main.py | 25 ++++------------------ 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/examples/led-matrix-painter/python/main.py b/examples/led-matrix-painter/python/main.py index 6f65344..d69826d 100644 --- a/examples/led-matrix-painter/python/main.py +++ b/examples/led-matrix-painter/python/main.py @@ -247,17 +247,9 @@ def export_frames(payload: dict = None): if not anim_frames: continue - # Build animation array + # Build animation array (delegated to AppFrame exporter) header_parts.append(f"// Animation: {anim_name}") - header_parts.append(f"const uint32_t {anim_name}[][5] = {{") - - for frame in anim_frames: - hex_values = frame.to_animation_hex() - hex_str = ", ".join(hex_values) - header_parts.append(f" {{{hex_str}}}, // {frame._export_name}") - - header_parts.append("};") - header_parts.append("") + header_parts.append(AppFrame.frames_to_c_animation_array(anim_frames, anim_name)) header = "\n".join(header_parts).strip() + "\n" return {'header': header} @@ -307,17 +299,8 @@ def play_animation(payload: dict): logger.debug(f"Loaded {len(frames)} frames for animation") # Build animation data as bytes (std::vector in sketch) - # Each uint32_t is sent as 4 bytes (little-endian) - animation_bytes = bytearray() - for frame in frames: - hex_values = frame.to_animation_hex() - # First 4 are hex pixel data - for i in range(4): - value = int(hex_values[i], 16) - animation_bytes.extend(value.to_bytes(4, byteorder='little')) - # 5th is duration in ms - duration = int(hex_values[4]) - animation_bytes.extend(duration.to_bytes(4, byteorder='little')) + # Each frame is 20 bytes (4 uint32_t pixels + 1 uint32_t duration), little-endian + animation_bytes = AppFrame.frames_to_animation_bytes(frames) logger.debug(f"Animation data prepared: {len(animation_bytes)} bytes ({len(animation_bytes)//20} frames)") From d79428d584bb387dd63e31bf1d7577d4acb57925 Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Sat, 20 Dec 2025 23:52:46 +0100 Subject: [PATCH 4/8] refactor: add _sanitize_c_ident and _export_name; use sanitized identifiers for exports --- .../led-matrix-painter/python/app_frame.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/examples/led-matrix-painter/python/app_frame.py b/examples/led-matrix-painter/python/app_frame.py index 47e470a..51299fb 100644 --- a/examples/led-matrix-painter/python/app_frame.py +++ b/examples/led-matrix-painter/python/app_frame.py @@ -6,6 +6,33 @@ import json from arduino.app_utils import Frame + +def _sanitize_c_ident(name: str, fallback: str = "frame") -> str: + """Sanitize an arbitrary string into a valid C identifier. + + Rules: + - allow lower-case letters, digits and underscore + - replace other chars with underscore + - if starts with a digit, prefix with 'f_' + - if result is empty, return fallback + """ + if name is None: + return fallback + s = str(name).strip().lower() + if not s: + return fallback + # keep letters, digits and underscore + s = re.sub(r'[^a-z0-9_]', '_', s) + # collapse multiple underscores + s = re.sub(r'_+', '_', s) + # remove leading/trailing underscore + s = s.strip('_') + if not s: + return fallback + if re.match(r'^[0-9]', s): + s = f"f_{s}" + return s + class AppFrame(Frame): """Extended Frame app_utils class with application-specific metadata. @@ -86,6 +113,8 @@ def __init__( self.name = name self.position = position self.duration_ms = duration_ms + # Export-friendly sanitized name used for C identifiers and exports + self._export_name = _sanitize_c_ident(self.name or f"frame_{self.id}") # -- JSON serialization/deserialization for frontend -------------------------------- @classmethod @@ -148,8 +177,8 @@ def to_c_string(self) -> str: str: C source fragment containing a const array initializer. """ c_type = "uint8_t" - # sanitize name into a safe C identifier - snake_name = re.sub(r'[^a-zA-Z0-9]', '_', self.name.lower()) + # use export-friendly sanitized name + snake_name = self._export_name scaled_arr = self.rescale_quantized_frame(scale_max=255) parts = [f"{c_type} {snake_name} [] = {{"] @@ -244,6 +273,9 @@ def to_animation_hex(self) -> list[str]: def to_animation_bytes(self) -> bytes: """Return this frame encoded as 20 bytes (4 x uint32_t pixels + 1 x uint32_t duration) in little-endian. + + Returns: + bytes: representation of the frame for RPC transmission. """ hex_values = self.to_animation_hex() ba = bytearray() @@ -278,8 +310,8 @@ def frames_to_c_animation_array(frames: list, name: str = 'Animation') -> str: This is suitable for inclusion in a .h and compatible with `Arduino_LED_Matrix::loadWrapper(const uint32_t[][5], uint32_t)`. """ - # sanitize name into a simple C identifier - snake = re.sub(r'[^a-zA-Z0-9]', '_', name) + # sanitize animation name into a simple C identifier + snake = _sanitize_c_ident(name or 'Animation') parts = [f"const uint32_t {snake}[][5] = {{"] for frame in frames: hex_values = frame.to_animation_hex() From ac742a0f0b262461fce5a1bd491f68b84c269bbc Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Mon, 22 Dec 2025 16:51:06 +0100 Subject: [PATCH 5/8] refactor: move sanitizer into AppFrame as private method and update docstring --- .../led-matrix-painter/python/app_frame.py | 104 +++++++++++------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/examples/led-matrix-painter/python/app_frame.py b/examples/led-matrix-painter/python/app_frame.py index 51299fb..6d9f024 100644 --- a/examples/led-matrix-painter/python/app_frame.py +++ b/examples/led-matrix-painter/python/app_frame.py @@ -6,33 +6,6 @@ import json from arduino.app_utils import Frame - -def _sanitize_c_ident(name: str, fallback: str = "frame") -> str: - """Sanitize an arbitrary string into a valid C identifier. - - Rules: - - allow lower-case letters, digits and underscore - - replace other chars with underscore - - if starts with a digit, prefix with 'f_' - - if result is empty, return fallback - """ - if name is None: - return fallback - s = str(name).strip().lower() - if not s: - return fallback - # keep letters, digits and underscore - s = re.sub(r'[^a-z0-9_]', '_', s) - # collapse multiple underscores - s = re.sub(r'_+', '_', s) - # remove leading/trailing underscore - s = s.strip('_') - if not s: - return fallback - if re.match(r'^[0-9]', s): - s = f"f_{s}" - return s - class AppFrame(Frame): """Extended Frame app_utils class with application-specific metadata. @@ -114,7 +87,7 @@ def __init__( self.position = position self.duration_ms = duration_ms # Export-friendly sanitized name used for C identifiers and exports - self._export_name = _sanitize_c_ident(self.name or f"frame_{self.id}") + self._export_name = self._sanitize_c_ident(self.name or f"frame_{self.id}") # -- JSON serialization/deserialization for frontend -------------------------------- @classmethod @@ -193,6 +166,43 @@ def to_c_string(self) -> str: parts.append("};") parts.append("") return "\n".join(parts) + + @staticmethod + def _sanitize_c_ident(name: str, fallback: str = "frame") -> str: + """Return a safe C identifier derived from ``name``. + + This produces a lower-case identifier containing only ASCII + letters, digits and underscores. Multiple non-allowed + characters collapse into a single underscore. Leading digits are + prefixed with ``f_`` to ensure the identifier is valid. If the + resulting name is empty, ``fallback`` is returned. + + Args: + name: The original name to sanitize. + fallback: The fallback identifier used when the sanitized + result would be empty. + + Returns: + A sanitized, C-safe identifier string. + """ + + if name is None: + return fallback + s = str(name).strip().lower() + if not s: + return fallback + + # keep letters, digits and underscore + s = re.sub(r'[^a-z0-9_]', '_', s) + # collapse multiple underscores + s = re.sub(r'_+', '_', s) + # remove leading/trailing underscore + s = s.strip('_') + if not s: + return fallback + if re.match(r'^[0-9]', s): + s = f"f_{s}" + return s # -- create empty AppFrame -------------------------------- @classmethod @@ -272,10 +282,16 @@ def to_animation_hex(self) -> list[str]: return hex_values def to_animation_bytes(self) -> bytes: - """Return this frame encoded as 20 bytes (4 x uint32_t pixels + 1 x uint32_t duration) in little-endian. + """Return this frame encoded as bytes for RPC transmission. + + The returned value is the wire format expected by the sketch's + `play_animation` provider: 5 uint32_t values per frame (4 words + containing packed pixel bits and 1 word for duration), each encoded + in little-endian order (20 bytes total). Returns: - bytes: representation of the frame for RPC transmission. + bytes: 20-byte little-endian representation of this frame + (4 x uint32_t pixels + 1 x uint32_t duration). """ hex_values = self.to_animation_hex() ba = bytearray() @@ -287,10 +303,16 @@ def to_animation_bytes(self) -> bytes: return bytes(ba) @staticmethod - def frames_to_animation_bytes(frames: list) -> bytes: - """Aggregate multiple AppFrame instances into a bytes sequence ready for RPC. + def frames_to_animation_bytes(frames: list["AppFrame"]) -> bytes: + """Aggregate multiple frames into a single bytes blob ready for RPC. - Each frame contributes 20 bytes (little-endian uint32 x5). + Args: + frames (list[AppFrame]): Sequence of AppFrame instances to include + in the resulting animation. + + Returns: + bytes: Concatenated little-endian byte sequence where each frame + contributes 20 bytes (4 uint32_t pixel words + 1 uint32_t duration). """ ba = bytearray() for f in frames: @@ -301,17 +323,23 @@ def frames_to_animation_bytes(frames: list) -> bytes: def frames_to_c_animation_array(frames: list, name: str = 'Animation') -> str: """Produce a C initializer for an animation sequence. - Example output: + Args: + frames (list[AppFrame]): Frames that make up the animation. + name (str): Desired C identifier for the animation array. Will be + sanitized into a valid C identifier. + + Returns: + str: C source fragment defining a `const uint32_t NAME[][5]` array + where each entry is `{word0, word1, word2, word3, duration}`. + + Example: const uint32_t Animation[][5] = { {0x..., 0x..., 0x..., 0x..., 1000}, ... }; - - This is suitable for inclusion in a .h and compatible with - `Arduino_LED_Matrix::loadWrapper(const uint32_t[][5], uint32_t)`. """ # sanitize animation name into a simple C identifier - snake = _sanitize_c_ident(name or 'Animation') + snake = AppFrame._sanitize_c_ident(name or 'Animation') parts = [f"const uint32_t {snake}[][5] = {{"] for frame in frames: hex_values = frame.to_animation_hex() From 75818879ca4b8c82c2071126bd166d7ba9c2a825 Mon Sep 17 00:00:00 2001 From: Dario Sammaruga Date: Mon, 22 Dec 2025 17:45:29 +0100 Subject: [PATCH 6/8] fix: missing attribute for rename frame at frontend --- examples/led-matrix-painter/assets/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/led-matrix-painter/assets/app.js b/examples/led-matrix-painter/assets/app.js index 7fcb71c..adee737 100644 --- a/examples/led-matrix-painter/assets/app.js +++ b/examples/led-matrix-painter/assets/app.js @@ -566,7 +566,7 @@ function renderFrames(){ fetchWithHandling('/persist_frame', { method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ id: f.id, name: newName, duration_ms: f.duration_ms, rows: rows }) + body: JSON.stringify({ id: f.id, name: newName, duration_ms: f.duration_ms, rows: rows, brightness_levels: BRIGHTNESS_LEVELS }) }).then(() => refreshFrames()); }); @@ -577,7 +577,7 @@ function renderFrames(){ fetchWithHandling('/persist_frame', { method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ id: f.id, name: f.name, duration_ms: durationMs, rows: rows }) + body: JSON.stringify({ id: f.id, name: f.name, duration_ms: durationMs, rows: rows, brightness_levels: BRIGHTNESS_LEVELS }) }).then(() => refreshFrames()); } }); From bff478dd6cc0c1539f6f7412f30ad030a0176e4f Mon Sep 17 00:00:00 2001 From: BeanRepo Date: Mon, 22 Dec 2025 23:54:17 +0100 Subject: [PATCH 7/8] feat(led-matrix-painter): 3-bit grayscale, AppFrame preview/board scaling and normalization --- examples/led-matrix-painter/README.md | 9 ++-- .../led-matrix-painter/python/app_frame.py | 54 +++++++++++++++---- examples/led-matrix-painter/sketch/sketch.ino | 6 +-- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/examples/led-matrix-painter/README.md b/examples/led-matrix-painter/README.md index 3ef18f7..5350aad 100644 --- a/examples/led-matrix-painter/README.md +++ b/examples/led-matrix-painter/README.md @@ -1,6 +1,6 @@ # LED Matrix Painter -The **LED Matrix Painter** example provides a web-based interface to draw, animate, and control the built-in LED Matrix of the Arduino UNO Q in real-time. It features a pixel editor with 8-bit brightness control, database storage for your designs, and a code generator to export your frames as ready-to-use C++ code. +The **LED Matrix Painter** example provides a web-based interface to draw, animate, and control the built-in LED Matrix of the Arduino UNO Q in real-time. It features a pixel editor with 3-bit (0-7) brightness control, database storage for your designs, and a code generator to export your frames as ready-to-use C++ code. ![LED Matrix Painter Example](assets/docs_assets/thumbnail.png) @@ -12,7 +12,7 @@ The application uses the `dbstorage_sqlstore` Brick to automatically save your w Key features include: - **Real-time Control:** Drawing on the web grid updates the UNO Q matrix instantly. -- **Grayscale Control:** 8 brightness presets (0-7) for intuitive pixel control, with full 8-bit precision (0-255) supported at the hardware level. +- **Grayscale Control:** 8 brightness presets (0-7) for intuitive pixel control; this example configures the board for 3-bit grayscale (0–7). - **Persistent Storage:** Frames are automatically saved to a database, allowing you to build complex animations over time. - **Transformation Tools:** Invert, rotate, or flip designs with a single click. - **Animation Mode:** Sequence frames to create animations and preview them on the board. @@ -156,8 +156,9 @@ The sketch is designed to be a passive renderer, accepting commands from the Pyt ```cpp void setup() { matrix.begin(); - // configure grayscale bits to 8 so the display can accept 0..255 brightness - matrix.setGrayscaleBits(8); + // configure grayscale bits to 3 so the display accepts 0..7 brightness + // The backend sends quantized values in 0..(2^3-1) == 0..7. + matrix.setGrayscaleBits(3); Bridge.begin(); // ... } diff --git a/examples/led-matrix-painter/python/app_frame.py b/examples/led-matrix-painter/python/app_frame.py index 6d9f024..ebabe7c 100644 --- a/examples/led-matrix-painter/python/app_frame.py +++ b/examples/led-matrix-painter/python/app_frame.py @@ -144,7 +144,9 @@ def to_record(self) -> dict: def to_c_string(self) -> str: """Export the frame as a C vector string. - The Frame is rescaled to the 0-255 range prior to exporting as board-compatible C source. + The frame is rescaled to the quantized range [0..brightness_levels-1] + for preview and code-panel output. This produces a `uint8_t` array + initializer suitable for display in the UI's code panel. Returns: str: C source fragment containing a const array initializer. @@ -152,7 +154,8 @@ def to_c_string(self) -> str: c_type = "uint8_t" # use export-friendly sanitized name snake_name = self._export_name - scaled_arr = self.rescale_quantized_frame(scale_max=255) + # represent preview values in the quantized range (0..brightness_levels-1) + scaled_arr = self.rescale_quantized_frame(scale_max=max(1, int(self.brightness_levels) - 1)) parts = [f"{c_type} {snake_name} [] = {{"] rows = scaled_arr.tolist() @@ -167,6 +170,22 @@ def to_c_string(self) -> str: parts.append("") return "\n".join(parts) + def to_board_bytes(self) -> bytes: + """Return the byte buffer (row-major) representing this frame for board consumption. + + This overrides ``Frame.to_board_bytes()`` to produce bytes scaled to + the AppFrame's configured ``brightness_levels - 1`` (for example + 0..7 when ``brightness_levels == 8``). The override keeps this + behaviour local to the application layer and avoids modifying the + upstream ``Frame`` implementation. + + Returns: + bytes: Flattened row-major byte sequence suitable for the firmware. + """ + scaled = self.rescale_quantized_frame(scale_max=max(1, int(self.brightness_levels) - 1)) + flat = [int(x) for x in scaled.flatten().tolist()] + return bytes(flat) + @staticmethod def _sanitize_c_ident(name: str, fallback: str = "frame") -> str: """Return a safe C identifier derived from ``name``. @@ -365,7 +384,12 @@ def from_rows( **Do NOT use it in the app directly, please use `AppFrame.from_json()` or `AppFrame.from_record()` instead.** This method overrides Frame.from_rows which constructs a Frame and it is intended - only for subclass construction and coherence with Frame API. + only for subclass construction and coherence with Frame API and accepts frontend rows either + already expressed in the target brightness range or in 8-bit + representation (0..255). If input values are out-of-range for the + requested ``brightness_levels``, the method will attempt to interpret + the input as 8-bit data and rescale it to the target range + automatically for retrocompatibility with previous versions. We delegate parsing/validation to Frame.from_rows and then construct an AppFrame instance with subclass-specific attributes. @@ -383,12 +407,20 @@ def from_rows( Returns: AppFrame: newly constructed AppFrame instance. """ - # Use Frame to parse rows into a numpy array/frame and to validate it - frame_instance = super().from_rows(rows, brightness_levels=brightness_levels) - - # Get the validated numpy array as a writable copy - arr = frame_instance.arr.copy() - - # Construct an AppFrame using the validated numpy array and brightness_levels - return cls(id, name, position, duration_ms, arr, brightness_levels=frame_instance.brightness_levels) + # Try to parse rows assuming they're already in the requested + # brightness range. If parsing fails because values are out-of-range + # (e.g. legacy rows in 0..255), attempt to parse them as 0..255 and + # rescale to the requested `brightness_levels - 1`. + try: + frame_instance = super().from_rows(rows, brightness_levels=brightness_levels) + arr = frame_instance.arr.copy() + return cls(id, name, position, duration_ms, arr, brightness_levels=frame_instance.brightness_levels) + except ValueError: + # Fallback: parse as 8-bit input and rescale down to target levels + raw = super().from_rows(rows, brightness_levels=256) + # rescale from 0..255 -> 0..(brightness_levels-1) + target_max = max(1, int(brightness_levels) - 1) + scaled = raw.rescale_quantized_frame(scale_max=target_max) + arr = scaled.copy() + return cls(id, name, position, duration_ms, arr, brightness_levels=brightness_levels) diff --git a/examples/led-matrix-painter/sketch/sketch.ino b/examples/led-matrix-painter/sketch/sketch.ino index cac69d6..978163a 100644 --- a/examples/led-matrix-painter/sketch/sketch.ino +++ b/examples/led-matrix-painter/sketch/sketch.ino @@ -78,9 +78,9 @@ void play_animation(std::vector animation_bytes) { void setup() { matrix.begin(); Serial.begin(115200); - // configure grayscale bits to 8 so the display can accept 0..255 brightness - // The MCU expects full-byte brightness values from the backend. - matrix.setGrayscaleBits(8); + // configure grayscale bits to 3 so the display accepts 0..7 brightness + // The backend will send quantized values in 0..(2^3-1) == 0..7. + matrix.setGrayscaleBits(3); matrix.clear(); Bridge.begin(); From 49cf16bae15352042093c2a738be8fb9622236f9 Mon Sep 17 00:00:00 2001 From: Dario Sammaruga Date: Tue, 23 Dec 2025 10:15:58 +0100 Subject: [PATCH 8/8] feat: add stop animation endpoint and sketch logic --- examples/led-matrix-painter/python/main.py | 22 +++++ examples/led-matrix-painter/sketch/sketch.ino | 88 +++++++++++++++---- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/examples/led-matrix-painter/python/main.py b/examples/led-matrix-painter/python/main.py index d69826d..020f347 100644 --- a/examples/led-matrix-painter/python/main.py +++ b/examples/led-matrix-painter/python/main.py @@ -311,6 +311,27 @@ def play_animation(payload: dict): return {'ok': True, 'frames_played': len(frames)} # Return immediately +def stop_animation(payload: dict = None): + """Stop any running animation on the board. + + This endpoint calls the sketch provider `stop_animation`. No payload + required. + + Args: + payload (dict, optional): Not used. Defaults to None. + + Returns: + dict: {'ok': True} on success, {'error': str} on failure. + """ + try: + Bridge.call("stop_animation") + logger.info("stop_animation called on board") + return {'ok': True} + except Exception as e: + logger.warning(f"Failed to request stop_animation: {e}") + return {'error': str(e)} + + ui.expose_api('POST', '/update_board', update_board) ui.expose_api('POST', '/persist_frame', persist_frame) ui.expose_api('POST', '/load_frame', load_frame) @@ -321,6 +342,7 @@ def play_animation(payload: dict): ui.expose_api('POST', '/export_frames', export_frames) ui.expose_api('POST', '/reorder_frames', reorder_frames) ui.expose_api('POST', '/play_animation', play_animation) +ui.expose_api('POST', '/stop_animation', stop_animation) ui.expose_api('GET', '/config', get_config) App.run() diff --git a/examples/led-matrix-painter/sketch/sketch.ino b/examples/led-matrix-painter/sketch/sketch.ino index 978163a..90ebed4 100644 --- a/examples/led-matrix-painter/sketch/sketch.ino +++ b/examples/led-matrix-painter/sketch/sketch.ino @@ -12,6 +12,15 @@ Arduino_LED_Matrix matrix; +// Animation playback state (cooperative, interruptible by `stop_animation`) +static const int MAX_FRAMES = 50; +static uint32_t animation_buf[MAX_FRAMES][5]; // 4 words + duration +static int animation_frame_count = 0; +static volatile bool animation_running = false; +static volatile bool animation_loop = false; +static volatile int animation_current_frame = 0; +static unsigned long animation_next_time = 0; + void draw(std::vector frame) { if (frame.empty()) { Serial.println("[sketch] draw called with empty frame"); @@ -52,27 +61,68 @@ void play_animation(std::vector animation_bytes) { frame_count = MAX_FRAMES; } - // Static buffer to avoid dynamic allocation - static uint32_t animation[MAX_FRAMES][5]; - - // Convert bytes to uint32_t array + // Parse bytes into the global animation buffer for cooperative playback const uint8_t* data = animation_bytes.data(); - for (int i = 0; i < frame_count; i++) { + int limit = min(frame_count, MAX_FRAMES); + for (int i = 0; i < limit; i++) { for (int j = 0; j < 5; j++) { int byte_offset = (i * 5 + j) * 4; - // Reconstruct uint32_t from 4 bytes (little-endian) - animation[i][j] = ((uint32_t)data[byte_offset]) | - ((uint32_t)data[byte_offset + 1] << 8) | - ((uint32_t)data[byte_offset + 2] << 16) | - ((uint32_t)data[byte_offset + 3] << 24); + animation_buf[i][j] = ((uint32_t)data[byte_offset]) | + ((uint32_t)data[byte_offset + 1] << 8) | + ((uint32_t)data[byte_offset + 2] << 16) | + ((uint32_t)data[byte_offset + 3] << 24); + } + } + animation_frame_count = limit; + animation_current_frame = 0; + animation_loop = false; // preserve existing behaviour (no loop) + animation_running = true; + animation_next_time = millis(); + Serial.print("[sketch] Animation queued, frames="); + Serial.println(animation_frame_count); +} + +// Provider to stop any running animation +void stop_animation() { + if (!animation_running) { + Serial.println("[sketch] stop_animation called but no animation running"); + return; + } + animation_running = false; + Serial.println("[sketch] stop_animation: animation halted"); +} + +// Cooperative animation tick executed from loop() +void animation_tick() { + if (!animation_running || animation_frame_count == 0) return; + + unsigned long now = millis(); + if (now < animation_next_time) return; + + // Prepare frame words (reverse bits as the library expects) + uint32_t frame[4]; + frame[0] = reverse(animation_buf[animation_current_frame][0]); + frame[1] = reverse(animation_buf[animation_current_frame][1]); + frame[2] = reverse(animation_buf[animation_current_frame][2]); + frame[3] = reverse(animation_buf[animation_current_frame][3]); + + // Display frame + matrixWrite(frame); + + // Schedule next frame + uint32_t interval = animation_buf[animation_current_frame][4]; + if (interval == 0) interval = 1; + animation_next_time = now + interval; + + animation_current_frame++; + if (animation_current_frame >= animation_frame_count) { + if (animation_loop) { + animation_current_frame = 0; + } else { + animation_running = false; + Serial.println("[sketch] Animation finished"); } } - - // Load and play the sequence using the Arduino_LED_Matrix library - matrix.loadWrapper(animation, frame_count * 5 * sizeof(uint32_t)); - matrix.playSequence(false); // Don't loop by default - - Serial.println("[sketch] Animation playback complete"); } void setup() { @@ -91,8 +141,12 @@ void setup() { // Register the animation player provider Bridge.provide("play_animation", play_animation); + // Provider to stop a running animation (invoked by backend) + Bridge.provide("stop_animation", stop_animation); } void loop() { - delay(200); + // Keep loop fast and let animation_tick handle playback timing + animation_tick(); + delay(10); }