讀取及寫入序列埠

Web Serial API 可讓網站與序列裝置通訊。

François Beaufort
François Beaufort

什麼是 Web Serial API?

序列埠是一種雙向通訊介面,可以 如何按位元組接收資料

Web Serial API 可讓網站讀取及寫入 以及 JavaScript 的序列裝置。序列裝置是透過 使用者系統上的序列埠,或是透過卸除式 USB 和藍牙裝置進行卸除 模擬序列埠。

也就是說,Web Serial API 可藉由 可讓網站與微控制器等序列裝置通訊 和 3D 列印機。

由於作業系統需要,此 API 也是 WebUSB 的妙用 可透過其較高等級的序列埠 序列 API,而非低階 USB API。

建議用途

在教育、業餘和工業領域中,使用者會連結週邊裝置 將裝置連接到電腦。這些裝置通常是由 可透過序列連線取得微控制器。一些自訂 用來控制這些裝置的軟體是以網路技術為基礎:

在某些情況下,網站會透過代理程式與裝置進行通訊 使用者手動安裝的應用程式。在其他平台中,應用程式是 透過 Electron 等架構在封裝應用程式中交付的成果 有些則需要使用者執行額外的步驟,例如 透過 USB 隨身碟將已編譯的應用程式複製到裝置中。

在這些情況下,如果直接提供 與受控制的裝置之間進行通訊。

目前狀態

步驟 狀態
1. 建立說明 完成
2. 建立規格的初始草稿 完成
3. 收集意見回饋與在設計上反覆測試 完成
4. 來源試用 完成
5. 啟動 完成

使用 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 產品 識別碼 (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();
敬上
網站中序列埠提示的螢幕截圖
使用者選取 BBC micro:bit 的提示

呼叫 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 處理。

建立序列埠連線後,readablewritable SerialPort 物件的屬性會傳回 ReadableStreamWritableStream。這些資產會用來接收來自 序列裝置。兩者都使用 Uint8Array 執行個體進行資料移轉。

序列裝置收到新資料時,port.readable.getReader().read() 非同步傳回兩個屬性:valuedone 布林值。如果 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.
  }
}

如果序列裝置傳回文字,您可以透過直立線port.readable透過 TextDecoderStream,如下所示。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()。將在以下裝置上撥打電話給releaseLock(): 必須有 port.writable.getWriter() 才能稍後關閉序列埠。

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.writableTextEncoderStream 傳送簡訊到裝置 如下所示。

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

關閉序列埠

如果 readablewritable 成員,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 裝置提供序列埠,則該裝置可能已連線 或是與系統中斷連線網站獲得下列權限時: 存取序列埠,其應會監控 connectdisconnect 事件。

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.
});
敬上

處理信號

建立序列埠連線後,即可明確查詢和設定 序列埠所公開的信號,用於裝置偵測及流量控制。這些 信號會定義為布林值例如,某些裝置,例如 Arduino 如果資料終端機已就緒 (DTR) 訊號,就會進入程式設計模式 已切換。

設定輸出信號及取得輸入信號,分別由以下方法完成: 正在呼叫 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() 方法,且 它會處理任何尚未處理的資料

如要使用轉換串流類別,您需要透過 基礎架構在第三個程式碼範例中,「Read from a serial port」(從序列埠讀取) 原始輸入串流只透過 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();

如要對序列裝置的通訊問題進行偵錯,請使用 tee() 方法 使用 port.readable 拆分序列裝置的串流。兩者 因此可獨立使用建立的串流,方便您列印 以便檢查。

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.
}

開發人員秘訣

使用內部頁面即可輕鬆在 Chrome 中對 Web Serial API 偵錯 about://device-log,您可以在這裡一次查看所有序列裝置相關事件 單一位置

用於 Web Serial API 偵錯的內部頁面螢幕截圖。
適用於 Web Serial API 偵錯的 Chrome 內部頁面。

程式碼研究室

Google 開發人員程式碼研究室中,您將使用 Web Serial API 與 BBC micro:bit 電路板會在 5x5 LED 矩陣上顯示圖片。

瀏覽器支援

Web Serial API 適用於所有電腦平台 (ChromeOS、Linux、macOS、 和 Windows)。

聚合物

在 Android 上,可使用 WebUSB API 支援 USB 序列埠 和 Serial API polyfill。這個 polyfill 僅限於硬體和 可透過 WebUSB API 存取裝置的平台 由內建裝置驅動程式聲明擁有權。

安全性和隱私權

規格作者使用核心架構設計並實作 Web Serial API 控管強大的 Web Platform 功能存取權中定義的原則, 包括使用者控制權、資訊公開以及人因工程學透過這項功能 API 主要由權限模型控管,該模型僅會將存取權授予單一 序列裝置。如要回應使用者提示,使用者必須主動操作 按步驟選取特定序列裝置。

如要瞭解安全性的取捨,請參閱安全性隱私權的相關說明。 《Web Serial API 說明》章節。

意見回饋

Chrome 小組很想聽聽您的想法和使用經驗 Web Serial API。

請與我們分享 API 設計

是否有 API 未正常運作?或者在那裡

前往 Web Serial API GitHub 存放區提交規格問題,或新增 對現有議題的看法

回報導入問題

您發現 Chrome 實作錯誤嗎?另一種是實作 該怎麼辦?

前往 https://new.crbug.com 回報錯誤。請務必盡量附上 請盡可能詳細說明,提供重現錯誤的簡單操作說明,並請 「元件」設為 Blink>SerialGlitch 適用於以下情境: 輕鬆快速地做出反應

顯示支援

您是否打算使用 Web Serial API?你的公開支援能協助 Chrome 讓功能優先處理,並向其他瀏覽器供應商說明其重要性 對他們提供支援

使用主題標記將推文傳送至 @ChromiumDev #SerialAPI敬上 ,並說明你使用這項服務的位置和方式。

實用連結

示範

特別銘謝

感謝 Reilly GrantJoe Medley 撰寫這篇文章。 飛機工廠相片 (Birmingham Museums Trust) 於 Unsplash 提供。