lr-shuttle 0.2.4__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.

Potentially problematic release.


This version of lr-shuttle might be problematic. Click here for more details.

Files changed (34) hide show
  1. lr_shuttle-0.2.4/PKG-INFO +258 -0
  2. lr_shuttle-0.2.4/README.md +232 -0
  3. lr_shuttle-0.2.4/pyproject.toml +44 -0
  4. lr_shuttle-0.2.4/setup.cfg +4 -0
  5. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/PKG-INFO +258 -0
  6. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/SOURCES.txt +32 -0
  7. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/dependency_links.txt +1 -0
  8. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/entry_points.txt +2 -0
  9. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/requires.txt +14 -0
  10. lr_shuttle-0.2.4/src/lr_shuttle.egg-info/top_level.txt +1 -0
  11. lr_shuttle-0.2.4/src/shuttle/cli.py +2850 -0
  12. lr_shuttle-0.2.4/src/shuttle/constants.py +41 -0
  13. lr_shuttle-0.2.4/src/shuttle/firmware/__init__.py +5 -0
  14. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/__init__.py +1 -0
  15. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
  16. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
  17. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
  18. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
  19. lr_shuttle-0.2.4/src/shuttle/firmware/esp32c5/manifest.json +17 -0
  20. lr_shuttle-0.2.4/src/shuttle/flash.py +136 -0
  21. lr_shuttle-0.2.4/src/shuttle/prodtest.py +279 -0
  22. lr_shuttle-0.2.4/src/shuttle/serial_client.py +514 -0
  23. lr_shuttle-0.2.4/src/shuttle/timo.py +499 -0
  24. lr_shuttle-0.2.4/tests/test_cli.py +3382 -0
  25. lr_shuttle-0.2.4/tests/test_cli_client.py +75 -0
  26. lr_shuttle-0.2.4/tests/test_cli_edge.py +14 -0
  27. lr_shuttle-0.2.4/tests/test_cli_seq.py +90 -0
  28. lr_shuttle-0.2.4/tests/test_cli_utils.py +46 -0
  29. lr_shuttle-0.2.4/tests/test_flash.py +92 -0
  30. lr_shuttle-0.2.4/tests/test_prodtest_edge.py +13 -0
  31. lr_shuttle-0.2.4/tests/test_prodtest_helpers.py +147 -0
  32. lr_shuttle-0.2.4/tests/test_serial_client.py +373 -0
  33. lr_shuttle-0.2.4/tests/test_timo.py +83 -0
  34. lr_shuttle-0.2.4/tests/test_timo_write.py +41 -0
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: lr-shuttle
3
+ Version: 0.2.4
4
+ Summary: CLI and Python client for host-side of json based serial communication with embedded device bridge.
5
+ Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
+ License: MIT
7
+ Keywords: CLI
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: rich>=13.7
14
+ Requires-Dist: pydantic>=2.8
15
+ Requires-Dist: typer>=0.12
16
+ Requires-Dist: pyserial>=3.5
17
+ Requires-Dist: esptool>=4.7
18
+ Provides-Extra: dev
19
+ Requires-Dist: build>=1.2.1; extra == "dev"
20
+ Requires-Dist: twine>=5.1.1; extra == "dev"
21
+ Requires-Dist: wheel; extra == "dev"
22
+ Requires-Dist: pytest>=8.4.2; extra == "dev"
23
+ Requires-Dist: black>=25.9.0; extra == "dev"
24
+ Requires-Dist: pytest-html; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+
27
+ # Shuttle
28
+
29
+ `shuttle` is a Typer-based command-line interface & python library for interacting with the ESP32-C5 devboard over its NDJSON serial protocol. The tool is packaged as `lr-shuttle` for PyPI distribution and exposes high-level helpers for common workflows such as probing firmware info, querying protocol metadata, and issuing TiMo SPI sequences.
30
+
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ python3 -m pip install lr-shuttle
36
+ ```
37
+
38
+ The package supports Python 3.11 and later. When working from this repository you can install it in editable mode:
39
+
40
+ ```bash
41
+ make -C host dev
42
+ ```
43
+
44
+ ## Connecting to the Devboard
45
+
46
+ - The CLI talks to the board over a serial device supplied via `--port` or the `SHUTTLE_PORT` environment variable (e.g., `/dev/ttyUSB0`).
47
+ - Default baud rate is `921600` with a `2s` read timeout. Both can be overridden using `--baud` and `--timeout`.
48
+ - Use `--log SERIAL.log` to capture raw RX/TX NDJSON lines with UTC timestamps for later inspection.
49
+
50
+ ## Core Commands
51
+
52
+ | Command | Description |
53
+ | --- | --- |
54
+ | `shuttle ping` | Sends a `ping` command to fetch firmware/protocol metadata. |
55
+ | `shuttle get-info` | Calls `get.info` and pretty-prints the returned capability snapshot. |
56
+ | `shuttle spi-cfg [options]` | Queries or updates the devboard SPI defaults (wraps the `spi.cfg` protocol command). |
57
+ | `shuttle uart-cfg [options]` | Queries or updates the devboard UART defaults (wraps the `uart.cfg` protocol command). |
58
+ | `shuttle uart-tx [payload]` | Transmits bytes over the devboard UART (wraps the `uart.tx` protocol command). |
59
+ | `shuttle flash --port /dev/ttyUSB0` | Programs the ESP32-C5 devboard using the bundled firmware artifacts. |
60
+
61
+
62
+ ### SPI Configuration Workflow
63
+
64
+ - Run `shuttle spi-cfg --port /dev/ttyUSB0` with no extra flags to fetch the current board-level SPI defaults (the response mirrors the firmware’s `spi` object).
65
+ - Provide overrides such as `--hz 1500000 --clock-phase trailing` to persist new defaults in the device’s NVS store. String arguments are case-insensitive; the CLI normalizes them to the lowercase values expected by the firmware.
66
+ - If you need to push a raw JSON document (e.g., the sample in [`src/spi.cfg`](../src/spi.cfg)), pipe it through a future send-file helper or `screen`/`cat` directly; `spi-cfg` itself focuses on structured flag input.
67
+
68
+
69
+ ### UART Configuration Workflow
70
+
71
+ - Use `shuttle uart-cfg --port /dev/ttyUSB0` with no overrides to dump the persisted UART defaults (`baudrate`, `stopbits`, `parity`).
72
+ - Supply `--baudrate`, `--stopbits`, or `--parity` (accepts `n/none`, `e/even`, `o/odd`) to persist new values. Arguments are validated client-side to match the firmware’s accepted ranges (baudrate 1.2 k–4 M, stopbits 1–2).
73
+ - Like the SPI helper, UART updates are persisted to the device’s NVS region, so you only need to run the command when changing settings.
74
+
75
+ ### UART Transmission
76
+
77
+ - `shuttle uart-tx [HEX] --port /dev/ttyUSB0` forwards a hex-encoded payload to the devboard using the `uart.tx` protocol command. The CLI trims whitespace/underscores and validates the string before sending.
78
+ - To avoid manual hex encoding, pass `--text "Hello"` (optionally with `--newline`) to send UTF-8 text, `--file payload.bin` to transmit the raw bytes of a file, or provide `-` as the argument to read from stdin. Exactly one payload source can be used per invocation.
79
+ - Use `--uart-port` if a future firmware exposes multiple UART instances; otherwise the option can be omitted and the default device UART is used.
80
+ - Responses echo the number of bytes accepted by the firmware, matching the `n` field returned by `uart.tx`.
81
+
82
+ ### Flashing Bundled Firmware
83
+
84
+ - `shuttle flash --port /dev/ttyUSB0` invokes `esptool.py` under the hood and writes the bundled ESP32-C5 firmware (bootloader, partitions, and application images) to the selected device. Pass `--erase-first` to issue a chip erase before programming.
85
+ - Firmware bundles live under `shuttle/firmware/<board>` inside the Python package. Run `make -f Makefile.arduino arduino-python` from the repo root after compiling with Arduino CLI to refresh the packaged binaries and `manifest.json` for distribution builds. The helper also copies `boot_app0.bin` from the ESP-IDF core (needed for the USB CDC-on-boot option) so the CLI uses the same flashing layout as `arduino-cli upload`.
86
+ - Use `--board <name>` if additional bundles are added; the command enumerates available bundles automatically and validates the provided identifier.
87
+
88
+
89
+ ### Sequence Integrity Checks
90
+
91
+ 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:
92
+
93
+ - During a command, any gap in response/event sequence numbers raises a `ShuttleSerialError`, helping you catch dropped frames immediately.
94
+ - 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.
95
+ - The metadata file stores a single integer. Delete it (or point `--seq-meta` to another location) if the device was power-cycled and its counter reset.
96
+
97
+
98
+ ### Logging and Diagnostics
99
+
100
+ - `--log FILE` appends every raw NDJSON line (RX and TX) along with an ISO-8601 timestamp. This is useful for post-run audits or attaching transcripts to bug reports.
101
+ - Combine `--log` with `--seq-meta` to maintain both a byte-perfect trace and an audit trail of sequence continuity.
102
+ - Rich panels highlight non-`ok` responses and include firmware error codes returned by the device, making it straightforward to spot invalid arguments or transport failures.
103
+
104
+ ### Environment Tips
105
+
106
+ - Export `SHUTTLE_PORT` in your shell profile to avoid typing `--port` for each command.
107
+ - For scripted flows, prefer `shuttle timo read-reg` and `shuttle timo nop` helpers instead of manually streaming raw JSON—they take care of command IDs, transfer framing, and error presentation.
108
+ - Use `make -C host test` to run the CLI unit tests and verify local changes before publishing to PyPI.
109
+
110
+
111
+
112
+ ## TiMo SPI Commands
113
+
114
+ Commands implementing the SPI protocol as described at [docs.lumenradio.io/timotwo/spi-interface](https://docs.lumenrad.io/timotwo/spi-interface/).
115
+
116
+ | Command | Description |
117
+ | --- | --- |
118
+ | `shuttle timo nop` | Issues a single-frame TiMo NOP SPI transfer through the devboard. |
119
+ | `shuttle timo read-reg --addr 0x05 --length 2` | Performs the two-phase TiMo register read sequence and decodes the resulting payload/IRQ flags. |
120
+ | `shuttle timo write-reg --addr 0x05 --data cafebabe` | Performs the two-phase TiMo register write sequence to write bytes to a register. |
121
+ | `shuttle timo read-dmx --length 12` | Reads the latest received DMX values from the TiMo device using a two-phase SPI sequence. |
122
+
123
+ All commands respect the global options declared on the root CLI (`--log`, `--seq-meta`, `--port`, etc.). Rich tables are used to render human-friendly summaries of responses and decoded payloads.
124
+
125
+
126
+ ### TiMo Register Read Example
127
+
128
+ To read bytes from a TiMo register, use the `read-reg` command. I.e. to read the device name:
129
+
130
+ ```bash
131
+ $ shuttle timo read-reg --addr 0x36 --length 12
132
+ TiMo read-reg
133
+ Status OK
134
+ Command spi.xfer (payload phase)
135
+ RX 00 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
136
+ IRQ level {'level': 'low'}
137
+ TiMo read-reg
138
+ Address 0x36
139
+ Length 12
140
+ Data 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
141
+ IRQ (command) 0x00
142
+ IRQ (payload) 0x00
143
+ Command RX 00
144
+ Payload RX 00 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
145
+ ```
146
+
147
+
148
+ ### TiMo Register Write Example
149
+
150
+ To write bytes to a TiMo register, use the `write-reg` command. I.e. to set the device name to `Hello World`:
151
+
152
+ ```bash
153
+ shuttle timo write-reg --addr 0x36 --data 48656c6c6f20576f726c6400 --port /dev/ttyUSB0
154
+ ```
155
+
156
+ - `--addr` specifies the register address (decimal or 0x-prefixed, 0-63)
157
+ - `--data` is a hex string of bytes to write (1-32 bytes)
158
+ - `--port` is your serial device
159
+
160
+ The command will print a summary table with the address, data written, and IRQ flags for each phase. If bit 7 of the IRQ flags is set, the sequence should be retried per the TiMo protocol.
161
+
162
+
163
+ ### TiMo DMX Read Example
164
+
165
+ Read the latest received DMX values from the window set up by the DMX_WINDOW register:
166
+
167
+ ```bash
168
+ shuttle timo read-dmx --length 12 --port /dev/ttyUSB0
169
+ ```
170
+
171
+ This will print a summary table with the length, data bytes (hex), and IRQ flags for each phase. If bit 7 of the IRQ flags is set, the sequence should be retried per the TiMo protocol.
172
+
173
+ - `--length` specifies the number of DMX bytes to read (1 - max_transfer_bytes)
174
+ - `--port` is your serial device
175
+
176
+
177
+ ### Using the Library from Python
178
+
179
+ Use the transport helpers for HIL tests with explicit request→response pairing:
180
+
181
+ ```python
182
+ from shuttle.serial_client import NDJSONSerialClient
183
+ from shuttle import timo
184
+
185
+ with NDJSONSerialClient("/dev/ttyUSB0") as client:
186
+ # Fire a TiMo read-reg using the async API
187
+ commands = timo.read_reg_sequence(address=0x05, length=2)
188
+ responses = [client.send_command("spi.xfer", cmd).result(timeout=1.0) for cmd in commands]
189
+ print("Command RX:", responses[0]["rx"])
190
+ print("Payload RX:", responses[1]["rx"])
191
+ ```
192
+
193
+ Legacy helpers (`spi_xfer`, `ping`, etc.) remain for simple sequential calls; prefer `send_command` when you need explicit request→response control.
194
+
195
+ #### Parsing registers with `REGISTER_MAP`
196
+
197
+ `REGISTER_MAP` in `shuttle.timo` documents the bit layout of TiMo registers. Example: read the `VERSION` register (0x10) and decode firmware/hardware versions.
198
+
199
+ ```python
200
+ from shuttle.serial_client import NDJSONSerialClient
201
+ from shuttle import timo
202
+
203
+ def read_register(client, reg_meta):
204
+ addr = reg_meta["address"]
205
+ length = reg_meta.get("length", 1)
206
+ seq = timo.read_reg_sequence(addr, length)
207
+ responses = [client.send_command("spi.xfer", cmd).result(timeout=1.0) for cmd in seq]
208
+ # The payload frame is in the second response's RX field
209
+ rx_payload = bytes.fromhex(responses[1]["rx"])
210
+ return rx_payload[1:] # skip IRQ flags byte
211
+
212
+ with NDJSONSerialClient("/dev/ttyUSB0") as client:
213
+ reg_meta = timo.REGISTER_MAP["VERSION"]
214
+ version_bytes = read_register(client, reg_meta)
215
+ fw_version = timo.slice_bits(version_bytes, *reg_meta["fields"]["FW_VERSION"]["bits"])
216
+ hw_version = timo.slice_bits(version_bytes, *reg_meta["fields"]["HW_VERSION"]["bits"])
217
+ print(f"VERSION: FW={fw_version:#x} HW={hw_version:#x}")
218
+ ```
219
+
220
+ Use the field metadata in `timo.REGISTER_MAP` to interpret other registers (e.g., check `REGISTER_MAP[0x01]["fields"]` for status flags).
221
+
222
+ More examples can be found in the [examples directory](examples/).
223
+
224
+ ### Async-style Command and Event Handling
225
+
226
+ `NDJSONSerialClient` now dispatches in a background reader thread and exposes futures so you can fan out work without changing the client for new ops:
227
+
228
+ - `send_command(op, params)` returns a `Future` that resolves to the matching response or raises on timeout/sequence gap. You can issue multiple commands back-to-back and wait later.
229
+ - `register_event_listener("ev.name")` returns a subscription whose `.next(timeout=…)` yields each event payload; multiple listeners can subscribe to the same event (e.g., IRQ and DMX streams).
230
+
231
+ Example HIL sketch:
232
+
233
+ ```python
234
+ client = NDJSONSerialClient(port, baudrate=DEFAULT_BAUD, timeout=DEFAULT_TIMEOUT)
235
+ irq_sub = client.register_event_listener("spi.irq")
236
+ cmd_future = client.send_command("timo.read-reg", {"address": 0x05, "length": 1})
237
+
238
+ # Wait for either side as your test requires
239
+ reg_resp = cmd_future.result(timeout=1)
240
+ irq_event = irq_sub.next(timeout=1)
241
+ ```
242
+
243
+ Events continue to emit until you close the subscription or client, so you can assert on multiple DMX frames or IRQ edges without recreating listeners.
244
+
245
+
246
+ ## Production Test SPI Commands
247
+
248
+ | Command | Description |
249
+ | --- | --- |
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 |
@@ -0,0 +1,232 @@
1
+ # Shuttle
2
+
3
+ `shuttle` is a Typer-based command-line interface & python library for interacting with the ESP32-C5 devboard over its NDJSON serial protocol. The tool is packaged as `lr-shuttle` for PyPI distribution and exposes high-level helpers for common workflows such as probing firmware info, querying protocol metadata, and issuing TiMo SPI sequences.
4
+
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ python3 -m pip install lr-shuttle
10
+ ```
11
+
12
+ The package supports Python 3.11 and later. When working from this repository you can install it in editable mode:
13
+
14
+ ```bash
15
+ make -C host dev
16
+ ```
17
+
18
+ ## Connecting to the Devboard
19
+
20
+ - The CLI talks to the board over a serial device supplied via `--port` or the `SHUTTLE_PORT` environment variable (e.g., `/dev/ttyUSB0`).
21
+ - Default baud rate is `921600` with a `2s` read timeout. Both can be overridden using `--baud` and `--timeout`.
22
+ - Use `--log SERIAL.log` to capture raw RX/TX NDJSON lines with UTC timestamps for later inspection.
23
+
24
+ ## Core Commands
25
+
26
+ | Command | Description |
27
+ | --- | --- |
28
+ | `shuttle ping` | Sends a `ping` command to fetch firmware/protocol metadata. |
29
+ | `shuttle get-info` | Calls `get.info` and pretty-prints the returned capability snapshot. |
30
+ | `shuttle spi-cfg [options]` | Queries or updates the devboard SPI defaults (wraps the `spi.cfg` protocol command). |
31
+ | `shuttle uart-cfg [options]` | Queries or updates the devboard UART defaults (wraps the `uart.cfg` protocol command). |
32
+ | `shuttle uart-tx [payload]` | Transmits bytes over the devboard UART (wraps the `uart.tx` protocol command). |
33
+ | `shuttle flash --port /dev/ttyUSB0` | Programs the ESP32-C5 devboard using the bundled firmware artifacts. |
34
+
35
+
36
+ ### SPI Configuration Workflow
37
+
38
+ - Run `shuttle spi-cfg --port /dev/ttyUSB0` with no extra flags to fetch the current board-level SPI defaults (the response mirrors the firmware’s `spi` object).
39
+ - Provide overrides such as `--hz 1500000 --clock-phase trailing` to persist new defaults in the device’s NVS store. String arguments are case-insensitive; the CLI normalizes them to the lowercase values expected by the firmware.
40
+ - If you need to push a raw JSON document (e.g., the sample in [`src/spi.cfg`](../src/spi.cfg)), pipe it through a future send-file helper or `screen`/`cat` directly; `spi-cfg` itself focuses on structured flag input.
41
+
42
+
43
+ ### UART Configuration Workflow
44
+
45
+ - Use `shuttle uart-cfg --port /dev/ttyUSB0` with no overrides to dump the persisted UART defaults (`baudrate`, `stopbits`, `parity`).
46
+ - Supply `--baudrate`, `--stopbits`, or `--parity` (accepts `n/none`, `e/even`, `o/odd`) to persist new values. Arguments are validated client-side to match the firmware’s accepted ranges (baudrate 1.2 k–4 M, stopbits 1–2).
47
+ - Like the SPI helper, UART updates are persisted to the device’s NVS region, so you only need to run the command when changing settings.
48
+
49
+ ### UART Transmission
50
+
51
+ - `shuttle uart-tx [HEX] --port /dev/ttyUSB0` forwards a hex-encoded payload to the devboard using the `uart.tx` protocol command. The CLI trims whitespace/underscores and validates the string before sending.
52
+ - To avoid manual hex encoding, pass `--text "Hello"` (optionally with `--newline`) to send UTF-8 text, `--file payload.bin` to transmit the raw bytes of a file, or provide `-` as the argument to read from stdin. Exactly one payload source can be used per invocation.
53
+ - Use `--uart-port` if a future firmware exposes multiple UART instances; otherwise the option can be omitted and the default device UART is used.
54
+ - Responses echo the number of bytes accepted by the firmware, matching the `n` field returned by `uart.tx`.
55
+
56
+ ### Flashing Bundled Firmware
57
+
58
+ - `shuttle flash --port /dev/ttyUSB0` invokes `esptool.py` under the hood and writes the bundled ESP32-C5 firmware (bootloader, partitions, and application images) to the selected device. Pass `--erase-first` to issue a chip erase before programming.
59
+ - Firmware bundles live under `shuttle/firmware/<board>` inside the Python package. Run `make -f Makefile.arduino arduino-python` from the repo root after compiling with Arduino CLI to refresh the packaged binaries and `manifest.json` for distribution builds. The helper also copies `boot_app0.bin` from the ESP-IDF core (needed for the USB CDC-on-boot option) so the CLI uses the same flashing layout as `arduino-cli upload`.
60
+ - Use `--board <name>` if additional bundles are added; the command enumerates available bundles automatically and validates the provided identifier.
61
+
62
+
63
+ ### Sequence Integrity Checks
64
+
65
+ 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:
66
+
67
+ - During a command, any gap in response/event sequence numbers raises a `ShuttleSerialError`, helping you catch dropped frames immediately.
68
+ - 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.
69
+ - The metadata file stores a single integer. Delete it (or point `--seq-meta` to another location) if the device was power-cycled and its counter reset.
70
+
71
+
72
+ ### Logging and Diagnostics
73
+
74
+ - `--log FILE` appends every raw NDJSON line (RX and TX) along with an ISO-8601 timestamp. This is useful for post-run audits or attaching transcripts to bug reports.
75
+ - Combine `--log` with `--seq-meta` to maintain both a byte-perfect trace and an audit trail of sequence continuity.
76
+ - Rich panels highlight non-`ok` responses and include firmware error codes returned by the device, making it straightforward to spot invalid arguments or transport failures.
77
+
78
+ ### Environment Tips
79
+
80
+ - Export `SHUTTLE_PORT` in your shell profile to avoid typing `--port` for each command.
81
+ - For scripted flows, prefer `shuttle timo read-reg` and `shuttle timo nop` helpers instead of manually streaming raw JSON—they take care of command IDs, transfer framing, and error presentation.
82
+ - Use `make -C host test` to run the CLI unit tests and verify local changes before publishing to PyPI.
83
+
84
+
85
+
86
+ ## TiMo SPI Commands
87
+
88
+ Commands implementing the SPI protocol as described at [docs.lumenradio.io/timotwo/spi-interface](https://docs.lumenrad.io/timotwo/spi-interface/).
89
+
90
+ | Command | Description |
91
+ | --- | --- |
92
+ | `shuttle timo nop` | Issues a single-frame TiMo NOP SPI transfer through the devboard. |
93
+ | `shuttle timo read-reg --addr 0x05 --length 2` | Performs the two-phase TiMo register read sequence and decodes the resulting payload/IRQ flags. |
94
+ | `shuttle timo write-reg --addr 0x05 --data cafebabe` | Performs the two-phase TiMo register write sequence to write bytes to a register. |
95
+ | `shuttle timo read-dmx --length 12` | Reads the latest received DMX values from the TiMo device using a two-phase SPI sequence. |
96
+
97
+ All commands respect the global options declared on the root CLI (`--log`, `--seq-meta`, `--port`, etc.). Rich tables are used to render human-friendly summaries of responses and decoded payloads.
98
+
99
+
100
+ ### TiMo Register Read Example
101
+
102
+ To read bytes from a TiMo register, use the `read-reg` command. I.e. to read the device name:
103
+
104
+ ```bash
105
+ $ shuttle timo read-reg --addr 0x36 --length 12
106
+ TiMo read-reg
107
+ Status OK
108
+ Command spi.xfer (payload phase)
109
+ RX 00 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
110
+ IRQ level {'level': 'low'}
111
+ TiMo read-reg
112
+ Address 0x36
113
+ Length 12
114
+ Data 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
115
+ IRQ (command) 0x00
116
+ IRQ (payload) 0x00
117
+ Command RX 00
118
+ Payload RX 00 48 65 6c 6c 6f 20 57 6f 72 6c 64 00
119
+ ```
120
+
121
+
122
+ ### TiMo Register Write Example
123
+
124
+ To write bytes to a TiMo register, use the `write-reg` command. I.e. to set the device name to `Hello World`:
125
+
126
+ ```bash
127
+ shuttle timo write-reg --addr 0x36 --data 48656c6c6f20576f726c6400 --port /dev/ttyUSB0
128
+ ```
129
+
130
+ - `--addr` specifies the register address (decimal or 0x-prefixed, 0-63)
131
+ - `--data` is a hex string of bytes to write (1-32 bytes)
132
+ - `--port` is your serial device
133
+
134
+ The command will print a summary table with the address, data written, and IRQ flags for each phase. If bit 7 of the IRQ flags is set, the sequence should be retried per the TiMo protocol.
135
+
136
+
137
+ ### TiMo DMX Read Example
138
+
139
+ Read the latest received DMX values from the window set up by the DMX_WINDOW register:
140
+
141
+ ```bash
142
+ shuttle timo read-dmx --length 12 --port /dev/ttyUSB0
143
+ ```
144
+
145
+ This will print a summary table with the length, data bytes (hex), and IRQ flags for each phase. If bit 7 of the IRQ flags is set, the sequence should be retried per the TiMo protocol.
146
+
147
+ - `--length` specifies the number of DMX bytes to read (1 - max_transfer_bytes)
148
+ - `--port` is your serial device
149
+
150
+
151
+ ### Using the Library from Python
152
+
153
+ Use the transport helpers for HIL tests with explicit request→response pairing:
154
+
155
+ ```python
156
+ from shuttle.serial_client import NDJSONSerialClient
157
+ from shuttle import timo
158
+
159
+ with NDJSONSerialClient("/dev/ttyUSB0") as client:
160
+ # Fire a TiMo read-reg using the async API
161
+ commands = timo.read_reg_sequence(address=0x05, length=2)
162
+ responses = [client.send_command("spi.xfer", cmd).result(timeout=1.0) for cmd in commands]
163
+ print("Command RX:", responses[0]["rx"])
164
+ print("Payload RX:", responses[1]["rx"])
165
+ ```
166
+
167
+ Legacy helpers (`spi_xfer`, `ping`, etc.) remain for simple sequential calls; prefer `send_command` when you need explicit request→response control.
168
+
169
+ #### Parsing registers with `REGISTER_MAP`
170
+
171
+ `REGISTER_MAP` in `shuttle.timo` documents the bit layout of TiMo registers. Example: read the `VERSION` register (0x10) and decode firmware/hardware versions.
172
+
173
+ ```python
174
+ from shuttle.serial_client import NDJSONSerialClient
175
+ from shuttle import timo
176
+
177
+ def read_register(client, reg_meta):
178
+ addr = reg_meta["address"]
179
+ length = reg_meta.get("length", 1)
180
+ seq = timo.read_reg_sequence(addr, length)
181
+ responses = [client.send_command("spi.xfer", cmd).result(timeout=1.0) for cmd in seq]
182
+ # The payload frame is in the second response's RX field
183
+ rx_payload = bytes.fromhex(responses[1]["rx"])
184
+ return rx_payload[1:] # skip IRQ flags byte
185
+
186
+ with NDJSONSerialClient("/dev/ttyUSB0") as client:
187
+ reg_meta = timo.REGISTER_MAP["VERSION"]
188
+ version_bytes = read_register(client, reg_meta)
189
+ fw_version = timo.slice_bits(version_bytes, *reg_meta["fields"]["FW_VERSION"]["bits"])
190
+ hw_version = timo.slice_bits(version_bytes, *reg_meta["fields"]["HW_VERSION"]["bits"])
191
+ print(f"VERSION: FW={fw_version:#x} HW={hw_version:#x}")
192
+ ```
193
+
194
+ Use the field metadata in `timo.REGISTER_MAP` to interpret other registers (e.g., check `REGISTER_MAP[0x01]["fields"]` for status flags).
195
+
196
+ More examples can be found in the [examples directory](examples/).
197
+
198
+ ### Async-style Command and Event Handling
199
+
200
+ `NDJSONSerialClient` now dispatches in a background reader thread and exposes futures so you can fan out work without changing the client for new ops:
201
+
202
+ - `send_command(op, params)` returns a `Future` that resolves to the matching response or raises on timeout/sequence gap. You can issue multiple commands back-to-back and wait later.
203
+ - `register_event_listener("ev.name")` returns a subscription whose `.next(timeout=…)` yields each event payload; multiple listeners can subscribe to the same event (e.g., IRQ and DMX streams).
204
+
205
+ Example HIL sketch:
206
+
207
+ ```python
208
+ client = NDJSONSerialClient(port, baudrate=DEFAULT_BAUD, timeout=DEFAULT_TIMEOUT)
209
+ irq_sub = client.register_event_listener("spi.irq")
210
+ cmd_future = client.send_command("timo.read-reg", {"address": 0x05, "length": 1})
211
+
212
+ # Wait for either side as your test requires
213
+ reg_resp = cmd_future.result(timeout=1)
214
+ irq_event = irq_sub.next(timeout=1)
215
+ ```
216
+
217
+ Events continue to emit until you close the subscription or client, so you can assert on multiple DMX frames or IRQ edges without recreating listeners.
218
+
219
+
220
+ ## Production Test SPI Commands
221
+
222
+ | Command | Description |
223
+ | --- | --- |
224
+ | `shuttle prodtest reset` | Reset GPIO pins, IRQ pin and Radio |
225
+ | `shuttle prodtest ping` | Send '+' and expect '-' to verify SPI link |
226
+ | `shuttle prodtest io-self-test` | Perform GPIO self-test on pins given as argument |
227
+ | `shuttle prodtest antenna` | Select antenna |
228
+ | `shuttle prodtest continuous-tx` | Continuous transmitter test |
229
+ | `shuttle prodtest hw-device-id` | Read the 8-byte HW Device ID |
230
+ | `shuttle prodtest serial-number [--value HEX]` | Read or write the 8-byte serial number |
231
+ | `shuttle prodtest config [--value HEX]` | Read or write the 5-byte config payload |
232
+ | `shuttle prodtest erase-nvmc HW_ID` | Erase NVMC if the provided 8-byte HW ID matches |
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lr-shuttle"
7
+ version = "0.2.4"
8
+ description = "CLI and Python client for host-side of json based serial communication with embedded device bridge."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [{name="Jonas Estberger", email="jonas.estberger@lumenradio.com"}]
13
+ dependencies = [
14
+ "rich>=13.7",
15
+ "pydantic>=2.8",
16
+ "typer>=0.12",
17
+ "pyserial>=3.5",
18
+ "esptool>=4.7",
19
+ ]
20
+ keywords = ["CLI"]
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ ]
26
+
27
+ [project.scripts]
28
+ # installs the `shuttle` executable
29
+ shuttle = "shuttle.cli:app"
30
+
31
+ [tool.setuptools]
32
+ package-dir = {"" = "src"}
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["build>=1.2.1", "twine>=5.1.1", "wheel", "pytest>=8.4.2", "black>=25.9.0", "pytest-html", "pytest-cov"]
39
+
40
+ [tool.setuptools.package-data]
41
+ "shuttle" = [
42
+ "firmware/esp32c5/*.bin",
43
+ "firmware/esp32c5/manifest.json",
44
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+