1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use crate::serial::Serial;
use crate::{selector, PortSelector};
use color_eyre::eyre::{bail, eyre, Context};
use color_eyre::Result;
use std::fs::read;
use std::path::{Path, PathBuf};
use std::process::{exit, Command};

fn copy_object(source: &Path, target: &Path) -> Result<()> {
    if Command::new("rust-objcopy").output().is_err() {
        bail!(
            "rust-objcopy not found, try installing cargo-binutils or refer to the course website"
        );
    }

    let op = Command::new("rust-objcopy")
        .arg("-O")
        .arg("binary")
        .arg(source)
        .arg(target)
        .output()
        .wrap_err("failed to run rust-objcopy")?;

    println!("creating binary file at {target:?}");

    if !op.status.success() {
        bail!(
            "running rust-objcopy failed: {}",
            String::from_utf8_lossy(&op.stderr)
        );
    }

    Ok(())
}

fn read_file(file: &Path) -> Result<Vec<u8>> {
    let mut target = file.to_path_buf();
    target.set_extension("bin");

    println!("converting elf file to bin file");
    copy_object(file, &target)?;

    println!("reading binary file");
    read(target).wrap_err("failed to read converted binary file to send to board")
}

/// Upload a file to a connected board. Select which serial port the board is on with the [`PortSelector`].
/// The file is expected to be the compiled `.elf` file created by cargo/rustc
/// Exit with an exit code of 1 when the upload fails.
///
/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
pub fn upload_file_or_stop(port: PortSelector, file: Option<impl AsRef<Path>>) -> PathBuf {
    if let Some(file) = file {
        match read_file(file.as_ref())
            .wrap_err_with(|| format!("failed to read from file {:?}", file.as_ref()))
        {
            Ok(i) => upload_or_stop(port, i, false),
            Err(e) => {
                eprintln!("{e:?}");
                exit(1);
            }
        }
    } else {
        upload_or_stop(port, [], true)
    }
}

/// Upload a file to a connected board. Select which serial port the board is on with the [`PortSelector`]
/// The file is expected to be the compiled `.elf` file created by cargo/rustc
/// Returns an error when the upload fails.
///
/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
pub fn upload_file(port: PortSelector, file: Option<impl AsRef<Path>>) -> Result<PathBuf> {
    upload(
        port,
        file.as_ref()
            .map(|f| {
                read_file(f.as_ref())
                    .wrap_err_with(|| format!("failed to read from file {:?}", f.as_ref()))
            })
            .transpose()?
            .unwrap_or_default(),
        file.is_none(),
    )
}

/// Upload (already read) bytes to a connected board. Select which serial port the board is on with the [`PortSelector`]
/// The bytes are the exact bytes that are uploaded to the board. That means it should be a binary file, and *not* contain
/// ELF headers or similar
/// Exit with an exit code of 1 when the upload fails.
///
/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
pub fn upload_or_stop(port: PortSelector, file: impl AsRef<[u8]>, dry_run: bool) -> PathBuf {
    match upload(port, file.as_ref(), dry_run) {
        Err(e) => {
            eprintln!("{e:?}");
            exit(1);
        }
        Ok(i) => i,
    }
}

/// Upload (already read) bytes to a connected board. Select which serial port the board is on with the [`PortSelector`]
/// The bytes are the exact bytes that are uploaded to the board. That means it should be a binary file, and *not* contain
/// ELF headers or similar
/// Returns an error when the upload fails.
///
/// Returns a path to a serial port over which uploading happened. This path can be used to communicate with the board.
pub fn upload(port: PortSelector, file: impl AsRef<[u8]>, dry_run: bool) -> Result<PathBuf> {
    upload_internal(port, file.as_ref(), dry_run)
}

fn upload_internal(port: PortSelector<'_>, file: &[u8], dry_run: bool) -> Result<PathBuf> {
    if dry_run && matches!(port, PortSelector::SearchAll) {
        bail!("can't use dry_run in SearchAll mode");
    }

    let (ports_to_try, stop_after_first_error): (Vec<Result<Serial>>, bool) = match port {
        PortSelector::SearchFirst => (
            selector::all_serial_ports()
                .map(PathBuf::from)
                .map(Serial::open)
                .collect(),
            true,
        ),
        PortSelector::SearchAll => (
            selector::all_serial_ports()
                .map(PathBuf::from)
                .map(Serial::open)
                .collect(),
            false,
        ),
        PortSelector::ChooseInteractive => (
            vec![Serial::open(PathBuf::from(selector::choose_interactive()?))],
            true,
        ),
        PortSelector::Named(n) => (vec![Serial::open(Path::new(n).to_path_buf())], false),
        PortSelector::AutoManufacturer => (
            vec![Serial::open(PathBuf::from(
                selector::find_available_serial_port_by_id()?,
            ))],
            true,
        ),
    };

    let mut errors = Vec::new();
    let num_ports = ports_to_try.len();

    for i in ports_to_try {
        let mut port = match i {
            Ok(i) => i,
            Err(e) => {
                if stop_after_first_error || num_ports == 1 {
                    return Err(e);
                }
                eprintln!("WARNING: {e}");
                errors.push(e);
                continue;
            }
        };

        if dry_run {
            return Ok(port.path);
        }

        if let Err(e) = port
            .try_do_upload(file)
            .wrap_err_with(|| format!("failed to upload to port {:?}", port.path))
        {
            if stop_after_first_error || num_ports == 1 {
                return Err(e);
            }
            eprintln!("WARNING: {e}");
            errors.push(e);
            continue;
        }
        return Ok(port.path);
    }

    Err(eyre!(
        "uploading failed because none of the ports tried worked (see previous warnings)"
    ))
}