diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a00f34b..2465e30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,9 @@ jobs: - run: sudo apt-get update && sudo apt-get install -y libudev-dev - run: rustup toolchain install nightly --profile minimal --component rustfmt,clippy - run: cargo +nightly fmt --all -- --check - - run: cargo +nightly clippy -- -D warnings + - run: cargo +nightly clippy --all-targets -- -D warnings - run: cargo +nightly build + - run: cargo +nightly test - run: cargo +nightly build --release ch32-v003: diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d69ed..7fad877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4. ### Added +- `tinyboot discover` lists the addressable devices on a shared bus by unique id, with mode, versions and compatibility key, walking the id tree with `Select`. +- `--uid ` on every device command picks one device by unique id (a unique prefix of whole bytes is enough). Without it the CLI selects the only device, refuses when several answer, and uses a device without addressing as it is. - `tinyboot baud ` stores the bootloader's baud and resets the device back into its current mode; in bootloader mode it reconnects at the new rate to check the bootloader answers. `tinyboot baud default` restores the build default. ### Changed +- Port auto-detection probes with an empty-prefix `Select` instead of Info, so it finds addressable devices too. - `tinyboot reset` and `tinyboot flash --reset` wait for the bootloader's reply and report a refused reset to the app. - `tinyboot flash` writes in chunks of the device's `max_payload` and, when the bootloader has a compatibility key, refuses an image with another key before erasing. `tinyboot info` prints the stored boot baud, the compatibility key and `max_payload`. diff --git a/cli/README.md b/cli/README.md index 3d2c8ce..d3d3f5a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -18,12 +18,22 @@ cargo install --path cli ## Usage +Every command that talks to a device takes `--port`, `--baud` and `--uid`. `--uid` picks one device on a shared bus by its unique id, in hex as `tinyboot discover` prints it. A unique prefix of whole bytes is enough. Without `--uid` the CLI selects the only device on the bus, and stops with an error when more than one answers. A device without addressing is used as it is. See the [shared bus guide](https://openservocore.github.io/tinyboot/shared-bus.html). + +### `tinyboot discover` + +List the addressable devices on the bus by unique id, with mode, versions and compatibility key. With `--uid`, only the devices under that prefix. + +```sh +tinyboot discover [--port /dev/ttyUSB0] [--baud 115200] [--uid 10] +``` + ### `tinyboot info` Query device info (capacity, erase size, versions, mode). ```sh -tinyboot info [--port /dev/ttyUSB0] [--baud 115200] +tinyboot info [--port /dev/ttyUSB0] [--baud 115200] [--uid 1032] ``` ### `tinyboot flash` @@ -69,7 +79,7 @@ tinyboot bin firmware.elf -o firmware.bin ## Auto-detection -If `--port` is omitted, the CLI probes USB serial ports (usbmodem, ttyACM, ttyUSB) by sending an Info command with a 100ms timeout. Non-USB serial ports are skipped. Both the bootloader and apps running `poll()` respond to Info, so auto-detection works in either mode. +If `--port` is omitted, the CLI probes USB serial ports (usbmodem, ttyACM, ttyUSB) by sending a `Select` with an empty prefix and listening for any reply. Non-USB serial ports are skipped. Addressable devices answer with their id, others with `Unsupported`, in the bootloader and in apps running `poll()` alike, so auto-detection works in either mode. ## Single-wire bus echo diff --git a/cli/src/client.rs b/cli/src/client.rs index 869c0c8..13d5829 100644 --- a/cli/src/client.rs +++ b/cli/src/client.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use log::debug; use tinyboot_protocol::crc::{CRC_INIT, crc16}; use tinyboot_protocol::frame::{EraseData, Flags, Frame, InfoData}; @@ -7,6 +9,38 @@ use tinyboot_protocol::{COMPAT_UNSET, Cmd, ResetFlags, Status, WriteFlags}; /// and the device's `max_payload`. const HOST_PAYLOAD: usize = 1024; +/// How long a read waits for a reply. +pub const TIMEOUT: Duration = Duration::from_secs(5); + +/// Longest unique id a device may have, in bytes. +const MAX_UID_LEN: usize = 16; + +/// The bus: a byte stream whose read timeout can be set. +pub trait Link: embedded_io::Read + embedded_io::Write { + fn set_timeout(&mut self, timeout: Duration); +} + +/// What one `Select` heard. +#[derive(Debug, PartialEq)] +pub enum Heard { + Nothing, + /// One device answered with this id. + One(Vec), + /// A device without addressing answered, refusing `Select`. + Unaddressed, + /// Several devices answered at once. + Garble, +} + +/// Devices a [`Client::discover`] found. +#[derive(Debug, PartialEq)] +pub enum Found { + /// Unique ids, each selectable with its full id. + Ids(Vec>), + /// One device without addressing. + Unaddressed, +} + #[derive(Debug, thiserror::Error)] pub enum FlashError { #[error("transport I/O error")] @@ -24,6 +58,97 @@ pub enum FlashError { image.map_or("missing".to_string(), |k| format!("{k:#010X}")) )] Incompatible { device: u32, image: Option }, + #[error("no device answered")] + NoDevice, + #[error("more than one device answered; pick one with --uid (see `tinyboot discover`)")] + SeveralDevices, + #[error("no device with uid {0}")] + NoSuchUid(String), + #[error("uid {0} matches more than one device; give more of it")] + AmbiguousUid(String), + #[error("the device is not addressable; drop --uid")] + NotAddressable, + #[error("two devices answer with one uid")] + DuplicateUid, +} + +/// Lowercase hex of an id, least significant byte first, as stored. +pub fn uid_hex(uid: &[u8]) -> String { + uid.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Parse an id, or a prefix of one, written as [`uid_hex`] prints it. +pub fn parse_uid(hex: &str) -> Result, String> { + if hex.len() % 2 != 0 || hex.len() > 2 * MAX_UID_LEN { + return Err(format!( + "bad uid {hex}: whole bytes, 2 hex digits each, at most {MAX_UID_LEN}" + )); + } + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| format!("bad uid {hex}"))) + .collect() +} + +/// Bit `k` of an id is `id[k / 8] >> (k % 8) & 1`. +fn bits_of(bytes: &[u8]) -> Vec { + (0..bytes.len() * 8) + .map(|k| bytes[k / 8] >> (k % 8) & 1 != 0) + .collect() +} + +/// What a probe's bytes hold: replies from one device read clean, from +/// several at once garble. The host's own request, echoed on one-wire +/// setups, is skipped. +fn hear(bytes: &[u8]) -> Heard { + let mut r = SliceReader { bytes, pos: 0 }; + let mut framed = 0; + let mut replies = Vec::new(); + while r.pos < bytes.len() { + let mut f = Frame::::default(); + match f.read(&mut r) { + Ok(Status::Ok) => {} + _ => return Heard::Garble, + } + framed += 12 + f.len as usize; + if f.status != Status::Request { + replies.push(f); + } + } + // Bytes the sync scan skipped are another device's. + if framed != bytes.len() { + return Heard::Garble; + } + match replies.as_slice() { + [] => Heard::Nothing, + [f] if f.status == Status::Ok => { + Heard::One(unsafe { f.data.raw[..f.len as usize].to_vec() }) + } + [f] if f.status == Status::Unsupported => Heard::Unaddressed, + _ => Heard::Garble, + } +} + +struct SliceReader<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl embedded_io::ErrorType for SliceReader<'_> { + type Error = embedded_io::ErrorKind; +} + +impl embedded_io::Read for SliceReader<'_> { + // An error at the end: the sync scan reads on through `Ok(0)`. + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos == self.bytes.len() { + return Err(embedded_io::ErrorKind::TimedOut); + } + let n = buf.len().min(self.bytes.len() - self.pos); + buf[..n].copy_from_slice(&self.bytes[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } } /// Compatibility key in an image's tail. The tail starts word-aligned, so @@ -47,17 +172,179 @@ pub struct DeviceInfo { pub max_payload: u16, } -pub struct Client { +pub struct Client { transport: T, frame: Frame, + /// How long a `Select` listens for replies. + probe_timeout: Duration, + seed: u8, + /// Id of the selected device; `None` before a select or when unaddressed. + uid: Option>, } -impl Client { - pub fn new(transport: T) -> Self { +impl Client { + /// `probe_timeout` must cover the longest `Select` reply: a 15 + /// byte-time slot and the reply itself. + pub fn new(transport: T, probe_timeout: Duration) -> Self { Self { transport, frame: Frame::default(), + probe_timeout, + seed: 0, + uid: None, + } + } + + /// Id of the selected device, `None` if it is unaddressed. + pub fn uid(&self) -> Option<&[u8]> { + self.uid.as_deref() + } + + /// Send `Select` for a `prefix` of id bits and listen for replies. + pub fn select(&mut self, prefix: &[bool]) -> Result { + // A fresh slot draw each time, so two devices that answered in + // unison do not again. + self.seed = self.seed.wrapping_add(1); + let n = prefix.len().div_ceil(8); + let mut data = vec![0u8; 2 + n]; + data[0] = self.seed; + data[1] = prefix.len() as u8; + for (k, &b) in prefix.iter().enumerate() { + data[2 + k / 8] |= (b as u8) << (k % 8); } + self.frame.cmd = Cmd::Select; + self.frame.set_addr(0); + self.frame.flags = Flags { raw: 0 }; + self.frame.len = data.len() as u16; + unsafe { self.frame.data.raw[..data.len()].copy_from_slice(&data) }; + self.frame.status = Status::Request; + debug!(">> Select seed={} prefix_len={}", self.seed, prefix.len()); + self.frame + .send(&mut self.transport) + .map_err(|_| FlashError::Io)?; + let bytes = self.listen(); + let heard = hear(&bytes); + debug!("<< {} bytes: {heard:?}", bytes.len()); + Ok(heard) + } + + /// Every byte until the line stays quiet for `probe_timeout`. + fn listen(&mut self) -> Vec { + self.transport.set_timeout(self.probe_timeout); + let mut bytes = Vec::new(); + let mut buf = [0u8; 256]; + while let Ok(n @ 1..) = self.transport.read(&mut buf) { + bytes.extend_from_slice(&buf[..n]); + } + self.transport.set_timeout(TIMEOUT); + bytes + } + + /// Deselect every device. + pub fn deselect_all(&mut self) -> Result<(), FlashError> { + // Longer than any id, so it matches none. + let none = vec![false; 8 * MAX_UID_LEN + 1]; + self.select(&none)?; + self.uid = None; + Ok(()) + } + + /// Select the one device whose id starts with `uid`, or the only device + /// on the bus when `uid` is `None`. A device without addressing is + /// used as it is. + pub fn attach(&mut self, uid: Option<&[u8]>) -> Result<(), FlashError> { + let prefix = bits_of(uid.unwrap_or_default()); + match self.select(&prefix)? { + Heard::One(id) => { + self.uid = Some(id); + Ok(()) + } + Heard::Unaddressed if uid.is_none() => { + self.uid = None; + Ok(()) + } + Heard::Unaddressed => Err(FlashError::NotAddressable), + Heard::Nothing => Err(match uid { + Some(u) => FlashError::NoSuchUid(uid_hex(u)), + None => FlashError::NoDevice, + }), + Heard::Garble => { + self.deselect_all()?; + Err(match uid { + Some(u) => FlashError::AmbiguousUid(uid_hex(u)), + None => FlashError::SeveralDevices, + }) + } + } + } + + /// Find every device whose id starts with `prefix` by walking the id + /// tree: a clean reply under a prefix is a candidate, garble descends + /// one bit, silence ends the branch. Leaves every device deselected. + pub fn discover(&mut self, prefix: &[u8]) -> Result { + let mut ids = Vec::new(); + match self.walk(&mut bits_of(prefix), &mut ids, None) { + Err(FlashError::NotAddressable) => return Ok(Found::Unaddressed), + r => r?, + } + self.deselect_all()?; + ids.sort(); + Ok(Found::Ids(ids)) + } + + /// `heard`: what this prefix is already known to hold, else probe it. + fn walk( + &mut self, + prefix: &mut Vec, + ids: &mut Vec>, + heard: Option, + ) -> Result<(), FlashError> { + let heard = match heard { + Some(h) => h, + None => self.select(prefix)?, + }; + match heard { + Heard::Nothing => return Ok(()), + Heard::Unaddressed => return Err(FlashError::NotAddressable), + Heard::One(id) if self.confirm(prefix, &id)? => { + ids.push(id); + return Ok(()); + } + _ => {} + } + if prefix.len() >= 8 * MAX_UID_LEN { + return Err(FlashError::DuplicateUid); + } + prefix.push(false); + let zero = self.select(prefix)?; + // Nothing under 0 leaves every device under 1: skip that probe. + let one = (zero == Heard::Nothing).then_some(Heard::Garble); + self.walk(prefix, ids, Some(zero))?; + prefix.pop(); + prefix.push(true); + self.walk(prefix, ids, one)?; + prefix.pop(); + Ok(()) + } + + /// Two devices in unison can read back as one clean reply. A real one + /// answers alone under the child prefix its id continues with, and + /// nothing answers under the other. + fn confirm(&mut self, prefix: &mut Vec, id: &[u8]) -> Result { + let bits = bits_of(id); + if !bits.starts_with(prefix) { + return Ok(false); + } + let Some(&next) = bits.get(prefix.len()) else { + return Ok(true); + }; + prefix.push(next); + let alone = self.select(prefix)? == Heard::One(id.to_vec()); + prefix.pop(); + prefix.push(!next); + let other = self.select(prefix)? == Heard::Nothing; + prefix.pop(); + Ok(alone && other) } /// Send current frame as a request and read the response. @@ -315,6 +602,262 @@ impl Client { mod tests { use super::*; + /// Devices on one wire. Replies to a `Select` from two or more devices + /// overlap and arrive as their bitwise AND, as on a wired-AND line. + struct Bus { + /// `(id, selected)`; an empty id is a device without addressing. + devices: Vec<(Vec, bool)>, + /// Request bytes so far: `send` writes the body, then the CRC. + tx: Vec, + rx: Vec, + selects: usize, + } + + impl Bus { + fn new(ids: &[&[u8]]) -> Self { + Self { + devices: ids.iter().map(|id| (id.to_vec(), false)).collect(), + tx: Vec::new(), + rx: Vec::new(), + selects: 0, + } + } + + fn reply(cmd: Cmd, status: Status, data: &[u8]) -> Vec { + let mut f = Frame::<64>::default(); + f.cmd = cmd; + f.status = status; + f.len = data.len() as u16; + unsafe { f.data.raw[..data.len()].copy_from_slice(data) }; + let mut out = Wire(Vec::new()); + f.send(&mut out).unwrap(); + out.0 + } + } + + struct Wire(Vec); + + impl embedded_io::ErrorType for Wire { + type Error = core::convert::Infallible; + } + + impl embedded_io::Write for Wire { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + } + + impl embedded_io::ErrorType for Bus { + type Error = embedded_io::ErrorKind; + } + + impl embedded_io::Write for Bus { + fn write(&mut self, buf: &[u8]) -> Result { + self.tx.extend_from_slice(buf); + let t = &self.tx; + if t.len() < 10 || t.len() < 12 + u16::from_le_bytes([t[8], t[9]]) as usize { + return Ok(buf.len()); + } + let req = std::mem::take(&mut self.tx); + let mut f = Frame::<64>::default(); + assert_eq!( + f.read(&mut SliceReader { + bytes: &req, + pos: 0 + }), + Ok(Status::Ok) + ); + self.selects += (f.cmd == Cmd::Select) as usize; + let mut replies = Vec::new(); + for (id, selected) in &mut self.devices { + if id.is_empty() { + replies.push(Self::reply(f.cmd, Status::Unsupported, &[])); + continue; + } + if f.cmd != Cmd::Select { + if *selected { + replies.push(Self::reply(f.cmd, Status::Ok, &[0; 18])); + } + continue; + } + let data = unsafe { &f.data.raw }; + let n = data[1] as usize; + let bits = bits_of(id); + *selected = n <= bits.len() + && (0..n).all(|k| bits[k] == (data[2 + k / 8] >> (k % 8) & 1 != 0)); + if *selected { + replies.push(Self::reply(Cmd::Select, Status::Ok, id)); + } + } + let len = replies.iter().map(Vec::len).max().unwrap_or(0); + let mut wire = vec![0xFF; len]; + for r in &replies { + for (w, b) in wire.iter_mut().zip(r) { + *w &= b; + } + } + self.rx = wire; + Ok(buf.len()) + } + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + } + + impl embedded_io::Read for Bus { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.rx.is_empty() { + return Err(embedded_io::ErrorKind::TimedOut); + } + let n = buf.len().min(self.rx.len()); + buf[..n].copy_from_slice(&self.rx[..n]); + self.rx.drain(..n); + Ok(n) + } + } + + impl Link for Bus { + fn set_timeout(&mut self, _: Duration) {} + } + + const IDS: [[u8; 12]; 5] = [ + [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x01, 0x23, 0x45, 0x07, + ], + [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x01, 0x23, 0x45, 0x87, + ], + [0; 12], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0xFF; 12], + ]; + + fn client(ids: &[&[u8]]) -> Client { + Client::new(Bus::new(ids), Duration::ZERO) + } + + #[test] + fn discover_finds_every_id_and_deselects_them() { + let ids: Vec<&[u8]> = IDS.iter().map(|i| &i[..]).collect(); + let mut c = client(&ids); + let Found::Ids(found) = c.discover(&[]).unwrap() else { + panic!("unaddressed"); + }; + let mut want: Vec> = IDS.iter().map(|i| i.to_vec()).collect(); + want.sort(); + assert_eq!(found, want); + assert!(c.transport.devices.iter().all(|(_, s)| !s)); + } + + #[test] + fn discover_descends_a_shared_prefix_in_two_probes_a_bit_at_most() { + // These two differ only in their last bit: 95 shared bits. + let mut c = client(&[&IDS[0], &IDS[1]]); + assert_eq!( + c.discover(&[]).unwrap(), + Found::Ids(vec![IDS[0].to_vec(), IDS[1].to_vec()]) + ); + let probes = c.transport.selects; + // A shared 1 bit costs one probe, a shared 0 bit two. + assert!(probes <= 2 * 96, "{probes} probes"); + } + + #[test] + fn discover_under_a_prefix_finds_only_that_subtree() { + let ids: Vec<&[u8]> = IDS.iter().map(|i| &i[..]).collect(); + let mut c = client(&ids); + let Found::Ids(found) = c.discover(&[0x10, 0x32]).unwrap() else { + panic!("unaddressed"); + }; + assert_eq!(found, vec![IDS[0].to_vec(), IDS[1].to_vec()]); + } + + #[test] + fn discover_reports_a_device_without_addressing() { + let mut c = client(&[&[]]); + assert_eq!(c.discover(&[]).unwrap(), Found::Unaddressed); + } + + #[test] + fn attach_selects_the_only_device() { + let mut c = client(&[&IDS[2]]); + c.attach(None).unwrap(); + assert_eq!(c.uid(), Some(&IDS[2][..])); + assert!(c.transport.devices[0].1); + } + + #[test] + fn attach_without_uid_refuses_several_devices() { + let mut c = client(&[&IDS[0], &IDS[2]]); + assert!(matches!(c.attach(None), Err(FlashError::SeveralDevices))); + assert!(c.transport.devices.iter().all(|(_, s)| !s)); + } + + #[test] + fn attach_by_prefix_selects_one_and_deselects_the_rest() { + let mut c = client(&[&IDS[0], &IDS[2], &IDS[4]]); + c.attach(Some(&[0xFF])).unwrap(); + assert_eq!(c.uid(), Some(&IDS[4][..])); + let selected: Vec = c.transport.devices.iter().map(|d| d.1).collect(); + assert_eq!(selected, [false, false, true]); + assert!(matches!( + c.attach(Some(&[0x10, 0x32])), + Ok(()) if c.uid() == Some(&IDS[0][..]) + )); + } + + #[test] + fn attach_names_a_missing_or_ambiguous_uid() { + let mut c = client(&[&IDS[0], &IDS[1]]); + assert!(matches!(c.attach(Some(&[0x11])), Err(FlashError::NoSuchUid(u)) if u == "11")); + assert!(matches!( + c.attach(Some(&[0x10])), + Err(FlashError::AmbiguousUid(_)) + )); + assert!(matches!( + client(&[]).attach(None), + Err(FlashError::NoDevice) + )); + assert!(matches!( + client(&[&[]]).attach(Some(&[1])), + Err(FlashError::NotAddressable) + )); + } + + #[test] + fn attach_uses_a_device_without_addressing_as_it_is() { + let mut c = client(&[&[]]); + c.attach(None).unwrap(); + assert_eq!(c.uid(), None); + } + + #[test] + fn hear_skips_the_echo_and_flags_trailing_bytes() { + let echo = Bus::reply(Cmd::Select, Status::Request, &[1, 0]); + let one = Bus::reply(Cmd::Select, Status::Ok, &IDS[0]); + assert_eq!( + hear(&[echo.clone(), one.clone()].concat()), + Heard::One(IDS[0].to_vec()) + ); + assert_eq!(hear(&echo), Heard::Nothing); + assert_eq!(hear(&[one.clone(), vec![0x00]].concat()), Heard::Garble); + assert_eq!(hear(&[vec![0x13], one.clone()].concat()), Heard::Garble); + assert_eq!(hear(&[one.clone(), one].concat()), Heard::Garble); + } + + #[test] + fn uid_hex_round_trips() { + assert_eq!(uid_hex(&IDS[0]), "1032547698badcfe01234507"); + assert_eq!(parse_uid("1032547698badcfe01234507").unwrap(), IDS[0]); + assert_eq!(parse_uid("10AB").unwrap(), [0x10, 0xAB]); + assert!(parse_uid("103").is_err()); + assert!(parse_uid("zz").is_err()); + } + #[test] fn image_compat_reads_a_word_aligned_tail() { let mut image = vec![0u8; 10]; diff --git a/cli/src/main.rs b/cli/src/main.rs index 6161033..0ac5389 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,7 +1,7 @@ mod client; mod transport; -use std::time::Instant; +use std::time::{Duration, Instant}; use clap::{Parser, Subcommand}; use indicatif::{ProgressBar, ProgressStyle}; @@ -9,7 +9,7 @@ use object::elf::{PT_LOAD, SHF_ALLOC}; use object::read::elf::{ElfFile32, ProgramHeader as _}; use object::{LittleEndian, Object, ObjectSection, SectionFlags}; -use client::{Client, FlashError}; +use client::{Client, FlashError, Found, Heard, uid_hex}; use tinyboot_protocol::{BaudRate, Status}; use transport::Serial; @@ -31,10 +31,20 @@ struct ConnectionArgs { /// Baud rate #[arg(long, default_value_t = 115200)] baud: u32, + /// Device unique id, in hex as `tinyboot discover` prints it; a unique + /// prefix of whole bytes is enough. Needed when several addressable + /// devices share the bus. + #[arg(long)] + uid: Option, } #[derive(Subcommand)] enum Commands { + /// List the addressable devices on the bus by unique id, with their info + Discover { + #[command(flatten)] + conn: ConnectionArgs, + }, /// Query device info (capacity, payload size, erase size) Info { #[command(flatten)] @@ -103,8 +113,8 @@ fn detect_port(baud: u32) -> Result> { Ok(s) => s, Err(_) => continue, }; - let mut client = Client::new(Serial(serial)); - if client.info().is_ok() { + let mut client = Client::new(Serial(serial), probe_timeout(baud)); + if client.select(&[]).is_ok_and(|h| h != Heard::Nothing) { eprintln!("detected tinyboot on {}", p.port_name); return Ok(p.port_name.clone()); } @@ -119,6 +129,68 @@ fn resolve_port(port: Option, baud: u32) -> Result Duration { + Duration::from_millis(50) + Duration::from_micros(64 * 10 * 1_000_000 / baud as u64) +} + +fn parse_uid_arg(uid: Option<&str>) -> Result>, Box> { + Ok(uid.map(client::parse_uid).transpose()?) +} + +/// Open the port and select the device, as named by `--uid` or the only +/// one on the bus. +fn connect(conn: &ConnectionArgs) -> Result<(Client, String), Box> { + let uid = parse_uid_arg(conn.uid.as_deref())?; + let port = resolve_port(conn.port.clone(), conn.baud)?; + let mut client = Client::new(open_serial(&port, conn.baud)?, probe_timeout(conn.baud)); + client.attach(uid.as_deref())?; + if let Some(id) = client.uid() { + eprintln!("selected {}", uid_hex(id)); + } + Ok((client, port)) +} + +fn print_info(info: &client::DeviceInfo) { + println!("capacity: {} bytes", info.capacity); + println!("erase_size: {} bytes", info.erase_size); + let (bm, bn, bp) = tinyboot_protocol::unpack_version(info.boot_version); + let (am, an, ap) = tinyboot_protocol::unpack_version(info.app_version); + if info.boot_version == 0xFFFF { + println!("boot_version: none"); + } else { + println!("boot_version: {bm}.{bn}.{bp}"); + } + if info.app_version == 0xFFFF { + println!("app_version: none"); + } else { + println!("app_version: {am}.{an}.{ap}"); + } + println!( + "mode: {}", + if info.mode == 0 { "bootloader" } else { "app" } + ); + match BaudRate::from_u8(info.boot_baud) { + Some(b) => println!("boot_baud: {}", b.hz()), + None => println!("boot_baud: default"), + } + if info.compat == tinyboot_protocol::COMPAT_UNSET { + println!("compat: none"); + } else { + println!("compat: {:#010X}", info.compat); + } + println!("max_payload: {} bytes", info.max_payload); +} + +fn version(v: u16) -> String { + if v == 0xFFFF { + return "none".into(); + } + let (m, n, p) = tinyboot_protocol::unpack_version(v); + format!("{m}.{n}.{p}") +} + /// Load firmware from file. If ELF, extract loadable sections into a flat binary /// using physical addresses (LMA). Skips `.uninit.*` sections. /// If raw binary (no ELF magic), use as-is. @@ -245,44 +317,47 @@ fn main() -> Result<(), Box> { env_logger::Builder::new().filter_level(log_level).init(); match cli.command { - Commands::Info { conn } => { + Commands::Discover { conn } => { + let prefix = parse_uid_arg(conn.uid.as_deref())?.unwrap_or_default(); let port = resolve_port(conn.port, conn.baud)?; - let serial = open_serial(&port, conn.baud)?; - let mut client = Client::new(serial); - let info = client.info()?; - println!("capacity: {} bytes", info.capacity); - println!("erase_size: {} bytes", info.erase_size); - let (bm, bn, bp) = tinyboot_protocol::unpack_version(info.boot_version); - let (am, an, ap) = tinyboot_protocol::unpack_version(info.app_version); - if info.boot_version == 0xFFFF { - println!("boot_version: none"); - } else { - println!("boot_version: {bm}.{bn}.{bp}"); - } - if info.app_version == 0xFFFF { - println!("app_version: none"); - } else { - println!("app_version: {am}.{an}.{ap}"); - } - println!( - "mode: {}", - if info.mode == 0 { "bootloader" } else { "app" } - ); - match BaudRate::from_u8(info.boot_baud) { - Some(b) => println!("boot_baud: {}", b.hz()), - None => println!("boot_baud: default"), - } - if info.compat == tinyboot_protocol::COMPAT_UNSET { - println!("compat: none"); - } else { - println!("compat: {:#010X}", info.compat); + let mut client = Client::new(open_serial(&port, conn.baud)?, probe_timeout(conn.baud)); + match client.discover(&prefix)? { + Found::Unaddressed => { + println!("one device, not addressable"); + print_info(&client.info()?); + } + Found::Ids(ids) if ids.is_empty() => println!("no addressable device answered"), + Found::Ids(ids) => { + println!( + "{:<32} {:<10} {:<8} {:<8} compat", + "uid", "mode", "boot", "app" + ); + for id in &ids { + client.attach(Some(id))?; + let info = client.info()?; + let compat = if info.compat == tinyboot_protocol::COMPAT_UNSET { + "none".to_string() + } else { + format!("{:#010X}", info.compat) + }; + println!( + "{:<32} {:<10} {:<8} {:<8} {compat}", + uid_hex(id), + if info.mode == 0 { "bootloader" } else { "app" }, + version(info.boot_version), + version(info.app_version), + ); + } + client.deselect_all()?; + } } - println!("max_payload: {} bytes", info.max_payload); + } + Commands::Info { conn } => { + let (mut client, _) = connect(&conn)?; + print_info(&client.info()?); } Commands::Erase { conn } => { - let port = resolve_port(conn.port, conn.baud)?; - let serial = open_serial(&port, conn.baud)?; - let mut client = Client::new(serial); + let (mut client, _) = connect(&conn)?; let info = client.info()?; if info.mode != 0 { @@ -319,11 +394,9 @@ fn main() -> Result<(), Box> { conn, reset, } => { - let port = resolve_port(conn.port, conn.baud)?; let file_data = std::fs::read(&firmware)?; let fw = load_firmware(&file_data)?; - let serial = open_serial(&port, conn.baud)?; - let mut client = Client::new(serial); + let (mut client, _) = connect(&conn)?; let info = client.info()?; if info.mode != 0 { @@ -365,11 +438,10 @@ fn main() -> Result<(), Box> { } Commands::Baud { rate, conn } => { let baud = parse_baud(&rate)?; - let port = resolve_port(conn.port, conn.baud)?; - let serial = open_serial(&port, conn.baud)?; - let mut client = Client::new(serial); + let (mut client, port) = connect(&conn)?; let bootloader = client.info()?.mode == 0; client.set_baud(baud.map_or(0xFF, |b| b as u8), bootloader, bootloader)?; + let uid = client.uid().map(<[u8]>::to_vec); drop(client); println!("baud stored: {rate}"); @@ -383,9 +455,9 @@ fn main() -> Result<(), Box> { }; // The device answers at the new rate once it has reset. std::thread::sleep(std::time::Duration::from_millis(100)); - let serial = open_serial(&port, baud.hz())?; - let mut client = Client::new(serial); - match client.info() { + let mut client = Client::new(open_serial(&port, baud.hz())?, probe_timeout(baud.hz())); + // The reset deselected it. + match client.attach(uid.as_deref()).and_then(|()| client.info()) { Ok(_) => println!("bootloader answers at {}", baud.hz()), Err(_) => { return Err(format!( @@ -398,9 +470,7 @@ fn main() -> Result<(), Box> { } } Commands::Reset { conn, bootloader } => { - let port = resolve_port(conn.port, conn.baud)?; - let serial = open_serial(&port, conn.baud)?; - let mut client = Client::new(serial); + let (mut client, _) = connect(&conn)?; let in_boot = client.info()?.mode == 0; client.reset(bootloader, in_boot).map_err(refused_app)?; println!( diff --git a/cli/src/transport.rs b/cli/src/transport.rs index 1a377f5..daf7034 100644 --- a/cli/src/transport.rs +++ b/cli/src/transport.rs @@ -20,3 +20,9 @@ impl embedded_io::Write for Serial { std::io::Write::flush(&mut self.0).map_err(|_| embedded_io::ErrorKind::Other) } } + +impl crate::client::Link for Serial { + fn set_timeout(&mut self, timeout: std::time::Duration) { + let _ = self.0.set_timeout(timeout); + } +}