Compare commits
3 Commits
3844aa9d6a
...
eee9327e15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eee9327e15 | ||
|
|
5f7acf38f0 | ||
|
|
e86312437c |
41
README.md
41
README.md
@@ -1,8 +1,39 @@
|
|||||||
# led-tool
|
# led-tool
|
||||||
- `-s, --show`: Display current settings from device
|
|
||||||
- `--no-download`: Don't download settings first (use empty settings)
|
|
||||||
**Default behavior:** Downloads settings and prints them. If any edit flags are provided, settings are modified and uploaded automatically (unless `--no-upload` is specified).
|
|
||||||
|
|
||||||
## Device Connection
|
CLI helpers for MicroPython LED devices: **`settings.json`** download/upload via **mpremote**, resets, follow/tail, recursive uploads, optional firmware flash, and **led-driver**-oriented flags (presets, transport, Wi-Fi fields).
|
||||||
|
|
||||||
The tools use an integrated mpremote transport to communicate with MicroPython devices. Make sure your device is connected and accessible via the specified serial port.## LicenseSee LICENSE file for details.
|
Connection is always via **`-p` / `--port`** (default `/dev/ttyACM0`). There is **no** separate “server IP” flag; the Pi or PC address is configured on the device in **`settings.json`** when using Wi-Fi mode.
|
||||||
|
|
||||||
|
## Common flags
|
||||||
|
|
||||||
|
| Flag | Purpose |
|
||||||
|
|------|--------|
|
||||||
|
| `-p`, `--port` | Serial device (default `/dev/ttyACM0`) |
|
||||||
|
| `-s`, `--show` | Print current settings from the device |
|
||||||
|
| `-n`, `--name` | Device **name** |
|
||||||
|
| `--id` | Numeric device id (ESP-NOW, 0–255) |
|
||||||
|
| `--pin` | LED GPIO (`led_pin`) |
|
||||||
|
| `-b`, `--brightness` | Brightness 0–255 |
|
||||||
|
| `-l`, `--leds` | LED count (`num_leds`) |
|
||||||
|
| `-d`, `--debug` | Debug 0 or 1 |
|
||||||
|
| `-o`, `--order` | LED colour order (`rgb`, `grb`, …) |
|
||||||
|
| `--preset` / `--pattern` | Create or replace a named preset in **led-driver** `presets.json` |
|
||||||
|
| `--default` | Startup preset name |
|
||||||
|
| `--transport` | `espnow` or `wifi` (`transport_type` on device) |
|
||||||
|
| `--ssid`, `--wifi-password`, `--wifi-channel` | Wi-Fi / channel fields for the driver |
|
||||||
|
| `-r`, `--reset` | Reset the device |
|
||||||
|
| `-f`, `--follow` | Follow serial output (optional timeout seconds) |
|
||||||
|
| `--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`) |
|
||||||
|
| `--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.
|
||||||
|
|
||||||
|
Run **`python cli.py -h`** for the full epilog and argument list.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See **LICENSE** in this directory.
|
||||||
|
|||||||
144
cli.py
144
cli.py
@@ -84,6 +84,59 @@ def upload_settings(device: str, settings: dict) -> None:
|
|||||||
pass
|
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:
|
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))
|
||||||
@@ -93,7 +146,8 @@ def print_settings(settings: Dict[str, Any]) -> None:
|
|||||||
_FLAGS_WITH_VALUE = frozenset({
|
_FLAGS_WITH_VALUE = frozenset({
|
||||||
'-p', '--port', '-n', '--name', '--pin', '-b', '--brightness',
|
'-p', '--port', '-n', '--name', '--pin', '-b', '--brightness',
|
||||||
'-l', '--leds', '-d', '-debug', '--debug', '-o', '--order',
|
'-l', '--leds', '-d', '-debug', '--debug', '-o', '--order',
|
||||||
'--preset', '--default',
|
'--preset', '--pattern', '--default', '--transport', '--ssid',
|
||||||
|
'--wifi-password', '--wifi-channel',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -291,13 +345,48 @@ Examples:
|
|||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--preset",
|
"--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(
|
parser.add_argument(
|
||||||
"--default",
|
"--default",
|
||||||
metavar="PATTERN",
|
metavar="NAME",
|
||||||
help="Default/startup pattern (stored as 'default' in settings.json)"
|
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(
|
||||||
|
"--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(
|
parser.add_argument(
|
||||||
@@ -501,12 +590,21 @@ Examples:
|
|||||||
if args.color_order is not None:
|
if args.color_order is not None:
|
||||||
edits["color_order"] = args.color_order
|
edits["color_order"] = args.color_order
|
||||||
|
|
||||||
if args.preset is not None:
|
|
||||||
edits["pattern"] = args.preset
|
|
||||||
|
|
||||||
if args.default is not None:
|
if args.default is not None:
|
||||||
edits["default"] = args.default
|
edits["default"] = args.default
|
||||||
|
|
||||||
|
if args.transport is not None:
|
||||||
|
edits["transport_type"] = args.transport
|
||||||
|
|
||||||
|
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:
|
if args.device_id is not None:
|
||||||
# Clamp into single-byte range; store as int in settings.json
|
# Clamp into single-byte range; store as int in settings.json
|
||||||
edits["id"] = max(0, min(255, args.device_id))
|
edits["id"] = max(0, min(255, args.device_id))
|
||||||
@@ -523,10 +621,10 @@ 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 only, print and exit
|
# If --show only, print and exit (unless presets or settings are being written)
|
||||||
if args.show:
|
if args.show:
|
||||||
print_settings(settings)
|
print_settings(settings)
|
||||||
if not edits:
|
if not edits and args.preset is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Edit: apply edits to downloaded settings
|
# 2. Edit: apply edits to downloaded settings
|
||||||
@@ -536,7 +634,33 @@ Examples:
|
|||||||
|
|
||||||
print_settings(settings)
|
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:
|
if edits:
|
||||||
try:
|
try:
|
||||||
print(f"\nUploading settings to {args.port}...", file=sys.stderr)
|
print(f"\nUploading settings to {args.port}...", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user