From d5d921d3d493c0809e57c836bbe85e2e53928e28 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Thu, 26 Jun 2025 20:09:19 +0200 Subject: [PATCH] feat(core/prodtest): add nrf update command & script [no changelog] --- core/embed/projects/prodtest/README.md | 4 + .../projects/prodtest/cmd/prodtest_nrf.c | 108 ++++++++++++++++++ core/tools/nrf_update.py | 76 ++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 core/tools/nrf_update.py diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md index 7f901d8da5..27dfaba0eb 100644 --- a/core/embed/projects/prodtest/README.md +++ b/core/embed/projects/prodtest/README.md @@ -322,6 +322,10 @@ nrf-version OK 0.1.2.3 ``` +### nrf-update +Updates the nRF firmware. + + ### touch-draw Starts a drawing canvas, where user can draw with finger on pen. Canvas is exited by sending CTRL+C command. ``` diff --git a/core/embed/projects/prodtest/cmd/prodtest_nrf.c b/core/embed/projects/prodtest/cmd/prodtest_nrf.c index a4603c6f26..ccf5f884db 100644 --- a/core/embed/projects/prodtest/cmd/prodtest_nrf.c +++ b/core/embed/projects/prodtest/cmd/prodtest_nrf.c @@ -69,6 +69,107 @@ static void prodtest_nrf_version(cli_t* cli) { info.version_patch, info.version_tweak); } +#define NRF_UPDATE_MAXSIZE 0x50000 +__attribute__((section(".buf"))) static uint8_t nrf_buffer[NRF_UPDATE_MAXSIZE]; + +static void prodtest_nrf_update(cli_t* cli) { + static size_t nrf_len = 0; + static bool nrf_update_in_progress = false; + + if (cli_arg_count(cli) < 1) { + cli_error_arg_count(cli); + return; + } + + const char* phase = cli_arg(cli, "phase"); + if (phase == NULL) { + cli_error_arg(cli, "Expecting phase (begin|chunk|end)."); + return; + } + + if (0 == strcmp(phase, "begin")) { + if (cli_arg_count(cli) != 1) { + cli_error_arg_count(cli); + goto cleanup; + } + + // Reset our state + nrf_len = 0; + nrf_update_in_progress = true; + cli_trace(cli, "begin"); + cli_ok(cli, ""); + + } else if (0 == strcmp(phase, "chunk")) { + if (cli_arg_count(cli) < 2) { + cli_error_arg_count(cli); + goto cleanup; + } + + if (!nrf_update_in_progress) { + cli_error(cli, CLI_ERROR, "Update not started. Use 'begin' first."); + goto cleanup; + } + + // Receive next piece of the image + size_t chunk_len = 0; + uint8_t chunk_buf[512]; // tune this if you like + + if (!cli_arg_hex(cli, "hex-data", chunk_buf, sizeof(chunk_buf), + &chunk_len)) { + cli_error_arg(cli, "Expecting hex-data for chunk."); + goto cleanup; + } + + if (nrf_len + chunk_len > NRF_UPDATE_MAXSIZE) { + cli_error(cli, CLI_ERROR, "Buffer overflow (have %u, need %u)", + (unsigned)nrf_len, (unsigned)chunk_len); + goto cleanup; + } + + memcpy(&nrf_buffer[nrf_len], chunk_buf, chunk_len); + nrf_len += chunk_len; + + cli_ok(cli, "%u %u", (unsigned)chunk_len, (unsigned)nrf_len); + + } else if (0 == strcmp(phase, "end")) { + if (cli_arg_count(cli) != 1) { + cli_error_arg_count(cli); + goto cleanup; + } + + if (!nrf_update_in_progress) { + cli_error(cli, CLI_ERROR, "Update not started. Use 'begin' first."); + goto cleanup; + } + + if (nrf_len == 0) { + cli_error(cli, CLI_ERROR, "No data received"); + goto cleanup; + } + + // Hand off to your firmware update routine + if (!nrf_update(nrf_buffer, nrf_len)) { + cli_error(cli, CLI_ERROR, "Update failed"); + goto cleanup; + } + + // Clear state so next begin is required + nrf_len = 0; + nrf_update_in_progress = false; + + cli_trace(cli, "Update successful"); + cli_ok(cli, ""); + + } else { + cli_error(cli, CLI_ERROR, "Unknown phase '%s' (begin|chunk|end)", phase); + } + + return; + +cleanup: + nrf_update_in_progress = false; +} + // clang-format off PRODTEST_CLI_CMD( @@ -85,4 +186,11 @@ PRODTEST_CLI_CMD( .args = "" ); +PRODTEST_CLI_CMD( + .name = "nrf-update", + .func = prodtest_nrf_update, + .info = "Update nRF firmware", + .args = " " +); + #endif diff --git a/core/tools/nrf_update.py b/core/tools/nrf_update.py new file mode 100644 index 0000000000..79531fb6ad --- /dev/null +++ b/core/tools/nrf_update.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +import time +from pathlib import Path +import sys + +import click +import serial + +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 + ser = serial.Serial(port, timeout=2) + time.sleep(0.1) + ser.reset_input_buffer() + ser.reset_output_buffer() + + # 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="") +@click.argument("binary", metavar="", 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. + + e.g. /dev/ttyUSB0 or COM3 + 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()