직렬 포트 읽기 및 쓰기

Web Serial API를 사용하면 웹사이트에서 직렬 기기와 통신할 수 있습니다.

François Beaufort
François Beaufort

Web Serial API란 무엇인가요?

직렬 포트는 포트 80 또는 240에 데이터를 송수신하는 데 사용할 수 있는 데이터를 바이트 단위로 받게 됩니다.

Web Serial API는 웹사이트에서 직렬 장치를 사용합니다. 직렬 장치는 사용자 시스템의 직렬 포트 또는 이동식 USB 및 블루투스 장치를 통해 직렬 포트를 에뮬레이트하는 것입니다.

즉, Web Serial API는 웹사이트가 마이크로컨트롤러와 같은 직렬 장치와 통신할 수 있도록 함 사용할 수 있습니다.

또한 운영체제에서 필요로 하므로 이 API는 WebUSB와 함께 사용하면 매우 유용합니다. 일부 직렬 포트와 통신하도록 애플리케이션이 더 높은 수준의 직렬 API를 사용하는 것입니다.

추천 사용 사례

교육, 취미 및 산업 부문의 사용자는 액세스할 수 있습니다. 이러한 장치는 종종 마이크로컨트롤러를 배급할 수 있습니다. 일부 맞춤 이러한 장치를 제어하는 소프트웨어는 다음과 같은 웹 기술로 개발되었습니다.

웹사이트에서 에이전트를 통해 기기와 통신하는 경우도 있습니다. 사용자가 수동으로 설치한 애플리케이션입니다. 그 외의 경우에는 애플리케이션이 패키징된 애플리케이션으로 제공됩니다. 사용자가 추가 단계를 수행해야 하는 경우도 있습니다. 컴파일된 애플리케이션을 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 공급업체(usbVendorId) 및 USB 제품(선택사항)이 있는 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();
드림 <ph type="x-smartling-placeholder">
</ph> 웹사이트에 표시된 직렬 포트 프롬프트의 스크린샷
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: 만들어야 하는 읽기 및 쓰기 버퍼의 크기입니다. 16MB 미만이어야 합니다.
  • flowControl: 흐름 제어 모드입니다 ("none" 또는 "hardware").

직렬 포트에서 읽기

Web Serial API의 입력 및 출력 스트림은 Streams API에서 처리됩니다.

직렬 포트 연결이 설정되면 readablewritableSerialPort 객체의 속성은 ReadableStream을 반환하고 WritableStream을 참조하십시오. 이 주소들은 직렬 장치입니다. 둘 다 데이터 전송에 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가 다음과 같이 됩니다. null입니다.

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

'Bring Your Own Buffer'를 사용하여 스트림에서 읽을 때 메모리가 할당되는 방식을 제어할 수 있습니다. 사용할 수 있습니다 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.writable로 파이핑된 TextEncoderStream를 통해 기기에 문자 메시지 전송 변경할 수 있습니다.

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

직렬 포트 닫기

port.close()readablewritable 구성원인 경우 직렬 포트를 닫습니다. 잠금 해제되어 있으며 이는 각각에 대해 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()를 호출합니다. 이렇게 하면 기본 직렬 포트로 전달됩니다 오류 전파로 인해 실행되지 않으므로 readableStreamClosedport.readable 시점을 감지하기 위해 이전에 생성된 프로미스 writableStreamClosed개 및 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.
});
드림

신호 처리

직렬 포트 연결을 설정한 후에는 IP 데이터 세트를 명시적으로 쿼리하고 장치 감지 및 흐름 제어를 위해 직렬 포트에서 노출되는 신호. 이러한 신호는 불리언 값으로 정의됩니다 예를 들어, 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하거나 다음 작업을 수행할 수 있는 자체 변환 스트림을 만듭니다. 수신 스트림을 파싱하고 파싱된 데이터를 반환합니다. 변환 스트림은 읽기 루프와 통신해야 합니다. 가능 데이터를 소비하기 전에 임의의 변환을 적용합니다. Kubernetes를 조립 라인: 위젯이 줄 아래로 내려오면 줄의 각 단계가 따라서 최종 목적지에 도달할 때쯤에는 완전히 새로운 있습니다.

