diff --git a/Cargo.lock b/Cargo.lock index ae3c31c..42114ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -155,6 +155,20 @@ name = "bytemuck" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "cexpr" @@ -402,6 +416,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ihex" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2" + [[package]] name = "indexmap" version = "2.7.0" @@ -743,6 +763,7 @@ dependencies = [ "bytemuck", "derive_more", "dotenvy", + "ihex", "lazy_static", "log", "num_enum", diff --git a/README.md b/README.md index 8938a88..39b30e1 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,22 @@ The justfile needs `just` and `nu` to be installed. ## Status Working functionality: -- Discover ST-Link probes -- Connect to a target - Write hex/bin file to target - Save memory to file - Mass erase memory -- Upgrade BLE stack -- Reset -- Get general device information - -Functionality to be added: - Enable readout protection - Disable readout protection -- And much more.. PRs are more than welcome! 😊 +- Read memory + - As bytes, half words, words + - As struct (needs to implement bytemuck::Pod + bytemuck::Zeroable) +- Firmware Update Service (only STM32WB5x/35xx) + - Read installed versions of FUS and BLE stack + - Delete BLE stack firmware + - Upgrade BLE stack firmware + +Functionality to be added: +- There are still many functions in `STM32CubeProgrammer_API.chm` which are not yet implemented. + - PRs are more than welcome! 😊 ## Platform support - Windows: Tested diff --git a/justfile b/justfile index 397d52f..9fbe898 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,27 @@ # Nushell needs to be installeds set shell := ["nu", "-c"] +shebang := if os() == 'windows' { + 'nu.exe' +} else { + '/usr/bin/env nu' +} + # List all the recipes default: just -l +sample-env-file: + #!{{shebang}} + let content = 'STM32_CUBE_PROGRAMMER_DIR = "" + STM32_CUBE_PROGRAMMER_DOWNLOAD_HEX_PATH = "" + STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_PATH = "" + STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_START_ADDRESS = "" + STM32_CUBE_PROGRAMMER_BLE_STACK_PATH = "" + STM32_CUBE_PROGRAMMER_BLE_STACK_START_ADDRESS = ""' + + echo $content | save .env + # Run all tests or a specific test with a specific verbosity # The log level maps to the `log` crate log levels: trace, debug, info, warn, error test name="" log_level="trace": diff --git a/stm32cubeprogrammer-cli/Cargo.toml b/stm32cubeprogrammer-cli/Cargo.toml index 24bb5ec..fd2e29e 100644 --- a/stm32cubeprogrammer-cli/Cargo.toml +++ b/stm32cubeprogrammer-cli/Cargo.toml @@ -3,6 +3,11 @@ name = "stm32cubeprogrammer-cli" version = "0.1.0" edition = "2021" +authors = ["Christian Krenslehner"] +description = "CLI for the STM32CubeProgrammer API" +license = "MIT" +repository = "https://github.com/ckrenslehner/stm32cubeprogrammer-rs" + [dependencies] clap = { version = "4.5.22", features = ["derive"] } clap-verbosity-flag = "3.0.1" diff --git a/stm32cubeprogrammer-cli/src/display_handler.rs b/stm32cubeprogrammer-cli/src/display_handler.rs new file mode 100644 index 0000000..842db67 --- /dev/null +++ b/stm32cubeprogrammer-cli/src/display_handler.rs @@ -0,0 +1,90 @@ +use std::borrow::Cow; + +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use indicatif_log_bridge::LogWrapper; + +/// Display handler which wraps a progress bar and a logger +#[derive(Debug)] +pub struct DisplayHandler { + _multi: MultiProgress, + progress_bar: indicatif::ProgressBar, + message: Cow<'static, str>, +} + +impl DisplayHandler { + /// Create a new display handler which combines a progress bar and a logger + pub fn new(logger: env_logger::Logger) -> Self { + let multi = MultiProgress::new(); + + // Installs new global logger + LogWrapper::new(multi.clone(), logger).try_init().unwrap(); + + let progress_bar = multi.add(ProgressBar::new(0)); + progress_bar.set_style( + ProgressStyle::default_bar() + .template("{msg} - {percent}% - [{wide_bar:.cyan/blue}]") + .unwrap() + .progress_chars("#>-"), + ); + + Self { + _multi: multi, + progress_bar, + message: Cow::Owned(String::new()), + } + } + + pub fn set_message(&mut self, message: impl Into>) { + self.message = message.into(); + } +} + +/// Implement the display callback trait for the display handler +/// This allows showing progress and log messages from CubeProgrammer API +impl stm32cubeprogrammer::DisplayCallback for DisplayHandler { + fn init_progressbar(&self) { + self.progress_bar.set_message(self.message.clone()); + self.progress_bar.set_length(0); + self.progress_bar.set_position(0); + } + + fn log_message(&self, message_type: stm32cubeprogrammer::LogMessageType, message: &str) { + if message.is_empty() || message == "\n" || message == "\r\n" { + return; + } + + match message_type { + stm32cubeprogrammer::LogMessageType::Normal => log::info!("{}", message), + stm32cubeprogrammer::LogMessageType::Info => log::info!("{}", message), + stm32cubeprogrammer::LogMessageType::GreenInfo => log::info!("{}", message), + stm32cubeprogrammer::LogMessageType::GreenInfoNoPopup => log::info!("{}", message), + + stm32cubeprogrammer::LogMessageType::Warning => log::warn!("{}", message), + stm32cubeprogrammer::LogMessageType::WarningNoPopup => log::warn!("{}", message), + + stm32cubeprogrammer::LogMessageType::Error => log::error!("{}", message), + stm32cubeprogrammer::LogMessageType::ErrorNoPopup => log::error!("{}", message), + + stm32cubeprogrammer::LogMessageType::Verbosity1 => log::info!("{}", message), + stm32cubeprogrammer::LogMessageType::Verbosity2 => log::debug!("{}", message), + stm32cubeprogrammer::LogMessageType::Verbosity3 => log::trace!("{}", message), + + _ => {} + } + } + + fn update_progressbar(&self, current_number: u64, total_number: u64) { + if current_number == total_number { + self.progress_bar.finish(); + return; + } + + if let Some(current_length) = self.progress_bar.length() { + if current_length != total_number { + self.progress_bar.set_length(total_number); + } + } + + self.progress_bar.set_position(current_number); + } +} diff --git a/stm32cubeprogrammer-cli/src/main.rs b/stm32cubeprogrammer-cli/src/main.rs index 3c43232..0fa3baa 100644 --- a/stm32cubeprogrammer-cli/src/main.rs +++ b/stm32cubeprogrammer-cli/src/main.rs @@ -1,76 +1,149 @@ -use std::env; +mod display_handler; +use anyhow::Context; +use clap::{Parser, Subcommand, ValueEnum}; +use display_handler::DisplayHandler; +use std::{env, sync::Mutex}; +use stm32cubeprogrammer::{fus, probe}; -use anyhow::{Context, Ok}; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use indicatif_log_bridge::LogWrapper; -use stm32cubeprogrammer::CubeProgrammer; +#[derive(Debug, Clone)] +struct HexAddress(u32); -use clap::{Parser, Subcommand}; +impl HexAddress { + fn from_cli_input(hex: &str) -> Result { + let address = if hex.starts_with("0x") || hex.starts_with("0X") { + &hex[2..] + } else { + hex + }; + + u32::from_str_radix(address, 16) + .map_err(|x| format!("Failed to parse {} as u32 number: {}", address, x)) + .map(Self) + } +} + +/// Binary file information for downloading +#[derive(Debug, Clone)] +struct BinFileInfo { + /// The start address where the binary file should be downloaded + address: HexAddress, + /// The path to the binary file + path: std::path::PathBuf, +} + +impl std::fmt::Display for BinFileInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Display the address in hex format and the path + write!(f, "{:#x}, {:?}", self.address.0, self.path) + } +} + +fn bin_file_info_from_cli_input(s: &str) -> Result { + let parts: Vec<&str> = s.split(',').collect(); + + if parts.len() != 2 { + return Err(format!("Invalid format: {}", s)); + } + + let address = HexAddress::from_cli_input(parts[0])?; + let path = std::path::PathBuf::from(parts[1]); + + Ok(BinFileInfo { address, path }) +} + +fn version_from_cli_input(s: &str) -> Result { + fus::Version::try_from(s).map_err(|x| x.to_string()) +} #[derive(Parser)] #[command(version, about, long_about = None)] #[command(propagate_version = true)] struct Cli { + /// The verbosity level #[command(flatten)] verbosity: clap_verbosity_flag::Verbosity, + /// Serial of the st-link probe + #[arg(long)] + serial: Option, + #[command(subcommand)] command: Commands, } +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +enum ReadProtection { + Enable, + Disable, +} + #[derive(Subcommand)] enum Commands { - /// Download a hex file to a device - DownloadHex { - hex_file: std::path::PathBuf, + MassErase, + Reset, + ReatProtection { + enable: ReadProtection, + }, + DownloadBin { + /// Binary file info. Specified as `hex_address,path/to/file.bin` + /// + /// Example: `0x08000000,my_bin.bin` + #[arg(value_parser=bin_file_info_from_cli_input)] + bin_file: BinFileInfo, + + /// Whether to skip the erase operation before downloading the hex file #[arg(long, default_value_t = false)] - erase: bool, + skip_erase: bool, + + /// Whether to skip the verify operation after downloading the hex file #[arg(long, default_value_t = false)] skip_verify: bool, }, -} - -/// Display handler which wraps a progress bar and a logger -struct DisplayHandler { - progress_bar: indicatif::ProgressBar, -} - -impl DisplayHandler { - fn new(logger: env_logger::Logger) -> Self { - let multi = MultiProgress::new(); - - LogWrapper::new(multi.clone(), logger).try_init().unwrap(); - - let progress_bar = ProgressBar::new(0); - progress_bar.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") - .unwrap() - .progress_chars("#>-")); + DownloadHex { + /// Path to the hex file to download + hex_file: std::path::PathBuf, - let progress_bar = multi.add(progress_bar); + /// Whether to skip the erase operation before downloading the hex file + #[arg(long, default_value_t = false)] + skip_erase: bool, - Self { progress_bar } - } -} + /// Whether to skip the verify operation after downloading the hex file + #[arg(long, default_value_t = false)] + skip_verify: bool, + }, -impl stm32cubeprogrammer::DisplayCallback for DisplayHandler { - fn init_progressbar(&self) { - self.progress_bar.set_length(0); - self.progress_bar.set_position(0); - } + /// Rework command which can be used to setup a device + /// + /// This command can be used to download multiple binary files to a device + /// - Bootloader + /// - Application + /// - Configuration + /// + /// Additionally, an optional BLE stack can be downloaded (stm32wb) + /// + /// Optionally, the a mass erase can be performed before downloading the files and the readout protection can be set after finishing all download operations + Rework { + /// A list of binary files to download. Each file is specified as `hex_address,path/to/file.bin` + /// + /// Example: `0x08000000,my_bin.bin` + /// The files are downloaded in the order they are specified + #[arg(value_parser=bin_file_info_from_cli_input, long)] + bin_file: Vec, - fn log_message(&self, message_type: stm32cubeprogrammer::LogMessageType, message: &str) { - log::trace!("{}: {}", message_type, message); - } + /// Optional BLE stack binary file to download. Specified as `hex_address,path/to/file.bin` + /// + /// Example: `0x08000000,my_ble_stack.bin` + /// This command will delete the existing BLE stack before downloading the new one (first install == true) + #[arg(value_parser=bin_file_info_from_cli_input, long)] + ble_stack: Option, - fn update_progressbar(&self, current_number: u64, total_number: u64) { - if current_number == total_number { - self.progress_bar.finish(); - return; - } + #[arg(value_parser=version_from_cli_input, long)] + ble_stack_version: Option, - self.progress_bar.set_length(total_number); - self.progress_bar.set_position(current_number); - } + /// Whether to set the readout protection after downloading the files + #[arg(long = "set-rdp", default_value_t = false)] + set_readout_protection: bool, + }, } /// Sample CLI application for CubeProgrammer. Needs .env file with STM32_CUBE_PROGRAMMER_DIR set or STM32_CUBE_PROGRAMMER_DIR environment variable set @@ -79,6 +152,7 @@ fn main() -> Result<(), anyhow::Error> { let logger = env_logger::Builder::new() .filter_level(cli.verbosity.into()) + .filter_module("stm32cubeprogrammer::api_log", log::LevelFilter::Off) // Silence the hyper crate .build(); if dotenvy::dotenv().is_err() { @@ -88,38 +162,232 @@ fn main() -> Result<(), anyhow::Error> { let cube_programmer_dir = env::var("STM32_CUBE_PROGRAMMER_DIR") .with_context(|| "STM32_CUBE_PROGRAMMER_DIR environment variable not set")?; - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(cube_programmer_dir) - .display_callback(std::sync::Arc::new(DisplayHandler::new(logger))) + let display_handler = std::sync::Arc::new(Mutex::new(DisplayHandler::new(logger))); + + let api = stm32cubeprogrammer::CubeProgrammerApi::builder() + .cube_programmer_dir(&cube_programmer_dir) + .display_callback(display_handler.clone()) .build() - .with_context(|| "Failed to create CubeProgrammer instance")?; + .with_context(|| "Failed to create CubeProgrammer API instance")?; + + let probes = api.list_available_probes(); + + if probes.is_empty() { + return Err(anyhow::anyhow!("No ST-Link probes found")); + } + + let selected_probe = if let Some(serial) = &cli.serial { + probes + .iter() + .find(|probe| **probe.as_ref() == *serial) + .ok_or_else(|| { + anyhow::anyhow!("No ST-Link probe found with serial number: {}", serial) + })? + } else { + log::info!("No probe serial supplied, selecting first connected probe"); + &probes[0] + }; + + let programmer = api + .connect_to_target( + selected_probe, + &probe::Protocol::Swd, + &probe::ConnectionParameters::default(), + ) + .with_context(|| "Failed to connect to target")?; match &cli.command { + Commands::Rework { + bin_file, + ble_stack, + ble_stack_version, + set_readout_protection, + } => { + // TODO: Read FUS information and check if read protection is enabled + log::info!("## Rework command ##"); + log::info!("#"); + + for (index, file) in bin_file.iter().enumerate() { + log::info!("# Binary file {}: {:?}", index, file); + } + + log::info!("#"); + + if let Some(ble_stack) = ble_stack { + log::info!("# BLE stack: {:?}", ble_stack); + } + + log::info!("#"); + log::info!("# Set readout protection: {}", set_readout_protection); + log::info!("#"); + log::info!("##"); + + programmer + .disable_read_out_protection() + .with_context(|| "Failed to disable readout protection")?; + + if let Some(ble_stack) = ble_stack { + log::info!("Start FUS and check if BLE stack needs to be flashed"); + programmer.disconnect(); + + let programmer = api + .connect_to_target_fus(selected_probe, &probe::Protocol::Swd) + .with_context(|| "Cannot start FUS")?; + + let flash_ble_stack = if let Some(version) = ble_stack_version { + let target_version = programmer.fus_info().wireless_stack_version; + if target_version != *version { + log::info!( + "Version on target {} NOT EQUAL to given version {}. Flash stack", + target_version, + version + ); + + true + } else { + log::info!( + "Version on target {} EQUAL to given version {}. Skip flashing stack", + target_version, + version + ); + + false + } + } else { + log::info!("No BLE stack version given. Flash stack"); + true + }; + + if flash_ble_stack { + programmer + .update_ble_stack(&ble_stack.path, ble_stack.address.0, false, true, true) + .with_context(|| "Failed to update BLE stack")?; + } else { + programmer + .start_wireless_stack() + .with_context(|| "Failed to start wireless stack")?; + } + + programmer.disconnect(); + } + + let programmer = api + .connect_to_target( + selected_probe, + &probe::Protocol::Swd, + &probe::ConnectionParameters::default(), + ) + .with_context(|| "Failed to connect to target")?; + + // Download binary files + for file in bin_file { + log::info!("Downloading binary file: {:?}", file); + display_handler + .lock() + .unwrap() + .set_message("Download binary"); + + programmer + .download_bin_file(&file.path, file.address.0, false, true) + .with_context(|| "Failed to download binary file")?; + } + + // Set readout protection + if *set_readout_protection { + log::info!("Setting readout protection"); + display_handler.lock().unwrap().set_message("Set RDP"); + + programmer + .enable_read_out_protection() + .with_context(|| "Failed to set readout protection")?; + } + + // Reset device + log::info!("Resetting device"); + programmer + .reset_target(probe::ResetMode::Hardware) + .with_context(|| "Failed to reset device")?; + + Ok(()) + } Commands::DownloadHex { hex_file, - erase, + skip_erase, skip_verify, } => { - println!("## Download Hex File"); - println!("#"); - println!("# Path: {:?}", hex_file); - println!("# Skip erase: {}, Verify: {}", !erase, !skip_verify); - println!("##"); - - let probes = programmer.list_connected_st_link_probes(); - if probes.is_empty() { - log::warn!("No probes found"); - return Ok(()); - } + log::info!("## Download hex command ##"); + log::info!("#"); + log::info!("# Hex file: {:?}", hex_file); + log::info!("#"); + log::info!("##"); + + programmer + .download_hex_file(hex_file, *skip_erase, *skip_verify) + .with_context(|| "Failed to download hex file")?; - println!("Found {} probes", probes.len()); - println!("Using first probe: {}", probes[0]); + Ok(()) + } + Commands::DownloadBin { + bin_file, + skip_erase, + skip_verify, + } => { + log::info!("## Download hex command ##"); + log::info!("#"); + log::info!("# Bin file info: {}", bin_file); + log::info!("#"); + log::info!("##"); + + display_handler + .lock() + .unwrap() + .set_message("Download binary"); - let connected = programmer.connect_to_target(&probes[0])?; - connected - .download_hex_file(hex_file, !*erase, *skip_verify) + programmer + .download_bin_file( + &bin_file.path, + bin_file.address.0, + *skip_erase, + *skip_verify, + ) .with_context(|| "Failed to download hex file")?; + Ok(()) + } + Commands::MassErase => { + log::info!("## Mass erase command ##"); + + programmer + .mass_erase() + .with_context(|| "Failed to mass erase")?; + + Ok(()) + } + Commands::Reset => { + log::info!("## Reset command ##"); + + programmer + .reset_target(probe::ResetMode::Hardware) + .with_context(|| "Failed to reset device")?; + + Ok(()) + } + Commands::ReatProtection { enable } => { + log::info!("## Read protection command ##"); + + match enable { + ReadProtection::Enable => { + programmer + .enable_read_out_protection() + .with_context(|| "Failed to enable read protection")?; + } + ReadProtection::Disable => { + programmer + .disable_read_out_protection() + .with_context(|| "Failed to disable read protection")?; + } + } + Ok(()) } } diff --git a/stm32cubeprogrammer-cli/tests/basic.rs b/stm32cubeprogrammer-cli/tests/basic.rs index 7b90099..a24c6e3 100644 --- a/stm32cubeprogrammer-cli/tests/basic.rs +++ b/stm32cubeprogrammer-cli/tests/basic.rs @@ -1,12 +1,12 @@ -use std::env; - -use assert_cmd::Command; - -#[test_log::test] -fn write_hex() { - dotenvy::dotenv().unwrap(); - let hex_file = env::var("STM32_CUBE_PROGRAMMER_DOWNLOAD_HEX_PATH").unwrap(); - - let mut cmd = Command::cargo_bin("stm32cubeprogrammer-cli").unwrap(); - cmd.arg("download-hex").arg(hex_file).assert().success(); -} +use std::env; + +use assert_cmd::Command; + +#[test_log::test] +fn write_hex() { + dotenvy::dotenv().unwrap(); + let hex_file = env::var("STM32_CUBE_PROGRAMMER_DOWNLOAD_HEX_PATH").unwrap(); + + let mut cmd = Command::cargo_bin("stm32cubeprogrammer-cli").unwrap(); + cmd.arg("download-hex").arg(hex_file).assert().success(); +} diff --git a/stm32cubeprogrammer-sys/Cargo.toml b/stm32cubeprogrammer-sys/Cargo.toml index d7fadb5..1a49b9c 100644 --- a/stm32cubeprogrammer-sys/Cargo.toml +++ b/stm32cubeprogrammer-sys/Cargo.toml @@ -3,6 +3,11 @@ name = "stm32cubeprogrammer-sys" version = "0.1.0" edition = "2021" +authors = ["Christian Krenslehner"] +description = "Rust bindings for the STM32CubeProgrammer API" +license = "MIT" +repository = "https://github.com/ckrenslehner/stm32cubeprogrammer-rs" + [dependencies] libloading = "0.8.6" diff --git a/stm32cubeprogrammer-sys/src/lib.rs b/stm32cubeprogrammer-sys/src/lib.rs index 535c65a..d1cc290 100644 --- a/stm32cubeprogrammer-sys/src/lib.rs +++ b/stm32cubeprogrammer-sys/src/lib.rs @@ -1,4 +1,10 @@ -#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused, clippy::all)] +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + unused, + clippy::all +)] #[cfg(windows)] include!("bindings_windows.rs"); @@ -9,6 +15,12 @@ include!("bindings_unix.rs"); // Re-export libloading so that the user doesn't have to depend on it pub use libloading; +/// Standard base address of STM32 flash memory +pub const FLASH_BASE_ADDRESS: u32 = 0x08000000; + +/// Standard base address of STM32 RAM +pub const SRAM_BASE_ADDRESS: u32 = 0x20000000; + #[cfg(windows)] pub const PATH_API_LIBRARY_RELATIVE: &str = "api/lib/CubeProgrammer_API.dll"; diff --git a/stm32cubeprogrammer/Cargo.toml b/stm32cubeprogrammer/Cargo.toml index 93bb0ea..c50f82b 100644 --- a/stm32cubeprogrammer/Cargo.toml +++ b/stm32cubeprogrammer/Cargo.toml @@ -3,6 +3,24 @@ name = "stm32cubeprogrammer" version = "0.1.0" edition = "2021" +authors = ["Christian Krenslehner"] +description = "Rust bindings for the STM32CubeProgrammer API" +license = "MIT" +repository = "https://github.com/ckrenslehner/stm32cubeprogrammer-rs" + +[package.metadata] +# std::sync::OnceLock is available since 1.70 +msrv = "1.70.0" + +[features] +default = ["validations"] + +# Adds support for parsing ihex files +ihex = ["dep:ihex"] + +# Adds support for input validations (e.g. if a file is indeed a hex file, or it the mcu supports the FUS) +validations = ["ihex"] + [dependencies] stm32cubeprogrammer-sys = { path = "../stm32cubeprogrammer-sys" } derive_more = { version = "1", features = [ @@ -11,15 +29,18 @@ derive_more = { version = "1", features = [ "error", "display", "into", + "as_ref" ] } log.workspace = true widestring = "1.1.0" num_enum = "0.7.3" -bytemuck = "1.20.0" +bytemuck = { version = "1.20.0", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] } lazy_static = "1.5.0" bon = "3.2.0" +ihex = { optional = true, version = "3.0" } + [dev-dependencies] dotenvy.workspace = true test-log.workspace = true diff --git a/stm32cubeprogrammer/src/api_log.rs b/stm32cubeprogrammer/src/api_log.rs index dde1347..118657e 100644 --- a/stm32cubeprogrammer/src/api_log.rs +++ b/stm32cubeprogrammer/src/api_log.rs @@ -1,119 +1,121 @@ -//! Display callback functions for CubeProgrammer API logging - -use log::trace; -use num_enum::{IntoPrimitive, TryFromPrimitive}; - -use crate::utility; - -/// Log message type -#[derive(Debug, Default, Clone, Copy, IntoPrimitive, TryFromPrimitive, strum::Display)] -#[repr(i32)] -pub enum LogMessageType { - Normal, - Info, - GreenInfo, - Title, - Warning, - Error, - Verbosity1, - Verbosity2, - Verbosity3, - GreenInfoNoPopup, - WarningNoPopup, - ErrorNoPopup, - - #[default] - Unknown = -1, -} - -/// Verbosity level -#[derive(Debug, Default, Clone, Copy, IntoPrimitive, strum::Display)] -#[repr(i32)] -pub enum Verbosity { - Level0, - Level1, - Level2, - Level3, - - #[default] - Unknown = -1, -} - -pub(crate) unsafe extern "C" fn display_callback_init_progressbar() { - log::trace!("Init progress bar"); - - // Forward to display handler if there is one - if let Some(display_handler) = crate::display::get_display_callback_handler() { - display_handler.init_progressbar(); - } -} - -#[cfg(unix)] -pub(crate) unsafe extern "C" fn display_callback_log_message(level: i32, message: *const u32) { - display_callback_log_message_inner(level, &widestring::WideCString::from_ptr_str(message)); -} - -#[cfg(windows)] -pub(crate) unsafe extern "C" fn display_callback_log_message(level: i32, message: *const u16) { - display_callback_log_message_inner(level, &widestring::WideCString::from_ptr_str(message)); -} - -pub(crate) unsafe extern "C" fn display_callback_load_bar( - mut current_number: i32, - total_number: i32, -) { - if current_number > total_number { - current_number = total_number; - } - - // Forward to display handler if there is one - if let Some(display_handler) = crate::display::get_display_callback_handler() { - if current_number < 0 || total_number < 0 { - return; - } - - display_handler.update_progressbar(current_number as u64, total_number as u64); - } - - log::trace!("Update progress bar: {}/{}", current_number, total_number); -} - -fn display_callback_log_message_inner(level: i32, message: &widestring::WideCString) { - let level = LogMessageType::try_from(level).unwrap_or(LogMessageType::default()); - - let log_level = match level { - LogMessageType::Verbosity3 => log::Level::Trace, - LogMessageType::Verbosity2 => log::Level::Debug, - LogMessageType::Verbosity1 => log::Level::Info, - LogMessageType::Normal => log::Level::Info, - LogMessageType::Info => log::Level::Info, - LogMessageType::GreenInfo => log::Level::Info, - LogMessageType::Title => log::Level::Info, - LogMessageType::Warning => log::Level::Warn, - LogMessageType::Error => log::Level::Error, - LogMessageType::GreenInfoNoPopup => log::Level::Info, - LogMessageType::WarningNoPopup => log::Level::Warn, - LogMessageType::ErrorNoPopup => log::Level::Error, - LogMessageType::Unknown => log::Level::Error, - }; - - if log_level != log::Level::Trace { - trace!( - "API log - level: {:?}, message: {}", - level, - utility::wide_cstring_to_string(message) - ); - } - - // Forward to display handler if there is one - if let Some(display_handler) = crate::display::get_display_callback_handler() { - display_handler.log_message(level, &utility::wide_cstring_to_string(message)); - } - - log::log!( - log_level, - "{:?}, {}", - level, - utility::wide_cstring_to_string(message) - ); -} +//! Display callback functions for CubeProgrammer API logging + +use log::trace; +use num_enum::{FromPrimitive, IntoPrimitive}; + +use crate::utility; + +/// Log message type +#[derive(Debug, Clone, Copy, IntoPrimitive, FromPrimitive, strum::Display)] +#[repr(i32)] +pub enum LogMessageType { + Normal, + Info, + GreenInfo, + Title, + Warning, + Error, + Verbosity1, + Verbosity2, + Verbosity3, + GreenInfoNoPopup, + WarningNoPopup, + ErrorNoPopup, + + #[num_enum(catch_all)] + Unknown(i32), +} + +/// Verbosity level +#[derive(Debug, Clone, Copy, IntoPrimitive, FromPrimitive, strum::Display)] +#[repr(i32)] +pub enum Verbosity { + Level0, + Level1, + Level2, + Level3, + + #[num_enum(catch_all)] + Unknown(i32), +} + +pub(crate) unsafe extern "C" fn display_callback_init_progressbar() { + log::trace!("Init progress bar"); + + // Forward to display handler if there is one + if let Some(display_handler) = crate::display::get_display_callback_handler() { + display_handler.lock().unwrap().init_progressbar(); + } +} + +#[cfg(unix)] +pub(crate) unsafe extern "C" fn display_callback_log_message(level: i32, message: *const u32) { + display_callback_log_message_inner(level, &widestring::WideCString::from_ptr_str(message)); +} + +#[cfg(windows)] +pub(crate) unsafe extern "C" fn display_callback_log_message(level: i32, message: *const u16) { + display_callback_log_message_inner(level, &widestring::WideCString::from_ptr_str(message)); +} + +pub(crate) unsafe extern "C" fn display_callback_load_bar( + mut current_number: i32, + total_number: i32, +) { + if total_number == 0 { + return; + } + + if current_number > total_number { + current_number = total_number; + } + + // Forward to display handler if there is one + if let Some(display_handler) = crate::display::get_display_callback_handler() { + if current_number < 0 || total_number < 0 { + return; + } + + display_handler + .lock() + .unwrap() + .update_progressbar(current_number as u64, total_number as u64); + } + + log::trace!("Update progress bar: {}/{}", current_number, total_number); +} + +fn display_callback_log_message_inner(level: i32, message: &widestring::WideCString) { + let level = LogMessageType::from_primitive(level); + + let log_level = match level { + LogMessageType::Verbosity3 => log::Level::Trace, + LogMessageType::Verbosity2 => log::Level::Debug, + LogMessageType::Verbosity1 => log::Level::Info, + LogMessageType::Normal => log::Level::Info, + LogMessageType::Info => log::Level::Info, + LogMessageType::GreenInfo => log::Level::Info, + LogMessageType::Title => log::Level::Info, + LogMessageType::Warning => log::Level::Warn, + LogMessageType::Error => log::Level::Error, + LogMessageType::GreenInfoNoPopup => log::Level::Info, + LogMessageType::WarningNoPopup => log::Level::Warn, + LogMessageType::ErrorNoPopup => log::Level::Error, + LogMessageType::Unknown(_) => log::Level::Error, + }; + + let converted_message = utility::widestring_to_string(message); + + if let Ok(message) = converted_message { + trace!("API log - level: {:?}, message: {}", level, message); + + // Forward to display handler if there is one + if let Some(display_handler) = crate::display::get_display_callback_handler() { + display_handler.lock().unwrap().log_message(level, &message); + } + + log::log!(log_level, "{:?}, {}", level, message); + } else { + log::error!("Failed to convert message to string"); + } +} diff --git a/stm32cubeprogrammer/src/api_types.rs b/stm32cubeprogrammer/src/api_types.rs index 232e0e1..4305699 100644 --- a/stm32cubeprogrammer/src/api_types.rs +++ b/stm32cubeprogrammer/src/api_types.rs @@ -1,139 +1,231 @@ -use crate::error::{CubeProgrammerApiError, CubeProgrammerApiResult}; -use derive_more::derive::{From, Into}; -use num_enum::{IntoPrimitive, TryFromPrimitive}; +use crate::error::{CubeProgrammerError, CubeProgrammerResult}; +use derive_more::derive::{AsRef, Deref, Display, From, Into}; +use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive}; + +/// Negative error codes returned by the CubeProgrammer API +#[derive(Debug, Copy, Clone, strum::Display, IntoPrimitive, FromPrimitive)] +#[repr(i32)] +pub enum ErrorCode { + #[num_enum(catch_all)] + Unknown(i32), + + DeviceNotConnected = -1, + NoDeviceFound = -2, + ConnectionError = -3, + FileNotFound = -4, + UnsupportedOperation = -5, + UnsupportedInterface = -6, + InsufficientMemory = -7, + UnknownParameters = -8, + MemoryReadError = -9, + MemoryWriteError = -10, + MemoryEraseError = -11, + UnsupportedFileFormat = -12, + RefreshRequired = -13, + SecurityError = -14, + FrequencyError = -15, + RdpEnabledError = -16, + UnknownError = -17, +} -/// Return code which is mapped to an error if it is not 0 +/// Return code which is mapped to an error if it is not equal to SUCCESS +/// Sometimes success is 0, sometimes it is 1 #[derive(Debug, From, Into)] -pub struct ReturnCode(pub i32); +pub(crate) struct ReturnCode(pub i32); -impl ReturnCode { - pub fn check(&self) -> CubeProgrammerApiResult<()> { - if self.0 == 0 { +impl ReturnCode { + pub(crate) fn check(&self) -> CubeProgrammerResult<()> { + if self.0 == SUCCESS { Ok(()) } else { - Err(CubeProgrammerApiError::CommandReturnCode { - return_code: self.0, + Err(CubeProgrammerError::CommandReturnCode { + return_code: ErrorCode::from(self.0), }) } } } -#[derive(Debug, Default, Clone, Copy, IntoPrimitive, TryFromPrimitive, strum::Display)] -#[cfg_attr(windows, repr(i32))] -#[cfg_attr(unix, repr(u32))] -pub enum DebugPort { - Jtag, - Swd, - - #[default] - #[cfg(windows)] - Unknown = -1, - - #[default] - #[cfg(unix)] - Unknown = 0xFF, -} - -#[derive(Debug, Default, Clone, Copy, IntoPrimitive, TryFromPrimitive, strum::Display)] -#[cfg_attr(windows, repr(i32))] -#[cfg_attr(unix, repr(u32))] -pub enum ResetMode { - SoftwareReset, - HardwareReset, - CoreReset, - - #[default] - #[cfg(windows)] - Unknown = -1, - - #[default] - #[cfg(unix)] - Unknown = 0xFF, -} - -#[derive(Debug, Default, Clone, Copy, IntoPrimitive, TryFromPrimitive, strum::Display)] -#[cfg_attr(windows, repr(i32))] -#[cfg_attr(unix, repr(u32))] -pub enum ConnectionMode { - NormalMode, - HotplugMode, - UnderResetMode, - PowerDownMode, - HwResetPulseMode, - - #[default] - #[cfg(windows)] - Unknown = -1, - - #[default] - #[cfg(unix)] - Unknown = 0xFF, -} - -#[derive(Debug, Clone)] -pub struct StLink(pub(crate) stm32cubeprogrammer_sys::debugConnectParameters); - -impl StLink { - pub fn serial_number(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.serialNumber.as_ref()) +pub mod probe { + use super::*; + + #[derive( + Debug, Default, Clone, Copy, PartialEq, IntoPrimitive, TryFromPrimitive, strum::Display, + )] + #[cfg_attr(windows, repr(i32))] + #[cfg_attr(unix, repr(u32))] + pub enum Protocol { + Jtag, + #[default] + Swd, } - pub fn board(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.board.as_ref()) + #[derive( + Debug, Default, Clone, Copy, PartialEq, IntoPrimitive, TryFromPrimitive, strum::Display, + )] + #[cfg_attr(windows, repr(i32))] + #[cfg_attr(unix, repr(u32))] + pub enum ResetMode { + Software, + #[default] + Hardware, + Core, } - pub fn firmware_version(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.firmwareVersion.as_ref()) + #[derive( + Debug, Default, Clone, Copy, PartialEq, IntoPrimitive, TryFromPrimitive, strum::Display, + )] + #[cfg_attr(windows, repr(i32))] + #[cfg_attr(unix, repr(u32))] + pub enum ConnectionMode { + #[default] + Normal, + HotPlug, + UnderReset, + PowerDown, + HardwareResetPulse, } - pub fn debug_port(&self) -> DebugPort { - DebugPort::try_from(self.0.dbgPort).unwrap_or(DebugPort::default()) + /// Frequency of the programmer (Low, Medium, High or Custom) depending on the chosen DebugPort + /// - Low: Lowest available frequency + /// - Medium: Medium frequency + /// - High: Highest available frequency + /// + /// Custom frequency is in Hz + #[derive(Debug, Default, Clone, PartialEq)] + pub enum Frequency { + Low, + Medium, + High, + #[default] + Highest, + + Custom(u32), } - pub fn connection_mode(&self) -> ConnectionMode { - ConnectionMode::try_from(self.0.connectionMode).unwrap_or(ConnectionMode::default()) + #[derive(Debug, Clone, PartialEq)] + pub struct ConnectionParameters { + pub frequency: Frequency, + pub reset_mode: ResetMode, + pub connection_mode: ConnectionMode, } - pub fn reset_mode(&self) -> ResetMode { - ResetMode::try_from(self.0.resetMode).unwrap_or(ResetMode::default()) + impl Default for ConnectionParameters { + fn default() -> Self { + Self { + frequency: Frequency::Highest, + reset_mode: ResetMode::Hardware, + connection_mode: ConnectionMode::Normal, + } + } } - pub fn shared(&self) -> bool { - self.0.shared != 0 - } + #[derive(Debug, Clone, Deref, From, AsRef, Into, Hash, PartialEq, Eq, Display)] + pub struct Serial(String); + + #[derive(Debug, Clone, Deref)] + #[repr(transparent)] + pub(crate) struct Probe(pub(crate) stm32cubeprogrammer_sys::debugConnectParameters); + + impl Probe { + /// Create a modified version of connect parameters + pub(crate) fn new( + probe: &Probe, + protocol: &Protocol, + connect_parameters: &ConnectionParameters, + ) -> Self { + let mut debug_probe = probe.clone(); + + debug_probe.set_debug_protocol(*protocol); + debug_probe.set_reset_mode(connect_parameters.reset_mode); + debug_probe.set_connection_mode(connect_parameters.connection_mode); + debug_probe.set_shared(false); + + let frequency = match (&connect_parameters.frequency, debug_probe.debug_port()) { + (Frequency::Custom(custom_frequency), _) => Some(*custom_frequency), + (Frequency::Low, Protocol::Jtag) => debug_probe.0.freq.jtagFreq.get(3).copied(), + (Frequency::Low, Protocol::Swd) => debug_probe.0.freq.swdFreq.get(3).copied(), + (Frequency::Medium, Protocol::Jtag) => debug_probe.0.freq.jtagFreq.get(2).copied(), + (Frequency::Medium, Protocol::Swd) => debug_probe.0.freq.swdFreq.get(2).copied(), + (Frequency::High, Protocol::Jtag) => debug_probe.0.freq.jtagFreq.get(1).copied(), + (Frequency::High, Protocol::Swd) => debug_probe.0.freq.swdFreq.get(1).copied(), + (Frequency::Highest, Protocol::Jtag) => { + debug_probe.0.freq.jtagFreq.first().copied() + } + (Frequency::Highest, Protocol::Swd) => debug_probe.0.freq.swdFreq.first().copied(), + }; + + debug_assert!(frequency.is_some()); + debug_probe.0.frequency = frequency.expect("Cannot get frequency") as i32; + + debug_probe + } - pub fn set_debug_port(&mut self, debug_port: DebugPort) { - self.0.dbgPort = debug_port.into(); - } + pub(crate) fn serial_number(&self) -> &str { + crate::utility::c_char_slice_to_string(self.0.serialNumber.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') + } - pub fn set_connection_mode(&mut self, connection_mode: ConnectionMode) { - self.0.connectionMode = connection_mode.into(); - } + pub(crate) fn board(&self) -> &str { + crate::utility::c_char_slice_to_string(self.0.board.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') + } - pub fn set_reset_mode(&mut self, reset_mode: ResetMode) { - self.0.resetMode = reset_mode.into(); - } + pub(crate) fn firmware_version(&self) -> &str { + crate::utility::c_char_slice_to_string(self.0.firmwareVersion.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') + } - pub fn set_shared(&mut self, shared: bool) { - self.0.shared = if shared { 1 } else { 0 }; - } -} + pub(crate) fn debug_port(&self) -> Protocol { + Protocol::try_from(self.0.dbgPort).expect("Cannot convert debug port") + } -impl std::fmt::Display for StLink { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use crate::utility::cchar_to_null_terminated_string; + pub(crate) fn connection_mode(&self) -> ConnectionMode { + ConnectionMode::try_from(self.0.connectionMode).expect("Cannot convert connection mode") + } - write!( + pub(crate) fn reset_mode(&self) -> ResetMode { + ResetMode::try_from(self.0.resetMode).expect("Cannot convert reset mode") + } + + pub(crate) fn shared(&self) -> bool { + self.0.shared != 0 + } + + pub(crate) fn set_debug_protocol(&mut self, protocol: Protocol) { + self.0.dbgPort = protocol.into(); + } + + pub(crate) fn set_connection_mode(&mut self, connection_mode: ConnectionMode) { + self.0.connectionMode = connection_mode.into(); + } + + pub(crate) fn set_reset_mode(&mut self, reset_mode: ResetMode) { + self.0.resetMode = reset_mode.into(); + } + + pub(crate) fn set_shared(&mut self, shared: bool) { + self.0.shared = if shared { 1 } else { 0 }; + } + } + + impl std::fmt::Display for Probe { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( f, - "STLink (Serial: {}), Board: {}, Firmware version: {}, Debug port: {}, Connection mode: {}, Reset mode: {}, Shared: {}", - cchar_to_null_terminated_string(self.0.serialNumber.as_ref()), - cchar_to_null_terminated_string(self.0.board.as_ref()), - cchar_to_null_terminated_string(self.0.firmwareVersion.as_ref()), - DebugPort::try_from(self.0.dbgPort).unwrap_or(DebugPort::default()), - ConnectionMode::try_from(self.0.connectionMode).unwrap_or(ConnectionMode::default()), - ResetMode::try_from(self.0.resetMode).unwrap_or(ResetMode::default()), - self.0.shared + "STLink (Serial: {}), Board: {}, Firmware version: {}, Debug port: {}, Connection mode: {}, Reset mode: {}, Frequency: {} Hz, Shared: {}", + self.serial_number(), + self.board(), + self.firmware_version(), + self.debug_port(), + self.connection_mode(), + self.reset_mode(), + self.0.frequency, + self.shared() ) + } } } @@ -154,51 +246,124 @@ impl TargetInformation { } pub fn device_type(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.type_.as_ref()) + crate::utility::c_char_slice_to_string(self.0.type_.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn cpu(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.cpu.as_ref()) + crate::utility::c_char_slice_to_string(self.0.cpu.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn name(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.name.as_ref()) + crate::utility::c_char_slice_to_string(self.0.name.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn series(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.series.as_ref()) + crate::utility::c_char_slice_to_string(self.0.series.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn description(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.description.as_ref()) + crate::utility::c_char_slice_to_string(self.0.description.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn revision_id(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.revisionId.as_ref()) + crate::utility::c_char_slice_to_string(self.0.revisionId.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } pub fn board(&self) -> &str { - crate::utility::cchar_to_null_terminated_string(self.0.board.as_ref()) + crate::utility::c_char_slice_to_string(self.0.board.as_ref()) + .unwrap_or("Unknown") + .trim_matches('\0') } } impl std::fmt::Display for TargetInformation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use crate::utility::cchar_to_null_terminated_string; - write!( f, "Target information (Device ID: {}, Flash size: {}, Bootloader version: {}, Device type: {}, CPU: {}, Name: {}, Series: {}, Description: {}, Revision ID: {}, Board: {})", - self.0.deviceId, - self.0.flashSize, - self.0.bootloaderVersion, - cchar_to_null_terminated_string(self.0.type_.as_ref()), - cchar_to_null_terminated_string(self.0.cpu.as_ref()), - cchar_to_null_terminated_string(self.0.name.as_ref()), - cchar_to_null_terminated_string(self.0.series.as_ref()), - cchar_to_null_terminated_string(self.0.description.as_ref()), - cchar_to_null_terminated_string(self.0.revisionId.as_ref()), - cchar_to_null_terminated_string(self.0.board.as_ref()) + self.device_id(), + self.flash_size(), + self.bootloader_version(), + self.device_type(), + self.cpu(), + self.name(), + self.series(), + self.description(), + self.revision_id(), + self.board() ) } } + +pub mod fus { + use super::*; + + #[derive(Copy, Clone, Debug, PartialEq, Default)] + pub struct Version { + pub major: u8, + pub minor: u8, + pub sub: u8, + } + + impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.sub) + } + } + + impl TryFrom<&str> for Version { + type Error = CubeProgrammerError; + + fn try_from(value: &str) -> Result { + let parts = value.split('.'); + + if parts.clone().count() == 3 { + if let Ok(converted) = parts + .map(|x| x.parse::()) + .collect::, _>>() + { + return Ok(Version { + major: converted[0], + minor: converted[1], + sub: converted[2], + }); + } + } + + Err(CubeProgrammerError::TypeConversion { + message: format!("Cannot convert \"{}\" to a version. Expecting the following format \"u8.u8.u8\" e.g. \"1.2.3\"", value), + source: crate::error::TypeConversionError::VersionError + }) + } + } + + #[derive(Copy, Clone, Debug, Default)] + pub struct Information { + pub wireless_stack_version: Version, + pub fus_version: Version, + pub uid64: u64, + pub device_id: u16, + } + + impl std::fmt::Display for Information { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Wireless stack version: {}, FUS version: {}, UUID64: {:X}, Device ID: {:X}", + self.wireless_stack_version, self.fus_version, self.uid64, self.device_id + ) + } + } +} diff --git a/stm32cubeprogrammer/src/cube_programmer.rs b/stm32cubeprogrammer/src/cube_programmer.rs index 92ec7fc..8e965d6 100644 --- a/stm32cubeprogrammer/src/cube_programmer.rs +++ b/stm32cubeprogrammer/src/cube_programmer.rs @@ -1,70 +1,95 @@ -// TODO: Add trait for Logger and Progressbar -> CLI and TUI / GUI - -use std::sync::Arc; +use std::{ + cell::RefCell, + collections::HashMap, + rc::Rc, + sync::{Arc, Mutex}, +}; use crate::{ api_log, api_types, display, - error::{CubeProgrammerApiError, CubeProgrammerApiResult}, + error::{CubeProgrammerError, CubeProgrammerResult}, utility, }; -use derive_more::Into; -use log::debug; -use stm32cubeprogrammer_sys::{libloading, CubeProgrammer_API}; use bon::bon; +use derive_more::Into; +use log::{debug, error}; +use stm32cubeprogrammer_sys::SRAM_BASE_ADDRESS; -pub struct NotConnected; -pub struct Connected; +type ProbeRegistry = HashMap>; -pub struct CubeProgrammer { +/// Struct which holds the FFI API and helps with loading and setting up the CubeProgrammer API +pub struct CubeProgrammerApi { + /// API api: stm32cubeprogrammer_sys::CubeProgrammer_API, - _phantom: std::marker::PhantomData, + /// HashMap to store connected probes. The key is the serial number of the probe + probe_registry: Rc>, +} + +/// ConnectedCubeProgrammer +/// State transitions: +/// - ConnectedCubeProgrammer -> CubeProgrammer +#[derive(Clone)] +pub struct ConnectedCubeProgrammer<'a> { + api: &'a stm32cubeprogrammer_sys::CubeProgrammer_API, + probe_registry: Rc>, + probe: crate::probe::Probe, + general_information: api_types::TargetInformation, +} + +/// ConnectedFusCubeProgrammer. +/// State transitions: +/// - ConnectedFusCubeProgrammer -> CubeProgrammer +#[derive(Clone)] +pub struct ConnectedFusCubeProgrammer<'a> { + programmer: ConnectedCubeProgrammer<'a>, + fus_info: crate::fus::Information, } #[bon] -impl CubeProgrammer { - /// Create new instance of CubeProgrammer +impl CubeProgrammerApi { + /// Create new instance of CubeProgrammerApi + /// - Load the CubeProgrammer API library + /// - Set the verbosity level + /// - Set the display callback handler + /// - Set the loader path #[builder] pub fn new( - cube_programmer_dir: impl AsRef, + cube_programmer_dir: &impl AsRef, log_verbosity: Option, - display_callback: Option>, - ) -> Result { + display_callback: Option>>, + ) -> Result { use stm32cubeprogrammer_sys::{PATH_API_LIBRARY_RELATIVE, PATH_LOADER_DIR_RELATIVE}; let api_path = cube_programmer_dir .as_ref() .join(PATH_API_LIBRARY_RELATIVE) .canonicalize() - .expect("Failed to get canonical path"); + .map_err(CubeProgrammerError::FileIo)?; let loader_path = cube_programmer_dir .as_ref() .join(PATH_LOADER_DIR_RELATIVE) .canonicalize() - .expect("Failed to get canonical path"); + .map_err(CubeProgrammerError::FileIo)?; debug!("API path: {:?}", api_path); debug!("Loader path: {:?}", loader_path); - debug!("Log verbosity: {:?}", log_verbosity); + + let library = Self::load_library(&api_path).map_err(CubeProgrammerError::LibLoading)?; + + let api = unsafe { + stm32cubeprogrammer_sys::CubeProgrammer_API::from_library(library) + .map_err(CubeProgrammerError::LibLoading)? + }; if let Some(display_callback) = display_callback { debug!("Set display callback handler"); display::set_display_callback_handler(display_callback); } - let library = - Self::load_library(&api_path).map_err(CubeProgrammerApiError::LibLoadingError)?; - - let api = unsafe { - CubeProgrammer_API::from_library(library) - .map_err(CubeProgrammerApiError::LibLoadingError)? - }; - unsafe { - api.setLoadersPath(utility::path_to_cstring(loader_path).as_ptr()); - { let verbosity = log_verbosity.unwrap_or({ debug!("Use default verbosity level"); @@ -83,61 +108,209 @@ impl CubeProgrammer { }; api.setDisplayCallbacks(display_callbacks); + api.setLoadersPath(utility::path_to_cstring(loader_path)?.as_ptr()); } Ok(Self { api, - _phantom: std::marker::PhantomData::, - }) - } - - /// Connect to target via ST-Link probe - pub fn connect_to_target( - self, - st_link: &api_types::StLink, - ) -> CubeProgrammerApiResult> { - let connection_param = st_link.0; - api_types::ReturnCode::from(unsafe { self.api.connectStLink(connection_param) }).check()?; - - Ok(CubeProgrammer:: { - api: self.api, - _phantom: std::marker::PhantomData::, + probe_registry: Rc::new(RefCell::new(HashMap::new())), }) } - /// List all connected ST-Link probes - pub fn list_connected_st_link_probes(&self) -> Vec { + fn scan_for_probes(&self) { let mut debug_parameters = std::ptr::null_mut::(); let return_value = unsafe { self.api.getStLinkList(&mut debug_parameters, 0) }; if return_value < 0 || debug_parameters.is_null() { - return vec![]; + return; } - let slice = unsafe { std::slice::from_raw_parts(debug_parameters, return_value as _) }; + let slice = unsafe { + std::slice::from_raw_parts( + debug_parameters as *mut crate::probe::Probe, + return_value as _, + ) + }; - let st_links = slice - .iter() - .map(|debug_parameters| api_types::StLink(*debug_parameters)) - .collect(); + let mut connected_probes = self.probe_registry.borrow_mut(); + + // Delete all entries where the value is not None -> There is no active connection + connected_probes.retain(|_, value| value.is_none()); + + for probe in slice { + // Only insert if the key is not already present + connected_probes + .entry(probe.serial_number().to_string().into()) + .or_insert_with(|| Some(probe.clone())); + } // Free the memory allocated by the API unsafe { self.api.deleteInterfaceList(); } + } + + /// List available probes. Scans for probes and then returns the list of probes which are not already in use + pub fn list_available_probes(&self) -> Vec { + self.scan_for_probes(); + + let connected_probes = self.probe_registry.borrow(); - st_links + connected_probes + .values() + .filter_map(|probe| { + probe + .as_ref() + .map(|probe| probe.serial_number().to_string().into()) + }) + .collect() + } + + /// Connect to a target via a given probe + pub fn connect_to_target( + &self, + probe_serial_number: &crate::probe::Serial, + protocol: &crate::probe::Protocol, + connection_parameters: &crate::probe::ConnectionParameters, + ) -> CubeProgrammerResult { + let mut connected_probes = self.probe_registry.borrow_mut(); + + if let Some(probe) = connected_probes.get_mut(probe_serial_number) { + if let Some(inner) = probe.take() { + // Try to connect to the target with the probe + match api_types::ReturnCode::<0>::from(unsafe { + self.api.connectStLink(*crate::probe::Probe::new( + &inner, + protocol, + connection_parameters, + )) + }) + .check() + { + Ok(_) => { + // Try to get the general device information + let general_information = unsafe { self.api.getDeviceGeneralInf() }; + if general_information.is_null() { + // Reinsert the probe into the connected_probes HashMap + *probe = Some(inner); + + unsafe { self.api.disconnect() }; + + return Err(CubeProgrammerError::NullValue { + message: "Cannot get target device information".to_string(), + }); + } + + // We could connect and get the general information + let general_information = + api_types::TargetInformation(unsafe { *general_information }); + + Ok(ConnectedCubeProgrammer { + api: &self.api, + probe: inner, + probe_registry: Rc::clone(&self.probe_registry), + general_information, + }) + } + Err(e) => { + error!( + "Cannot connect to target via probe with serial number: {}", + probe_serial_number + ); + + // Reinsert the probe into the connected_probes HashMap + *probe = Some(inner); + + Err(e) + } + } + } else { + Err(CubeProgrammerError::Parameter { + message: format!( + "Probe with serial number {} already in use", + probe_serial_number + ), + }) + } + } else { + Err(CubeProgrammerError::Parameter { + message: format!("Probe with serial number {} not found", probe_serial_number), + }) + } + } + + /// Connect to the firmware update service (FUS) of a target via a given probe + pub fn connect_to_target_fus( + &self, + probe_serial_number: &crate::probe::Serial, + protocol: &crate::probe::Protocol, + ) -> CubeProgrammerResult { + // Connect with hardware reset an normal mode + let connected = self.connect_to_target( + probe_serial_number, + protocol, + &crate::probe::ConnectionParameters { + frequency: crate::probe::Frequency::Highest, + reset_mode: crate::probe::ResetMode::Hardware, + connection_mode: crate::probe::ConnectionMode::Normal, + }, + )?; + + if !connected.supports_fus() { + let target_information = connected.target_information().clone(); + connected.disconnect(); + + return Err(CubeProgrammerError::NotSupported { + message: format!( + "FUS not supported by connected target series: {}", + target_information.series() + ), + }); + } + + // Start the FUS + api_types::ReturnCode::<1>::from(unsafe { connected.api.startFus() }).check()?; + + // Disconnect + connected.disconnect(); + + // Reconnect with hot plug + let connected = self.connect_to_target( + probe_serial_number, + protocol, + &crate::probe::ConnectionParameters { + frequency: crate::probe::Frequency::Highest, + reset_mode: crate::probe::ResetMode::Hardware, + connection_mode: crate::probe::ConnectionMode::HotPlug, + }, + )?; + + // Read the FUS information + let fus_info = connected.read_fus_info()?; + + Ok(ConnectedFusCubeProgrammer { + programmer: connected, + fus_info, + }) } /// Load the dynamic library with libloading fn load_library( api_library_path: impl AsRef, - ) -> Result { + ) -> Result< + stm32cubeprogrammer_sys::libloading::Library, + stm32cubeprogrammer_sys::libloading::Error, + > { #[cfg(windows)] unsafe fn load_inner( path: impl AsRef, - ) -> Result { + ) -> Result< + stm32cubeprogrammer_sys::libloading::Library, + stm32cubeprogrammer_sys::libloading::Error, + > { + use stm32cubeprogrammer_sys::libloading; + let library: libloading::Library = unsafe { libloading::os::windows::Library::load_with_flags( path, @@ -165,29 +338,130 @@ impl CubeProgrammer { } } -impl CubeProgrammer { +impl std::fmt::Debug for CubeProgrammerApi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CubeProgrammerApi").finish_non_exhaustive() + } +} + +impl Drop for ConnectedCubeProgrammer<'_> { + /// Disconnect and re-insert the probe into the probe registry of the api + fn drop(&mut self) { + unsafe { + self.api.disconnect(); + } + + // Re-insert probe into probe registry + let mut registry = self.probe_registry.borrow_mut(); + registry.insert( + self.probe.serial_number().to_owned().into(), + Some(self.probe.clone()), + ); + } +} + +impl ConnectedCubeProgrammer<'_> { /// Disconnect from target - pub fn disconnect(&self) { - unsafe { self.api.disconnect() }; + pub fn disconnect(self) { + // Consume self -> Drop is called to disconnect } /// Get general device information - pub fn get_general_device_information( - &self, - ) -> CubeProgrammerApiResult { - let general_information = unsafe { self.api.getDeviceGeneralInf() }; + pub fn target_information(&self) -> &api_types::TargetInformation { + &self.general_information + } + + /// Check if the connected target supports the firmware update service (FUS) + fn supports_fus(&self) -> bool { + // TODO: Add support for wb1x + self.general_information.name().eq("STM32WB5x/35xx") + } + + /// Reads the firmware update service (FUS) information from the shared SRAM2A + /// To read the device info table the following procedure needs to be followed ([source](https://wiki.st.com/stm32mcu/wiki/Connectivity:STM32WB_FUS)): + /// - Disconnect + /// - Connect (mode: normal ; reset: hardware) + /// - Start FUS + /// - Disconnect + /// - Connect (mode: normal ; reset: hot-plug) + /// - Read the device info table + /// + /// The connect/disconnect procedure above needs to be performed before calling this function + fn read_fus_info(&self) -> CubeProgrammerResult { + /// Helper function to convert the version word to major, minor, sub + fn u32_to_version(version: u32) -> crate::fus::Version { + const INFO_VERSION_MAJOR_OFFSET: u32 = 24; + const INFO_VERSION_MAJOR_MASK: u32 = 0xff000000; + const INFO_VERSION_MINOR_OFFSET: u32 = 16; + const INFO_VERSION_MINOR_MASK: u32 = 0x00ff0000; + const INFO_VERSION_SUB_OFFSET: u32 = 8; + const INFO_VERSION_SUB_MASK: u32 = 0x0000ff00; + + crate::fus::Version { + major: ((version & INFO_VERSION_MAJOR_MASK) >> INFO_VERSION_MAJOR_OFFSET) as u8, + minor: ((version & INFO_VERSION_MINOR_MASK) >> INFO_VERSION_MINOR_OFFSET) as u8, + sub: ((version & INFO_VERSION_SUB_MASK) >> INFO_VERSION_SUB_OFFSET) as u8, + } + } - if general_information.is_null() { - return Err(CubeProgrammerApiError::CommandReturnNull); + /// Keyword to check if the FUS device info table is valid + const FUS_DEVICE_INFO_TABLE_VALIDITY_KEYWORD: u32 = 0xA94656B9; + /// Offset of the shared RAM + /// TODO: Add support for WB1 + const SRAM2A_BASE_ADDRESS: u32 = SRAM_BASE_ADDRESS + 0x00030000; + + /// FUS device info table struct + /// Ported to rust from `STM32_WPAN/interface/patterns/ble_thread/tl/mbox_def.h` + #[repr(C, packed)] + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Debug)] + struct MBFusDeviceInfoTable { + /// Needs to be equal to `FUS_DEVICE_INFO_TABLE_VALIDITY_KEYWORD` for the table to be valid + device_info_table_state: u32, + reserved1: u8, + last_fus_active_state: u8, + last_wireless_stack_state: u8, + current_wireless_stack_type: u8, + safe_boot_version: u32, + fus_version: u32, + fus_memory_size: u32, + wireless_stack_version: u32, + wireless_stack_memory_size: u32, + wireless_firmware_ble_info: u32, + wireless_firmware_thread_info: u32, + reserved2: u32, + uid64: u64, + device_id: u16, } - let general_information = api_types::TargetInformation(unsafe { *general_information }); - Ok(general_information) + let info_table_address = self.read_memory::(SRAM2A_BASE_ADDRESS, 1)?[0]; + + if info_table_address == 0 { + return Err(CubeProgrammerError::NullValue { + message: "The FUS device info table address is null".to_string(), + }); + } + + let info_table = self.read_memory::(info_table_address, 1)?[0]; + + if info_table.device_info_table_state != FUS_DEVICE_INFO_TABLE_VALIDITY_KEYWORD { + error!("Read FUS info table is not valid. Return default FUS info"); + return Err(CubeProgrammerError::NullValue { + message: "The FUS device info table data is not present".to_string(), + }); + } + + Ok(crate::fus::Information { + fus_version: u32_to_version(info_table.fus_version), + wireless_stack_version: u32_to_version(info_table.wireless_stack_version), + device_id: info_table.device_id, + uid64: info_table.uid64, + }) } /// Reset target - pub fn reset_target(&self, reset_mode: api_types::ResetMode) -> CubeProgrammerApiResult<()> { - api_types::ReturnCode::from(unsafe { self.api.reset(reset_mode.into()) }).check() + pub fn reset_target(&self, reset_mode: crate::probe::ResetMode) -> CubeProgrammerResult<()> { + self.check_connection()?; + api_types::ReturnCode::<0>::from(unsafe { self.api.reset(reset_mode.into()) }).check() } /// Download hex file to target @@ -196,12 +470,45 @@ impl CubeProgrammer { file_path: impl AsRef, skip_erase: bool, verify: bool, - ) -> CubeProgrammerApiResult<()> { - let file_path = utility::path_to_wide_cstring(file_path); + ) -> CubeProgrammerResult<()> { + // Validate if the given file is a valid hex file if the feature is enabled + #[cfg(feature = "ihex")] + { + // Check if the given file is really a hex file + // Unfortunately, the CubeProgrammer API does not check this and simply programs to address 0 if a bin file is passed + let file_content = std::fs::read(&file_path).map_err(CubeProgrammerError::FileIo)?; + let file_content = + std::str::from_utf8(&file_content).map_err(|_| CubeProgrammerError::Parameter { + message: "Invalid intelhex file".to_string(), + })?; + + let reader = ihex::Reader::new_with_options( + file_content, + ihex::ReaderOptions { + stop_after_first_error: true, + stop_after_eof: true, + }, + ); + + for record in reader { + match record { + Ok(_) => {} + Err(e) => { + return Err(CubeProgrammerError::Parameter { + message: format!("Invalid intelhex file: {}", e), + }); + } + } + } + } + + self.check_connection()?; - api_types::ReturnCode::from(unsafe { + let file_path = utility::path_to_widestring(file_path); + + api_types::ReturnCode::<0>::from(unsafe { self.api.downloadFile( - file_path.as_ptr(), + file_path?.as_ptr(), 0, if skip_erase { 1 } else { 0 }, if verify { 1 } else { 0 }, @@ -218,12 +525,14 @@ impl CubeProgrammer { start_address: u32, skip_erase: bool, verify: bool, - ) -> CubeProgrammerApiResult<()> { - let file_path = utility::path_to_wide_cstring(file_path); + ) -> CubeProgrammerResult<()> { + self.check_connection()?; + + let file_path = utility::path_to_widestring(file_path); - api_types::ReturnCode::from(unsafe { + api_types::ReturnCode::<0>::from(unsafe { self.api.downloadFile( - file_path.as_ptr(), + file_path?.as_ptr(), start_address, if skip_erase { 1 } else { 0 }, if verify { 1 } else { 0 }, @@ -234,12 +543,225 @@ impl CubeProgrammer { } /// Perform mass erase - pub fn mass_erase(&self) -> CubeProgrammerApiResult<()> { - api_types::ReturnCode::from(unsafe { self.api.massErase(std::ptr::null_mut()) }).check() + pub fn mass_erase(&self) -> CubeProgrammerResult<()> { + self.check_connection()?; + + api_types::ReturnCode::<0>::from(unsafe { self.api.massErase(std::ptr::null_mut()) }) + .check() + } + + /// Save memory to file + /// Attention: The file path must end with .hex or .bin + pub fn save_memory_file( + &self, + file_path: impl AsRef, + start_address: u32, + size_bytes: u32, + ) -> CubeProgrammerResult<()> { + self.check_connection()?; + + api_types::ReturnCode::<0>::from(unsafe { + self.api.saveMemoryToFile( + i32::try_from(start_address).map_err(|x| CubeProgrammerError::Parameter { + message: format!("Start address exceeds max value: {}", x), + })?, + i32::try_from(size_bytes).map_err(|x| CubeProgrammerError::Parameter { + message: format!("Size exceeds max value: {}", x), + })?, + utility::path_to_widestring(file_path)?.as_ptr(), + ) + }) + .check() + } + + /// Enable roud out protection level 1 (0xBB) + pub fn enable_read_out_protection(&self) -> CubeProgrammerResult<()> { + /// Command according to Example 3 of the CubeProgrammer API documentation + const COMMAND_ENABLE_ROP_LEVEL_1: &str = "-ob rdp=0xbb"; + + self.check_connection()?; + + api_types::ReturnCode::<0>::from(unsafe { + self.api.sendOptionBytesCmd( + utility::string_to_cstring(COMMAND_ENABLE_ROP_LEVEL_1)?.as_ptr() + as *mut std::ffi::c_char, + ) + }) + .check() + } + + /// Disable read out protection + /// Attention: This command will eOrase the device memory + pub fn disable_read_out_protection(&self) -> CubeProgrammerResult<()> { + self.check_connection()?; + api_types::ReturnCode::<0>::from(unsafe { self.api.readUnprotect() }).check()?; + Ok(()) + } + + /// Check connection to target + /// Consumes self and and only returns self if the connection is still maintained + /// If the connection is lost, the user is forced to reconnect + fn check_connection(&self) -> CubeProgrammerResult<()> { + api_types::ReturnCode::<1>::from(unsafe { self.api.checkDeviceConnection() }) + .check() + .map_err(|_| CubeProgrammerError::ConnectionLost) + } + + /// Read in bytes from memory + /// + /// # Arguments + /// address: Start address to read from + /// count: Number of bytes to read + pub fn read_memory_bytes(&self, address: u32, count: usize) -> CubeProgrammerResult> { + let mut data = std::ptr::null_mut(); + let size = u32::try_from(count).map_err(|x| CubeProgrammerError::Parameter { + message: format!("Size exceeds max value: {}", x), + })?; + + api_types::ReturnCode::<0>::from(unsafe { self.api.readMemory(address, &mut data, size) }) + .check()?; + + if data.is_null() { + return Err(CubeProgrammerError::NullValue { + message: "Read memory returned null".to_string(), + }); + } + + let vec = unsafe { std::slice::from_raw_parts_mut(data, count) }.to_vec(); + + unsafe { + self.api.freeLibraryMemory(data as *mut std::ffi::c_void); + } + + Ok(vec) + } + + /// Write memory as bytes + /// + /// # Arguments + /// address: Start address to write to + /// data: Data to write + pub fn write_memory_bytes(&self, address: u32, data: &[u8]) -> CubeProgrammerResult<()> { + let size = u32::try_from(data.len()).map_err(|x| CubeProgrammerError::Parameter { + message: format!("Size exceeds max value: {}", x), + })?; + + api_types::ReturnCode::<0>::from(unsafe { + self.api.writeMemory(address, data.as_ptr() as *mut _, size) + }) + .check() + } + + /// Read memory as struct + /// The struct needs to support the traits `bytemuck::Pod` and `bytemuck::Zeroable` + /// These traits are implemented for lots of types e.g. (full list available [here](https://docs.rs/bytemuck/1.21.0/bytemuck/trait.Pod.html)): + /// - u8, u16, u32 + /// - i8, i16, i32 + /// - f32 + /// + /// # Arguments + /// address: Start address to read from + /// count: Number of struct elements to read + pub fn read_memory( + &self, + address: u32, + count: usize, + ) -> CubeProgrammerResult> { + let size = u32::try_from(std::mem::size_of::() * count).map_err(|x| { + CubeProgrammerError::Parameter { + message: format!("Size exceeds max value: {}", x), + } + })?; + + let mut data = std::ptr::null_mut(); + + api_types::ReturnCode::<0>::from(unsafe { self.api.readMemory(address, &mut data, size) }) + .check()?; + + if data.is_null() { + return Err(CubeProgrammerError::NullValue { + message: "Read memory returned null".to_string(), + }); + } + + let pod_data: &[T] = + bytemuck::try_cast_slice(unsafe { std::slice::from_raw_parts(data, size as _) }) + .map_err(|x| CubeProgrammerError::TypeConversion { + message: format!("Failed to convert bytes to struct: {x}"), + source: crate::error::TypeConversionError::BytemuckError, + })?; + + let pod_data = pod_data.to_vec(); + + if pod_data.len() != count { + return Err(CubeProgrammerError::TypeConversion { + message: "Unexpected number of elements in vector".to_string(), + source: crate::error::TypeConversionError::BytemuckError, + }); + } + + unsafe { + self.api.freeLibraryMemory(data as *mut std::ffi::c_void); + } + + Ok(pod_data) + } + + /// Write memory as struct + /// The struct needs to support the traits `bytemuck::Pod` and `bytemuck::Zeroable` + /// These traits are implemented for lots of types e.g. (full list available [here](https://docs.rs/bytemuck/1.21.0/bytemuck/trait.Pod.html)): + /// - u8, u16, u32 + /// - i8, i16, i32 + /// - f32 + /// + /// # Arguments + /// address: Start address to write to + /// data: A slice of struct elements to write + pub fn write_memory( + &self, + address: u32, + data: &[T], + ) -> CubeProgrammerResult<()> { + let size = u32::try_from(data.len() * std::mem::size_of::()).map_err(|x| { + CubeProgrammerError::Parameter { + message: format!("Size exceeds max value: {}", x), + } + })?; + + let mut bytes = data + .iter() + .flat_map(|x| bytemuck::bytes_of(x).to_vec()) + .collect::>(); + + api_types::ReturnCode::<0>::from(unsafe { + self.api + .writeMemory(address, bytes.as_mut_ptr() as *mut i8, size) + }) + .check() + } + + /// Start the wireless stack + pub fn start_wireless_stack(&self) -> CubeProgrammerResult<()> { + if !self.supports_fus() { + return Err(CubeProgrammerError::NotSupported { + message: "Starting the wireless stack is not supported by the connected target" + .to_string(), + }); + } + + api_types::ReturnCode::<1>::from(unsafe { self.api.startWirelessStack() }).check() + } +} + +impl ConnectedFusCubeProgrammer<'_> { + pub fn fus_info(&self) -> &crate::fus::Information { + &self.fus_info + } + + pub fn delete_ble_stack(&self) -> CubeProgrammerResult<()> { + api_types::ReturnCode::<1>::from(unsafe { self.programmer.api.firmwareDelete() }).check() } - /// Update BLE stack - /// TODO: Check for stm32wb pub fn update_ble_stack( &self, file_path: impl AsRef, @@ -247,42 +769,26 @@ impl CubeProgrammer { first_install: bool, verify: bool, start_stack_after_update: bool, - ) -> CubeProgrammerApiResult<()> { - let return_value = unsafe { - self.api.firmwareUpgrade( - utility::path_to_wide_cstring(file_path).as_ptr(), + ) -> CubeProgrammerResult<()> { + self.programmer.check_connection()?; + + api_types::ReturnCode::<1>::from(unsafe { + self.programmer.api.firmwareUpgrade( + utility::path_to_widestring(file_path)?.as_ptr(), start_address, if first_install { 1 } else { 0 }, if verify { 1 } else { 0 }, if start_stack_after_update { 1 } else { 0 }, ) - }; + }) + .check() + } - // The command returns 1 on success - if return_value == 1 { - Ok(()) - } else { - Err(CubeProgrammerApiError::CommandReturnCode { - return_code: return_value, - }) - } + pub fn start_wireless_stack(&self) -> CubeProgrammerResult<()> { + self.programmer.start_wireless_stack() } - /// Save memory to file - /// Attention: The file path must end with .hex or .bin - pub fn save_memory_file( - &self, - file_path: impl AsRef, - start_address: u32, - size_bytes: u32, - ) -> CubeProgrammerApiResult<()> { - api_types::ReturnCode::from(unsafe { - self.api.saveMemoryToFile( - start_address as i32, // TODO: Handle properly - size_bytes as i32, // TODO: Handle properly - utility::path_to_wide_cstring(file_path).as_ptr(), - ) - }) - .check() + pub fn disconnect(self) { + self.programmer.disconnect() } } diff --git a/stm32cubeprogrammer/src/display.rs b/stm32cubeprogrammer/src/display.rs index ae78fa7..2ce1cca 100644 --- a/stm32cubeprogrammer/src/display.rs +++ b/stm32cubeprogrammer/src/display.rs @@ -1,26 +1,22 @@ -use crate::LogMessageType; -use lazy_static::lazy_static; -use std::sync::{Arc, RwLock}; - -lazy_static! { - static ref DISPLAY_CALLBACK_HANDLER: RwLock>> = - RwLock::new(None); -} - -/// Trait for display callback -/// A library user can implement this trait to receive log messages and update progress bar -pub trait DisplayCallback: Send + Sync { - fn init_progressbar(&self); - fn log_message(&self, message_type: LogMessageType, message: &str); - fn update_progressbar(&self, current_number: u64, total_number: u64); -} - -pub(crate) fn set_display_callback_handler(handler: Arc) { - let mut lock = DISPLAY_CALLBACK_HANDLER.write().unwrap(); - *lock = Some(handler); -} - -pub(crate) fn get_display_callback_handler() -> Option> { - let lock = DISPLAY_CALLBACK_HANDLER.read().unwrap(); - lock.clone() -} \ No newline at end of file +use crate::LogMessageType; + +use std::fmt::Debug; +use std::sync::{Arc, Mutex, OnceLock}; + +static DISPLAY_CALLBACK_HANDLER: OnceLock>> = OnceLock::new(); + +/// Trait for display callback +/// A library user can implement this trait to receive log messages and update progress bar +pub trait DisplayCallback: Send + Sync + Debug { + fn init_progressbar(&self); + fn log_message(&self, message_type: LogMessageType, message: &str); + fn update_progressbar(&self, current_number: u64, total_number: u64); +} + +pub(crate) fn set_display_callback_handler(handler: Arc>) { + DISPLAY_CALLBACK_HANDLER.set(handler).unwrap(); +} + +pub(crate) fn get_display_callback_handler() -> Option<&'static Arc>> { + DISPLAY_CALLBACK_HANDLER.get() +} diff --git a/stm32cubeprogrammer/src/error.rs b/stm32cubeprogrammer/src/error.rs index cf5b293..088ea6a 100644 --- a/stm32cubeprogrammer/src/error.rs +++ b/stm32cubeprogrammer/src/error.rs @@ -1,16 +1,52 @@ use derive_more::{Display, Error}; -use stm32cubeprogrammer_sys::libloading; -pub type CubeProgrammerApiResult = std::result::Result; +pub type CubeProgrammerResult = std::result::Result; +/// Add additional context why a type conversion failed #[derive(Debug, Error, Display)] -pub enum CubeProgrammerApiError { - #[display("LibLoadingError: {}", _0)] - LibLoadingError(libloading::Error), +pub enum TypeConversionError { + Utf8Error, + Utf16Error, + NullError, + BytemuckError, + VersionError, +} - #[display("CommandError: {}", return_code)] - CommandReturnCode { return_code: i32 }, +#[derive(Debug, Error, Display)] +pub enum CubeProgrammerError { + #[display("Command return code error: {}", return_code)] + CommandReturnCode { + return_code: crate::api_types::ErrorCode, + }, + + #[display("Null value error: {}", message)] + NullValue { + message: String, + }, + + #[display("Operation not supported: {}", message)] + NotSupported { + message: String, + }, + + #[display("Parameter error: {}", message)] + Parameter { + message: String, + }, + + #[display("Conversion error: {}", message)] + TypeConversion { + message: String, + + #[error(source)] + source: TypeConversionError, + }, + + #[display("Target connection lost")] + ConnectionLost, + + #[display("File IO error: {}", _0)] + FileIo(std::io::Error), - #[display("CommandError: Command returned null")] - CommandReturnNull, -} \ No newline at end of file + LibLoading(stm32cubeprogrammer_sys::libloading::Error), +} diff --git a/stm32cubeprogrammer/src/lib.rs b/stm32cubeprogrammer/src/lib.rs index 6447463..ebcb298 100644 --- a/stm32cubeprogrammer/src/lib.rs +++ b/stm32cubeprogrammer/src/lib.rs @@ -2,13 +2,19 @@ pub mod api_log; pub use api_log::{LogMessageType, Verbosity}; pub mod api_types; -pub use api_types::*; +pub use api_types::{fus, probe, TargetInformation}; pub mod display; pub use display::DisplayCallback; pub mod cube_programmer; -pub use cube_programmer::{CubeProgrammer, CubeProgrammerBuilder}; +pub use cube_programmer::CubeProgrammerApi; pub mod error; pub mod utility; + +// Re-export of the `bytemuck` crate -> needed for reading/writing of structs from/to memory +pub use bytemuck; + +#[cfg(feature = "ihex")] +pub use ihex; diff --git a/stm32cubeprogrammer/src/utility.rs b/stm32cubeprogrammer/src/utility.rs index 77297ad..117d0ba 100644 --- a/stm32cubeprogrammer/src/utility.rs +++ b/stm32cubeprogrammer/src/utility.rs @@ -1,55 +1,87 @@ -use std::ffi::{c_char, CStr, CString}; -use std::path::Path; - -/// Remove the extended path prefix from a path -fn remove_extended_path_prefix(path: &str) -> &str { - #[cfg(windows)] - const EXTENDED_PATH_PREFIX: &str = "\\\\?\\"; - - #[cfg(windows)] - if path.starts_with(EXTENDED_PATH_PREFIX) { - path.strip_prefix(EXTENDED_PATH_PREFIX).unwrap() - } else { - path - } - - #[cfg(not(windows))] - path -} - -/// Convert a path to a CString -/// If the path is a extended length path, the prefix will be removed -pub(crate) fn path_to_cstring(path: impl AsRef) -> CString { - let path = path - .as_ref() - .to_str() - .expect("Cannot convert path to string"); - - CString::new(remove_extended_path_prefix(path)).expect("Cannot convert path to CString") -} - -/// Convert a path to a CString -/// If the path is a extended length path, the prefix will be removed -pub(crate) fn path_to_wide_cstring(path: impl AsRef) -> widestring::WideCString { - let path = path - .as_ref() - .to_str() - .expect("Cannot convert path to string"); - - widestring::WideCString::from_str(remove_extended_path_prefix(path)) - .expect("Cannot convert path to WideCString") -} - -/// Convert a wide cstring to a string -pub(crate) fn wide_cstring_to_string(wide_cstring: &widestring::WideCString) -> String { - wide_cstring - .to_string() - .expect("Cannot convert WideCString to string") -} - -/// Convert a c_char slice to a null-terminated string -pub(crate) fn cchar_to_null_terminated_string(slice: &[c_char]) -> &str { - let cstr = - CStr::from_bytes_until_nul(bytemuck::cast_slice(slice)).expect("Failed to convert CStr"); - cstr.to_str().expect("Failed to convert CStr to str") -} +use core::str; +use std::ffi::{c_char, CString}; +use std::path::Path; + +use crate::error::{CubeProgrammerError, CubeProgrammerResult, TypeConversionError}; + +/// Remove the extended path prefix from a path +fn remove_extended_path_prefix(path: &str) -> &str { + #[cfg(windows)] + const EXTENDED_PATH_PREFIX: &str = "\\\\?\\"; + + #[cfg(windows)] + if path.starts_with(EXTENDED_PATH_PREFIX) { + path.strip_prefix(EXTENDED_PATH_PREFIX).unwrap() + } else { + path + } + + #[cfg(not(windows))] + path +} + +/// Convert a path to cstring +/// If the path is a extended length path, the prefix will be removed +pub(crate) fn path_to_cstring(path: impl AsRef) -> CubeProgrammerResult { + let path = path + .as_ref() + .to_str() + .ok_or(CubeProgrammerError::TypeConversion { + message: format!("Cannot convert path `{:?}` to string", path.as_ref()), + source: TypeConversionError::Utf8Error, + })?; + + string_to_cstring(remove_extended_path_prefix(path)) +} + +/// Convert a path to a wide string +/// If the path is a extended length path, the prefix will be removed +pub(crate) fn path_to_widestring( + path: impl AsRef, +) -> CubeProgrammerResult { + let path = path + .as_ref() + .to_str() + .ok_or(CubeProgrammerError::TypeConversion { + message: format!("Cannot convert path `{:?}` to string", path.as_ref()), + source: TypeConversionError::Utf8Error, + })?; + + string_to_widestring(remove_extended_path_prefix(path)) +} + +/// Convert a string to a wide string +pub(crate) fn string_to_widestring(s: &str) -> CubeProgrammerResult { + widestring::WideCString::from_str(s).map_err(|x| CubeProgrammerError::TypeConversion { + message: format!("Cannot convert string to widestring: {:?}", x), + source: TypeConversionError::NullError, + }) +} + +/// Convert a wide cstring to a string +pub(crate) fn widestring_to_string( + wide_string: &widestring::WideCString, +) -> CubeProgrammerResult { + wide_string + .to_string() + .map_err(|x| CubeProgrammerError::TypeConversion { + message: format!("Cannot convert widestring to string: {:?}", x), + source: TypeConversionError::Utf16Error, + }) +} + +/// Convert a c_char slice to a null-terminated string +pub(crate) fn c_char_slice_to_string(slice: &[c_char]) -> CubeProgrammerResult<&str> { + str::from_utf8(bytemuck::cast_slice(slice)).map_err(|x| CubeProgrammerError::TypeConversion { + message: format!("Failed to convert c_char slice to string: {:?}", x), + source: TypeConversionError::Utf8Error, + }) +} + +/// Convert a string to a cstring +pub(crate) fn string_to_cstring(s: &str) -> CubeProgrammerResult { + CString::new(s).map_err(|x| CubeProgrammerError::TypeConversion { + message: format!("Failed to convert str to cstring: {:?}", x), + source: TypeConversionError::NullError, + }) +} diff --git a/stm32cubeprogrammer/tests/basic.rs b/stm32cubeprogrammer/tests/basic.rs index 07ee250..6a75246 100644 --- a/stm32cubeprogrammer/tests/basic.rs +++ b/stm32cubeprogrammer/tests/basic.rs @@ -1,6 +1,9 @@ -#[cfg(test)] -use log::{debug, info, warn}; -use stm32cubeprogrammer::{CubeProgrammer, ResetMode, Verbosity}; +use log::{debug, info}; + +use stm32cubeprogrammer::{ + cube_programmer::{ConnectedCubeProgrammer, ConnectedFusCubeProgrammer}, + probe, CubeProgrammerApi, +}; /// Environment variable name for the path to the STM32CubeProgrammer directory /// Needs to be the root path of the STM32CubeProgrammer installatios @@ -49,259 +52,248 @@ fn get_address_from_env_file(env_key: &str) -> u32 { /// Get the target directory -> Unfortunately MANIFEST_DIR does not work with cargo workspaces? fn get_target_dir() -> std::path::PathBuf { - let path = std::env::current_dir().unwrap(); - - debug!("Current dir: {:?}", path); - - let path = path.join("..").join("target").canonicalize().unwrap(); - - debug!("Target dir: {:?}", path); + let path = std::env::current_dir() + .unwrap() + .join("..") + .join("target") + .canonicalize() + .unwrap(); path } -#[test_log::test] -fn discover_st_links() { - dotenvy::dotenv().unwrap(); - - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) +fn get_api() -> CubeProgrammerApi { + let api: CubeProgrammerApi = CubeProgrammerApi::builder() + .cube_programmer_dir(&get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) + .log_verbosity(stm32cubeprogrammer::Verbosity::Level3) .build() .unwrap(); - let probes = programmer.list_connected_st_link_probes(); - - for probe in probes { - info!("Found ST-Link probe: {}", probe); - } + api } -#[test_log::test] -fn connect_to_target() { - dotenvy::dotenv().unwrap(); - - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) - .build() - .unwrap(); - - let probes = programmer.list_connected_st_link_probes(); +fn connect( + api: &mut CubeProgrammerApi, + connect_param: probe::ConnectionParameters, +) -> ConnectedCubeProgrammer { + let probes = api.list_available_probes(); if !probes.is_empty() { info!("Found {} ST-Link probes - Trying to connect", probes.len()); info!("Connecting to target via probe: {}", probes[0]); - let connected_programmer = programmer.connect_to_target(&probes[0]).unwrap(); - - let target_information = connected_programmer - .get_general_device_information() + let connected_programmer = api + .connect_to_target(&probes[0], &probe::Protocol::Swd, &connect_param) .unwrap(); - info!("Connected to target: {}", target_information); - - info!("Connected to target. Disconnecting..."); - connected_programmer.disconnect(); + connected_programmer } else { - info!("No ST-Link probes found"); + panic!("No ST-Link probes found"); } } -#[test_log::test] -fn download_hex_file() { - dotenvy::dotenv().unwrap(); - - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) - .build() - .unwrap(); - - let probes = programmer.list_connected_st_link_probes(); +fn connect_fus(api: &mut CubeProgrammerApi) -> ConnectedFusCubeProgrammer { + let probes = api.list_available_probes(); if !probes.is_empty() { info!("Found {} ST-Link probes - Trying to connect", probes.len()); info!("Connecting to target via probe: {}", probes[0]); - let connected_programmer = programmer.connect_to_target(&probes[0]).unwrap(); - - let target_information = connected_programmer - .get_general_device_information() + let connected_programmer = api + .connect_to_target_fus(&probes[0], &probe::Protocol::Swd) .unwrap(); - info!("Connected to target: {}", target_information); - - let hex_file = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_HEX_PATH); - info!("Downloading hex file: {:?}", hex_file); - connected_programmer - .download_hex_file(hex_file, false, true) - .unwrap(); - - info!("Connected to target. Disconnecting..."); - - connected_programmer - .reset_target(ResetMode::HardwareReset) - .unwrap(); - connected_programmer.disconnect(); } else { - info!("No ST-Link probes found"); + panic!("No ST-Link probes found"); } } +// -- TEST CASES -- // #[test_log::test] -fn download_bin_file() { +fn connect_twice() { dotenvy::dotenv().unwrap(); + let api = get_api(); + let probes = api.list_available_probes(); - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) - .build() - .unwrap(); - - let probes = programmer.list_connected_st_link_probes(); - - if !probes.is_empty() { - info!("Found {} ST-Link probes - Trying to connect", probes.len()); - info!("Connecting to target via probe: {}", probes[0]); - - let connected_programmer = programmer.connect_to_target(&probes[0]).unwrap(); - - let target_information = connected_programmer - .get_general_device_information() - .unwrap(); - - info!("Connected to target: {}", target_information); - - let hex_file = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_PATH); - info!("Downloading bin file: {:?}", hex_file); + if probes.is_empty() { + panic!("No ST-Link probes found"); + } - connected_programmer - .download_bin_file( - hex_file, - get_address_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_START_ADDRESS), - false, - true, - ) - .unwrap(); + info!("Found {} ST-Link probes - Trying to connect", probes.len()); + info!("Connecting to target via probe: {}", probes[0]); - info!("Connected to target. Disconnecting..."); + let _connected_programmer = api + .connect_to_target( + &probes[0], + &probe::Protocol::Swd, + &probe::ConnectionParameters::default(), + ) + .unwrap(); - connected_programmer - .reset_target(ResetMode::HardwareReset) - .unwrap(); - connected_programmer.disconnect(); - } else { - info!("No ST-Link probes found"); - } + // Connect to same probe again -> must not work + if let Ok(_) = api.connect_to_target( + &probes[0], + &probe::Protocol::Swd, + &probe::ConnectionParameters::default(), + ) { + panic!("Connecting to the same probe twice must not work"); + }; } #[test_log::test] -fn upgrade_ble_stack() { +fn write_and_read() { + let data_bytes = b"\x01\x02\x03\x04\xaa\xbb\xcc\xdd_Hello_Cube"; + + #[repr(C, packed)] + #[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] + struct MyData { + one: u8, + two: u8, + three: i16, + four: u32, + five: [u8; 11], + } + dotenvy::dotenv().unwrap(); + let mut api = get_api(); + let connected_programmer = connect( + &mut api, + probe::ConnectionParameters { + connection_mode: probe::ConnectionMode::UnderReset, + ..Default::default() + }, + ); + + let address = stm32cubeprogrammer_sys::SRAM_BASE_ADDRESS + 1024; + + // Write bytes and read pod + connected_programmer + .write_memory_bytes(address, data_bytes) + .unwrap(); + connected_programmer + .write_memory_bytes(address + std::mem::size_of::() as u32, data_bytes) + .unwrap(); - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) - .build() + let read = connected_programmer + .read_memory::(address, 2) .unwrap(); - let probes = programmer.list_connected_st_link_probes(); + dbg!(&read); + assert_eq!(bytemuck::bytes_of(&read[0]), data_bytes); + assert_eq!(read[0], read[1]); - if !probes.is_empty() { - info!("Found {} ST-Link probes - Trying to connect", probes.len()); - info!("Connecting to target via probe: {}", probes[0]); + // Read elements separately + let curr = connected_programmer.read_memory::(address, 1).unwrap()[0]; + assert_eq!(curr, read[0].one); - let connected_programmer = programmer.connect_to_target(&probes[0]).unwrap(); + let curr = connected_programmer.read_memory::(address + 1, 1).unwrap()[0]; + assert_eq!(curr, read[0].two); - let target_information = connected_programmer - .get_general_device_information() - .unwrap(); + let curr = connected_programmer.read_memory::(address + 2, 1).unwrap()[0]; + let three = read[0].three; + assert_eq!(curr, three); - info!("Connected to target: {}", target_information); + let curr = connected_programmer.read_memory::(address + 4, 1).unwrap()[0]; + let four = read[0].four; + assert_eq!(curr, four); - let ble_stack_binary = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_BLE_STACK_PATH); - info!("Downloading ble stack file: {:?}", ble_stack_binary); + let curr = connected_programmer.read_memory::<[u8; 11]>(address + 8, 1).unwrap()[0]; + let five = read[0].five; + assert_eq!(curr, five); - connected_programmer - .update_ble_stack( - ble_stack_binary, - get_address_from_env_file(STM32_CUBE_PROGRAMMER_BLE_STACK_START_ADDRESS), - false, - true, - true, - ) - .unwrap(); + let address = stm32cubeprogrammer_sys::SRAM_BASE_ADDRESS + 2048; - info!("Connected to target. Disconnecting..."); + // Write pod and read bytes + let data = vec![read[0]; 2]; + dbg!(&data); - connected_programmer - .reset_target(ResetMode::HardwareReset) - .unwrap(); - connected_programmer.disconnect(); - } else { - info!("No ST-Link probes found"); - } + connected_programmer + .write_memory::(address, &data) + .unwrap(); + + let read = connected_programmer + .read_memory_bytes(address, std::mem::size_of::()) + .unwrap(); + assert_eq!(read.as_slice(), data_bytes); + + let read = connected_programmer + .read_memory_bytes( + address + std::mem::size_of::() as u32, + std::mem::size_of::(), + ) + .unwrap(); + assert_eq!(read.as_slice(), data_bytes); } -/// Test showing how to register a custom display handler -/// This can be used in e.g. a CLI or GUI application to show the progress of the operations #[test_log::test] -fn register_display_handler() { - use std::sync::Arc; - - /// Custom display handler - struct MyDisplayHandler; - - impl stm32cubeprogrammer::DisplayCallback for MyDisplayHandler { - fn init_progressbar(&self) { - warn!("MyDisplayHandler - Init progress bar"); - } - - fn log_message(&self, message_type: stm32cubeprogrammer::LogMessageType, message: &str) { - info!( - "MyDisplayHandler - Log message: {:?} - {}", - message_type, message - ); - } - - fn update_progressbar(&self, current_number: u64, total_number: u64) { - warn!( - "MyDisplayHandler - Update progress bar: {}/{}", - current_number, total_number - ); - } - } - +fn fus_actions() { dotenvy::dotenv().unwrap(); - - let programmer = CubeProgrammer::builder() - .cube_programmer_dir(get_path_from_env_file(ENV_CUBE_PROGRAMMER_DIR)) - .log_verbosity(Verbosity::Level2) - .display_callback(Arc::new(MyDisplayHandler)) - .build() + let mut api = get_api(); + + let connected_programmer = connect_fus(&mut api); + dbg!(connected_programmer.fus_info()); + + // Delete BLE stack + connected_programmer.delete_ble_stack().unwrap(); + connected_programmer.disconnect(); + + // Reconnect to read updated FUS information + let connected_programmer = connect_fus(&mut api); + let fus_info = connected_programmer.fus_info(); + dbg!(&fus_info); + assert_eq!( + fus_info.wireless_stack_version, + stm32cubeprogrammer::fus::Version::try_from("0.0.0").unwrap() + ); + + // Upgrade BLE stack + let ble_stack_binary = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_BLE_STACK_PATH); + info!("Downloading ble stack file: {:?}", ble_stack_binary); + + connected_programmer + .update_ble_stack( + ble_stack_binary, + get_address_from_env_file(STM32_CUBE_PROGRAMMER_BLE_STACK_START_ADDRESS), + false, + true, + true, + ) .unwrap(); - let probes = programmer.list_connected_st_link_probes(); - - if !probes.is_empty() { - info!("Found {} ST-Link probes - Trying to connect", probes.len()); - info!("Connecting to target via probe: {}", probes[0]); - - let connected_programmer = programmer.connect_to_target(&probes[0]).unwrap(); + connected_programmer.disconnect(); + let connected_programmer = connect_fus(&mut api); + dbg!(connected_programmer.fus_info()); +} - let target_information = connected_programmer - .get_general_device_information() - .unwrap(); +#[test_log::test] +fn download_hex_file() { + dotenvy::dotenv().unwrap(); + let mut api = get_api(); + let connected_programmer = connect(&mut api, probe::ConnectionParameters::default()); - info!("Connected to target: {}", target_information); + let hex_file = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_HEX_PATH); + info!("Downloading hex file: {:?}", hex_file); - // Read the memory and store it to the target dir - connected_programmer - .save_memory_file( - get_target_dir().join("memory.bin"), - get_address_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_START_ADDRESS), - 1024 * 400, - ) - .unwrap(); + connected_programmer + .download_hex_file(hex_file, false, true) + .unwrap(); +} - connected_programmer.disconnect(); - } else { - info!("No ST-Link probes found"); - } +#[test_log::test] +fn download_bin_file() { + dotenvy::dotenv().unwrap(); + let mut api = get_api(); + let connected_programmer = connect(&mut api, probe::ConnectionParameters::default()); + + let hex_file = get_path_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_PATH); + info!("Downloading hex file: {:?}", hex_file); + + connected_programmer + .download_bin_file( + hex_file, + get_address_from_env_file(ENV_STM32_CUBE_PROGRAMMER_DOWNLOAD_BIN_START_ADDRESS), + false, + true, + ) + .unwrap(); }