API WebUSB giúp USB an toàn và dễ sử dụng hơn bằng cách đưa USB lên Web.
Nếu tôi nói đơn giản và rõ ràng là "USB", thì rất có thể bạn sẽ nghĩ ngay đến bàn phím, chuột, âm thanh, video và thiết bị lưu trữ. Bạn nói đúng, nhưng bạn sẽ thấy các loại thiết bị khác của Cổng nối tiếp đa năng (USB) trên thị trường.
Những thiết bị USB không chuẩn này yêu cầu nhà cung cấp phần cứng phải viết trình điều khiển và SDK dành riêng cho nền tảng để bạn (nhà phát triển) có thể tận dụng các thiết bị này. Đáng tiếc là mã dành riêng cho nền tảng này trước đây đã ngăn Web sử dụng các thiết bị này. Đó là một trong những lý do tạo ra API WebUSB: để cung cấp một cách hiển thị các dịch vụ thiết bị USB cho Web. Với API này, các nhà sản xuất phần cứng sẽ có thể tạo SDK JavaScript đa nền tảng cho thiết bị của họ.
Nhưng quan trọng nhất là việc này sẽ giúp USB an toàn và dễ sử dụng hơn bằng cách đưa USB lên Web.
Hãy xem hành vi mà bạn có thể mong đợi với API WebUSB:
- Mua thiết bị USB.
- Cắm vào máy tính. Một thông báo sẽ xuất hiện ngay lập tức, cùng với trang web phù hợp để truy cập cho thiết bị này.
- Nhấp vào thông báo đó. Trang web đã được tạo và sẵn sàng để sử dụng!
- Nhấp để kết nối và trình chọn thiết bị USB sẽ xuất hiện trong Chrome để bạn có thể chọn thiết bị của mình.
Tada!
Quy trình này sẽ như thế nào nếu không có API WebUSB?
- Cài đặt một ứng dụng dành riêng cho nền tảng.
- Nếu ứng dụng đó được hỗ trợ trên hệ điều hành của tôi, hãy xác minh rằng tôi đã tải đúng ứng dụng.
- Cài đặt ứng dụng. Nếu may mắn, bạn sẽ không thấy lời nhắc hoặc cửa sổ bật lên đáng sợ của hệ điều hành cảnh báo bạn về việc cài đặt trình điều khiển/ứng dụng từ Internet. Nếu bạn không may, các trình điều khiển hoặc ứng dụng đã cài đặt sẽ hoạt động không đúng cách và gây hại cho máy tính của bạn. (Hãy nhớ rằng web được xây dựng để chứa các trang web không hoạt động đúng cách).
- Nếu bạn chỉ sử dụng tính năng này một lần, mã sẽ vẫn nằm trên máy tính cho đến khi bạn nghĩ đến việc xoá mã đó. (Trên web, không gian không sử dụng cuối cùng sẽ được thu hồi.)
Trước khi bắt đầu
Bài viết này giả định rằng bạn có một số kiến thức cơ bản về cách hoạt động của USB. Nếu không, bạn nên đọc bài viết USB in a NutShell (USB tóm tắt). Để biết thông tin cơ bản về USB, hãy xem thông số kỹ thuật chính thức của USB.
WebUSB API có trong Chrome 61.
Có trong bản dùng thử theo nguyên gốc
Để nhận được nhiều ý kiến phản hồi nhất có thể từ các nhà phát triển sử dụng API WebUSB trong thực tế, trước đây chúng tôi đã thêm tính năng này vào Chrome 54 và Chrome 57 dưới dạng bản dùng thử theo nguyên gốc.
Chương trình thử nghiệm mới nhất đã kết thúc thành công vào tháng 9 năm 2017.
Quyền riêng tư và bảo mật
Chỉ HTTPS
Do sức mạnh của tính năng này, tính năng này chỉ hoạt động trên các ngữ cảnh bảo mật. Điều này có nghĩa là bạn cần xây dựng ứng dụng có tính đến TLS.
Bắt buộc phải có cử chỉ của người dùng
Để phòng ngừa bảo mật, bạn chỉ có thể gọi navigator.usb.requestDevice()
thông qua một cử chỉ của người dùng, chẳng hạn như thao tác chạm hoặc nhấp chuột.
Chính sách về quyền
Chính sách về quyền là một cơ chế cho phép nhà phát triển bật và tắt có chọn lọc nhiều tính năng và API của trình duyệt. Bạn có thể xác định thuộc tính này thông qua tiêu đề HTTP và/hoặc thuộc tính "allow" (cho phép) của iframe.
Bạn có thể xác định Chính sách quyền kiểm soát xem thuộc tính usb
có hiển thị trên đối tượng Trình điều hướng hay không, hay nói cách khác là liệu bạn có cho phép WebUSB hay không.
Dưới đây là ví dụ về chính sách tiêu đề không cho phép WebUSB:
Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com
Dưới đây là một ví dụ khác về chính sách vùng chứa cho phép USB:
<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>
Bắt đầu lập trình
WebUSB API phụ thuộc nhiều vào Promise (Lời hứa) của JavaScript. Nếu bạn chưa quen thuộc với các hàm này, hãy xem hướng dẫn tuyệt vời về Lời hứa. Ngoài ra, () => {}
chỉ là Hàm mũi tên ECMAScript 2015.
Truy cập vào thiết bị USB
Bạn có thể nhắc người dùng chọn một thiết bị USB đã kết nối bằng cách sử dụng navigator.usb.requestDevice()
hoặc gọi navigator.usb.getDevices()
để nhận danh sách tất cả thiết bị USB đã kết nối mà trang web đã được cấp quyền truy cập.
Hàm navigator.usb.requestDevice()
nhận một đối tượng JavaScript bắt buộc xác định filters
. Các bộ lọc này được dùng để so khớp mọi thiết bị USB với mã nhận dạng nhà cung cấp (vendorId
) và mã nhận dạng sản phẩm (productId
) (không bắt buộc).
Bạn cũng có thể xác định các khoá classCode
, protocolCode
, serialNumber
và subclassCode
tại đó.
Ví dụ: sau đây là cách truy cập vào một thiết bị Arduino đã kết nối được định cấu hình để cho phép nguồn gốc.
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
console.log(device.productName); // "Arduino Micro"
console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });
Trước khi bạn hỏi, tôi không phải tự nhiên nghĩ ra số thập lục phân 0x2341
này. Tôi chỉ cần tìm kiếm từ "Arduino" trong Danh sách mã nhận dạng USB này.
device
USB được trả về trong lời hứa đã thực hiện ở trên có một số thông tin cơ bản nhưng quan trọng về thiết bị, chẳng hạn như phiên bản USB được hỗ trợ, kích thước gói tối đa, nhà cung cấp và mã sản phẩm, số lượng cấu hình có thể có của thiết bị. Về cơ bản, tệp này chứa tất cả các trường trong Trình mô tả USB của thiết bị.
// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
devices.forEach(device => {
console.log(device.productName); // "Arduino Micro"
console.log(device.manufacturerName); // "Arduino LLC"
});
})
Nhân tiện, nếu một thiết bị USB thông báo hỗ trợ WebUSB, cũng như xác định URL trang đích, thì Chrome sẽ hiển thị một thông báo liên tục khi thiết bị USB được cắm vào. Khi bạn nhấp vào thông báo này, trang đích sẽ mở ra.
Trò chuyện với bo mạch USB Arduino
Bây giờ, hãy xem cách dễ dàng giao tiếp từ một bảng Arduino tương thích với WebUSB qua cổng USB. Hãy xem hướng dẫn tại https://github.com/webusb/arduino để bật WebUSB cho bản phác thảo.
Đừng lo, tôi sẽ đề cập đến tất cả các phương thức thiết bị WebUSB được đề cập bên dưới trong phần sau của bài viết này.
let device;
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
device = selectedDevice;
return device.open(); // Begin a session.
})
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x22,
value: 0x01,
index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
const decoder = new TextDecoder();
console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });
Xin lưu ý rằng thư viện WebUSB mà tôi đang sử dụng chỉ triển khai một giao thức mẫu (dựa trên giao thức nối tiếp USB tiêu chuẩn) và nhà sản xuất có thể tạo bất kỳ tập hợp và loại điểm cuối nào mà họ muốn. Việc chuyển quyền kiểm soát đặc biệt phù hợp với các lệnh cấu hình nhỏ vì các lệnh này được ưu tiên trên bus và có cấu trúc được xác định rõ ràng.
Và đây là bản phác thảo đã được tải lên bảng Arduino.
// Third-party WebUSB Arduino library
#include <WebUSB.h>
WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");
#define Serial WebUSBSerial
void setup() {
Serial.begin(9600);
while (!Serial) {
; // Wait for serial port to connect.
}
Serial.write("WebUSB FTW!");
Serial.flush();
}
void loop() {
// Nothing here for now.
}
Thư viện WebUSB Arduino của bên thứ ba được dùng trong mã mẫu ở trên về cơ bản thực hiện hai việc:
- Thiết bị này đóng vai trò là thiết bị WebUSB cho phép Chrome đọc URL trang đích.
- Trình bổ trợ này hiển thị một API nối tiếp WebUSB mà bạn có thể sử dụng để ghi đè API mặc định.
Xem lại mã JavaScript. Sau khi người dùng chọn device
, device.open()
sẽ chạy tất cả các bước dành riêng cho nền tảng để bắt đầu một phiên với thiết bị USB. Sau đó, tôi phải chọn một Cấu hình USB có sẵn bằng device.selectConfiguration()
. Hãy nhớ rằng cấu hình chỉ định cách cấp nguồn cho thiết bị, mức tiêu thụ điện năng tối đa và số lượng giao diện.
Nói về giao diện, tôi cũng cần yêu cầu quyền truy cập độc quyền bằng device.claimInterface()
vì dữ liệu chỉ có thể được chuyển đến một giao diện hoặc điểm cuối được liên kết khi giao diện được xác nhận quyền sở hữu. Cuối cùng, bạn cần gọi device.controlTransferOut()
để thiết lập thiết bị Arduino bằng các lệnh thích hợp để giao tiếp thông qua API nối tiếp WebUSB.
Từ đó, device.transferIn()
thực hiện chuyển hàng loạt lên thiết bị để thông báo cho thiết bị rằng máy chủ đã sẵn sàng nhận dữ liệu hàng loạt. Sau đó, lời hứa sẽ được thực hiện bằng một đối tượng result
chứa DataView data
phải được phân tích cú pháp một cách thích hợp.
Nếu bạn đã quen thuộc với USB, thì tất cả những điều này sẽ khá quen thuộc.
Tôi muốn có thêm
WebUSB API cho phép bạn tương tác với tất cả các loại điểm cuối/chuyển USB:
- Các lượt chuyển CONTROL, dùng để gửi hoặc nhận các tham số cấu hình hoặc lệnh đến một thiết bị USB, được xử lý bằng
controlTransferIn(setup, length)
vàcontrolTransferOut(setup, data)
. - Các lượt chuyển INTERRUPT (GIÁN ĐỨT) được dùng cho một lượng nhỏ dữ liệu nhạy cảm về thời gian được xử lý bằng các phương thức giống như các lượt chuyển BULK (SỐ LƯỢNG LỚN) bằng
transferIn(endpointNumber, length)
vàtransferOut(endpointNumber, data)
. - Các lượt chuyển ISOCHRONOUS (đồng bộ) dùng cho các luồng dữ liệu như video và âm thanh được xử lý bằng
isochronousTransferIn(endpointNumber, packetLengths)
vàisochronousTransferOut(endpointNumber, data, packetLengths)
. - Các lượt chuyển BULK (hàng loạt) được dùng để chuyển một lượng lớn dữ liệu không nhạy cảm về thời gian một cách đáng tin cậy, được xử lý bằng
transferIn(endpointNumber, length)
vàtransferOut(endpointNumber, data)
.
Bạn cũng nên xem dự án WebLight của Mike Tsao. Dự án này cung cấp một ví dụ cơ bản về cách tạo thiết bị LED được điều khiển bằng USB được thiết kế cho WebUSB API (không sử dụng Arduino ở đây). Bạn sẽ thấy phần cứng, phần mềm và chương trình cơ sở.
Thu hồi quyền truy cập vào thiết bị USB
Trang web có thể xoá các quyền truy cập vào thiết bị USB mà trang web không cần nữa bằng cách gọi forget()
trên thực thể USBDevice
. Ví dụ: đối với một ứng dụng web giáo dục được dùng trên một máy tính dùng chung với nhiều thiết bị, việc tích luỹ nhiều quyền do người dùng tạo sẽ gây ra trải nghiệm không tốt cho người dùng.
// Voluntarily revoke access to this USB device.
await device.forget();
Vì forget()
có trong Chrome 101 trở lên, hãy kiểm tra xem tính năng này có được hỗ trợ với các tính năng sau hay không:
if ("usb" in navigator && "forget" in USBDevice.prototype) {
// forget() is supported.
}
Giới hạn về kích thước tệp cần chuyển
Một số hệ điều hành áp đặt giới hạn về lượng dữ liệu có thể nằm trong giao dịch USB đang chờ xử lý. Việc chia dữ liệu thành các giao dịch nhỏ hơn và chỉ gửi một vài giao dịch cùng một lúc sẽ giúp bạn tránh được những hạn chế đó. Điều này cũng làm giảm dung lượng bộ nhớ được sử dụng và cho phép ứng dụng báo cáo tiến trình khi quá trình chuyển hoàn tất.
Vì nhiều lượt chuyển được gửi đến một điểm cuối luôn thực thi theo thứ tự, nên bạn có thể cải thiện thông lượng bằng cách gửi nhiều đoạn xếp hàng để tránh độ trễ giữa các lượt chuyển qua USB. Mỗi khi một phần được truyền hoàn toàn, phần đó sẽ thông báo cho mã của bạn rằng phần đó sẽ cung cấp thêm dữ liệu như được ghi lại trong ví dụ về hàm trợ giúp bên dưới.
const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;
async function sendRawPayload(device, endpointNumber, data) {
let i = 0;
let pendingTransfers = [];
let remainingBytes = data.byteLength;
while (remainingBytes > 0) {
const chunk = data.subarray(
i * BULK_TRANSFER_SIZE,
(i + 1) * BULK_TRANSFER_SIZE
);
// If we've reached max number of transfers, let's wait.
if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
await pendingTransfers.shift();
}
// Submit transfers that will be executed in order.
pendingTransfers.push(device.transferOut(endpointNumber, chunk));
remainingBytes -= chunk.byteLength;
i++;
}
// And wait for last remaining transfers to complete.
await Promise.all(pendingTransfers);
}
Mẹo
Giờ đây, bạn có thể gỡ lỗi USB trong Chrome dễ dàng hơn nhờ trang nội bộ about://device-log
. Tại đây, bạn có thể xem tất cả các sự kiện liên quan đến thiết bị USB ở cùng một nơi.
Trang nội bộ about://usb-internals
cũng rất hữu ích và cho phép bạn mô phỏng việc kết nối và ngắt kết nối của các thiết bị WebUSB ảo.
Điều này rất hữu ích khi kiểm thử giao diện người dùng mà không cần phần cứng thực.
Trên hầu hết các hệ thống Linux, các thiết bị USB được liên kết với quyền chỉ có thể đọc theo mặc định. Để cho phép Chrome mở thiết bị USB, bạn cần thêm một quy tắc udev mới. Tạo một tệp tại /etc/udev/rules.d/50-yourdevicename.rules
có nội dung sau:
SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"
trong đó [yourdevicevendor]
là 2341
nếu thiết bị của bạn là Arduino.
Bạn cũng có thể thêm ATTR{idProduct}
để có quy tắc cụ thể hơn. Đảm bảo rằng user
là thành viên của nhóm plugdev
. Sau đó, bạn chỉ cần kết nối lại thiết bị.
Tài nguyên
- Stack Overflow: https://stackoverflow.com/questions/tagged/webusb
- Thông số kỹ thuật API WebUSB: http://wicg.github.io/webusb/
- Trạng thái tính năng của Chrome: https://www.chromestatus.com/feature/5651917954875392
- Vấn đề về thông số kỹ thuật: https://github.com/WICG/webusb/issues
- Lỗi triển khai: http://crbug.com?q=component:Blink>USB
- WebUSB ❤ ️Arduino: https://github.com/webusb/arduino
- IRC: #webusb trên IRC của W3C
- Danh sách gửi thư WICG: https://lists.w3.org/Archives/Public/public-wicg/
- Dự án WebLight: https://github.com/sowbug/weblight
Gửi một tweet đến @ChromiumDev bằng hashtag
#WebUSB
và cho chúng tôi biết bạn đang sử dụng ở đâu và như thế nào.
Lời cảm ơn
Cảm ơn Joe Medley đã xem xét bài viết này.