feat(core/tools): make nrf update script more universal and allow updating boardloader with it
What changed, and why it matters
This commit is a routine developer tooling refactor. It renames and generalizes an internal Python helper script used to upload firmware to Trezor hardware over a USB serial port, so it can handle both nRF chip updates and boardloader updates. There is no change to device firmware, no user-facing feature, and no security fix or vulnerability indicated in the diff or commit message.
No security action required. Treat as normal tooling refactor.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change deletes core/tools/nrf_update.py and adds core/tools/bin_update.py. The new script parameterizes the previously hardcoded ‘nrf-update’ CLI command, adding a second update type (‘boardloader-update’) selected via a new –type option. The serial protocol, chunking logic, and command structure remain identical. The script is a host-side development/maintenance utility and is not part of the embedded firmware or runtime attack surface.
Changed components
core/tools/bin_update.pycore/tools/nrf_update.pyInspect captured patch +114 / −91
diff --git a/core/tools/bin_update.py b/core/tools/bin_update.py
new file mode 100644
index 00000000..f8a58906
--- /dev/null
+++ b/core/tools/bin_update.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+import sys
+import time
+from pathlib import Path
+
+import click
+import serial
+
+UPDATE_TYPES = {
+ "boardloader": {
+ "command": "boardloader-update",
+ "success_message": "Boardloader update complete.",
+ },
+ "nrf": {"command": "nrf-update", "success_message": "nRF update complete."},
+}
+
+
+def exit_interactive_mode(ser):
+ ser.write(("." + "\r\n").encode())
+ time.sleep(0.1)
+ ser.reset_input_buffer()
+
+
+def send_cmd(ser, cmd, expect_ok=True):
+ """Send a line, read response, and abort on non-OK."""
+ ser.write((cmd + "\r\n").encode())
+ # Give the device a moment to process
+ time.sleep(0.05)
+ resp = ser.readline().decode(errors="ignore").strip()
+ # Skip comments or empty lines
+ while resp.startswith("#") or len(resp) == 0:
+ resp = ser.readline().decode(errors="ignore").strip()
+ click.echo(f"> {cmd}")
+ click.echo(f"< {resp}")
+ if expect_ok and not resp.startswith("OK"):
+ click.echo("Error from device, aborting.", err=True)
+ sys.exit(1)
+ return resp
+
+
+def upload_binary(port, bin_path, chunk_size, update_type):
+ if update_type not in UPDATE_TYPES:
+ raise ValueError(
+ f"Invalid update type. Must be one of: {', '.join(UPDATE_TYPES.keys())}"
+ )
+
+ # Read binary file
+ data = Path(bin_path).read_bytes()
+ total = len(data)
+ click.echo(f"Read {total} bytes from {bin_path!r}")
+
+ update_config = UPDATE_TYPES[update_type]
+ command = update_config["command"]
+
+ # Open USB-VCP port using context manager
+ with serial.Serial(port, timeout=2) as ser:
+ time.sleep(0.1)
+ ser.reset_input_buffer()
+ ser.reset_output_buffer()
+
+ exit_interactive_mode(ser)
+
+ # 1) Begin transfer
+ send_cmd(ser, f"{command} begin")
+
+ # 2) Stream chunks
+ offset = 0
+ while offset < total:
+ chunk = data[offset : offset + chunk_size]
+ hexstr = chunk.hex()
+ send_cmd(ser, f"{command} chunk {hexstr}")
+ offset += len(chunk)
+ pct = offset * 100 // total
+ click.echo(f" Uploaded {offset}/{total} bytes ({pct}%)")
+
+ # 3) Finish transfer
+ send_cmd(ser, f"{command} end")
+ click.echo(update_config["success_message"])
+
+
+@click.command(context_settings={"help_option_names": ["-h", "--help"]})
+@click.argument("port", metavar="<serial-port>")
+@click.argument(
+ "binary", metavar="<binary>", type=click.Path(exists=True, dir_okay=False)
+)
+@click.option(
+ "--chunk-size", "-c", default=512, show_default=True, help="Max bytes per chunk"
+)
+@click.option(
+ "--type",
+ "-t",
+ type=click.Choice(list(UPDATE_TYPES.keys()), case_sensitive=False),
+ required=True,
+ help="Type of update to perform",
+)
+def main(port, binary, chunk_size, type):
+ """
+ Upload a firmware image via USB-VCP CLI.
+
+ <serial-port> e.g. /dev/ttyUSB0 or COM3
+ <binary> path to the .bin file
+ """
+ try:
+ upload_binary(port, binary, chunk_size, type)
+ except serial.SerialException as e:
+ click.echo(f"Serial error: {e}", err=True)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ click.echo("Interrupted by user.", err=True)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/core/tools/nrf_update.py b/core/tools/nrf_update.py
deleted file mode 100644
index 90f723e1..00000000
--- a/core/tools/nrf_update.py
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/usr/bin/env python3
-import sys
-import time
-from pathlib import Path
-
-import click
-import serial
-
-
-def exit_interactive_mode(ser):
- ser.write(("." + "\r\n").encode())
- time.sleep(0.1)
- ser.reset_input_buffer()
-
-
-def send_cmd(ser, cmd, expect_ok=True):
- """Send a line, read response, and abort on non-OK."""
- ser.write((cmd + "\r\n").encode())
- # Give the device a moment to process
- time.sleep(0.05)
- resp = ser.readline().decode(errors="ignore").strip()
- # Skip comments or empty lines
- while resp.startswith("#") or len(resp) == 0:
- resp = ser.readline().decode(errors="ignore").strip()
- click.echo(f"> {cmd}")
- click.echo(f"< {resp}")
- if expect_ok and not resp.startswith("OK"):
- click.echo("Error from device, aborting.", err=True)
- sys.exit(1)
- return resp
-
-
-def upload_nrf(port, bin_path, chunk_size):
- # Read binary file
- data = Path(bin_path).read_bytes()
- total = len(data)
- click.echo(f"Read {total} bytes from {bin_path!r}")
-
- # Open USB-VCP port using context manager
- with serial.Serial(port, timeout=2) as ser:
- time.sleep(0.1)
- ser.reset_input_buffer()
- ser.reset_output_buffer()
-
- exit_interactive_mode(ser)
-
- # 1) Begin transfer
- send_cmd(ser, "nrf-update begin")
-
- # 2) Stream chunks
- offset = 0
- while offset < total:
- chunk = data[offset : offset + chunk_size]
- hexstr = chunk.hex()
- send_cmd(ser, f"nrf-update chunk {hexstr}")
- offset += len(chunk)
- pct = offset * 100 // total
- click.echo(f" Uploaded {offset}/{total} bytes ({pct}%)")
-
- # 3) Finish transfer
- send_cmd(ser, "nrf-update end")
- click.echo("nRF update complete.")
-
-
-@click.command(context_settings={"help_option_names": ["-h", "--help"]})
-@click.argument("port", metavar="<serial-port>")
-@click.argument(
- "binary", metavar="<nrf-binary>", type=click.Path(exists=True, dir_okay=False)
-)
-@click.option(
- "--chunk-size", "-c", default=512, show_default=True, help="Max bytes per chunk"
-)
-def main(port, binary, chunk_size):
- """
- Upload an nRF firmware image via USB-VCP CLI.
-
- <serial-port> e.g. /dev/ttyUSB0 or COM3
- <nrf-binary> path to the .bin file
- """
- try:
- upload_nrf(port, binary, chunk_size)
- except serial.SerialException as e:
- click.echo(f"Serial error: {e}", err=True)
- sys.exit(1)
- except KeyboardInterrupt:
- click.echo("Interrupted by user.", err=True)
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.