Skip to content

Commit 65f7873

Browse files
authored
Merge pull request #68 from esp-rs/save-image
add cargo-espflash subcommand to save the generated image to disk
2 parents 9dec55e + 1f95f8f commit 65f7873

File tree

11 files changed

+264
-69
lines changed

11 files changed

+264
-69
lines changed

cargo-espflash/src/error.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,31 @@ pub enum Error {
4444
#[error(transparent)]
4545
#[diagnostic(transparent)]
4646
UnsupportedTarget(UnsupportedTargetError),
47-
#[error("No target specified in cargo configuration")]
47+
#[error(transparent)]
48+
#[diagnostic(transparent)]
49+
NoTarget(#[from] NoTargetError),
50+
#[error("No serial port specified in arguments or config")]
4851
#[diagnostic(
49-
code(cargo_espflash::no_target),
50-
help("Specify the target in `.cargo/config.toml`, the {} support the following targets: {}", chip, chip.supported_targets().join(", "))
52+
code(cargo_espflash::no_serial),
53+
help("Add a command line option with the serial port to use")
5154
)]
52-
NoTarget { chip: Chip },
55+
NoSerial,
56+
#[error("Failed to detect chip for target {0}")]
57+
#[diagnostic(
58+
code(cargo_espflash::unknown_target),
59+
help(
60+
"The following targets are recognized:
61+
- ESP8266: {}
62+
- ESP32: {}
63+
- ESP32-S2: {}
64+
- ESP32-C3: {}",
65+
Chip::Esp8266.supported_targets().join(", "),
66+
Chip::Esp32.supported_targets().join(", "),
67+
Chip::Esp32s2.supported_targets().join(", "),
68+
Chip::Esp32c3.supported_targets().join(", ")
69+
)
70+
)]
71+
UnknownTarget(String),
5372
}
5473

5574
#[derive(Debug)]
@@ -143,3 +162,29 @@ impl UnsupportedTargetError {
143162
self.chip.supported_targets().join(", ")
144163
}
145164
}
165+
166+
#[derive(Debug, Error)]
167+
#[error("No target specified in cargo configuration")]
168+
pub struct NoTargetError {
169+
chip: Option<Chip>,
170+
}
171+
172+
impl NoTargetError {
173+
pub fn new(chip: Option<Chip>) -> Self {
174+
NoTargetError { chip }
175+
}
176+
}
177+
178+
impl Diagnostic for NoTargetError {
179+
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
180+
Some(Box::new("cargo_espflash::no_target"))
181+
}
182+
183+
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
184+
Some(Box::new(match &self.chip {
185+
Some(chip) => format!("Specify the target in `.cargo/config.toml`, the {} support the following targets: {}", chip, chip.supported_targets().join(", ")),
186+
None => "Specify the target in `.cargo/config.toml`".into(),
187+
}
188+
))
189+
}
190+
}

cargo-espflash/src/main.rs

Lines changed: 158 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ use std::{
66
};
77

88
use cargo_metadata::Message;
9-
use clap::{App, Arg, SubCommand};
9+
use clap::{App, Arg, ArgMatches, SubCommand};
1010
use error::Error;
11-
use espflash::{Chip, Config, Flasher, PartitionTable};
11+
use espflash::{Chip, Config, FirmwareImage, Flasher, PartitionTable};
1212
use miette::{IntoDiagnostic, Result, WrapErr};
1313
use monitor::monitor;
1414
use package_metadata::CargoEspFlashMeta;
1515
use serial::{BaudRate, FlowControl, SerialPort};
1616

17+
use crate::cargo_config::CargoConfig;
18+
use crate::error::NoTargetError;
1719
use crate::{cargo_config::parse_cargo_config, error::UnsupportedTargetError};
1820

