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.
- {lr_shuttle-0.1.0.dist-info → lr_shuttle-0.1.1.dist-info}/METADATA +2 -2
- lr_shuttle-0.1.1.dist-info/RECORD +10 -0
- shuttle/cli.py +221 -0
- shuttle/serial_client.py +6 -0
- lr_shuttle-0.1.0.dist-info/RECORD +0 -10
- {lr_shuttle-0.1.0.dist-info → lr_shuttle-0.1.1.dist-info}/WHEEL +0 -0
- {lr_shuttle-0.1.0.dist-info → lr_shuttle-0.1.1.dist-info}/entry_points.txt +0 -0
- {lr_shuttle-0.1.0.dist-info → lr_shuttle-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lr-shuttle
|
|
3
|
-
Version: 0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|