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à một giao diện giao tiếp hai chiều, cho phép gửi và nhận dữ liệu theo từng byte.
Web Serial API cung cấp cho các trang web một cách để đọc và ghi vào thiết bị nối tiếp bằng JavaScript. 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 kết nối 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ư vi điều khiển và máy in 3D.
API này cũng là một công cụ hỗ trợ 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 lĩnh vực giáo dục, người có sở thích 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ủa họ. Các thiết bị này thường được điều khiển bằng vi điều khiển thông qua một 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 tuỳ 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. Trong những 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 một ứ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 thông tin liên lạc 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 quy trình 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 đối tượ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 ngăn chặn giao diện người dùng của trang web chặn khi chờ dữ liệu đầu vào. Đây là điều 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 để theo dõi dữ liệu đó.
Để mở một cổng nối tiếp, trước tiên, hãy truy cập vào một đối tượng SerialPort
. Để làm 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, chẳng hạn như thao tác chạm hoặc nhấp chuột, hoặc chọn một cổng trong số các cổng 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()
có một đối tượng chữ tuỳ chọn xác định các 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à mã 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();

Khi gọi requestPort()
, người dùng sẽ được nhắc chọn một thiết bị và trả về một đối tượng SerialPort
. Sau khi bạn có một đối tượng SerialPort
, việc gọi port.open()
với tốc độ truyền 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 đường truyền nối tiếp. Tốc độ này được biểu thị bằng đơn vị bit trên giây (bps). Hãy xem 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à dữ liệu vô nghĩa nếu bạn chỉ định sai giá trị này. Đố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ị bỏ qua khi mô phỏng.
// 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ỳ lựa chọn nào dưới đây khi mở một cổng nối tiếp. Bạn không bắt buộc phải sử dụng các lựa chọn này và chúng 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 hình (1 hoặc 2).parity
: Chế độ chẵn lẻ ("none"
,"even"
hoặc"odd"
).bufferSize
: Kích thước của các vùng đệm đọc và ghi cần được tạo (phải nhỏ hơn 16 MB).flowControl
: Chế độ kiểm soát luồng dữ liệu ("none"
hoặc"hardware"
).
Đọc từ cổng nối tiếp
Các luồng đầu vào và đầu ra trong Web Serial API được xử lý bằng Streams API.
Sau khi thiết lập kết nối cổng nối tiế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 cổng này sẽ được dùng để nhận 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
để truyề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ề không đồng bộ hai thuộc tính: value
và một giá trị boolean done
. Nếu done
là true, tức là cổng nối tiếp đã bị đóng hoặc không có thêm dữ liệu nào đến. Việc gọi port.readable.getReader()
sẽ tạo một trình đọc và khoá readable
cho trình đọc đó. Khi readable
ở trạng thái 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, chẳng hạn như tràn bộ nhớ đệm, lỗi khung hoặc lỗi chẵn lẻ. Những thao tác này được đưa ra dưới dạng ngoại lệ và có thể được nắm bắt bằng cách thêm một vòng lặp khác lên trên vòng lặp trước đó để kiểm tra port.readable
. Điều này có hiệu quả vì miễn là các lỗi không nghiêm trọng, một 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 giá trị 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 lại văn bản, bạn có thể truyền port.readable
qua TextDecoderStream
như minh hoạ dưới đây. TextDecoderStream
là một transform stream lấy tất cả các khối Uint8Array
và chuyển đổi chúng 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 "Bring Your Own Buffer". 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 dùng lại vùng đệm ngoài 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 một TextEncoderStream
được chuyển đến port.writable
như minh hoạ bên dưới.
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()
sẽ đóng cổng nối tiếp nếu các thành phần readable
và writable
của cổng này ở trạng thái chưa khoá, tức là releaseLock()
đã được gọi cho trình đọc và trình ghi tương ứng.
await port.close();
Tuy nhiên, khi đọc liên tục dữ liệu từ một thiết bị nối tiếp bằng cách sử dụ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()
giải quyết ngay lập tức bằng { value: undefined, done: true }
và 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 sẽ 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()
. Thao tác 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 diễn ra ngay lập tức, nên bạn cần sử dụng các promise 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ỷ bỏ; đây là lý do bạn phải bắt và bỏ qua lỗi kết quả.
// 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 sự kiện kết nối và ngắt kết nối
Nếu một thiết bị USB cung cấp cổng nối tiế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 một cổng nối tiếp, trang web phải 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 lưu lượ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.
Việc thiết lập tín hiệu đầu ra và nhận tín hiệu đầu vào được thực hiện bằng cách gọi port.setSignals()
và port.getSignals()
. Hãy xem các 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 tất cả dữ liệu cùng một lúc. Nội dung có thể được chia thành các phần tuỳ ý. Để biết thêm thông tin, hãy xem phần Các khái niệm về Streams API.
Để xử lý 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. Luồng này cho phép bạn 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. Bạn có thể áp dụng một phép biến đổi tuỳ ý trước khi sử dụng dữ liệu. Hãy coi đây 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 đó, sao cho đến khi tiện ích đến được đích đến cuối cùng, thì đó là một tiện ích hoạt động đầy đủ.

