lr-shuttle 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lr-shuttle might be problematic. Click here for more details.
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/PKG-INFO +1 -1
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/pyproject.toml +1 -1
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/PKG-INFO +1 -1
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/cli.py +189 -10
- lr_shuttle-0.2.3/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/serial_client.py +9 -1
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_cli.py +2 -0
- lr_shuttle-0.2.2/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/README.md +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/setup.cfg +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/SOURCES.txt +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/dependency_links.txt +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/entry_points.txt +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/requires.txt +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/lr_shuttle.egg-info/top_level.txt +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/constants.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/__init__.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/__init__.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/manifest.json +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/flash.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/prodtest.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/timo.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_cli_client.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_cli_edge.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_cli_seq.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_cli_utils.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_flash.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_prodtest_edge.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_prodtest_helpers.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_serial_client.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_timo.py +0 -0
- {lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/tests/test_timo_write.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lr-shuttle"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "CLI and Python client for host-side of json based serial communication with embedded device bridge."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import ipaddress
|
|
6
|
+
import re
|
|
5
7
|
import string
|
|
6
8
|
import sys
|
|
7
9
|
import time
|
|
@@ -107,6 +109,9 @@ for entry in PRODTEST_TX_POWER_LEVELS:
|
|
|
107
109
|
PRODTEST_TX_POWER_ALIASES[str(entry["value"])] = entry["value"]
|
|
108
110
|
PRODTEST_TX_POWER_CANONICAL = [entry["aliases"][0] for entry in PRODTEST_TX_POWER_LEVELS]
|
|
109
111
|
|
|
112
|
+
_HOST_PORT_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+:\d+$")
|
|
113
|
+
_IPV6_HOST_PORT_PATTERN = re.compile(r"^\[[0-9A-Fa-f:]+\]:\d+$")
|
|
114
|
+
|
|
110
115
|
# Backwards-compatible aliases for tests and external callers
|
|
111
116
|
_SerialLogger = SerialLogger
|
|
112
117
|
_SequenceTracker = SequenceTracker
|
|
@@ -251,9 +256,24 @@ def _resolve_uart_payload(
|
|
|
251
256
|
return payload_bytes.hex(), len(payload_bytes)
|
|
252
257
|
|
|
253
258
|
|
|
259
|
+
def _normalize_port(port: str) -> str:
|
|
260
|
+
trimmed = port.strip()
|
|
261
|
+
if not trimmed:
|
|
262
|
+
raise typer.BadParameter(
|
|
263
|
+
"Serial port is required (use --port or SHUTTLE_PORT)"
|
|
264
|
+
)
|
|
265
|
+
if "://" in trimmed:
|
|
266
|
+
return trimmed
|
|
267
|
+
if trimmed.startswith("/") or trimmed.startswith("\\"):
|
|
268
|
+
return trimmed
|
|
269
|
+
if _HOST_PORT_PATTERN.match(trimmed) or _IPV6_HOST_PORT_PATTERN.match(trimmed):
|
|
270
|
+
return f"socket://{trimmed}"
|
|
271
|
+
return trimmed
|
|
272
|
+
|
|
273
|
+
|
|
254
274
|
def _require_port(port: Optional[str]) -> str:
|
|
255
275
|
if port:
|
|
256
|
-
return port
|
|
276
|
+
return _normalize_port(port)
|
|
257
277
|
raise typer.BadParameter("Serial port is required (use --port or SHUTTLE_PORT)")
|
|
258
278
|
|
|
259
279
|
|
|
@@ -269,6 +289,16 @@ def _parse_int_option(value: str, *, name: str) -> int:
|
|
|
269
289
|
return parsed
|
|
270
290
|
|
|
271
291
|
|
|
292
|
+
def _parse_ipv4(value: Optional[str], *, name: str) -> Optional[str]:
|
|
293
|
+
if value is None:
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
ipaddress.IPv4Address(value)
|
|
297
|
+
except ipaddress.AddressValueError as exc:
|
|
298
|
+
raise typer.BadParameter(f"{name} must be a valid IPv4 address") from exc
|
|
299
|
+
return value
|
|
300
|
+
|
|
301
|
+
|
|
272
302
|
def _parse_prodtest_mask(value: str) -> bytes:
|
|
273
303
|
try:
|
|
274
304
|
return prodtest.mask_from_hex(value)
|
|
@@ -1521,17 +1551,49 @@ def prodtest_ping(
|
|
|
1521
1551
|
logger=resources.get("logger"),
|
|
1522
1552
|
seq_tracker=resources.get("seq_tracker"),
|
|
1523
1553
|
)
|
|
1524
|
-
if not responses
|
|
1554
|
+
if not responses:
|
|
1525
1555
|
console.print("[red]Device returned no response[/]")
|
|
1526
1556
|
raise typer.Exit(1)
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1557
|
+
|
|
1558
|
+
failed_idx = next(
|
|
1559
|
+
(idx for idx, resp in enumerate(responses) if not resp.get("ok")), None
|
|
1560
|
+
)
|
|
1561
|
+
if failed_idx is not None:
|
|
1562
|
+
phase = "command" if failed_idx == 0 else "payload"
|
|
1563
|
+
_render_spi_response(
|
|
1564
|
+
f"prodtest ping ({phase})",
|
|
1565
|
+
responses[failed_idx],
|
|
1566
|
+
command_label=f"spi.xfer (prodtest {phase})",
|
|
1567
|
+
)
|
|
1568
|
+
raise typer.Exit(1)
|
|
1569
|
+
|
|
1570
|
+
if len(responses) != len(sequence):
|
|
1571
|
+
console.print("[red]Prodtest command halted before completing all SPI phases[/]")
|
|
1572
|
+
raise typer.Exit(1)
|
|
1573
|
+
|
|
1574
|
+
command_response, payload_response = responses
|
|
1575
|
+
_render_spi_response(
|
|
1576
|
+
"prodtest ping (command)",
|
|
1577
|
+
command_response,
|
|
1578
|
+
command_label="spi.xfer (prodtest command)",
|
|
1579
|
+
)
|
|
1580
|
+
_render_spi_response(
|
|
1581
|
+
"prodtest ping (payload)",
|
|
1582
|
+
payload_response,
|
|
1583
|
+
command_label="spi.xfer (prodtest payload)",
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
rx_bytes = _decode_hex_response(
|
|
1587
|
+
payload_response, label="prodtest ping (payload)"
|
|
1588
|
+
)
|
|
1589
|
+
if not rx_bytes or rx_bytes[0] != 0x2D: # ord('-')
|
|
1590
|
+
console.print(
|
|
1591
|
+
"[red]Ping failed: expected '-' (0x2D), got: "
|
|
1592
|
+
f"{_format_hex(payload_response.get('rx', ''))}[/]"
|
|
1593
|
+
)
|
|
1594
|
+
raise typer.Exit(1)
|
|
1595
|
+
|
|
1596
|
+
console.print("[green]Ping successful: got '-' response[/]")
|
|
1535
1597
|
|
|
1536
1598
|
|
|
1537
1599
|
@prodtest_app.command("antenna")
|
|
@@ -2328,6 +2390,123 @@ def uart_sub_command(
|
|
|
2328
2390
|
_render_payload_response("uart.sub", response)
|
|
2329
2391
|
|
|
2330
2392
|
|
|
2393
|
+
@app.command("wifi-cfg")
|
|
2394
|
+
def wifi_cfg_command(
|
|
2395
|
+
ctx: typer.Context,
|
|
2396
|
+
port: Optional[str] = typer.Option(
|
|
2397
|
+
None,
|
|
2398
|
+
"--port",
|
|
2399
|
+
envvar="SHUTTLE_PORT",
|
|
2400
|
+
help="Serial port or host:port (e.g., /dev/ttyUSB0 or 192.168.1.10:5000)",
|
|
2401
|
+
),
|
|
2402
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
2403
|
+
timeout: float = typer.Option(
|
|
2404
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
2405
|
+
),
|
|
2406
|
+
ssid: Optional[str] = typer.Option(
|
|
2407
|
+
None,
|
|
2408
|
+
"--ssid",
|
|
2409
|
+
help="Set the station SSID",
|
|
2410
|
+
show_default=False,
|
|
2411
|
+
),
|
|
2412
|
+
psk: Optional[str] = typer.Option(
|
|
2413
|
+
None,
|
|
2414
|
+
"--psk",
|
|
2415
|
+
help="Set the WPA/WPA2/WPA3 passphrase",
|
|
2416
|
+
show_default=False,
|
|
2417
|
+
),
|
|
2418
|
+
dhcp: Optional[bool] = typer.Option(
|
|
2419
|
+
None,
|
|
2420
|
+
"--dhcp/--static",
|
|
2421
|
+
help="Enable DHCP or force static IPv4 addressing",
|
|
2422
|
+
),
|
|
2423
|
+
ip_addr: Optional[str] = typer.Option(
|
|
2424
|
+
None,
|
|
2425
|
+
"--ip",
|
|
2426
|
+
help="Static IPv4 address (requires --static or other static fields)",
|
|
2427
|
+
show_default=False,
|
|
2428
|
+
),
|
|
2429
|
+
netmask: Optional[str] = typer.Option(
|
|
2430
|
+
None,
|
|
2431
|
+
"--netmask",
|
|
2432
|
+
help="Static subnet mask (e.g., 255.255.255.0)",
|
|
2433
|
+
show_default=False,
|
|
2434
|
+
),
|
|
2435
|
+
gateway: Optional[str] = typer.Option(
|
|
2436
|
+
None,
|
|
2437
|
+
"--gateway",
|
|
2438
|
+
help="Static default gateway IPv4 address",
|
|
2439
|
+
show_default=False,
|
|
2440
|
+
),
|
|
2441
|
+
dns: Optional[str] = typer.Option(
|
|
2442
|
+
None,
|
|
2443
|
+
"--dns",
|
|
2444
|
+
help="Primary DNS server IPv4 address",
|
|
2445
|
+
show_default=False,
|
|
2446
|
+
),
|
|
2447
|
+
dns_alt: Optional[str] = typer.Option(
|
|
2448
|
+
None,
|
|
2449
|
+
"--dns-alt",
|
|
2450
|
+
help="Secondary DNS server IPv4 address",
|
|
2451
|
+
show_default=False,
|
|
2452
|
+
),
|
|
2453
|
+
):
|
|
2454
|
+
"""Query or update Wi-Fi credentials and network settings."""
|
|
2455
|
+
|
|
2456
|
+
resources = _ctx_resources(ctx)
|
|
2457
|
+
wifi_payload: Dict[str, Any] = {}
|
|
2458
|
+
if ssid is not None:
|
|
2459
|
+
wifi_payload["ssid"] = ssid
|
|
2460
|
+
if psk is not None:
|
|
2461
|
+
wifi_payload["psk"] = psk
|
|
2462
|
+
if dhcp is not None:
|
|
2463
|
+
wifi_payload["dhcp"] = dhcp
|
|
2464
|
+
|
|
2465
|
+
network_payload: Dict[str, Any] = {}
|
|
2466
|
+
parsed_ip = _parse_ipv4(ip_addr, name="--ip")
|
|
2467
|
+
parsed_mask = _parse_ipv4(netmask, name="--netmask")
|
|
2468
|
+
parsed_gateway = _parse_ipv4(gateway, name="--gateway")
|
|
2469
|
+
parsed_dns_primary = _parse_ipv4(dns, name="--dns")
|
|
2470
|
+
parsed_dns_secondary = _parse_ipv4(dns_alt, name="--dns-alt")
|
|
2471
|
+
|
|
2472
|
+
if parsed_ip is not None:
|
|
2473
|
+
network_payload["ip"] = parsed_ip
|
|
2474
|
+
if parsed_mask is not None:
|
|
2475
|
+
network_payload["netmask"] = parsed_mask
|
|
2476
|
+
if parsed_gateway is not None:
|
|
2477
|
+
network_payload["gateway"] = parsed_gateway
|
|
2478
|
+
|
|
2479
|
+
dns_entries = [entry for entry in (parsed_dns_primary, parsed_dns_secondary) if entry]
|
|
2480
|
+
if dns_entries:
|
|
2481
|
+
network_payload["dns"] = dns_entries
|
|
2482
|
+
|
|
2483
|
+
if network_payload:
|
|
2484
|
+
if wifi_payload.get("dhcp") is True:
|
|
2485
|
+
raise typer.BadParameter(
|
|
2486
|
+
"Static network options cannot be combined with --dhcp"
|
|
2487
|
+
)
|
|
2488
|
+
wifi_payload.setdefault("dhcp", False)
|
|
2489
|
+
wifi_payload["network"] = network_payload
|
|
2490
|
+
|
|
2491
|
+
resolved_port = _require_port(port)
|
|
2492
|
+
action = "Updating" if wifi_payload else "Querying"
|
|
2493
|
+
with spinner(f"{action} wifi.cfg over {resolved_port}"):
|
|
2494
|
+
try:
|
|
2495
|
+
with NDJSONSerialClient(
|
|
2496
|
+
resolved_port,
|
|
2497
|
+
baudrate=baudrate,
|
|
2498
|
+
timeout=timeout,
|
|
2499
|
+
logger=resources.get("logger"),
|
|
2500
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
2501
|
+
) as client:
|
|
2502
|
+
response = client.wifi_cfg(wifi_payload if wifi_payload else None)
|
|
2503
|
+
except ShuttleSerialError as exc:
|
|
2504
|
+
console.print(f"[red]{exc}[/]")
|
|
2505
|
+
raise typer.Exit(1) from exc
|
|
2506
|
+
|
|
2507
|
+
_render_payload_response("wifi.cfg", response)
|
|
2508
|
+
|
|
2509
|
+
|
|
2331
2510
|
@app.command("uart-tx")
|
|
2332
2511
|
def uart_tx_command(
|
|
2333
2512
|
ctx: typer.Context,
|
|
Binary file
|
|
@@ -228,7 +228,9 @@ class NDJSONSerialClient:
|
|
|
228
228
|
seq_tracker: Optional[SequenceTracker] = None,
|
|
229
229
|
):
|
|
230
230
|
try:
|
|
231
|
-
self._serial = serial.
|
|
231
|
+
self._serial = serial.serial_for_url(
|
|
232
|
+
url=port, baudrate=baudrate, timeout=timeout
|
|
233
|
+
)
|
|
232
234
|
except SerialException as exc: # pragma: no cover - hardware specific
|
|
233
235
|
raise ShuttleSerialError(f"Unable to open {port}: {exc}") from exc
|
|
234
236
|
self._serial.reset_input_buffer()
|
|
@@ -345,6 +347,12 @@ class NDJSONSerialClient:
|
|
|
345
347
|
payload["uart"] = uart
|
|
346
348
|
return self._command("uart.cfg", payload)
|
|
347
349
|
|
|
350
|
+
def wifi_cfg(self, wifi: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
351
|
+
payload: Dict[str, Any] = {}
|
|
352
|
+
if wifi:
|
|
353
|
+
payload["wifi"] = wifi
|
|
354
|
+
return self._command("wifi.cfg", payload)
|
|
355
|
+
|
|
348
356
|
def uart_tx(self, data: str, port: Optional[int] = None) -> Dict[str, Any]:
|
|
349
357
|
payload: Dict[str, Any] = {"data": data}
|
|
350
358
|
if port is not None:
|
|
@@ -859,6 +859,8 @@ def test_prodtest_ping_success(dummy_serial, recorded_console):
|
|
|
859
859
|
|
|
860
860
|
assert result.exit_code == 0
|
|
861
861
|
output = recorded_console.getvalue()
|
|
862
|
+
assert "spi.xfer (prodtest command)" in output
|
|
863
|
+
assert "spi.xfer (prodtest payload)" in output
|
|
862
864
|
assert "Ping successful" in output
|
|
863
865
|
|
|
864
866
|
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin
RENAMED
|
File without changes
|
{lr_shuttle-0.2.2 → lr_shuttle-0.2.3}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|