The WebUSB API provides classes and methods that allow us to connect devices and send or receive data via USB on web browsers.
We use the USB.requestDevice()
method to display the pairing dialog to the user. After the user selects a USB device from the dialog, it returns the selected device as an USBDevice
instance.
We can later send or receive data from this device. To get a list of the already paired devices, we can use the USB.getDevices()
method.
Not all browsers might support the WebUSB API. At the moment, the supported browsers are Google Chrome, Microsoft Edge, Opera, Samsung Internet, and Baidu Browser. You can check which browsers support WebUSB.
One way to check if the browser supports WebUSB with JavaScript is:
if (!('usb' in navigator)) {
console.log('WebUSB API is not supported!');
}
The WebUSB API can only be used in secure contexts, in other words, it must be served over https://
or wss://
URLs.
However, you can still test it on localhost
because, according to MDN Web Docs, resources that are local are considered to have been delivered securely:
Locally-delivered resources such as those with http://127.0.0.1 URLs, http://localhost and http://*.localhost URLs (e.g. http://dev.whatever.localhost/), and file:// URLs are also considered to have been delivered securely.
If you want to disable the WebUSB API, you can add an HTTP header Permissions-Policy: usb=()
, and use the allow
attribute for iframes. Here is an example for Node.js:
res.setHeader('Permissions-Policy', 'usb=()');
If you try to use the WebUSB API after disabling it, you will get an error: Permissions policy violation: usb is not allowed in this document
. Check out MDN Web Docs - Permissions Policy for more details.
Let's explore how we can use the WebUSB API with step-by-step examples.
We can use the USB.requestDevice()
method to pair and gain access to the USB device. This method opens a dialog in the browser where we can select the USB device we want to connect to. Upon success, it returns an instance of USBDevice
.
If no device is selected, it throws an exception: Failed to execute 'requestDevice' on 'USB': No device selected.
This method can only be called with user gestures like clicks. Otherwise, it throws an exception: Failed to execute 'requestDevice' on 'USB': Must be handling a user gesture to show a permission request.
To reset permission to the USB device, we can use device.forget()
.
Let's write some code. Add a simple button that calls the USB.requestDevice()
method on click:
<button onclick="requestDevice()">Request device</button>
…
async function requestDevice() {
try {
const device = await navigator.usb.requestDevice({ filters: [] });
console.log(device);
} catch (e) {
console.error(e);
}
}
The filters
parameter is used to specify a list of objects for possible devices we would like to pair. If left empty, it returns all USB devices. Filter objects can have properties like vendorId
, productId
, classCode
, subclassCode
, protocolCode
, and serialNumber
.
For example, if we only want to get a list of Apple devices, the code should look like this:
navigator.usb.requestDevice({ filters: [{ vendorId: 0x05ac }] });
Here is the list of USB ID’s.
Once we pair and get permission, we can get all paired devices using the USB.getDevices()
method. We only need to use the USB.requestDevice()
method once to get permission, and thereafter we can get an array of paired devices with USB.getDevices()
.
async function getDevices() {
const devices = await navigator.usb.getDevices();
devices.forEach((device) => {
console.log(`Name: ${device.productName}, Serial: ${device.serialNumber}`);
});
return devices;
}
Now that we have permission to use the USB device and can get the paired device list, let's see how we can send data to it.
Here's a simple code that sends data to a connected device:
async function transferOutTest(device) {
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
await device.transferOut(
2,
new Uint8Array(
new TextEncoder().encode('Test value\n')
),
);
await device.close();
}
open()
starts the device session.
selectConfiguration()
selects device-specific configuration.
claimInterface()
claims the device-specific interface for exclusive access.
transferOut()
sends data to the USB device.
close()
releases interfaces and ends the device session.
I created a
<button onclick="requestDevice()">Request device</button><br><br>
<button id="device"></button>
<script>
async function requestDevice() {
try {
const device = await navigator.usb.requestDevice({ filters: [] });
const elem = document.querySelector('#device');
elem.textContent = `Print with '${device.productName}'`;
elem.onclick = () => testPrint(device);
} catch (e) {
console.error(e);
}
}
async function testPrint(device) {
const cmds = [
'SIZE 48 mm,25 mm',
'CLS',
'TEXT 30,10,"4",0,1,1,"HackerNoon"',
'TEXT 30,50,"2",0,1,1,"WebUSB API"',
'BARCODE 30,80,"128",70,1,0,2,2,"altospos.com"',
'PRINT 1',
'END',
];
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
await device.transferOut(
device.configuration.interfaces[0].alternate.endpoints.find(obj => obj.direction === 'out').endpointNumber,
new Uint8Array(
new TextEncoder().encode(cmds.join('\r\n'))
),
);
await device.close();
}
</script>
And the result looks like this:
If you're interested in learning more about TSPL/TSPL2, I have written a few articles that you might find helpful. They cover topics like how to print labels and images, as well as how to print receipts using TSPL and JavaScript.
I wrote these articles after adding printing support to Alto's POS & Inventory, hoping that these would be useful to someone in a similar situation.
Some useful tools:
chrome://device-log/
- Use this to get device details and create a test device.chrome://device-log/
- This shows logs related to devices.
No War! ✋🏽