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.

Files changed (36) hide show
  1. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/PKG-INFO +1 -1
  2. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/pyproject.toml +1 -1
  3. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/PKG-INFO +1 -1
  4. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/SOURCES.txt +1 -0
  5. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/cli.py +29 -0
  6. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.bin +0 -0
  7. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/serial_client.py +18 -0
  8. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli.py +2 -0
  9. lr_shuttle-0.2.9/tests/test_flash.py +269 -0
  10. lr_shuttle-0.2.9/tests/test_serial_client_flush.py +55 -0
  11. lr_shuttle-0.2.8/tests/test_flash.py +0 -92
  12. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/README.md +0 -0
  13. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/setup.cfg +0 -0
  14. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/dependency_links.txt +0 -0
  15. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/entry_points.txt +0 -0
  16. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/requires.txt +0 -0
  17. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/lr_shuttle.egg-info/top_level.txt +0 -0
  18. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/constants.py +0 -0
  19. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/__init__.py +0 -0
  20. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/__init__.py +0 -0
  21. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/boot_app0.bin +0 -0
  22. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.bootloader.bin +0 -0
  23. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/devboard.ino.partitions.bin +0 -0
  24. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/firmware/esp32c5/manifest.json +0 -0
  25. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/flash.py +0 -0
  26. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/prodtest.py +0 -0
  27. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/src/shuttle/timo.py +0 -0
  28. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_client.py +0 -0
  29. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_edge.py +0 -0
  30. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_seq.py +0 -0
  31. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_cli_utils.py +0 -0
  32. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_prodtest_edge.py +0 -0
  33. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_prodtest_helpers.py +0 -0
  34. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_serial_client.py +0 -0
  35. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_timo.py +0 -0
  36. {lr_shuttle-0.2.8 → lr_shuttle-0.2.9}/tests/test_timo_write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lr-shuttle"
7
- version = "0.2.8"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-shuttle
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -28,5 +28,6 @@ tests/test_flash.py
28
28
  tests/test_prodtest_edge.py
29
29
  tests/test_prodtest_helpers.py
30
30
  tests/test_serial_client.py
31
+ tests/test_serial_client_flush.py
31
32
  tests/test_timo.py
32
33
  tests/test_timo_write.py
@@ -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}[/]"
@@ -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)
@@ -3018,6 +3018,8 @@ def test_flash_command_invokes_helper(monkeypatch, recorded_console):
3018
3018
  "--board",
3019
3019
  "esp32c5",
3020
3020
  "--erase-first",
3021
+ "--sleep-after-flash",
3022
+ "0",
3021
3023
  ],
3022
3024
  )
3023
3025
 
@@ -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