diff --git a/README.md b/README.md index 5d3324b..413ae43 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cli.py b/cli.py index 03785a2..484e16f 100755 --- a/cli.py +++ b/cli.py @@ -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__": diff --git a/device.py b/device.py index 80c8991..56fe6cf 100644 --- a/device.py +++ b/device.py @@ -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 == '/':