use std::{io::Write, path::PathBuf};
use super::cargo::ArtifactError;
use crate::util::parse_u64;
use probe_rs::{
Permissions, Session, Target,
config::{Registry, RegistryError, TargetSelector},
flashing::{FileDownloadError, FlashError},
integration::FakeProbe,
probe::{
DebugProbeError, DebugProbeInfo, DebugProbeSelector, Probe, WireProtocol, list::Lister,
},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, clap::Parser)]
pub struct BinaryDownloadOptions {
#[arg(long, help_heading = "DOWNLOAD CONFIGURATION")]
pub disable_progressbars: bool,
#[arg(long, help_heading = "DOWNLOAD CONFIGURATION")]
pub disable_double_buffering: bool,
#[arg(long, help_heading = "DOWNLOAD CONFIGURATION")]
pub restore_unwritten: bool,
#[arg(
value_name = "filename",
long = "flash-layout",
help_heading = "DOWNLOAD CONFIGURATION"
)]
pub flash_layout_output_path: Option<String>,
#[arg(long, help_heading = "DOWNLOAD CONFIGURATION")]
pub preverify: bool,
#[arg(long, help_heading = "DOWNLOAD CONFIGURATION")]
pub verify: bool,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, clap::ValueEnum)]
pub enum ReadWriteBitWidth {
B8 = 8,
B16 = 16,
B32 = 32,
B64 = 64,
}
#[derive(Debug, clap::Parser)]
pub struct ReadWriteOptions {
#[clap(value_enum, ignore_case = true)]
pub width: ReadWriteBitWidth,
#[clap(value_parser = parse_u64)]
pub address: u64,
}
#[derive(clap::Parser, Clone, Debug, Serialize, Deserialize)]
pub struct ProbeOptions {
#[arg(long, env = "PROBE_RS_CHIP", help_heading = "PROBE CONFIGURATION")]
pub chip: Option<String>,
#[arg(
value_name = "chip description file path",
long,
env = "PROBE_RS_CHIP_DESCRIPTION_PATH",
help_heading = "PROBE CONFIGURATION"
)]
pub chip_description_path: Option<PathBuf>,
#[arg(long, env = "PROBE_RS_PROTOCOL", help_heading = "PROBE CONFIGURATION")]
pub protocol: Option<WireProtocol>,
#[arg(
long,
env = "PROBE_RS_NON_INTERACTIVE",
help_heading = "PROBE CONFIGURATION"
)]
pub non_interactive: bool,
#[arg(long, env = "PROBE_RS_PROBE", help_heading = "PROBE CONFIGURATION")]
pub probe: Option<DebugProbeSelector>,
#[arg(long, env = "PROBE_RS_SPEED", help_heading = "PROBE CONFIGURATION")]
pub speed: Option<u32>,
#[arg(
long,
env = "PROBE_RS_CONNECT_UNDER_RESET",
help_heading = "PROBE CONFIGURATION"
)]
pub connect_under_reset: bool,
#[arg(long, env = "PROBE_RS_DRY_RUN", help_heading = "PROBE CONFIGURATION")]
pub dry_run: bool,
#[arg(
long,
env = "PROBE_RS_ALLOW_ERASE_ALL",
help_heading = "PROBE CONFIGURATION"
)]
pub allow_erase_all: bool,
}
impl ProbeOptions {
pub fn load(self, registry: &mut Registry) -> Result<LoadedProbeOptions, OperationError> {
LoadedProbeOptions::new(self, registry)
}
pub fn simple_attach<'r>(
self,
registry: &'r mut Registry,
lister: &Lister,
) -> Result<(Session, LoadedProbeOptions<'r>), OperationError> {
let common_options = self.load(registry)?;
let target = common_options.get_target_selector()?;
let probe = common_options.attach_probe(lister)?;
let session = common_options.attach_session(probe, target)?;
Ok((session, common_options))
}
}
pub struct LoadedProbeOptions<'r>(ProbeOptions, &'r mut Registry);
impl<'r> LoadedProbeOptions<'r> {
pub(crate) fn new(
probe_options: ProbeOptions,
registry: &'r mut Registry,
) -> Result<Self, OperationError> {
let mut options = Self(probe_options, registry);
options.maybe_load_chip_desc()?;
Ok(options)
}
fn maybe_load_chip_desc(&mut self) -> Result<(), OperationError> {
if let Some(ref cdp) = self.0.chip_description_path {
let yaml = std::fs::read_to_string(cdp).map_err(|error| {
OperationError::ChipDescriptionNotFound {
source: error,
path: cdp.clone(),
}
})?;
self.1.add_target_family_from_yaml(&yaml).map_err(|error| {
OperationError::FailedChipDescriptionParsing {
source: error,
path: cdp.clone(),
}
})?;
}
Ok(())
}
pub fn get_target_selector(&self) -> Result<TargetSelector, OperationError> {
let target = if let Some(chip_name) = &self.0.chip {
let target = self.1.get_target_by_name(chip_name).map_err(|error| {
OperationError::ChipNotFound {
source: error,
name: chip_name.clone(),
}
})?;
TargetSelector::Specified(target)
} else {
TargetSelector::Auto
};
Ok(target)
}
fn interactive_probe_select(
list: &[DebugProbeInfo],
) -> Result<&DebugProbeInfo, OperationError> {
println!("Available Probes:");
for (i, probe_info) in list.iter().enumerate() {
println!("{i}: {probe_info}");
}
print!("Selection: ");
std::io::stdout().flush().unwrap();
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Expect input for probe selection");
let probe_idx = input
.trim()
.parse::<usize>()
.map_err(OperationError::ParseProbeIndex)?;
list.get(probe_idx).ok_or(OperationError::NoProbesFound)
}
fn select_probe(lister: &Lister, non_interactive: bool) -> Result<Probe, OperationError> {
let list = lister.list_all();
let selected = match list.len() {
0 | 1 => list.first().ok_or(OperationError::NoProbesFound),
_ if non_interactive => Err(OperationError::MultipleProbesFound { list }),
_ => Self::interactive_probe_select(&list),
};
selected.and_then(|probe_info| Ok(lister.open(probe_info)?))
}
pub fn attach_probe(&self, lister: &Lister) -> Result<Probe, OperationError> {
let mut probe = if self.0.dry_run {
Probe::from_specific_probe(Box::new(FakeProbe::with_mocked_core()))
} else {
match &self.0.probe {
Some(selector) => lister.open(selector)?,
None => Self::select_probe(lister, self.0.non_interactive)?,
}
};
if let Some(protocol) = self.0.protocol {
probe.select_protocol(protocol).map_err(|error| {
OperationError::FailedToSelectProtocol {
source: error,
protocol,
}
})?;
}
if let Some(speed) = self.0.speed {
let _actual_speed = probe.set_speed(speed).map_err(|error| {
OperationError::FailedToSelectProtocolSpeed {
source: error,
speed,
}
})?;
let protocol_speed = probe.speed_khz();
if let Some(speed) = self.0.speed {
if protocol_speed < speed {
tracing::warn!(
"Unable to use specified speed of {} kHz, actual speed used is {} kHz",
speed,
protocol_speed
);
}
}
tracing::info!("Protocol speed {} kHz", protocol_speed);
}
Ok(probe)
}
pub fn attach_session(
&self,
probe: Probe,
target: TargetSelector,
) -> Result<Session, OperationError> {
let mut permissions = Permissions::new();
if self.0.allow_erase_all {
permissions = permissions.allow_erase_all();
}
let session = if self.0.connect_under_reset {
probe.attach_under_reset(target, permissions)
} else {
probe.attach(target, permissions)
}
.map_err(|error| OperationError::AttachingFailed {
source: error,
connect_under_reset: self.0.connect_under_reset,
})?;
Ok(session)
}
pub(crate) fn connect_under_reset(&self) -> bool {
self.0.connect_under_reset
}
pub(crate) fn dry_run(&self) -> bool {
self.0.dry_run
}
pub(crate) fn chip(&self) -> Option<String> {
self.0.chip.clone()
}
}
impl AsRef<ProbeOptions> for LoadedProbeOptions<'_> {
fn as_ref(&self) -> &ProbeOptions {
&self.0
}
}
#[derive(clap::Parser, Debug, Default)]
pub struct CargoOptions {
#[arg(value_name = "binary", long, hide = true)]
pub bin: Option<String>,
#[arg(long, hide = true)]
pub example: Option<String>,
#[arg(short, long, hide = true)]
pub package: Option<String>,
#[arg(long, hide = true)]
pub release: bool,
#[arg(long, hide = true)]
pub target: Option<String>,
#[arg(value_name = "PATH", long, hide = true)]
pub manifest_path: Option<PathBuf>,
#[arg(long, hide = true)]
pub no_default_features: bool,
#[arg(long, hide = true)]
pub all_features: bool,
#[arg(long, hide = true)]
pub features: Vec<String>,
#[arg(hide = true)]
pub trailing_opts: Vec<String>,
}
impl CargoOptions {
pub fn help_message(bin: &str) -> String {
format!(
r#"
CARGO BUILD OPTIONS:
The following options are forwarded to 'cargo build':
--bin
--example
-p, --package
--release
--target
--manifest-path
--no-default-features
--all-features
--features
Additionally, all options passed after a sentinel '--'
are also forwarded.
For example, if you run the command '{bin} --release -- \
--some-cargo-flag', this means that 'cargo build \
--release --some-cargo-flag' will be called.
"#
)
}
pub fn to_cargo_options(&self) -> Vec<String> {
let mut args: Vec<String> = vec![];
macro_rules! maybe_push_str_opt {
($field:expr, $name:expr) => {{
if let Some(value) = $field {
args.push(format!("--{}", stringify!($name)));
args.push(value.clone());
}
}};
}
maybe_push_str_opt!(&self.bin, bin);
maybe_push_str_opt!(&self.example, example);
maybe_push_str_opt!(&self.package, package);
if self.release {
args.push("--release".to_string());
}
maybe_push_str_opt!(&self.target, target);
if let Some(path) = &self.manifest_path {
args.push("--manifest-path".to_string());
args.push(path.display().to_string());
}
if self.no_default_features {
args.push("--no-default-features".to_string());
}
if self.all_features {
args.push("--all-features".to_string());
}
if !self.features.is_empty() {
args.push("--features".to_string());
args.push(self.features.join(","));
}
args.append(&mut self.trailing_opts.clone());
args
}
}
#[derive(Debug, thiserror::Error)]
pub enum OperationError {
#[error("No connected probes were found.")]
NoProbesFound,
#[error("Failed to open the ELF file '{path}' for flashing.")]
#[allow(dead_code)]
FailedToOpenElf {
#[source]
source: std::io::Error,
path: PathBuf,
},
#[error("Failed to load the ELF data.")]
#[allow(dead_code)]
FailedToLoadElfData(#[source] FileDownloadError),
#[error("Failed to open the debug probe.")]
FailedToOpenProbe(#[from] DebugProbeError),
#[error("{} probes were found: {}", .list.len(), print_list(.list))]
MultipleProbesFound { list: Vec<DebugProbeInfo> },
#[error("The flashing procedure failed for '{path}'.")]
FlashingFailed {
source: Box<FlashError>,
target: Box<Target>, target_spec: Option<String>,
path: PathBuf,
},
#[error("Failed to open the chip description '{path}'.")]
ChipDescriptionNotFound {
source: std::io::Error,
path: PathBuf,
},
#[error("Failed to parse the chip description '{path}'.")]
FailedChipDescriptionParsing {
source: RegistryError,
path: PathBuf,
},
#[error("Failed to change the working directory to '{path}'.")]
FailedToChangeWorkingDirectory {
source: std::io::Error,
path: PathBuf,
},
#[error("Failed to build the cargo project at '{path}'.")]
FailedToBuildExternalCargoProject {
source: ArtifactError,
path: PathBuf,
},
#[error("Failed to build the cargo project.")]
FailedToBuildCargoProject(#[source] ArtifactError),
#[error("The chip '{name}' was not found in the database.")]
ChipNotFound { source: RegistryError, name: String },
#[error("The protocol '{protocol}' could not be selected.")]
FailedToSelectProtocol {
source: DebugProbeError,
protocol: WireProtocol,
},
#[error("The protocol speed could not be set to '{speed}' kHz.")]
FailedToSelectProtocolSpeed { source: DebugProbeError, speed: u32 },
#[error("Connecting to the chip was unsuccessful.")]
AttachingFailed {
source: probe_rs::Error,
connect_under_reset: bool,
},
#[error("Failed to get a handle to the first core.")]
AttachingToCoreFailed(#[source] probe_rs::Error),
#[error("The reset of the target failed.")]
TargetResetFailed(#[source] probe_rs::Error),
#[error("The target could not be reset and halted.")]
TargetResetHaltFailed(#[source] probe_rs::Error),
#[error("Failed to write to file")]
IOError(#[source] std::io::Error),
#[error("Failed to parse CLI arguments.")]
CliArgument(#[from] clap::Error),
#[error("Failed to parse interactive probe index selection")]
ParseProbeIndex(#[source] std::num::ParseIntError),
}
fn print_list(list: &[impl std::fmt::Display]) -> String {
let mut output = String::new();
for (i, entry) in list.iter().enumerate() {
output.push_str(&format!("\n {}. {}", i + 1, entry));
}
output
}
impl From<std::io::Error> for OperationError {
fn from(e: std::io::Error) -> Self {
OperationError::IOError(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_cargo_options() {
assert_eq!(
CargoOptions {
bin: Some("foobar".into()),
example: Some("foobar".into()),
package: Some("foobar".into()),
release: true,
target: Some("foobar".into()),
manifest_path: Some("/tmp/Cargo.toml".into()),
no_default_features: true,
all_features: true,
features: vec!["feat1".into(), "feat2".into()],
trailing_opts: vec!["--some-cargo-option".into()],
}
.to_cargo_options(),
[
"--bin",
"foobar",
"--example",
"foobar",
"--package",
"foobar",
"--release",
"--target",
"foobar",
"--manifest-path",
"/tmp/Cargo.toml",
"--no-default-features",
"--all-features",
"--features",
"feat1,feat2",
"--some-cargo-option",
]
);
}
}