Add cli tool

This commit is contained in:
2026-01-25 18:29:08 +13:00
parent 030029bf91
commit 4a3a384181

286
cli.py Executable file
View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""
LED Bar Configuration CLI Tool
Command-line interface for downloading, editing, and uploading settings.json
to/from MicroPython devices via mpremote.
"""
import json
import argparse
import sys
from typing import Dict, Any, List
# Import functions from tool.py
import tempfile
import subprocess
import os
def _run_mpremote_copy(from_device: bool, device: str, temp_path: str) -> None:
"""Run mpremote copy command."""
if from_device:
cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path]
else:
cmd = ["mpremote", "connect", device, "cp", temp_path, ":/settings.json"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
raise RuntimeError(f"mpremote error: {result.stderr.strip() or result.stdout.strip()}")
def download_settings(device: str) -> dict:
"""Download settings.json from the device using mpremote."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
temp_file.close()
try:
_run_mpremote_copy(from_device=True, device=device, temp_path=temp_path)
with open(temp_path, "r", encoding="utf-8") as f:
return json.load(f)
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def upload_settings(device: str, settings: dict) -> None:
"""Upload settings.json to the device using mpremote and reset device."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
try:
json.dump(settings, temp_file, indent=2)
temp_file.close()
_run_mpremote_copy(from_device=False, device=device, temp_path=temp_path)
# Reset device (best effort)
try:
import serial # type: ignore
with serial.Serial(device, baudrate=115200) as ser:
ser.write(b"\x03\x03\x04")
except Exception:
reset_cmd = [
"mpremote",
"connect",
device,
"exec",
"import machine; machine.reset()",
]
try:
subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5)
except subprocess.TimeoutExpired:
pass
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def parse_colors(color_string: str) -> List[tuple]:
"""
Parse color string into list of RGB tuples.
Format: "r,g,b" or "r,g,b;r2,g2,b2" for multiple colors
"""
colors = []
for color_part in color_string.split(";"):
parts = color_part.strip().split(",")
if len(parts) == 3:
try:
r = int(parts[0].strip())
g = int(parts[1].strip())
b = int(parts[2].strip())
colors.append((r, g, b))
except ValueError:
raise ValueError(f"Invalid color format: {color_part}. Expected 'r,g,b'")
else:
raise ValueError(f"Invalid color format: {color_part}. Expected 'r,g,b'")
return colors
def print_settings(settings: Dict[str, Any]) -> None:
"""Pretty print settings dictionary."""
print(json.dumps(settings, indent=2))
def main() -> None:
parser = argparse.ArgumentParser(
description="LED Bar Configuration CLI Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Download and print current settings
%(prog)s
# Download, edit device name and brightness, then upload
%(prog)s -n "LED-Strip-1" -b 128
# Set colors (format: r,g,b or r,g,b;r2,g2,b2 for multiple)
%(prog)s -c "255,0,0;0,255,0;0,0,255"
# Set multiple parameters
%(prog)s -n "MyLED" -b 200 --preset "rainbow" -n1 10 -n2 20
# Set number of LEDs, color order, and debug mode
%(prog)s -l 60 -o rgb -d 1
"""
)
parser.add_argument(
"-p", "--port",
default="/dev/ttyACM0",
help="Serial port/device path (default: /dev/ttyACM0)"
)
parser.add_argument(
"-n", "--name",
help="Device name"
)
parser.add_argument(
"-b", "--brightness",
type=int,
help="Brightness (0-255)"
)
parser.add_argument(
"-l", "--leds",
type=int,
help="Number of LEDs"
)
parser.add_argument(
"-d", "--debug",
type=int,
choices=[0, 1],
help="Debug mode (0 or 1)"
)
parser.add_argument(
"-o", "--color-order",
choices=["rgb", "rbg", "grb", "gbr", "brg", "bgr"],
help="Color order (rgb, rbg, grb, gbr, brg, bgr)"
)
parser.add_argument(
"--preset",
help="Pattern preset name"
)
parser.add_argument(
"-c", "--colors",
help="Colors in format 'r,g,b' or 'r,g,b;r2,g2,b2' for multiple colors"
)
# Add n1 through n8 parameters
for i in range(1, 9):
parser.add_argument(
f"-n{i}",
type=int,
help=f"N{i} parameter"
)
parser.add_argument(
"--no-upload",
action="store_true",
help="Don't upload settings after editing (only download and print)"
)
parser.add_argument(
"--no-download",
action="store_true",
help="Don't download settings first (use empty settings)"
)
args = parser.parse_args()
# Collect all edit parameters
edits: Dict[str, Any] = {}
if args.name is not None:
edits["name"] = args.name
if args.brightness is not None:
edits["brightness"] = args.brightness
if args.leds is not None:
edits["num_leds"] = args.leds
if args.debug is not None:
edits["debug"] = bool(args.debug)
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.colors is not None:
colors = parse_colors(args.colors)
edits["colors"] = colors
# Add n1-n8 parameters
for i in range(1, 9):
attr_name = f"n{i}"
value = getattr(args, attr_name, None)
if value is not None:
edits[attr_name] = value
# Download current settings (unless --no-download is specified)
if args.no_download:
settings = {}
else:
try:
print(f"Downloading settings from {args.port}...", file=sys.stderr)
settings = download_settings(args.port)
print("Settings downloaded successfully.", file=sys.stderr)
except FileNotFoundError:
print("Error: mpremote not found. Install with: pip install mpremote", file=sys.stderr)
sys.exit(1)
except subprocess.TimeoutExpired:
print(f"Error: Connection timeout. Check device connection on {args.port}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error downloading settings: {e}", file=sys.stderr)
sys.exit(1)
# Apply edits
if edits:
print(f"Applying {len(edits)} edit(s)...", file=sys.stderr)
settings.update(edits)
# Handle colors specially - convert list of tuples to format expected by device
if "colors" in edits:
# Store colors as list of lists for JSON serialization
settings["colors"] = [list(c) for c in edits["colors"]]
# Print settings
print_settings(settings)
# Upload if edits were made and --no-upload is not specified
if edits and not args.no_upload:
try:
print(f"\nUploading settings to {args.port}...", file=sys.stderr)
upload_settings(args.port, settings)
print("Settings uploaded successfully. Device will reset.", file=sys.stderr)
except FileNotFoundError:
print("Error: mpremote not found. Install with: pip install mpremote", file=sys.stderr)
sys.exit(1)
except subprocess.TimeoutExpired:
print(f"Error: Connection timeout. Check device connection on {args.port}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error uploading settings: {e}", file=sys.stderr)
sys.exit(1)
elif edits and args.no_upload:
print("\nSettings modified but not uploaded (--no-upload specified).", file=sys.stderr)
if __name__ == "__main__":
main()