Web Serial API 시작하기

1. 소개

최종 업데이트: 2022년 9월 19일

빌드할 항목

이 Codelab에서는 Web Serial API를 사용하여 BBC micro:bit 보드와 상호작용하여 5x5 LED 매트릭스에 이미지를 표시하는 웹페이지를 빌드합니다. Web Serial API와 읽기, 쓰기, 변환 스트림을 사용하여 브라우저를 통해 직렬 기기와 통신하는 방법을 알아봅니다.

67543f4caaaca5de.png

학습할 내용

  • 웹 직렬 포트를 열고 닫는 방법
  • 읽기 루프를 사용하여 입력 스트림의 데이터를 처리하는 방법
  • 쓰기 스트림을 통해 데이터를 전송하는 방법

필요한 항목

이 Codelab에서는 micro:bit v1을 사용했습니다. 가격이 저렴하고 몇 가지 입력(버튼)과 출력(5x5 LED 디스플레이)을 제공하며 추가 입력과 출력을 제공할 수 있기 때문입니다. micro:bit의 기능에 대한 자세한 내용은 Espruino 사이트의 BBC 마이크로:비트 페이지를 참조하세요.

2. Web Serial API 정보

Web Serial API는 웹사이트에서 스크립트를 사용하여 직렬 기기에서 읽고 쓰는 방법을 제공합니다. 이 API는 웹사이트가 마이크로컨트롤러 및 3D 프린터와 같은 직렬 기기와 통신할 수 있도록 허용하여 웹과 실제 세계를 연결합니다.

웹 기술을 사용하여 빌드된 제어 소프트웨어의 예는 많습니다. 예를 들면 다음과 같습니다.

경우에 따라 이러한 웹사이트는 사용자가 수동으로 설치한 네이티브 에이전트 애플리케이션을 통해 기기와 통신합니다. 다른 경우에는 Electron과 같은 프레임워크를 통해 패키징된 네이티브 애플리케이션으로 애플리케이션이 제공됩니다. 그 밖의 경우에는 사용자가 컴파일된 애플리케이션을 USB 플래시 드라이브 기기로 복사하는 등의 추가 단계를 수행해야 합니다.

사이트와 제어 중인 기기 간에 직접 통신을 제공하여 사용자 환경을 개선할 수 있습니다.

3. 설정

코드 가져오기

이 Codelab에 필요한 모든 것을 Glitch 프로젝트에 넣었습니다.

  1. 새 브라우저 탭을 열고 https://web-serial-codelab-start.glitch.me/로 이동합니다.
  2. Remix Glitch 링크를 클릭하여 나만의 시작 프로젝트 버전을 만듭니다.
  3. 표시 버튼을 클릭한 다음 새 창을 선택하여 코드가 실행되는 모습을 확인합니다.

4. 직렬 연결 열기

Web Serial API 지원 여부 확인

가장 먼저 할 일은 현재 브라우저에서 Web Serial API가 지원되는지 확인하는 것입니다. 이렇게 하려면 serialnavigator에 있는지 확인하세요.

DOMContentLoaded 이벤트에서 프로젝트에 다음 코드를 추가합니다.

script.js - DOMContentLoaded

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

Web Serial이 지원되는지 확인합니다. 지원되는 경우 이 코드는 웹 일련번호가 지원되지 않는다는 내용의 배너를 숨깁니다.

직접 해 보기

  1. 페이지를 로드합니다.
  2. 페이지에 웹 일련번호가 지원되지 않는다는 빨간색 배너가 표시되지 않는지 확인합니다.

직렬 포트 열기

다음으로 직렬 포트를 열어야 합니다. 다른 대부분의 최신 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();

직접 해 보기

이제 프로젝트에 시작하는 데 필요한 최소한의 요소가 있습니다. Connect(연결) 버튼을 클릭하면 연결할 직렬 기기를 선택하라는 메시지가 표시된 후 micro:bit에 연결됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  4. 탭에 직렬 기기에 연결되었음을 나타내는 아이콘이 표시됩니다.

e695daf2277cd3a2.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. 연결 버튼을 클릭합니다.
  3. Serial Port chooser(직렬 포트 선택 도구) 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. Espruino 로고가 표시됩니다.

dd52b5c37fc4b393.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 micro:bit 보드는 Node.js 셸에서 제공하는 것과 유사한 JavaScript REPL (read-eval-print 루프) 역할을 합니다. 다음으로, 스트림으로 데이터를 전송하는 메서드를 제공해야 합니다. 아래 코드는 출력 스트림에서 작성기를 가져온 후 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. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  4. Chrome DevTools에서 콘솔 탭을 열고 writeToStream('console.log("yes")');을 입력합니다.

