From e86312437c833ac3767c3c30cd8b77ae49d61cd4 Mon Sep 17 00:00:00 2001 From: pi Date: Sun, 5 Apr 2026 21:12:58 +1200 Subject: [PATCH] feat(cli): create presets on device; flags for led-driver transport Made-with: Cursor --- cli.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 10 deletions(-) diff --git a/cli.py b/cli.py index 1a13d4a..f0ea01d 100755 --- a/cli.py +++ b/cli.py @@ -84,6 +84,59 @@ def upload_settings(device: str, settings: dict) -> None: pass +PRESETS_REMOTE = "presets.json" + + +def download_presets(device: str) -> dict: + """Download presets.json from the device; missing file -> {}.""" + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) + temp_path = temp_file.name + temp_file.close() + + try: + copy_file(from_device=True, device=device, remote_path=PRESETS_REMOTE, local_path=temp_path) + with open(temp_path, "r", encoding="utf-8") as f: + return json.load(f) + except (OSError, RuntimeError) as e: + msg = str(e).lower() + if "errno 2" in msg or "enoent" in msg or "not found" in msg: + return {} + raise + finally: + if os.path.exists(temp_path): + try: + os.unlink(temp_path) + except OSError: + pass + + +def upload_presets(device: str, presets: dict, *, reset: bool = True) -> None: + """Upload presets.json; optional reset (skip when caller will reset via settings upload).""" + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) + temp_path = temp_file.name + + try: + json.dump(presets, temp_file, indent=2) + temp_file.close() + + copy_file(from_device=False, device=device, remote_path=PRESETS_REMOTE, local_path=temp_path) + + if reset: + try: + conn = DeviceConnection(device) + conn.connect() + conn.reset() + conn.disconnect() + except Exception: + pass + finally: + if os.path.exists(temp_path): + try: + os.unlink(temp_path) + except OSError: + pass + + def print_settings(settings: Dict[str, Any]) -> None: """Pretty print settings dictionary.""" print(json.dumps(settings, indent=2)) @@ -93,7 +146,8 @@ def print_settings(settings: Dict[str, Any]) -> None: _FLAGS_WITH_VALUE = frozenset({ '-p', '--port', '-n', '--name', '--pin', '-b', '--brightness', '-l', '--leds', '-d', '-debug', '--debug', '-o', '--order', - '--preset', '--default', + '--preset', '--pattern', '--default', '--transport', '--server-ip', '--ssid', + '--wifi-password', '--wifi-channel', }) @@ -291,13 +345,54 @@ Examples: parser.add_argument( "--preset", - help="Pattern preset name" + metavar="NAME", + help="Create or replace preset NAME in presets.json (led-driver; use --pattern)", + ) + + parser.add_argument( + "--pattern", + choices=[ + "off", "on", "blink", "rainbow", "pulse", "transition", "chase", "circle", + ], + help="Pattern type for --preset (default: on)", ) parser.add_argument( "--default", - metavar="PATTERN", - help="Default/startup pattern (stored as 'default' in settings.json)" + metavar="NAME", + help="Default/startup preset name in settings.json (which preset runs at boot)", + ) + + parser.add_argument( + "--transport", + choices=["espnow", "wifi"], + help="led-driver transport_type", + ) + + parser.add_argument( + "--server-ip", + metavar="IP", + help="led-driver server_ip (Pi TCP host in wifi mode)", + ) + + parser.add_argument( + "--ssid", + help="led-driver ssid (Wi-Fi network in wifi mode)", + ) + + parser.add_argument( + "--wifi-password", + dest="wifi_password", + metavar="PASS", + help="led-driver password (Wi-Fi PSK in wifi mode)", + ) + + parser.add_argument( + "--wifi-channel", + dest="wifi_channel", + type=int, + metavar="1-11", + help="led-driver wifi_channel (ESP-NOW mode)", ) parser.add_argument( @@ -501,12 +596,24 @@ Examples: if args.color_order is not None: edits["color_order"] = args.color_order - if args.preset is not None: - edits["pattern"] = args.preset - if args.default is not None: edits["default"] = args.default + if args.transport is not None: + edits["transport_type"] = args.transport + + if args.server_ip is not None: + edits["server_ip"] = args.server_ip + + if args.ssid is not None: + edits["ssid"] = args.ssid + + if args.wifi_password is not None: + edits["password"] = args.wifi_password + + if args.wifi_channel is not None: + edits["wifi_channel"] = max(1, min(11, args.wifi_channel)) + if args.device_id is not None: # Clamp into single-byte range; store as int in settings.json edits["id"] = max(0, min(255, args.device_id)) @@ -523,10 +630,10 @@ Examples: print(f"Error downloading settings: {e}", file=sys.stderr) sys.exit(1) - # If --show only, print and exit + # If --show only, print and exit (unless presets or settings are being written) if args.show: print_settings(settings) - if not edits: + if not edits and args.preset is None: return # 2. Edit: apply edits to downloaded settings @@ -536,7 +643,33 @@ Examples: print_settings(settings) - # 3. Upload: write settings back to device when edits were made + # 3a. Presets file (led-driver presets.json) + if args.preset is not None: + pattern = args.pattern if args.pattern is not None else "on" + try: + print(f"Downloading presets from {args.port}...", file=sys.stderr) + presets_data = download_presets(args.port) + entry = presets_data.get(args.preset) + if not isinstance(entry, dict): + entry = {} + entry["p"] = pattern + presets_data[args.preset] = entry + print( + f"Writing preset {args.preset!r} (pattern={pattern}) to {PRESETS_REMOTE}...", + file=sys.stderr, + ) + upload_presets(args.port, presets_data, reset=not bool(edits)) + print("Presets uploaded successfully.", file=sys.stderr) + if not edits: + print("Device will reset.", file=sys.stderr) + except Exception as e: + if "timeout" in str(e).lower() or "connection" in str(e).lower(): + print(f"Error: Connection timeout. Check device connection on {args.port}", file=sys.stderr) + else: + print(f"Error uploading presets: {e}", file=sys.stderr) + sys.exit(1) + + # 3b. Settings upload (resets device) if edits: try: print(f"\nUploading settings to {args.port}...", file=sys.stderr)