開始使用 Web Serial API

上次更新日期:2019-11-08

建構項目

在本程式碼研究室中,您將建立一個使用 Web Serial API 的網頁與 BBC micro:bit 板互動的網頁,以便在其 5x5 LED 矩陣上顯示圖片。您將瞭解 Web Serial API,以及如何使用可讀取、可寫入及轉換的串流,透過瀏覽器與序列裝置進行通訊。

您將會瞭解的內容

  • 如何開啟及關閉網路序列埠
  • 如何使用讀取迴圈處理輸入串流的資料
  • 如何透過寫入串流傳送資料

軟硬體需求

  • 搭載 Bspruino 韌體BBC micro:bit
  • 最新版的 Chrome (78 以上版本)
  • 對 HTML、CSS、JavaScript 和 Chrome 開發人員工具的瞭解

我們選擇使用 micro:bit 作為這個程式碼研究室,因為其經濟實惠,提供數個輸入 (按鈕) 和輸出 (5x5 LED 顯示),且可提供額外的輸入和輸出。請參閱 Espruino 網站上的 BBC micro:bit 網頁,瞭解 micro:bit 具備的功能。

Web Serial API 可讓網站透過指令碼讀取及寫入序列裝置。這個 API 可讓網站與微控制器 (例如微控制器和 3D 印表機) 等通訊裝置相互通訊,藉此連結網路和實體世界。

許多使用網路技術建構的控制軟體都有許多範例。例如:

在某些情況下,這些網站會透過使用者手動安裝的原生代理程式應用程式與裝置通訊。在其他情況下,應用程式會透過封裝軟體內建應用程式,透過 Electron 等架構提交。在其他情況下,使用者必須執行額外步驟,例如將經過編譯的應用程式透過 USB 隨身碟複製到裝置。

使用者可直接在受控制的網站與裝置間進行通訊,藉此改善使用者體驗。

啟用 Web Serial API

Web Serial API 目前處於開發階段,只能使用一個標記。您必須在 chrome://flags 中啟用 #enable-experimental-web-platform-features 標記。

取得程式碼

我們已將這個程式碼研究室所需的一切工具都放到 Glitch 專案中。

  1. 開啟新的瀏覽器分頁,然後前往 https://web-Serial-codelab-start.glitch.me/
  2. 按一下 [Remix Glitch] 連結,建立您自己的入門專案版本。
  3. 按一下 [顯示] 按鈕,然後選擇 [在新視窗中開啟],即可查看程式碼的運作情形。

查看是否支援 Web Serial API

首先,請檢查目前瀏覽器是否支援 Web Serial API。方法很簡單,請檢查serial是否位在「navigator」中。

DOMContentLoaded 事件中,將下列程式碼加進專案:

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
if ('serial' in navigator) {
  const notSupported = document.getElementById('notSupported');
  notSupported.classList.add('hidden');
}

這會檢查是否支援網路序列。如果是的話,這段程式碼會隱藏顯示不支援「網路序列」的橫幅。

試用

  1. 載入網頁。
  2. 確認網頁並未顯示紅色橫幅,指出不支援網路序列。

開啟序列埠

接下來,我們必須開啟序列埠。如同其他許多新式 API,Web Serial API 是非同步的。如此可以防止使用者介面在等待輸入時封鎖,但這也很重要,因為網頁可能隨時收到序列資料,而我們需要一個方法來監聽。

由於電腦可能有多個序列裝置,當瀏覽器嘗試要求連接埠時,系統會提示使用者選擇要連線的裝置。

將下列程式碼新增到您的專案:

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudrate: 9600 });

requestPort 通話會提示使用者要連結的裝置。呼叫 port.open 會開啟通訊埠。此外,我們還需要提供與序列裝置通訊的速度。BBC micro:bit 使用 USB 對序列晶片與主處理器之間的連線為 9600。

此外,這也讓 [連線] 按鈕掛上了按鈕,並在使用者點擊按鈕時呼叫 connect()

將下列程式碼新增到您的專案:

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

試用

我們的專案現在已成為最低的門檻,無法開始使用。按一下 [連線] 按鈕,提示使用者選取要連線的序列裝置,然後連線至 micro:bit。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 該標籤上會顯示圖示,指出您已連上序列裝置:

設定輸入串流來監聽序列埠的資料

建立連線後,我們必須設定輸入串流和讀取器,以便讀取裝置中的資料。首先,我們會呼叫 port.readable 從通訊埠取得可讀取的串流。由於我們知道要接收裝置傳來的文字,因此會透過文字解碼器來傳送管道資訊。接著,我們會讀取讀取器,並開始讀取迴圈。

將下列程式碼新增到您的專案:

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