Ví dụ: hãy xem xét cách tạo một lớp luồng biến đổi sử dụng một luồng và chia thành các đoạn dựa trên dấu ngắt dòng. Phương thức transform()
của nó được gọi mỗi khi luồng nhận được dữ liệu mới. Thao tác này có thể xếp hàng dữ liệu hoặc lưu dữ liệu để dùng sau. Phương thức flush()
được gọi khi luồng bị đóng và phương thức này xử lý mọi dữ liệu chưa được xử lý.
Để sử dụng lớp luồng biến đổi, bạn cần truyền một luồng dữ liệu đến thông qua lớp này. 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 truyền qua TextDecoderStream
, vì vậy, chúng ta cần gọi pipeThrough()
để truyền luồng này 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 với thiết bị nối tiếp, hãy sử dụng phương thức tee()
của port.readable
để chia các luồng đi đế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 này 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 phiên bản 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 số lượng lớn các quyền do người dùng tạo được 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 làm như sau:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Mẹo 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
. Tại đây, bạn có thể xem tất cả các sự kiện liên quan đến thiết bị nối tiếp ở một nơi duy nhất.

Lớp học lập trình
Trong lớp học lập trình của Google dành cho nhà phát triển, 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 của bảng này.
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 WebUSB API và Serial API polyfill. Polyfill này chỉ giới hạn ở phần cứng và nền tảng mà thiết bị có thể truy cập thông qua API WebUSB vì 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 tả đã thiết kế và triển khai Web Serial API bằng các nguyên tắc cốt lõi được xác định trong 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 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ằng 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 duy nhất 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 thực hiện các bước chủ động để chọn một thiết bị nối tiếp cụ thể.
Để hiểu rõ những điểm đá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 Tài liệu giải thích về Web Serial API.
Phản hồi
Nhóm Chrome rất muốn biết ý kiến và trải nghiệm của bạn về Web Serial API.
Hãy cho chúng tôi biết về thiết kế API
Có vấn đề gì về API không hoạt động như mong đợi không? Hoặc 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ữ Web Serial API GitHub 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 thấy lỗi trong quá trình triển khai của Chrome không? Hay việc triển khai có khác với quy cách không?
Gửi báo cáo lỗi tại https://new.crbug.com. Nhớ cung cấp càng nhiều thông tin chi tiết càng tốt, hướng dẫn đơn giản để mô phỏng lỗi và đặt Thành phần thành Blink>Serial
.
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 này.
Gửi một tweet đến @ChromiumDev bằng thẻ bắt đầu bằng #SerialAPI
và cho chúng tôi biết bạn đang sử dụng tính năng này ở đâu và như thế nào.
Đường liên kết hữu ích
- Quy cách kỹ thuật
- Lỗi theo dõi
- Mục nhập trên ChromeStatus.com
- Thành phần Blink:
Blink>Serial
Bản minh hoạ
Lời cảm ơn
Cảm ơn Reilly Grant và Joe Medley đã xem xét bài viết này. Ảnh nhà máy sản xuất máy bay của Birmingham Museums Trust trên Unsplash.