lr-shuttle 0.2.8__py3-none-any.whl → 0.2.10__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.2.8.dist-info → lr_shuttle-0.2.10.dist-info}/METADATA +1 -1
- {lr_shuttle-0.2.8.dist-info → lr_shuttle-0.2.10.dist-info}/RECORD +8 -8
- {lr_shuttle-0.2.8.dist-info → lr_shuttle-0.2.10.dist-info}/WHEEL +1 -1
- shuttle/cli.py +42 -0
- shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- shuttle/serial_client.py +64 -23
- {lr_shuttle-0.2.8.dist-info → lr_shuttle-0.2.10.dist-info}/entry_points.txt +0 -0
- {lr_shuttle-0.2.8.dist-info → lr_shuttle-0.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
shuttle/cli.py,sha256
|
|
1
|
+
shuttle/cli.py,sha256=-Lb5d3dwmJ0a2pFC9vrApQPx92x4Vw68OAaab6fawp0,110024
|
|
2
2
|
shuttle/constants.py,sha256=GUlAg3iEuPxLQ2mDCvlv5gVXHnlawl_YeLtaUSqsnPM,757
|
|
3
3
|
shuttle/flash.py,sha256=9ph23MHL40SjKZoL38Sbd3JbykGb-ECxvzBzCIjAues,4492
|
|
4
4
|
shuttle/prodtest.py,sha256=nI8k2OndhqsOv8BMtXwfcpGEdmHU7ywbIgMuW49EULU,8006
|
|
5
|
-
shuttle/serial_client.py,sha256=
|
|
5
|
+
shuttle/serial_client.py,sha256=8VPLP2nrbXIb34pWsnuXBNJ4K82bkdy3H9QWD7n0TVk,21147
|
|
6
6
|
shuttle/timo.py,sha256=SfWgiYUtPjSsUln5hgDLiYMYOt8zg1DLL5t07sgu2wY,18336
|
|
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=OIGcK7ZFonpK67YaYfQLzxCPzIikrj-KfgvgeJ5jGe8,1101248
|
|
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.10.dist-info/METADATA,sha256=iP9luZKiBfh8p52Ki419v-KB24CM54sbkjuerFim0hM,15575
|
|
15
|
+
lr_shuttle-0.2.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
+
lr_shuttle-0.2.10.dist-info/entry_points.txt,sha256=obqdFPgvQLB1_EWcnD9ch8HjQRlNVT_pdB_EidDRDco,44
|
|
17
|
+
lr_shuttle-0.2.10.dist-info/top_level.txt,sha256=PtNxNQQdya-Xs8DYublNTBTa8c1TrtfEpQ0lUd_OeZY,8
|
|
18
|
+
lr_shuttle-0.2.10.dist-info/RECORD,,
|
shuttle/cli.py
CHANGED
|
@@ -151,6 +151,19 @@ def _handle_sys_error_event(event: Dict[str, Any]) -> None:
|
|
|
151
151
|
console.print(f"[red]{' '.join(parts)}[/]")
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def _flush_client_input(client) -> None:
|
|
155
|
+
"""Best-effort drain any unread serial noise before issuing commands."""
|
|
156
|
+
|
|
157
|
+
flush = getattr(client, "flush_input_and_log", None)
|
|
158
|
+
if flush is None:
|
|
159
|
+
return
|
|
160
|
+
try:
|
|
161
|
+
flush()
|
|
162
|
+
except Exception:
|
|
163
|
+
# Flushing is opportunistic; failures should not leak into CLI flows.
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
|
|
154
167
|
@contextmanager
|
|
155
168
|
def _open_serial_client(
|
|
156
169
|
resolved_port: str,
|
|
@@ -767,6 +780,9 @@ def _execute_timo_sequence(
|
|
|
767
780
|
logger=logger,
|
|
768
781
|
seq_tracker=seq_tracker,
|
|
769
782
|
) as client:
|
|
783
|
+
# Drain any pending serial noise before issuing commands, to avoid
|
|
784
|
+
# mixing stale data into NDJSON responses.
|
|
785
|
+
_flush_client_input(client)
|
|
770
786
|
for transfer in sequence:
|
|
771
787
|
response = client.spi_xfer(**transfer)
|
|
772
788
|
responses.append(response)
|
|
@@ -2634,6 +2650,7 @@ def spi_enable_command(
|
|
|
2634
2650
|
logger=resources.get("logger"),
|
|
2635
2651
|
seq_tracker=resources.get("seq_tracker"),
|
|
2636
2652
|
) as client:
|
|
2653
|
+
_flush_client_input(client)
|
|
2637
2654
|
response = client.spi_enable()
|
|
2638
2655
|
except ShuttleSerialError as exc:
|
|
2639
2656
|
console.print(f"[red]{exc}[/]")
|
|
@@ -2668,6 +2685,7 @@ def spi_disable_command(
|
|
|
2668
2685
|
logger=resources.get("logger"),
|
|
2669
2686
|
seq_tracker=resources.get("seq_tracker"),
|
|
2670
2687
|
) as client:
|
|
2688
|
+
_flush_client_input(client)
|
|
2671
2689
|
response = client.spi_disable()
|
|
2672
2690
|
except ShuttleSerialError as exc:
|
|
2673
2691
|
console.print(f"[red]{exc}[/]")
|
|
@@ -3138,6 +3156,7 @@ def power_command(
|
|
|
3138
3156
|
logger=resources.get("logger"),
|
|
3139
3157
|
seq_tracker=resources.get("seq_tracker"),
|
|
3140
3158
|
) as client:
|
|
3159
|
+
_flush_client_input(client)
|
|
3141
3160
|
method = getattr(client, method_name)
|
|
3142
3161
|
response = method()
|
|
3143
3162
|
except ShuttleSerialError as exc:
|
|
@@ -3171,6 +3190,11 @@ def flash_command(
|
|
|
3171
3190
|
"--erase-first/--no-erase-first",
|
|
3172
3191
|
help="Erase the entire flash before writing",
|
|
3173
3192
|
),
|
|
3193
|
+
sleep_after_flash: float = typer.Option(
|
|
3194
|
+
1.25,
|
|
3195
|
+
"--sleep-after-flash",
|
|
3196
|
+
help="Seconds to wait after flashing to allow device reboot",
|
|
3197
|
+
),
|
|
3174
3198
|
):
|
|
3175
3199
|
"""Flash the bundled firmware image to the devboard."""
|
|
3176
3200
|
|
|
@@ -3194,6 +3218,24 @@ def flash_command(
|
|
|
3194
3218
|
console.print(f"[red]{exc}[/]")
|
|
3195
3219
|
raise typer.Exit(1) from exc
|
|
3196
3220
|
|
|
3221
|
+
if sleep_after_flash:
|
|
3222
|
+
time.sleep(
|
|
3223
|
+
sleep_after_flash
|
|
3224
|
+
) # Give the device a moment to reboot. 0.75s is sometimes too short.
|
|
3225
|
+
|
|
3226
|
+
# After flashing, drain/log any startup output from the device before further commands
|
|
3227
|
+
logger = ctx.obj["logger"] if ctx.obj and "logger" in ctx.obj else None
|
|
3228
|
+
try:
|
|
3229
|
+
from .serial_client import NDJSONSerialClient
|
|
3230
|
+
|
|
3231
|
+
# Use a short timeout just for draining
|
|
3232
|
+
with NDJSONSerialClient(
|
|
3233
|
+
resolved_port, baudrate=baudrate, timeout=0.5, logger=logger
|
|
3234
|
+
) as client:
|
|
3235
|
+
_flush_client_input(client)
|
|
3236
|
+
except Exception:
|
|
3237
|
+
pass
|
|
3238
|
+
|
|
3197
3239
|
label = str(manifest.get("label", board))
|
|
3198
3240
|
console.print(
|
|
3199
3241
|
f"[green]Successfully flashed {label} ({board}) over {resolved_port}[/]"
|
|
Binary file
|
shuttle/serial_client.py
CHANGED
|
@@ -253,7 +253,7 @@ class NDJSONSerialClient:
|
|
|
253
253
|
except AttributeError:
|
|
254
254
|
# Test stubs without an open() method are already "connected"
|
|
255
255
|
pass
|
|
256
|
-
self.
|
|
256
|
+
self._reset_input_buffer()
|
|
257
257
|
self._lock = threading.Lock()
|
|
258
258
|
self._pending: Dict[int, CommandFuture] = {}
|
|
259
259
|
self._response_backlog: Dict[int, Dict[str, Any]] = {}
|
|
@@ -281,9 +281,41 @@ class NDJSONSerialClient:
|
|
|
281
281
|
if getattr(self, "_serial", None) and self._serial.is_open:
|
|
282
282
|
self._serial.close()
|
|
283
283
|
|
|
284
|
+
def _reset_input_buffer(self) -> None:
|
|
285
|
+
serial_obj = getattr(self, "_serial", None)
|
|
286
|
+
if serial_obj is None:
|
|
287
|
+
return
|
|
288
|
+
reset = getattr(serial_obj, "reset_input_buffer", None)
|
|
289
|
+
if reset is None:
|
|
290
|
+
return
|
|
291
|
+
try:
|
|
292
|
+
reset()
|
|
293
|
+
except SerialException:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
def flush_input_and_log(self):
|
|
297
|
+
"""Read and log all available data from the serial buffer before sending a command."""
|
|
298
|
+
if not hasattr(self, "_serial"):
|
|
299
|
+
return
|
|
300
|
+
try:
|
|
301
|
+
while True:
|
|
302
|
+
waiting = getattr(self._serial, "in_waiting", 0)
|
|
303
|
+
if not waiting:
|
|
304
|
+
break
|
|
305
|
+
data = self._serial.read(waiting)
|
|
306
|
+
if data:
|
|
307
|
+
self._log_serial("RX", data)
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
finally:
|
|
311
|
+
self._reset_input_buffer()
|
|
312
|
+
|
|
284
313
|
def send_command(self, op: str, params: Dict[str, Any]) -> CommandFuture:
|
|
285
314
|
"""Send a command without blocking, returning a future for the response."""
|
|
286
315
|
|
|
316
|
+
# Flush and log any unread data before sending a command
|
|
317
|
+
self.flush_input_and_log()
|
|
318
|
+
|
|
287
319
|
cmd_id = self._next_cmd_id()
|
|
288
320
|
message: Dict[str, Any] = {"type": "cmd", "id": cmd_id, "op": op}
|
|
289
321
|
message.update(params)
|
|
@@ -457,28 +489,37 @@ class NDJSONSerialClient:
|
|
|
457
489
|
self._log_serial("TX", payload)
|
|
458
490
|
|
|
459
491
|
def _read(self) -> Optional[Dict[str, Any]]:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
while True:
|
|
493
|
+
try:
|
|
494
|
+
line = self._serial.readline()
|
|
495
|
+
except SerialException as exc: # pragma: no cover - hardware specific
|
|
496
|
+
raise ShuttleSerialError(f"Serial read failed: {exc}") from exc
|
|
497
|
+
if not line:
|
|
498
|
+
return None
|
|
499
|
+
self._log_serial("RX", line)
|
|
500
|
+
stripped = line.strip()
|
|
501
|
+
if not stripped:
|
|
502
|
+
return None
|
|
503
|
+
try:
|
|
504
|
+
decoded = stripped.decode("utf-8")
|
|
505
|
+
except UnicodeDecodeError as exc:
|
|
506
|
+
self._reset_input_buffer()
|
|
507
|
+
raise ShuttleSerialError(f"Invalid UTF-8 from device: {exc}") from exc
|
|
508
|
+
trimmed = decoded.lstrip()
|
|
509
|
+
if not trimmed:
|
|
510
|
+
continue
|
|
511
|
+
if trimmed[0] not in ("{", "["):
|
|
512
|
+
self._reset_input_buffer()
|
|
513
|
+
continue
|
|
514
|
+
try:
|
|
515
|
+
message = json.loads(decoded)
|
|
516
|
+
except json.JSONDecodeError as exc:
|
|
517
|
+
self._reset_input_buffer()
|
|
518
|
+
raise ShuttleSerialError(
|
|
519
|
+
f"Invalid JSON from device: {decoded} ({exc})"
|
|
520
|
+
) from exc
|
|
521
|
+
self._record_sequence(message)
|
|
522
|
+
return message
|
|
482
523
|
|
|
483
524
|
def _dispatch(self, message: Dict[str, Any]) -> None:
|
|
484
525
|
mtype = message.get("type")
|
|
File without changes
|
|
File without changes
|