Improve led-tool flashing and settings flow
Made-with: Cursor
This commit is contained in:
76
build.py
76
build.py
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Build script for creating binary executables from the CLI and web tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
def build_binary(script_name, output_name=None):
|
|
||||||
"""Build a binary from a Python script using PyInstaller."""
|
|
||||||
if output_name is None:
|
|
||||||
output_name = script_name.replace('.py', '')
|
|
||||||
|
|
||||||
print(f"Building {script_name} -> {output_name}...")
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
'pyinstaller',
|
|
||||||
'--onefile', # Create a single executable file
|
|
||||||
'--name', output_name,
|
|
||||||
'--clean', # Clean PyInstaller cache
|
|
||||||
script_name
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add hidden imports that might be needed
|
|
||||||
hidden_imports = [
|
|
||||||
'mpremote.transport_serial',
|
|
||||||
'mpremote.transport',
|
|
||||||
'mpremote.console',
|
|
||||||
'mpremote.mp_errno',
|
|
||||||
'serial',
|
|
||||||
'serial.tools.list_ports',
|
|
||||||
]
|
|
||||||
|
|
||||||
for imp in hidden_imports:
|
|
||||||
cmd.extend(['--hidden-import', imp])
|
|
||||||
|
|
||||||
# Include the lib directory
|
|
||||||
cmd.extend(['--add-data', 'lib:lib'])
|
|
||||||
|
|
||||||
result = subprocess.run(cmd, cwd=os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"✓ Successfully built {output_name}")
|
|
||||||
print(f" Binary location: dist/{output_name}")
|
|
||||||
else:
|
|
||||||
print(f"✗ Failed to build {output_name}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("Building binaries for led-tool...")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
success = True
|
|
||||||
|
|
||||||
# Build CLI binary
|
|
||||||
if build_binary('cli.py', 'led-cli'):
|
|
||||||
print()
|
|
||||||
else:
|
|
||||||
success = False
|
|
||||||
|
|
||||||
# Optionally build web binary (commented out as it's less common)
|
|
||||||
# if build_binary('web.py', 'led-tool-web'):
|
|
||||||
# print()
|
|
||||||
# else:
|
|
||||||
# success = False
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print("=" * 60)
|
|
||||||
print("Build complete! Binaries are in the 'dist' directory.")
|
|
||||||
else:
|
|
||||||
print("=" * 60)
|
|
||||||
print("Build failed. Check errors above.")
|
|
||||||
sys.exit(1)
|
|
||||||
37
build.sh
37
build.sh
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build script for creating binaries
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Building led-tool binaries..."
|
|
||||||
echo "================================"
|
|
||||||
|
|
||||||
# Check if we're in a pipenv environment or use pipenv run
|
|
||||||
if command -v pipenv &> /dev/null; then
|
|
||||||
echo "Using pipenv..."
|
|
||||||
pipenv install --dev
|
|
||||||
PYINSTALLER_CMD="pipenv run pyinstaller"
|
|
||||||
else
|
|
||||||
# Check if pyinstaller is available directly
|
|
||||||
if ! command -v pyinstaller &> /dev/null; then
|
|
||||||
echo "Installing PyInstaller..."
|
|
||||||
pip install pyinstaller
|
|
||||||
fi
|
|
||||||
PYINSTALLER_CMD="pyinstaller"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build CLI binary using the spec file
|
|
||||||
echo ""
|
|
||||||
echo "Building CLI binary..."
|
|
||||||
$PYINSTALLER_CMD --clean led-cli.spec
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "================================"
|
|
||||||
echo "Build complete!"
|
|
||||||
echo "Binary location: dist/led-cli"
|
|
||||||
echo ""
|
|
||||||
echo "To test: ./dist/led-cli -h"
|
|
||||||
echo ""
|
|
||||||
echo "You can distribute the binary from the dist/ directory"
|
|
||||||
echo "without requiring Python to be installed on the target system."
|
|
||||||
|
|
||||||
307
cli.py
307
cli.py
@@ -8,14 +8,26 @@ to/from MicroPython devices via mpremote.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, Any, List
|
import time
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
from device import copy_file, DeviceConnection
|
from device import copy_file, DeviceConnection
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_flash_binary(path: str) -> Optional[str]:
|
||||||
|
"""Resolve binary path: full path or in bin/ directory."""
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return path if os.path.exists(path) else None
|
||||||
|
if os.path.exists(path):
|
||||||
|
return os.path.abspath(path)
|
||||||
|
bin_path = os.path.join("bin", path)
|
||||||
|
return os.path.abspath(bin_path) if os.path.exists(bin_path) else None
|
||||||
|
|
||||||
|
|
||||||
def download_settings(device: str) -> dict:
|
def download_settings(device: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Download settings.json from the device.
|
Download settings.json from the device.
|
||||||
@@ -72,32 +84,84 @@ def upload_settings(device: str, settings: dict) -> None:
|
|||||||
pass
|
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:
|
def print_settings(settings: Dict[str, Any]) -> None:
|
||||||
"""Pretty print settings dictionary."""
|
"""Pretty print settings dictionary."""
|
||||||
print(json.dumps(settings, indent=2))
|
print(json.dumps(settings, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
# Flags that consume the next argv as their value (for skipping during order scan)
|
||||||
|
_FLAGS_WITH_VALUE = frozenset({
|
||||||
|
'-p', '--port', '-n', '--name', '--pin', '-b', '--brightness',
|
||||||
|
'-l', '--leds', '-d', '-debug', '--debug', '-o', '--order',
|
||||||
|
'--preset', '--default',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ordered_actions(argv: List[str]) -> List[tuple]:
|
||||||
|
"""
|
||||||
|
Scan argv and return list of (action_name, value) in order of appearance.
|
||||||
|
Actions: flash, pause, reset, upload, erase_all, rm, follow.
|
||||||
|
"""
|
||||||
|
actions = []
|
||||||
|
i = 1
|
||||||
|
while i < len(argv):
|
||||||
|
arg = argv[i]
|
||||||
|
if arg == '--pause':
|
||||||
|
if i + 1 < len(argv):
|
||||||
|
try:
|
||||||
|
secs = float(argv[i + 1])
|
||||||
|
actions.append(('pause', secs))
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("--pause requires a number (seconds)")
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
raise ValueError("--pause requires an argument")
|
||||||
|
continue
|
||||||
|
if arg == '--flash':
|
||||||
|
if i + 1 < len(argv):
|
||||||
|
actions.append(('flash', argv[i + 1]))
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
raise ValueError("--flash requires an argument")
|
||||||
|
continue
|
||||||
|
if arg in ('-r', '--reset'):
|
||||||
|
actions.append(('reset', None))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if arg in ('-u', '--upload'):
|
||||||
|
vals = []
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and not argv[i].startswith('-'):
|
||||||
|
vals.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
if len(vals) >= 2:
|
||||||
|
break
|
||||||
|
if vals:
|
||||||
|
actions.append(('upload', vals))
|
||||||
|
continue
|
||||||
|
if arg == '-e':
|
||||||
|
actions.append(('erase_all', None))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if arg == '--rm':
|
||||||
|
if i + 1 < len(argv):
|
||||||
|
actions.append(('rm', argv[i + 1]))
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
raise ValueError("--rm requires an argument")
|
||||||
|
continue
|
||||||
|
if arg in ('-f', '--follow'):
|
||||||
|
actions.append(('follow', None))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
# Skip non-action flags and their values
|
||||||
|
if arg in _FLAGS_WITH_VALUE and i + 1 < len(argv):
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="LED Bar Configuration CLI Tool",
|
description="LED Bar Configuration CLI Tool",
|
||||||
@@ -110,11 +174,8 @@ Examples:
|
|||||||
# Download, edit device name and brightness, then upload
|
# Download, edit device name and brightness, then upload
|
||||||
%(prog)s -n "LED-Strip-1" -b 128
|
%(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
|
# Set multiple parameters
|
||||||
%(prog)s -n "MyLED" -b 200 --preset "rainbow" -n1 10 -n2 20
|
%(prog)s -n "MyLED" -b 200 --preset "rainbow"
|
||||||
|
|
||||||
# Set number of LEDs, color order, and debug mode
|
# Set number of LEDs, color order, and debug mode
|
||||||
%(prog)s -l 60 -o rgb -d 1
|
%(prog)s -l 60 -o rgb -d 1
|
||||||
@@ -127,6 +188,16 @@ Examples:
|
|||||||
|
|
||||||
# Reset and then follow output
|
# Reset and then follow output
|
||||||
%(prog)s -r -f
|
%(prog)s -r -f
|
||||||
|
|
||||||
|
# Flash firmware binary (full path or filename in bin/)
|
||||||
|
%(prog)s --flash firmware.bin
|
||||||
|
%(prog)s -p /dev/ttyUSB0 --flash /path/to/firmware.bin
|
||||||
|
|
||||||
|
# Reset, pause 2 seconds, then follow output
|
||||||
|
%(prog)s --reset --pause 2 --follow
|
||||||
|
|
||||||
|
# Set name, num_leds, default pattern, and upload
|
||||||
|
%(prog)s --name "MyStrip" -l 60 --default rainbow
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,30 +215,37 @@ Examples:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--pin",
|
"--pin",
|
||||||
type=int,
|
type=int,
|
||||||
help="LED GPIO pin number (updates 'led_pin' in settings.json)"
|
metavar="N",
|
||||||
|
help="LED GPIO pin number (led_pin in settings.json)"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-b", "--brightness",
|
"-b", "--brightness",
|
||||||
type=int,
|
type=int,
|
||||||
|
metavar="0-255",
|
||||||
help="Brightness (0-255)"
|
help="Brightness (0-255)"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l", "--leds",
|
"-l", "--leds",
|
||||||
|
dest="leds",
|
||||||
type=int,
|
type=int,
|
||||||
help="Number of LEDs"
|
metavar="N",
|
||||||
|
help="Number of LEDs (num_leds in settings.json)"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d", "--debug",
|
"-d", "-debug", "--debug",
|
||||||
|
dest="debug",
|
||||||
type=int,
|
type=int,
|
||||||
choices=[0, 1],
|
choices=[0, 1],
|
||||||
|
metavar="0|1",
|
||||||
help="Debug mode (0 or 1)"
|
help="Debug mode (0 or 1)"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--color-order",
|
"-o", "--order",
|
||||||
|
dest="color_order",
|
||||||
choices=["rgb", "rbg", "grb", "gbr", "brg", "bgr"],
|
choices=["rgb", "rbg", "grb", "gbr", "brg", "bgr"],
|
||||||
help="Color order (rgb, rbg, grb, gbr, brg, bgr)"
|
help="Color order (rgb, rbg, grb, gbr, brg, bgr)"
|
||||||
)
|
)
|
||||||
@@ -178,28 +256,16 @@ Examples:
|
|||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--colors",
|
"--default",
|
||||||
help="Colors in format 'r,g,b' or 'r,g,b;r2,g2,b2' for multiple colors"
|
metavar="PATTERN",
|
||||||
)
|
help="Default/startup pattern (startup_preset in settings.json)"
|
||||||
|
|
||||||
# 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(
|
parser.add_argument(
|
||||||
"--no-upload",
|
"--pause",
|
||||||
action="store_true",
|
type=float,
|
||||||
help="Don't upload settings after editing (only download and print)"
|
metavar="SECONDS",
|
||||||
)
|
help="Pause for N seconds (use in action sequence, e.g. --reset --pause 2 --follow)"
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-download",
|
|
||||||
action="store_true",
|
|
||||||
help="Don't download settings first (use empty settings)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -240,85 +306,112 @@ Examples:
|
|||||||
help="Delete a file or directory on the device (recursive for directories)"
|
help="Delete a file or directory on the device (recursive for directories)"
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
parser.add_argument(
|
||||||
|
"--flash",
|
||||||
|
metavar="BINARY",
|
||||||
|
help="Flash firmware binary to device. Path can be full path or filename in bin/"
|
||||||
|
)
|
||||||
|
|
||||||
# Handle reset option (can be combined with follow/erase/upload)
|
|
||||||
if args.reset:
|
|
||||||
try:
|
try:
|
||||||
print(f"Resetting device on {args.port}...", file=sys.stderr)
|
args = parser.parse_args()
|
||||||
conn = DeviceConnection(args.port)
|
ordered_actions = _get_ordered_actions(sys.argv)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
port = args.port
|
||||||
|
ran_reset = False
|
||||||
|
|
||||||
|
# Execute actions in command-line order
|
||||||
|
for action_name, value in ordered_actions:
|
||||||
|
if action_name == 'pause':
|
||||||
|
print(f"Pausing for {value} seconds...", file=sys.stderr)
|
||||||
|
time.sleep(value)
|
||||||
|
elif action_name == 'flash':
|
||||||
|
resolved = resolve_flash_binary(value)
|
||||||
|
if not resolved:
|
||||||
|
print(f"Error: binary not found: {value} (checked bin/)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
print(f"Erasing flash on {port}...", file=sys.stderr)
|
||||||
|
subprocess.run(["esptool", "-p", port, "erase_flash"], check=True)
|
||||||
|
print(f"Writing {resolved} to flash at 0...", file=sys.stderr)
|
||||||
|
subprocess.run(["esptool", "-p", port, "write_flash", "0", resolved], check=True)
|
||||||
|
print("Flash complete.", file=sys.stderr)
|
||||||
|
print("Waiting 2s for device to boot...", file=sys.stderr)
|
||||||
|
time.sleep(2)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error flashing: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Error: esptool not found. Install it with: pip install esptool", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif action_name == 'reset':
|
||||||
|
ran_reset = True
|
||||||
|
try:
|
||||||
|
print(f"Resetting device on {port}...", file=sys.stderr)
|
||||||
|
conn = DeviceConnection(port)
|
||||||
conn.reset()
|
conn.reset()
|
||||||
print("Device reset.", file=sys.stderr)
|
print("Device reset.", file=sys.stderr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error resetting device: {e}", file=sys.stderr)
|
print(f"Error resetting device: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Handle upload option (can be combined with other operations)
|
elif action_name == 'upload':
|
||||||
if args.upload:
|
if len(value) > 2:
|
||||||
import os
|
|
||||||
if len(args.upload) > 2:
|
|
||||||
print("Error: -u accepts at most 2 arguments (source and optional destination)", file=sys.stderr)
|
print("Error: -u accepts at most 2 arguments (source and optional destination)", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
upload_dir = value[0]
|
||||||
upload_dir = args.upload[0]
|
remote_dir = value[1] if len(value) > 1 else None
|
||||||
remote_dir = args.upload[1] if len(args.upload) > 1 else None
|
|
||||||
|
|
||||||
if not os.path.exists(upload_dir):
|
if not os.path.exists(upload_dir):
|
||||||
print(f"Error: Directory does not exist: {upload_dir}", file=sys.stderr)
|
print(f"Error: Directory does not exist: {upload_dir}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.isdir(upload_dir):
|
if not os.path.isdir(upload_dir):
|
||||||
print(f"Error: Not a directory: {upload_dir}", file=sys.stderr)
|
print(f"Error: Not a directory: {upload_dir}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if remote_dir:
|
if remote_dir:
|
||||||
print(f"Uploading {upload_dir} to {remote_dir} on device {args.port}...", file=sys.stderr)
|
print(f"Uploading {upload_dir} to {remote_dir} on device {port}...", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Uploading {upload_dir} to device on {args.port}...", file=sys.stderr)
|
print(f"Uploading {upload_dir} to device on {port}...", file=sys.stderr)
|
||||||
conn = DeviceConnection(args.port)
|
conn = DeviceConnection(port)
|
||||||
files_copied, dirs_created = conn.upload_directory(upload_dir, remote_dir)
|
files_copied, dirs_created = conn.upload_directory(upload_dir, remote_dir)
|
||||||
print(f"Upload complete: {files_copied} files, {dirs_created} directories created.", file=sys.stderr)
|
print(f"Upload complete: {files_copied} files, {dirs_created} directories created.", file=sys.stderr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error uploading directory: {e}", file=sys.stderr)
|
print(f"Error uploading directory: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Handle erase-all option (can be combined with other operations)
|
elif action_name == 'erase_all':
|
||||||
if args.erase_all:
|
|
||||||
try:
|
try:
|
||||||
print(f"Erasing all code on device {args.port}...", file=sys.stderr)
|
print(f"Erasing all code on device {port}...", file=sys.stderr)
|
||||||
conn = DeviceConnection(args.port)
|
conn = DeviceConnection(port)
|
||||||
# List top-level items and delete everything except settings.json
|
|
||||||
items = conn.list_files('')
|
items = conn.list_files('')
|
||||||
for name, is_dir, size in items:
|
for name, is_dir, size in items:
|
||||||
if name == "settings.json":
|
if name == "settings.json":
|
||||||
continue
|
continue
|
||||||
path = name
|
conn.delete(name)
|
||||||
conn.delete(path)
|
|
||||||
print("Erase complete.", file=sys.stderr)
|
print("Erase complete.", file=sys.stderr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error erasing device: {e}", file=sys.stderr)
|
print(f"Error erasing device: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Handle targeted remove option (can be combined with other operations)
|
elif action_name == 'rm':
|
||||||
if args.rm:
|
|
||||||
try:
|
try:
|
||||||
print(f"Deleting {args.rm} on device {args.port}...", file=sys.stderr)
|
print(f"Deleting {value} on device {port}...", file=sys.stderr)
|
||||||
conn = DeviceConnection(args.port)
|
conn = DeviceConnection(port)
|
||||||
conn.delete(args.rm)
|
conn.delete(value)
|
||||||
print(f"Successfully deleted: {args.rm}", file=sys.stderr)
|
print(f"Successfully deleted: {value}", file=sys.stderr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error deleting {args.rm}: {e}", file=sys.stderr)
|
print(f"Error deleting {value}: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Handle follow option (can be combined with reset)
|
elif action_name == 'follow':
|
||||||
if args.follow:
|
if ran_reset:
|
||||||
try:
|
|
||||||
if args.reset:
|
|
||||||
# Small delay after reset to let device boot
|
|
||||||
import time
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
print(f"Following output from {args.port}... (Press Ctrl+C to stop)", file=sys.stderr)
|
try:
|
||||||
conn = DeviceConnection(args.port)
|
print(f"Following output from {port}... (Press Ctrl+C to stop)", file=sys.stderr)
|
||||||
|
conn = DeviceConnection(port)
|
||||||
conn.follow_output()
|
conn.follow_output()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nStopped following.", file=sys.stderr)
|
print("\nStopped following.", file=sys.stderr)
|
||||||
@@ -326,10 +419,10 @@ Examples:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error following output: {e}", file=sys.stderr)
|
print(f"Error following output: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return # Don't continue with settings operations if following
|
return # follow blocks; when interrupted we're done
|
||||||
|
|
||||||
# If only reset was specified (without follow/other actions), we're done
|
# If we ran any actions and follow wasn't last, we're done
|
||||||
if args.reset and not (args.upload or args.erase_all or args.rm or args.show):
|
if ordered_actions:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Collect all edit parameters
|
# Collect all edit parameters
|
||||||
@@ -356,21 +449,10 @@ Examples:
|
|||||||
if args.preset is not None:
|
if args.preset is not None:
|
||||||
edits["pattern"] = args.preset
|
edits["pattern"] = args.preset
|
||||||
|
|
||||||
if args.colors is not None:
|
if args.default is not None:
|
||||||
colors = parse_colors(args.colors)
|
edits["startup_preset"] = args.default
|
||||||
edits["colors"] = colors
|
|
||||||
|
|
||||||
# Add n1-n8 parameters
|
# 1. Download: get current settings from device
|
||||||
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 and not showing)
|
|
||||||
if args.no_download and not args.show:
|
|
||||||
settings = {}
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
print(f"Downloading settings from {args.port}...", file=sys.stderr)
|
print(f"Downloading settings from {args.port}...", file=sys.stderr)
|
||||||
settings = download_settings(args.port)
|
settings = download_settings(args.port)
|
||||||
@@ -382,26 +464,21 @@ Examples:
|
|||||||
print(f"Error downloading settings: {e}", file=sys.stderr)
|
print(f"Error downloading settings: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# If --show is set, print current settings. If there are no edits, exit afterwards.
|
# If --show only, print and exit
|
||||||
if args.show:
|
if args.show:
|
||||||
print_settings(settings)
|
print_settings(settings)
|
||||||
if not edits:
|
if not edits:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Apply edits
|
# 2. Edit: apply edits to downloaded settings
|
||||||
if edits:
|
if edits:
|
||||||
print(f"Applying {len(edits)} edit(s)...", file=sys.stderr)
|
print(f"Applying {len(edits)} edit(s)...", file=sys.stderr)
|
||||||
settings.update(edits)
|
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)
|
print_settings(settings)
|
||||||
|
|
||||||
# Upload if edits were made and --no-upload is not specified
|
# 3. Upload: write settings back to device when edits were made
|
||||||
if edits and not args.no_upload:
|
if edits:
|
||||||
try:
|
try:
|
||||||
print(f"\nUploading settings to {args.port}...", file=sys.stderr)
|
print(f"\nUploading settings to {args.port}...", file=sys.stderr)
|
||||||
upload_settings(args.port, settings)
|
upload_settings(args.port, settings)
|
||||||
@@ -412,8 +489,6 @@ Examples:
|
|||||||
else:
|
else:
|
||||||
print(f"Error uploading settings: {e}", file=sys.stderr)
|
print(f"Error uploading settings: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif edits and args.no_upload:
|
|
||||||
print("\nSettings modified but not uploaded (--no-upload specified).", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import errno
|
import errno
|
||||||
import platform
|
|
||||||
|
try:
|
||||||
|
import platform
|
||||||
|
except Exception: # MicroPython or restricted envs
|
||||||
|
platform = None
|
||||||
|
|
||||||
# This table maps numeric values defined by `py/mperrno.h` to host errno code.
|
# This table maps numeric values defined by `py/mperrno.h` to host errno code.
|
||||||
MP_ERRNO_TABLE = {
|
MP_ERRNO_TABLE = {
|
||||||
@@ -51,5 +55,5 @@ MP_ERRNO_TABLE = {
|
|||||||
115: errno.EINPROGRESS,
|
115: errno.EINPROGRESS,
|
||||||
125: errno.ECANCELED,
|
125: errno.ECANCELED,
|
||||||
}
|
}
|
||||||
if platform.system() != "Windows":
|
if (platform.system() if platform else None) != "Windows":
|
||||||
MP_ERRNO_TABLE[15] = errno.ENOTBLK
|
MP_ERRNO_TABLE[15] = errno.ENOTBLK
|
||||||
|
|||||||
Reference in New Issue
Block a user