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

WebUSB API 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 một cách đơn giản và chỉ đơn giản là "USB", có khả năng bạn sẽ nghĩ ngay đến bàn phím, chuột, âm thanh, video và các thiết bị lưu trữ. Đúng vậy, nhưng bạn sẽ thấy các loại thiết bị Bus nối tiếp Universal (USB) khác trên thị trường.

Các thiết bị USB không được chuẩn hoá này yêu cầu các nhà cung cấp phần cứng 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 trình điều khiển và SDK này. Đáng buồn thay, 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. Và đó là một trong những lý do khiến API WebUSB được tạo ra: để cung cấp một cách hiển thị các dịch vụ thiết bị USB lên Web. Với API này, các nhà sản xuất phần cứng sẽ có thể tạo các SDK JavaScript đa nền tảng cho thiết bị của họ.

Nhưng điều quan trọng nhất là điều 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 bạn có thể mong đợi với API WebUSB:

  1. Mua một thiết bị USB.
  2. Cắm thiết bị vào máy tính của bạn. Một thông báo sẽ xuất hiện ngay lập tức, kèm theo trang web phù hợ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 tính năng này còn đượ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 xuống đúng nội dung.
  3. Cài đặt thiết bị. Nếu là người may mắn, bạn sẽ không nhận được 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 về việc cài đặt trình điều khiển/ứng dụng qua Internet. Nếu bạn không may mắn, các trình điều khiển hoặc ứng dụng đã cài đặt sẽ gặp sự cố 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 hoạt động sai chức năng).
  4. Nếu bạn chỉ sử dụng tính năng này một lần, mã sẽ vẫn lưu lại trên máy tính cho đến khi bạn muốn 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 tôi 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 USB hoạt động. Nếu không, bạn nên đọc 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 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.

Phiên bản dùng thử gần đây 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 chỉ hoạt động trên ngữ cảnh bảo mật. Tức là bạn cần lưu ý đến TLS khi tạo bản dựng.

Yêu cầu cử chỉ của người dùng

Để đảm bảo an toàn, 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ư 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 nhiều tính năng và API của trình duyệt một cách có chọn lọc. Bạn có thể xác định quy tắc này qua tiêu đề HTTP và/hoặc thuộc tính "cho phép" của iframe.

Bạn có thể xác định Chính sách quyền kiểm soát việc thuộc tính usb có hiển thị trên đối tượng Trình điều hướng hay không, nói cách khác là nếu bạn cho phép WebUSB.

Dưới đây là ví dụ về một chính sách đối với 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

API WebUSB chủ yếu dựa vào Promise của JavaScript. Nếu bạn chưa hiểu rõ về các tính năng đó, hãy xem hướng dẫn về Promises tuyệt vời này. Ngoài ra, () => {} chỉ đơn giản là các hàm Mũi tên của 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() để lấy 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() lấy một đối tượng JavaScript bắt buộc xác định filters. Các bộ lọc này dùng để so khớp mọi thiết bị USB với giá trị nhận dạng nhà cung cấp (vendorId) và giá trị nhận dạng sản phẩm (productId) nhất định. 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 dành cho người dùng thiết bị USB trong Chrome
Lời nhắc dành cho người dùng thiết bị USB.

Ví dụ: dưới đâ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 tự nhiên nghĩ ra số thập lục phân 0x2341 này. Tôi chỉ đơn giản là tìm kiếm từ "Arduino" trong Danh sách ID USB này.

USB device đượ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, mã sản phẩm và số lượng cấu hình mà thiết bị có thể có. Về cơ bản, tệp này chứa tất cả các trường trong 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, Chrome sẽ hiển thị một thông báo liên tục khi thiết bị USB được cắm. 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.

Giao tiếp với bảng USB Arduino

Được rồi, bây giờ hãy xem giao tiếp dễ dàng từ một bo mạch 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 để cho phép WebUSB kích hoạt bản phác thảo của bạn.

