lr-shuttle 0.2.2__py3-none-any.whl → 0.2.12__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.2.dist-info → lr_shuttle-0.2.12.dist-info}/METADATA +19 -1
- lr_shuttle-0.2.12.dist-info/RECORD +18 -0
- {lr_shuttle-0.2.2.dist-info → lr_shuttle-0.2.12.dist-info}/WHEEL +1 -1
- shuttle/cli.py +676 -39
- shuttle/constants.py +0 -1
- shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- shuttle/prodtest.py +1 -3
- shuttle/serial_client.py +123 -25
- shuttle/timo.py +67 -7
- lr_shuttle-0.2.2.dist-info/RECORD +0 -18
- {lr_shuttle-0.2.2.dist-info → lr_shuttle-0.2.12.dist-info}/entry_points.txt +0 -0
- {lr_shuttle-0.2.2.dist-info → lr_shuttle-0.2.12.dist-info}/top_level.txt +0 -0
shuttle/constants.py
CHANGED
|
Binary file
|
shuttle/prodtest.py
CHANGED
|
@@ -62,9 +62,7 @@ def command(
|
|
|
62
62
|
) -> dict:
|
|
63
63
|
"""Build an NDJSON-ready spi.xfer payload for a prodtest command."""
|
|
64
64
|
|
|
65
|
-
return timo.command_payload(
|
|
66
|
-
_build_command_bytes(opcode, arguments), params=params
|
|
67
|
-
)
|
|
65
|
+
return timo.command_payload(_build_command_bytes(opcode, arguments), params=params)
|
|
68
66
|
|
|
69
67
|
|
|
70
68
|
def reset() -> dict:
|
shuttle/serial_client.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import json
|
|
8
8
|
import secrets
|
|
9
9
|
import threading
|
|
10
|
+
import time
|
|
10
11
|
from datetime import datetime, timezone
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Callable, Dict, List, Optional
|
|
@@ -17,6 +18,11 @@ import serial
|
|
|
17
18
|
from serial import SerialException
|
|
18
19
|
from .constants import DEFAULT_BAUD, DEFAULT_TIMEOUT
|
|
19
20
|
|
|
21
|
+
USB_CDC_PACKET_SIZE = 64
|
|
22
|
+
# Delay between USB CDC write chunks to avoid overwhelming the host USB stack with back-to-back packets.
|
|
23
|
+
# Tune for typical desktop OS USB stacks & current use cases; may need adjustment for other hosts.
|
|
24
|
+
USB_CDC_WRITE_DELAY_S = 0.000
|
|
25
|
+
|
|
20
26
|
|
|
21
27
|
class ShuttleSerialError(Exception):
|
|
22
28
|
"""Raised when serial transport encounters an unrecoverable error."""
|
|
@@ -228,10 +234,25 @@ class NDJSONSerialClient:
|
|
|
228
234
|
seq_tracker: Optional[SequenceTracker] = None,
|
|
229
235
|
):
|
|
230
236
|
try:
|
|
231
|
-
self._serial = serial.
|
|
237
|
+
self._serial = serial.serial_for_url(
|
|
238
|
+
url=port,
|
|
239
|
+
baudrate=baudrate,
|
|
240
|
+
timeout=timeout,
|
|
241
|
+
do_not_open=True,
|
|
242
|
+
)
|
|
243
|
+
except SerialException as exc: # pragma: no cover - hardware specific
|
|
244
|
+
raise ShuttleSerialError(f"Unable to initialize {port}: {exc}") from exc
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if getattr(self._serial, "open", None) is not None:
|
|
248
|
+
if not getattr(self._serial, "is_open", False):
|
|
249
|
+
self._serial.open()
|
|
232
250
|
except SerialException as exc: # pragma: no cover - hardware specific
|
|
233
251
|
raise ShuttleSerialError(f"Unable to open {port}: {exc}") from exc
|
|
234
|
-
|
|
252
|
+
except AttributeError:
|
|
253
|
+
# Test stubs without an open() method are already "connected"
|
|
254
|
+
pass
|
|
255
|
+
self._reset_input_buffer()
|
|
235
256
|
self._lock = threading.Lock()
|
|
236
257
|
self._pending: Dict[int, CommandFuture] = {}
|
|
237
258
|
self._response_backlog: Dict[int, Dict[str, Any]] = {}
|
|
@@ -241,6 +262,7 @@ class NDJSONSerialClient:
|
|
|
241
262
|
self._logger = logger
|
|
242
263
|
self._seq_tracker = seq_tracker
|
|
243
264
|
self._reader: Optional[threading.Thread] = None
|
|
265
|
+
self._event_callback: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
244
266
|
|
|
245
267
|
def __enter__(self) -> "NDJSONSerialClient":
|
|
246
268
|
return self
|
|
@@ -258,9 +280,41 @@ class NDJSONSerialClient:
|
|
|
258
280
|
if getattr(self, "_serial", None) and self._serial.is_open:
|
|
259
281
|
self._serial.close()
|
|
260
282
|
|
|
283
|
+
def _reset_input_buffer(self) -> None:
|
|
284
|
+
serial_obj = getattr(self, "_serial", None)
|
|
285
|
+
if serial_obj is None:
|
|
286
|
+
return
|
|
287
|
+
reset = getattr(serial_obj, "reset_input_buffer", None)
|
|
288
|
+
if reset is None:
|
|
289
|
+
return
|
|
290
|
+
try:
|
|
291
|
+
reset()
|
|
292
|
+
except SerialException:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
def flush_input_and_log(self):
|
|
296
|
+
"""Read and log all available data from the serial buffer before sending a command."""
|
|
297
|
+
if not hasattr(self, "_serial"):
|
|
298
|
+
return
|
|
299
|
+
try:
|
|
300
|
+
while True:
|
|
301
|
+
waiting = getattr(self._serial, "in_waiting", 0)
|
|
302
|
+
if not waiting:
|
|
303
|
+
break
|
|
304
|
+
data = self._serial.read(waiting)
|
|
305
|
+
if data:
|
|
306
|
+
self._log_serial("RX", data)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
finally:
|
|
310
|
+
self._reset_input_buffer()
|
|
311
|
+
|
|
261
312
|
def send_command(self, op: str, params: Dict[str, Any]) -> CommandFuture:
|
|
262
313
|
"""Send a command without blocking, returning a future for the response."""
|
|
263
314
|
|
|
315
|
+
# Flush and log any unread data before sending a command
|
|
316
|
+
self.flush_input_and_log()
|
|
317
|
+
|
|
264
318
|
cmd_id = self._next_cmd_id()
|
|
265
319
|
message: Dict[str, Any] = {"type": "cmd", "id": cmd_id, "op": op}
|
|
266
320
|
message.update(params)
|
|
@@ -304,6 +358,13 @@ class NDJSONSerialClient:
|
|
|
304
358
|
self._ensure_reader_started()
|
|
305
359
|
return listener
|
|
306
360
|
|
|
361
|
+
def set_event_callback(
|
|
362
|
+
self, callback: Optional[Callable[[Dict[str, Any]], None]]
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Register a callback for every device event, regardless of listeners."""
|
|
365
|
+
|
|
366
|
+
self._event_callback = callback
|
|
367
|
+
|
|
307
368
|
def spi_xfer(
|
|
308
369
|
self, *, tx: str, n: Optional[int] = None, **overrides: Any
|
|
309
370
|
) -> Dict[str, Any]:
|
|
@@ -345,6 +406,12 @@ class NDJSONSerialClient:
|
|
|
345
406
|
payload["uart"] = uart
|
|
346
407
|
return self._command("uart.cfg", payload)
|
|
347
408
|
|
|
409
|
+
def wifi_cfg(self, wifi: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
410
|
+
payload: Dict[str, Any] = {}
|
|
411
|
+
if wifi:
|
|
412
|
+
payload["wifi"] = wifi
|
|
413
|
+
return self._command("wifi.cfg", payload)
|
|
414
|
+
|
|
348
415
|
def uart_tx(self, data: str, port: Optional[int] = None) -> Dict[str, Any]:
|
|
349
416
|
payload: Dict[str, Any] = {"data": data}
|
|
350
417
|
if port is not None:
|
|
@@ -405,33 +472,53 @@ class NDJSONSerialClient:
|
|
|
405
472
|
def _write(self, message: Dict[str, Any]) -> None:
|
|
406
473
|
serialized = json.dumps(message, separators=(",", ":"))
|
|
407
474
|
payload = serialized.encode("utf-8") + b"\n"
|
|
475
|
+
total_written = 0
|
|
408
476
|
with self._lock:
|
|
409
|
-
|
|
477
|
+
while total_written < len(payload):
|
|
478
|
+
# Throttling writes to avoid overwhelming the USB stack
|
|
479
|
+
chunk = payload[total_written : total_written + USB_CDC_PACKET_SIZE]
|
|
480
|
+
written = self._serial.write(chunk)
|
|
481
|
+
if written != len(chunk):
|
|
482
|
+
raise ShuttleSerialError(
|
|
483
|
+
f"Short write to serial port: wrote {written} of {len(chunk)} bytes"
|
|
484
|
+
)
|
|
485
|
+
total_written += written
|
|
486
|
+
if total_written < len(payload):
|
|
487
|
+
time.sleep(USB_CDC_WRITE_DELAY_S)
|
|
410
488
|
self._log_serial("TX", payload)
|
|
411
489
|
|
|
412
490
|
def _read(self) -> Optional[Dict[str, Any]]:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
491
|
+
while True:
|
|
492
|
+
try:
|
|
493
|
+
line = self._serial.readline()
|
|
494
|
+
except SerialException as exc: # pragma: no cover - hardware specific
|
|
495
|
+
raise ShuttleSerialError(f"Serial read failed: {exc}") from exc
|
|
496
|
+
if not line:
|
|
497
|
+
return None
|
|
498
|
+
self._log_serial("RX", line)
|
|
499
|
+
stripped = line.strip()
|
|
500
|
+
if not stripped:
|
|
501
|
+
return None
|
|
502
|
+
try:
|
|
503
|
+
decoded = stripped.decode("utf-8")
|
|
504
|
+
except UnicodeDecodeError as exc:
|
|
505
|
+
self._reset_input_buffer()
|
|
506
|
+
raise ShuttleSerialError(f"Invalid UTF-8 from device: {exc}") from exc
|
|
507
|
+
trimmed = decoded.lstrip()
|
|
508
|
+
if not trimmed:
|
|
509
|
+
continue
|
|
510
|
+
if trimmed[0] not in ("{", "["):
|
|
511
|
+
self._reset_input_buffer()
|
|
512
|
+
continue
|
|
513
|
+
try:
|
|
514
|
+
message = json.loads(decoded)
|
|
515
|
+
except json.JSONDecodeError as exc:
|
|
516
|
+
self._reset_input_buffer()
|
|
517
|
+
raise ShuttleSerialError(
|
|
518
|
+
f"Invalid JSON from device: {decoded} ({exc})"
|
|
519
|
+
) from exc
|
|
520
|
+
self._record_sequence(message)
|
|
521
|
+
return message
|
|
435
522
|
|
|
436
523
|
def _dispatch(self, message: Dict[str, Any]) -> None:
|
|
437
524
|
mtype = message.get("type")
|
|
@@ -450,6 +537,7 @@ class NDJSONSerialClient:
|
|
|
450
537
|
ev_name = message.get("ev")
|
|
451
538
|
if not isinstance(ev_name, str):
|
|
452
539
|
raise ShuttleSerialError("Device event missing ev field")
|
|
540
|
+
self._emit_event_callback(message)
|
|
453
541
|
with self._lock:
|
|
454
542
|
listeners = list(self._event_listeners.get(ev_name, []))
|
|
455
543
|
for listener in listeners:
|
|
@@ -491,3 +579,13 @@ class NDJSONSerialClient:
|
|
|
491
579
|
future.mark_exception(exc)
|
|
492
580
|
for listener in listeners:
|
|
493
581
|
listener.fail(exc)
|
|
582
|
+
|
|
583
|
+
def _emit_event_callback(self, message: Dict[str, Any]) -> None:
|
|
584
|
+
callback = getattr(self, "_event_callback", None)
|
|
585
|
+
if callback is None:
|
|
586
|
+
return
|
|
587
|
+
try:
|
|
588
|
+
callback(message)
|
|
589
|
+
except Exception:
|
|
590
|
+
# Callback failures should not kill the serial reader loop
|
|
591
|
+
pass
|
shuttle/timo.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#! /usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Helpers for TiMo SPI command sequences."""
|
|
4
|
+
|
|
4
5
|
from __future__ import annotations
|
|
5
|
-
from typing import Any, Dict, Sequence
|
|
6
|
+
from typing import Any, BinaryIO, Dict, Iterator, Sequence, Tuple, Union
|
|
6
7
|
|
|
7
8
|
NOP_OPCODE = 0xFF
|
|
8
9
|
READ_REG_BASE = 0b00000000
|
|
@@ -18,8 +19,21 @@ READ_RDM_CMD = 0x83 # 1000 0011: Read received RDM request
|
|
|
18
19
|
WRITE_DMX_CMD = 0x91 # 1001 0001: Write DMX generation buffer
|
|
19
20
|
WRITE_RDM_CMD = 0x92 # 1001 0010: Write an RDM response
|
|
20
21
|
|
|
22
|
+
FW_BLOCK_CMD_1 = 0x8E
|
|
23
|
+
FW_BLOCK_CMD_2 = 0x8F
|
|
24
|
+
FW_BLOCK_CMD_1_SIZE = 254
|
|
25
|
+
FW_BLOCK_CMD_2_SIZE = 18
|
|
26
|
+
CCI_CHUNK_SIZE = FW_BLOCK_CMD_1_SIZE + FW_BLOCK_CMD_2_SIZE
|
|
27
|
+
CCI_HEADER_SIZE = 4
|
|
28
|
+
|
|
21
29
|
IRQ_FLAG_RESTART = 0x80 # Bit 7 signals the slave could not process the transfer
|
|
22
|
-
IRQ_WAIT_TIMEOUT_US =
|
|
30
|
+
IRQ_WAIT_TIMEOUT_US = 1_000_000 # Allow up to 1 second for IRQ trailing edge
|
|
31
|
+
|
|
32
|
+
WaitIrqOption = Union[Dict[str, Any], bool, None]
|
|
33
|
+
DEFAULT_WAIT_IRQ: Dict[str, Any] = {
|
|
34
|
+
"edge": "trailing",
|
|
35
|
+
"timeout_us": IRQ_WAIT_TIMEOUT_US,
|
|
36
|
+
}
|
|
23
37
|
|
|
24
38
|
# Selected register map and field descriptions from TiMo SPI interface docs
|
|
25
39
|
REGISTER_MAP: Dict[str, Dict[str, Any]] = {
|
|
@@ -39,6 +53,12 @@ REGISTER_MAP: Dict[str, Dict[str, Any]] = {
|
|
|
39
53
|
"access": "R/W",
|
|
40
54
|
"desc": "0=UART RDM, 1=SPI RDM",
|
|
41
55
|
},
|
|
56
|
+
"UPDATE_MODE": {
|
|
57
|
+
"bits": (5, 5),
|
|
58
|
+
"access": "W",
|
|
59
|
+
"reset": 0,
|
|
60
|
+
"desc": "1=driver update mode",
|
|
61
|
+
},
|
|
42
62
|
"RADIO_ENABLE": {
|
|
43
63
|
"bits": (7, 7),
|
|
44
64
|
"access": "R/W",
|
|
@@ -229,9 +249,9 @@ REGISTER_MAP: Dict[str, Dict[str, Any]] = {
|
|
|
229
249
|
},
|
|
230
250
|
"DEVICE_NAME": {
|
|
231
251
|
"address": 0x36,
|
|
232
|
-
"length":
|
|
252
|
+
"length": 32,
|
|
233
253
|
"fields": {
|
|
234
|
-
"DEVICE_NAME": {"bits": (0,
|
|
254
|
+
"DEVICE_NAME": {"bits": (0, 255), "access": "R/W"},
|
|
235
255
|
},
|
|
236
256
|
},
|
|
237
257
|
"UNIVERSE_NAME": {
|
|
@@ -420,7 +440,12 @@ def nop_sequence() -> Sequence[Dict[str, Any]]:
|
|
|
420
440
|
return [command_payload(nop_frame())]
|
|
421
441
|
|
|
422
442
|
|
|
423
|
-
def read_reg_sequence(
|
|
443
|
+
def read_reg_sequence(
|
|
444
|
+
address: int,
|
|
445
|
+
length: int,
|
|
446
|
+
*,
|
|
447
|
+
wait_irq: WaitIrqOption = None,
|
|
448
|
+
) -> Sequence[Dict[str, Any]]:
|
|
424
449
|
"""Build the SPI transfer sequence to read a TiMo register."""
|
|
425
450
|
|
|
426
451
|
if not 0 <= address <= READ_REG_ADDR_MASK:
|
|
@@ -429,10 +454,19 @@ def read_reg_sequence(address: int, length: int) -> Sequence[Dict[str, Any]]:
|
|
|
429
454
|
raise ValueError(f"length must be 1..{READ_REG_MAX_LEN}")
|
|
430
455
|
|
|
431
456
|
command_byte = READ_REG_BASE | (address & READ_REG_ADDR_MASK)
|
|
432
|
-
#
|
|
457
|
+
# Allow callers to override the default IRQ wait behavior for edge cases
|
|
458
|
+
if wait_irq is False:
|
|
459
|
+
resolved_wait = None
|
|
460
|
+
elif wait_irq is None or wait_irq is True:
|
|
461
|
+
resolved_wait = dict(DEFAULT_WAIT_IRQ)
|
|
462
|
+
else:
|
|
463
|
+
resolved_wait = wait_irq
|
|
464
|
+
command_params: Dict[str, Any] = {}
|
|
465
|
+
if resolved_wait is not None:
|
|
466
|
+
command_params["wait_irq"] = resolved_wait
|
|
433
467
|
command_transfer = command_payload(
|
|
434
468
|
bytes([command_byte]),
|
|
435
|
-
params=
|
|
469
|
+
params=command_params or None,
|
|
436
470
|
)
|
|
437
471
|
payload_transfer = command_payload(bytes([READ_REG_DUMMY] + [0x00] * length))
|
|
438
472
|
return [command_transfer, payload_transfer]
|
|
@@ -497,3 +531,29 @@ def requires_restart(irq_flags: int) -> bool:
|
|
|
497
531
|
"""Return True when bit 7 indicates the command must be retried."""
|
|
498
532
|
|
|
499
533
|
return bool(irq_flags & IRQ_FLAG_RESTART)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def read_cci_header(stream: BinaryIO) -> bytes:
|
|
537
|
+
"""Read and return the 4-byte TiMo CCI header."""
|
|
538
|
+
|
|
539
|
+
header = stream.read(CCI_HEADER_SIZE)
|
|
540
|
+
if len(header) != CCI_HEADER_SIZE:
|
|
541
|
+
raise ValueError("CCI firmware header must contain 4 bytes")
|
|
542
|
+
return header
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def iter_cci_chunks(stream: BinaryIO) -> Iterator[Tuple[int, bytes, bytes]]:
|
|
546
|
+
"""Yield successive FW block payloads (254+18 bytes) from a TiMo CCI image."""
|
|
547
|
+
|
|
548
|
+
block_index = 0
|
|
549
|
+
while True:
|
|
550
|
+
chunk_1 = stream.read(FW_BLOCK_CMD_1_SIZE)
|
|
551
|
+
if not chunk_1:
|
|
552
|
+
break
|
|
553
|
+
if len(chunk_1) != FW_BLOCK_CMD_1_SIZE:
|
|
554
|
+
raise ValueError("CCI firmware truncated in FW_BLOCK_CMD_1 payload")
|
|
555
|
+
chunk_2 = stream.read(FW_BLOCK_CMD_2_SIZE)
|
|
556
|
+
if len(chunk_2) != FW_BLOCK_CMD_2_SIZE:
|
|
557
|
+
raise ValueError("CCI firmware truncated in FW_BLOCK_CMD_2 payload")
|
|
558
|
+
block_index += 1
|
|
559
|
+
yield block_index, chunk_1, chunk_2
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
shuttle/cli.py,sha256=eMJEEmHG6Ar3vQsrQmCbPLY0pHUJg6gUyUDCaW0KNGM,87851
|
|
2
|
-
shuttle/constants.py,sha256=GUlAg3iEuPxLQ2mDCvlv5gVXHnlawl_YeLtaUSqsnPM,757
|
|
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
|
-
shuttle/timo.py,sha256=1K18y0QtDF2lw2Abeok9PgrpPUiCEbQdGQXOQik75Hw,16481
|
|
7
|
-
shuttle/firmware/__init__.py,sha256=KRXyz3xJ2GIB473tCHAky3DdPIQb78gX64Qn-uu55To,120
|
|
8
|
-
shuttle/firmware/esp32c5/__init__.py,sha256=U2xXnb80Wv8EJaJ6Tv9iev1mVlpoaEeqsNmjmEtxdFQ,41
|
|
9
|
-
shuttle/firmware/esp32c5/boot_app0.bin,sha256=-UxdeGp6j6sGrF0Q4zvzdxGmaXY23AN1WeoZzEEKF_A,8192
|
|
10
|
-
shuttle/firmware/esp32c5/devboard.ino.bin,sha256=vH-IBzbJCpSKgCLDl6snXqe2d5PFJJ3RDVQq41b3iKg,378880
|
|
11
|
-
shuttle/firmware/esp32c5/devboard.ino.bootloader.bin,sha256=LPU51SdUwebYemCZb5Pya-wGe7RC4UXrkRmBnsHePp0,20784
|
|
12
|
-
shuttle/firmware/esp32c5/devboard.ino.partitions.bin,sha256=FIuVnL_xw4qo4dXAup1hLFSZe5ReVqY_QSI-72UGU6E,3072
|
|
13
|
-
shuttle/firmware/esp32c5/manifest.json,sha256=CPOegfEK4PTtI6UPeohuUKkJNeg0t8aWntEczpoxYt4,480
|
|
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,,
|
|
File without changes
|
|
File without changes
|