開始使用 Web Serial API

1. 簡介

上次更新時間:2020 年 7 月 21 日

建構項目

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

81167ab7c01d353d.png

課程內容

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

軟硬體需求

我們選擇使用 micro:bit 進行本程式碼研究室,原因是這個程式庫價格實惠、提供了一些輸入 (按鈕) 和輸出內容 (5x5 LED 顯示螢幕),並提供額外的輸入和輸出內容。請參閱 Espruino 網站的 BBC micro:bit 頁面,進一步瞭解 micro:bit 的功能。

2. 關於 Web Serial API

Web Serial API 可讓網站使用指令碼讀取序列裝置並寫入資料。這個 API 可讓網站與微控制器和 3D 印表機等序列裝置進行通訊,藉此連結網路與實體世界。

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

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

使用者可以與受控制的裝置直接通訊,提升使用者體驗。

3. 開始設定

取得程式碼

我們已將本程式碼研究室所需的各項資源,都放在 Glitch 專案中。

  1. 開啟新的瀏覽器分頁,然後前往 https://web-serial-codelab-start.glitch.me/
  2. 按一下「Remix Glitch」連結,即可自行建立範例專案。
  3. 按一下「顯示」按鈕,然後選擇「在新視窗中」,即可查看程式碼的實際運作情形。

4. 開啟序列連線

確認系統是否支援 Web Serial API

首先,請檢查目前的瀏覽器是否支援 Web Serial API。如要執行此操作,請確認 serial 是否位於 navigator

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

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
const notSupported = document.getElementById('notSupported');
notSupported.classList.toggle('hidden', 'serial' in navigator);

這項操作會檢查是否支援 Web Serial。如果是,這個程式碼會隱藏橫幅,指出不支援 Web Serial。

試用

  1. 載入網頁。
  2. 確認網頁未顯示不支援 Web Serial 的紅色橫幅。

開啟序列埠

接著,請開啟序列埠。如同大多數其他新式 API,Web Serial API 也不例外。這可避免 UI 在等待輸入時封鎖,但同樣重要,因為網頁隨時可能會接收序列資料,因此我們需要一個監聽的方法。

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

將下列程式碼加入專案:

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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 分頁標籤上應會顯示圖示,表示已連線至序列裝置:

d9d0d3966960aeab.png

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

建立連線後,我們還需要設定輸入串流和讀取器,才能從裝置讀取資料。首先,透過呼叫 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 為 true,代表通訊埠已關閉,或沒有其他資料。

將下列程式碼加入專案:

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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會看到 Espruino 標誌:

93494fd58ea835eb.png

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

序列通訊通常為雙向通訊。除了從序列埠接收資料之外,我們也想將資料傳送至通訊埠。與輸入串流一樣,我們只會透過輸出串流將文字傳送至 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 微:位元面板會成為 JavaScript read-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();

如要讓系統進入已知狀態,避免系統回應我們傳送的字元,我們需傳送 CTRL-C 並關閉回音。

script.js - connect()

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

試用

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

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

頁面中應會顯示類似下方的內容:

a13187e7e6260f7f.png

5. 控制 LED 矩陣

建構矩陣格線字串

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

我們會反覆檢查核取方塊,並產生 1 和 0 陣列,指出已勾選或不會勾選的項目。接著,我們必須反轉陣列,因為核取方塊的順序與矩陣中 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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會在 micro:bit LED 矩陣中看到一個笑臉。
  5. 變更核取方塊,在 LED 矩陣上畫出不同的圖案。

6. 連接 micro:bit 按鈕

在 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);

接下來,每次在序列埠連接到裝置時,我們需要將兩個按鈕 (名為 BTN1 和 micro:bit 板上的 BTN2) 連接在一起。

script.js - clickConnect()

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

試用

除了在連線時顯示笑臉外,只要按下 micro:bit 上的其中一個按鈕,就會在頁面中加入說明已按下哪個按鈕的文字。很有可能每個角色各佔一行。

  1. 重新載入網頁。
  2. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會在 micro:bits LED 矩陣中看到一個笑臉。
  5. 按下 micro:bit 上的按鈕,確認它附加了新的文字到已按下按鈕的詳細資訊。

7. 使用轉換串流剖析傳入資料

基本串流處理

其中一個 micro:bit 按鈕推送時,micro:bit 會透過串流將資料傳送到序列埠。串流相當實用,但也很困難,因為您不一定一次取得所有資料,而且可能會任意分割。

應用程式目前會在收到串流時列印傳入串流 (在 readLoop 中)。在大多數情況下,每個字元都是獨立一行,但這樣沒有很實用。在理想情況下,串流應剖析為每一行,且每則訊息會分別以不同的行顯示。

使用 TransformStream 轉換串流

為此,我們可以使用轉換串流 ( TransformStream),以剖析傳入串流並傳回剖析的資料。轉換串流可位於串流來源 (在本例中為 micro:bit) 之間,以及正在使用串流的任何項目 (在本例中為 readLoop),並可在最終使用前套用任意轉換。這就像是組裝行:隨著小工具沿著線條移動,線條中的每個步驟都會修改小工具,因此當小工具到達最終目的地時,這是功能完善的小工具。

詳情請參閱 MDN 的 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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會在 micro:bit LED 矩陣中看到一個笑臉。
  5. 按下 micro:bit 上的按鈕,確認系統顯示出如下所示:

6c2193880c748412.png

使用 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 將串流傳輸到該資料。我們知道 JSON 只會以單行傳送,因此能簡化 JSONTransformer

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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會在 micro:bits LED 矩陣中看到一個笑臉。
  5. 按下 micro:bit 上的按鈕,並確認 LED 矩陣有變。

8. 關閉序列埠

最後一步是連接中斷連線的功能,在使用者執行完畢後關閉通訊埠。

在使用者點選「連線/中斷連線」按鈕時關閉通訊埠

當使用者按一下「連線」/「中斷連線」按鈕時,我們必須關閉連線。如果通訊埠已開啟,請呼叫 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.catch(() => {});
  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. 按一下「Connect」按鈕。
  3. 在「序列埠選擇器」對話方塊中,選取 BBC micro:bit 裝置,然後按一下「Connect」
  4. 您應該會在 micro:bit LED 矩陣上看到一個笑臉
  5. 按下「中斷連線」按鈕,並確認 LED 矩陣已關閉,且控制台中沒有任何錯誤。

9. 恭喜

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

請留意 https://goo.gle/fugu-api-tracker,瞭解 Web Serial API 的最新消息,以及 Chrome 團隊目前正在開發的其他令人期待的新網路功能。