Đừng lo, tôi sẽ đề cập đến tất cả các phương thức thiết bị WebUSB được đề cập ở phần sau trong 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ỉ đang 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à các nhà sản xuất có thể tạo bất kỳ bộ và loại điểm cuối nào mà họ muốn. Các thao tác chuyển điều khiển đặc biệt hữu ích với các lệnh cấu hình nhỏ vì các lệnh này có mức độ ưu tiên xe buýt và có cấu trúc được xác định rõ.

Đâ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 có hai việc:

  • Thiết bị này hoạt động như một thiết bị WebUSB cho phép Chrome đọc URL trang đích.
  • Tệp này cho thấy một API nối tiếp WebUSB mà bạn có thể dùng để ghi đè API mặc định.

Xem lại mã JavaScript. Sau khi tôi nhận được device do người dùng chọn, 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 bằng thiết bị USB. Sau đó, tôi phải chọn một Cấu hình USB có sẵn với 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ụ năng lượ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 cá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 WebUSB Serial.

Từ đó, device.transferIn() sẽ chuyển hàng loạt sang thiết bị để thông báo rằng máy chủ lưu trữ đã sẵn sàng nhận dữ liệu hàng loạt. Sau đó, lời hứa đượ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 phù hợp.

Nếu bạn đã quen thuộc với USB, tất cả những điều này có thể khá quen thuộc.

Tôi muốn biết thêm

API WebUSB cho phép bạn tương tác với tất cả các loại điểm cuối/truyền qua USB:

  • Các hoạt động chuyển CONTROL (dùng để gửi hoặc nhận các tham số lệnh hoặc cấu hình cho thiết bị USB) được xử lý bằng controlTransferIn(setup, length)controlTransferOut(setup, data).
  • Các phương thức chuyển TRANSRUPT (được dùng cho một lượng nhỏ dữ liệu có giới hạn thời gian) được xử lý theo các phương thức tương tự như chuyển hàng loạt bằng transferIn(endpointNumber, length)transferOut(endpointNumber, data).
  • Quá trình chuyển ISOCHRONOUS (sử 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).
  • Thao tác chuyển hàng loạt (được dùng để chuyển một lượng lớn dữ liệu không có giới hạn về thời gian một cách đáng tin cậy) sẽ đượ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 cung cấp ví dụ từ đầu về việc xây dựng thiết bị LED điều khiển qua USB được thiết kế cho API WebUSB (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 một thiết bị USB

Trang web có thể xoá các quyền để truy cập vào một 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 sử dụng trên một máy tính dùng chung có nhiều thiết bị, việc tích luỹ một lượng lớn các quyền do người dùng tạo sẽ tạo ra trải nghiệm kém 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ợ hay không:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

Giới hạn về kích thước chuyển

Một số hệ điều hành áp dụng các giới hạn về lượng dữ liệu có thể là một phần của các giao dịch USB đang chờ xử lý. Việc chia dữ liệu của bạn 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 tránh được các hạn chế đó. Điều này cũng giúp giảm mức sử dụng bộ nhớ 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 quá trình 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 trong hàng đợi để tránh độ trễ giữa các lần chuyển USB. Mỗi khi một đoạn được truyền đầy đủ, đoạn mã đó sẽ thông báo rằng mã đó cần cung cấp thêm dữ liệu như được nêu trong ví dụ về hàm trợ giúp dưới đây.

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

Việc gỡ lỗi USB trong Chrome sẽ dễ dàng hơn thông qua 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 kết nối và ngắt kết nối của các thiết bị WebUSB ảo. Việc này rất hữu ích khi kiểm thử giao diện người dùng mà không cần sử dụng 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.

Theo mặc định, 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. Để cho phép Chrome mở thiết bị USB, bạn cần thêm 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 user của bạn là một 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

Hãy gửi một bài đăng trên Twitter tới @ChromiumDev kèm theo hashtag #WebUSB và cho chúng tôi biết vị trí cũng như cách bạn sử dụng bài đăng này.

Xác nhận

Cảm ơn Joe Medley đã đánh giá bài viết này.