Truy cập thiết bị USB trên web

API WebUSB giúp USB an toàn và dễ sử dụng hơn bằng cách đưa USB lên Web.

François Beaufort
François Beaufort

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ữ. Đúng vậy, nhưng bạn sẽ tìm thấy các loại thiết bị Universal Serial Bus (USB) khác ngoài kia.

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:

  1. Mua thiết bị USB.
  2. Cắm vào máy tính. Một thông báo sẽ xuất hiện ngay lập tức với trang web phù hợp cần truy cập cho thiết bị này.
  3. Nhấp vào thông báo đó. Trang web đã sẵn sàng để sử dụng!
  4. 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?

  1. Cài đặt ứng dụng dành riêng cho nền tảng.
  2. 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.
  3. Cài đặt. Nếu may mắn, bạn sẽ không nhận được lời nhắc hệ điều hành hay cửa sổ bật lên đáng sợ nào cảnh báo bạn về việc cài đặt trình điều khiển/ứng dụng qua 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).
  4. 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 chưa sử dụng cuối cùng sẽ được xác nhận lại.)

Trước khi bắt đầu

Bài viết này giả định 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 phần USB trong NutShell. Để 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.

API WebUSB có trong Chrome 61.

Có thể dùng cho 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 tôi đã kết thúc thành công chương trình thử nghiệm mới nhất 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 sẽ cần lưu ý đến TLS khi xây dựng.

Bắt buộc phải có cử chỉ của người dùng

Vì lý do bảo mật, navigator.usb.requestDevice() chỉ có thể được gọi thông qua một cử chỉ của người dùng, chẳng hạn như 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 giá trị này thông qua tiêu đề HTTP và/hoặc thuộc tính "allow" (cho phép) 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ó đượ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ề một 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>

Hãy 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, serialNumbersubclassCode tại đó.

Ảnh chụp màn hình lời nhắc cho người dùng về thiết bị USB trong Chrome
Lời nhắc của người dùng về thiết bị USB.

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, mã 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.

Ảnh chụp màn hình thông báo WebUSB trong Chrome
Thông báo WebUSB.

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 mọi 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. Tính năng 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ì chúng được ưu tiên cho xe buýt 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 sử dụng trong mã mẫu ở trên về cơ bản thực hiện 2 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 tính năng này trông khá quen thuộc.

Tôi muốn nhiều hơn

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)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 sẽ được xử lý bằng các phương thức tương tự như các lượt chuyển BULK (SỐ LƯỢNG LỚN) bằng transferIn(endpointNumber, length)transferOut(endpointNumber, data).
  • Quá trình chuyển ISOCHRONOUS (dùng cho các luồng dữ liệu như video và âm thanh) được xử lý bằng isochronousTransferIn(endpointNumber, packetLengths)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)transferOut(endpointNumber, data).

Bạn cũng nên xem dự án WebLight của Mike Tsao, một ví dụ minh hoạ từ đầu về cách tạo thiết bị LED điều khiển qua 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();

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 các 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.

Ảnh chụp màn hình trang nhật ký thiết bị để gỡ lỗi WebUSB trong Chrome
Trang nhật ký thiết bị trong Chrome để gỡ lỗi API WebUSB.

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.

Ảnh chụp màn hình trang nội bộ để gỡ lỗi WebUSB trong Chrome
Trang nội bộ trong Chrome để gỡ lỗi API WebUSB.

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]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 userthà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

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.