Web Serial API 可讓網站與序列裝置通訊。
什麼是 Web Serial API?
序列埠是雙向通訊介面,可逐位元組傳送及接收資料。
Web Serial API 可讓網站透過 JavaScript 讀取及寫入序列裝置。序列裝置可透過使用者系統上的序列埠連線,或是透過模擬序列埠的可移除 USB 和藍牙裝置連線。
換句話說,Web Serial API 可讓網站與序列裝置 (例如微控制器和 3D 印表機) 進行通訊,連結網路與實體世界。
此 API 也非常適合 WebUSB,因為作業系統要求應用程式必須使用其較高層級的序列 API (而非低階 USB API) 與某些序列埠通訊。
建議用途
在教育、業餘愛好者和工業領域,使用者會將周邊裝置連接至電腦。這些裝置通常會透過自訂軟體使用的序列連線,由微控制器控制。某些用於控制這些裝置的自訂軟體是採用網路技術打造而成:
在某些情況下,網站會透過使用者手動安裝的代理程式應用程式,與裝置進行通訊。在其他情況下,應用程式會透過 Electron 等架構,以封裝應用程式的方式提供。在其他情況下,使用者必須執行額外步驟,例如透過 USB 隨身碟將已編譯的應用程式複製到裝置。
在所有這些情況中,只要在網站和所控制的裝置之間提供直接通訊,就能改善使用者體驗。
目前狀態
使用 Web Serial API
特徵偵測
如要檢查是否支援 Web Serial API,請使用:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
開啟序列埠
Web Serial API 的設計為非同步。這可避免網站 UI 在等待輸入時遭到封鎖,這點相當重要,因為序列資料可隨時接收,因此需要有一種方式來監聽。
如要開啟序列埠,請先存取 SerialPort
物件。為此,您可以呼叫 navigator.serial.requestPort()
來回應使用者的手勢 (例如觸控或滑鼠點擊),提示使用者選取單一序列埠,或是從 navigator.serial.getPorts()
中選取一個,該函式會傳回網站已獲授權存取的序列埠清單。
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();
navigator.serial.requestPort()
函式會採用選用的物件常值,用於定義篩選器。這些資訊可用於比對透過 USB 連線的任何序列裝置,以及強制規定的 USB 供應商 (usbVendorId
) 和選用的 USB 產品 ID (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();
呼叫 requestPort()
會提示使用者選取裝置,並傳回 SerialPort
物件。取得 SerialPort
物件後,使用所需波特率呼叫 port.open()
即可開啟序列埠。baudRate
字典成員會指定資料透過序列線傳送的速度。以每秒位元數 (bps) 為單位。請查看裝置的說明文件,找出正確的值,因為如果指定的值不正確,您傳送和接收的所有資料都會變成亂碼。對於模擬序列埠的部分 USB 和藍牙裝置,這個值可以安全地設為任何值,因為模擬程序會忽略這個值。
// 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 });
您也可以在開啟序列埠時指定下列任何選項。這些選項為選用選項,並提供方便的預設值。
dataBits
:每個影格內的資料位元數量 (7 或 8)。stopBits
:影格結尾的停止位元數量 (1 或 2)。parity
:奇偶校驗模式 ("none"
、"even"
或"odd"
)。bufferSize
:應建立的讀取和寫入緩衝區大小 (必須小於 16 MB)。flowControl
:流量控制模式 ("none"
或"hardware"
)。
從序列埠讀取
Web Serial API 中的輸入和輸出串流由 Streams API 處理。
建立序列埠連線後,SerialPort
物件的 readable
和 writable
屬性會傳回 ReadableStream 和 WritableStream。這些會用於接收序列裝置的資料,並將資料傳送至序列裝置。兩者都使用 Uint8Array
例項進行資料傳輸。
當序列裝置傳送新資料時,port.readable.getReader().read()
會以非同步方式傳回兩個屬性:value
和 done
布林值。如果 done
為 true,表示序列埠已關閉或沒有更多資料傳入。呼叫 port.readable.getReader()
會建立讀取器,並將 readable
鎖定在其中。readable
鎖定時,序列埠無法關閉。
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);
}
在某些情況下,可能會發生一些非致命的序列埠讀取錯誤,例如緩衝區溢位、框架錯誤或奇偶錯誤。這些錯誤會以例外狀況擲回,您可以透過在先前檢查 port.readable
的迴圈上方新增另一個迴圈來捕捉這些錯誤。這項功能之所以有效,是因為只要錯誤不嚴重,系統就會自動建立新的 ReadableStream。如果發生重大錯誤 (例如移除序列裝置),port.readable
就會變成空值。
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.
}
}
如果序列裝置傳回文字,您可以透過 TextDecoderStream
管道傳送 port.readable
,如下所示。TextDecoderStream
是轉換串流,可擷取所有 Uint8Array
區塊並轉換為字串。
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);
}
使用「自備緩衝區」讀取器讀取串流時,您可以控管記憶體的分配方式。呼叫 port.readable.getReader({ mode: "byob" })
以取得 ReadableStreamBYOBReader 介面,並在呼叫 read()
時提供您自己的 ArrayBuffer
。請注意,Web Serial API 在 Chrome 106 以上版本中支援這項功能。
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()...
}
}
以下範例說明如何重複使用 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`.
}
以下是另一個從序列埠讀取特定資料量資料的範例:
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);
寫入序列埠
如要將資料傳送至序列裝置,請將資料傳遞至 port.writable.getWriter().write()
。必須在 port.writable.getWriter()
上呼叫 releaseLock()
,才能在稍後關閉序列埠。
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();
透過傳遞至 port.writable
的 TextEncoderStream
將文字傳送至裝置,如下所示。
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
關閉序列埠
如果 readable
和 writable
成員已解鎖,port.close()
就會關閉序列埠,這表示已為其讀取器和寫入器分別呼叫 releaseLock()
。
await port.close();
不過,如果使用迴圈持續讀取序列裝置的資料,port.readable
會一直處於鎖定狀態,直到遇到錯誤為止。在這種情況下,呼叫 reader.cancel()
會強制 reader.read()
立即使用 { value: undefined, done: true }
解析,因此可讓迴圈呼叫 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;
});
使用轉換串流時,關閉序列埠的程序會更加複雜。如先前所述,呼叫 reader.cancel()
。然後呼叫 writer.close()
和 port.close()
。這會透過轉換串流將錯誤傳播至基礎序列埠。由於錯誤傳播並不會立即發生,您必須使用先前建立的 readableStreamClosed
和 writableStreamClosed
承諾,偵測 port.readable
和 port.writable
何時已解鎖。取消 reader
會導致串流中止,因此您必須擷取並忽略產生的錯誤。
// 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();
監聽連線和中斷連線
如果序列埠是由 USB 裝置提供,則該裝置可能已與系統連線或中斷連線。網站獲得存取序列埠的權限後,應監控 connect
和 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.
});
處理信號
建立序列埠連線後,您可以明確查詢和設定由序列埠公開的信號,以用於裝置偵測和流量控制。這些信號已定義為布林值。舉例來說,如果切換資料終端機就緒 (DTR) 信號,Arduino 等部分裝置就會進入程式設計模式。
您可以分別呼叫 port.setSignals()
和 port.getSignals()
,設定輸出信號和取得輸入信號。請參閱下方的使用範例。
// 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}`);
轉換串流
從序列裝置接收資料時,不一定會一次取得所有資料。可任意分割。詳情請參閱 Streams API 概念。
如要解決這個問題,您可以使用 TextDecoderStream
等內建轉換串流,或是自行建立轉換串流,以便剖析傳入的串流並傳回剖析的資料。轉換串流位於序列裝置和讀取迴圈之間,而讀取迴圈會消耗串流。在使用資料前,可以套用任意轉換。您可以將其視為裝配線:當小工具沿著裝配線移動時,每個步驟都會修改小工具,因此當小工具到達最終目的地時,就會是完全運作的小工具。
舉例來說,請考慮如何建立會取用串流的轉換串流類別,並根據換行符號進行分段。每次資料流收到新資料時,就會呼叫其 transform()
方法。它可以將資料排入佇列,或儲存起來以供日後使用。串流關閉時會呼叫 flush()
方法,並處理尚未處理的任何資料。
如要使用轉換串流類別,您需要透過管道管道管道傳入傳入串流。在「從序列埠讀取」下方的第三個程式碼範例中,原始輸入串流只會透過 TextDecoderStream
管道傳送,因此我們需要呼叫 pipeThrough()
,將其透過新的 LineBreakTransformer
管道傳送。
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();
如要偵錯序列裝置通訊問題,請使用 port.readable
的 tee()
方法,將傳入或傳出序列裝置的串流分割。您可以獨立使用建立的兩個串流,並將其中一個串流輸出至控制台以供檢查。
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.
撤銷序列埠的存取權
網站可在 SerialPort
例項上呼叫 forget()
,藉此清除序列埠的存取權限,以免繼續保留這些權限。舉例來說,如果在有多個裝置共用的電腦上使用教育用途的網路應用程式,大量累積的使用者產生權限會導致使用者體驗不佳。
// Voluntarily revoke access to this serial port.
await port.forget();
forget()
可在 Chrome 103 以上版本中使用,請確認是否支援下列功能:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
開發人員提示
您可以使用內部頁面 about://device-log
輕鬆在 Chrome 中偵錯 Web Serial API,在該頁面中,您可以一次查看所有序列裝置相關事件。
程式碼研究室
在 Google 開發人員程式碼研究室中,您將使用 Web Serial API 與 BBC micro:bit 面板互動,以顯示 5x5 LED 矩陣上的圖片。
瀏覽器支援
在 Chrome 89 中,所有電腦平台 (ChromeOS、Linux、macOS 和 Windows) 皆可使用 Web Serial API。
聚酯纖維
在 Android 上,您可以使用 WebUSB API 和 Serial API polyfill 支援 USB 序列埠。這個 polyfill 僅適用於硬體和平台,因為這些裝置可透過 WebUSB API 存取,但未由內建裝置驅動程式宣告。
安全性和隱私權
規格作者根據「控管強大網路平台功能的存取權」一文中定義的核心原則,設計並實作 Web Serial API,包括使用者控制、資訊公開和人體工學。使用這個 API 的能力主要受到權限模型的限制,該模型會一次只授予單一序列裝置的存取權。使用者必須採取主動步驟,才能選取特定序列裝置,回應使用者提示。
如要瞭解安全性取捨,請參閱 Web Serial API 說明的「安全性」和「隱私權」部分。
意見回饋
Chrome 團隊很樂意聽取你對 Web Serial API 的想法和使用體驗。
請說明 API 設計
API 是否有任何功能無法正常運作?還是需要實現想法的方法或屬性?
在 Web Serial API GitHub 存放區中提交規格問題,或在現有問題中加入您的想法。
回報實作問題
你是否發現 Chrome 實作項目有錯誤?還是實作方式與規格不同?
請前往 https://new.crbug.com 提交錯誤。請務必盡可能提供詳細資訊,提供重現錯誤的簡單操作說明,並將「元件」設為 Blink>Serial
。Glitch 非常適合用來快速輕鬆地提出重新提案。
顯示支援
您是否打算使用 Web Serial API?您的公開支援有助於 Chrome 團隊決定功能的優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。
使用主題標記 #SerialAPI
將推文傳送至 @ChromiumDev,並告知我們您使用該標記的位置和方式。
實用連結
- 規格
- 追蹤錯誤
- ChromeStatus.com 項目
- Blink 元件:
Blink>Serial
示範
特別銘謝
感謝 Reilly Grant 和 Joe Medley 審查本文。飛機工廠相片,由 Birmingham Museums Trust 提供,並發布於 Unsplash。