lr-shuttle 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.1.0
3
+ Version: 0.1.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
@@ -80,7 +80,7 @@ make -C host dev
80
80
 
81
81
  ### Sequence Integrity Checks
82
82
 
83
- Every device message carries a monotonically increasing `seq` counter. Shuttle enforces sequential integrity both within multi-transfer operations and across invocations when requested:
83
+ Every device message carries a monotonically increasing `seq` counter emitted by the firmware transport itself. Shuttle enforces sequential integrity both within multi-transfer operations and across invocations when requested:
84
84
 
85
85
  - During a command, any gap in response/event sequence numbers raises a `ShuttleSerialError`, helping you catch dropped frames immediately.
86
86
  - Pass `--seq-meta /path/to/seq.meta` to persist the last observed sequence number. Subsequent Shuttle runs expect the very next `seq` value; if a gap is detected (for example because the device dropped messages while Shuttle was offline), the CLI exits with an error detailing the missing value.
@@ -0,0 +1,10 @@
1
+ shuttle/cli.py,sha256=UvaRuBKVdM0JsGC-Gqh8tYauMslouAlF6QuZTXGHjTM,68046
2
+ shuttle/constants.py,sha256=GUlAg3iEuPxLQ2mDCvlv5gVXHnlawl_YeLtaUSqsnPM,757
3
+ shuttle/prodtest.py,sha256=V0wkbicAb-kqMPKsvvi7lQgcPvto7M8RbACU9pf7y8I,3595
4
+ shuttle/serial_client.py,sha256=B4ci-Ox-ig65H0xO32FgJHfTG0Ievg6VjGfQ_PSZjWE,17104
5
+ shuttle/timo.py,sha256=1K18y0QtDF2lw2Abeok9PgrpPUiCEbQdGQXOQik75Hw,16481
6
+ lr_shuttle-0.1.1.dist-info/METADATA,sha256=HYaly9JaR-hKo_S_SzPzJc0LgCoLkh4o-Mar-oRvxlE,12040
7
+ lr_shuttle-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ lr_shuttle-0.1.1.dist-info/entry_points.txt,sha256=obqdFPgvQLB1_EWcnD9ch8HjQRlNVT_pdB_EidDRDco,44
9
+ lr_shuttle-0.1.1.dist-info/top_level.txt,sha256=PtNxNQQdya-Xs8DYublNTBTa8c1TrtfEpQ0lUd_OeZY,8
10
+ lr_shuttle-0.1.1.dist-info/RECORD,,
shuttle/cli.py CHANGED
@@ -4,11 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import string
6
6
  import sys
7
+ import time
7
8
  from contextlib import contextmanager
8
9
  from pathlib import Path
9
10
  from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
10
11
 
11
12
  import atexit
13
+ from concurrent.futures import TimeoutError as FutureTimeout
12
14
  import typer
13
15
  from rich.console import Console
14
16
  from rich.panel import Panel
@@ -40,6 +42,7 @@ app.add_typer(
40
42
  )
41
43
 
42
44
  console = Console()
45
+ UART_RX_POLL_INTERVAL = 0.25
43
46
 
44
47
  # Backwards-compatible aliases for tests and external callers
45
48
  _SerialLogger = SerialLogger
@@ -369,6 +372,74 @@ def _render_ping_response(response: Dict[str, Any]) -> None:
369
372
  _render_payload_response("ping", response)
370
373
 
371
374
 
375
+ def _render_uart_event(event: Dict[str, Any]) -> None:
376
+ data_hex = event.get("data")
377
+ if not isinstance(data_hex, str):
378
+ console.print("[yellow]uart.rx event missing data payload[/]")
379
+ return
380
+ try:
381
+ payload = bytes.fromhex(data_hex)
382
+ except ValueError:
383
+ console.print("[red]uart.rx event payload is not valid hex[/]")
384
+ return
385
+
386
+ seq = event.get("seq", "?")
387
+ port = event.get("port", 0)
388
+ n_field = event.get("n")
389
+ byte_count = n_field if isinstance(n_field, int) else len(payload)
390
+ preview_limit = 64
391
+ ascii_preview = "".join(
392
+ chr(b) if 32 <= b < 127 else "." for b in payload[:preview_limit]
393
+ )
394
+ if len(payload) > preview_limit:
395
+ ascii_preview += " ..."
396
+
397
+ console.print(f"[green]uart.rx[/] seq={seq} port={port} bytes={byte_count}")
398
+ console.print(f" hex : {_format_hex(data_hex)}")
399
+ if payload:
400
+ console.print(
401
+ f" ascii: {ascii_preview if ascii_preview else '(non-printable)'}"
402
+ )
403
+
404
+
405
+ def _consume_uart_events(
406
+ listener,
407
+ *,
408
+ duration: Optional[float],
409
+ forever: bool,
410
+ ) -> int:
411
+ events_seen = 0
412
+ start = time.monotonic()
413
+ deadline = start + duration if duration is not None else None
414
+
415
+ while True:
416
+ if deadline is not None:
417
+ remaining = deadline - time.monotonic()
418
+ if remaining <= 0:
419
+ break
420
+ timeout_value = (
421
+ remaining
422
+ if remaining < UART_RX_POLL_INTERVAL
423
+ else UART_RX_POLL_INTERVAL
424
+ )
425
+ elif forever:
426
+ timeout_value = UART_RX_POLL_INTERVAL
427
+ else:
428
+ timeout_value = None
429
+
430
+ try:
431
+ event = listener.next(timeout=timeout_value)
432
+ except FutureTimeout:
433
+ continue
434
+
435
+ _render_uart_event(event)
436
+ events_seen += 1
437
+ if duration is None and not forever:
438
+ break
439
+
440
+ return events_seen
441
+
442
+
372
443
  @app.callback()
