usb: work on SCSI
This commit is contained in:
parent
7515369984
commit
2e5a437e59
@ -28,3 +28,6 @@ heapless = "0.7.10"
|
|||||||
# for HID
|
# for HID
|
||||||
usbd-hid = { version = "0.6.0", optional = true }
|
usbd-hid = { version = "0.6.0", optional = true }
|
||||||
ssmarshal = { version = "1.0", default-features = false, optional = true }
|
ssmarshal = { version = "1.0", default-features = false, optional = true }
|
||||||
|
|
||||||
|
paste = "1.0"
|
||||||
|
num_enum = { version = "0.5", default-features = false }
|
||||||
|
132
embassy-usb/src/class/msc/subclass/scsi/bitfield.rs
Normal file
132
embassy-usb/src/class/msc/subclass/scsi/bitfield.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
pub trait Packet {
|
||||||
|
fn data(&self) -> &[u8];
|
||||||
|
fn data_mut(&mut self) -> &mut [u8];
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get<F: AsPacketField>(&self, field: &F) -> F::T {
|
||||||
|
field.get(self.data())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set<F: AsPacketField>(&mut self, field: &F, val: F::T) {
|
||||||
|
field.set(self.data_mut(), val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AsPacketField {
|
||||||
|
type T;
|
||||||
|
|
||||||
|
fn get(&self, data: &[u8]) -> Self::T;
|
||||||
|
fn set(&self, data: &mut [u8], val: Self::T);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BitField {
|
||||||
|
pub byte: usize,
|
||||||
|
pub bit: u8,
|
||||||
|
pub mask: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitField {
|
||||||
|
pub const fn new(byte: usize, bit: u8, size: u8) -> Self {
|
||||||
|
let mask = (0xFF >> size) << bit;
|
||||||
|
Self { byte, bit, mask }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsPacketField for BitField {
|
||||||
|
type T = u8;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get(&self, data: &[u8]) -> Self::T {
|
||||||
|
(data[self.byte] & self.mask) >> self.bit
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(&self, data: &mut [u8], val: Self::T) {
|
||||||
|
data[self.byte] = (val << self.bit) | (data[self.byte] & !self.mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BoolField {
|
||||||
|
pub byte: usize,
|
||||||
|
pub bit: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoolField {
|
||||||
|
pub const fn new(byte: usize, bit: u8) -> Self {
|
||||||
|
Self { byte, bit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsPacketField for BoolField {
|
||||||
|
type T = bool;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get(&self, data: &[u8]) -> Self::T {
|
||||||
|
data[self.byte] & (1 << self.bit) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(&self, data: &mut [u8], val: Self::T) {
|
||||||
|
data[self.byte] = ((val as u8) << self.bit) | (data[self.byte] & !(1 << self.bit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Field<T> {
|
||||||
|
pub byte: usize,
|
||||||
|
_phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Field<T> {
|
||||||
|
pub const fn new(byte: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
byte,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AsPacketField for Field<T> {
|
||||||
|
type T = T;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get(&self, data: &[u8]) -> Self::T {
|
||||||
|
unsafe { core::ptr::read(data.as_ptr().offset(self.byte as _) as *const T) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(&self, data: &mut [u8], val: Self::T) {
|
||||||
|
unsafe { core::ptr::write(data.as_mut_ptr().offset(self.byte as _) as *mut T, val) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{AsPacketField, BitField, BoolField};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bitfield() {
|
||||||
|
let field = BitField::new(0, 4, 3);
|
||||||
|
|
||||||
|
let mut data = [0b1111_1111];
|
||||||
|
assert_eq!(field.get(&data), 0b111);
|
||||||
|
|
||||||
|
field.set(&mut data, 0b000);
|
||||||
|
assert_eq!(field.get(&data), 0b000);
|
||||||
|
assert_eq!(data, [0b1000_1111]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boolfield() {
|
||||||
|
let field = BoolField::new(0, 5);
|
||||||
|
|
||||||
|
let mut data = [0b1111_1111];
|
||||||
|
assert_eq!(field.get(&data), true);
|
||||||
|
|
||||||
|
field.set(&mut data, false);
|
||||||
|
assert_eq!(field.get(&data), false);
|
||||||
|
assert_eq!(data, [0b1101_1111]);
|
||||||
|
}
|
||||||
|
}
|
19
embassy-usb/src/class/msc/subclass/scsi/block_device.rs
Normal file
19
embassy-usb/src/class/msc/subclass/scsi/block_device.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BlockDeviceError {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BlockDevice {
|
||||||
|
/// The number of bytes per block. This determines the size of the buffer passed
|
||||||
|
/// to read/write functions
|
||||||
|
fn block_size(&self) -> usize;
|
||||||
|
|
||||||
|
/// Number of blocks in device (max LBA index)
|
||||||
|
fn num_blocks(&self) -> u32;
|
||||||
|
|
||||||
|
/// Read the block indicated by `lba` into the provided buffer
|
||||||
|
fn read_block(&self, lba: u32, block: &mut [u8]) -> Result<(), BlockDeviceError>;
|
||||||
|
|
||||||
|
/// Write the `block` buffer to the block indicated by `lba`
|
||||||
|
fn write_block(&mut self, lba: u32, block: &[u8]) -> Result<(), BlockDeviceError>;
|
||||||
|
}
|
10
embassy-usb/src/class/msc/subclass/scsi/commands/control.rs
Normal file
10
embassy-usb/src/class/msc/subclass/scsi/commands/control.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// #[bitfield(bytes = 1)]
|
||||||
|
// #[derive(BitfieldSpecifier)]
|
||||||
|
// pub struct Control {
|
||||||
|
// pub vendor_specific: B2,
|
||||||
|
// #[skip]
|
||||||
|
// __: B3,
|
||||||
|
// pub naca: B1,
|
||||||
|
// #[skip]
|
||||||
|
// __: B2,
|
||||||
|
// }
|
17
embassy-usb/src/class/msc/subclass/scsi/commands/inquiry.rs
Normal file
17
embassy-usb/src/class/msc/subclass/scsi/commands/inquiry.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// use super::control::Control;
|
||||||
|
|
||||||
|
// #[bitfield(bytes = 6)]
|
||||||
|
// pub struct InquiryCommand {
|
||||||
|
// /// Always 0x12
|
||||||
|
// pub op_code: B8,
|
||||||
|
// #[skip]
|
||||||
|
// __: B7,
|
||||||
|
// /// If set, return vital data related to the page_code field
|
||||||
|
// pub enable_vital_product_data: B1,
|
||||||
|
// /// What kind of vital data to return
|
||||||
|
// pub page_code: B8,
|
||||||
|
// /// Amount of bytes allocation for data-in transfer
|
||||||
|
// pub allocation_length: B16,
|
||||||
|
// /// Control byte
|
||||||
|
// pub control: Control,
|
||||||
|
// }
|
5
embassy-usb/src/class/msc/subclass/scsi/commands/mod.rs
Normal file
5
embassy-usb/src/class/msc/subclass/scsi/commands/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// `bytes` in `#[bitfield(bytes = 6)]` causes a warning
|
||||||
|
#![allow(redundant_semicolons)]
|
||||||
|
|
||||||
|
pub mod control;
|
||||||
|
pub mod inquiry;
|
55
embassy-usb/src/class/msc/subclass/scsi/enums.rs
Normal file
55
embassy-usb/src/class/msc/subclass/scsi/enums.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use crate::gen_enum;
|
||||||
|
|
||||||
|
gen_enum! {
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub enum PeripheralQualifier<u8> {
|
||||||
|
/// A peripheral device having the specified peripheral device type is connected to this logical unit. If the device server is unable to determine whether or not a peripheral device is connected, it also shall use this peripheral qualifier. This peripheral qualifier does not mean that the peripheral device connected to the logical unit is ready for access.
|
||||||
|
Connected = 0b000,
|
||||||
|
/// A peripheral device having the specified peripheral device type is not connected to this logical unit. However, the device server is capable of supporting the specified peripheral device type on this logical unit.
|
||||||
|
NotConnected = 0b001,
|
||||||
|
/// The device server is not capable of supporting a peripheral device on this logical unit. For this peripheral qualifier the peripheral device type shall be set to 1Fh. All other peripheral device type values are reserved for this peripheral qualifier.
|
||||||
|
Incapable = 0b011,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_enum! {
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub enum PeripheralDeviceType<u8> {
|
||||||
|
/// Direct access block device (e.g., magnetic disk)
|
||||||
|
DirectAccessBlock = 0x00,
|
||||||
|
/// Sequential-access device (e.g., magnetic tape)
|
||||||
|
SequentialAccess = 0x01,
|
||||||
|
/// Printer device
|
||||||
|
Printer = 0x02,
|
||||||
|
/// Processor device
|
||||||
|
Processor = 0x03,
|
||||||
|
/// Write-once device (e.g., some optical disks)
|
||||||
|
WriteOnce = 0x04,
|
||||||
|
/// CD/DVD device
|
||||||
|
CdDvd = 0x05,
|
||||||
|
/// Optical memory device (e.g., some optical disks)
|
||||||
|
OpticalMemory = 0x07,
|
||||||
|
/// Media changer device (e.g., jukeboxes)
|
||||||
|
MediaChanger = 0x08,
|
||||||
|
/// Storage array controller device (e.g., RAID)
|
||||||
|
StorageArrayController = 0x0C,
|
||||||
|
/// Enclosure services device
|
||||||
|
EnclosureServices = 0x0D,
|
||||||
|
/// Simplified direct-access device (e.g., magnetic disk)
|
||||||
|
SimplifiedDirectAccess = 0x0E,
|
||||||
|
/// Optical card reader/writer device
|
||||||
|
OpticaCardReaderWriter = 0x0F,
|
||||||
|
/// Bridge Controller Commands
|
||||||
|
BridgeController = 0x10,
|
||||||
|
/// Object-based Storage Device
|
||||||
|
ObjectBasedStorage = 0x11,
|
||||||
|
/// Automation/Drive Interface
|
||||||
|
AutomationInterface = 0x12,
|
||||||
|
/// Security manager device
|
||||||
|
SecurityManager = 0x13,
|
||||||
|
/// Well known logical unit
|
||||||
|
WellKnownLogicalUnit = 0x1E,
|
||||||
|
/// Unknown or no device type
|
||||||
|
UnknownOrNone = 0x1F,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
// pub mod bitfield;
|
||||||
|
pub mod block_device;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod enums;
|
||||||
|
pub mod packet;
|
||||||
|
pub mod responses;
|
||||||
|
|
||||||
|
use self::block_device::BlockDevice;
|
||||||
|
use crate::class::msc::transport::{self, CommandSetHandler};
|
||||||
|
|
||||||
|
pub struct Scsi<B: BlockDevice> {
|
||||||
|
device: B,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: BlockDevice> CommandSetHandler for Scsi<B> {
|
||||||
|
async fn command_out(
|
||||||
|
&mut self,
|
||||||
|
lun: u8,
|
||||||
|
cmd: &[u8],
|
||||||
|
pipe: &mut impl transport::DataPipeOut,
|
||||||
|
) -> Result<(), transport::CommandError> {
|
||||||
|
assert!(lun == 0, "LUNs are not supported");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command_in(
|
||||||
|
&mut self,
|
||||||
|
lun: u8,
|
||||||
|
cmd: &[u8],
|
||||||
|
pipe: &mut impl transport::DataPipeIn,
|
||||||
|
) -> Result<(), transport::CommandError> {
|
||||||
|
assert!(lun == 0, "LUNs are not supported");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
151
embassy-usb/src/class/msc/subclass/scsi/packet.rs
Normal file
151
embassy-usb/src/class/msc/subclass/scsi/packet.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
pub trait BitField {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
fn get(data: &[u8], offset: usize, size: usize) -> Self::Output;
|
||||||
|
fn set(data: &mut [u8], offset: usize, size: usize, val: Self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitField for u8 {
|
||||||
|
type Output = u8;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get(data: &[u8], offset: usize, size: usize) -> Self::Output {
|
||||||
|
let byte = offset / 8;
|
||||||
|
let bit = offset % 8;
|
||||||
|
let mask = (0xFF >> size) << bit;
|
||||||
|
(data[byte] & mask) >> bit
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(data: &mut [u8], offset: usize, size: usize, val: Self) {
|
||||||
|
let byte = offset / 8;
|
||||||
|
let bit = offset % 8;
|
||||||
|
data[byte] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! gen_packet {
|
||||||
|
(
|
||||||
|
$(#[$meta:meta])*
|
||||||
|
$sv:vis struct $name:ident<$size:literal> {
|
||||||
|
$(
|
||||||
|
#[offset = $offset:expr, size = $bit_size:expr]
|
||||||
|
$field:ident: $ty:ty,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
$sv struct $name<T: AsRef<[u8]>> {
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $name<[u8; $size]> {
|
||||||
|
const SIZE: usize = $size;
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
data: [0u8; Self::SIZE]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> $name<T> {
|
||||||
|
pub const unsafe fn new_unchecked(data: T) -> Self {
|
||||||
|
Self { data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(buf: T) -> Option<Self> {
|
||||||
|
if buf.as_ref().len() < $name::SIZE {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(unsafe { Self::new_unchecked(buf) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(
|
||||||
|
#[inline]
|
||||||
|
pub fn $field(&self) -> <$ty as crate::class::msc::subclass::scsi::packet::BitField>::Output {
|
||||||
|
const _: () = core::assert!($offset + $bit_size <= $size * 8, "Field offset is out of range");
|
||||||
|
<$ty as crate::class::msc::subclass::scsi::packet::BitField>::get(self.data.as_ref(), $offset, $bit_size)
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + AsMut<[u8]>> $name<T> {
|
||||||
|
$(
|
||||||
|
paste::paste! {
|
||||||
|
#[inline]
|
||||||
|
pub fn [<set_$field>](&mut self, val: $ty) {
|
||||||
|
<$ty as crate::class::msc::subclass::scsi::packet::BitField>::set(self.data.as_mut(), $offset, $bit_size, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_packet!(pub struct Test<8> {
|
||||||
|
// #[offset = 8 * 7, size = 3]
|
||||||
|
// test: u8,
|
||||||
|
// });
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! gen_enum {
|
||||||
|
(
|
||||||
|
$(#[$meta:meta])*
|
||||||
|
$sv:vis enum $name:ident<$ty:ty> {
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta:meta])*
|
||||||
|
$variant:ident = $variant_val:literal,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
$sv enum $name {
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta])*
|
||||||
|
$variant = $variant_val
|
||||||
|
),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<$ty> for $name {
|
||||||
|
type Error = $ty;
|
||||||
|
|
||||||
|
fn try_from(value: $ty) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
$($variant_val => Ok($name::$variant),)*
|
||||||
|
_ => Err(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$name> for $ty {
|
||||||
|
fn from(value: $name) -> $ty {
|
||||||
|
value as $ty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::class::msc::subclass::scsi::packet::BitField for $name {
|
||||||
|
type Output = Result<Self, $ty>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get(data: &[u8], offset: usize, size: usize) -> Self::Output {
|
||||||
|
let val = <$ty as crate::class::msc::subclass::scsi::packet::BitField>::get(data, offset, size);
|
||||||
|
Self::try_from(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(data: &mut [u8], offset: usize, size: usize, val: Self) {
|
||||||
|
<$ty as crate::class::msc::subclass::scsi::packet::BitField>::set(data, offset, size, val.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_enum! {
|
||||||
|
// pub enum Testas<u8> {
|
||||||
|
// Hello = 0b111,
|
||||||
|
// Test = 0b1111,
|
||||||
|
// }
|
||||||
|
// }
|
17
embassy-usb/src/class/msc/subclass/scsi/responses/inquiry.rs
Normal file
17
embassy-usb/src/class/msc/subclass/scsi/responses/inquiry.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use super::super::enums::PeripheralQualifier;
|
||||||
|
use crate::class::msc::subclass::scsi::enums::PeripheralDeviceType;
|
||||||
|
use crate::gen_packet;
|
||||||
|
|
||||||
|
gen_packet! {
|
||||||
|
/// Inquiry response can contain many extensions. We support only the minimum required 36 bytes.
|
||||||
|
pub struct InquiryResponse<36> {
|
||||||
|
#[offset = 0, size = 5]
|
||||||
|
peripheral_qualifier: PeripheralQualifier,
|
||||||
|
#[offset = 5, size = 3]
|
||||||
|
peripheral_device_type: PeripheralDeviceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test() {
|
||||||
|
let packet = InquiryResponse::new();
|
||||||
|
}
|
4
embassy-usb/src/class/msc/subclass/scsi/responses/mod.rs
Normal file
4
embassy-usb/src/class/msc/subclass/scsi/responses/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// `bytes` in `#[bitfield(bytes = 6)]` causes a warning
|
||||||
|
#![allow(redundant_semicolons)]
|
||||||
|
|
||||||
|
pub mod inquiry;
|
@ -1,15 +1,15 @@
|
|||||||
pub mod command_block_wrapper;
|
pub mod cbw;
|
||||||
pub mod command_status_wrapper;
|
pub mod csw;
|
||||||
|
|
||||||
use core::mem::{size_of, MaybeUninit};
|
use core::mem::{size_of, MaybeUninit};
|
||||||
|
|
||||||
use embassy_usb_driver::{Direction, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
use embassy_usb_driver::{Direction, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
||||||
|
|
||||||
use self::command_block_wrapper::CommandBlockWrapper;
|
use self::cbw::CommandBlockWrapper;
|
||||||
use self::command_status_wrapper::{CommandStatus, CommandStatusWrapper};
|
use self::csw::{CommandStatus, CommandStatusWrapper};
|
||||||
use super::{CommandError, CommandSetHandler, DataPipeError, DataPipeIn, DataPipeOut};
|
use super::{CommandError, CommandSetHandler, DataPipeError, DataPipeIn, DataPipeOut};
|
||||||
use crate::class::msc::{MscProtocol, MscSubclass, USB_CLASS_MSC};
|
use crate::class::msc::{MscProtocol, MscSubclass, USB_CLASS_MSC};
|
||||||
use crate::control::{ControlHandler, InResponse, OutResponse, Request, RequestType};
|
use crate::control::{ControlHandler, InResponse, Request, RequestType};
|
||||||
use crate::driver::Driver;
|
use crate::driver::Driver;
|
||||||
use crate::types::InterfaceNumber;
|
use crate::types::InterfaceNumber;
|
||||||
use crate::Builder;
|
use crate::Builder;
|
||||||
|
Loading…
Reference in New Issue
Block a user