페이지에 다음과 같이 출력됩니다.

15e2df0064b5de28.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. 연결 버튼을 클릭합니다.
  3. Serial Port chooser(직렬 포트 선택 도구) 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. 마이크로:비트 LED 매트릭스에 미소가 표시됩니다.
  5. 체크박스를 변경하여 LED 매트릭스에 다른 패턴을 그립니다.

6. 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 및 BTN2)을 모두 연결해야 합니다.

script.js - clickConnect()

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

직접 해 보기

연결되었을 때 행복한 얼굴이 표시되는 것 외에도 micro:bit의 버튼 중 하나를 누르면 어떤 버튼을 눌렀는지 나타내는 텍스트가 페이지에 추가됩니다. 대부분의 경우 각 문자가 한 줄에 하나씩 표시됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  4. micro:bits LED 매트릭스에 미소가 표시됩니다.
  5. micro:bit에서 버튼을 누르고, 누른 버튼의 세부정보가 있는 페이지에 새 텍스트가 추가되는지 확인합니다.

7. 변환 스트림을 사용하여 수신 데이터 파싱

기본 스트림 처리

micro:bit 버튼 중 하나를 누르면 micro:bit이 스트림을 통해 직렬 포트로 데이터를 전송합니다. 스트림은 매우 유용하지만 동시에 모든 데이터를 가져올 수 없고 임의로 청크로 분할될 수 있기 때문에 문제가 될 수 있습니다.

현재 앱은 수신되는 스트림이 도착할 때마다 readLoop에 출력합니다. 대부분의 경우 각 문자는 자체 줄에 있지만 그다지 유용하지는 않습니다. 스트림을 개별 행으로 파싱하고 각 메시지를 자체 행으로 표시하는 것이 이상적입니다.

TransformStream로 스트림 변환

이를 위해 변환 스트림(TransformStream)을 사용하면 됩니다. 그러면 수신 스트림을 파싱하고 파싱된 데이터를 반환할 수 있습니다. 변환 스트림은 스트림 소스(이 경우 micro:bit)와 스트림을 소비하는 항목(이 경우 readLoop) 사이에 있을 수 있으며, 최종적으로 소비되기 전에 임의의 변환을 적용할 수 있습니다. 조립 라인과 같이 생각해 보세요. 위젯이 라인을 따라 내려가면서 라인의 각 단계에서 위젯이 수정되므로 최종 목적지에 도착할 때는 완전히 작동하는 위젯이 됩니다.

자세한 내용은 MDN의 Streams API 개념을 참조하세요.

LineBreakTransformer를 사용하여 스트림 변환

스트림을 가져와서 줄바꿈(\r\n)을 기준으로 청크로 분할하는 LineBreakTransformer 클래스를 만들어 보겠습니다. 이 클래스에는 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를 통해서만 파이프되었으므로 새 LineBreakTransformer를 통해 파이프하려면 pipeThrough를 추가해야 합니다.

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. Serial Port chooser(직렬 포트 선택 도구) 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. 마이크로:비트 LED 매트릭스에 미소가 표시됩니다.
  5. micro:bit의 버튼을 누르고 다음과 같이 표시되는지 확인합니다.

eead3553d29ee581.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. 연결 버튼을 클릭합니다.
  3. Serial Port chooser(직렬 포트 선택 도구) 대화상자에서 BBC micro:bit 기기를 선택하고 Connect(연결)를 클릭합니다.
  4. 마이크로:비트 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();
  }
}

직접 해 보기

이제 마이크로비트 버튼 중 하나를 누르면 LED 매트릭스가 행복한 얼굴 또는 슬픈 얼굴로 변경됩니다.

  1. 페이지를 새로고침합니다.
  2. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  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. 연결 버튼을 클릭합니다.
  3. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  4. micro:bit LED 매트릭스에 미소가 표시됩니다.
  5. 연결 해제 버튼을 누르고 LED 매트릭스가 꺼지고 콘솔에 오류가 없는지 확인합니다.

9. 축하합니다

축하합니다. Web Serial API를 사용하는 첫 번째 웹 앱을 빌드했습니다.

https://goo.gle/fugu-api-tracker에서 Web Serial API 및 Chrome팀에서 개발 중인 기타 흥미로운 새로운 웹 기능에 대한 최신 소식을 확인하세요.