<ph type="x-smartling-placeholder">
</ph> 항공기 공장 사진
2차 세계 대전 캐슬 브롬위치 비행기 공장

예를 들어, 입력 시퀀스를 소비하는 변환 스트림 클래스를 줄바꿈에 따라 청크로 분할합니다. transform() 메서드가 호출됩니다. 스트림에서 새 데이터를 수신할 때마다 데이터를 큐에 추가하거나 저장합니다. flush() 메서드는 스트림이 닫힐 때 호출됩니다. 아직 처리되지 않은 모든 데이터를 처리합니다

변환 스트림 클래스를 사용하려면 들어오는 스트림을 파이핑해야 합니다. 있습니다. 직렬 포트에서 읽기의 세 번째 코드 예에서는 원래 입력 스트림은 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: 여기에서 모든 직렬 기기 관련 이벤트를 한곳에서 볼 수 있습니다. 한곳에 모일 수 있습니다.

<ph type="x-smartling-placeholder">
</ph> Web Serial API 디버깅을 위한 내부 페이지의 스크린샷
Web Serial API 디버깅을 위한 Chrome의 내부 페이지

Codelab

Google 개발자 Codelab에서는 Web Serial API를 사용하여 BBC micro:bit 보드를 사용하여 5x5 LED 매트릭스에 이미지를 표시합니다.

브라우저 지원

Web Serial API는 모든 데스크톱 플랫폼 (ChromeOS, Linux, macOS, 및 Windows)에서 지원됩니다.

폴리필

Android에서 WebUSB API를 사용하여 USB 기반 직렬 포트 지원 가능 및 Serial API 폴리필입니다. 이 폴리필은 하드웨어 및 웹 USB API를 통해 기기에 액세스할 수 있는 내장 기기 드라이버에 의해 소유권이 주장되었습니다.

보안 및 개인 정보 보호

사양 작성자는 웹 시리얼을 위한 핵심 API를 사용하여 Web Serial API를 강력한 웹 플랫폼 기능에 대한 액세스 제어에 정의된 원칙 사용자 제어, 투명성, 인체공학을 포함합니다. 이 기능을 사용해 API는 기본적으로 단일 항목에만 액세스 권한을 부여하는 권한 모델로 관리됩니다. 직렬 장치를 지원합니다 사용자 프롬프트에 응답하여 사용자는 활성 특정 직렬 장치를 선택하는 단계를 수행합니다.

보안의 장단점을 알아보려면 보안개인 정보 보호를 확인하세요. 몇 가지 섹션을 살펴보겠습니다.

의견

Chrome팀은 Chrome OS에 대한 여러분의 의견과 경험을 Web Serial API

API 설계에 대해 알려주세요.

API에서 예상대로 작동하지 않는 부분이 있나요? 아니면 Google에서 아이디어를 구현하는 데 필요한 메서드나 속성이 누락되었나요?

Web Serial API GitHub 저장소에서 사양 문제를 신고하거나 다음을 추가합니다. 기존 문제에 대한 생각을 할 수 있습니다

구현 문제 신고

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 어떻게 해야 할까요?

https://new.crbug.com에서 버그를 신고합니다. 최대한 많은 버그 재현을 위한 간단한 안내를 제공하고, 구성요소Blink>Serial로 설정됨 Glitch는 쉽고 빠르게 재현할 수 있습니다.

지지 표시

Web Serial API를 사용할 계획이신가요? 여러분의 공개적 후원은 Chrome 팀은 기능의 우선순위를 정하고 다른 브라우저 공급업체에 도움이 될 수 있습니다

해시태그를 사용하여 @ChromiumDev에 트윗을 보냅니다. #SerialAPI 어디서 어떻게 사용하는지 Google에 알려주세요.

유용한 링크

데모

감사의 말씀

이 기사를 검토해 주신 라일리 그랜트조 메들리님께 감사드립니다. UnsplashBirmingham Museums Trust에서 제공한 항공기 공장 사진