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 giúp 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 một cổng nối tiếp trên hệ thống của người dùng hoặc thông qua thiết bị Bluetooth và USB có thể tháo rời mô phỏng một 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 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 lựa chọn đồng hành tuyệt vời cho WebUSB khi mà các hệ điều hành đòi hỏi để giao tiếp với một số cổng nối tiếp bằng các cổng cấp cao hơn nối tiếp thay vì API USB cấp thấp.
Các trường hợp sử dụng được đề xuất
Trong lĩnh vực giáo dục, sản xuất theo sở thích và công nghiệp, người dùng kết nối thiết bị ngoại vi thiết bị sang máy tính của họ. Các thiết bị này thường do vi điều khiển qua kết nối nối tiếp do phần mềm tuỳ chỉnh sử dụng. Một số thành phần tuỳ chỉnh phần mềm đ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 tác nhân mà người dùng đã cài đặt theo cách thủ công. Còn ở các quốc gia khác, ứng dụng lại được phân phối trong ứng dụng đóng gói thông qua một khung, chẳng hạn như Electron. Và nói cách khác, người dùng được yêu cầu thực hiện thêm một bước như sao chép ứng dụng đã biên dịch sang thiết bị qua ổ USB flash.
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 hoạt động giao 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 thông báo giải thích | Hoàn tất |
2. Tạo bản nháp ban đầu của thông số kỹ thuật | 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. Ra mắt | 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 dùng:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Mở cổng nối tiếp
Web Serial API không đồng bộ theo thiết kế. Thao tác này ngăn giao diện người dùng của trang web chặn khi đang chờ đầu vào. Điều này rất quan trọng vì dữ liệu nối tiếp có thể nhận được bất cứ lúc nào, đòi hỏi phải có cách nghe mới.
Để mở cổng nối tiếp, trước tiên hãy truy cập vào đối tượng SerialPort
. Để làm được điều 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ư chạm
hoặc nhấp chuột hoặc chọn một từ navigator.serial.getPorts()
để trả về
danh sách 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 thẻ đó dùng để khớp với bất kỳ thiết bị nối tiếp nào được kết nối qua
USB với nhà cung cấp USB bắt buộc (usbVendorId
) và sản phẩm USB tùy chọn
(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ó đối tượng SerialPort
, hãy gọi port.open()
có tốc độ baud mong muốn sẽ mở cổng nối tiếp. Từ điển baudRate
thành viên chỉ định tốc độ gửi dữ liệu qua đường nối tiếp. Ngôn ngữ này được thể hiện bằng
đơn vị bit/giây (bps). Hãy xem tài liệu của thiết bị để biết
giá trị chính xác do tất cả dữ liệu bạn gửi và nhận sẽ bị vô nghĩa nếu đây là
chỉ định không chính xác. Đối với một số thiết bị USB và Bluetooth mô phỏng một nối tiếp
cổng này có thể được đặt an toàn thành bất kỳ giá trị nào vì nó bị bỏ qua
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ỳ tuỳ chọn nào dưới đây khi mở cổng nối tiếp. Các là 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 (7 hoặc 8).stopBits
: Số bit dừng ở cuối một 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 qua cổng nối tiếp
Các luồng đầu vào và đầu ra trong Web Serial API do API Luồng xử lý.
Sau khi thiết lập kết nối cổng nối tiếp, readable
và writable
các thuộc tính từ đối tượng SerialPort
sẽ trả về ReadableStream và
WritableStream. Các cookie đó sẽ được dùng để nhận dữ liệu và gửi dữ liệu đến
thiết bị nối tiếp. Cả hai đều sử dụng phiên bản Uint8Array
để chuyển dữ liệu.
Khi thiết bị nối tiếp nhận được dữ liệu mới, port.readable.getReader().read()
trả về hai thuộc tính không đồng bộ: value
và boolean done
. Nếu
done
là true, cổng nối tiếp đã đóng hoặc không còn dữ liệu nào
trong năm Việc gọi port.readable.getReader()
sẽ tạo một trình đọc và khoá readable
để
nó. Mặc dù readable
đang khoá nhưng 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 vùng đệm, lỗi đóng khung hoặc lỗi tương đương. Những quảng cáo đó được gửi dưới dạng
ngoại lệ và có thể phát hiện được 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
. Phương pháp này hiệu quả vì miễn là 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
xảy ra, chẳng hạn như thiết bị nối tiếp bị xoá, thì port.readable
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 một
TextDecoderStream
như được hiển thị dưới đây. TextDecoderStream
là một luồng biến đổi
để lấy tất cả Uint8Array
phần 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 bộ nhớ được phân bổ khi đọc từ luồng bằng cách sử dụng tính năng "Lấy bộ đệm riêng" người đọc. Gọi port.readable.getReader({ mode: "byob" })
để nhận giao diện ReadableStreamBYOBReader và cung cấp ArrayBuffer
của riêng bạn khi gọi read()
. Lưu ý rằng API Web Serial 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()...
}
}
Dưới đây là ví dụ về cách sử 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ể qua 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 một 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()
. Đang gọi cho releaseLock()
Bạn phải có 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 tin nhắn văn bản đến thiết bị thông qua một TextEncoderStream
được nối tới 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 nó
được mở khoá, nghĩa là releaseLock()
đã được gọi cho
độc giả và tác giả.
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 vòng lặp,
port.readable
sẽ luôn bị khoá cho đến khi gặp lỗi. Trong phần này
trong trường hợp khác, việc gọi reader.cancel()
sẽ buộc reader.read()
phải phân giải
ngay lập tức với { value: undefined, done: true }
, nhờ đó cho phé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 đây.
Sau đó, hãy gọi writer.close()
và port.close()
. Thao tác này sẽ truyền lỗi thông qua
chuyển đổi luồng sang cổng nối tiếp cơ bản. Do sự lan truyền lỗi
không xảy ra ngay lập tức, bạn cần phải sử dụng readableStreamClosed
và
writableStreamClosed
lời hứa được tạo trước đó để phát hiện khi port.readable
và port.writable
đã được mở khoá. Việc loại bỏ reader
sẽ khiến
phát trực tuyến bị huỷ bỏ; đây là lý do bạn phải phát hiện và bỏ qua lỗi phát sinh.
// 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 thông báo kết nối và ngắt kết nối
Nếu 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 bị ngắt kết nối khỏi hệ thống. Khi trang web đã được cấp quyền để
một cổng nối tiếp, cổng này sẽ giám sát 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à thiết lập tín hiệu do cổng nối tiếp hiển thị để phát hiện thiết bị và điều khiển luồng. Các tín hiệu đượ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 Sẵn sàng (DTR) là bật/tắt.
Việc đặt tín hiệu đầu ra và nhận tín hiệu đầu vào tương ứng được thực hiện bằng cách
đang 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 bạn nhận được dữ liệu từ thiết bị nối tiếp, không phải lúc nào bạn cũng nhận được tất cả dữ liệu cùng một lúc. Khối này có thể được phân đoạn tuỳ ý. Để biết thêm thông tin, hãy xem Khái niệm về Streams API.
Để 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 biến đổi của riêng bạn để có thể
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 ở lại
giữa thiết bị nối tiếp và vòng lặp đọc đang sử dụng luồng. Chiến dịch này có thể
áp dụng phép biến đổi tuỳ ý trước khi sử dụng dữ liệu. Hãy nghĩ về điều này như một
dây chuyền lắp ráp: khi một tiện ích xuất hiện trong dòng, mỗi bước trong dòng sẽ sửa đổi
để tiện ích con đó đi tới đích cuối cùng, nó
tiện ích hoạt động.
Ví dụ: hãy xem xét cách tạo một lớp luồng chuyển đổi sử dụng
và phân đoạn dữ liệu đó 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 lại để dùng sau. Phương thức flush()
được gọi khi luồng bị đóng và
nó xử lý mọi dữ liệu chưa được xử lý.
Để sử dụng lớp chuyển đổi luồng, bạn cần chuyển một luồng đến qua
nó. Trong ví dụ về mã thứ ba trong mục Đọc từ cổng nối tiếp,
luồng đầu vào ban đầu chỉ được dẫn qua TextDecoderStream
, vì vậy chúng tôi
cần gọi pipeThrough()
để chuyển nó qua LineBreakTransformer
mới của chúng tô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 sự cố 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 từ thiết bị nối tiếp. Hai
luồng được tạo có thể được dùng một cách độc lập 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ể dọn dẹp quyền truy cập vào một cổng nối tiếp mà trang web này không còn hoạt động nữa
muốn giữ lại bằng cách gọi forget()
trên thực thể SerialPort
. Cho
ví dụ: đối với ứng dụng web giáo dục dùng trên máy tính dùng chung với
trên thiết bị, một số lượng lớn các quyền do người dùng tạo tích luỹ sẽ tạo ra
trải nghiệm người dùng.
// 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ợ bằng các tính năng 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 thông qua trang nội bộ,
about://device-log
nơi bạn có thể xem tất cả sự kiện liên quan đến thiết bị nối tiếp trong một
vị trí duy nhất.
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 bằng bảng BBC micro:bit để 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 dành cho máy tính (ChromeOS, Linux, macOS, và Windows) trong Chrome 89.
Ống polyfill
Trên Android, có thể hỗ trợ cổng nối tiếp dựa trên USB bằng cách sử dụng WebUSB API và polyfill API nối tiếp. Đoạn polyfill này chỉ dùng được cho phần cứng và nền tảng nơi thiết bị có thể truy cập được qua WebUSB API vì ứng dụng này chưa do một 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 này đã thiết kế và triển khai Web Serial API bằng cách sử dụng nguyên tắc xác định trong Kiểm soát quyền truy cập vào các tính năng nền tảng web mạnh mẽ, bao gồm cả quyền kiểm soát của người dùng, tính minh bạch và công thái học. Khả năng sử dụng tính năng này API chủ yếu chịu sự kiểm soát của một mô hình quản lý quyền chỉ cấp quyền truy cập vào một tài khoản duy nhất thiết bị nối tiếp cùng một lúc. Để phản hồi lời nhắc của người dùng, người dùng phải kích hoạt để chọn một thiết bị nối tiếp cụ thể.
Để hiểu rõ các yếu tố đánh đổi về bảo mật, hãy xem phần bảo mật và quyền riêng tư trong Công cụ giải thích 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ới Web Serial API.
Cho chúng tôi biết về thiết kế API
Có điều gì về API không hoạt động như mong đợi không? Hoặc có còn thiếu phương thức hoặc thuộc tính nào mà bạn cần để triển khai ý tưởng của mình?
Gửi vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub về Web Serial API hoặc thêm về một vấn đề hiện tại.
Báo cáo sự cố về triển khai
Bạn có phát hiện lỗi trong quá trình triển khai Chrome không? Hoặc là triển khai khác với thông số kỹ thuật không?
Báo cáo lỗi tại https://new.crbug.com. Hãy nhớ bao gồm nhiều
chi tiết nhất có thể, cung cấp hướng dẫn đơn giản để tái tạo lỗi và
Đã đặt Thành phần thành Blink>Serial
. Lỗi trục phù hợp với
chia sẻ các bản ghi lại nhanh chóng và dễ dàng.
Thể hiện sự ủng hộ
Bạn có định sử dụng Web Serial API không? Sự hỗ trợ công khai của bạn sẽ giúp Chrome nhóm ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác biết tầm quan trọng của việc này hỗ trợ họ.
Gửi một bài đăng đến @ChromiumDev kèm theo hashtag
#SerialAPI
đồng thời cho chúng tôi biết bạn đang sử dụng ở đâu và như thế nào.
Các đường liên kết hữu ích
- Thông số kỹ thuật
- Theo dõi lỗi
- Mục ChromeStatus.com
- Thành phần nháy:
Blink>Serial
Bản thu thử
Xác nhận
Cảm ơn Reilly Grant và Joe Medley đã đánh giá bài viết này. Hình ảnh nhà máy máy bay của Birmingham Museums Trust trên Unsplash.