Web Serial API cho phép các trang web giao tiếp với thiết bị nối tiếp.
Web Serial API là gì?
Cổng nối tiếp là giao diện giao tiếp hai chiều cho phép gửi và nhận dữ liệu từng byte.
Web Serial API cung cấp một cách để các trang web đọc và ghi vào thiết bị nối tiếp bằng JavaScript. Các thiết bị nối tiếp được kết nối thông qua cổng nối tiếp trên hệ thống của người dùng hoặc thông qua các thiết bị USB và Bluetooth có thể tháo rời mô phỏng cổng nối tiếp.
Nói cách khác, Web Serial API là cầu nối giữa web và thế giới thực bằng cách cho phép các trang web giao tiếp với các thiết bị nối tiếp, chẳng hạn như bộ vi điều khiển và máy in 3D.
API này cũng là một công cụ đồng hành tuyệt vời cho WebUSB vì các hệ điều hành yêu cầu ứng dụng giao tiếp với một số cổng nối tiếp bằng API nối tiếp cấp cao hơn thay vì API USB cấp thấp.
Các trường hợp sử dụng được đề xuất
Trong các ngành giáo dục, nghiệp dư và công nghiệp, người dùng kết nối các thiết bị ngoại vi với máy tính. Các thiết bị này thường được điều khiển bằng bộ vi điều khiển thông qua kết nối nối tiếp do phần mềm tuỳ chỉnh sử dụng. Một số phần mềm tùy chỉnh để điều khiển các thiết bị này được xây dựng bằng công nghệ web:
Trong một số trường hợp, các trang web giao tiếp với thiết bị thông qua một ứng dụng tác nhân mà người dùng đã cài đặt theo cách thủ công. Trong các trường hợp khác, ứng dụng được phân phối trong một ứng dụng đóng gói thông qua một khung như Electron. Còn trong một số trường hợp khác, người dùng phải thực hiện thêm một bước, chẳng hạn như sao chép ứng dụng đã biên dịch vào thiết bị thông qua ổ đĩa flash USB.
Trong tất cả các trường hợp này, trải nghiệm người dùng sẽ được cải thiện bằng cách cung cấp tính năng giao tiếp trực tiếp giữa trang web và thiết bị mà trang web đó đang kiểm soát.
Trạng thái hiện tại
Bước | Trạng thái |
---|---|
1. Tạo video giải thích | Hoàn tất |
2. Tạo bản nháp ban đầu của quy cách | Hoàn tất |
3. Thu thập ý kiến phản hồi và lặp lại thiết kế | Hoàn tất |
4. Bản dùng thử theo nguyên gốc | Hoàn tất |
5. Chạy | Hoàn tất |
Sử dụng Web Serial API
Phát hiện tính năng
Để kiểm tra xem Web Serial API có được hỗ trợ hay không, hãy sử dụng:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Mở cổng nối tiếp
Theo thiết kế, Web Serial API là không đồng bộ. Điều này giúp giao diện người dùng của trang web không bị chặn khi chờ dữ liệu đầu vào. Điều này rất quan trọng vì dữ liệu nối tiếp có thể được nhận bất cứ lúc nào, đòi hỏi phải có cách để nghe dữ liệu đó.
Để mở cổng nối tiếp, trước tiên, hãy truy cập vào đối tượng SerialPort
. Để thực hiện việc này, bạn có thể nhắc người dùng chọn một cổng nối tiếp bằng cách gọi navigator.serial.requestPort()
để phản hồi một cử chỉ của người dùng như chạm hoặc nhấp chuột, hoặc chọn một cổng trong navigator.serial.getPorts()
. Thao tác này sẽ trả về danh sách các cổng nối tiếp mà trang web đã được cấp quyền truy cập.
document.querySelector('button').addEventListener('click', async () => {
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();
Hàm navigator.serial.requestPort()
nhận một giá trị cố định đối tượng không bắt buộc xác định bộ lọc. Các giá trị này được dùng để so khớp mọi thiết bị nối tiếp được kết nối qua USB với một nhà cung cấp USB bắt buộc (usbVendorId
) và giá trị nhận dạng sản phẩm USB không bắt buộc (usbProductId
).
// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
Việc gọi requestPort()
sẽ nhắc người dùng chọn một thiết bị và trả về một đối tượng SerialPort
. Sau khi bạn có đối tượng SerialPort
, việc gọi port.open()
với tốc độ truyền dữ liệu mong muốn sẽ mở cổng nối tiếp. Thành phần từ điển baudRate
chỉ định tốc độ gửi dữ liệu qua một dòng nối tiếp. Tốc độ này được biểu thị bằng đơn vị bit/giây (bps). Hãy kiểm tra tài liệu của thiết bị để biết giá trị chính xác vì tất cả dữ liệu bạn gửi và nhận sẽ là những ký tự vô nghĩa nếu bạn chỉ định giá trị này không chính xác. Đối với một số thiết bị USB và Bluetooth mô phỏng cổng nối tiếp, bạn có thể đặt giá trị này thành bất kỳ giá trị nào một cách an toàn vì giá trị này sẽ bị mô phỏng bỏ qua.
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Wait for the serial port to open.
await port.open({ baudRate: 9600 });
Bạn cũng có thể chỉ định bất kỳ tuỳ chọn nào bên dưới khi mở cổng nối tiếp. Các tuỳ chọn này không bắt buộc và có giá trị mặc định thuận tiện.
dataBits
: Số bit dữ liệu trên mỗi khung hình (7 hoặc 8).stopBits
: Số bit dừng ở cuối khung (1 hoặc 2).parity
: Chế độ đồng nhất ("none"
,"even"
hoặc"odd"
).bufferSize
: Kích thước của vùng đệm đọc và ghi cần tạo (phải nhỏ hơn 16 MB).flowControl
: Chế độ kiểm soát luồng ("none"
hoặc"hardware"
).
Đọc từ cổng nối tiếp
Luồng đầu vào và đầu ra trong Web Serial API được xử lý bằng Streams API.
Sau khi kết nối cổng nối tiếp được thiết lập, các thuộc tính readable
và writable
từ đối tượng SerialPort
sẽ trả về một ReadableStream và một WritableStream. Các hàm đó sẽ được dùng để nhận dữ liệu từ và gửi dữ liệu đến thiết bị nối tiếp. Cả hai đều sử dụng các thực thể Uint8Array
để chuyển dữ liệu.
Khi dữ liệu mới đến từ thiết bị nối tiếp, port.readable.getReader().read()
sẽ trả về hai thuộc tính không đồng bộ: value
và boolean done
. Nếu done
là true, thì cổng nối tiếp đã bị đóng hoặc không có thêm dữ liệu nào được truyền vào. Việc gọi port.readable.getReader()
sẽ tạo một trình đọc và khoá readable
vào trình đọc đó. Khi readable
bị khoá, bạn không thể đóng cổng nối tiếp.
const reader = port.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a Uint8Array.
console.log(value);
}
Một số lỗi đọc cổng nối tiếp không nghiêm trọng có thể xảy ra trong một số điều kiện như tràn bộ đệm, lỗi định dạng hoặc lỗi chẵn lẻ. Các ngoại lệ đó được gửi dưới dạng ngoại lệ và có thể được phát hiện bằng cách thêm một vòng lặp khác vào vòng lặp trước đó để kiểm tra port.readable
. Điều này hoạt động vì miễn là các lỗi không nghiêm trọng, ReadableStream mới sẽ được tạo tự động. Nếu xảy ra lỗi nghiêm trọng, chẳng hạn như thiết bị nối tiếp bị xoá, thì port.readable
sẽ trở thành rỗng.
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
}
}
Nếu thiết bị nối tiếp gửi văn bản trở lại, bạn có thể chuyển port.readable
qua TextDecoderStream
như minh hoạ dưới đây. TextDecoderStream
là một luồng biến đổi
nắm bắt tất cả các đoạn Uint8Array
và chuyển đổi các đoạn đó thành chuỗi.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
Bạn có thể kiểm soát cách phân bổ bộ nhớ khi đọc từ luồng bằng trình đọc "Tự mang vùng đệm". Gọi port.readable.getReader({ mode: "byob" })
để lấy giao diện ReadableStreamBYOBReader và cung cấp ArrayBuffer
của riêng bạn khi gọi read()
. Xin lưu ý rằng Web Serial API hỗ trợ tính năng này trong Chrome 106 trở lên.
try {
const reader = port.readable.getReader({ mode: "byob" });
// Call reader.read() to read data into a buffer...
} catch (error) {
if (error instanceof TypeError) {
// BYOB readers are not supported.
// Fallback to port.readable.getReader()...
}
}
Sau đây là ví dụ về cách sử dụng lại vùng đệm trong value.buffer
:
const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);
// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });
const reader = port.readable.getReader({ mode: "byob" });
while (true) {
const { value, done } = await reader.read(new Uint8Array(buffer));
if (done) {
break;
}
buffer = value.buffer;
// Handle `value`.
}
Dưới đây là một ví dụ khác về cách đọc một lượng dữ liệu cụ thể từ cổng nối tiếp:
async function readInto(reader, buffer) {
let offset = 0;
while (offset < buffer.byteLength) {
const { value, done } = await reader.read(
new Uint8Array(buffer, offset)
);
if (done) {
break;
}
buffer = value.buffer;
offset += value.byteLength;
}
return buffer;
}
const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);
Ghi vào cổng nối tiếp
Để gửi dữ liệu đến một thiết bị nối tiếp, hãy truyền dữ liệu đến port.writable.getWriter().write()
. Bạn phải gọi releaseLock()
trên port.writable.getWriter()
để đóng cổng nối tiếp sau này.
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// Allow the serial port to be closed later.
writer.releaseLock();
Gửi văn bản đến thiết bị thông qua TextEncoderStream
được chuyển đến port.writable
như minh hoạ dưới đây.
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
Đóng cổng nối tiếp
port.close()
đóng cổng nối tiếp nếu các thành phần readable
và writable
của cổng này đã mở khoá, nghĩa là releaseLock()
đã được gọi cho trình đọc và trình ghi tương ứng.
await port.close();
Tuy nhiên, khi liên tục đọc dữ liệu từ một thiết bị nối tiếp bằng một vòng lặp, port.readable
sẽ luôn bị khoá cho đến khi gặp lỗi. Trong trường hợp này, việc gọi reader.cancel()
sẽ buộc reader.read()
phân giải ngay lập tức bằng { value: undefined, done: true }
, do đó cho phép vòng lặp gọi reader.releaseLock()
.
// Without transform streams.
let keepReading = true;
let reader;
async function readUntilClosed() {
while (port.readable && keepReading) {
reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// reader.cancel() has been called.
break;
}
// value is a Uint8Array.
console.log(value);
}
} catch (error) {
// Handle error...
} finally {
// Allow the serial port to be closed later.
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// User clicked a button to close the serial port.
keepReading = false;
// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
reader.cancel();
await closedPromise;
});
Việc đóng cổng nối tiếp phức tạp hơn khi sử dụng luồng biến đổi. Gọi reader.cancel()
như trước.
Sau đó, hãy gọi writer.close()
và port.close()
. Điều này sẽ truyền lỗi thông qua các luồng biến đổi đến cổng nối tiếp cơ bản. Vì quá trình truyền lỗi không xảy ra ngay lập tức, nên bạn cần sử dụng các lời hứa readableStreamClosed
và writableStreamClosed
đã tạo trước đó để phát hiện thời điểm port.readable
và port.writable
được mở khoá. Việc huỷ reader
sẽ khiến luồng bị huỷ; đó là lý do bạn phải phát hiện và bỏ qua lỗi xảy ra.
// With transform streams.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed;
await port.close();
Nghe kết nối và ngắt kết nối
Nếu cổng nối tiếp do thiết bị USB cung cấp, thì thiết bị đó có thể được kết nối hoặc ngắt kết nối khỏi hệ thống. Khi được cấp quyền truy cập vào cổng nối tiếp, trang web sẽ theo dõi các sự kiện connect
và disconnect
.
navigator.serial.addEventListener("connect", (event) => {
// TODO: Automatically open event.target or warn user a port is available.
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
});
Xử lý tín hiệu
Sau khi thiết lập kết nối cổng nối tiếp, bạn có thể truy vấn và đặt rõ ràng các tín hiệu do cổng nối tiếp hiển thị để phát hiện thiết bị và kiểm soát luồng. Các tín hiệu này được xác định là giá trị boolean. Ví dụ: một số thiết bị như Arduino sẽ chuyển sang chế độ lập trình nếu tín hiệu Data Terminal Ready (DTR) được bật/tắt.
Bạn có thể đặt tín hiệu đầu ra và nhận tín hiệu đầu vào tương ứng bằng cách gọi port.setSignals()
và port.getSignals()
. Hãy xem ví dụ về cách sử dụng bên dưới.
// Turn off Serial Break signal.
await port.setSignals({ break: false });
// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });
// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send: ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready: ${signals.dataSetReady}`);
console.log(`Ring Indicator: ${signals.ringIndicator}`);
Biến đổi luồng
Khi nhận dữ liệu từ thiết bị nối tiếp, bạn không nhất thiết phải nhận được tất cả dữ liệu cùng một lúc. Tệp này có thể được phân đoạn tuỳ ý. Để biết thêm thông tin, hãy xem phần Các khái niệm về API luồng.
Để giải quyết vấn đề này, bạn có thể sử dụng một số luồng chuyển đổi tích hợp sẵn như TextDecoderStream
hoặc tạo luồng chuyển đổi của riêng mình để phân tích cú pháp luồng đến và trả về dữ liệu đã phân tích cú pháp. Luồng biến đổi nằm giữa thiết bị nối tiếp và vòng lặp đọc đang sử dụng luồng. Phương thức này có thể áp dụng một phép biến đổi tuỳ ý trước khi dữ liệu được sử dụng. Hãy coi đó như một dây chuyền lắp ráp: khi một tiện ích đi xuống dây chuyền, mỗi bước trong dây chuyền sẽ sửa đổi tiện ích đó, để khi đến đích cuối cùng, tiện ích đó sẽ hoạt động đầy đủ.
Ví dụ: hãy xem xét cách tạo một lớp luồng chuyển đổi sử dụng một luồng và chia luồng đó thành các phần dựa trên dấu ngắt dòng. Phương thức transform()
của lớp này được gọi mỗi khi luồng nhận được dữ liệu mới. Phương thức này có thể thêm dữ liệu vào hàng đợi hoặc lưu dữ liệu để sử dụng sau. Phương thức flush()
được gọi khi luồng bị đóng và xử lý mọi dữ liệu chưa được xử lý.
Để sử dụng lớp luồng chuyển đổi, bạn cần chuyển luồng đến qua lớp đó. Trong ví dụ mã thứ ba trong phần Đọc từ cổng nối tiếp, luồng đầu vào ban đầu chỉ được chuyển qua TextDecoderStream
, vì vậy, chúng ta cần gọi pipeThrough()
để chuyển luồng đó qua LineBreakTransformer
mới.
class LineBreakTransformer {
constructor() {
// A container for holding stream data until a new line.
this.chunks = "";
}
transform(chunk, controller) {
// Append new chunks to existing chunks.
this.chunks += chunk;
// For each line breaks in chunks, send the parsed lines out.
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}
flush(controller) {
// When the stream is closed, flush any remaining chunks out.
controller.enqueue(this.chunks);
}
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.getReader();
Để gỡ lỗi các vấn đề về giao tiếp thiết bị nối tiếp, hãy sử dụng phương thức tee()
của port.readable
để phân tách các luồng đến hoặc đi từ thiết bị nối tiếp. Bạn có thể sử dụng độc lập hai luồng được tạo và điều này cho phép bạn in một luồng vào bảng điều khiển để kiểm tra.
const [appReadable, devReadable] = port.readable.tee();
// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.
Thu hồi quyền truy cập vào cổng nối tiếp
Trang web có thể xoá các quyền truy cập vào cổng nối tiếp mà trang web không còn muốn giữ lại bằng cách gọi forget()
trên thực thể SerialPort
. Ví dụ: đối với một ứng dụng web giáo dục được dùng trên máy tính dùng chung với nhiều thiết bị, một lượng lớn quyền do người dùng tạo tích luỹ sẽ tạo ra trải nghiệm người dùng kém.
// Voluntarily revoke access to this serial port.
await port.forget();
Vì forget()
có trong Chrome 103 trở lên, hãy kiểm tra xem tính năng này có được hỗ trợ hay không bằng cách sau:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Mẹo dành cho nhà phát triển
Bạn có thể dễ dàng gỡ lỗi Web Serial API trong Chrome bằng trang nội bộ about://device-log
, nơi bạn có thể xem tất cả các sự kiện liên quan đến thiết bị nối tiếp ở cùng một nơi.
Lớp học lập trình
Trong lớp học lập trình dành cho nhà phát triển của Google, bạn sẽ sử dụng Web Serial API để tương tác với bảng BBC micro:bit nhằm hiển thị hình ảnh trên ma trận LED 5x5.
Hỗ trợ trình duyệt
Web Serial API có trên tất cả các nền tảng máy tính (ChromeOS, Linux, macOS và Windows) trong Chrome 89.
Polyfill
Trên Android, bạn có thể hỗ trợ cổng nối tiếp dựa trên USB bằng cách sử dụng API WebUSB và tiện ích bổ sung Serial API. Trình bổ trợ này chỉ dành cho phần cứng và nền tảng mà bạn có thể truy cập vào thiết bị thông qua API WebUSB vì trình bổ trợ này chưa được trình điều khiển thiết bị tích hợp xác nhận quyền sở hữu.
Bảo mật và quyền riêng tư
Các tác giả của thông số kỹ thuật đã thiết kế và triển khai Web Serial API bằng cách sử dụng các nguyên tắc cốt lõi được xác định trong bài viết Kiểm soát quyền truy cập vào các tính năng mạnh mẽ của nền tảng web, bao gồm cả quyền kiểm soát của người dùng, tính minh bạch và tính công thái học. Khả năng sử dụng API này chủ yếu được kiểm soát bởi một mô hình quyền chỉ cấp quyền truy cập vào một thiết bị nối tiếp tại một thời điểm. Để phản hồi lời nhắc của người dùng, người dùng phải chủ động thực hiện các bước để chọn một thiết bị nối tiếp cụ thể.
Để hiểu rõ những đánh đổi về bảo mật, hãy xem các phần bảo mật và quyền riêng tư trong phần Giải thích về Web Serial API.
Phản hồi
Nhóm Chrome rất muốn biết suy nghĩ và trải nghiệm của bạn về Web Serial API.
Giới thiệu cho chúng tôi về thiết kế API
API có hoạt động như mong đợi không? Hay có phương thức hoặc thuộc tính nào bị thiếu mà bạn cần để triển khai ý tưởng của mình không?
Gửi vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub của Web Serial API hoặc thêm ý kiến của bạn vào một vấn đề hiện có.
Báo cáo vấn đề về việc triển khai
Bạn có phát hiện lỗi khi triển khai Chrome không? Hay cách triển khai khác với thông số kỹ thuật?
Gửi lỗi tại https://new.crbug.com. Hãy nhớ cung cấp nhiều thông tin chi tiết nhất có thể, hướng dẫn đơn giản để tái hiện lỗi và đặt Components (Thành phần) thành Blink>Serial
. Glitch rất phù hợp để chia sẻ các bản tái hiện nhanh chóng và dễ dàng.
Thể hiện sự ủng hộ
Bạn có dự định sử dụng Web Serial API không? Sự ủng hộ công khai của bạn giúp nhóm Chrome ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác thấy tầm quan trọng của việc hỗ trợ các tính năng đó.
Gửi một tweet đến @ChromiumDev bằng hashtag
#SerialAPI
và cho chúng tôi biết bạn đang sử dụng ở đâu và như thế nào.
Đường liên kết hữu ích
- Thông số kỹ thuật
- Theo dõi lỗi
- Mục nhập trên ChromeStatus.com
- Thành phần Blink:
Blink>Serial
Bản thu thử
Lời cảm ơn
Cảm ơn Reilly Grant và Joe Medley đã xem xét bài viết này. Ảnh chụp nhà máy sản xuất máy bay của Quỹ bảo tàng Birmingham trên Unsplash.