feat(cli): create presets on device; flags for led-driver transport

Made-with: Cursor
This commit is contained in:
pi
2026-04-05 21:12:58 +12:00
parent 3844aa9d6a
commit e86312437c

153
cli.py
View File

@@ -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)