Compare commits

...

1 Commits

Author SHA1 Message Date
ccc215acbd fix(cli): lazy settings fetch and full device erase
Download settings only for --show or when applying edits; skip upload
when unchanged; erase entire device root including settings.json.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:14:54 +12:00
3 changed files with 108 additions and 58 deletions

View File

@@ -27,11 +27,11 @@ Connection is always via **`-p` / `--port`** (default `/dev/ttyACM0`). There is
| `--pause` | Sleep N seconds (for chained actions) |
| `-u`, `--upload` | Recursive upload: `-u SRC [DEST]` |
| `--src`, `--lib` | Upload `src/` or a tree to `/lib` |
| `-e` | Erase device code (keeps `settings.json`) |
| `-e`, `--erase` | Erase everything at device root (including `settings.json` and `presets.json`) |
| `--rm` | Remove a path on the device |
| `--flash` | Flash a firmware binary (uses **esptool** on the host) |
**Default behaviour:** the tool always downloads `settings.json`, prints the merged view, and uploads again **only** when you pass setting edits (`--name`, `--leds`, …), **`--preset`**, or the relevant upload/flash/erase actions in order.
**Settings I/O:** With no arguments, **`led-cli`** prints help (no device access). **`--show`** downloads `settings.json` and prints it (read-only). Setting fields (`--name`, `-b`, …) download once, merge, print, and **upload only when a value actually changed**. **`--preset`** alone only touches **`presets.json`** (no `settings.json` download). Ordered actions (`--reset`, `--upload`, `-e`, …) run without touching settings unless you also pass **`--show`** or setting / preset edits as above.
Run **`python cli.py -h`** for the full epilog and argument list.

155
cli.py
View File

@@ -2,8 +2,10 @@
"""
LED Bar Configuration CLI Tool
Command-line interface for downloading, editing, and uploading settings.json
to/from MicroPython devices via mpremote.
Command-line interface for editing settings.json on MicroPython devices via mpremote.
Settings are downloaded from the device only when displaying (--show) or when applying
setting changes; uploads occur only when something changed. Use --erase to clear the
device filesystem (including settings).
"""
import json
@@ -279,10 +281,13 @@ def main() -> None:
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Download and print current settings
# Show help (no device I/O)
%(prog)s
# Download, edit device name and brightness, then upload
# Show current settings.json from device (read-only)
%(prog)s --show
# Edit device name and brightness, then upload only if values changed
%(prog)s -n "LED-Strip-1" -b 128
# Set multiple parameters
@@ -500,7 +505,7 @@ Examples:
"-e", "--erase",
dest="erase_all",
action="store_true",
help="Erase all code on the device (delete all files except settings.json)"
help="Erase the device filesystem: delete all files and directories at device root (including settings.json and presets.json)",
)
parser.add_argument(
@@ -516,6 +521,9 @@ Examples:
)
try:
if len(sys.argv) <= 1:
parser.print_help()
return
args = parser.parse_args()
ordered_actions = _get_ordered_actions(sys.argv)
except ValueError as e:
@@ -631,14 +639,12 @@ Examples:
elif action_name == 'erase_all':
try:
print(f"Erasing all code on device {port}...", file=sys.stderr)
print(f"Erasing device filesystem on {port}...", file=sys.stderr)
conn = DeviceConnection(port)
items = conn.list_files('')
for name, is_dir, size in items:
if name == "settings.json":
continue
conn.delete(name)
print("Erase complete.", file=sys.stderr)
print("Erase complete (device root is empty).", file=sys.stderr)
except Exception as e:
print(f"Error erasing device: {e}", file=sys.stderr)
sys.exit(1)
@@ -735,40 +741,86 @@ Examples:
# Clamp into single-byte range; store as int in settings.json
edits["id"] = max(0, min(255, args.device_id))
# 1. Download: get current settings from device
try:
print(f"Downloading settings from {args.port}...", file=sys.stderr)
settings = download_settings(args.port)
print("Settings downloaded successfully.", 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 downloading settings: {e}", file=sys.stderr)
sys.exit(1)
settings_work = args.show or bool(edits)
if settings_work:
try:
print(f"Downloading settings from {args.port}...", file=sys.stderr)
settings = download_settings(args.port)
print("Settings downloaded successfully.", 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 downloading settings: {e}", file=sys.stderr)
sys.exit(1)
if args.show:
print_settings(settings)
if not edits and args.preset is None:
return
changed_edits: Dict[str, Any] = {}
for key, value in edits.items():
if settings.get(key) != value:
changed_edits[key] = value
if edits and not changed_edits:
print("No settings changes detected; skipping settings upload.", file=sys.stderr)
if changed_edits:
print(f"Applying {len(changed_edits)} setting change(s)...", file=sys.stderr)
settings.update(changed_edits)
# If --show only, print and exit (unless presets or settings are being written)
if args.show:
print_settings(settings)
if not edits and args.preset is None:
return
# 2. Edit: only apply/upload settings when values actually change
changed_edits: Dict[str, Any] = {}
for key, value in edits.items():
if settings.get(key) != value:
changed_edits[key] = value
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)
if edits and not changed_edits:
print("No settings changes detected; skipping settings upload.", file=sys.stderr)
if changed_edits:
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 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 settings: {e}", file=sys.stderr)
sys.exit(1)
return
if changed_edits:
print(f"Applying {len(changed_edits)} setting change(s)...", file=sys.stderr)
settings.update(changed_edits)
print_settings(settings)
# 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:
@@ -783,29 +835,24 @@ Examples:
f"Writing preset {args.preset!r} (pattern={pattern}) to {PRESETS_REMOTE}...",
file=sys.stderr,
)
upload_presets(args.port, presets_data, reset=not bool(edits))
upload_presets(args.port, presets_data, reset=True)
print("Presets uploaded successfully.", file=sys.stderr)
if not edits:
print("Device will reset.", file=sys.stderr)
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)
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)
return
# 3b. Settings upload (resets device)
if changed_edits:
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 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 settings: {e}", file=sys.stderr)
sys.exit(1)
if ordered_actions:
return
parser.print_help()
if __name__ == "__main__":

View File

@@ -137,7 +137,8 @@ class DeviceConnection:
dirs_created = 0
for root, dirs, files in os.walk(local_dir):
# Calculate relative path from local_dir
# Never upload Python bytecode trees (MicroPython does not use them).
dirs[:] = [d for d in dirs if d != "__pycache__"]
rel_path = os.path.relpath(root, local_dir)
# Build remote path
@@ -156,8 +157,10 @@ class DeviceConnection:
self.transport.fs_mkdir(remote_base)
dirs_created += 1
# Copy files
# Copy files (skip bytecode; __pycache__ dirs are pruned above)
for file in files:
if file.endswith((".pyc", ".pyo")):
continue
local_file = os.path.join(root, file)
# Handle root directory case properly
if remote_base == '/':