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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.2.8
3
+ Version: 0.2.10
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
@@ -1,18 +1,18 @@
1
- shuttle/cli.py,sha256=fv5HfJxfg8hnyO4CCA2vwG-OfTDSygnV6MrsZwv1yD8,108548
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=0srdCjKHW35LQFmZM_q-A9QEtSNadJp3WpqzIAHI1zo,19743
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=idrwg2JFHYjLK2KQVj4d_v31GHkqO8gEgr2TWkPHbbY,1101248
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.8.dist-info/METADATA,sha256=6yYpMjMjWpAOEMIJntX3XDiX1MveRgqJ3TnjheFZ_Ks,15574
15
- lr_shuttle-0.2.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
16
- lr_shuttle-0.2.8.dist-info/entry_points.txt,sha256=obqdFPgvQLB1_EWcnD9ch8HjQRlNVT_pdB_EidDRDco,44
17
- lr_shuttle-0.2.8.dist-info/top_level.txt,sha256=PtNxNQQdya-Xs8DYublNTBTa8c1TrtfEpQ0lUd_OeZY,8
18
- lr_shuttle-0.2.8.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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._serial.reset_input_buffer()
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
- try:
461
- line = self._serial.readline()
462
- except SerialException as exc: # pragma: no cover - hardware specific
463
- raise ShuttleSerialError(f"Serial read failed: {exc}") from exc
464
- if not line:
465
- return None
466
- self._log_serial("RX", line)
467
- stripped = line.strip()
468
- if not stripped:
469
- return None
470
- try:
471
- decoded = stripped.decode("utf-8")
472
- except UnicodeDecodeError as exc:
473
- raise ShuttleSerialError(f"Invalid UTF-8 from device: {exc}") from exc
474
- try:
475
- message = json.loads(decoded)
476
- except json.JSONDecodeError as exc:
477
- raise ShuttleSerialError(
478
- f"Invalid JSON from device: {decoded} ({exc})"
479
- ) from exc
480
- self._record_sequence(message)
481
- return message
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")