use super::CmsisDapDevice;
use crate::probe::{
BoxedProbeError, DebugProbeInfo, DebugProbeSelector, ProbeCreationError,
cmsisdap::{CmsisDapFactory, commands::CmsisDapError, commands::DEFAULT_USB_TIMEOUT},
};
#[cfg(feature = "cmsisdap_v1")]
use hidapi::HidApi;
use nusb::{DeviceInfo, MaybeFuture, descriptors::TransferType, transfer::Direction};
const USB_CLASS_HID: u8 = 0x03;
const USB_CMSIS_DAP_CLASS: u8 = 0xFF;
const USB_CMSIS_DAP_SUBCLASS: u8 = 0;
#[tracing::instrument(skip_all)]
pub fn list_cmsisdap_devices() -> Vec<DebugProbeInfo> {
tracing::debug!("Searching for CMSIS-DAP probes using nusb");
#[cfg_attr(not(feature = "cmsisdap_v1"), expect(unused_mut))]
let mut probes = match nusb::list_devices().wait() {
Ok(devices) => devices
.flat_map(|device| get_cmsisdap_info(&device, false))
.collect(),
Err(e) => {
tracing::warn!("error listing devices with nusb: {e}");
vec![]
}
};
#[cfg(feature = "cmsisdap_v1")]
tracing::debug!(
"Found {} CMSIS-DAP probes using nusb, searching HID",
probes.len()
);
#[cfg(feature = "cmsisdap_v1")]
if let Ok(api) = hidapi::HidApi::new() {
for device in api.device_list() {
if let Some(info) = get_cmsisdap_hid_info(device) {
if !probes.iter().any(|p| {
p.vendor_id == info.vendor_id
&& p.product_id == info.product_id
&& p.serial_number == info.serial_number
}) {
tracing::trace!("Adding new HID-only probe {:?}", info);
probes.push(info)
} else {
tracing::trace!("Ignoring duplicate {:?}", info);
}
}
}
}
tracing::debug!("Found {} CMSIS-DAP probes total", probes.len());
probes
}
fn get_cmsisdap_info(device: &DeviceInfo, list_both_versions: bool) -> Vec<DebugProbeInfo> {
let prod_str = device.product_string().unwrap_or("");
let sn_str = device.serial_number();
let cmsis_dap_product = is_cmsis_dap(prod_str) || is_known_cmsis_dap_dev(device);
let mut v1_ifaces = vec![];
let mut v2_ifaces = vec![];
for interface in device.interfaces() {
let Some(interface_desc) = interface.interface_string() else {
tracing::trace!(
"interface {} has no string, skipping",
interface.interface_number()
);
continue;
};
if is_cmsis_dap(interface_desc) {
tracing::trace!(
" Interface {}: {}",
interface.interface_number(),
interface_desc,
);
let selected_interface = Some(interface.interface_number());
let is_hid_interface = if interface.class() == USB_CLASS_HID {
tracing::trace!(" HID interface found");
true
} else if (interface.class(), interface.subclass())
!= (USB_CMSIS_DAP_CLASS, USB_CMSIS_DAP_SUBCLASS)
{
tracing::trace!(
"Interface {} has a cmsis-dap description but wrong classes ({}, {}), skipping",
interface.interface_number(),
interface.class(),
interface.subclass(),
);
continue;
} else {
false
};
let info = DebugProbeInfo::new(
prod_str.to_string(),
device.vendor_id(),
device.product_id(),
sn_str.map(Into::into),
&CmsisDapFactory,
selected_interface,
is_hid_interface,
);
if is_hid_interface {
v1_ifaces.push(info);
} else {
v2_ifaces.push(info);
}
}
}
if cmsis_dap_product {
tracing::trace!(
"{}: CMSIS-DAP device with {} interfaces",
prod_str,
device.interfaces().count()
);
if !v1_ifaces.is_empty() {
tracing::trace!("Device has {} CMSIS-DAPv1 interfaces", v1_ifaces.len());
}
if !v2_ifaces.is_empty() {
tracing::trace!("Device has {} CMSIS-DAPv2 interfaces", v2_ifaces.len());
}
}
let mut results = v2_ifaces;
if list_both_versions || results.is_empty() {
results.extend(v1_ifaces);
}
results
}
#[cfg(feature = "cmsisdap_v1")]
fn get_cmsisdap_hid_info(device: &hidapi::DeviceInfo) -> Option<DebugProbeInfo> {
let prod_str = device.product_string().unwrap_or("");
let path = device.path().to_str().unwrap_or("");
if is_cmsis_dap(prod_str) || is_cmsis_dap(path) {
tracing::trace!("CMSIS-DAP device with USB path: {:?}", device.path());
tracing::trace!(" product_string: {:?}", prod_str);
tracing::trace!(
" interface: {}",
device.interface_number()
);
Some(DebugProbeInfo::new(
prod_str.to_owned(),
device.vendor_id(),
device.product_id(),
device.serial_number().map(|s| s.to_owned()),
&CmsisDapFactory,
Some(device.interface_number() as u8),
true,
))
} else {
None
}
}
pub fn open_v2_device(
device_info: &DeviceInfo,
selected_interface: Option<u8>,
) -> Result<Option<CmsisDapDevice>, ProbeCreationError> {
let vid = device_info.vendor_id();
let pid = device_info.product_id();
tracing::trace!(
"Trying to open {:04x}:{:04x} in cmsis-dap v2 mode",
vid,
pid
);
let device = match device_info.open().wait() {
Ok(device) => device,
Err(e) => {
tracing::debug!(
vendor_id = %format!("{vid:04x}"),
product_id = %format!("{pid:04x}"),
error = %e,
"failed to open device for CMSIS-DAP v2"
);
return Ok(None);
}
};
let Some(c_desc) = device.configurations().next() else {
tracing::trace!("No cmsis-dap v2 interface found");
return Ok(None);
};
for interface in c_desc.interfaces() {
tracing::trace!("Checking interface {}", interface.interface_number());
if let Some(iface) = selected_interface
&& interface.interface_number() != iface
{
tracing::trace!(
"Interface number does not match selector {} != {}",
iface,
interface.interface_number()
);
continue;
}
for i_desc in interface.alt_settings() {
let Some(interface_str) = device_info
.interfaces()
.find(|i| i.interface_number() == interface.interface_number())
.and_then(|i| i.interface_string())
else {
tracing::trace!("Interface does not have interface string");
continue;
};
if !is_cmsis_dap(interface_str) {
tracing::trace!("Interface does not have 'CMSIS-DAP' in string");
continue;
}
let n_ep = i_desc.num_endpoints();
if !(2..=3).contains(&n_ep) {
tracing::trace!(
"Interface does not have the correct number of endpoints ({})",
n_ep
);
continue;
}
let eps: Vec<_> = i_desc.endpoints().collect();
if eps[0].transfer_type() != TransferType::Bulk || eps[0].direction() != Direction::Out
{
tracing::trace!("First interface endpoint is not bulk out");
continue;
}
if eps[1].transfer_type() != TransferType::Bulk || eps[1].direction() != Direction::In {
tracing::trace!("Second interface endpoint is not bulk in");
continue;
}
let mut swo_ep = None;
if eps.len() > 2
&& eps[2].transfer_type() == TransferType::Bulk
&& eps[2].direction() == Direction::In
{
swo_ep = Some((eps[2].address(), eps[2].max_packet_size()));
}
match device.claim_interface(interface.interface_number()).wait() {
Ok(handle) => {
tracing::debug!("Opening {:04x}:{:04x} in CMSIS-DAPv2 mode", vid, pid);
reject_probe_by_version(
device_info.vendor_id(),
device_info.product_id(),
device_info.device_version(),
)?;
return Ok(Some(CmsisDapDevice::V2 {
handle,
out_ep: eps[0].address(),
in_ep: eps[1].address(),
swo_ep,
max_packet_size: eps[1].max_packet_size(),
usb_timeout: DEFAULT_USB_TIMEOUT,
}));
}
Err(e) => {
tracing::debug!(
interface = interface.interface_number(),
error = %e,
"failed to claim interface"
);
continue;
}
}
}
}
tracing::debug!(
"Could not open {:04x}:{:04x} in CMSIS-DAP v2 mode",
vid,
pid
);
Ok(None)
}
fn reject_probe_by_version(
vendor_id: u16,
product_id: u16,
device_version: u16,
) -> Result<(), ProbeCreationError> {
let denylist = [
|vid, pid, version| (vid == 0x2e8a && pid == 0x000c && version < 0x0220).then_some("2.2.0"), ];
tracing::debug!(
"Checking against denylist: {:04x}:{:04x} v{:04x}",
vendor_id,
product_id,
device_version
);
for deny in denylist {
if let Some(min_version) = deny(vendor_id, product_id, device_version) {
return Err(ProbeCreationError::ProbeSpecific(BoxedProbeError::from(
CmsisDapError::ProbeFirmwareOutdated(min_version),
)));
}
}
Ok(())
}
pub fn open_device_from_selector(
selector: &DebugProbeSelector,
) -> Result<CmsisDapDevice, ProbeCreationError> {
tracing::trace!("Attempting to open device matching {}", selector);
#[cfg(feature = "cmsisdap_v1")]
let mut hid_device_info = None;
match nusb::list_devices().wait() {
Ok(devices) => {
for device in devices {
tracing::trace!("Trying device {:?}", device);
if selector.matches(&device)
&& let Some(device_info) =
get_cmsisdap_info(&device, true).into_iter().find(|dpi| {
tracing::trace!("DebugProbeInfo: {:?}", dpi);
selector.interface.is_none_or(|i| Some(i) == dpi.interface)
})
{
if let Some(device) = open_v2_device(&device, device_info.interface)? {
return Ok(device);
} else {
#[cfg(feature = "cmsisdap_v1")]
{
hid_device_info = Some(device_info);
}
}
}
tracing::trace!("Device did not match");
}
}
Err(e) => {
tracing::debug!("No devices matched using nusb: {e}");
}
}
#[cfg(not(feature = "cmsisdap_v1"))]
return Err(ProbeCreationError::NotFound);
#[cfg(feature = "cmsisdap_v1")]
{
let vid = selector.vendor_id;
let pid = selector.product_id;
let sn = selector.serial_number.as_deref();
tracing::debug!(
"Attempting to open {:04x}:{:04x} in CMSIS-DAP v1 mode",
vid,
pid
);
let Ok(hid_api) = HidApi::new() else {
return Err(ProbeCreationError::NotFound);
};
let mut device_list = hid_api.device_list();
let device_info = device_list
.find(|info| {
let mut device_match = info.vendor_id() == vid && info.product_id() == pid;
if let Some(sn) = sn {
device_match &= Some(sn) == info.serial_number();
}
if let Some(hid_interface) = hid_device_info
.as_ref()
.and_then(|info| info.interface.filter(|_| info.is_hid_interface))
{
device_match &= info.interface_number() == hid_interface as i32;
}
device_match
})
.ok_or(ProbeCreationError::NotFound)?;
let Ok(device) = device_info.open_device(&hid_api) else {
return Err(ProbeCreationError::NotFound);
};
match device.get_product_string() {
Ok(Some(s)) if is_cmsis_dap(&s) => {
reject_probe_by_version(
device_info.vendor_id(),
device_info.product_id(),
device_info.release_number(),
)?;
Ok(CmsisDapDevice::V1 {
handle: device,
report_size: hid_report_size(device_info),
usb_timeout: DEFAULT_USB_TIMEOUT,
})
}
_ => {
Err(ProbeCreationError::NotFound)
}
}
}
}
fn is_cmsis_dap(id: &str) -> bool {
id.contains("CMSIS-DAP") || id.contains("CMSIS_DAP")
}
fn is_known_cmsis_dap_dev(device: &DeviceInfo) -> bool {
const KNOWN_DAPS: &[(u16, u16)] = &[(0x1a86, 0x8012)];
KNOWN_DAPS
.iter()
.any(|&(vid, pid)| device.vendor_id() == vid && device.product_id() == pid)
}
#[cfg(feature = "cmsisdap_v1")]
fn hid_report_size(device: &hidapi::DeviceInfo) -> usize {
if device.vendor_id() == 0x03eb
&& let Some(s) = device.product_string()
&& s.contains("EDBG")
{
tracing::debug!("Overriding packet size to 512 bytes for EDBG device");
return 512;
}
64
}