lr-shuttle 0.2.0__tar.gz → 0.2.1__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.
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/PKG-INFO +1 -1
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/pyproject.toml +1 -1
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/PKG-INFO +1 -1
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/cli.py +54 -0
- lr_shuttle-0.2.1/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/flash.py +6 -2
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/serial_client.py +9 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli.py +80 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_serial_client.py +15 -11
- lr_shuttle-0.2.0/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/README.md +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/setup.cfg +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/SOURCES.txt +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/dependency_links.txt +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/entry_points.txt +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/requires.txt +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/top_level.txt +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/constants.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/__init__.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/__init__.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/manifest.json +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/prodtest.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/timo.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_client.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_edge.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_seq.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_utils.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_flash.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_prodtest_edge.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_prodtest_helpers.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_timo.py +0 -0
- {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/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.1"
|
|
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"
|
|
@@ -1977,6 +1977,60 @@ def uart_rx_command(
|
|
|
1977
1977
|
console.print("[yellow]No uart.rx events observed[/]")
|
|
1978
1978
|
|
|
1979
1979
|
|
|
1980
|
+
@app.command("power")
|
|
1981
|
+
def power_command(
|
|
1982
|
+
ctx: typer.Context,
|
|
1983
|
+
port: Optional[str] = typer.Option(
|
|
1984
|
+
None,
|
|
1985
|
+
"--port",
|
|
1986
|
+
envvar="SHUTTLE_PORT",
|
|
1987
|
+
help="Serial port (e.g., /dev/ttyUSB0)",
|
|
1988
|
+
),
|
|
1989
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1990
|
+
timeout: float = typer.Option(
|
|
1991
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1992
|
+
),
|
|
1993
|
+
enable: Optional[bool] = typer.Option(
|
|
1994
|
+
None,
|
|
1995
|
+
"--enable/--disable",
|
|
1996
|
+
help="Enable or disable the downstream power rail",
|
|
1997
|
+
),
|
|
1998
|
+
):
|
|
1999
|
+
"""Query or toggle the downstream power rail."""
|
|
2000
|
+
|
|
2001
|
+
resources = _ctx_resources(ctx)
|
|
2002
|
+
resolved_port = _require_port(port)
|
|
2003
|
+
if enable is None:
|
|
2004
|
+
action = "Querying"
|
|
2005
|
+
label = "power.state"
|
|
2006
|
+
method_name = "power_state"
|
|
2007
|
+
elif enable:
|
|
2008
|
+
action = "Enabling"
|
|
2009
|
+
label = "power.enable"
|
|
2010
|
+
method_name = "power_enable"
|
|
2011
|
+
else:
|
|
2012
|
+
action = "Disabling"
|
|
2013
|
+
label = "power.disable"
|
|
2014
|
+
method_name = "power_disable"
|
|
2015
|
+
|
|
2016
|
+
with spinner(f"{action} power over {resolved_port}"):
|
|
2017
|
+
try:
|
|
2018
|
+
with NDJSONSerialClient(
|
|
2019
|
+
resolved_port,
|
|
2020
|
+
baudrate=baudrate,
|
|
2021
|
+
timeout=timeout,
|
|
2022
|
+
logger=resources.get("logger"),
|
|
2023
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
2024
|
+
) as client:
|
|
2025
|
+
method = getattr(client, method_name)
|
|
2026
|
+
response = method()
|
|
2027
|
+
except ShuttleSerialError as exc:
|
|
2028
|
+
console.print(f"[red]{exc}[/]")
|
|
2029
|
+
raise typer.Exit(1) from exc
|
|
2030
|
+
|
|
2031
|
+
_render_payload_response(label, response)
|
|
2032
|
+
|
|
2033
|
+
|
|
1980
2034
|
@app.command("flash")
|
|
1981
2035
|
def flash_command(
|
|
1982
2036
|
ctx: typer.Context,
|
|
Binary file
|
|
@@ -90,12 +90,16 @@ def flash_firmware(
|
|
|
90
90
|
offset = segment.get("offset")
|
|
91
91
|
file_name = segment.get("file")
|
|
92
92
|
if not offset or not file_name:
|
|
93
|
-
raise FirmwareFlashError(
|
|
93
|
+
raise FirmwareFlashError(
|
|
94
|
+
"Manifest segment entries require 'offset' and 'file'"
|
|
95
|
+
)
|
|
94
96
|
traversable = resources.files(package) / file_name
|
|
95
97
|
try:
|
|
96
98
|
file_path = stack.enter_context(resources.as_file(traversable))
|
|
97
99
|
except FileNotFoundError as exc:
|
|
98
|
-
raise FirmwareFlashError(
|
|
100
|
+
raise FirmwareFlashError(
|
|
101
|
+
f"Missing firmware artifact: {file_name}"
|
|
102
|
+
) from exc
|
|
99
103
|
resolved_segments.append((str(offset), Path(file_path)))
|
|
100
104
|
|
|
101
105
|
if erase_first:
|
|
@@ -324,6 +324,15 @@ class NDJSONSerialClient:
|
|
|
324
324
|
def ping(self) -> Dict[str, Any]:
|
|
325
325
|
return self._command("ping", {})
|
|
326
326
|
|
|
327
|
+
def power_state(self) -> Dict[str, Any]:
|
|
328
|
+
return self._command("power.state", {})
|
|
329
|
+
|
|
330
|
+
def power_enable(self) -> Dict[str, Any]:
|
|
331
|
+
return self._command("power.enable", {})
|
|
332
|
+
|
|
333
|
+
def power_disable(self) -> Dict[str, Any]:
|
|
334
|
+
return self._command("power.disable", {})
|
|
335
|
+
|
|
327
336
|
def spi_cfg(self, spi: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
328
337
|
payload: Dict[str, Any] = {}
|
|
329
338
|
if spi:
|
|
@@ -2018,6 +2018,86 @@ def test_uart_rx_no_events_message(monkeypatch, recorded_console):
|
|
|
2018
2018
|
assert "No uart.rx events observed" in recorded_console.getvalue()
|
|
2019
2019
|
|
|
2020
2020
|
|
|
2021
|
+
def test_power_command_queries_state(monkeypatch, recorded_console):
|
|
2022
|
+
monkeypatch.setattr(cli_module, "spinner", _noop_spinner)
|
|
2023
|
+
record = SimpleNamespace(calls=[])
|
|
2024
|
+
|
|
2025
|
+
class PowerClient:
|
|
2026
|
+
def __init__(self, *args, **kwargs):
|
|
2027
|
+
pass
|
|
2028
|
+
|
|
2029
|
+
def __enter__(self):
|
|
2030
|
+
return self
|
|
2031
|
+
|
|
2032
|
+
def __exit__(self, exc_type, exc, exc_tb):
|
|
2033
|
+
return False
|
|
2034
|
+
|
|
2035
|
+
def power_state(self):
|
|
2036
|
+
record.calls.append("power_state")
|
|
2037
|
+
return {
|
|
2038
|
+
"type": "resp",
|
|
2039
|
+
"id": 1,
|
|
2040
|
+
"ok": True,
|
|
2041
|
+
"power": {"enabled": False},
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
monkeypatch.setattr(cli_module, "NDJSONSerialClient", PowerClient)
|
|
2045
|
+
runner = CliRunner()
|
|
2046
|
+
result = runner.invoke(app, ["power", "--port", "/dev/ttyUSB0"])
|
|
2047
|
+
|
|
2048
|
+
assert result.exit_code == 0
|
|
2049
|
+
assert record.calls == ["power_state"]
|
|
2050
|
+
assert "power.state payload" in recorded_console.getvalue()
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
@pytest.mark.parametrize(
|
|
2054
|
+
"flag, expected_call, panel_label",
|
|
2055
|
+
[
|
|
2056
|
+
("--enable", "power_enable", "power.enable payload"),
|
|
2057
|
+
("--disable", "power_disable", "power.disable payload"),
|
|
2058
|
+
],
|
|
2059
|
+
)
|
|
2060
|
+
def test_power_command_toggles(monkeypatch, recorded_console, flag, expected_call, panel_label):
|
|
2061
|
+
monkeypatch.setattr(cli_module, "spinner", _noop_spinner)
|
|
2062
|
+
record = SimpleNamespace(calls=[])
|
|
2063
|
+
|
|
2064
|
+
class PowerClient:
|
|
2065
|
+
def __init__(self, *args, **kwargs):
|
|
2066
|
+
pass
|
|
2067
|
+
|
|
2068
|
+
def __enter__(self):
|
|
2069
|
+
return self
|
|
2070
|
+
|
|
2071
|
+
def __exit__(self, exc_type, exc, exc_tb):
|
|
2072
|
+
return False
|
|
2073
|
+
|
|
2074
|
+
def power_enable(self):
|
|
2075
|
+
record.calls.append("power_enable")
|
|
2076
|
+
return {
|
|
2077
|
+
"type": "resp",
|
|
2078
|
+
"id": 2,
|
|
2079
|
+
"ok": True,
|
|
2080
|
+
"power": {"enabled": True},
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
def power_disable(self):
|
|
2084
|
+
record.calls.append("power_disable")
|
|
2085
|
+
return {
|
|
2086
|
+
"type": "resp",
|
|
2087
|
+
"id": 3,
|
|
2088
|
+
"ok": True,
|
|
2089
|
+
"power": {"enabled": False},
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
monkeypatch.setattr(cli_module, "NDJSONSerialClient", PowerClient)
|
|
2093
|
+
runner = CliRunner()
|
|
2094
|
+
result = runner.invoke(app, ["power", "--port", "/dev/ttyUSB0", flag])
|
|
2095
|
+
|
|
2096
|
+
assert result.exit_code == 0
|
|
2097
|
+
assert record.calls == [expected_call]
|
|
2098
|
+
assert panel_label in recorded_console.getvalue()
|
|
2099
|
+
|
|
2100
|
+
|
|
2021
2101
|
def test_render_uart_event_handles_invalid_payload(monkeypatch):
|
|
2022
2102
|
buffer = io.StringIO()
|
|
2023
2103
|
console = Console(file=buffer, force_terminal=False, width=80)
|
|
@@ -73,17 +73,21 @@ def test_ndjson_serial_client_payload_builders():
|
|
|
73
73
|
client.spi_disable()
|
|
74
74
|
client.uart_sub({"enable": True, "gap_ms": 5})
|
|
75
75
|
client.uart_sub()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assert calls
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
client.power_state()
|
|
77
|
+
client.power_enable()
|
|
78
|
+
client.power_disable()
|
|
79
|
+
|
|
80
|
+
assert calls == [
|
|
81
|
+
("spi.cfg", {"spi": {"hz": 123}}),
|
|
82
|
+
("uart.cfg", {"uart": {"baudrate": 9600}}),
|
|
83
|
+
("spi.enable", {}),
|
|
84
|
+
("spi.disable", {}),
|
|
85
|
+
("uart.sub", {"uart": {"sub": {"enable": True, "gap_ms": 5}}}),
|
|
86
|
+
("uart.sub", {}),
|
|
87
|
+
("power.state", {}),
|
|
88
|
+
("power.enable", {}),
|
|
89
|
+
("power.disable", {}),
|
|
90
|
+
]
|
|
87
91
|
|
|
88
92
|
|
|
89
93
|
def test_record_sequence_no_tracker(monkeypatch):
|
|
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.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin
RENAMED
|
File without changes
|
{lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/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
|