#[allow(unused_imports)]
use log::{debug, warn};
use picoboot::{Picoboot, Reader as PicobootReader, Target, usb::Timeouts};
use sdrr_fw_parser::Parser;
use std::time::Duration;
use crate::Error;
pub use crate::picobootx::LedSubCmd;
use crate::picobootx::{ONEROM_CMD_SET_LED, ONEROM_MAGIC};
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,
};
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)
}
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 mut reader = PicobootReader::new(picoboot).await.map_err(Error::Usb)?;
let mut parser = Parser::with_base_flash_address(&mut reader, FLASH_BASE, RAM_BASE);
let onerom = parser.parse().await;
device.update_onerom(onerom);
Ok(())
}
#[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 mut args = [0u8; 16];
args[0] = led_id;
args[1] = sub_cmd as u8;
let cmd = picoboot::PicobootXCmd::new(ONEROM_MAGIC, ONEROM_CMD_SET_LED, 0x10, 0, args);
let mut picoboot = get_picoboot(device, false).await?;
picoboot
.send_picobootx_cmd(cmd, None)
.await
.map(|_| ())
.map_err(|e| Error::Usb(e.to_string()))
}