tudelft_serial_upload/
upload.rs

1use crate::serial::Serial;
2use crate::{selector, PortSelector};
3use color_eyre::eyre::{bail, eyre, Context};
4use color_eyre::Result;
5use std::fs::read;
6use std::path::{Path, PathBuf};
7use std::process::{exit, Command};
8
9fn copy_object(source: &Path, target: &Path) -> Result<()> {
10    if Command::new("rust-objcopy").output().is_err() {
11        bail!(
12            "rust-objcopy not found, try installing cargo-binutils or refer to the course website"
13        );
14    }
15
16    let op = Command::new("rust-objcopy")
17        .arg("-O")
18        .arg("binary")
19        .arg(source)
20        .arg(target)
21        .output()
22        .wrap_err("failed to run rust-objcopy")?;
23
24    println!("creating binary file at {target:?}");
25
26    if !op.status.success() {
27        bail!(
28            "running rust-objcopy failed: {}",
29            String::from_utf8_lossy(&op.stderr)
30        );
31    }
32
33    Ok(())
34}
35
36fn read_file(file: &Path) -> Result<Vec<u8>> {
37    let mut target = file.to_path_buf();
38    target.set_extension("bin");
39
40    println!("converting elf file to bin file");
41    copy_object(file, &target)?;
42
43    println!("reading binary file");
44    read(target).wrap_err("failed to read converted binary file to send to board")
45}
46
47/// Upload a file to a connected board. Select which serial port the board is on with the [`PortSelector`].
48/// The file is expected to be the compiled `.elf` file created by cargo/rustc
49/// Exit with an exit code of 1 when the upload fails.
50///
51/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
52pub fn upload_file_or_stop(port: PortSelector, file: Option<impl AsRef<Path>>) -> PathBuf {
53    if let Some(file) = file {
54        match read_file(file.as_ref())
55            .wrap_err_with(|| format!("failed to read from file {:?}", file.as_ref()))
56        {
57            Ok(i) => upload_or_stop(port, i, false),
58            Err(e) => {
59                eprintln!("{e:?}");
60                exit(1);
61            }
62        }
63    } else {
64        upload_or_stop(port, [], true)
65    }
66}
67
68/// Upload a file to a connected board. Select which serial port the board is on with the [`PortSelector`]
69/// The file is expected to be the compiled `.elf` file created by cargo/rustc
70/// Returns an error when the upload fails.
71///
72/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
73pub fn upload_file(port: PortSelector, file: Option<impl AsRef<Path>>) -> Result<PathBuf> {
74    upload(
75        port,
76        file.as_ref()
77            .map(|f| {
78                read_file(f.as_ref())
79                    .wrap_err_with(|| format!("failed to read from file {:?}", f.as_ref()))
80            })
81            .transpose()?
82            .unwrap_or_default(),
83        file.is_none(),
84    )
85}
86
87/// Upload (already read) bytes to a connected board. Select which serial port the board is on with the [`PortSelector`]
88/// The bytes are the exact bytes that are uploaded to the board. That means it should be a binary file, and *not* contain
89/// ELF headers or similar
90/// Exit with an exit code of 1 when the upload fails.
91///
92/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
93pub fn upload_or_stop(port: PortSelector, file: impl AsRef<[u8]>, dry_run: bool) -> PathBuf {
94    match upload(port, file.as_ref(), dry_run) {
95        Err(e) => {
96            eprintln!("{e:?}");
97            exit(1);
98        }
99        Ok(i) => i,
100    }
101}
102
103/// Upload (already read) bytes to a connected board. Select which serial port the board is on with the [`PortSelector`]
104/// The bytes are the exact bytes that are uploaded to the board. That means it should be a binary file, and *not* contain
105/// ELF headers or similar
106/// Returns an error when the upload fails.
107///
108/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
109pub fn upload(port: PortSelector, file: impl AsRef<[u8]>, dry_run: bool) -> Result<PathBuf> {
110    upload_internal(port, file.as_ref(), dry_run)
111}
112
113fn upload_internal(port: PortSelector<'_>, file: &[u8], dry_run: bool) -> Result<PathBuf> {
114    if dry_run && matches!(port, PortSelector::SearchAll) {
115        bail!("can't use dry_run in SearchAll mode");
116    }
117
118    let (ports_to_try, stop_after_first_error): (Vec<Result<Serial>>, bool) = match port {
119        PortSelector::SearchFirst => (
120            selector::all_serial_ports()
121                .map(PathBuf::from)
122                .map(Serial::open)
123                .collect(),
124            true,
125        ),
126        PortSelector::SearchAll => (
127            selector::all_serial_ports()
128                .map(PathBuf::from)
129                .map(Serial::open)
130                .collect(),
131            false,
132        ),
133        PortSelector::ChooseInteractive => (
134            vec![Serial::open(PathBuf::from(selector::choose_interactive()?))],
135            true,
136        ),
137        PortSelector::Named(n) => (vec![Serial::open(Path::new(n).to_path_buf())], false),
138        PortSelector::AutoManufacturer => (
139            vec![Serial::open(PathBuf::from(
140                selector::find_available_serial_port_by_id()?,
141            ))],
142            true,
143        ),
144    };
145
146    let mut errors = Vec::new();
147    let num_ports = ports_to_try.len();
148
149    for i in ports_to_try {
150        let mut port = match i {
151            Ok(i) => i,
152            Err(e) => {
153                if stop_after_first_error || num_ports == 1 {
154                    return Err(e);
155                }
156                eprintln!("WARNING: {e}");
157                errors.push(e);
158                continue;
159            }
160        };
161
162        if dry_run {
163            return Ok(port.path);
164        }
165
166        if let Err(e) = port
167            .try_do_upload(file)
168            .wrap_err_with(|| format!("failed to upload to port {:?}", port.path))
169        {
170            if stop_after_first_error || num_ports == 1 {
171                return Err(e);
172            }
173            eprintln!("WARNING: {e}");
174            errors.push(e);
175            continue;
176        }
177        return Ok(port.path);
178    }
179
180    Err(eyre!(
181        "uploading failed because none of the ports tried worked (see previous warnings)"
182    ))
183}