讀取迴圈是一種在迴圈中執行的非同步函式,等候內容時不會封鎖主執行緒。新資料抵達時,讀者會傳回兩個屬性:valuedone 布林值。如果值為 done,表示連接埠已關閉,或沒有其他資料。

將下列程式碼新增到您的專案:

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

試用

我們的專案現在可以連線至裝置,並將從裝置接收的任何資料附加到記錄元素中。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 畫面會顯示 Espruino 標誌:

設定輸出串流以將資料傳送至序列埠

序列通訊通常是雙向通訊。除了從序列埠接收資料外,我們也想將資料傳送至通訊埠。和輸入串流一樣,我們只會將輸出串流上的文字傳送至 micro:bit。

首先,請建立文字編碼器串流,並將串流傳送至 port.writeable

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

透過 Espruino 韌體使用序列連線時,BBC micro:bit 板的作用就像 JavaScript 讀取 eval-print 迴圈 (REPL),類似在 Node.js 殼層中取得的資訊。接下來,我們需要提供將資料傳送至串流的方法。以下程式碼會從輸出串流中取得寫入者,並使用 write 傳送每一行。傳送的每一行都包含一個新行字元 (\n),以指示 micro:bit 評估已傳送的指令。

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

如果要讓系統進入已知狀態,並避免系統回電傳送給 Google 的字元,就必須傳送 CTRL-C 並關閉回音。

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

試用

我們的專案現在可以從 micro:bit 傳送和接收資料。請驗證我們能否正確傳送指令:

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 在 Chrome 開發人員工具中開啟 [Console] 分頁標籤,然後輸入
    writeToStream('console.log("yes")');

您應該會在網頁中看到如下內容:

建立矩陣格線字串

如要控制 micro:bit 上的 LED 矩陣,我們必須呼叫 show()。這個方法可在內建 5x5 的 LED 螢幕上顯示圖片。這會使用二進位數字或字串。

我們將反應在核取方塊上,將生成 1s 和 0s 陣列,表示檢查的,哪些是檢查的。接著,我們必須反轉陣列,因為核取方塊的順序與矩陣中的 LED 順序相反。接下來,我們將陣列轉換成字串,並建立指令以傳送至 micro:bit。

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

掛上核取方塊以更新矩陣