1921
mod cargo_config;
@@ -25,6 +27,27 @@ mod package_metadata;
2527
fn main() -> Result<()> {
2628
miette::set_panic_hook();
2729

30+
let build_args = [
31+
Arg::with_name("release")
32+
.long("release")
33+
.help("Build the application using the release profile"),
34+
Arg::with_name("example")
35+
.long("example")
36+
.takes_value(true)
37+
.value_name("EXAMPLE")
38+
.help("Example to build and flash"),
39+
Arg::with_name("features")
40+
.long("features")
41+
.use_delimiter(true)
42+
.takes_value(true)
43+
.value_name("FEATURES")
44+
.help("Comma delimited list of build features"),
45+
];
46+
let connect_args = [Arg::with_name("serial")
47+
.takes_value(true)
48+
.value_name("SERIAL")
49+
.help("Serial port connected to target device")];
50+
2851
let mut app = App::new(env!("CARGO_PKG_NAME"))
2952
.bin_name("cargo")
3053
.subcommand(
@@ -34,40 +57,21 @@ fn main() -> Result<()> {
3457
.arg(
3558
Arg::with_name("board_info")
3659
.long("board-info")
37-
.help("Display the connected board's information"),
60+
.help("Display the connected board's information (deprecated, use the `board-info` subcommand instead)"),
3861
)
62+
.args(&build_args)
3963
.arg(
4064
Arg::with_name("ram")
4165
.long("ram")
4266
.help("Load the application to RAM instead of Flash"),
4367
)
44-
.arg(
45-
Arg::with_name("release")
46-
.long("release")
47-
.help("Build the application using the release profile"),
48-
)
4968
.arg(
5069
Arg::with_name("bootloader")
5170
.long("bootloader")
5271
.takes_value(true)
5372
.value_name("PATH")
5473
.help("Path to a binary (.bin) bootloader file"),
5574
)
56-
.arg(
57-
Arg::with_name("example")
58-
.long("example")
59-
.takes_value(true)
60-
.value_name("EXAMPLE")
61-
.help("Example to build and flash"),
62-
)
63-
.arg(
64-
Arg::with_name("features")
65-
.long("features")
66-
.use_delimiter(true)
67-
.takes_value(true)
68-
.value_name("FEATURES")
69-
.help("Comma delimited list of build features"),
70-
)
7175
.arg(
7276
Arg::with_name("partition_table")
7377
.long("partition-table")
@@ -82,16 +86,30 @@ fn main() -> Result<()> {
8286
.value_name("SPEED")
8387
.help("Baud rate at which to flash target device"),
8488
)
85-
.arg(
86-
Arg::with_name("serial")
87-
.takes_value(true)
88-
.value_name("SERIAL")
89-
.help("Serial port connected to target device"),
90-
)
89+
.args(&connect_args)
9190
.arg(
9291
Arg::with_name("monitor")
9392
.long("monitor")
9493
.help("Open a serial monitor after flashing"),
94+
)
95+
.subcommand(
96+
SubCommand::with_name("save-image")
97+
.version(env!("CARGO_PKG_VERSION"))
98+
.about("Save the image to disk instead of flashing to device")
99+
.arg(
100+
Arg::with_name("file")
101+
.takes_value(true)
102+
.required(true)
103+
.value_name("FILE")
104+
.help("File name to save the generated image to"),
105+
)
106+
.args(&build_args),
107+
)
108+
.subcommand(
109+
SubCommand::with_name("board-info")
110+
.version(env!("CARGO_PKG_VERSION"))
111+
.about("Display the connected board's information")
112+
.args(&connect_args),
95113
),
96114
);
97115

@@ -106,18 +124,30 @@ fn main() -> Result<()> {
106124

107125
let config = Config::load();
108126
let metadata = CargoEspFlashMeta::load("Cargo.toml")?;
127+
let cargo_config = parse_cargo_config(".")?;
128+
129+
match matches.subcommand() {
130+
("board-info", Some(matches)) => board_info(matches, config, metadata, cargo_config),
131+
("save-image", Some(matches)) => save_image(matches, config, metadata, cargo_config),
132+
_ => flash(matches, config, metadata, cargo_config),
133+
}
134+
}
109135

136+
fn get_serial_port(matches: &ArgMatches, config: &Config) -> Result<String, Error> {
110137
// The serial port must be specified, either as a command-line argument or in
111138
// the cargo configuration file. In the case that both have been provided the
112139
// command-line argument will take precedence.
113-
let port = if let Some(serial) = matches.value_of("serial") {
114-
serial.to_string()
115-
} else if let Some(serial) = config.connection.serial {
116-
serial
140+
if let Some(serial) = matches.value_of("serial") {
141+
Ok(serial.to_string())
142+
} else if let Some(serial) = &config.connection.serial {
143+
Ok(serial.into())
117144
} else {
118-
app.print_help().into_diagnostic()?;
119-
exit(0);
120-
};
145+
Err(Error::NoSerial)
146+
}
147+
}
148+
149+
fn connect(matches: &ArgMatches, config: &Config) -> Result<Flasher> {
150+
let port = get_serial_port(matches, config)?;
121151

122152
// Attempt to open the serial port and set its initial baud rate.
123153
println!("Serial port: {}", port);
@@ -144,19 +174,29 @@ fn main() -> Result<()> {
144174
// Connect the Flasher to the target device and print the board information
145175
// upon connection. If the '--board-info' flag has been provided, we have
146176
// nothing left to do so exit early.
147-
let mut flasher = Flasher::connect(serial, speed)?;
177+
Ok(Flasher::connect(serial, speed)?)
178+
}
179+
180+
fn flash(
181+
matches: &ArgMatches,
182+
config: Config,
183+
metadata: CargoEspFlashMeta,
184+
cargo_config: CargoConfig,
185+
) -> Result<()> {
186+
// Connect the Flasher to the target device and print the board information
187+
// upon connection. If the '--board-info' flag has been provided, we have
188+
// nothing left to do so exit early.
189+
let mut flasher = connect(matches, &config)?;
148190
flasher.board_info()?;
149191

150192
if matches.is_present("board_info") {
151193
return Ok(());
152194
}
153195

154-
let release = matches.is_present("release");
155-
let example = matches.value_of("example");
156-
let features = matches.value_of("features");
196+
let build_options = BuildOptions::from_args(matches);
157197

158-
let path =
159-
build(release, example, features, flasher.chip()).wrap_err("Failed to build project")?;
198+
let path = build(build_options, &cargo_config, Some(flasher.chip()))
199+
.wrap_err("Failed to build project")?;
160200

161201
// If the '--bootloader' option is provided, load the binary file at the
162202
// specified path.
@@ -205,38 +245,56 @@ fn main() -> Result<()> {
205245
Ok(())
206246
}
207247

208-
fn build(
248+
struct BuildOptions<'a> {
209249
release: bool,
210-
example: Option<&str>,
211-
features: Option<&str>,
212-
chip: Chip,
250+
example: Option<&'a str>,
251+
features: Option<&'a str>,
252+
}
253+
254+
impl<'a> BuildOptions<'a> {
255+
pub fn from_args(args: &'a ArgMatches) -> Self {
256+
BuildOptions {
257+
release: args.is_present("release"),
258+
example: args.value_of("example"),
259+
features: args.value_of("features"),
260+
}
261+
}
262+
}
263+
264+
fn build(
265+
build_options: BuildOptions,
266+
cargo_config: &CargoConfig,
267+
chip: Option<Chip>,
213268
) -> Result<PathBuf> {
214269
// The 'build-std' unstable cargo feature is required to enable
215270
// cross-compilation. If it has not been set then we cannot build the
216271
// application.
217-
let cargo_config = parse_cargo_config(".")?;
218272
if !cargo_config.has_build_std() {
219273
return Err(Error::NoBuildStd.into());
220274
};
221275

222-
let target = cargo_config.target().ok_or(Error::NoTarget { chip })?;
223-
if !chip.supports_target(target) {
224-
return Err(Error::UnsupportedTarget(UnsupportedTargetError::new(target, chip)).into());
276+
let target = cargo_config
277+
.target()
278+
.ok_or_else(|| NoTargetError::new(chip))?;
279+
if let Some(chip) = chip {
280+
if !chip.supports_target(target) {
281+
return Err(Error::UnsupportedTarget(UnsupportedTargetError::new(target, chip)).into());
282+
}
225283
}
226284

227285
// Build the list of arguments to pass to 'cargo build'.
228286
let mut args = vec![];
229287

230-
if release {
288+
if build_options.release {
231289
args.push("--release");
232290
}
233291

234-
if let Some(example) = example {
292+
if let Some(example) = build_options.example {
235293
args.push("--example");
236294
args.push(example);
237295
}
238296

239-
if let Some(features) = features {
297+
if let Some(features) = build_options.features {
240298
args.push("--features");
241299
args.push(features);
242300
}
@@ -281,7 +339,7 @@ fn build(
281339
}
282340

283341
// Check if the command succeeded, otherwise return an error. Any error messages
284-
// occuring during the build are shown above, when the compiler messages are
342+
// occurring during the build are shown above, when the compiler messages are
285343
// rendered.
286344
if !output.status.success() {
287345
exit_with_process_status(output.status);
@@ -295,6 +353,52 @@ fn build(
295353
Ok(artifact_path)
296354
}
297355

356+
fn save_image(
357+
matches: &ArgMatches,
358+
_config: Config,
359+
_metadata: CargoEspFlashMeta,
360+
cargo_config: CargoConfig,
361+
) -> Result<()> {
362+
let target = cargo_config
363+
.target()
364+
.ok_or_else(|| NoTargetError::new(None))?;
365+
let chip = Chip::from_target(target).ok_or_else(|| Error::UnknownTarget(target.into()))?;
366+
let build_options = BuildOptions::from_args(matches);
367+
368+
let path = build(build_options, &cargo_config, Some(chip))?;
369+
let elf_data = fs::read(path).into_diagnostic()?;
370+
371+
let image = FirmwareImage::from_data(&elf_data)?;
372+
373+
let flash_image = chip.get_flash_image(&image, None, None, None)?;
374+
let parts: Vec<_> = flash_image.ota_segments().collect();
375+
376+
let out_path = matches.value_of("file").unwrap();
377+
378+
match parts.as_slice() {
379+
[single] => fs::write(out_path, &single.data).into_diagnostic()?,
380+
parts => {
381+
for part in parts {
382+
let part_path = format!("{:#x}_{}", part.addr, out_path);
383+
fs::write(part_path, &part.data).into_diagnostic()?
384+
}
385+
}
386+
}
387+
388+
Ok(())
389+
}
390+
391+
fn board_info(
392+
matches: &ArgMatches,
393+
config: Config,
394+
_metadata: CargoEspFlashMeta,
395+
_cargo_config: CargoConfig,
396+
) -> Result<()> {
397+
let mut flasher = connect(matches, &config)?;
398+
flasher.board_info()?;
399+
Ok(())
400+
}
401+
298402
#[cfg(unix)]
299403
fn exit_with_process_status(status: ExitStatus) -> ! {
300404
use std::os::unix::process::ExitStatusExt;

0 commit comments

Comments
 (0)