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. Show(표시) 버튼을 클릭한 다음 In a New Window(새 창에서 열기)를 선택하여 코드의 작동 방식을 확인합니다.

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

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

직접 해 보기

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

직접 해 보기

이제 프로젝트를 시작하기 위한 최소한의 기준이 준비되었습니다. 연결 버튼을 클릭하면 연결할 직렬 기기를 선택하라는 메시지가 표시된 후 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. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  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. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  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. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  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. 직렬 포트 선택기 대화상자에서 BBC micro:bit 기기를 선택하고 연결을 클릭합니다.
  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';
}

마이크로비트 버튼을 누르면 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. 마이크로:비트 LED 매트릭스에 미소가 표시됩니다.
  5. 연결 해제 버튼을 누르고 LED 매트릭스가 꺼지고 콘솔에 오류가 없는지 확인합니다.

9. 축하합니다

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

https://goo.gle/fugu-api-tracker에서 Web Serial API에 관한 최신 소식과 Chrome팀에서 개발 중인 새롭고 흥미로운 웹 기능에 관한 최신 정보를 확인해 보세요.