接著,我們必須監聽核取方塊的變更;如果有所變更,則會將該資訊傳送到 micro:bit。在功能偵測代碼 (// CODELAB: Add feature detection here.) 中,加入下列這行程式碼:

script.js - DOMContentLoaded

initCheckboxes();

當首次完成 micro:bit 連線時,我們就要重設格線,讓它顯示開心的臉孔。已提供 drawGrid() 函式。此函式的運作方式與 sendGrid() 類似,需要使用 1 秒和 0 秒的陣列,並視情況勾選核取方塊。

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

試用

現在,當網頁開啟與 micro:bit 的連線時,就會傳送一個開心的臉孔。勾選核取方塊即可更新 LED 矩陣螢幕。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應能在 micro:bit LED 矩陣中看到笑臉。
  5. 只要變更核取方塊,即可在 LED 矩陣上畫出不同的圖案。

透過 micro:bit 按鈕新增觀看事件

micro:bit 有兩個按鈕,其中一個是 LED 矩陣的兩側。Espruino 提供了 setWatch 函式,會在按下按鈕時傳送事件/回呼。由於我們想要傾聽這兩個按鈕,因此會讓函式形成通用,並且讓它列印事件的詳細資料。

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

接下來,我們每次連接到串行端口時,我們都需要連接同個按鈕(在 micro:bit 板上命名 BBN1 和 BTN2)。

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

試用

連線時,除了在微時刻顯示時,系統還會顯示幸福臉孔,同時按下 micro:bit 的其中一個按鈕,即會在網頁中新增文字,指出使用者所按下的按鈕。每個字元的一行可能自成一行。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應會在 micro:bit LED 矩陣中看到笑臉。
  5. 按下 micro:bit 上的按鈕,確認按鈕會將新的文字附加至網頁,而且該按鈕會附上按鈕的詳細資訊。

基本串流處理

推送其中一個 micro:bit 按鈕時,micro:bit 會透過串流將資料傳送至序列埠。串流是非常實用的功能,但這些直播也是有挑戰性的,因為您不一定要一次取得所有資料,而且可能會被分割成多個片段。

這個應用程式目前會在收到串流時 (在 readLoop 中) 列印。在多數情況下,每個字元會自成一行,但是這樣就不實用。在理想情況下,串流應剖析成個別行,並將每一封郵件分別顯示為一行。

使用 TransformStream 轉換串流

為了達成此目標,您可以使用轉換串流 (TransformStream),以便剖析傳入的串流並傳回剖析的資料。轉換串流可能位於串流來源 (這裡是指 micro:bit) 與使用串流的任何資源 (此案例為 readLoop) 之間,並且可在最終使用之前套用任意轉換。這就像是組裝的線路,只要把小工具放在一起瀏覽,線條上的每一個步驟就會修改這項小工具,如此一來,直到到達最終目的地為止,這是功能完善的小工具。

詳情請參閱 MDN's Streams API 概念

使用 LineBreakTransformer 轉換串流

讓我們建立 LineBreakTransformer 類別,這個類別會擷取串流,並依據斷行 (\r\n) 加以分類。該類別需要使用兩種方法:transformflush。每次串流收到新資料時,系統就會呼叫 transform 方法。應用程式可將資料排入佇列或儲存以供日後使用。系統會在關閉串流時呼叫 flush 方法,並處理尚未處理的所有資料。

transform 方法中,我們會將新資料新增至 container,然後檢查 container 是否有任何換行符號。如果有,請分割成陣列,然後逐行反覆執行,呼叫 controller.enqueue() 以傳送剖析的行。

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

關閉串流後,我們會使用 enqueue 來清除容器中剩餘的任何資料。

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

最後,我們需要對收到的串流使用新的 LineBreakTransformer 管道。我們的原始輸入串流只透過 TextDecoderStream 傳送,因此必須新增額外的 pipeThrough,才能透過新的 LineBreakTransformer 進行管道。

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

試用

現在,當您按下一個 micro:bit 按鈕時,應在單行中傳回列印的資料。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應能在 micro:bit LED 矩陣中看到笑臉。
  5. 按下 micro:bit 上的按鈕,確認顯示如下的內容:

使用 JSONTransformer 轉換串流

我們會嘗試在 readLoop 中將字串剖析為 JSON,但可改為建立非常簡單的 JSON 轉換器,這會將資料轉換成 JSON 物件。如果資料並非有效的 JSON,只需傳回傳回的資料即可。

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

接著,在通過 LineBreakTransformer 之後,透過 JSONTransformer 來串流處理串流。這可讓我們保持 JSONTransformer 的簡潔,因為我們知道 JSON 只會以單行傳送。

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

試用

現在,當您按下其中一個 micro:bit 按鈕時,應該會看到網頁上印有 [object Object]

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應能在 micro:bit LED 矩陣中看到笑臉。
  5. 按下 micro:bit 上的按鈕,確認顯示如下的內容:

回應按鈕動作

如要回應 micro:bit 按鈕,請更新 readLoop,確認其收到的資料是否為含有 button 屬性的 object。接著呼叫 buttonPushed 來處理按鈕推送。

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

按下 micro:bit 按鈕後,LED 矩陣的顯示幕應會改變。請使用以下程式碼設定矩陣:

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

試用

現在,當您按下其中一個 micro:bit 按鈕時,LED 矩陣應該會變換為笑臉或傷心的臉。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應會在 micro:bit LED 矩陣中看到笑臉。
  5. 按下 micro:bit 上的按鈕,確認 LED 矩陣有所變更。

最後一步是連結使用者中斷連線,以便在使用者完成時關閉連接埠。

使用者按下 [連線/中斷連線] 按鈕時關閉通訊埠

使用者按一下 [連線]/[中斷連線] 按鈕時,我們必須關閉連結。如果連接埠已開啟,請呼叫 disconnect() 並更新 UI,表示該網頁已不再與序列裝置連線。

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

關閉串流和通訊埠

disconnect 函式中,我們需要關閉輸入串流、關閉輸出串流並關閉通訊埠。如要關閉輸入串流,請呼叫 reader.cancel()。對 cancel 的呼叫是非同步的,因此我們必須使用 await 來等待它完成:

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone;
  reader = null;
  inputDone = null;
}

如要關閉輸出串流,請取得 writer、呼叫 close() 並等待 outputDone 物件關閉:

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

最後,關閉序列埠並等待關閉:

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

試用

現在,您隨時可以開啟及關閉序列埠。

  1. 重新載入網頁。
  2. 按一下 [連線] 按鈕。
  3. 在「序列埠選擇工具」對話方塊中,選取 BBC micro:bit 裝置,然後按一下 [連線]
  4. 您應該會看到在 micro:bit LED 矩陣上
  5. 按下 [中斷連線] 按鈕,確認 LED 矩陣已關閉,且控制台中沒有任何錯誤。

恭喜!您已成功建立使用 Web Serial API 的第一個網頁應用程式。

密切注意 https://goo.gle/fugu-api-tracker,以便取得 Web Serial API 的最新消息,以及 Chrome 小組目前開發的其他所有令人期待的全新網路功能。