Skip to content

Commit b19d632

Browse files
vtzmangelajo
authored andcommitted
fix: address PR review feedback
- Remove ELF-to-BIN conversion per maintainer feedback; reject .elf files with a clear error message pointing to objcopy - Remove objcopy_path config option (no longer needed) - Fix info() docstring to match implementation (only parses DETAILS.TXT) - Use os.open/os.fsync/os.close for proper fd-based sync - Remove dump() from README (unsupported API) - Add stlink-msd to docs toctree (fixes check-warnings CI) Made-with: Cursor
1 parent 96aa730 commit b19d632

5 files changed

Lines changed: 52 additions & 116 deletions

File tree

python/docs/source/reference/package-apis/drivers/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ Drivers for debugging and programming devices:
9595
UF2 flashing via BOOTSEL mass storage
9696
* **[Probe-RS](probe-rs.md)** (`jumpstarter-driver-probe-rs`) - Debugging probe
9797
support
98+
* **[ST-LINK MSD](stlink-msd.md)** (`jumpstarter-driver-stlink-msd`) - ST-LINK
99+
mass storage flasher for STM32 Nucleo and Discovery boards
98100
* **[Android Emulator](androidemulator.md)** (`jumpstarter-driver-androidemulator`) -
99101
Android emulator lifecycle management with ADB tunneling
100102
* **[QEMU](qemu.md)** (`jumpstarter-driver-qemu`) - QEMU virtualization platform
@@ -145,6 +147,7 @@ shell.md
145147
ssh.md
146148
snmp.md
147149
someip.md
150+
stlink-msd.md
148151
tasmota.md
149152
tmt.md
150153
tftp.md

python/packages/jumpstarter-driver-stlink-msd/README.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,21 @@ built-in mass storage interface handles all the flash programming.
1111

1212
| Format | Handling |
1313
|--------|----------|
14-
| `.elf` | Auto-converted to `.bin` via `objcopy`, then copied to the volume |
1514
| `.bin` | Copied directly to the ST-LINK volume |
1615
| `.hex` | Copied directly to the ST-LINK volume |
1716

18-
Using `.elf` allows the same build artifact for both virtual (Renode) and physical targets.
17+
ELF files must be converted externally before flashing:
18+
19+
```shell
20+
arm-none-eabi-objcopy -O binary zephyr.elf zephyr.bin
21+
```
1922

2023
## Installation
2124

2225
```shell
2326
pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-stlink-msd
2427
```
2528

26-
For `.elf` support, ensure one of these is on your `PATH`:
27-
28-
- `arm-none-eabi-objcopy` (ARM GCC toolchain)
29-
- `llvm-objcopy` (LLVM/Clang)
30-
- `arm-zephyr-eabi-objcopy` (Zephyr SDK)
31-
3229
## Configuration
3330

3431
```yaml
@@ -37,25 +34,21 @@ export:
3734
type: jumpstarter_driver_stlink_msd.driver.StlinkMsdFlasher
3835
config:
3936
# volume_name: "NOD_H755ZI" # optional: auto-detected if only one ST-LINK is connected
40-
# objcopy_path: "/path/to/objcopy" # optional: auto-detected from PATH
4137
```
4238

4339
| Parameter | Description | Type | Required | Default |
4440
|---------------|------------------------------------------------------------------|----------------|----------|--------------|
4541
| volume_name | Name of the mounted ST-LINK volume (e.g. `NOD_H755ZI`) | str \| None | no | auto-detect |
46-
| objcopy_path | Path to objcopy binary for ELF-to-BIN conversion | str \| None | no | auto-detect |
4742

4843
## Shell Commands
4944

5045
```shell
51-
j flasher flash firmware.elf # flash an ELF (auto-converts to .bin)
5246
j flasher flash firmware.bin # flash a raw binary
47+
j flasher flash firmware.hex # flash an Intel HEX file
5348
j flasher info # show ST-LINK volume details
5449
```
5550

5651
## API
5752

58-
- **`flash(source, target=None)`** — Flash firmware to the board. Accepts `.elf`, `.bin`, or `.hex`.
59-
ELF files are automatically converted to `.bin` using `objcopy`.
53+
- **`flash(source, target=None)`** — Flash firmware to the board. Accepts `.bin` or `.hex` files.
6054
- **`info()`** — Read `DETAILS.TXT` from the ST-LINK volume and return board metadata.
61-
- **`dump()`** — Not supported (ST-LINK mass storage is write-only).

python/packages/jumpstarter-driver-stlink-msd/jumpstarter_driver_stlink_msd/client.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,13 @@ class StlinkMsdFlasherClient(FlasherClient):
1212
"""Client interface for ST-LINK mass storage flasher.
1313
1414
Flashes STM32 boards by copying firmware to the ST-LINK's USB
15-
mass storage volume. Supports .elf, .bin, and .hex files.
15+
mass storage volume. Supports .bin and .hex files.
1616
"""
1717

