usbhubd: full Linux 7.1 hub enumeration state machine (P3-A)
Port the Linux 7.1 hub.c port state machine into usbhubd, replacing the minimal connect/reset handling: New module port_ops.rs (pure logic, side effects injected, 14 unit tests): - debounce_until_connected: hub_port_debounce_be_connected() port (hub.c:4696-4737) — 25ms polls, connection stable for 100ms, 2s budget, connection-change bit cleared in-loop. - wait_for_reset: hub_port_wait_reset() port (hub.c:2953-3047) — 10ms polls until RESET clears with CONNECTION set, escalate to 200ms after two short waits, 800ms budget; then 50ms TRSTRCY recovery (hub.c:3159) and C_PORT_RESET clear. Replaces the previous bare sleep(10ms). - wait_for_u0: USB 3.0 polling→U0 wait after port power-on — 36ms steps, 400ms ceiling (tPollingLFPSTimeout = 360ms; Linux hub.c:1226 debounce path). - accumulate_hub_delay_ns: wHubDelay chain rule (hub.c:1507-1519: wHubDelay + parent->hub_delay + 40ns, cap 65535ns). main.rs wiring: - Port status normalized to PortStatusSnapshot (decouples the state machine from the V2/V3 wire formats; V3 link state extracted from bits 8:5). - Debounce on connection-change before attach; C_PORT_ENABLE cleared once handled (Linux port_event semantics). - Reset path uses wait_for_reset instead of sleep(10ms). - USB 3 power-on path waits for U0 before proceeding. - wHubDelay: ancestor-chain walk fetching USB 3 ancestor hub descriptors, accumulated per Linux; delivered to newly attached SuperSpeed children via SET_ISOCH_DELAY (USB 3.0 9.4.11; Linux message.c:1142 — hubs and non-SS skipped, children inherit the hub's accumulated delay verbatim per hub.c:5128-5129). - attach/detach failure logs now identify the port. hub.rs (xhcid usb module): - HubDescriptorV3 extended with device_removable: u16 — the SS hub descriptor is 12 bytes (spec Table 10-15); the old struct under-read by 2 bytes. Stale TODO corrected: SS descriptors have no PortPwrCtrlMask (that is USB 2.0-only, still unparsed). Verified: cargo check clean (0 usbhubd warnings), 14/14 usbhubd tests, xhcid unaffected (43/43 tests).
This commit is contained in:
+246
-97
@@ -1,10 +1,108 @@
|
||||
use std::{env, thread, time};
|
||||
|
||||
use xhcid_interface::{
|
||||
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDesc,
|
||||
EndpDirection, PortId, PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
|
||||
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDirection, PortId,
|
||||
PortReqRecipient, PortReqTy, XhciClientHandle, XhciClientHandleError, XhciEndpHandle,
|
||||
};
|
||||
|
||||
mod port_ops;
|
||||
|
||||
use port_ops::PortStatusSnapshot;
|
||||
|
||||
/// Issue GET_STATUS(Class, Other, port) on the hub's control pipe and
|
||||
/// normalize the wire-format status into a `PortStatusSnapshot`.
|
||||
/// Linux 7.1 hub.c:600 `get_port_status()`.
|
||||
fn fetch_status(
|
||||
handle: &XhciClientHandle,
|
||||
port: u8,
|
||||
usb_3: bool,
|
||||
) -> Result<PortStatusSnapshot, XhciClientHandleError> {
|
||||
if usb_3 {
|
||||
let mut sts = usb::HubPortStatusV3::default();
|
||||
handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut sts) }),
|
||||
)?;
|
||||
Ok(PortStatusSnapshot::from(&usb::HubPortStatus::V3(sts)))
|
||||
} else {
|
||||
let mut sts = usb::HubPortStatusV2::default();
|
||||
handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut sts) }),
|
||||
)?;
|
||||
Ok(PortStatusSnapshot::from(&usb::HubPortStatus::V2(sts)))
|
||||
}
|
||||
}
|
||||
|
||||
/// SET_FEATURE(Class, Other, feature, port). Linux `set_port_feature()`.
|
||||
fn set_feature(
|
||||
handle: &XhciClientHandle,
|
||||
port: u8,
|
||||
feature: usb::HubPortFeature,
|
||||
) -> Result<(), XhciClientHandleError> {
|
||||
handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
feature as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
)
|
||||
}
|
||||
|
||||
/// CLEAR_FEATURE(Class, Other, feature, port). Linux `clear_port_feature()`.
|
||||
fn clear_feature(
|
||||
handle: &XhciClientHandle,
|
||||
port: u8,
|
||||
feature: usb::HubPortFeature,
|
||||
) -> Result<(), XhciClientHandleError> {
|
||||
handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::ClearFeature as u8,
|
||||
feature as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
)
|
||||
}
|
||||
|
||||
/// Deliver the accumulated hub delay to a newly attached SuperSpeed child
|
||||
/// via SET_ISOCH_DELAY (USB 3.0 §9.4.11). Mirrors Linux 7.1
|
||||
/// `usb_set_isoch_delay()` (message.c:1142-1158): skipped for hubs (a
|
||||
/// child hub propagates the chain through its own daemon) and non-SS
|
||||
/// devices. The value is this hub's accumulated `hub_delay` — children
|
||||
/// inherit it verbatim (hub.c:5128-5129).
|
||||
fn maybe_send_isoch_delay(handle: &XhciClientHandle, hub_delay_ns: u32) {
|
||||
let Ok(desc) = handle.get_standard_descs() else {
|
||||
return;
|
||||
};
|
||||
const USB_CLASS_HUB: u8 = 0x09;
|
||||
if desc.class == USB_CLASS_HUB || desc.major_version() < 3 {
|
||||
return;
|
||||
}
|
||||
let delay = hub_delay_ns.min(port_ops::USB_TP_TRANSMISSION_DELAY_MAX_NS) as u16;
|
||||
const SET_ISOCH_DELAY: u8 = 0x31;
|
||||
match handle.device_request(
|
||||
PortReqTy::Standard,
|
||||
PortReqRecipient::Device,
|
||||
SET_ISOCH_DELAY,
|
||||
delay,
|
||||
0,
|
||||
DeviceReqData::NoData,
|
||||
) {
|
||||
Ok(()) => log::info!("usbhubd: SET_ISOCH_DELAY {} ns delivered", delay),
|
||||
Err(e) => log::warn!("usbhubd: SET_ISOCH_DELAY({} ns) failed: {}", delay, e),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
common::init();
|
||||
let mut args = env::args().skip(1);
|
||||
@@ -61,7 +159,7 @@ fn main() {
|
||||
.expect("Failed to find suitable configuration");
|
||||
|
||||
// Read hub descriptor
|
||||
let (ports, usb_3, b_pwr_on_2_pwr_good, w_hub_delay) = if desc.major_version() >= 3 {
|
||||
let (ports, usb_3, b_pwr_on_2_pwr_good, w_hub_delay, device_removable) = if desc.major_version() >= 3 {
|
||||
let mut hub_desc = usb::HubDescriptorV3::default();
|
||||
handle
|
||||
.device_request(
|
||||
@@ -80,6 +178,7 @@ fn main() {
|
||||
// the spec says to use 10 (20ms) as default.
|
||||
10u8,
|
||||
u16::from(hub_desc.delay),
|
||||
hub_desc.device_removable,
|
||||
)
|
||||
} else {
|
||||
let mut hub_desc = usb::HubDescriptorV2::default();
|
||||
@@ -98,6 +197,9 @@ fn main() {
|
||||
false,
|
||||
hub_desc.power_on_good,
|
||||
0u16, // wHubDelay only exists in USB 3 hub descriptors
|
||||
// USB 2.0 device_removable bitmaps are variable-length and
|
||||
// not parsed (see hub.rs).
|
||||
0u16,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -105,6 +207,65 @@ fn main() {
|
||||
"usbhubd: {} port(s) detected, PwrOn2PwrGood={}*2ms, wHubDelay={}",
|
||||
ports, b_pwr_on_2_pwr_good, w_hub_delay,
|
||||
);
|
||||
if device_removable != 0 {
|
||||
let fixed: Vec<u8> = (1..=ports)
|
||||
.filter(|&p| (device_removable >> (p - 1)) & 1 == 1)
|
||||
.collect();
|
||||
log::info!("usbhubd: non-removable (integrated) ports: {:?}", fixed);
|
||||
}
|
||||
|
||||
// wHubDelay chain accumulation (Linux 7.1 hub.c:1507-1519):
|
||||
// hub_delay = wHubDelay + parent->hub_delay + tTPTransmissionDelay(40ns),
|
||||
// clamped to 65535 ns. Walk the PortId ancestor chain so multi-tier
|
||||
// topologies accumulate correctly; each USB 3 ancestor hub contributes
|
||||
// its own wHubDelay plus one transmission-delay hop. Children behind
|
||||
// this hub inherit the result via SET_ISOCH_DELAY at attach time.
|
||||
let hub_delay_ns = if usb_3 {
|
||||
let mut ancestors: Vec<PortId> = Vec::new();
|
||||
let mut cur = port_id.parent();
|
||||
while let Some((p, _port_num_on_parent)) = cur {
|
||||
ancestors.push(p);
|
||||
cur = p.parent();
|
||||
}
|
||||
ancestors.reverse(); // root-first, matching Linux's recursive chain
|
||||
|
||||
let mut acc = 0u32;
|
||||
for ancestor in ancestors {
|
||||
let Ok(ah) = XhciClientHandle::new(scheme.clone(), ancestor) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(adesc) = ah.get_standard_descs() else {
|
||||
continue;
|
||||
};
|
||||
const USB_CLASS_HUB: u8 = 0x09;
|
||||
if adesc.class != USB_CLASS_HUB || adesc.major_version() < 3 {
|
||||
continue;
|
||||
}
|
||||
let mut ancestor_desc = usb::HubDescriptorV3::default();
|
||||
if ah
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Device,
|
||||
usb::SetupReq::GetDescriptor as u8,
|
||||
u16::from(usb::HubDescriptorV3::DESCRIPTOR_KIND) << 8,
|
||||
0,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut ancestor_desc) }),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
acc = port_ops::accumulate_hub_delay_ns(u32::from(u16::from(ancestor_desc.delay)), acc);
|
||||
}
|
||||
}
|
||||
let total = port_ops::accumulate_hub_delay_ns(u32::from(w_hub_delay), acc);
|
||||
log::info!(
|
||||
"usbhubd: accumulated hub delay {} ns (depth {})",
|
||||
total,
|
||||
port_id.hub_depth()
|
||||
);
|
||||
total
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Configure as hub device. USB 3 hubs do not need explicit
|
||||
// interface_desc / alternate_setting in ConfigureEndpointsReq —
|
||||
@@ -172,7 +333,7 @@ fn main() {
|
||||
// Initialize states
|
||||
struct PortState {
|
||||
port_id: PortId,
|
||||
port_sts: usb::HubPortStatus,
|
||||
last_status: Option<PortStatusSnapshot>,
|
||||
handle: XhciClientHandle,
|
||||
attached: bool,
|
||||
}
|
||||
@@ -187,7 +348,7 @@ fn main() {
|
||||
match self.handle.attach() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: attach failed for port: {}", e);
|
||||
log::warn!("usbhubd: attach failed for port {}: {}", self.port_id, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -195,7 +356,7 @@ fn main() {
|
||||
match self.handle.detach() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: detach failed for port: {}", e);
|
||||
log::warn!("usbhubd: detach failed for port {}: {}", self.port_id, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -210,11 +371,7 @@ fn main() {
|
||||
let child_port_id = port_id.child(port).expect("Cannot get child port ID");
|
||||
states.push(PortState {
|
||||
port_id: child_port_id,
|
||||
port_sts: if usb_3 {
|
||||
usb::HubPortStatus::V3(usb::HubPortStatusV3::default())
|
||||
} else {
|
||||
usb::HubPortStatus::V2(usb::HubPortStatusV2::default())
|
||||
},
|
||||
last_status: None,
|
||||
handle: XhciClientHandle::new(scheme.clone(), child_port_id)
|
||||
.expect("Failed to open XhciClientHandle"),
|
||||
attached: false,
|
||||
@@ -278,87 +435,50 @@ fn main() {
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let port_sts = if usb_3 {
|
||||
let mut port_sts = usb::HubPortStatusV3::default();
|
||||
match handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
) {
|
||||
Ok(()) => usb::HubPortStatus::V3(port_sts),
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut port_sts = usb::HubPortStatusV2::default();
|
||||
match handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
) {
|
||||
Ok(()) => usb::HubPortStatus::V2(port_sts),
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
let mut snap = match fetch_status(&handle, port, usb_3) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if state.port_sts != port_sts {
|
||||
state.port_sts = port_sts;
|
||||
log::info!("port {} status {:X?}", port, port_sts);
|
||||
if state.last_status != Some(snap) {
|
||||
state.last_status = Some(snap);
|
||||
log::info!("port {} status {:?}", port, snap);
|
||||
}
|
||||
|
||||
// Ensure port is powered on.
|
||||
// Linux 7.1 hub_power_on(): issue SET_FEATURE(PORT_POWER),
|
||||
// then sleep bPwrOn2PwrGood * 2ms (minimum 100ms).
|
||||
if !port_sts.is_powered() {
|
||||
// then sleep max(bPwrOn2PwrGood * 2ms, 100ms).
|
||||
if !snap.powered {
|
||||
log::info!("power on port {port}");
|
||||
if let Err(e) = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortPower as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
) {
|
||||
if let Err(e) = set_feature(&handle, port, usb::HubPortFeature::PortPower) {
|
||||
log::warn!("usbhubd: SetPortPower failed for port {}: {}", port, e);
|
||||
continue;
|
||||
}
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
|
||||
// USB 3.0: the link can sit in Polling after power-on;
|
||||
// wait for it to reach U0 before touching the port
|
||||
// (tPollingLFPSTimeout = 360ms; Linux hub.c:1226).
|
||||
if usb_3 {
|
||||
if let Err(e) = port_ops::wait_for_u0(
|
||||
| | fetch_status(&handle, port, true),
|
||||
|ms| thread::sleep(time::Duration::from_millis(ms)),
|
||||
) {
|
||||
log::warn!("usbhubd: port {} polling→U0 wait: {}", port, e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Linux 7.1: over-current detection and recovery.
|
||||
// C_PORT_OVERCURRENT → log, clear, power-cycle.
|
||||
if port_sts.is_over_current_changed() {
|
||||
if snap.over_current_changed {
|
||||
log::warn!("usbhubd: over-current change on port {} — power cycling", port);
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::ClearFeature as u8,
|
||||
usb::HubPortFeature::CPortOverCurrent as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::ClearFeature as u8,
|
||||
usb::HubPortFeature::PortPower as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
let _ = clear_feature(&handle, port, usb::HubPortFeature::CPortOverCurrent);
|
||||
let _ = clear_feature(&handle, port, usb::HubPortFeature::PortPower);
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
|
||||
continue;
|
||||
@@ -366,52 +486,81 @@ fn main() {
|
||||
|
||||
// Linux 7.1: port indicator — set amber during reset/power-on,
|
||||
// green when enabled. Helps diagnose which port is active.
|
||||
if port_sts.is_connected() && port_sts.is_enabled() && !port_sts.is_resetting() {
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortIndicator as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
if snap.connected && snap.enabled && !snap.resetting {
|
||||
let _ = set_feature(&handle, port, usb::HubPortFeature::PortIndicator);
|
||||
}
|
||||
|
||||
// Ignore disconnected port
|
||||
if !port_sts.is_connected() {
|
||||
if !snap.connected {
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore port in reset
|
||||
if port_sts.is_resetting() {
|
||||
if snap.resetting {
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Linux 7.1 hub_port_debounce_be_connected(): on a
|
||||
// connection-change event, require the connection bit to stay
|
||||
// stable for 100ms (25ms steps, 2s budget) before enumerating.
|
||||
if snap.connection_changed && !state.attached {
|
||||
match port_ops::debounce_until_connected(
|
||||
| | fetch_status(&handle, port, usb_3),
|
||||
| | clear_feature(&handle, port, usb::HubPortFeature::CPortConnection),
|
||||
|ms| thread::sleep(time::Duration::from_millis(ms)),
|
||||
) {
|
||||
Ok(s) => {
|
||||
snap = s;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: port {} debounce: {}", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !snap.connected {
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure port is enabled.
|
||||
// Linux 7.1 hub_port_reset(): issue SET_FEATURE(PORT_RESET),
|
||||
// then wait for reset completion (up to USB_PORT_RESET_TIMEOUT
|
||||
// = 5000ms for USB 3, 1000ms for USB 2).
|
||||
if !port_sts.is_enabled() {
|
||||
// then hub_port_wait_reset(): poll until RESET clears with
|
||||
// CONNECTION set (10ms steps escalating to 200ms, 800ms budget),
|
||||
// then 50ms TRSTRCY recovery and C_PORT_RESET clear.
|
||||
if !snap.enabled {
|
||||
log::info!("reset port {port}");
|
||||
if let Err(e) = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortReset as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
) {
|
||||
if let Err(e) = set_feature(&handle, port, usb::HubPortFeature::PortReset) {
|
||||
log::warn!("usbhubd: SetPortReset failed for port {}: {}", port, e);
|
||||
continue;
|
||||
}
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
if let Err(e) = port_ops::wait_for_reset(
|
||||
| | fetch_status(&handle, port, usb_3),
|
||||
| | clear_feature(&handle, port, usb::HubPortFeature::CPortReset),
|
||||
|ms| thread::sleep(time::Duration::from_millis(ms)),
|
||||
) {
|
||||
log::warn!("usbhubd: port {} reset wait: {}", port, e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear the enable-change bit once handled (Linux port_event).
|
||||
if snap.enable_changed {
|
||||
let _ = clear_feature(&handle, port, usb::HubPortFeature::CPortEnable);
|
||||
}
|
||||
|
||||
let was_attached = state.attached;
|
||||
state.ensure_attached(true);
|
||||
// New SuperSpeed child: deliver the accumulated hub delay so the
|
||||
// device can compensate isochronous scheduling (Linux
|
||||
// usb_set_isoch_delay after enumeration, hub.c:5126-5131).
|
||||
if !was_attached && state.attached && usb_3 && hub_delay_ns > 0 {
|
||||
maybe_send_isoch_delay(&state.handle, hub_delay_ns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
//! Hub port state-machine operations ported from Linux 7.1
|
||||
//! `drivers/usb/core/hub.c`.
|
||||
//!
|
||||
//! Pure logic with side effects injected as closures: production wiring
|
||||
//! in `main.rs` supplies real hub-class control transfers and sleeps;
|
||||
//! unit tests supply scripted fakes. Covered algorithms:
|
||||
//! - `debounce_until_connected` — `hub_port_debounce_be_connected()` (hub.c:4696)
|
||||
//! - `wait_for_reset` — `hub_port_wait_reset()` (hub.c:2953) + TRSTRCY recovery + C_PORT_RESET clear
|
||||
//! - `wait_for_u0` — USB 3.0 polling→U0 wait after port power-on (hub.c:1226, tPollingLFPSTimeout)
|
||||
//! - `accumulate_hub_delay_ns` — wHubDelay chain rule (hub.c:1507-1519)
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use xhcid_interface::usb;
|
||||
|
||||
// ---- Linux 7.1 timing constants ----
|
||||
|
||||
/// hub.c:138 — maximum total debounce time.
|
||||
pub const HUB_DEBOUNCE_TIMEOUT_MS: u64 = 2000;
|
||||
/// hub.c:139 — debounce poll interval.
|
||||
pub const HUB_DEBOUNCE_STEP_MS: u64 = 25;
|
||||
/// hub.c:140 — required stable-connection duration.
|
||||
pub const HUB_DEBOUNCE_STABLE_MS: u64 = 100;
|
||||
/// hub.c:2902 — initial reset wait step.
|
||||
pub const HUB_SHORT_RESET_TIME_MS: u64 = 10;
|
||||
/// hub.c:2904 — escalated reset wait step after two short waits.
|
||||
pub const HUB_LONG_RESET_TIME_MS: u64 = 200;
|
||||
/// hub.c:2905 — total reset wait budget.
|
||||
pub const HUB_RESET_TIMEOUT_MS: u64 = 800;
|
||||
/// hub.c:3159 — post-reset recovery: TRSTRCY (10 ms) + 40 ms slack.
|
||||
pub const RESET_RECOVERY_TIME_MS: u64 = 50;
|
||||
/// USB 3.0 tPollingLFPSTimeout = 360 ms, polled as 10 × 36 ms.
|
||||
pub const USB3_U0_WAIT_STEP_MS: u64 = 36;
|
||||
/// Bounded ceiling for the U0 wait (spec timeout + slack).
|
||||
pub const USB3_U0_WAIT_TIMEOUT_MS: u64 = 400;
|
||||
/// xHCI PLS value for the Polling link state (USB 3.0 port status bits 8:5).
|
||||
pub const USB_SS_PORT_LS_POLLING: u8 = 0x7;
|
||||
|
||||
// ---- wHubDelay chain constants (hub.c:62-63) ----
|
||||
|
||||
/// tTPTransmissionDelay per hub hop.
|
||||
pub const USB_TP_TRANSMISSION_DELAY_NS: u32 = 40;
|
||||
/// Spec ceiling for the accumulated delay (u16 transport).
|
||||
pub const USB_TP_TRANSMISSION_DELAY_MAX_NS: u32 = 65535;
|
||||
|
||||
/// Normalized view of one hub port status read, covering every field the
|
||||
/// state machines consume. Decoupled from the wire format so tests can
|
||||
/// construct sequences directly; production builds it from
|
||||
/// `usb::HubPortStatus`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PortStatusSnapshot {
|
||||
pub connected: bool,
|
||||
pub enabled: bool,
|
||||
pub resetting: bool,
|
||||
pub powered: bool,
|
||||
pub connection_changed: bool,
|
||||
pub enable_changed: bool,
|
||||
pub reset_changed: bool,
|
||||
pub over_current_changed: bool,
|
||||
/// USB 3.0 link state (port status bits 8:5); `None` on USB 2.0 hubs.
|
||||
pub link_state: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<&usb::HubPortStatus> for PortStatusSnapshot {
|
||||
fn from(sts: &usb::HubPortStatus) -> Self {
|
||||
let (connection_changed, enable_changed, reset_changed, link_state) = match sts {
|
||||
usb::HubPortStatus::V2(x) => (
|
||||
x.contains(usb::HubPortStatusV2::CONNECTION_CHANGED),
|
||||
x.contains(usb::HubPortStatusV2::ENABLE_CHANGED),
|
||||
x.contains(usb::HubPortStatusV2::RESET_CHANGED),
|
||||
None,
|
||||
),
|
||||
usb::HubPortStatus::V3(x) => (
|
||||
x.contains(usb::HubPortStatusV3::CONNECTION_CHANGED),
|
||||
// USB 3.0 port status has no ENABLE_CHANGED bit (bits 17-18 reserved).
|
||||
false,
|
||||
x.contains(usb::HubPortStatusV3::RESET_CHANGED),
|
||||
Some(((x.bits() >> 5) & 0xF) as u8),
|
||||
),
|
||||
};
|
||||
Self {
|
||||
connected: sts.is_connected(),
|
||||
enabled: sts.is_enabled(),
|
||||
resetting: sts.is_resetting(),
|
||||
powered: sts.is_powered(),
|
||||
connection_changed,
|
||||
enable_changed,
|
||||
reset_changed,
|
||||
over_current_changed: sts.is_over_current_changed(),
|
||||
link_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure modes shared by the wait state machines.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum WaitError<E> {
|
||||
/// Total time budget exhausted (Linux: -ETIMEDOUT / -EBUSY).
|
||||
Timeout,
|
||||
/// Connection lost during the wait (Linux: -ENOTCONN).
|
||||
Disconnected,
|
||||
/// The injected status-fetch or feature-clear closure failed.
|
||||
Io(E),
|
||||
}
|
||||
|
||||
impl<E: fmt::Debug> fmt::Display for WaitError<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Timeout => write!(f, "timed out"),
|
||||
Self::Disconnected => write!(f, "device disconnected during wait"),
|
||||
Self::Io(e) => write!(f, "I/O error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: fmt::Debug> std::error::Error for WaitError<E> {}
|
||||
|
||||
/// Linux 7.1 `hub_port_debounce_be_connected()` (hub.c:4696-4737).
|
||||
///
|
||||
/// Poll the port until the connection bit stays continuously set for
|
||||
/// `HUB_DEBOUNCE_STABLE_MS`, sampling every `HUB_DEBOUNCE_STEP_MS` with a
|
||||
/// total budget of `HUB_DEBOUNCE_TIMEOUT_MS`. Any connection-change event
|
||||
/// or state flip restarts the stable window. The change bit is cleared
|
||||
/// during the loop so subsequent reads reflect hardware, not the
|
||||
/// original event. Returns the final stable status.
|
||||
pub fn debounce_until_connected<E>(
|
||||
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
|
||||
mut clear_connection_change: impl FnMut() -> Result<(), E>,
|
||||
mut sleep: impl FnMut(u64),
|
||||
) -> Result<PortStatusSnapshot, WaitError<E>> {
|
||||
let mut stable_ms = 0u64;
|
||||
let mut total_ms = 0u64;
|
||||
// Linux seeds with 0xffff ("unknown") so the first read always
|
||||
// (re)starts the window.
|
||||
let mut connection: Option<bool> = None;
|
||||
|
||||
loop {
|
||||
let cur = fetch().map_err(WaitError::Io)?;
|
||||
|
||||
if !cur.connection_changed && connection == Some(cur.connected) {
|
||||
stable_ms += HUB_DEBOUNCE_STEP_MS;
|
||||
if stable_ms >= HUB_DEBOUNCE_STABLE_MS {
|
||||
return if cur.connected {
|
||||
Ok(cur)
|
||||
} else {
|
||||
Err(WaitError::Disconnected)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
stable_ms = 0;
|
||||
connection = Some(cur.connected);
|
||||
}
|
||||
|
||||
if cur.connection_changed {
|
||||
clear_connection_change().map_err(WaitError::Io)?;
|
||||
}
|
||||
|
||||
if total_ms >= HUB_DEBOUNCE_TIMEOUT_MS {
|
||||
return Err(WaitError::Timeout);
|
||||
}
|
||||
total_ms += HUB_DEBOUNCE_STEP_MS;
|
||||
sleep(HUB_DEBOUNCE_STEP_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux 7.1 `hub_port_wait_reset()` (hub.c:2953-3047) followed by the
|
||||
/// TRSTRCY recovery sleep from `hub_port_reset()` (hub.c:3159) and the
|
||||
/// C_PORT_RESET clear from `port_event()` (hub.c:5803).
|
||||
///
|
||||
/// Call after SET_FEATURE(PORT_RESET). Polls until RESET clears with
|
||||
/// CONNECTION still set: 10 ms steps, escalating to 200 ms steps after
|
||||
/// two short waits, within an 800 ms total budget. On completion, sleeps
|
||||
/// `RESET_RECOVERY_TIME_MS` (50 ms) and clears the reset-change bit.
|
||||
/// A reset that completes without ENABLE is not treated as an error
|
||||
/// here — the caller's outer loop re-issues the reset, matching Linux's
|
||||
/// PORT_RESET_TRIES retry through `hub_port_reset()`.
|
||||
pub fn wait_for_reset<E>(
|
||||
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
|
||||
mut clear_reset_change: impl FnMut() -> Result<(), E>,
|
||||
mut sleep: impl FnMut(u64),
|
||||
) -> Result<PortStatusSnapshot, WaitError<E>> {
|
||||
let mut total_ms = 0u64;
|
||||
let mut delay = HUB_SHORT_RESET_TIME_MS;
|
||||
|
||||
let mut cur = fetch().map_err(WaitError::Io)?;
|
||||
loop {
|
||||
if !cur.resetting && cur.connected {
|
||||
sleep(RESET_RECOVERY_TIME_MS);
|
||||
if cur.reset_changed {
|
||||
clear_reset_change().map_err(WaitError::Io)?;
|
||||
}
|
||||
return Ok(cur);
|
||||
}
|
||||
if !cur.connected {
|
||||
return Err(WaitError::Disconnected);
|
||||
}
|
||||
if total_ms >= HUB_RESET_TIMEOUT_MS {
|
||||
return Err(WaitError::Timeout);
|
||||
}
|
||||
sleep(delay);
|
||||
total_ms += delay;
|
||||
// hub.c:2993-2995 — escalate after two short waits.
|
||||
if total_ms >= 2 * HUB_SHORT_RESET_TIME_MS {
|
||||
delay = HUB_LONG_RESET_TIME_MS;
|
||||
}
|
||||
cur = fetch().map_err(WaitError::Io)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// USB 3.0 polling→U0 wait after port power-on (hub.c:1226-1229;
|
||||
/// tPollingLFPSTimeout = 360 ms from USB 3.0 §9.4.2.1).
|
||||
///
|
||||
/// A downstream port can sit in the Polling link state for some time
|
||||
/// after power is applied; enumeration must wait for the link to leave
|
||||
/// Polling. Polls every `USB3_U0_WAIT_STEP_MS` up to
|
||||
/// `USB3_U0_WAIT_TIMEOUT_MS`. Returns immediately when the port is not
|
||||
/// in Polling (including USB 2 hubs and empty ports).
|
||||
pub fn wait_for_u0<E>(
|
||||
mut fetch: impl FnMut() -> Result<PortStatusSnapshot, E>,
|
||||
mut sleep: impl FnMut(u64),
|
||||
) -> Result<(), WaitError<E>> {
|
||||
let mut total_ms = 0u64;
|
||||
loop {
|
||||
let cur = fetch().map_err(WaitError::Io)?;
|
||||
let polling = cur.link_state == Some(USB_SS_PORT_LS_POLLING);
|
||||
if !polling || !cur.connected {
|
||||
return Ok(());
|
||||
}
|
||||
if total_ms >= USB3_U0_WAIT_TIMEOUT_MS {
|
||||
return Err(WaitError::Timeout);
|
||||
}
|
||||
total_ms += USB3_U0_WAIT_STEP_MS;
|
||||
sleep(USB3_U0_WAIT_STEP_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux 7.1 hub.c:1507-1519 chain rule for a hub's own accumulated
|
||||
/// delay: `wHubDelay + parent->hub_delay + tTPTransmissionDelay`,
|
||||
/// clamped to 65535 ns. Child devices behind this hub inherit the
|
||||
/// result verbatim for SetIsochDelay (hub.c:5128-5129).
|
||||
pub fn accumulate_hub_delay_ns(own_w_hub_delay: u32, parent_accumulated: u32) -> u32 {
|
||||
own_w_hub_delay
|
||||
.saturating_add(parent_accumulated)
|
||||
.saturating_add(USB_TP_TRANSMISSION_DELAY_NS)
|
||||
.min(USB_TP_TRANSMISSION_DELAY_MAX_NS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
fn snap() -> PortStatusSnapshot {
|
||||
PortStatusSnapshot::default()
|
||||
}
|
||||
|
||||
fn connected() -> PortStatusSnapshot {
|
||||
PortStatusSnapshot {
|
||||
connected: true,
|
||||
powered: true,
|
||||
..snap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Scripted test harness: queued status reads, recorded sleeps and clears.
|
||||
/// Once the queue is exhausted, the last queued snapshot repeats forever.
|
||||
struct Rig {
|
||||
reads: VecDeque<PortStatusSnapshot>,
|
||||
last: PortStatusSnapshot,
|
||||
sleeps: Vec<u64>,
|
||||
clears: u32,
|
||||
}
|
||||
|
||||
impl Rig {
|
||||
fn new(reads: impl IntoIterator<Item = PortStatusSnapshot>) -> Self {
|
||||
let reads: VecDeque<_> = reads.into_iter().collect();
|
||||
let last = reads.front().copied().unwrap_or_default();
|
||||
Self {
|
||||
reads,
|
||||
last,
|
||||
sleeps: Vec::new(),
|
||||
clears: 0,
|
||||
}
|
||||
}
|
||||
fn fetch(&mut self) -> Result<PortStatusSnapshot, ()> {
|
||||
if let Some(s) = self.reads.pop_front() {
|
||||
self.last = s;
|
||||
}
|
||||
Ok(self.last)
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), ()> {
|
||||
self.clears += 1;
|
||||
Ok(())
|
||||
}
|
||||
fn sleep(&mut self, ms: u64) {
|
||||
self.sleeps.push(ms);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debounce_stable_immediately() {
|
||||
let rig = RefCell::new(Rig::new([connected(), connected(), connected(), connected()]));
|
||||
let out = debounce_until_connected(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(connected()));
|
||||
// First read seeds the window; four stable steps of 25 ms reach 100 ms.
|
||||
assert_eq!(rig.sleeps, vec![25, 25, 25, 25]);
|
||||
assert_eq!(rig.clears, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debounce_flap_restarts_window() {
|
||||
let rig = RefCell::new(Rig::new([
|
||||
connected(),
|
||||
snap(), // flap: disconnect
|
||||
connected(), // window restarts here
|
||||
connected(),
|
||||
connected(),
|
||||
connected(),
|
||||
]));
|
||||
let out = debounce_until_connected(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(connected()));
|
||||
assert_eq!(rig.sleeps.len(), 6);
|
||||
assert!(rig.sleeps.iter().all(|&s| s == HUB_DEBOUNCE_STEP_MS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debounce_stable_disconnect_errors() {
|
||||
let rig = RefCell::new(Rig::new([snap(), snap(), snap(), snap()]));
|
||||
let out = debounce_until_connected(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
assert_eq!(out, Err(WaitError::Disconnected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debounce_change_bit_cleared_and_timeout() {
|
||||
let flapping = PortStatusSnapshot {
|
||||
connection_changed: true,
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new(
|
||||
(0..200).map(|i| if i % 2 == 0 { flapping } else { connected() }),
|
||||
));
|
||||
let out = debounce_until_connected(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Err(WaitError::Timeout));
|
||||
assert!(rig.clears > 0);
|
||||
assert_eq!(rig.sleeps.iter().sum::<u64>(), HUB_DEBOUNCE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_completes_after_two_polls() {
|
||||
let resetting = PortStatusSnapshot {
|
||||
resetting: true,
|
||||
..connected()
|
||||
};
|
||||
let done = PortStatusSnapshot {
|
||||
enabled: true,
|
||||
reset_changed: true,
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([resetting, resetting, done]));
|
||||
let out = wait_for_reset(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(done));
|
||||
assert_eq!(rig.sleeps, vec![10, 10, RESET_RECOVERY_TIME_MS]);
|
||||
assert_eq!(rig.clears, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_escalates_to_long_step() {
|
||||
let resetting = PortStatusSnapshot {
|
||||
resetting: true,
|
||||
..connected()
|
||||
};
|
||||
let done = connected();
|
||||
let rig = RefCell::new(Rig::new([resetting, resetting, resetting, resetting, done]));
|
||||
let out = wait_for_reset(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(done));
|
||||
assert_eq!(rig.sleeps[0], HUB_SHORT_RESET_TIME_MS);
|
||||
assert_eq!(rig.sleeps[1], HUB_SHORT_RESET_TIME_MS);
|
||||
assert_eq!(rig.sleeps[2], HUB_LONG_RESET_TIME_MS);
|
||||
assert_eq!(*rig.sleeps.last().unwrap(), RESET_RECOVERY_TIME_MS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_timeout() {
|
||||
let resetting = PortStatusSnapshot {
|
||||
resetting: true,
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([resetting]));
|
||||
let out = wait_for_reset(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
assert_eq!(out, Err(WaitError::Timeout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_disconnect_mid_wait() {
|
||||
let resetting = PortStatusSnapshot {
|
||||
resetting: true,
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([resetting, snap()]));
|
||||
let out = wait_for_reset(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|| rig.borrow_mut().clear(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Err(WaitError::Disconnected));
|
||||
assert_eq!(rig.sleeps, vec![HUB_SHORT_RESET_TIME_MS]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u0_wait_leaves_polling() {
|
||||
let polling = PortStatusSnapshot {
|
||||
link_state: Some(USB_SS_PORT_LS_POLLING),
|
||||
..connected()
|
||||
};
|
||||
let u0 = PortStatusSnapshot {
|
||||
link_state: Some(0),
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([polling, polling, polling, u0]));
|
||||
let out = wait_for_u0(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(()));
|
||||
assert_eq!(rig.sleeps, vec![36, 36, 36]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u0_wait_skips_non_polling_and_empty_ports() {
|
||||
let rx_detect = PortStatusSnapshot {
|
||||
link_state: Some(5),
|
||||
..snap()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([rx_detect]));
|
||||
let out = wait_for_u0(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
let rig = rig.into_inner();
|
||||
assert_eq!(out, Ok(()));
|
||||
assert!(rig.sleeps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u0_wait_timeout() {
|
||||
let polling = PortStatusSnapshot {
|
||||
link_state: Some(USB_SS_PORT_LS_POLLING),
|
||||
..connected()
|
||||
};
|
||||
let rig = RefCell::new(Rig::new([polling]));
|
||||
let out = wait_for_u0(
|
||||
|| rig.borrow_mut().fetch(),
|
||||
|ms| rig.borrow_mut().sleep(ms),
|
||||
);
|
||||
assert_eq!(out, Err(WaitError::Timeout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_delay_chain_and_cap() {
|
||||
// First-tier hub: parent (root) accumulated = 0.
|
||||
assert_eq!(accumulate_hub_delay_ns(100, 0), 140);
|
||||
// Second-tier: own + parent chain + 40.
|
||||
assert_eq!(accumulate_hub_delay_ns(200, 140), 380);
|
||||
// Cap at 65535 ns.
|
||||
assert_eq!(accumulate_hub_delay_ns(65500, 65500), 65535);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_from_v3_extracts_link_state() {
|
||||
let raw = usb::HubPortStatusV3::CONNECTION
|
||||
| usb::HubPortStatusV3::POWER
|
||||
| usb::HubPortStatusV3::LINK_STATE_0
|
||||
| usb::HubPortStatusV3::LINK_STATE_1; // link state 0b0011 = 3
|
||||
let snap = PortStatusSnapshot::from(&usb::HubPortStatus::V3(raw));
|
||||
assert!(snap.connected);
|
||||
assert!(snap.powered);
|
||||
assert_eq!(snap.link_state, Some(3));
|
||||
assert!(!snap.enable_changed); // reserved on V3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_from_v2_has_no_link_state() {
|
||||
let raw = usb::HubPortStatusV2::CONNECTION
|
||||
| usb::HubPortStatusV2::POWER
|
||||
| usb::HubPortStatusV2::ENABLE_CHANGED;
|
||||
let snap = PortStatusSnapshot::from(&usb::HubPortStatus::V2(raw));
|
||||
assert!(snap.connected);
|
||||
assert!(snap.enable_changed);
|
||||
assert_eq!(snap.link_state, None);
|
||||
}
|
||||
}
|
||||
@@ -47,17 +47,23 @@ pub struct HubDescriptorV3 {
|
||||
pub current: u8,
|
||||
pub decode_latency: u8,
|
||||
pub delay: u16,
|
||||
/*TODO: USB 2 and 3 disagree on the descriptor, so some fields are disabled
|
||||
// device_removable: bitmap of ports, maximum of 256 bits (32 bytes)
|
||||
// power_control_mask: bitmap of ports, maximum of 256 bits (32 bytes)
|
||||
bitmaps: [u8; 64],
|
||||
*/
|
||||
/// Bitmap of non-removable ports: bit N set = port N+1 is permanently
|
||||
/// attached. USB 3.0 spec Table 10-15 — this is the final field of
|
||||
/// the 12-byte SuperSpeed hub descriptor (SS descriptors have no
|
||||
/// PortPwrCtrlMask; that exists only in the variable-length USB 2.0
|
||||
/// descriptor, whose bitmaps are still not parsed).
|
||||
pub device_removable: u16,
|
||||
}
|
||||
|
||||
unsafe impl plain::Plain for HubDescriptorV3 {}
|
||||
|
||||
impl HubDescriptorV3 {
|
||||
pub const DESCRIPTOR_KIND: u8 = 0x2A;
|
||||
|
||||
/// Whether the given 1-based port number is marked non-removable.
|
||||
pub fn is_port_non_removable(&self, port: u8) -> bool {
|
||||
port >= 1 && (self.device_removable >> (port - 1)) & 1 == 1
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HubDescriptorV3 {
|
||||
@@ -71,9 +77,7 @@ impl Default for HubDescriptorV3 {
|
||||
current: 0,
|
||||
decode_latency: 0,
|
||||
delay: 0,
|
||||
/*
|
||||
bitmaps: [0; 64],
|
||||
*/
|
||||
device_removable: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user