Skip to content

Commit 48cf442

Browse files
authored
Merge pull request #16 from node-usb/device-control-transfer
Support device control transfers
2 parents c569a7a + 87b8ad6 commit 48cf442

2 files changed

Lines changed: 150 additions & 80 deletions

File tree

src/webusb_device.rs

Lines changed: 117 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,59 @@ fn get_string(device: &nusb::Device, index: Option<std::num::NonZeroU8>) -> Resu
2727
}
2828
}
2929

30+
fn control_type_from_request_type(request_type: &str) -> nusb::transfer::ControlType {
31+
match request_type {
32+
"standard" => nusb::transfer::ControlType::Standard,
33+
"class" => nusb::transfer::ControlType::Class,
34+
"vendor" => nusb::transfer::ControlType::Vendor,
35+
_ => nusb::transfer::ControlType::Standard,
36+
}
37+
}
38+
39+
fn recipient_from_request_recipient(recipient: &str) -> nusb::transfer::Recipient {
40+
match recipient {
41+
"device" => nusb::transfer::Recipient::Device,
42+
"interface" => nusb::transfer::Recipient::Interface,
43+
"endpoint" => nusb::transfer::Recipient::Endpoint,
44+
"other" => nusb::transfer::Recipient::Other,
45+
_ => nusb::transfer::Recipient::Other,
46+
}
47+
}
48+
49+
fn control_in_setup(
50+
setup: &UsbControlTransferParameters,
51+
control_type: nusb::transfer::ControlType,
52+
recipient: nusb::transfer::Recipient,
53+
index: u16,
54+
length: u16,
55+
) -> nusb::transfer::ControlIn {
56+
nusb::transfer::ControlIn {
57+
control_type,
58+
recipient,
59+
request: setup.request,
60+
value: setup.value,
61+
index,
62+
length,
63+
}
64+
}
65+
66+
fn control_out_setup<'a>(
67+
setup: &UsbControlTransferParameters,
68+
control_type: nusb::transfer::ControlType,
69+
recipient: nusb::transfer::Recipient,
70+
index: u16,
71+
data: &'a [u8],
72+
) -> nusb::transfer::ControlOut<'a> {
73+
nusb::transfer::ControlOut {
74+
control_type,
75+
recipient,
76+
request: setup.request,
77+
value: setup.value,
78+
index,
79+
data,
80+
}
81+
}
82+
3083
pub(crate) async fn run_blocking<T, F>(f: F) -> Result<T>
3184
where
3285
T: Send + 'static,
@@ -533,44 +586,37 @@ impl UsbDevice {
533586
timeout: u32,
534587
length: u16,
535588
) -> Result<Option<Uint8Array>> {
536-
let control_type = match setup.requestType.as_str() {
537-
"standard" => nusb::transfer::ControlType::Standard,
538-
"class" => nusb::transfer::ControlType::Class,
539-
"vendor" => nusb::transfer::ControlType::Vendor,
540-
_ => nusb::transfer::ControlType::Standard,
541-
};
542-
let recipient = match setup.recipient.as_str() {
543-
"device" => nusb::transfer::Recipient::Device,
544-
"interface" => nusb::transfer::Recipient::Interface,
545-
"endpoint" => nusb::transfer::Recipient::Endpoint,
546-
"other" => nusb::transfer::Recipient::Other,
547-
_ => nusb::transfer::Recipient::Other,
548-
};
549-
match self.get_interface(recipient, setup.index) {
550-
Some(interface) => {
551-
let result = run_blocking(move || {
552-
interface
553-
.control_in(
554-
nusb::transfer::ControlIn {
555-
control_type,
556-
recipient,
557-
request: setup.request,
558-
value: setup.value,
559-
index: setup.index,
560-
length,
561-
},
562-
Duration::from_millis(timeout as u64),
563-
)
564-
.wait()
565-
.map_err(|e| format!("controlTransferIn error: {e}"))
566-
})
567-
.await?;
568-
Ok(Some(Uint8Array::from(result)))
569-
}
570-
None => Err(napi::Error::from_reason(
571-
"controlTransferIn error: invalid state",
572-
)),
589+
let control_type = control_type_from_request_type(&setup.requestType);
590+
let recipient = recipient_from_request_recipient(&setup.recipient);
591+
592+
#[cfg(not(windows))]
593+
if recipient == nusb::transfer::Recipient::Device {
594+
let device = self.device.as_ref().cloned().ok_or_else(|| {
595+
napi::Error::from_reason(format!("controlTransferIn error: invalid state"))
596+
})?;
597+
let request = control_in_setup(&setup, control_type, recipient, setup.index, length);
598+
let result = run_blocking(move || {
599+
device
600+
.control_in(request, Duration::from_millis(timeout as u64))
601+
.wait()
602+
.map_err(|e| format!("controlTransferIn error: {e}"))
603+
})
604+
.await?;
605+
return Ok(Some(Uint8Array::from(result)));
573606
}
607+
608+
let interface = self.get_interface(recipient, setup.index).ok_or_else(|| {
609+
napi::Error::from_reason(format!("controlTransferIn error: invalid state"))
610+
})?;
611+
let request = control_in_setup(&setup, control_type, recipient, setup.index, length);
612+
let result = run_blocking(move || {
613+
interface
614+
.control_in(request, Duration::from_millis(timeout as u64))
615+
.wait()
616+
.map_err(|e| format!("controlTransferIn error: {e}"))
617+
})
618+
.await?;
619+
Ok(Some(Uint8Array::from(result)))
574620
}
575621

576622
#[napi(js_name = "nativeControlTransferOut")]
@@ -580,46 +626,40 @@ impl UsbDevice {
580626
timeout: u32,
581627
data: Option<Uint8Array>,
582628
) -> Result<u32> {
583-
let control_type = match setup.requestType.as_str() {
584-
"standard" => nusb::transfer::ControlType::Standard,
585-
"class" => nusb::transfer::ControlType::Class,
586-
"vendor" => nusb::transfer::ControlType::Vendor,
587-
_ => nusb::transfer::ControlType::Standard,
588-
};
589-
let recipient = match setup.recipient.as_str() {
590-
"device" => nusb::transfer::Recipient::Device,
591-
"interface" => nusb::transfer::Recipient::Interface,
592-
"endpoint" => nusb::transfer::Recipient::Endpoint,
593-
"other" => nusb::transfer::Recipient::Other,
594-
_ => nusb::transfer::Recipient::Other,
595-
};
596-
match self.get_interface(recipient, setup.index) {
597-
Some(interface) => {
598-
let bytes = data.map(|b| b.to_vec()).unwrap_or_default();
599-
let bytes_len = bytes.len();
600-
run_blocking(move || {
601-
interface
602-
.control_out(
603-
nusb::transfer::ControlOut {
604-
control_type,
605-
recipient,
606-
request: setup.request,
607-
value: setup.value,
608-
index: setup.index,
609-
data: &bytes,
610-
},
611-
Duration::from_millis(timeout as u64),
612-
)
613-
.wait()
614-
.map_err(|e| format!("controlTransferOut error: {e}"))
615-
})
616-
.await?;
617-
Ok(bytes_len as u32)
618-
}
619-
None => Err(napi::Error::from_reason(
620-
"controlTransferOut error: invalid state",
621-
)),
629+
let control_type = control_type_from_request_type(&setup.requestType);
630+
let recipient = recipient_from_request_recipient(&setup.recipient);
631+
let bytes = data.map(|b| b.to_vec()).unwrap_or_default();
632+
let bytes_len = bytes.len();
633+
634+
#[cfg(not(windows))]
635+
if recipient == nusb::transfer::Recipient::Device {
636+
let device = self.device.as_ref().cloned().ok_or_else(|| {
637+
napi::Error::from_reason(format!("controlTransferOut error: invalid state"))
638+
})?;
639+
run_blocking(move || {
640+
let request =
641+
control_out_setup(&setup, control_type, recipient, setup.index, &bytes);
642+
device
643+
.control_out(request, Duration::from_millis(timeout as u64))
644+
.wait()
645+
.map_err(|e| format!("controlTransferOut error: {e}"))
646+
})
647+
.await?;
648+
return Ok(bytes_len as u32);
622649
}
650+
651+
let interface = self.get_interface(recipient, setup.index).ok_or_else(|| {
652+
napi::Error::from_reason(format!("controlTransferOut error: invalid state"))
653+
})?;
654+
run_blocking(move || {
655+
let request = control_out_setup(&setup, control_type, recipient, setup.index, &bytes);
656+
interface
657+
.control_out(request, Duration::from_millis(timeout as u64))
658+
.wait()
659+
.map_err(|e| format!("controlTransferOut error: {e}"))
660+
})
661+
.await?;
662+
Ok(bytes_len as u32)
623663
}
624664

625665
#[napi(js_name = "nativeTransferIn")]
@@ -830,7 +870,7 @@ impl UsbDevice {
830870
}
831871
}
832872

833-
// Return any claimed interface
873+
// Return any claimed interface (e.g. for device control transfers on Windows)
834874
let maybe_iface = self.interfaces.iter().find_map(|x| x.clone());
835875
if maybe_iface.is_some() {
836876
return maybe_iface;

test/webusb.coffee

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,15 @@ describe 'Alternates', ->
185185
device.close()
186186
await new Promise (resolve) -> setTimeout(resolve, 1000)
187187

188-
describe 'Transfers', ->
188+
describe 'Control Transfers', ->
189189
device = null
190190
b1 = Uint8Array.from([0x30...0x40]).buffer
191-
b2 = Uint8Array.from([0x32...0x42]).buffer
192191

193192
before ->
194193
device = await webusb.requestDevice({ filters: [{ vendorId: 0x59e3 }] });
195194
await device.open()
196-
await device.claimInterface(0)
195+
if process.platform == 'win32'
196+
await device.claimInterface(0)
197197

198198
it 'should control transfer OUT', ->
199199
transferResult = await device.controlTransferOut({
@@ -222,6 +222,20 @@ describe 'Transfers', ->
222222
expectedBuffer = Buffer.from(b1, 0, b1.byteLength);
223223
assert(resultBuffer.equals(expectedBuffer));
224224

225+
after ->
226+
if process.platform == 'win32'
227+
await device.releaseInterface(0)
228+
device.close()
229+
230+
describe 'Transfers', ->
231+
device = null
232+
b2 = Uint8Array.from([0x32...0x42]).buffer
233+
234+
before ->
235+
device = await webusb.requestDevice({ filters: [{ vendorId: 0x59e3 }] });
236+
await device.open()
237+
await device.claimInterface(0)
238+
225239
it 'should transfer OUT', ->
226240
transferResult = await device.transferOut(4, b2)
227241

@@ -248,9 +262,25 @@ describe 'Throwing Transfers', ->
248262
before ->
249263
device = await webusb.requestDevice({ filters: [{ vendorId: 0x59e3 }] });
250264

265+
it 'should fail control transfer unless opened', ->
266+
assert.rejects(device.controlTransferIn({
267+
requestType: 'vendor',
268+
recipient: 'device',
269+
request: 0x81,
270+
value: 0,
271+
index: 0
272+
}, 128), 'The device must be opened first')
273+
251274
it 'should fail transfer unless opened', ->
252275
assert.rejects(device.transferIn(1, 64), 'The device must be opened first')
253276

277+
it 'should fail transfer unless claimed', ->
278+
await device.open()
279+
assert.rejects(device.transferIn(1, 64), 'The device must be claimed first')
280+
281+
after ->
282+
device.close()
283+
254284
describe 'WebUSB Hotplug', ->
255285
it 'should detect disconnect', (done) ->
256286
fn = (e) ->

0 commit comments

Comments
 (0)