373
444
  def main(
374
445
  ctx: typer.Context,
@@ -1671,6 +1742,69 @@ def uart_cfg_command(
1671
1742
  _render_payload_response("uart.cfg", response)
1672
1743
 
1673
1744
 
1745
+ @app.command("uart-sub")
1746
+ def uart_sub_command(
1747
+ ctx: typer.Context,
1748
+ port: Optional[str] = typer.Option(
1749
+ None,
1750
+ "--port",
1751
+ envvar="SHUTTLE_PORT",
1752
+ help="Serial port (e.g., /dev/ttyUSB0)",
1753
+ ),
1754
+ baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
1755
+ timeout: float = typer.Option(
1756
+ DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
1757
+ ),
1758
+ enable: Optional[bool] = typer.Option(
1759
+ None,
1760
+ "--enable/--disable",
1761
+ help="Enable or disable uart.rx event emission",
1762
+ ),
1763
+ gap_ms: Optional[int] = typer.Option(
1764
+ None,
1765
+ "--gap-ms",
1766
+ min=0,
1767
+ max=1000,
1768
+ help="Milliseconds of idle before emitting buffered bytes",
1769
+ ),
1770
+ buf: Optional[int] = typer.Option(
1771
+ None,
1772
+ "--buf",
1773
+ min=1,
1774
+ max=1024,
1775
+ help="Emit an event once this many bytes are buffered",
1776
+ ),
1777
+ ):
1778
+ """Query or update uart.rx subscription settings."""
1779
+
1780
+ resources = _ctx_resources(ctx)
1781
+ sub_payload: Dict[str, Any] = {}
1782
+ if enable is not None:
1783
+ sub_payload["enable"] = enable
1784
+ if gap_ms is not None:
1785
+ sub_payload["gap_ms"] = gap_ms
1786
+ if buf is not None:
1787
+ sub_payload["buf"] = buf
1788
+
1789
+ resolved_port = _require_port(port)
1790
+ action = "Updating" if sub_payload else "Querying"
1791
+ with spinner(f"{action} uart.sub over {resolved_port}"):
1792
+ try:
1793
+ with NDJSONSerialClient(
1794
+ resolved_port,
1795
+ baudrate=baudrate,
1796
+ timeout=timeout,
1797
+ logger=resources.get("logger"),
1798
+ seq_tracker=resources.get("seq_tracker"),
1799
+ ) as client:
1800
+ response = client.uart_sub(sub_payload if sub_payload else None)
1801
+ except ShuttleSerialError as exc:
1802
+ console.print(f"[red]{exc}[/]")
1803
+ raise typer.Exit(1) from exc
1804
+
1805
+ _render_payload_response("uart.sub", response)
1806
+
1807
+
1674
1808
  @app.command("uart-tx")
1675
1809
  def uart_tx_command(
1676
1810
  ctx: typer.Context,
@@ -1754,6 +1888,93 @@ def uart_tx_command(
1754
1888
  _render_payload_response("uart.tx", response)
1755
1889
 
1756
1890
 
1891
+ @app.command("uart-rx")
1892
+ def uart_rx_command(
1893
+ ctx: typer.Context,
1894
+ port: Optional[str] = typer.Option(
1895
+ None,
1896
+ "--port",
1897
+ envvar="SHUTTLE_PORT",
1898
+ help="Serial port (e.g., /dev/ttyUSB0)",
1899
+ ),
1900
+ baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
1901
+ timeout: float = typer.Option(
1902
+ DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
1903
+ ),
1904
+ duration: Optional[float] = typer.Option(
1905
+ None,
1906
+ "--duration",
1907
+ min=0.0,
1908
+ help="Listen for uart.rx events for N seconds before exiting",
1909
+ ),
1910
+ forever: bool = typer.Option(
1911
+ False,
1912
+ "--forever",
1913
+ help="Stream uart.rx events until interrupted",
1914
+ ),
1915
+ ensure_subscription: bool = typer.Option(
1916
+ True,
1917
+ "--ensure-subscription/--no-ensure-subscription",
1918
+ help="Call uart.sub --enable before listening",
1919
+ ),
1920
+ gap_ms: Optional[int] = typer.Option(
1921
+ None,
1922
+ "--gap-ms",
1923
+ min=0,
1924
+ max=1000,
1925
+ help="Override gap_ms while ensuring the subscription",
1926
+ ),
1927
+ buf: Optional[int] = typer.Option(
1928
+ None,
1929
+ "--buf",
1930
+ min=1,
1931
+ max=1024,
1932
+ help="Override buf while ensuring the subscription",
1933
+ ),
1934
+ ):
1935
+ """Stream uart.rx events emitted by the firmware."""
1936
+
1937
+ if forever and duration is not None:
1938
+ raise typer.BadParameter("--duration cannot be combined with --forever")
1939
+
1940
+ if (gap_ms is not None or buf is not None) and not ensure_subscription:
1941
+ raise typer.BadParameter("--gap-ms/--buf require --ensure-subscription")
1942
+
1943
+ resources = _ctx_resources(ctx)
1944
+ resolved_port = _require_port(port)
1945
+ console.print(f"Listening for uart.rx events on {resolved_port}...")
1946
+
1947
+ events_seen = 0
1948
+ try:
1949
+ with NDJSONSerialClient(
1950
+ resolved_port,
1951
+ baudrate=baudrate,
1952
+ timeout=timeout,
1953
+ logger=resources.get("logger"),
1954
+ seq_tracker=resources.get("seq_tracker"),
1955
+ ) as client:
1956
+ if ensure_subscription:
1957
+ sub_payload: Dict[str, Any] = {"enable": True}
1958
+ if gap_ms is not None:
1959
+ sub_payload["gap_ms"] = gap_ms
1960
+ if buf is not None:
1961
+ sub_payload["buf"] = buf
1962
+ client.uart_sub(sub_payload)
1963
+ listener = client.register_event_listener("uart.rx")
1964
+ events_seen = _consume_uart_events(
1965
+ listener, duration=duration, forever=forever
1966
+ )
1967
+ except ShuttleSerialError as exc:
1968
+ console.print(f"[red]{exc}[/]")
1969
+ raise typer.Exit(1) from exc
1970
+ except KeyboardInterrupt:
1971
+ console.print("\n[yellow]Interrupted by user[/]")
1972
+ raise typer.Exit(1)
1973
+
1974
+ if not events_seen:
1975
+ console.print("[yellow]No uart.rx events observed[/]")
1976
+
1977
+
1757
1978
  @app.command("get-info")
1758
1979
  def get_info(
1759
1980
  ctx: typer.Context,
shuttle/serial_client.py CHANGED
@@ -342,6 +342,12 @@ class NDJSONSerialClient:
342
342
  payload["port"] = port
343
343
  return self._command("uart.tx", payload)
344
344
 
345
+ def uart_sub(self, sub: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
346
+ payload: Dict[str, Any] = {}
347
+ if sub:
348
+ payload["uart"] = {"sub": sub}
349
+ return self._command("uart.sub", payload)
350
+
345
351
  def _command(self, op: str, params: Dict[str, Any]) -> Dict[str, Any]:
346
352
  future = self.send_command(op, params)
347
353
  return future.result()
@@ -1,10 +0,0 @@
1
- shuttle/cli.py,sha256=KKNThf4eG6oOmqR__tw6Yqx-Y62epztbKje3WfNWGy0,61201
2
- shuttle/constants.py,sha256=GUlAg3iEuPxLQ2mDCvlv5gVXHnlawl_YeLtaUSqsnPM,757
3
- shuttle/prodtest.py,sha256=V0wkbicAb-kqMPKsvvi7lQgcPvto7M8RbACU9pf7y8I,3595
4
- shuttle/serial_client.py,sha256=YdZFM13KrOKHGjqW0R25jdfaf4i_O1OtaPDSzVWm5c8,16877
5
- shuttle/timo.py,sha256=1K18y0QtDF2lw2Abeok9PgrpPUiCEbQdGQXOQik75Hw,16481
6
- lr_shuttle-0.1.0.dist-info/METADATA,sha256=TvjPypziHRV3HhEwQXJOMRFgBr8fWm4C5FmDGYcSuC8,11999
7
- lr_shuttle-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- lr_shuttle-0.1.0.dist-info/entry_points.txt,sha256=obqdFPgvQLB1_EWcnD9ch8HjQRlNVT_pdB_EidDRDco,44
9
- lr_shuttle-0.1.0.dist-info/top_level.txt,sha256=PtNxNQQdya-Xs8DYublNTBTa8c1TrtfEpQ0lUd_OeZY,8
10
- lr_shuttle-0.1.0.dist-info/RECORD,,