1818
def info(self) -> dict[str, str]:
1919
"""Read board info from the ST-LINK volume."""
2020
return self.call("info")
2121

22-
def flash_file(self, filepath) -> str:
23-
"""Flash a local file to the STM32 board."""
24-
absolute = Path(filepath).resolve()
25-
return self.flash(absolute)
26-
2722
def cli(self):
2823
base = super().cli()
2924
base.commands.pop("flash", None)
@@ -42,7 +37,7 @@ def info():
4237
@click.argument("file", type=click.Path(exists=True))
4338
@click.option("--compression", type=click.Choice(Compression, case_sensitive=False))
4439
def flash(file, compression):
45-
"""Flash firmware (.elf, .bin, or .hex) to the STM32 board."""
40+
"""Flash firmware (.bin or .hex) to the STM32 board."""
4641
name = Path(file).name
4742
click.echo(f"Flashing {name}...")
4843
self.flash(file, target=name, compression=compression)

python/packages/jumpstarter-driver-stlink-msd/jumpstarter_driver_stlink_msd/driver.py

Lines changed: 27 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import os
66
import shutil
7-
import subprocess
87
import tempfile
98
from dataclasses import dataclass
109

@@ -15,61 +14,29 @@
1514
from .stlink_mount import find_all_stlink_mounts, find_stlink_mount
1615
from jumpstarter.driver import Driver, export
1716

17+
_SUPPORTED_EXTENSIONS = frozenset({".bin", ".hex"})
1818

19-
def _find_objcopy() -> str | None:
20-
"""Find an objcopy binary that can handle ARM ELF files."""
21-
for name in (
22-
"arm-none-eabi-objcopy",
23-
"llvm-objcopy",
24-
"arm-zephyr-eabi-objcopy",
25-
"objcopy",
26-
):
27-
path = shutil.which(name)
28-
if path:
29-
return path
30-
return None
31-
32-
33-
def _elf_to_bin(elf_path: str, bin_path: str, objcopy_path: str | None = None) -> None:
34-
"""Convert an ELF file to raw binary using objcopy."""
35-
objcopy = objcopy_path or _find_objcopy()
36-
if objcopy is None:
37-
raise FileNotFoundError(
38-
"No objcopy found. Install arm-none-eabi-gcc, llvm, or the Zephyr SDK."
39-
)
40-
41-
result = subprocess.run(
42-
[objcopy, "-O", "binary", elf_path, bin_path],
43-
capture_output=True,
44-
text=True,
45-
)
46-
if result.returncode != 0:
47-
raise RuntimeError(f"objcopy failed: {result.stderr.strip()}")
4819

49-
50-
def _detect_format(filename: str) -> str:
51-
"""Detect firmware format from filename extension."""
52-
lower = filename.lower()
53-
if lower.endswith(".elf"):
54-
return "elf"
55-
if lower.endswith(".bin"):
56-
return "bin"
57-
if lower.endswith(".hex"):
58-
return "hex"
59-
return "unknown"
20+
def _validate_firmware_name(name: str) -> None:
21+
"""Raise if the firmware file extension is not supported by ST-LINK MSD."""
22+
_, ext = os.path.splitext(name.lower())
23+
if ext not in _SUPPORTED_EXTENSIONS:
24+
raise ValueError(
25+
f"Unsupported firmware format '{ext or name}'. "
26+
f"ST-LINK mass storage only accepts .bin or .hex files. "
27+
f"Convert ELF files with: arm-none-eabi-objcopy -O binary input.elf output.bin"
28+
)
6029

6130

6231
@dataclass(kw_only=True)
6332
class StlinkMsdFlasher(FlasherInterface, Driver):
6433
"""Flash STM32 boards by copying firmware to the ST-LINK USB mass storage volume.
6534
66-
Supports .elf files (converted to .bin via objcopy), .bin files (copied directly),
67-
and .hex files (copied directly). This allows using the same .elf build artifact
68-
for both virtual targets (Renode) and physical targets (Nucleo/Discovery boards).
35+
Supports .bin and .hex files. ELF files must be converted to .bin
36+
externally (e.g. via ``arm-none-eabi-objcopy -O binary``).
6937
"""
7038

7139
volume_name: str | None = None
72-
objcopy_path: str | None = None
7340

7441
def __post_init__(self):
7542
if hasattr(super(), "__post_init__"):
@@ -101,7 +68,7 @@ def _resolve_mount(self) -> str:
10168

10269
@export
10370
def info(self) -> dict[str, str]:
104-
"""Read DETAILS.TXT and MBED.HTM from the ST-LINK volume."""
71+
"""Read DETAILS.TXT from the ST-LINK volume and return board metadata."""
10572
mount = self._resolve_mount()
10673
result: dict[str, str] = {}
10774

