lr-shuttle 0.2.8__tar.gz → 0.2.9__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.
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/PKG-INFO +1 -1
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/pyproject.toml +1 -1
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/PKG-INFO +1 -1
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/SOURCES.txt +1 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/cli.py +29 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/serial_client.py +18 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli.py +2 -0
- lr_shuttle-0.2.9/tests/test_flash.py +269 -0
- lr_shuttle-0.2.9/tests/test_serial_client_flush.py +55 -0
- lr_shuttle-0.2.8/tests/test_flash.py +0 -92
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/README.md +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/setup.cfg +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/dependency_links.txt +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/entry_points.txt +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/requires.txt +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/top_level.txt +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/constants.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/__init__.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/__init__.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/manifest.json +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/flash.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/prodtest.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/timo.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_client.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_edge.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_seq.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_utils.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_prodtest_edge.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_prodtest_helpers.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_serial_client.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_timo.py +0 -0
- {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_timo_write.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lr-shuttle"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "CLI and Python client for host-side of json based serial communication with embedded device bridge."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -767,6 +767,9 @@ def _execute_timo_sequence(
|
|
|
767
767
|
logger=logger,
|
|
768
768
|
seq_tracker=seq_tracker,
|
|
769
769
|
) as client:
|
|
770
|
+
# Drain any pending serial noise before issuing commands, to avoid
|
|
771
|
+
# mixing stale data into NDJSON responses.
|
|
772
|
+
client.flush_input_and_log()
|
|
770
773
|
for transfer in sequence:
|
|
771
774
|
response = client.spi_xfer(**transfer)
|
|
772
775
|
responses.append(response)
|
|
@@ -2634,6 +2637,7 @@ def spi_enable_command(
|
|
|
2634
2637
|
logger=resources.get("logger"),
|
|
2635
2638
|
seq_tracker=resources.get("seq_tracker"),
|
|
2636
2639
|
) as client:
|
|
2640
|
+
client.flush_input_and_log()
|
|
2637
2641
|
response = client.spi_enable()
|
|
2638
2642
|
except ShuttleSerialError as exc:
|
|
2639
2643
|
console.print(f"[red]{exc}[/]")
|
|
@@ -2668,6 +2672,7 @@ def spi_disable_command(
|
|
|
2668
2672
|
logger=resources.get("logger"),
|
|
2669
2673
|
seq_tracker=resources.get("seq_tracker"),
|
|
2670
2674
|
) as client:
|
|
2675
|
+
client.flush_input_and_log()
|
|
2671
2676
|
response = client.spi_disable()
|
|
2672
2677
|
except ShuttleSerialError as exc:
|
|
2673
2678
|
console.print(f"[red]{exc}[/]")
|
|
@@ -3138,6 +3143,7 @@ def power_command(
|
|
|
3138
3143
|
logger=resources.get("logger"),
|
|
3139
3144
|
seq_tracker=resources.get("seq_tracker"),
|
|
3140
3145
|
) as client:
|
|
3146
|
+
client.flush_input_and_log()
|
|
3141
3147
|
method = getattr(client, method_name)
|
|
3142
3148
|
response = method()
|
|
3143
3149
|
except ShuttleSerialError as exc:
|
|
@@ -3171,6 +3177,11 @@ def flash_command(
|
|
|
3171
3177
|
"--erase-first/--no-erase-first",
|
|
3172
3178
|
help="Erase the entire flash before writing",
|
|
3173
3179
|
),
|
|
3180
|
+
sleep_after_flash: float = typer.Option(
|
|
3181
|
+
1.25,
|
|
3182
|
+
"--sleep-after-flash",
|
|
3183
|
+
help="Seconds to wait after flashing to allow device reboot",
|
|
3184
|
+
),
|
|
3174
3185
|
):
|
|
3175
3186
|
"""Flash the bundled firmware image to the devboard."""
|
|
3176
3187
|
|
|
@@ -3194,6 +3205,24 @@ def flash_command(
|
|
|
3194
3205
|
console.print(f"[red]{exc}[/]")
|
|
3195
3206
|
raise typer.Exit(1) from exc
|
|
3196
3207
|
|
|
3208
|
+
if sleep_after_flash:
|
|
3209
|
+
time.sleep(
|
|
3210
|
+
sleep_after_flash
|
|
3211
|
+
) # Give the device a moment to reboot. 0.75s is sometimes too short.
|
|
3212
|
+
|
|
3213
|
+
# After flashing, drain/log any startup output from the device before further commands
|
|
3214
|
+
logger = ctx.obj["logger"] if ctx.obj and "logger" in ctx.obj else None
|
|
3215
|
+
try:
|
|
3216
|
+
from .serial_client import NDJSONSerialClient
|
|
3217
|
+
|
|
3218
|
+
# Use a short timeout just for draining
|
|
3219
|
+
with NDJSONSerialClient(
|
|
3220
|
+
resolved_port, baudrate=baudrate, timeout=0.5, logger=logger
|
|
3221
|
+
) as client:
|
|
3222
|
+
client.flush_input_and_log()
|
|
3223
|
+
except Exception:
|
|
3224
|
+
pass
|
|
3225
|
+
|
|
3197
3226
|
label = str(manifest.get("label", board))
|
|
3198
3227
|
console.print(
|
|
3199
3228
|
f"[green]Successfully flashed {label} ({board}) over {resolved_port}[/]"
|
|
Binary file
|
|
@@ -281,9 +281,27 @@ class NDJSONSerialClient:
|
|
|
281
281
|
if getattr(self, "_serial", None) and self._serial.is_open:
|
|
282
282
|
self._serial.close()
|
|
283
283
|
|
|
284
|
+
def flush_input_and_log(self):
|
|
285
|
+
"""Read and log all available data from the serial buffer before sending a command."""
|
|
286
|
+
if not hasattr(self, "_serial") or not getattr(self._serial, "in_waiting", 0):
|
|
287
|
+
return
|
|
288
|
+
try:
|
|
289
|
+
while True:
|
|
290
|
+
waiting = getattr(self._serial, "in_waiting", 0)
|
|
291
|
+
if not waiting:
|
|
292
|
+
break
|
|
293
|
+
data = self._serial.read(waiting)
|
|
294
|
+
if data:
|
|
295
|
+
self._log_serial("RX", data)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
284
299
|
def send_command(self, op: str, params: Dict[str, Any]) -> CommandFuture:
|
|
285
300
|
"""Send a command without blocking, returning a future for the response."""
|
|
286
301
|
|
|
302
|
+
# Flush and log any unread data before sending a command
|
|
303
|
+
self.flush_input_and_log()
|
|
304
|
+
|
|
287
305
|
cmd_id = self._next_cmd_id()
|
|
288
306
|
message: Dict[str, Any] = {"type": "cmd", "id": cmd_id, "op": op}
|
|
289
307
|
message.update(params)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#! /usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from shuttle import flash
|
|
10
|
+
from shuttle.firmware import DEFAULT_BOARD
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_list_available_boards_includes_default():
|
|
14
|
+
assert DEFAULT_BOARD in flash.list_available_boards()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_load_firmware_manifest_returns_segments():
|
|
18
|
+
manifest, package = flash.load_firmware_manifest("esp32c5")
|
|
19
|
+
assert manifest["segments"]
|
|
20
|
+
assert package.endswith("esp32c5")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_flash_firmware_invokes_esptool(monkeypatch):
|
|
24
|
+
calls = []
|
|
25
|
+
|
|
26
|
+
def fake_run(args):
|
|
27
|
+
calls.append(args)
|
|
28
|
+
|
|
29
|
+
monkeypatch.setattr(flash, "_run_esptool", fake_run)
|
|
30
|
+
|
|
31
|
+
manifest = flash.flash_firmware(
|
|
32
|
+
port="/dev/ttyUSB0",
|
|
33
|
+
baudrate=921600,
|
|
34
|
+
board="esp32c5",
|
|
35
|
+
erase_first=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert manifest["label"]
|
|
39
|
+
assert len(calls) == 2 # erase + write
|
|
40
|
+
erase_args, write_args = calls
|
|
41
|
+
assert erase_args[:4] == ["--chip", manifest["chip"], "--port", "/dev/ttyUSB0"]
|
|
42
|
+
assert "write-flash" in write_args
|
|
43
|
+
assert any("devboard.ino.bin" in arg for arg in write_args)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_flash_firmware_unknown_board():
|
|
47
|
+
with pytest.raises(flash.FirmwareFlashError):
|
|
48
|
+
flash.flash_firmware(port="/dev/null", baudrate=921600, board="does-not-exist")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_load_firmware_manifest_missing_file(monkeypatch, tmp_path):
|
|
52
|
+
board = "missing"
|
|
53
|
+
pkg_dir = tmp_path / "shuttle.firmware" / board
|
|
54
|
+
pkg_dir.mkdir(parents=True)
|
|
55
|
+
|
|
56
|
+
original_files = flash.resources.files
|
|
57
|
+
|
|
58
|
+
def fake_files(package):
|
|
59
|
+
if package == f"shuttle.firmware.{board}":
|
|
60
|
+
return pkg_dir
|
|
61
|
+
return original_files(package)
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr(flash.resources, "files", fake_files)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(flash.FirmwareFlashError, match="manifest missing"):
|
|
66
|
+
flash.load_firmware_manifest(board)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_load_firmware_manifest_corrupt(monkeypatch, tmp_path):
|
|
70
|
+
board = "corrupt"
|
|
71
|
+
pkg_dir = tmp_path / "shuttle.firmware" / board
|
|
72
|
+
pkg_dir.mkdir(parents=True)
|
|
73
|
+
(pkg_dir / "manifest.json").write_text("not-json", encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
original_files = flash.resources.files
|
|
76
|
+
|
|
77
|
+
def fake_files(package):
|
|
78
|
+
if package == f"shuttle.firmware.{board}":
|
|
79
|
+
return pkg_dir
|
|
80
|
+
return original_files(package)
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(flash.resources, "files", fake_files)
|
|
83
|
+
|
|
84
|
+
with pytest.raises(flash.FirmwareFlashError, match="Invalid manifest"):
|
|
85
|
+
flash.load_firmware_manifest(board)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_load_firmware_manifest_missing_segments(monkeypatch, tmp_path):
|
|
89
|
+
board = "no-segments"
|
|
90
|
+
pkg_dir = tmp_path / "shuttle.firmware" / board
|
|
91
|
+
pkg_dir.mkdir(parents=True)
|
|
92
|
+
(pkg_dir / "manifest.json").write_text("{}", encoding="utf-8")
|
|
93
|
+
|
|
94
|
+
original_files = flash.resources.files
|
|
95
|
+
|
|
96
|
+
def fake_files(package):
|
|
97
|
+
if package == f"shuttle.firmware.{board}":
|
|
98
|
+
return pkg_dir
|
|
99
|
+
return original_files(package)
|
|
100
|
+
|
|
101
|
+
monkeypatch.setattr(flash.resources, "files", fake_files)
|
|
102
|
+
|
|
103
|
+
with pytest.raises(flash.FirmwareFlashError, match="defines no segments"):
|
|
104
|
+
flash.load_firmware_manifest(board)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize("bad_segment", [{"file": "foo.bin"}, {"offset": "0x0"}])
|
|
108
|
+
def test_flash_firmware_segment_missing_fields(monkeypatch, bad_segment):
|
|
109
|
+
manifest = {
|
|
110
|
+
"chip": "esp32c5",
|
|
111
|
+
"segments": [bad_segment],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
monkeypatch.setattr(
|
|
115
|
+
flash, "load_firmware_manifest", lambda _board: (manifest, "pkg")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
with pytest.raises(
|
|
119
|
+
flash.FirmwareFlashError,
|
|
120
|
+
match="Manifest segment entries require 'offset' and 'file'",
|
|
121
|
+
):
|
|
122
|
+
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600, board="esp32c5")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_flash_firmware_missing_artifact(monkeypatch):
|
|
126
|
+
manifest = {
|
|
127
|
+
"chip": "esp32c5",
|
|
128
|
+
"segments": [{"offset": "0x0", "file": "missing.bin"}],
|
|
129
|
+
}
|
|
130
|
+
calls = []
|
|
131
|
+
tmp_pkg = Path("/nonexistent/pkg")
|
|
132
|
+
|
|
133
|
+
monkeypatch.setattr(
|
|
134
|
+
flash, "load_firmware_manifest", lambda _board: (manifest, "pkg")
|
|
135
|
+
)
|
|
136
|
+
monkeypatch.setattr(flash.resources, "files", lambda _pkg: tmp_pkg)
|
|
137
|
+
monkeypatch.setattr(
|
|
138
|
+
flash.resources,
|
|
139
|
+
"as_file",
|
|
140
|
+
lambda _traversable: (_ for _ in ()).throw(FileNotFoundError()),
|
|
141
|
+
)
|
|
142
|
+
monkeypatch.setattr(flash, "_run_esptool", lambda args: calls.append(args))
|
|
143
|
+
|
|
144
|
+
with pytest.raises(
|
|
145
|
+
flash.FirmwareFlashError, match="Missing firmware artifact: missing.bin"
|
|
146
|
+
):
|
|
147
|
+
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600, board="esp32c5")
|
|
148
|
+
|
|
149
|
+
assert calls == []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_flash_firmware_adds_flash_options(monkeypatch, tmp_path):
|
|
153
|
+
artifact = tmp_path / "fw.bin"
|
|
154
|
+
artifact.write_bytes(b"fw")
|
|
155
|
+
manifest = {
|
|
156
|
+
"chip": "esp32c5",
|
|
157
|
+
"segments": [{"offset": "0x1000", "file": artifact.name}],
|
|
158
|
+
"flash-mode": "dout",
|
|
159
|
+
"flash-freq": "80m",
|
|
160
|
+
"flash-size": "2MB",
|
|
161
|
+
}
|
|
162
|
+
calls = []
|
|
163
|
+
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
flash, "load_firmware_manifest", lambda _board: (manifest, "pkg")
|
|
166
|
+
)
|
|
167
|
+
monkeypatch.setattr(flash.resources, "files", lambda _pkg: tmp_path)
|
|
168
|
+
|
|
169
|
+
@contextmanager
|
|
170
|
+
def fake_as_file(traversable):
|
|
171
|
+
yield traversable
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(flash.resources, "as_file", fake_as_file)
|
|
174
|
+
monkeypatch.setattr(flash, "_run_esptool", lambda args: calls.append(args))
|
|
175
|
+
|
|
176
|
+
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600, board="esp32c5")
|
|
177
|
+
|
|
178
|
+
assert len(calls) == 1
|
|
179
|
+
write_args = calls[0]
|
|
180
|
+
assert ["--flash-mode", "dout"] in [
|
|
181
|
+
write_args[i : i + 2] for i in range(len(write_args) - 1)
|
|
182
|
+
]
|
|
183
|
+
assert ["--flash-freq", "80m"] in [
|
|
184
|
+
write_args[i : i + 2] for i in range(len(write_args) - 1)
|
|
185
|
+
]
|
|
186
|
+
assert ["--flash-size", "2MB"] in [
|
|
187
|
+
write_args[i : i + 2] for i in range(len(write_args) - 1)
|
|
188
|
+
]
|
|
189
|
+
assert str(artifact) in write_args
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_flash_firmware_skips_missing_flash_options(monkeypatch, tmp_path):
|
|
193
|
+
artifact = tmp_path / "fw.bin"
|
|
194
|
+
artifact.write_bytes(b"fw")
|
|
195
|
+
manifest = {
|
|
196
|
+
"chip": "esp32c5",
|
|
197
|
+
"segments": [{"offset": "0x1000", "file": artifact.name}],
|
|
198
|
+
# no flash-mode/freq/size keys -> exercise falsy branch
|
|
199
|
+
}
|
|
200
|
+
calls = []
|
|
201
|
+
|
|
202
|
+
monkeypatch.setattr(
|
|
203
|
+
flash, "load_firmware_manifest", lambda _board: (manifest, "pkg")
|
|
204
|
+
)
|
|
205
|
+
monkeypatch.setattr(flash.resources, "files", lambda _pkg: tmp_path)
|
|
206
|
+
|
|
207
|
+
@contextmanager
|
|
208
|
+
def fake_as_file(traversable):
|
|
209
|
+
yield traversable
|
|
210
|
+
|
|
211
|
+
monkeypatch.setattr(flash.resources, "as_file", fake_as_file)
|
|
212
|
+
monkeypatch.setattr(flash, "_run_esptool", lambda args: calls.append(args))
|
|
213
|
+
|
|
214
|
+
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600, board="esp32c5")
|
|
215
|
+
|
|
216
|
+
assert len(calls) == 1
|
|
217
|
+
write_args = calls[0]
|
|
218
|
+
assert all(
|
|
219
|
+
flag not in write_args
|
|
220
|
+
for flag in ("--flash-mode", "--flash-freq", "--flash-size")
|
|
221
|
+
)
|
|
222
|
+
assert str(artifact) in write_args
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_flash_firmware_handles_no_compress(monkeypatch):
|
|
226
|
+
calls = []
|
|
227
|
+
|
|
228
|
+
def fake_run(args):
|
|
229
|
+
calls.append(args)
|
|
230
|
+
|
|
231
|
+
manifest, package = flash.load_firmware_manifest("esp32c5")
|
|
232
|
+
custom_manifest = dict(manifest)
|
|
233
|
+
custom_manifest["compress"] = False
|
|
234
|
+
|
|
235
|
+
monkeypatch.setattr(flash, "_run_esptool", fake_run)
|
|
236
|
+
monkeypatch.setattr(
|
|
237
|
+
flash,
|
|
238
|
+
"load_firmware_manifest",
|
|
239
|
+
lambda _board: (custom_manifest, package),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600)
|
|
243
|
+
|
|
244
|
+
assert len(calls) == 1
|
|
245
|
+
assert "--no-compress" in calls[0]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_run_esptool_success_with_exit_zero(monkeypatch):
|
|
249
|
+
recorded = []
|
|
250
|
+
|
|
251
|
+
def fake_main(args):
|
|
252
|
+
recorded.append(list(args))
|
|
253
|
+
raise SystemExit(0)
|
|
254
|
+
|
|
255
|
+
monkeypatch.setattr(flash.esptool, "main", fake_main)
|
|
256
|
+
|
|
257
|
+
flash._run_esptool(["ping"])
|
|
258
|
+
|
|
259
|
+
assert recorded[0] == ["ping"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_run_esptool_failure(monkeypatch):
|
|
263
|
+
def fake_main(_args):
|
|
264
|
+
raise SystemExit(2)
|
|
265
|
+
|
|
266
|
+
monkeypatch.setattr(flash.esptool, "main", fake_main)
|
|
267
|
+
|
|
268
|
+
with pytest.raises(flash.FirmwareFlashError):
|
|
269
|
+
flash._run_esptool(["bad"])
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import pytest
|
|
3
|
+
import shuttle.serial_client as serial_client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DummySerial:
|
|
7
|
+
def __init__(self, data=b"", fail=False):
|
|
8
|
+
self._data = data
|
|
9
|
+
self._fail = fail
|
|
10
|
+
self.read_calls = 0
|
|
11
|
+
self.in_waiting = len(data)
|
|
12
|
+
self.is_open = True
|
|
13
|
+
|
|
14
|
+
def read(self, n):
|
|
15
|
+
self.read_calls += 1
|
|
16
|
+
if self._fail:
|
|
17
|
+
raise Exception("fail")
|
|
18
|
+
d, self._data = self._data[:n], self._data[n:]
|
|
19
|
+
self.in_waiting = len(self._data)
|
|
20
|
+
return d
|
|
21
|
+
|
|
22
|
+
def close(self):
|
|
23
|
+
self.is_open = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DummyLogger:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.logged = []
|
|
29
|
+
|
|
30
|
+
def log(self, direction, data):
|
|
31
|
+
self.logged.append((direction, data))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_flush_input_and_log_reads_and_logs(monkeypatch):
|
|
35
|
+
client = serial_client.NDJSONSerialClient.__new__(serial_client.NDJSONSerialClient)
|
|
36
|
+
logger = DummyLogger()
|
|
37
|
+
# Data in buffer
|
|
38
|
+
client._serial = DummySerial(b"abc123")
|
|
39
|
+
client._logger = logger
|
|
40
|
+
client.flush_input_and_log()
|
|
41
|
+
assert logger.logged == [("RX", b"abc123")]
|
|
42
|
+
# No data in buffer
|
|
43
|
+
client._serial = DummySerial(b"")
|
|
44
|
+
logger.logged.clear()
|
|
45
|
+
client.flush_input_and_log()
|
|
46
|
+
assert logger.logged == []
|
|
47
|
+
# Exception in read is handled
|
|
48
|
+
client._serial = DummySerial(b"abc", fail=True)
|
|
49
|
+
client.flush_input_and_log() # Should not raise
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_flush_input_and_log_no_serial():
|
|
53
|
+
client = serial_client.NDJSONSerialClient.__new__(serial_client.NDJSONSerialClient)
|
|
54
|
+
client._logger = DummyLogger()
|
|
55
|
+
client.flush_input_and_log() # Should not raise
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from shuttle import flash
|
|
7
|
-
from shuttle.firmware import DEFAULT_BOARD
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_list_available_boards_includes_default():
|
|
11
|
-
assert DEFAULT_BOARD in flash.list_available_boards()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_load_firmware_manifest_returns_segments():
|
|
15
|
-
manifest, package = flash.load_firmware_manifest("esp32c5")
|
|
16
|
-
assert manifest["segments"]
|
|
17
|
-
assert package.endswith("esp32c5")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_flash_firmware_invokes_esptool(monkeypatch):
|
|
21
|
-
calls = []
|
|
22
|
-
|
|
23
|
-
def fake_run(args):
|
|
24
|
-
calls.append(args)
|
|
25
|
-
|
|
26
|
-
monkeypatch.setattr(flash, "_run_esptool", fake_run)
|
|
27
|
-
|
|
28
|
-
manifest = flash.flash_firmware(
|
|
29
|
-
port="/dev/ttyUSB0",
|
|
30
|
-
baudrate=921600,
|
|
31
|
-
board="esp32c5",
|
|
32
|
-
erase_first=True,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
assert manifest["label"]
|
|
36
|
-
assert len(calls) == 2 # erase + write
|
|
37
|
-
erase_args, write_args = calls
|
|
38
|
-
assert erase_args[:4] == ["--chip", manifest["chip"], "--port", "/dev/ttyUSB0"]
|
|
39
|
-
assert "write-flash" in write_args
|
|
40
|
-
assert any("devboard.ino.bin" in arg for arg in write_args)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_flash_firmware_unknown_board():
|
|
44
|
-
with pytest.raises(flash.FirmwareFlashError):
|
|
45
|
-
flash.flash_firmware(port="/dev/null", baudrate=921600, board="does-not-exist")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_flash_firmware_handles_no_compress(monkeypatch):
|
|
49
|
-
calls = []
|
|
50
|
-
|
|
51
|
-
def fake_run(args):
|
|
52
|
-
calls.append(args)
|
|
53
|
-
|
|
54
|
-
manifest, package = flash.load_firmware_manifest("esp32c5")
|
|
55
|
-
custom_manifest = dict(manifest)
|
|
56
|
-
custom_manifest["compress"] = False
|
|
57
|
-
|
|
58
|
-
monkeypatch.setattr(flash, "_run_esptool", fake_run)
|
|
59
|
-
monkeypatch.setattr(
|
|
60
|
-
flash,
|
|
61
|
-
"load_firmware_manifest",
|
|
62
|
-
lambda _board: (custom_manifest, package),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
flash.flash_firmware(port="/dev/ttyUSB0", baudrate=921600)
|
|
66
|
-
|
|
67
|
-
assert len(calls) == 1
|
|
68
|
-
assert "--no-compress" in calls[0]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_run_esptool_success_with_exit_zero(monkeypatch):
|
|
72
|
-
recorded = []
|
|
73
|
-
|
|
74
|
-
def fake_main(args):
|
|
75
|
-
recorded.append(list(args))
|
|
76
|
-
raise SystemExit(0)
|
|
77
|
-
|
|
78
|
-
monkeypatch.setattr(flash.esptool, "main", fake_main)
|
|
79
|
-
|
|
80
|
-
flash._run_esptool(["ping"])
|
|
81
|
-
|
|
82
|
-
assert recorded[0] == ["ping"]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_run_esptool_failure(monkeypatch):
|
|
86
|
-
def fake_main(_args):
|
|
87
|
-
raise SystemExit(2)
|
|
88
|
-
|
|
89
|
-
monkeypatch.setattr(flash.esptool, "main", fake_main)
|
|
90
|
-
|
|
91
|
-
with pytest.raises(flash.FirmwareFlashError):
|
|
92
|
-
flash._run_esptool(["bad"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin
RENAMED
|
File without changes
|
{lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|