#[allow(unused_imports)]
use log::{debug, warn};
use onerom_config::mcu::{Rp235xChipId, RpVariant};
use onerom_fw_parser::Parser;
use picoboot::cmd::PicobootStatus;
use picoboot::{
Picoboot, PicobootCmd, PicobootCmdId, PicobootXCmd, Reader as PicobootReader, Target,
usb::Timeouts,
};
use std::time::Duration;
use crate::Error;
pub use crate::picobootx::{
Caps, GpioEntry, GpioSetArgs, GpioState, GpioUse, LedSubCmd, SetLedArgs,
};
use crate::picobootx::{
GpioQueryArgs, ONEROM_CAPS_LEN, ONEROM_CMD_ARGS_LEN, ONEROM_CMD_GET_CAPS,
ONEROM_CMD_GPIO_QUERY, ONEROM_CMD_GPIO_SET, ONEROM_CMD_SET_LED, ONEROM_FEAT_GPIO_HOLD,
ONEROM_FEAT_GPIO_QUERY, ONEROM_FEAT_GPIO_SET, ONEROM_MAGIC, PICOBOOT_DIR_IN,
};
use crate::{Device, DeviceState};
pub const FLASH_BASE: u32 = 0x1000_0000;
pub const RAM_BASE: u32 = 0x2000_0000;
pub const FLASH_READ_SIZE_KB: u32 = 64;
pub const FLASH_READ_SIZE_BYTES: u32 = FLASH_READ_SIZE_KB * 1024;
pub const DEFAULT_ONEROM_PICOBOOT_TARGETS: [Target; 3] = [
Target::Rp2350,
Target::Custom {
vid: 0x1209,
pid: 0xF540,
},
Target::Custom {
vid: 0x1209,
pid: 0xF542,
},
];
pub async fn enumerate_devices(
unrecognised: bool,
vid_pid: &[(u16, u16)],
) -> Result<Vec<Device>, Error> {
let targets: Vec<Target> = vid_pid
.iter()
.map(|&(vid, pid)| Target::Custom { vid, pid })
.collect();
let targets = if targets.is_empty() {
DEFAULT_ONEROM_PICOBOOT_TARGETS.to_vec()
} else {
targets
};
let device_infos = Picoboot::list_devices(Some(&targets))
.await
.map_err(|e| Error::Usb(e.to_string()))?;
let mut devices = Vec::new();
for info in device_infos {
debug!(
"Found Fire device: {:04x}:{:04x} bus {} addr {}",
info.vendor_id(),
info.product_id(),
info.bus_id(),
info.device_address(),
);
let mut device = Device {
vid: info.vendor_id(),
pid: info.product_id(),
bus_id: info.bus_id().to_owned(),
address: info.device_address(),
serial: info.serial_number().map(str::to_owned),
device_info: info,
onerom: None,
state: DeviceState::Unknown,
usb_can_run: false,
chip_id: None,
rp_variant: None,
};
if let Err(e) = read_device_info(&mut device).await {
warn!("Failed to read device info on {device:?}: {e}");
}
if device.is_recognised() || unrecognised {
devices.push(device);
} else {
debug!("Excluding unrecognised device: {device:?}");
}
}
Ok(devices)
}
async fn get_picoboot(device: &Device, long: bool) -> Result<Picoboot, Error> {
let mut picoboot = Picoboot::new(device.device_info.clone())
.await
.map_err(|e| Error::Usb(e.to_string()))?;
let timeout = if long {
Duration::from_secs(20)
} else {
Duration::from_millis(2500)
};
debug!("Setting PICOBOOT timeouts to {timeout:?} (long={long})");
picoboot.set_timeouts(Timeouts {
endpoint: timeout,
..Timeouts::default()
});
Ok(picoboot)
}
#[derive(Debug, Clone, Copy)]
pub struct ChipInfo {
pub chip_id: Rp235xChipId,
pub package: Option<RpVariant>,
}
pub async fn read_chip_info(pb: &mut Picoboot) -> Result<ChipInfo, Error> {
const PB_INFO_SYS: u8 = 0x01;
const CHIP_INFO_FLAG: u32 = 0x0000_0001;
const RESP_BYTES: u32 = 32;
let conn = pb.connect().await.map_err(|e| Error::Usb(e.to_string()))?;
conn.reset_interface()
.await
.map_err(|e| Error::Usb(e.to_string()))?;
let mut args = [0u8; 16];
args[0] = PB_INFO_SYS;
args[4..8].copy_from_slice(&CHIP_INFO_FLAG.to_le_bytes());
let cmd = PicobootCmd::new(PicobootCmdId::GetInfo, 0x10, RESP_BYTES, args);
let resp = conn
.send_cmd(cmd, None)
.await
.map_err(|e| Error::Usb(e.to_string()))?;
let word = |i: usize| u32::from_le_bytes([resp[i], resp[i + 1], resp[i + 2], resp[i + 3]]);
let count = if resp.len() >= 4 { word(0) as usize } else { 0 };
if count < 3 || resp.len() < (count + 1) * 4 {
return Err(Error::Usb(format!(
"GET_INFO CHIP_INFO returned {} bytes with count {count}; too short",
resp.len()
)));
}
let data = (count - 2) * 4;
let package_sel = word(data);
let package = RpVariant::from_package_sel(package_sel);
if package.is_none() {
warn!("Unrecognised RP2350 package_sel {package_sel:#x} in CHIP_INFO");
}
Ok(ChipInfo {
chip_id: Rp235xChipId::from_chip_info([package_sel, word(data + 4), word(data + 8)]),
package,
})
}
pub async fn read_device_info(device: &mut Device) -> Result<(), Error> {
debug!("Reading {FLASH_READ_SIZE_KB}KB from {FLASH_BASE:#010x} on {device}");
let picoboot = get_picoboot(device, false).await?;
let onerom = {
let mut reader = PicobootReader::new(picoboot).await.map_err(Error::Usb)?;
let mut parser = Parser::with_base_flash_address(&mut reader, FLASH_BASE, RAM_BASE);
parser.parse_device().await
};
device.update_onerom(onerom);
let (chip_id, rp_variant) = resolve_chip_id(device).await;
device.chip_id = chip_id;
device.rp_variant = rp_variant;
Ok(())
}
async fn resolve_chip_id(device: &Device) -> (Option<Rp235xChipId>, Option<RpVariant>) {
match read_device_chip_info(device).await {
Ok(info) => (Some(info.chip_id), info.package),
Err(e) => {
warn!("GET_INFO failed on {device}, falling back to serial: {e}");
let chip_id = device
.serial
.as_deref()
.and_then(Rp235xChipId::from_hex_serial);
(chip_id, None)
}
}
}
async fn read_device_chip_info(device: &Device) -> Result<ChipInfo, Error> {
let mut picoboot = get_picoboot(device, false).await?;
read_chip_info(&mut picoboot).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebootMode {
None,
Stopped { msd: bool },
Running,
}
impl std::fmt::Display for RebootMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RebootMode::None => write!(f, "none (skip reboot)"),
RebootMode::Stopped { msd: true } => write!(f, "stopped (MSD enabled)"),
RebootMode::Stopped { msd: false } => write!(f, "stopped"),
RebootMode::Running => write!(f, "running"),
}
}
}
impl TryFrom<RebootMode> for picoboot::RebootType {
type Error = Error;
fn try_from(mode: RebootMode) -> Result<Self, Self::Error> {
match mode {
RebootMode::Stopped { msd } => Ok(picoboot::RebootType::Bootsel {
disable_msd: !msd,
disable_picoboot: false,
}),
RebootMode::Running => Ok(picoboot::RebootType::Normal),
RebootMode::None => Err(Error::NoReboot),
}
}
}
pub struct RebootArgs {
pub mode: RebootMode,
pub fast: bool,
pub check_usb_can_run: bool,
}
impl RebootArgs {
pub fn stopped(msd: bool, fast: bool) -> Self {
Self {
mode: RebootMode::Stopped { msd },
fast,
check_usb_can_run: false,
}
}
pub fn running(fast: bool, check_usb_can_run: bool) -> Self {
Self {
mode: RebootMode::Running,
fast,
check_usb_can_run,
}
}
pub fn none() -> Self {
Self {
mode: RebootMode::None,
fast: false,
check_usb_can_run: false,
}
}
pub fn is_none(&self) -> bool {
self.mode == RebootMode::None
}
}
pub async fn reboot(device: &Device, args: &RebootArgs) -> Result<(), Error> {
if args.mode == RebootMode::Running && args.check_usb_can_run && !device.usb_can_run() {
return Err(Error::NoRebootIntoRunning(device.to_string()));
}
let mut picoboot = get_picoboot(device, false).await?;
let reboot_type = if let Ok(rt) = args.mode.try_into() {
rt
} else {
debug!("No reboot requested, skipping");
return Ok(());
};
const REBOOT_TIMER: Duration = Duration::from_millis(10);
debug!("Rebooting device {device} with type {reboot_type:?} and timer {REBOOT_TIMER:?}");
picoboot
.reboot(reboot_type, REBOOT_TIMER)
.await
.map_err(|e| Error::Usb(e.to_string()))?;
if !args.fast {
pause_reenumeration().await;
}
Ok(())
}
enum MemoryType {
BootRom,
Flash,
Ram,
VirtualRw,
}
struct MemoryRegion {
_name: &'static str,
start: u32,
len: u32,
mem_type: MemoryType,
}
impl MemoryRegion {
const fn new(name: &'static str, start: u32, len: u32, mem_type: MemoryType) -> Self {
Self {
_name: name,
start,
len,
mem_type,
}
}
fn contains(&self, address: u32, length: u32) -> bool {
address >= self.start && length <= self.len && address - self.start <= self.len - length
}
}
const VALID_REGIONS: &[MemoryRegion] = &[
MemoryRegion::new("Flash", 0x1000_0000, 0x0020_0000, MemoryType::Flash),
MemoryRegion::new("SRAM", 0x2000_0000, 0x0008_2000, MemoryType::Ram),
MemoryRegion::new("ROM", 0x0000_0000, 0x0000_8000, MemoryType::BootRom),
MemoryRegion::new(
"Live ROM Image",
0x9000_0000,
0x0008_0000,
MemoryType::VirtualRw,
),
];
fn check_memory_range(
device: &Device,
address: u32,
length: u32,
write: bool,
flash_writes_allowed: bool,
) -> Result<(), Error> {
for region in VALID_REGIONS {
if region.contains(address, length) {
return match region.mem_type {
MemoryType::BootRom => {
if write {
Err(Error::MemoryNotWriteable)
} else {
Ok(())
}
}
MemoryType::Flash => {
if !write || flash_writes_allowed {
Ok(())
} else {
Err(Error::MemoryNotWriteable)
}
}
MemoryType::Ram => Ok(()),
MemoryType::VirtualRw => {
if device.is_running() {
Ok(())
} else {
Err(Error::MemoryDeviceNotRunning)
}
}
};
}
}
Err(Error::InvalidMemoryRange(address, length))
}
pub async fn read_memory(device: &Device, address: u32, length: u32) -> Result<Vec<u8>, Error> {
check_memory_range(device, address, length, false, false)?;
let mut picoboot = get_picoboot(device, false).await?;
picoboot
.read(address, length)
.await
.map_err(|e| Error::Usb(e.to_string()))
}
pub async fn write_memory(device: &Device, address: u32, data: &[u8]) -> Result<(), Error> {
check_memory_range(device, address, data.len() as u32, true, false)?;
let mut picoboot = get_picoboot(device, false).await?;
picoboot
.write(address, data)
.await
.map_err(|e| Error::Usb(e.to_string()))
}
pub async fn flash_program(device: &Device, data: &[u8]) -> Result<(), Error> {
let mut picoboot = get_picoboot(device, true).await?;
picoboot
.flash_erase_and_write(FLASH_BASE, data)
.await
.map_err(|e| Error::Usb(e.to_string()))
}
pub async fn flash_program_read(device: &Device, size: u32) -> Result<Vec<u8>, Error> {
let mut picoboot = get_picoboot(device, false).await?;
picoboot
.flash_read(FLASH_BASE, size)
.await
.map_err(|e| Error::Usb(e.to_string()))
}
pub async fn flash_erase(device: &Device, offset: u32, size: u32) -> Result<(), Error> {
const SECTOR_SIZE: u32 = 4096;
if !offset.is_multiple_of(SECTOR_SIZE) {
return Err(Error::Other(format!(
"offset {offset:#x} is not sector-aligned (must be a multiple of {SECTOR_SIZE:#x})"
)));
}
if size == 0 || !size.is_multiple_of(SECTOR_SIZE) {
return Err(Error::Other(format!(
"size {size:#x} must be a non-zero multiple of {SECTOR_SIZE:#x}"
)));
}
let address = FLASH_BASE + offset;
check_memory_range(device, address, size, true, true)?;
let mut picoboot = get_picoboot(device, true).await?;
picoboot
.flash_erase(address, size)
.await
.map_err(|e| Error::Usb(e.to_string()))
}
async fn pause_reenumeration() {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
pub async fn set_led(device: &Device, led_id: u8, sub_cmd: LedSubCmd) -> Result<(), Error> {
let args = SetLedArgs { led_id, sub_cmd }.encode();
send_onerom_cmd(device, "SET_LED", ONEROM_CMD_SET_LED, 0, args)
.await
.map(|_| ())
.map_err(|failure| cmd_error("SET_LED", failure))
}
const ONEROM_CMD_SIZE: u8 = 0x10;
#[derive(Debug, Clone, PartialEq, Eq)]
enum CmdFailure {
UnknownCmd,
InvalidCmdLength,
NotPermitted,
InvalidArg,
PreconditionNotMet,
Transport(String),
}
impl std::fmt::Display for CmdFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownCmd => write!(f, "the device did not recognise the command"),
Self::InvalidCmdLength => write!(f, "the device rejected the command's length"),
Self::NotPermitted => write!(f, "the device refused the command"),
Self::InvalidArg => write!(f, "the device rejected the command's arguments"),
Self::PreconditionNotMet => write!(f, "the device has no free hold slot"),
Self::Transport(detail) => write!(f, "{detail}"),
}
}
}
impl CmdFailure {
fn classify(status: Option<PicobootStatus>, detail: String) -> Self {
match status {
Some(PicobootStatus::UnknownCmd) => Self::UnknownCmd,
Some(PicobootStatus::InvalidCmdLength) => Self::InvalidCmdLength,
Some(PicobootStatus::NotPermitted) => Self::NotPermitted,
Some(PicobootStatus::InvalidArg) => Self::InvalidArg,
Some(PicobootStatus::PreconditionNotMet) => Self::PreconditionNotMet,
Some(other) => Self::Transport(format!("{detail}\n Device status: {other:?}")),
None => Self::Transport(detail),
}
}
fn means_too_old(&self) -> bool {
matches!(self, Self::UnknownCmd | Self::InvalidCmdLength)
}
}
fn cmd_error(label: &str, failure: CmdFailure) -> Error {
Error::Usb(format!("One ROM {label} command failed:\n {failure}"))
}
async fn send_onerom_cmd(
device: &Device,
label: &str,
cmd_id: u8,
transfer_len: u32,
args: [u8; ONEROM_CMD_ARGS_LEN],
) -> Result<Vec<u8>, CmdFailure> {
let mut picoboot = get_picoboot(device, false)
.await
.map_err(|e| CmdFailure::Transport(e.to_string()))?;
let conn = picoboot
.connect()
.await
.map_err(|e| CmdFailure::Transport(e.to_string()))?;
conn.reset_interface()
.await
.map_err(|e| CmdFailure::Transport(e.to_string()))?;
let cmd = PicobootXCmd::new(ONEROM_MAGIC, cmd_id, ONEROM_CMD_SIZE, transfer_len, args);
debug!("Sending One ROM {label} command (id {cmd_id:#04x}) to {device}");
match conn.send_picobootx_cmd(cmd, None).await {
Ok(data) => Ok(data),
Err(e) => {
let status = conn
.get_command_status()
.await
.inspect_err(|e| debug!("Could not read command status after failure: {e}"))
.ok()
.map(|status| status.get_status_code());
debug!("One ROM {label} command failed: {e} (status {status:?})");
conn.reset_interface().await.ok();
Err(CmdFailure::classify(status, e.to_string()))
}
}
}
pub async fn get_caps(device: &Device) -> Result<Caps, Error> {
let data = send_onerom_cmd(
device,
"GET_CAPS",
ONEROM_CMD_GET_CAPS | PICOBOOT_DIR_IN,
ONEROM_CAPS_LEN,
[0u8; ONEROM_CMD_ARGS_LEN],
)
.await
.map_err(|failure| {
if failure.means_too_old() {
Error::PluginTooOldForGpio(device.to_string())
} else {
cmd_error("GET_CAPS", failure)
}
})?;
Ok(Caps::decode(&data)?)
}
fn check_feature(caps: &Caps, feature: u32, device: &str) -> Result<(), Error> {
if caps.has_feature(feature) {
Ok(())
} else {
Err(Error::FirmwareTooOldForGpio(device.to_string()))
}
}
fn check_gpio_range(caps: &Caps, first_gpio: u8, count: u8) -> Result<(), Error> {
debug_assert!(count > 0, "an empty GPIO run should not reach here");
let past_end = first_gpio as u16 + count as u16;
if past_end > caps.num_gpios as u16 {
let highest = past_end.saturating_sub(1).min(u8::MAX as u16) as u8;
return Err(Error::GpioOutOfRange(highest, caps.num_gpios));
}
Ok(())
}
#[allow(clippy::wildcard_enum_match_arm)]
pub async fn gpio_set(device: &Device, caps: &Caps, args: GpioSetArgs) -> Result<(), Error> {
check_feature(caps, ONEROM_FEAT_GPIO_SET, &device.to_string())?;
check_gpio_range(caps, args.gpio, 1)?;
if args.duration_ms != 0 {
if !caps.has_feature(ONEROM_FEAT_GPIO_HOLD) {
return Err(Error::GpioHoldUnsupported(device.to_string()));
}
if caps.max_hold_ms != 0 && args.duration_ms > caps.max_hold_ms {
return Err(Error::GpioHoldTooLong(args.duration_ms, caps.max_hold_ms));
}
}
send_onerom_cmd(device, "GPIO_SET", ONEROM_CMD_GPIO_SET, 0, args.encode())
.await
.map(|_| ())
.map_err(|failure| match failure {
CmdFailure::NotPermitted => Error::GpioInUse(args.gpio),
CmdFailure::InvalidArg => Error::GpioRejected(args.gpio),
CmdFailure::PreconditionNotMet => Error::GpioNoHoldSlot,
failure if failure.means_too_old() => Error::PluginTooOldForGpio(device.to_string()),
failure => cmd_error("GPIO_SET", failure),
})
}
#[allow(clippy::wildcard_enum_match_arm)]
pub async fn gpio_query(
device: &Device,
caps: &Caps,
first_gpio: u8,
count: u8,
) -> Result<Vec<GpioEntry>, Error> {
check_feature(caps, ONEROM_FEAT_GPIO_QUERY, &device.to_string())?;
if count == 0 {
return Ok(Vec::new());
}
check_gpio_range(caps, first_gpio, count)?;
let args = GpioQueryArgs { first_gpio, count };
let data = send_onerom_cmd(
device,
"GPIO_QUERY",
ONEROM_CMD_GPIO_QUERY | PICOBOOT_DIR_IN,
args.transfer_len(),
args.encode(),
)
.await
.map_err(|failure| match failure {
CmdFailure::InvalidArg => Error::GpioRejected(first_gpio),
failure if failure.means_too_old() => Error::PluginTooOldForGpio(device.to_string()),
failure => cmd_error("GPIO_QUERY", failure),
})?;
let entries = GpioEntry::decode_all(&data)?;
if entries.len() != count as usize {
return Err(Error::PicobootxDecode(format!(
"GPIO_QUERY returned {} entries for GPIO{first_gpio}, expected {count}",
entries.len()
)));
}
Ok(entries)
}
pub async fn gpio_query_all(device: &Device, caps: &Caps) -> Result<Vec<GpioEntry>, Error> {
gpio_query(device, caps, 0, caps.num_gpios).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_status_that_means_too_old_is_recognised() {
assert!(CmdFailure::classify(Some(PicobootStatus::UnknownCmd), "e".into()).means_too_old());
assert!(
CmdFailure::classify(Some(PicobootStatus::InvalidCmdLength), "e".into())
.means_too_old()
);
}
#[test]
fn a_refusal_is_not_confused_with_being_too_old() {
let refusal = CmdFailure::classify(Some(PicobootStatus::NotPermitted), "e".into());
assert_eq!(refusal, CmdFailure::NotPermitted);
assert!(!refusal.means_too_old());
let bad_arg = CmdFailure::classify(Some(PicobootStatus::InvalidArg), "e".into());
assert_eq!(bad_arg, CmdFailure::InvalidArg);
assert!(!bad_arg.means_too_old());
}
#[test]
fn a_transport_failure_is_not_confused_with_a_refusal() {
let failure = CmdFailure::classify(None, "endpoint timed out".into());
assert_eq!(
failure,
CmdFailure::Transport("endpoint timed out".to_string())
);
assert!(!failure.means_too_old());
assert!(failure.to_string().contains("endpoint timed out"));
let failure = CmdFailure::classify(Some(PicobootStatus::InvalidState), "stalled".into());
assert!(!failure.means_too_old());
let msg = failure.to_string();
assert!(msg.contains("stalled"), "{msg}");
assert!(msg.contains("InvalidState"), "{msg}");
}
#[test]
fn a_gpio_run_is_checked_against_the_devices_own_gpio_count() {
let a = Caps {
num_gpios: 30,
..Caps::default()
};
let b = Caps {
num_gpios: 48,
..Caps::default()
};
assert!(check_gpio_range(&a, 29, 1).is_ok());
assert!(check_gpio_range(&a, 0, 30).is_ok());
assert!(check_gpio_range(&a, 30, 1).is_err());
assert!(check_gpio_range(&a, 0, 48).is_err());
assert!(check_gpio_range(&b, 30, 1).is_ok());
assert!(check_gpio_range(&b, 0, 48).is_ok());
assert!(check_gpio_range(&b, 47, 2).is_err());
assert!(check_gpio_range(&Caps::default(), 0, 1).is_err());
let msg = check_gpio_range(&a, 30, 1).unwrap_err().to_string();
assert!(msg.contains("GPIO30"), "{msg}");
assert!(msg.contains("30 GPIOs"), "{msg}");
let msg = check_gpio_range(&a, 0, 48).unwrap_err().to_string();
assert!(msg.contains("GPIO47"), "{msg}");
}
#[test]
fn a_missing_feature_bit_blames_the_firmware_not_the_plugin() {
let caps = Caps {
num_gpios: 30,
features: 0,
..Caps::default()
};
for feature in [ONEROM_FEAT_GPIO_SET, ONEROM_FEAT_GPIO_QUERY] {
let msg = check_feature(&caps, feature, "One ROM Fire 24 F")
.unwrap_err()
.to_string();
assert!(msg.contains("firmware"), "{msg}");
assert!(msg.contains("One ROM Fire 24 F"), "{msg}");
assert!(!msg.contains("plugin predates"), "{msg}");
}
let caps = Caps {
features: ONEROM_FEAT_GPIO_SET | ONEROM_FEAT_GPIO_QUERY,
..caps
};
assert!(check_feature(&caps, ONEROM_FEAT_GPIO_SET, "d").is_ok());
assert!(check_feature(&caps, ONEROM_FEAT_GPIO_QUERY, "d").is_ok());
assert!(check_feature(&caps, ONEROM_FEAT_GPIO_HOLD, "d").is_err());
}
}