diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index a21d0ce..27c5551 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -47,7 +47,8 @@ /* Liveness safety bound for the fixed two-cycle wealth spin intro (not the * animation length). On timeout the worker proceeds to reveal + rotate anyway. */ #define CONTROL_PLANE_SPIN_INTRO_TIMEOUT_MS 6000 -#define CONTROL_PLANE_COMMAND_ROTATION_DEG 45.0f +#define CONTROL_PLANE_COMMAND_ROTATION_DEG 100.0f +#define CONTROL_PLANE_COMMAND_RETURN_DELAY_MS 5000 #if (configSUPPORT_STATIC_ALLOCATION == 1) && \ ((defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \ @@ -716,8 +717,14 @@ static esp_err_t control_plane_apply_wealth_direction(size_t direction_index, (void)direction_index; - float current_angle = domain_get_current_angle(); - return control_plane_rotate_to_angle(current_angle + CONTROL_PLANE_COMMAND_ROTATION_DEG); + float start_angle = domain_get_current_angle(); + esp_err_t rotate_err = control_plane_rotate_to_angle(start_angle + CONTROL_PLANE_COMMAND_ROTATION_DEG); + if (rotate_err != ESP_OK) { + return rotate_err; + } + + vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_COMMAND_RETURN_DELAY_MS)); + return control_plane_rotate_to_angle(start_angle); } static esp_err_t control_plane_run_model_calibration_motion(void) diff --git a/components/platform/src/platform_stepper_driver.c b/components/platform/src/platform_stepper_driver.c index 4006966..21535f1 100644 --- a/components/platform/src/platform_stepper_driver.c +++ b/components/platform/src/platform_stepper_driver.c @@ -40,7 +40,7 @@ #define PLATFORM_STEPPER_DECEL_STEPS 48 #define PLATFORM_STEPPER_STEPS_PER_REV 4128.0f #define PLATFORM_STEPPER_POWER_OFF_WAIT_MS 1000 -#define PLATFORM_STEPPER_MAX_ROTATION_DEG 330.0f +#define PLATFORM_STEPPER_MAX_ROTATION_DEG 100.0f #define PLATFORM_GPIO_LOW_BANK_MAX_PIN 31 #define PLATFORM_GPIO_HIGH_BANK_OFFSET 32 diff --git a/tools/png_to_rle.py b/tools/png_to_rle.py new file mode 100644 index 0000000..cc0a7e1 --- /dev/null +++ b/tools/png_to_rle.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Convert PNG images to the binary RGB565 RLE format used by lcd_direct.""" + +import argparse +import struct +from pathlib import Path + +try: + from PIL import Image +except ImportError as exc: + raise SystemExit("Pillow is required. Install it with: python -m pip install Pillow") from exc + + +MAX_RUN_COUNT = 0xFFFF + + +def parse_rgb(value): + text = value.strip() + if text.startswith("#"): + text = text[1:] + if len(text) != 6: + raise argparse.ArgumentTypeError("expected RRGGBB or #RRGGBB") + try: + return tuple(int(text[index:index + 2], 16) for index in (0, 2, 4)) + except ValueError as exc: + raise argparse.ArgumentTypeError("background must be hexadecimal RGB") from exc + + +def rgb888_to_rgb565(red, green, blue): + return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3) + + +def image_to_runs(path, background): + with Image.open(path) as image: + rgba = image.convert("RGBA") + width, height = rgba.size + bg_red, bg_green, bg_blue = background + + runs = [] + current_color = None + current_count = 0 + + for red, green, blue, alpha in rgba.getdata(): + if alpha < 255: + red = ((red * alpha) + (bg_red * (255 - alpha))) // 255 + green = ((green * alpha) + (bg_green * (255 - alpha))) // 255 + blue = ((blue * alpha) + (bg_blue * (255 - alpha))) // 255 + + color = rgb888_to_rgb565(red, green, blue) + if color == current_color and current_count < MAX_RUN_COUNT: + current_count += 1 + continue + + if current_color is not None: + runs.append((current_color, current_count)) + current_color = color + current_count = 1 + + if current_color is not None: + runs.append((current_color, current_count)) + + return width, height, runs + + +def write_rle(path, runs): + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("wb") as output: + for color, count in runs: + output.write(struct.pack(" {out_path} ({width}x{height}, {len(runs)} runs)") + + +def main(): + parser = argparse.ArgumentParser( + description="Convert PNG files to lcd_direct RGB565 RLE binary assets.", + ) + parser.add_argument("input", type=Path, help="PNG file or directory containing PNG files.") + parser.add_argument( + "-o", + "--output", + type=Path, + help="Output .rle file for one input file, or output directory for an input directory.", + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + help="Recursively convert PNG files when input is a directory.", + ) + parser.add_argument( + "--background", + type=parse_rgb, + default=(0, 0, 0), + help="RGB background for transparent pixels, as RRGGBB or #RRGGBB. Default: 000000.", + ) + args = parser.parse_args() + + input_path = args.input.resolve() + if not input_path.exists(): + raise SystemExit(f"input path does not exist: {input_path}") + + output = args.output.resolve() if args.output is not None else None + inputs = list(iter_inputs(input_path, args.recursive)) + if not inputs: + raise SystemExit(f"no PNG files found in: {input_path}") + + if input_path.is_file() and output is not None and output.suffix.lower() != ".rle": + raise SystemExit("single-file output must use a .rle suffix") + + for input_file in inputs: + convert_file(input_file, input_path, output, args.background) + + +if __name__ == "__main__": + main()