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.
Files changed (35) hide show
  1. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/PKG-INFO +1 -1
  2. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/pyproject.toml +1 -1
  3. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/PKG-INFO +1 -1
  4. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/cli.py +54 -0
  5. lr_shuttle-0.2.1/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
  6. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/flash.py +6 -2
  7. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/serial_client.py +9 -0
  8. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli.py +80 -0
  9. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_serial_client.py +15 -11
  10. lr_shuttle-0.2.0/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
  11. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/README.md +0 -0
  12. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/setup.cfg +0 -0
  13. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/SOURCES.txt +0 -0
  14. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/dependency_links.txt +0 -0
  15. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/entry_points.txt +0 -0
  16. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/requires.txt +0 -0
  17. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/lr_shuttle.egg-info/top_level.txt +0 -0
  18. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/constants.py +0 -0
  19. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/__init__.py +0 -0
  20. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/__init__.py +0 -0
  21. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
  22. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
  23. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
  24. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/firmware/esp32c5/manifest.json +0 -0
  25. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/prodtest.py +0 -0
  26. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/src/shuttle/timo.py +0 -0
  27. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_client.py +0 -0
  28. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_edge.py +0 -0
  29. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_seq.py +0 -0
  30. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_cli_utils.py +0 -0
  31. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_flash.py +0 -0
  32. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_prodtest_edge.py +0 -0
  33. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_prodtest_helpers.py +0 -0
  34. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_timo.py +0 -0
  35. {lr_shuttle-0.2.0 → lr_shuttle-0.2.1}/tests/test_timo_write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: CLI and Python client for host-side of json based serial communication with embedded device bridge.
5
5
  Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lr-shuttle"
7
- version = "0.2.0"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: CLI and Python client for host-side of json based serial communication with embedded device bridge.
5
5
  Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
6
  License: MIT
@@ -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,
@@ -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("Manifest segment entries require 'offset' and 'file'")
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(f"Missing firmware artifact: {file_name}") from exc
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
- assert calls[0][0] == "spi.cfg"
78
- assert calls[0][1]["spi"]["hz"] == 123
79
- assert calls[1][0] == "uart.cfg"
80
- assert calls[1][1]["uart"]["baudrate"] == 9600
81
- assert calls[2][0] == "spi.enable"
82
- assert calls[3][0] == "spi.disable"
83
- assert calls[4][0] == "uart.sub"
84
- assert calls[4][1]["uart"]["sub"] == {"enable": True, "gap_ms": 5}
85
- assert calls[5][0] == "uart.sub"
86
- assert calls[5][1] == {}
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):
File without changes
File without changes