use log::debug;
use nusb::DeviceInfo;
use onerom_config::hw::Board;
use onerom_config::mcu::{Rp235xChipId, RpVariant};
use onerom_fw_parser::ParsedDevice;
use wildmatch::WildMatch;
use crate::error::Error;
use crate::usb::enumerate_devices;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DeviceState {
Unknown,
Stopped,
Running,
Limp,
}
impl std::fmt::Display for DeviceState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state_str = match self {
DeviceState::Unknown => "Unknown",
DeviceState::Stopped => "Stopped",
DeviceState::Running => "Running",
DeviceState::Limp => "Limp Mode",
};
write!(f, "{state_str}")
}
}
pub struct Device {
pub vid: u16,
pub pid: u16,
pub bus_id: String,
pub address: u8,
pub serial: Option<String>,
#[allow(unused)]
pub device_info: DeviceInfo,
pub onerom: Option<ParsedDevice>,
pub state: DeviceState,
pub usb_can_run: bool,
pub chip_id: Option<Rp235xChipId>,
pub rp_variant: Option<RpVariant>,
}
impl std::fmt::Display for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let serial = self.serial.as_deref().unwrap_or("(no serial)");
let info_str = match self.onerom.as_ref() {
Some(ParsedDevice::Original(sdrr))
if sdrr.flash.as_ref().and_then(|f| f.board.as_ref()).is_some() =>
{
let info = sdrr.flash.as_ref().unwrap();
let board = info.board.as_ref().unwrap();
let fw_version = &info.version;
format!("One ROM {} - Firmware: {fw_version}", board_label(board))
}
Some(ParsedDevice::Schema(onerom)) if onerom.info().is_some() => {
let info = onerom.info().unwrap();
let hw_rev = onerom
.metadata()
.map(|m| m.hw.hw_rev.as_str())
.unwrap_or("unknown");
let fw_version = format!(
"v{}.{}.{}",
info.major_version, info.minor_version, info.patch_version
);
let board_part = match Board::try_from_str(hw_rev) {
Some(board) => board_label(&board),
None => hw_rev.to_string(),
};
format!("One ROM {board_part} - Firmware: {fw_version}")
}
_ => "Unknown - Firmware: n/a ".to_string(),
};
write!(f, "{info_str} State: {} Serial: {serial}", self.state)
}
}
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Device")
.field("vid", &format_args!("{:#06x}", self.vid))
.field("pid", &format_args!("{:#06x}", self.pid))
.field("bus_id", &self.bus_id)
.field("address", &self.address)
.field("serial", &self.serial)
.finish()
}
}
impl Device {
pub fn is_recognised(&self) -> bool {
self.onerom
.as_ref()
.is_some_and(ParsedDevice::is_recognised)
}
pub fn is_running(&self) -> bool {
self.state == DeviceState::Running
}
pub fn usb_can_run(&self) -> bool {
self.usb_can_run
}
pub fn update_onerom(&mut self, onerom: ParsedDevice) {
self.onerom = Some(onerom);
self.update_state();
}
#[allow(clippy::wildcard_enum_match_arm)]
fn update_state(&mut self) {
self.usb_can_run = false;
self.state = DeviceState::Unknown;
let Some(onerom) = self.onerom.as_ref() else {
return;
};
match onerom {
ParsedDevice::Original(sdrr) => {
if sdrr.flash.is_none() {
return;
};
if let Some(runtime_info) = &sdrr.ram {
self.state = match runtime_info.limp_mode.as_ref() {
Some(limp_mode)
if *limp_mode != onerom_fw_parser::types::LimpMode::None =>
{
DeviceState::Limp
}
_ => DeviceState::Running,
}
} else {
self.state = DeviceState::Stopped;
}
}
ParsedDevice::Schema(onerom) => {
if onerom.info().is_none() {
return;
};
if let Some(runtime_info) = &onerom.runtime() {
self.state = match runtime_info.limp_mode {
onerom_metadata::LimpModePattern::LimpModeNone => DeviceState::Running,
_ => DeviceState::Limp,
}
} else {
self.state = DeviceState::Stopped;
}
}
}
self.usb_can_run = onerom.is_usb_run_capable();
}
pub fn get_active_rom_set_index(&self) -> Option<u8> {
self.onerom.as_ref()?.active_slot_index().map(|i| i as u8)
}
fn active_rom_facts(&self) -> Option<(String, usize)> {
if !self.is_running() {
return None;
}
let onerom = self.onerom.as_ref()?;
let slot = onerom.slots().find(|s| s.active)?;
let rom = slot.roms().next()?;
Some((rom.rom_type.into_owned(), rom.size))
}
pub fn get_active_rom_type(&self) -> Option<String> {
self.active_rom_facts().map(|(ty, _)| ty)
}
pub fn get_active_rom_size(&self) -> Option<usize> {
self.active_rom_facts().map(|(_, size)| size)
}
pub fn matches_serial(&self, pattern: &str) -> bool {
matches_serial(self.serial.as_deref(), pattern)
}
pub fn mcu_chip_id_line(&self) -> Option<String> {
let id = self.chip_id?;
Some(match self.rp_variant {
Some(variant) => format!("MCU: {variant} Chip ID: {id}"),
None => format!("Chip ID: {id}"),
})
}
pub fn sort_key(&self) -> (String, String) {
let board = self
.onerom
.as_ref()
.and_then(|o| match o {
ParsedDevice::Original(sdrr) => sdrr
.flash
.as_ref()
.and_then(|f| f.board.as_ref())
.map(|b| b.model().to_string()),
ParsedDevice::Schema(onerom) => onerom.metadata().map(|m| m.hw.hw_rev.clone()),
})
.unwrap_or_else(|| "~".to_string()); let serial = self.serial.clone().unwrap_or_else(|| "~".to_string());
(board, serial)
}
}
fn board_label(board: &Board) -> String {
let model = board.model();
let pins = board.chip_pins();
let rev = board
.name()
.rsplit_once('-')
.map(|(_, rev)| rev)
.unwrap_or("")
.to_uppercase();
format!("{model} {pins} {rev}")
}
pub fn matches_serial(serial: Option<&str>, pattern: &str) -> bool {
let pattern_upper = pattern.to_uppercase();
let matcher = WildMatch::new(&pattern_upper);
serial
.map(|s| matcher.matches(&s.to_uppercase()))
.unwrap_or(false)
}
pub async fn select_device(
selector: Option<&str>,
unrecognised: bool,
vid_pid: &[(u16, u16)],
) -> Result<Device, Error> {
let devices = enumerate_devices(unrecognised, vid_pid).await?;
if devices.is_empty() {
debug!("No devices found");
return Err(Error::NoDevices);
}
match selector {
None => {
if devices.len() > 1 {
let serials: Vec<String> = devices
.iter()
.map(|d| d.serial.as_deref().unwrap_or("(no serial)").to_string())
.collect();
debug!("Multiple devices found with no selector: {serials:?}");
Err(Error::MultipleDevices(serials))
} else {
let device = devices.into_iter().next().unwrap();
debug!("Auto-selected device: {device}");
Ok(device)
}
}
Some(pattern) => {
let mut matched: Vec<Device> = devices
.into_iter()
.filter(|d| matches_serial(d.serial.as_deref(), pattern))
.collect();
match matched.len() {
0 => Err(Error::DeviceNotFound(pattern.to_string())),
1 => Ok(matched.remove(0)),
_ => {
let serials: Vec<String> = matched
.iter()
.map(|d| d.serial.as_deref().unwrap_or("(no serial)").to_string())
.collect();
debug!("Multiple devices found with selector '{pattern}': {serials:?}");
Err(Error::MultipleDevices(serials))
}
}
}
}
}
pub async fn select_device_by_chip_id(
chip_id: Option<Rp235xChipId>,
unrecognised: bool,
vid_pid: &[(u16, u16)],
) -> Result<Device, Error> {
let devices = enumerate_devices(unrecognised, vid_pid).await?;
if devices.is_empty() {
debug!("No devices found");
return Err(Error::NoDevices);
}
let Some(id) = chip_id else {
if devices.len() > 1 {
let serials: Vec<String> = devices
.iter()
.map(|d| d.serial.as_deref().unwrap_or("(no serial)").to_string())
.collect();
return Err(Error::MultipleDevices(serials));
}
return Ok(devices.into_iter().next().unwrap());
};
let mut matched: Vec<Device> = devices
.into_iter()
.filter(|d| d.chip_id == Some(id))
.collect();
match matched.len() {
0 => Err(Error::DeviceNotFound(id.to_string())),
1 => Ok(matched.remove(0)),
_ => Err(Error::MultipleDevices(vec![id.to_string()])),
}
}