@@ -120,44 +87,34 @@ def info(self) -> dict[str, str]:
12087
async def flash(self, source, target: str | None = None):
12188
"""Flash firmware to the STM32 board via ST-LINK mass storage.
12289
123-
Accepts .elf (auto-converted to .bin), .bin, or .hex files.
124-
The ``target`` parameter is the destination filename on the volume
125-
(default: ``firmware.bin``).
90+
Accepts .bin or .hex files only. ELF files are rejected — convert
91+
them externally before flashing.
92+
93+
:param source: Firmware resource (local path or storage handle).
94+
:param target: Destination filename on the volume (default: ``firmware.bin``).
12695
"""
12796
mount = self._resolve_mount()
128-
src_name = target or "firmware.bin"
97+
dest_name = target or "firmware.bin"
98+
_validate_firmware_name(dest_name)
12999

130100
with tempfile.TemporaryDirectory() as tmpdir:
131-
tmp_path = os.path.join(tmpdir, "input_firmware")
101+
tmp_path = os.path.join(tmpdir, dest_name)
132102

133103
async with await FileWriteStream.from_path(tmp_path) as stream:
134104
async with self.resource(source) as res:
135105
async for chunk in res:
136106
await stream.send(chunk)
137107

138-
fmt = _detect_format(src_name)
139-
if fmt == "unknown":
140-
fmt = _detect_format(tmp_path)
141-
142-
if fmt == "elf":
143-
bin_path = os.path.join(tmpdir, "firmware.bin")
144-
self.logger.info("Converting ELF to BIN using objcopy")
145-
await to_thread.run_sync(
146-
lambda: _elf_to_bin(tmp_path, bin_path, self.objcopy_path)
147-
)
148-
flash_src = bin_path
149-
dest_name = src_name.rsplit(".", 1)[0] + ".bin" if src_name.lower().endswith(".elf") else src_name
150-
else:
151-
flash_src = tmp_path
152-
dest_name = src_name
153-
154108
dest_path = os.path.join(mount, dest_name)
155109
self.logger.info("Copying firmware to %s", dest_path)
156110

157111
def _copy() -> None:
158-
shutil.copy2(flash_src, dest_path)
159-
with open(dest_path, "rb") as f:
160-
os.fsync(f.fileno())
112+
shutil.copy2(tmp_path, dest_path)
113+
fd = os.open(dest_path, os.O_RDONLY)
114+
try:
115+
os.fsync(fd)
116+
finally:
117+
os.close(fd)
161118

162119
await to_thread.run_sync(_copy)
163120

python/packages/jumpstarter-driver-stlink-msd/jumpstarter_driver_stlink_msd/driver_test.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from unittest.mock import patch
2-
31
import pytest
42

3+
from .driver import _validate_firmware_name
54
from .stlink_mount import looks_like_stlink_volume
65

76

@@ -37,34 +36,23 @@ def test_looks_like_stlink_volume_not_dir(tmp_path):
3736
assert looks_like_stlink_volume(f) is False
3837

3938

40-
def test_detect_format():
41-
from .driver import _detect_format
42-
43-
assert _detect_format("firmware.elf") == "elf"
44-
assert _detect_format("firmware.bin") == "bin"
45-
assert _detect_format("firmware.hex") == "hex"
46-
assert _detect_format("firmware.unknown") == "unknown"
47-
assert _detect_format("FIRMWARE.ELF") == "elf"
39+
def test_validate_firmware_name_bin():
40+
_validate_firmware_name("firmware.bin")
4841

4942

50-
def test_elf_to_bin_missing_objcopy():
51-
from .driver import _elf_to_bin
43+
def test_validate_firmware_name_hex():
44+
_validate_firmware_name("firmware.hex")
5245

53-
with pytest.raises(FileNotFoundError, match="No objcopy found"):
54-
with patch("jumpstarter_driver_stlink_msd.driver._find_objcopy", return_value=None):
55-
_elf_to_bin("/fake/input.elf", "/fake/output.bin")
5646

47+
def test_validate_firmware_name_bin_uppercase():
48+
_validate_firmware_name("FIRMWARE.BIN")
5749

58-
def test_elf_to_bin_success(tmp_path):
59-
from .driver import _elf_to_bin
6050

61-
elf_file = tmp_path / "test.elf"
62-
bin_file = tmp_path / "test.bin"
63-
elf_file.write_bytes(b"\x7fELF" + b"\x00" * 100)
51+
def test_validate_firmware_name_elf_rejected():
52+
with pytest.raises(ValueError, match="Unsupported firmware format"):
53+
_validate_firmware_name("firmware.elf")
6454

65-
fake_objcopy = tmp_path / "fake_objcopy.sh"
66-
fake_objcopy.write_text('#!/bin/sh\ncp "$3" "$4"\n')
67-
fake_objcopy.chmod(0o755)
6855

69-
_elf_to_bin(str(elf_file), str(bin_file), objcopy_path=str(fake_objcopy))
70-
assert bin_file.exists()
56+
def test_validate_firmware_name_unknown_rejected():
57+
with pytest.raises(ValueError, match="Unsupported firmware format"):
58+
_validate_firmware_name("firmware.xyz")

0 commit comments

Comments
 (0)