lr-shuttle 0.2.0__py3-none-any.whl → 0.2.2__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.
- {lr_shuttle-0.2.0.dist-info → lr_shuttle-0.2.2.dist-info}/METADATA +10 -4
- {lr_shuttle-0.2.0.dist-info → lr_shuttle-0.2.2.dist-info}/RECORD +10 -10
- shuttle/cli.py +575 -0
- shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- shuttle/flash.py +6 -2
- shuttle/prodtest.py +166 -5
- shuttle/serial_client.py +9 -0
- {lr_shuttle-0.2.0.dist-info → lr_shuttle-0.2.2.dist-info}/WHEEL +0 -0
- {lr_shuttle-0.2.0.dist-info → lr_shuttle-0.2.2.dist-info}/entry_points.txt +0 -0
- {lr_shuttle-0.2.0.dist-info → lr_shuttle-0.2.2.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.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -247,6 +247,12 @@ Events continue to emit until you close the subscription or client, so you can a
|
|
|
247
247
|
|
|
248
248
|
| Command | Description |
|
|
249
249
|
| --- | --- |
|
|
250
|
-
| `shuttle prodtest reset` |
|
|
251
|
-
| `shuttle prodtest ping` |
|
|
252
|
-
| `shuttle prodtest io-self-test` |
|
|
250
|
+
| `shuttle prodtest reset` | Reset GPIO pins, IRQ pin and Radio |
|
|
251
|
+
| `shuttle prodtest ping` | Send '+' and expect '-' to verify SPI link |
|
|
252
|
+
| `shuttle prodtest io-self-test` | Perform GPIO self-test on pins given as argument |
|
|
253
|
+
| `shuttle prodtest antenna` | Select antenna |
|
|
254
|
+
| `shuttle prodtest continuous-tx` | Continuous transmitter test |
|
|
255
|
+
| `shuttle prodtest hw-device-id` | Read the 8-byte HW Device ID |
|
|
256
|
+
| `shuttle prodtest serial-number [--value HEX]` | Read or write the 8-byte serial number |
|
|
257
|
+
| `shuttle prodtest config [--value HEX]` | Read or write the 5-byte config payload |
|
|
258
|
+
| `shuttle prodtest erase-nvmc HW_ID` | Erase NVMC if the provided 8-byte HW ID matches |
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
shuttle/cli.py,sha256=
|
|
1
|
+
shuttle/cli.py,sha256=eMJEEmHG6Ar3vQsrQmCbPLY0pHUJg6gUyUDCaW0KNGM,87851
|
|
2
2
|
shuttle/constants.py,sha256=GUlAg3iEuPxLQ2mDCvlv5gVXHnlawl_YeLtaUSqsnPM,757
|
|
3
|
-
shuttle/flash.py,sha256=
|
|
4
|
-
shuttle/prodtest.py,sha256=
|
|
5
|
-
shuttle/serial_client.py,sha256=
|
|
3
|
+
shuttle/flash.py,sha256=9ph23MHL40SjKZoL38Sbd3JbykGb-ECxvzBzCIjAues,4492
|
|
4
|
+
shuttle/prodtest.py,sha256=1q-1dZYtrWBpI_e0jPgROVGbb_42Y0q0DIxDoo4MWUk,8020
|
|
5
|
+
shuttle/serial_client.py,sha256=CnqWpC4CyxNXzsQQgRQsGwkDf19OoIEYvipu4kt2IQo,17392
|
|
6
6
|
shuttle/timo.py,sha256=1K18y0QtDF2lw2Abeok9PgrpPUiCEbQdGQXOQik75Hw,16481
|
|
7
7
|
shuttle/firmware/__init__.py,sha256=KRXyz3xJ2GIB473tCHAky3DdPIQb78gX64Qn-uu55To,120
|
|
8
8
|
shuttle/firmware/esp32c5/__init__.py,sha256=U2xXnb80Wv8EJaJ6Tv9iev1mVlpoaEeqsNmjmEtxdFQ,41
|
|
9
9
|
shuttle/firmware/esp32c5/boot_app0.bin,sha256=-UxdeGp6j6sGrF0Q4zvzdxGmaXY23AN1WeoZzEEKF_A,8192
|
|
10
|
-
shuttle/firmware/esp32c5/devboard.ino.bin,sha256=
|
|
10
|
+
shuttle/firmware/esp32c5/devboard.ino.bin,sha256=vH-IBzbJCpSKgCLDl6snXqe2d5PFJJ3RDVQq41b3iKg,378880
|
|
11
11
|
shuttle/firmware/esp32c5/devboard.ino.bootloader.bin,sha256=LPU51SdUwebYemCZb5Pya-wGe7RC4UXrkRmBnsHePp0,20784
|
|
12
12
|
shuttle/firmware/esp32c5/devboard.ino.partitions.bin,sha256=FIuVnL_xw4qo4dXAup1hLFSZe5ReVqY_QSI-72UGU6E,3072
|
|
13
13
|
shuttle/firmware/esp32c5/manifest.json,sha256=CPOegfEK4PTtI6UPeohuUKkJNeg0t8aWntEczpoxYt4,480
|
|
14
|
-
lr_shuttle-0.2.
|
|
15
|
-
lr_shuttle-0.2.
|
|
16
|
-
lr_shuttle-0.2.
|
|
17
|
-
lr_shuttle-0.2.
|
|
18
|
-
lr_shuttle-0.2.
|
|
14
|
+
lr_shuttle-0.2.2.dist-info/METADATA,sha256=eS1NFB_YNkvsq7eT4akMzyUij0ormtqHHtagAMghonA,13611
|
|
15
|
+
lr_shuttle-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
lr_shuttle-0.2.2.dist-info/entry_points.txt,sha256=obqdFPgvQLB1_EWcnD9ch8HjQRlNVT_pdB_EidDRDco,44
|
|
17
|
+
lr_shuttle-0.2.2.dist-info/top_level.txt,sha256=PtNxNQQdya-Xs8DYublNTBTa8c1TrtfEpQ0lUd_OeZY,8
|
|
18
|
+
lr_shuttle-0.2.2.dist-info/RECORD,,
|
shuttle/cli.py
CHANGED
|
@@ -45,6 +45,67 @@ app.add_typer(
|
|
|
45
45
|
|
|
46
46
|
console = Console()
|
|
47
47
|
UART_RX_POLL_INTERVAL = 0.25
|
|
48
|
+
PRODTEST_ANTENNA_CHOICES = {
|
|
49
|
+
"internal": 0,
|
|
50
|
+
"external": 1,
|
|
51
|
+
}
|
|
52
|
+
PRODTEST_TX_POWER_LEVELS = [
|
|
53
|
+
{
|
|
54
|
+
"value": 0,
|
|
55
|
+
"label": "-30 dBm",
|
|
56
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg30dBm",
|
|
57
|
+
"aliases": ["neg30", "neg30dbm", "-30", "-30dbm"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"value": 1,
|
|
61
|
+
"label": "-20 dBm",
|
|
62
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg20dBm",
|
|
63
|
+
"aliases": ["neg20", "neg20dbm", "-20", "-20dbm"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"value": 2,
|
|
67
|
+
"label": "-16 dBm",
|
|
68
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg16dBm",
|
|
69
|
+
"aliases": ["neg16", "neg16dbm", "-16", "-16dbm"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"value": 3,
|
|
73
|
+
"label": "-12 dBm",
|
|
74
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg12dBm",
|
|
75
|
+
"aliases": ["neg12", "neg12dbm", "-12", "-12dbm"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"value": 4,
|
|
79
|
+
"label": "-8 dBm",
|
|
80
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg8dBm",
|
|
81
|
+
"aliases": ["neg8", "neg8dbm", "-8", "-8dbm"],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"value": 5,
|
|
85
|
+
"label": "-4 dBm",
|
|
86
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Neg4dBm",
|
|
87
|
+
"aliases": ["neg4", "neg4dbm", "-4", "-4dbm"],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"value": 6,
|
|
91
|
+
"label": "0 dBm",
|
|
92
|
+
"macro": "RADIO_TXPOWER_TXPOWER_0dBm",
|
|
93
|
+
"aliases": ["0dbm", "0db", "0d", "zero"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"value": 7,
|
|
97
|
+
"label": "+4 dBm",
|
|
98
|
+
"macro": "RADIO_TXPOWER_TXPOWER_Pos4dBm",
|
|
99
|
+
"aliases": ["pos4", "pos4dbm", "+4", "+4dbm"],
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
PRODTEST_TX_POWER_META = {entry["value"]: entry for entry in PRODTEST_TX_POWER_LEVELS}
|
|
103
|
+
PRODTEST_TX_POWER_ALIASES: Dict[str, int] = {}
|
|
104
|
+
for entry in PRODTEST_TX_POWER_LEVELS:
|
|
105
|
+
for alias in entry["aliases"]:
|
|
106
|
+
PRODTEST_TX_POWER_ALIASES[alias.lower()] = entry["value"]
|
|
107
|
+
PRODTEST_TX_POWER_ALIASES[str(entry["value"])] = entry["value"]
|
|
108
|
+
PRODTEST_TX_POWER_CANONICAL = [entry["aliases"][0] for entry in PRODTEST_TX_POWER_LEVELS]
|
|
48
109
|
|
|
49
110
|
# Backwards-compatible aliases for tests and external callers
|
|
50
111
|
_SerialLogger = SerialLogger
|
|
@@ -79,6 +140,26 @@ def _normalize_choice(value: Optional[str], *, name: str) -> Optional[str]:
|
|
|
79
140
|
return normalized
|
|
80
141
|
|
|
81
142
|
|
|
143
|
+
def _resolve_prodtest_power_choice(value: str) -> Tuple[int, Dict[str, str]]:
|
|
144
|
+
trimmed = value.strip().lower()
|
|
145
|
+
if not trimmed:
|
|
146
|
+
raise typer.BadParameter("Power level cannot be empty")
|
|
147
|
+
resolved = PRODTEST_TX_POWER_ALIASES.get(trimmed)
|
|
148
|
+
if resolved is None:
|
|
149
|
+
try:
|
|
150
|
+
parsed = int(trimmed, 0)
|
|
151
|
+
except ValueError:
|
|
152
|
+
parsed = None
|
|
153
|
+
if parsed is not None and parsed in PRODTEST_TX_POWER_META:
|
|
154
|
+
resolved = parsed
|
|
155
|
+
if resolved is None:
|
|
156
|
+
allowed = ", ".join(PRODTEST_TX_POWER_CANONICAL)
|
|
157
|
+
raise typer.BadParameter(
|
|
158
|
+
f"Power must be one of: {allowed} or an index 0-7"
|
|
159
|
+
)
|
|
160
|
+
return resolved, PRODTEST_TX_POWER_META[resolved]
|
|
161
|
+
|
|
162
|
+
|
|
82
163
|
def _normalize_uart_parity(value: Optional[str]) -> Optional[str]:
|
|
83
164
|
if value is None:
|
|
84
165
|
return None
|
|
@@ -195,6 +276,21 @@ def _parse_prodtest_mask(value: str) -> bytes:
|
|
|
195
276
|
raise typer.BadParameter(str(exc)) from exc
|
|
196
277
|
|
|
197
278
|
|
|
279
|
+
def _parse_fixed_hex(value: str, *, name: str, length: int) -> bytes:
|
|
280
|
+
cleaned = value.strip()
|
|
281
|
+
if cleaned.startswith(("0x", "0X")):
|
|
282
|
+
cleaned = cleaned[2:]
|
|
283
|
+
cleaned = cleaned.replace("_", "").replace(" ", "")
|
|
284
|
+
if len(cleaned) != length * 2:
|
|
285
|
+
raise typer.BadParameter(
|
|
286
|
+
f"{name} must be {length * 2} hex characters ({length} bytes)"
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
return bytes.fromhex(cleaned)
|
|
290
|
+
except ValueError as exc:
|
|
291
|
+
raise typer.BadParameter(f"{name} must be valid hex") from exc
|
|
292
|
+
|
|
293
|
+
|
|
198
294
|
def _format_hex(hex_str: str) -> str:
|
|
199
295
|
if not hex_str:
|
|
200
296
|
return "—"
|
|
@@ -1438,6 +1534,431 @@ def prodtest_ping(
|
|
|
1438
1534
|
raise typer.Exit(1)
|
|
1439
1535
|
|
|
1440
1536
|
|
|
1537
|
+
@prodtest_app.command("antenna")
|
|
1538
|
+
def prodtest_antenna(
|
|
1539
|
+
ctx: typer.Context,
|
|
1540
|
+
antenna: str = typer.Argument(
|
|
1541
|
+
...,
|
|
1542
|
+
metavar="ANTENNA",
|
|
1543
|
+
help="Antenna to select (internal/external)",
|
|
1544
|
+
),
|
|
1545
|
+
port: Optional[str] = typer.Option(
|
|
1546
|
+
None,
|
|
1547
|
+
"--port",
|
|
1548
|
+
envvar="SHUTTLE_PORT",
|
|
1549
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1550
|
+
),
|
|
1551
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1552
|
+
timeout: float = typer.Option(
|
|
1553
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1554
|
+
),
|
|
1555
|
+
):
|
|
1556
|
+
"""Select the active prodtest antenna (opcode 'a')."""
|
|
1557
|
+
|
|
1558
|
+
resources = _ctx_resources(ctx)
|
|
1559
|
+
normalized = antenna.strip().lower()
|
|
1560
|
+
try:
|
|
1561
|
+
antenna_value = PRODTEST_ANTENNA_CHOICES[normalized]
|
|
1562
|
+
except KeyError as exc:
|
|
1563
|
+
allowed = ", ".join(sorted(PRODTEST_ANTENNA_CHOICES))
|
|
1564
|
+
raise typer.BadParameter(
|
|
1565
|
+
f"Antenna must be one of: {allowed}"
|
|
1566
|
+
) from exc
|
|
1567
|
+
|
|
1568
|
+
sequence = [prodtest.select_antenna(antenna_value)]
|
|
1569
|
+
responses = _execute_timo_sequence(
|
|
1570
|
+
port=port,
|
|
1571
|
+
baudrate=baudrate,
|
|
1572
|
+
timeout=timeout,
|
|
1573
|
+
sequence=sequence,
|
|
1574
|
+
spinner_label=f"Selecting prodtest antenna ({normalized})",
|
|
1575
|
+
logger=resources.get("logger"),
|
|
1576
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
if not responses:
|
|
1580
|
+
console.print("[red]Device returned no response[/]")
|
|
1581
|
+
raise typer.Exit(1)
|
|
1582
|
+
|
|
1583
|
+
response = responses[0]
|
|
1584
|
+
_render_spi_response(
|
|
1585
|
+
"prodtest antenna",
|
|
1586
|
+
response,
|
|
1587
|
+
command_label="spi.xfer (prodtest)",
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
if not response.get("ok"):
|
|
1591
|
+
raise typer.Exit(1)
|
|
1592
|
+
|
|
1593
|
+
console.print(f"[green]Antenna set to {normalized}")
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
@prodtest_app.command("continuous-tx")
|
|
1597
|
+
def prodtest_continuous_tx(
|
|
1598
|
+
ctx: typer.Context,
|
|
1599
|
+
channel: int = typer.Argument(
|
|
1600
|
+
...,
|
|
1601
|
+
metavar="CHANNEL",
|
|
1602
|
+
min=0,
|
|
1603
|
+
max=100,
|
|
1604
|
+
help="NRF_RADIO->FREQUENCY channel index (0=2400 MHz, 100=2500 MHz)",
|
|
1605
|
+
),
|
|
1606
|
+
power: str = typer.Argument(
|
|
1607
|
+
...,
|
|
1608
|
+
metavar="POWER",
|
|
1609
|
+
help=(
|
|
1610
|
+
"Output power alias (neg30, neg20, neg16, neg12, neg8, neg4, 0, pos4) "
|
|
1611
|
+
"or numeric index 0-7"
|
|
1612
|
+
),
|
|
1613
|
+
),
|
|
1614
|
+
port: Optional[str] = typer.Option(
|
|
1615
|
+
None,
|
|
1616
|
+
"--port",
|
|
1617
|
+
envvar="SHUTTLE_PORT",
|
|
1618
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1619
|
+
),
|
|
1620
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1621
|
+
timeout: float = typer.Option(
|
|
1622
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1623
|
+
),
|
|
1624
|
+
):
|
|
1625
|
+
"""Run the prodtest continuous transmitter test (opcode 'o')."""
|
|
1626
|
+
|
|
1627
|
+
resources = _ctx_resources(ctx)
|
|
1628
|
+
power_value, power_meta = _resolve_prodtest_power_choice(power)
|
|
1629
|
+
freq_mhz = 2400 + channel
|
|
1630
|
+
sequence = [prodtest.continuous_transmitter(channel, power_value)]
|
|
1631
|
+
responses = _execute_timo_sequence(
|
|
1632
|
+
port=port,
|
|
1633
|
+
baudrate=baudrate,
|
|
1634
|
+
timeout=timeout,
|
|
1635
|
+
sequence=sequence,
|
|
1636
|
+
spinner_label=(
|
|
1637
|
+
f"Enabling continuous TX (ch={channel}, power={power_meta['label']})"
|
|
1638
|
+
),
|
|
1639
|
+
logger=resources.get("logger"),
|
|
1640
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
if not responses:
|
|
1644
|
+
console.print("[red]Device returned no response[/]")
|
|
1645
|
+
raise typer.Exit(1)
|
|
1646
|
+
|
|
1647
|
+
response = responses[0]
|
|
1648
|
+
_render_spi_response(
|
|
1649
|
+
"prodtest continuous-tx",
|
|
1650
|
+
response,
|
|
1651
|
+
command_label="spi.xfer (prodtest)",
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
if not response.get("ok"):
|
|
1655
|
+
raise typer.Exit(1)
|
|
1656
|
+
|
|
1657
|
+
console.print(
|
|
1658
|
+
f"[green]Continuous transmitter enabled[/] channel={channel} ({freq_mhz} MHz) "
|
|
1659
|
+
f"power={power_meta['label']} ({power_meta['macro']})"
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
@prodtest_app.command("hw-device-id")
|
|
1664
|
+
def prodtest_hw_device_id(
|
|
1665
|
+
ctx: typer.Context,
|
|
1666
|
+
port: Optional[str] = typer.Option(
|
|
1667
|
+
None,
|
|
1668
|
+
"--port",
|
|
1669
|
+
envvar="SHUTTLE_PORT",
|
|
1670
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1671
|
+
),
|
|
1672
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1673
|
+
timeout: float = typer.Option(
|
|
1674
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1675
|
+
),
|
|
1676
|
+
):
|
|
1677
|
+
"""Read the prodtest HW Device ID (opcode 'I')."""
|
|
1678
|
+
|
|
1679
|
+
resources = _ctx_resources(ctx)
|
|
1680
|
+
sequence = prodtest.hw_device_id_sequence()
|
|
1681
|
+
responses = _execute_timo_sequence(
|
|
1682
|
+
port=port,
|
|
1683
|
+
baudrate=baudrate,
|
|
1684
|
+
timeout=timeout,
|
|
1685
|
+
sequence=sequence,
|
|
1686
|
+
spinner_label="Reading prodtest HW Device ID",
|
|
1687
|
+
logger=resources.get("logger"),
|
|
1688
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
if not responses:
|
|
1692
|
+
console.print("[red]Device returned no response[/]")
|
|
1693
|
+
raise typer.Exit(1)
|
|
1694
|
+
|
|
1695
|
+
failed_idx = next(
|
|
1696
|
+
(idx for idx, resp in enumerate(responses) if not resp.get("ok")), None
|
|
1697
|
+
)
|
|
1698
|
+
if failed_idx is not None:
|
|
1699
|
+
phase = "command" if failed_idx == 0 else "payload"
|
|
1700
|
+
_render_spi_response(
|
|
1701
|
+
f"prodtest hw-device-id ({phase})",
|
|
1702
|
+
responses[failed_idx],
|
|
1703
|
+
command_label=f"spi.xfer (prodtest {phase})",
|
|
1704
|
+
)
|
|
1705
|
+
raise typer.Exit(1)
|
|
1706
|
+
|
|
1707
|
+
if len(responses) != len(sequence):
|
|
1708
|
+
console.print("[red]Prodtest command halted before completing all SPI phases[/]")
|
|
1709
|
+
raise typer.Exit(1)
|
|
1710
|
+
|
|
1711
|
+
result_response = responses[-1]
|
|
1712
|
+
_render_spi_response(
|
|
1713
|
+
"prodtest hw-device-id",
|
|
1714
|
+
result_response,
|
|
1715
|
+
command_label="spi.xfer (prodtest payload)",
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
rx_bytes = _decode_hex_response(result_response, label="prodtest hw-device-id")
|
|
1719
|
+
if len(rx_bytes) < prodtest.HW_DEVICE_ID_RESULT_LEN:
|
|
1720
|
+
console.print("[red]Prodtest HW Device ID response shorter than expected[/]")
|
|
1721
|
+
raise typer.Exit(1)
|
|
1722
|
+
|
|
1723
|
+
hw_id = rx_bytes[-prodtest.HW_DEVICE_ID_RESULT_LEN :]
|
|
1724
|
+
console.print(f"HW Device ID: {_format_hex(hw_id.hex())}")
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
@prodtest_app.command("serial-number")
|
|
1728
|
+
def prodtest_serial_number(
|
|
1729
|
+
ctx: typer.Context,
|
|
1730
|
+
value: Optional[str] = typer.Option(
|
|
1731
|
+
None,
|
|
1732
|
+
"--value",
|
|
1733
|
+
"-v",
|
|
1734
|
+
help="Hex-encoded 8-byte serial number to write (omit to read)",
|
|
1735
|
+
),
|
|
1736
|
+
port: Optional[str] = typer.Option(
|
|
1737
|
+
None,
|
|
1738
|
+
"--port",
|
|
1739
|
+
envvar="SHUTTLE_PORT",
|
|
1740
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1741
|
+
),
|
|
1742
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1743
|
+
timeout: float = typer.Option(
|
|
1744
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1745
|
+
),
|
|
1746
|
+
):
|
|
1747
|
+
"""Read or write the prodtest serial number (opcode 'x'/'X')."""
|
|
1748
|
+
|
|
1749
|
+
resources = _ctx_resources(ctx)
|
|
1750
|
+
if value is None:
|
|
1751
|
+
sequence = prodtest.serial_number_read_sequence()
|
|
1752
|
+
spinner_label = "Reading prodtest serial number"
|
|
1753
|
+
else:
|
|
1754
|
+
serial_bytes = _parse_fixed_hex(
|
|
1755
|
+
value, name="--value", length=prodtest.SERIAL_NUMBER_LEN
|
|
1756
|
+
)
|
|
1757
|
+
sequence = [prodtest.serial_number_write(serial_bytes)]
|
|
1758
|
+
spinner_label = "Writing prodtest serial number"
|
|
1759
|
+
|
|
1760
|
+
responses = _execute_timo_sequence(
|
|
1761
|
+
port=port,
|
|
1762
|
+
baudrate=baudrate,
|
|
1763
|
+
timeout=timeout,
|
|
1764
|
+
sequence=sequence,
|
|
1765
|
+
spinner_label=spinner_label,
|
|
1766
|
+
logger=resources.get("logger"),
|
|
1767
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
if not responses:
|
|
1771
|
+
console.print("[red]Device returned no response[/]")
|
|
1772
|
+
raise typer.Exit(1)
|
|
1773
|
+
|
|
1774
|
+
failed_idx = next(
|
|
1775
|
+
(idx for idx, resp in enumerate(responses) if not resp.get("ok")), None
|
|
1776
|
+
)
|
|
1777
|
+
if failed_idx is not None:
|
|
1778
|
+
phase = "command" if failed_idx == 0 else "payload"
|
|
1779
|
+
_render_spi_response(
|
|
1780
|
+
f"prodtest serial-number ({phase})",
|
|
1781
|
+
responses[failed_idx],
|
|
1782
|
+
command_label=f"spi.xfer (prodtest {phase})",
|
|
1783
|
+
)
|
|
1784
|
+
raise typer.Exit(1)
|
|
1785
|
+
|
|
1786
|
+
if len(responses) != len(sequence):
|
|
1787
|
+
console.print(
|
|
1788
|
+
"[red]Prodtest command halted before completing all SPI phases[/]"
|
|
1789
|
+
)
|
|
1790
|
+
raise typer.Exit(1)
|
|
1791
|
+
|
|
1792
|
+
if value is None:
|
|
1793
|
+
result_response = responses[-1]
|
|
1794
|
+
_render_spi_response(
|
|
1795
|
+
"prodtest serial-number",
|
|
1796
|
+
result_response,
|
|
1797
|
+
command_label="spi.xfer (prodtest payload)",
|
|
1798
|
+
)
|
|
1799
|
+
rx_bytes = _decode_hex_response(
|
|
1800
|
+
result_response, label="prodtest serial-number"
|
|
1801
|
+
)
|
|
1802
|
+
if len(rx_bytes) < prodtest.SERIAL_NUMBER_LEN:
|
|
1803
|
+
console.print("[red]Prodtest serial-number response shorter than expected[/]")
|
|
1804
|
+
raise typer.Exit(1)
|
|
1805
|
+
serial_bytes = rx_bytes[-prodtest.SERIAL_NUMBER_LEN :]
|
|
1806
|
+
console.print(f"Serial number: {_format_hex(serial_bytes.hex())}")
|
|
1807
|
+
else:
|
|
1808
|
+
_render_spi_response(
|
|
1809
|
+
"prodtest serial-number",
|
|
1810
|
+
responses[0],
|
|
1811
|
+
command_label="spi.xfer (prodtest command)",
|
|
1812
|
+
)
|
|
1813
|
+
console.print(
|
|
1814
|
+
f"[green]Serial number updated[/] value={_format_hex(serial_bytes.hex())}"
|
|
1815
|
+
)
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
@prodtest_app.command("config")
|
|
1819
|
+
def prodtest_config(
|
|
1820
|
+
ctx: typer.Context,
|
|
1821
|
+
value: Optional[str] = typer.Option(
|
|
1822
|
+
None,
|
|
1823
|
+
"--value",
|
|
1824
|
+
"-v",
|
|
1825
|
+
help="Hex-encoded 5-byte config payload to write (omit to read)",
|
|
1826
|
+
),
|
|
1827
|
+
port: Optional[str] = typer.Option(
|
|
1828
|
+
None,
|
|
1829
|
+
"--port",
|
|
1830
|
+
envvar="SHUTTLE_PORT",
|
|
1831
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1832
|
+
),
|
|
1833
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1834
|
+
timeout: float = typer.Option(
|
|
1835
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1836
|
+
),
|
|
1837
|
+
):
|
|
1838
|
+
"""Read or write the prodtest config (opcode 'r'/'R')."""
|
|
1839
|
+
|
|
1840
|
+
resources = _ctx_resources(ctx)
|
|
1841
|
+
if value is None:
|
|
1842
|
+
sequence = prodtest.config_read_sequence()
|
|
1843
|
+
spinner_label = "Reading prodtest config"
|
|
1844
|
+
else:
|
|
1845
|
+
config_bytes = _parse_fixed_hex(
|
|
1846
|
+
value, name="--value", length=prodtest.CONFIG_WRITE_LEN
|
|
1847
|
+
)
|
|
1848
|
+
sequence = [prodtest.config_write(config_bytes)]
|
|
1849
|
+
spinner_label = "Writing prodtest config"
|
|
1850
|
+
|
|
1851
|
+
responses = _execute_timo_sequence(
|
|
1852
|
+
port=port,
|
|
1853
|
+
baudrate=baudrate,
|
|
1854
|
+
timeout=timeout,
|
|
1855
|
+
sequence=sequence,
|
|
1856
|
+
spinner_label=spinner_label,
|
|
1857
|
+
logger=resources.get("logger"),
|
|
1858
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
if not responses:
|
|
1862
|
+
console.print("[red]Device returned no response[/]")
|
|
1863
|
+
raise typer.Exit(1)
|
|
1864
|
+
|
|
1865
|
+
failed_idx = next(
|
|
1866
|
+
(idx for idx, resp in enumerate(responses) if not resp.get("ok")), None
|
|
1867
|
+
)
|
|
1868
|
+
if failed_idx is not None:
|
|
1869
|
+
phase = "command" if failed_idx == 0 else "payload"
|
|
1870
|
+
_render_spi_response(
|
|
1871
|
+
f"prodtest config ({phase})",
|
|
1872
|
+
responses[failed_idx],
|
|
1873
|
+
command_label=f"spi.xfer (prodtest {phase})",
|
|
1874
|
+
)
|
|
1875
|
+
raise typer.Exit(1)
|
|
1876
|
+
|
|
1877
|
+
if len(responses) != len(sequence):
|
|
1878
|
+
console.print(
|
|
1879
|
+
"[red]Prodtest command halted before completing all SPI phases[/]"
|
|
1880
|
+
)
|
|
1881
|
+
raise typer.Exit(1)
|
|
1882
|
+
|
|
1883
|
+
if value is None:
|
|
1884
|
+
result_response = responses[-1]
|
|
1885
|
+
_render_spi_response(
|
|
1886
|
+
"prodtest config",
|
|
1887
|
+
result_response,
|
|
1888
|
+
command_label="spi.xfer (prodtest payload)",
|
|
1889
|
+
)
|
|
1890
|
+
rx_bytes = _decode_hex_response(result_response, label="prodtest config")
|
|
1891
|
+
if len(rx_bytes) < prodtest.CONFIG_RESULT_LEN:
|
|
1892
|
+
console.print("[red]Prodtest config response shorter than expected[/]")
|
|
1893
|
+
raise typer.Exit(1)
|
|
1894
|
+
config_bytes = rx_bytes[-prodtest.CONFIG_RESULT_LEN :]
|
|
1895
|
+
console.print(f"Config: {_format_hex(config_bytes.hex())}")
|
|
1896
|
+
else:
|
|
1897
|
+
_render_spi_response(
|
|
1898
|
+
"prodtest config",
|
|
1899
|
+
responses[0],
|
|
1900
|
+
command_label="spi.xfer (prodtest command)",
|
|
1901
|
+
)
|
|
1902
|
+
console.print(
|
|
1903
|
+
f"[green]Config updated[/] value={_format_hex(config_bytes.hex())}"
|
|
1904
|
+
)
|
|
1905
|
+
|
|
1906
|
+
|
|
1907
|
+
@prodtest_app.command("erase-nvmc")
|
|
1908
|
+
def prodtest_erase_nvmc(
|
|
1909
|
+
ctx: typer.Context,
|
|
1910
|
+
hw_device_id: str = typer.Argument(
|
|
1911
|
+
...,
|
|
1912
|
+
metavar="HW_ID",
|
|
1913
|
+
help="Hex-encoded 8-byte HW Device ID (must match device to erase)",
|
|
1914
|
+
),
|
|
1915
|
+
port: Optional[str] = typer.Option(
|
|
1916
|
+
None,
|
|
1917
|
+
"--port",
|
|
1918
|
+
envvar="SHUTTLE_PORT",
|
|
1919
|
+
help="Serial port (e.g., /dev/ttyACM0)",
|
|
1920
|
+
),
|
|
1921
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
1922
|
+
timeout: float = typer.Option(
|
|
1923
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
1924
|
+
),
|
|
1925
|
+
):
|
|
1926
|
+
"""Erase NVMC if the provided HW Device ID matches (opcode 'e')."""
|
|
1927
|
+
|
|
1928
|
+
resources = _ctx_resources(ctx)
|
|
1929
|
+
hw_id_bytes = _parse_fixed_hex(
|
|
1930
|
+
hw_device_id, name="HW_ID", length=prodtest.ERASE_NVMC_LEN
|
|
1931
|
+
)
|
|
1932
|
+
sequence = [prodtest.erase_nvmc(hw_id_bytes)]
|
|
1933
|
+
responses = _execute_timo_sequence(
|
|
1934
|
+
port=port,
|
|
1935
|
+
baudrate=baudrate,
|
|
1936
|
+
timeout=timeout,
|
|
1937
|
+
sequence=sequence,
|
|
1938
|
+
spinner_label="Erasing NVMC (if HW ID matches)",
|
|
1939
|
+
logger=resources.get("logger"),
|
|
1940
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
if not responses:
|
|
1944
|
+
console.print("[red]Device returned no response[/]")
|
|
1945
|
+
raise typer.Exit(1)
|
|
1946
|
+
|
|
1947
|
+
response = responses[0]
|
|
1948
|
+
_render_spi_response(
|
|
1949
|
+
"prodtest erase-nvmc",
|
|
1950
|
+
response,
|
|
1951
|
+
command_label="spi.xfer (prodtest)",
|
|
1952
|
+
)
|
|
1953
|
+
|
|
1954
|
+
if not response.get("ok"):
|
|
1955
|
+
raise typer.Exit(1)
|
|
1956
|
+
|
|
1957
|
+
console.print(
|
|
1958
|
+
f"[green]erase-nvmc completed[/] hw-id={_format_hex(hw_id_bytes.hex())}"
|
|
1959
|
+
)
|
|
1960
|
+
|
|
1961
|
+
|
|
1441
1962
|
@prodtest_app.command("io-self-test")
|
|
1442
1963
|
def prodtest_io_self_test(
|
|
1443
1964
|
ctx: typer.Context,
|
|
@@ -1977,6 +2498,60 @@ def uart_rx_command(
|
|
|
1977
2498
|
console.print("[yellow]No uart.rx events observed[/]")
|
|
1978
2499
|
|
|
1979
2500
|
|
|
2501
|
+
@app.command("power")
|
|
2502
|
+
def power_command(
|
|
2503
|
+
ctx: typer.Context,
|
|
2504
|
+
port: Optional[str] = typer.Option(
|
|
2505
|
+
None,
|
|
2506
|
+
"--port",
|
|
2507
|
+
envvar="SHUTTLE_PORT",
|
|
2508
|
+
help="Serial port (e.g., /dev/ttyUSB0)",
|
|
2509
|
+
),
|
|
2510
|
+
baudrate: int = typer.Option(DEFAULT_BAUD, "--baud", help="Serial baud rate"),
|
|
2511
|
+
timeout: float = typer.Option(
|
|
2512
|
+
DEFAULT_TIMEOUT, "--timeout", help="Read timeout in seconds"
|
|
2513
|
+
),
|
|
2514
|
+
enable: Optional[bool] = typer.Option(
|
|
2515
|
+
None,
|
|
2516
|
+
"--enable/--disable",
|
|
2517
|
+
help="Enable or disable the downstream power rail",
|
|
2518
|
+
),
|
|
2519
|
+
):
|
|
2520
|
+
"""Query or toggle the downstream power rail."""
|
|
2521
|
+
|
|
2522
|
+
resources = _ctx_resources(ctx)
|
|
2523
|
+
resolved_port = _require_port(port)
|
|
2524
|
+
if enable is None:
|
|
2525
|
+
action = "Querying"
|
|
2526
|
+
label = "power.state"
|
|
2527
|
+
method_name = "power_state"
|
|
2528
|
+
elif enable:
|
|
2529
|
+
action = "Enabling"
|
|
2530
|
+
label = "power.enable"
|
|
2531
|
+
method_name = "power_enable"
|
|
2532
|
+
else:
|
|
2533
|
+
action = "Disabling"
|
|
2534
|
+
label = "power.disable"
|
|
2535
|
+
method_name = "power_disable"
|
|
2536
|
+
|
|
2537
|
+
with spinner(f"{action} power over {resolved_port}"):
|
|
2538
|
+
try:
|
|
2539
|
+
with NDJSONSerialClient(
|
|
2540
|
+
resolved_port,
|
|
2541
|
+
baudrate=baudrate,
|
|
2542
|
+
timeout=timeout,
|
|
2543
|
+
logger=resources.get("logger"),
|
|
2544
|
+
seq_tracker=resources.get("seq_tracker"),
|
|
2545
|
+
) as client:
|
|
2546
|
+
method = getattr(client, method_name)
|
|
2547
|
+
response = method()
|
|
2548
|
+
except ShuttleSerialError as exc:
|
|
2549
|
+
console.print(f"[red]{exc}[/]")
|
|
2550
|
+
raise typer.Exit(1) from exc
|
|
2551
|
+
|
|
2552
|
+
_render_payload_response(label, response)
|
|
2553
|
+
|
|
2554
|
+
|
|
1980
2555
|
@app.command("flash")
|
|
1981
2556
|
def flash_command(
|
|
1982
2557
|
ctx: typer.Context,
|
|
Binary file
|
shuttle/flash.py
CHANGED
|
@@ -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:
|
shuttle/prodtest.py
CHANGED
|
@@ -10,10 +10,32 @@ from . import timo
|
|
|
10
10
|
|
|
11
11
|
RESET_OPCODE = ord("?")
|
|
12
12
|
PLUS_OPCODE = ord("+")
|
|
13
|
+
ANTENNA_OPCODE = ord("a")
|
|
14
|
+
CONTINUOUS_TX_OPCODE = ord("o")
|
|
15
|
+
HW_DEVICE_ID_OPCODE = ord("I")
|
|
16
|
+
SERIAL_NUMBER_READ_OPCODE = ord("x")
|
|
17
|
+
SERIAL_NUMBER_WRITE_OPCODE = ord("X")
|
|
18
|
+
CONFIG_READ_OPCODE = ord("r")
|
|
19
|
+
CONFIG_WRITE_OPCODE = ord("R")
|
|
20
|
+
ERASE_NVMC_OPCODE = ord("e")
|
|
13
21
|
IO_SELF_TEST_OPCODE = ord("T")
|
|
14
22
|
IO_SELF_TEST_MASK_LEN = 8
|
|
15
23
|
IO_SELF_TEST_DUMMY_BYTE = 0xFF
|
|
16
|
-
IO_SELF_TEST_IRQ_TIMEOUT_US =
|
|
24
|
+
IO_SELF_TEST_IRQ_TIMEOUT_US = 500_000
|
|
25
|
+
RESET_IRQ_TIMEOUT_US = 200_000
|
|
26
|
+
CONTINUOUS_TX_IRQ_TIMEOUT_US = 100_000
|
|
27
|
+
HW_DEVICE_ID_RESULT_LEN = 8
|
|
28
|
+
HW_DEVICE_ID_DUMMY_BYTE = 0xFF
|
|
29
|
+
HW_DEVICE_ID_IRQ_TIMEOUT_US = 100_000
|
|
30
|
+
SERIAL_NUMBER_LEN = 8
|
|
31
|
+
SERIAL_NUMBER_DUMMY_BYTE = 0xFF
|
|
32
|
+
SERIAL_NUMBER_IRQ_TIMEOUT_US = 100_000
|
|
33
|
+
CONFIG_RESULT_LEN = 6
|
|
34
|
+
CONFIG_WRITE_LEN = 5
|
|
35
|
+
CONFIG_DUMMY_BYTE = 0xFF
|
|
36
|
+
CONFIG_IRQ_TIMEOUT_US = 100_000
|
|
37
|
+
ERASE_NVMC_LEN = 8
|
|
38
|
+
ERASE_NVMC_IRQ_TIMEOUT_US = 500_000
|
|
17
39
|
|
|
18
40
|
|
|
19
41
|
def _ensure_byte(value: int) -> int:
|
|
@@ -32,16 +54,31 @@ def _build_command_bytes(opcode: int, arguments: Iterable[int] | bytes = ()) ->
|
|
|
32
54
|
return bytes([opcode]) + payload
|
|
33
55
|
|
|
34
56
|
|
|
35
|
-
def command(
|
|
57
|
+
def command(
|
|
58
|
+
opcode: int,
|
|
59
|
+
arguments: Iterable[int] | bytes = (),
|
|
60
|
+
*,
|
|
61
|
+
params: dict | None = None,
|
|
62
|
+
) -> dict:
|
|
36
63
|
"""Build an NDJSON-ready spi.xfer payload for a prodtest command."""
|
|
37
64
|
|
|
38
|
-
return timo.command_payload(
|
|
65
|
+
return timo.command_payload(
|
|
66
|
+
_build_command_bytes(opcode, arguments), params=params
|
|
67
|
+
)
|
|
39
68
|
|
|
40
69
|
|
|
41
70
|
def reset() -> dict:
|
|
42
71
|
"""Return the prodtest reset command (single '?' byte)."""
|
|
43
72
|
|
|
44
|
-
return
|
|
73
|
+
return timo.command_payload(
|
|
74
|
+
bytes([RESET_OPCODE]),
|
|
75
|
+
params={
|
|
76
|
+
"wait_irq": {
|
|
77
|
+
"edge": "leading",
|
|
78
|
+
"timeout_us": RESET_IRQ_TIMEOUT_US,
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
)
|
|
45
82
|
|
|
46
83
|
|
|
47
84
|
def reset_transfer() -> dict:
|
|
@@ -50,6 +87,130 @@ def reset_transfer() -> dict:
|
|
|
50
87
|
return reset()
|
|
51
88
|
|
|
52
89
|
|
|
90
|
+
def select_antenna(selection: int) -> dict:
|
|
91
|
+
"""Return the prodtest antenna command for the given antenna index."""
|
|
92
|
+
|
|
93
|
+
return command(ANTENNA_OPCODE, [selection])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def continuous_transmitter(channel: int, power_level: int) -> dict:
|
|
97
|
+
"""Return the prodtest continuous transmitter command (opcode 'o')."""
|
|
98
|
+
|
|
99
|
+
return command(
|
|
100
|
+
CONTINUOUS_TX_OPCODE,
|
|
101
|
+
[channel, power_level],
|
|
102
|
+
params={
|
|
103
|
+
"wait_irq": {
|
|
104
|
+
"edge": "trailing",
|
|
105
|
+
"timeout_us": CONTINUOUS_TX_IRQ_TIMEOUT_US,
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def hw_device_id_sequence() -> Sequence[dict]:
|
|
112
|
+
"""Return the SPI frames required to read the HW Device ID (opcode 'I')."""
|
|
113
|
+
|
|
114
|
+
command = timo.command_payload(
|
|
115
|
+
bytes([HW_DEVICE_ID_OPCODE]),
|
|
116
|
+
params={
|
|
117
|
+
"wait_irq": {
|
|
118
|
+
"edge": "trailing",
|
|
119
|
+
"timeout_us": HW_DEVICE_ID_IRQ_TIMEOUT_US,
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
readback = timo.command_payload(
|
|
124
|
+
bytes([HW_DEVICE_ID_DUMMY_BYTE] * HW_DEVICE_ID_RESULT_LEN)
|
|
125
|
+
)
|
|
126
|
+
return (command, readback)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def serial_number_read_sequence() -> Sequence[dict]:
|
|
130
|
+
"""Return the SPI frames required to read the prodtest serial number (opcode 'x')."""
|
|
131
|
+
|
|
132
|
+
command = timo.command_payload(
|
|
133
|
+
bytes([SERIAL_NUMBER_READ_OPCODE]),
|
|
134
|
+
params={
|
|
135
|
+
"wait_irq": {
|
|
136
|
+
"edge": "trailing",
|
|
137
|
+
"timeout_us": SERIAL_NUMBER_IRQ_TIMEOUT_US,
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
readback = timo.command_payload(
|
|
142
|
+
bytes([SERIAL_NUMBER_DUMMY_BYTE] * SERIAL_NUMBER_LEN)
|
|
143
|
+
)
|
|
144
|
+
return (command, readback)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def serial_number_write(serial_bytes: bytes) -> dict:
|
|
148
|
+
"""Build the prodtest serial-number write command (opcode 'X')."""
|
|
149
|
+
|
|
150
|
+
if len(serial_bytes) != SERIAL_NUMBER_LEN:
|
|
151
|
+
raise ValueError("Serial number must be exactly 8 bytes")
|
|
152
|
+
return command(
|
|
153
|
+
SERIAL_NUMBER_WRITE_OPCODE,
|
|
154
|
+
serial_bytes,
|
|
155
|
+
params={
|
|
156
|
+
"wait_irq": {
|
|
157
|
+
"edge": "trailing",
|
|
158
|
+
"timeout_us": SERIAL_NUMBER_IRQ_TIMEOUT_US,
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def config_read_sequence() -> Sequence[dict]:
|
|
165
|
+
"""Return the SPI frames required to read the prodtest config (opcode 'r')."""
|
|
166
|
+
|
|
167
|
+
command = timo.command_payload(
|
|
168
|
+
bytes([CONFIG_READ_OPCODE]),
|
|
169
|
+
params={
|
|
170
|
+
"wait_irq": {
|
|
171
|
+
"edge": "trailing",
|
|
172
|
+
"timeout_us": CONFIG_IRQ_TIMEOUT_US,
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
readback = timo.command_payload(bytes([CONFIG_DUMMY_BYTE] * CONFIG_RESULT_LEN))
|
|
177
|
+
return (command, readback)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def config_write(config_bytes: bytes) -> dict:
|
|
181
|
+
"""Build the prodtest config write command (opcode 'R')."""
|
|
182
|
+
|
|
183
|
+
if len(config_bytes) != CONFIG_WRITE_LEN:
|
|
184
|
+
raise ValueError("Config payload must be exactly 5 bytes")
|
|
185
|
+
return command(
|
|
186
|
+
CONFIG_WRITE_OPCODE,
|
|
187
|
+
config_bytes,
|
|
188
|
+
params={
|
|
189
|
+
"wait_irq": {
|
|
190
|
+
"edge": "trailing",
|
|
191
|
+
"timeout_us": CONFIG_IRQ_TIMEOUT_US,
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def erase_nvmc(hw_id: bytes) -> dict:
|
|
198
|
+
"""Build the prodtest erase command (opcode 'e')."""
|
|
199
|
+
|
|
200
|
+
if len(hw_id) != ERASE_NVMC_LEN:
|
|
201
|
+
raise ValueError("Erase requires an 8-byte HW Device ID argument")
|
|
202
|
+
return command(
|
|
203
|
+
ERASE_NVMC_OPCODE,
|
|
204
|
+
hw_id,
|
|
205
|
+
params={
|
|
206
|
+
"wait_irq": {
|
|
207
|
+
"edge": "trailing",
|
|
208
|
+
"timeout_us": ERASE_NVMC_IRQ_TIMEOUT_US,
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
53
214
|
def ping_sequence() -> Sequence[dict]:
|
|
54
215
|
"""Return the two SPI frames for the prodtest ping action ('+' then dummy)."""
|
|
55
216
|
# First transfer: send '+' (PLUS_OPCODE), expect response (should be ignored)
|
|
@@ -72,7 +233,7 @@ def io_self_test(mask: bytes) -> Sequence[dict]:
|
|
|
72
233
|
command,
|
|
73
234
|
params={
|
|
74
235
|
"wait_irq": {
|
|
75
|
-
"edge": "
|
|
236
|
+
"edge": "trailing",
|
|
76
237
|
"timeout_us": IO_SELF_TEST_IRQ_TIMEOUT_US,
|
|
77
238
|
}
|
|
78
239
|
},
|
shuttle/serial_client.py
CHANGED
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|