WebSerial

현행 표준 — 마지막 업데이트

참여:
GitHub whatwg/serial (새 이슈, 열린 이슈)
Matrix에서 대화하기
커밋:
GitHub whatwg/serial/commits
이 커밋 시점의 스냅샷
테스트:
web-platform-tests serial/ (진행 중인 작업)
번역 (비규범적):
日本語
简体中文
한국어

초록

WebSerial API는 웹사이트에서 읽고 쓰는 방법을 제공하며, 스크립트를 통해 직렬 장치에서 이를 수행합니다. 이러한 API는 웹과 물리적 세계를 연결하며, 문서가 장치와 통신할 수 있도록 합니다. 마이크로컨트롤러, 3D 프린터 및 기타 직렬 장치 등이 이에 해당합니다. 또한 함께 제공되는 해설 문서가 있습니다.

1. 확장 사항: Navigator 인터페이스

[Exposed=Window, SecureContext]
partial interface Navigator {
  [SameObject] readonly attribute Serial serial;
};

1.1. serial 속성

값을 가져올 때, serial 속성이 항상 반환하는 것은 동일한 인스턴스이며, 이는 Serial 객체의 인스턴스입니다.

2. 확장 사항: WorkerNavigator 인터페이스

[Exposed=DedicatedWorker, SecureContext]
partial interface WorkerNavigator {
  [SameObject] readonly attribute Serial serial;
};

2.1. serial 속성

값을 가져올 때, serial 속성이 항상 반환하는 것은 동일한 인스턴스이며, 이는 Serial 객체의 인스턴스입니다.

3. Serial 인터페이스

[Exposed=(DedicatedWorker, Window), SecureContext]
interface Serial : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  Promise<sequence<SerialPort>> getPorts();
  [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
};

3.1. requestPort() 메서드

사용자가 사이트를 처음 방문하면 사이트에는 어떠한 장치에도 접근할 권한이 없습니다. 직렬 장치에 접근하려면 사이트는 먼저 다음을 호출해야 합니다: requestPort(). 이 호출은 브라우저가 사이트에서 제어할 장치를 사용자에게 물을 기회를 제공합니다. 사이트가 특정 장치와 함께 작동하도록 설계되었으며, 그 장치가 항상 USB를 통해 연결된다면 사이트는 사용자가 선택할 수 있는 장치를 제한하는 필터를 제공하여, 호환되는 장치만 선택하게 할 수 있습니다. 예를 들어 Arduino 기반 로봇을 프로그래밍하는 사이트는 다음과 같이 지정하여 선택 가능한 포트 집합을 USB 장치로만 제한할 수 있습니다. 이 장치는 Arduino의 USB 공급업체 ID를 가진 장치입니다.
const filter = { usbVendorId: 0x2341 };
const port = await navigator.serial.requestPort({ filters: [filter] });

반면 사이트가 매우 다양한 종류의 장치나 USB-직렬 변환기를 통해 연결된 장치와 함께 사용될 것으로 예상한다면, 필터를 전혀 지정하지 않고 사용자가 적절한 장치를 선택하도록 할 수 있습니다.

const port = await navigator.serial.requestPort();

사용자에게 포트 선택을 요청하려면 사용자에게 프롬프트를 표시해야 하므로, 사이트에는 일시적 활성화가 있어야 하며, 이는 사용자가 버튼을 클릭하는 등의 동작에서 비롯됩니다.

<button id="connect">연결</button>
const connectButton = document.getElementById("connect");
connectButton.addEventListener('click', () => {
  try {
    const port = await navigator.serial.requestPort();
    // |port|에 연결된 장치에 대한 연결을 계속 진행합니다.
  } catch (e) {
    // 장치를 선택하지 않고 프롬프트를 닫았습니다.
  }
});

사용자는 장치를 선택하지 않을 수 있으며, 이 경우 Promise 는 다음으로 거부됩니다: "NotFoundError" DOMException 이는 사이트에서 반드시 처리해야 합니다.

이 requestPort() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this의 관련 전역 객체의 연관된 Document가 사용하도록 허용되지 않은 정책으로 제어되는 기능의 이름이 "serial"이면, 거부합니다: promise를 다음으로: "SecurityError" DOMException 그리고 promise를 반환합니다.

  3. 만약 관련 전역 객체, 즉 this의 해당 객체에 일시적 활성화가 없으면, 거부합니다: promise를 다음으로: "SecurityError" DOMException 그리고 promise를 반환합니다.

  4. 만약 options["filters"] 가 있으면, 각 filter에 대해, 그 대상은 options["filters"] 에 속하는 항목이며, 다음 단계를 실행합니다:

    1. 만약 filter["bluetoothServiceClassId"] 가 있으면:

      1. 만약 filter["usbVendorId"] 가 있으면, 거부합니다: promise를 TypeError로 그리고 promise를 반환합니다.

      2. 만약 filter["usbProductId"] 가 있으면, 거부합니다: promise를 TypeError로 그리고 promise를 반환합니다.

    2. 만약 filter["usbVendorId"] 가 없으면, 거부합니다: promise를 TypeError로 그리고 promise를 반환합니다.

      참고: 이 검사는 다음의 결합된 규칙을 구현합니다: SerialPortFilter 는 비어 있을 수 없으며, 만약 usbProductId 가 지정되었다면 usbVendorId 도 반드시 함께 지정해야 합니다.

  5. 다음 단계를 실행합니다. 실행 방식은 병렬입니다:

    1. allPorts를 빈 목록으로 둡니다.

    2. 각각에 대해, 그 대상은 시스템에 등록된 Bluetooth 장치입니다:

      1. 각각에 대해, 그 대상은 BluetoothServiceUUID uuid이며, 이를 지원하는 것은 해당 장치입니다:

        1. 만약 uuid가 차단된 Bluetooth 서비스 클래스 UUID가 아니면:

    3. 각각에 대해, 그 대상은 사용 가능한 비 Bluetooth 직렬 포트입니다:

      1. port를 다음으로 둡니다: SerialPort 이는 해당 포트를 나타냅니다.

      2. 추가합니다: port를 allPorts에.

    4. 사용자에게 포트 목록을 표시하여 사이트의 직렬 포트 접근을 허용하도록 요청합니다. 표시되는 것은 allPorts에 있는 포트 중 하나 이상의 필터와 일치하는 포트이며, 필터는 options["filters"] 가 있으면 해당 값에 있는 필터이고, 없으면 allPorts를 표시합니다.

    5. 사용자가 포트를 선택하지 않으면, 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "NotFoundError" DOMException 그리고 이 단계들을 중단합니다.

    6. port를 다음으로 둡니다: SerialPort 이는 사용자가 선택한 포트를 나타냅니다.

    7. 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 port로.

  6. promise를 반환합니다.

직렬 포트가 사용 가능한 조건은, 유선 직렬 포트이면서 해당 포트가 시스템에 물리적으로 연결되어 있거나, 무선 직렬 포트이면서 해당 포트를 제공하는 무선 장치가 시스템에 등록되어 있는 경우입니다.

3.1.1. SerialPortRequestOptions 사전

dictionary SerialPortRequestOptions {
  sequence<SerialPortFilter> filters;
  sequence<BluetoothServiceUUID> allowedBluetoothServiceClassIds;
};
filters 멤버, 다음 타입 sequence<SerialPortFilter>
직렬 포트에 대한 필터
allowedBluetoothServiceClassIds 멤버, 타입 sequence<BluetoothServiceUUID>
다음의 목록입니다: BluetoothServiceUUID 값으로, Bluetooth 서비스 클래스 ID를 나타냅니다. 사용자 정의 서비스 클래스 ID를 가진 Bluetooth 포트는 사용자에게 표시되는 포트 목록에서 제외됩니다. 단, 서비스 클래스 ID가 이 목록에 포함되어 있으면 제외되지 않습니다.

3.1.2. SerialPortFilter 사전

dictionary SerialPortFilter {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};
usbVendorId 멤버, 다음 타입 unsigned short
USB 공급업체 ID
usbProductId 멤버, 다음 타입 unsigned short
USB 제품 ID
bluetoothServiceClassId 멤버, 타입 BluetoothServiceUUID
Bluetooth 서비스 클래스 ID
직렬 포트 port가 필터와 일치한다는 것은 filter에 대해 다음 단계가 true를 반환한다는 뜻입니다:
  1. info를 다음 호출의 결과로 둡니다: port.getInfo().

  2. 만약 filter["bluetoothServiceClassId"] 가 있으면:

    1. 직렬 포트가 Bluetooth 장치의 일부가 아니면, false를 반환합니다.

    2. 만약 filter["bluetoothServiceClassId"] 가 다음과 같으면: info["bluetoothServiceClassId"], true를 반환합니다.

    3. 그렇지 않으면, false를 반환합니다.

  3. 만약 filter["usbVendorId"] 가 없으면, 다음을 반환합니다: true.

  4. 직렬 포트가 USB 장치의 일부가 아니면, false를 반환합니다.

  5. 만약 info["usbVendorId"] 가 다음과 같지 않으면: filter["usbVendorId"], false를 반환합니다.

  6. 만약 filter["usbProductId"] 가 없으면, 다음을 반환합니다: true.

  7. 만약 info["usbProductId"] 가 다음과 같지 않으면: filter["usbProductId"], false를 반환합니다.

  8. 그렇지 않으면, true를 반환합니다.

직렬 포트 port가 하나 이상의 필터와 일치한다는 것은, 해당 시퀀스가 SerialPortFilter의 시퀀스이며, 다음 단계가 true를 반환한다는 뜻입니다:
  1. 시퀀스의 각 filter에 대해, 다음 하위 단계를 실행합니다:

    1. 만약 port가 필터와 일치하며 그 필터가 filter이면, 다음을 반환합니다: true.

  2. false를 반환합니다.

3.2. getPorts() 메서드

USB 장치가 직렬 포트를 제공한다면 해당 장치는 시스템에 연결되거나 시스템에서 분리될 수 있습니다. 사이트가 포트에 접근할 권한을 얻으면, 이러한 이벤트를 수신하고 연결된 장치 집합을 조회할 수 있으며, 이는 현재 접근할 수 있는 장치들입니다.
// 페이지가 로드될 때 사용 가능한 포트를 확인합니다.
document.addEventListener('DOMContentLoaded', async () => {
  let ports = await navigator.serial.getPorts();
  // 사용자가 선택할 수 있는 옵션으로 UI를 채우거나
  // 장치에 자동으로 연결합니다.
});

navigator.serial.addEventListener('connect', e => {
  // |e.target|을 UI에 추가하거나 자동으로 연결합니다.
});

navigator.serial.addEventListener('disconnect', e => {
  // |e.target|을 UI에서 제거합니다. 장치가 열려 있었다면
  // 연결 해제를 스트림 오류로도 관찰할 수 있습니다.
});
이 getPorts() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this의 관련 전역 객체의 연관된 Document가 사용하도록 허용되지 않은 정책으로 제어되는 기능의 이름이 "serial"이면, 거부합니다: promise를 다음으로: "SecurityError" DOMException 그리고 다음을 반환합니다: promise.

  3. 다음 단계를 실행합니다. 실행 방식은 병렬입니다:

    1. availablePorts를 다음의 시퀀스로 둡니다: 사용 가능한 직렬 포트 중에서, 이전 호출의 결과로 사용자가 사이트에 접근을 허용한 포트이며, 호출 대상은 requestPort()입니다.

    2. ports를 다음의 시퀀스로 둡니다: SerialPort 객체들로, 이들이 나타내는 것은 availablePorts에 있는 포트입니다.

    3. 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 ports로.

  4. promise를 반환합니다.

3.3. onconnect 속성

onconnect 는 이벤트 처리기 IDL 속성이며, connect 이벤트 유형에 해당합니다.

3.4. ondisconnect 속성

ondisconnect 는 이벤트 처리기 IDL 속성이며, disconnect 이벤트 유형에 해당합니다.

4. SerialPort 인터페이스

[Exposed=(DedicatedWorker,Window), SecureContext]
interface SerialPort : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  readonly attribute boolean connected;
  readonly attribute ReadableStream? readable;
  readonly attribute WritableStream? writable;

  SerialPortInfo getInfo();

  Promise<undefined> open(SerialOptions options);
  Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
  Promise<SerialInputSignals> getSignals();
  Promise<undefined> close();
  Promise<undefined> forget();
};

이 인터페이스의 메서드는 일반적으로 비동기적으로 완료되며, 작업을 다음에 큐잉합니다: 직렬 포트 작업 소스.

이 부모 가져오기 알고리즘의 대상인 SerialPort 에 대해서는 동일한 Serial 인스턴스를 반환하며, 그 인스턴스를 반환하는 것은 SerialPort의 관련 전역 객체의 Navigator 객체의 serial 게터입니다.

인스턴스의 대상인 SerialPort 는 내부 슬롯을 가진 인스턴스로 생성되며, 해당 슬롯은 다음 표에 설명되어 있습니다:

내부 슬롯 초기값 설명 (비규범적)
[[state]] "closed" 활성 상태를 추적하며, 그 대상은 SerialPort
[[bufferSize]] undefined 송수신을 위해 버퍼링할 데이터의 양
[[connected]] false 직렬 포트의 논리적 연결 상태를 나타내는 플래그
[[readable]] null 해당 ReadableStream 은 포트에서 데이터를 수신합니다
[[readFatal]] false 포트에 치명적인 읽기 오류가 발생했음을 나타내는 플래그
[[writable]] null 해당 WritableStream 은 포트로 데이터를 전송합니다
[[writeFatal]] false 포트에 치명적인 쓰기 오류가 발생했음을 나타내는 플래그
[[pendingClosePromise]] null 해당 Promise 는 다음을 기다리는 데 사용됩니다: readable 및 writable 이 닫히는 것

4.1. onconnect 속성

onconnect 는 이벤트 처리기 IDL 속성이며, connect 이벤트 유형에 해당합니다.

사용자가 사이트의 접근을 허용한 직렬 포트가 있고, 이 허용은 이전의 다음 호출 결과입니다: requestPort() 해당 포트가 논리적으로 연결됨 상태가 되면, 실행합니다: 다음 단계를.
  1. port를 다음으로 둡니다: SerialPort 이는 해당 포트를 나타냅니다.

  2. 설정합니다: port.[[connected]] 를 true로.

  3. 이벤트를 발생시킵니다. 그 이름은 connect 이고, 대상은 port이며, 이벤트의 bubbles 속성은 true로 초기화합니다.

직렬 포트가 논리적으로 연결됨 상태인 조건은, 유선 직렬 포트이면서 시스템에 물리적으로 연결되어 있거나, 무선 직렬 포트이면서 시스템이 무선 장치에 대한 활성 연결을 가지고 있는 경우입니다(예: 열린 Bluetooth L2CAP 채널).

4.2. ondisconnect 속성

ondisconnect 는 이벤트 처리기 IDL 속성이며, disconnect 이벤트 유형에 해당합니다.

사용자가 사이트의 접근을 허용한 직렬 포트가 있고, 이 허용은 이전의 다음 호출 결과입니다: requestPort() 해당 포트가 더 이상 논리적으로 연결됨 상태가 아니면, 다음 단계를 실행합니다:
  1. port를 다음으로 둡니다: SerialPort 이는 해당 포트를 나타냅니다.

  2. 설정합니다: port.[[connected]] 를 false로.

  3. 이벤트를 발생시킵니다. 그 이름은 disconnect 이고, 대상은 port이며, 이벤트의 bubbles 속성은 true로 초기화합니다.

4.3. getInfo() 메서드

이 getInfo() 메서드의 단계는 다음과 같습니다:
  1. info를 빈 순서 있는 맵으로 둡니다.

  2. 포트가 USB 장치의 일부이면, 다음 단계를 수행합니다:

    1. 설정합니다: info["usbVendorId"] 를 공급업체 ID로, 그 대상은 해당 장치입니다.

    2. 설정합니다: info["usbProductId"] 를 제품 ID로, 그 대상은 해당 장치입니다.

  3. 포트가 Bluetooth 장치의 서비스이면, 다음 단계를 수행합니다:

    1. 설정합니다: info["bluetoothServiceClassId"] 를 서비스 클래스 UUID로, 이는 해당 Bluetooth 서비스의 값입니다.

  4. info를 반환합니다.

4.3.1. SerialPortInfo 사전

dictionary SerialPortInfo {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};
usbVendorId 멤버, 타입 unsigned short
포트가 USB 장치의 일부이면 이 멤버는 16비트 공급업체 ID이며, 해당 장치의 ID입니다. 그렇지 않으면 undefined입니다.
usbProductId 멤버, 타입 unsigned short
포트가 USB 장치의 일부이면 이 멤버는 16비트 제품 ID로서 해당 장치의 ID입니다. 그렇지 않으면 undefined입니다.
bluetoothServiceClassId 멤버, 타입 BluetoothServiceUUID
포트가 Bluetooth 장치의 서비스이면 이 멤버는 다음과 같습니다: BluetoothServiceUUID 이는 서비스 클래스 UUID를 포함합니다. 그렇지 않으면 undefined입니다.

4.4. open() 메서드

직렬 포트로 통신하기 전에 포트를 열어야 합니다. 포트를 열면 사이트에서 데이터 전송 방식을 제어하는 데 필요한 매개변수를 지정할 수 있으며, 이는 송신과 수신에 적용됩니다. 개발자는 문서를 확인해야 하며, 연결하려는 장치의 문서에서 적절한 매개변수를 찾아야 합니다.
await port.open({ baudRate: /* 사용할 보 레이트를 선택합니다 */ });

일단 open() 이 해결되면 readable 및 writable 속성에 접근하여 다음을 얻을 수 있습니다: ReadableStream 및 WritableStream 인스턴스로, 데이터를 수신하고 송신할 대상은 연결된 장치입니다.

이 open() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this.[[state]] 가 "closed"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError" DOMException 이며, promise를 반환합니다.

  3. 만약 options["baudRate"] 가 0이면, promise를 다음으로 거부합니다: TypeError 그리고 promise를 반환합니다.

  4. 만약 options["dataBits"] 가 7도 8도 아니면, promise를 거부합니다. 거부 사유는 TypeError 이며, promise를 반환합니다.

  5. 만약 options["stopBits"] 가 1도 2도 아니면, promise를 거부합니다. 거부 사유는 TypeError 이며, promise를 반환합니다.

  6. 만약 options["bufferSize"] 가 0이면, promise를 다음으로 거부합니다: TypeError 그리고 promise를 반환합니다.

  7. 선택적으로, 만약 options["bufferSize"] 가 다음보다 크면, 즉 구현이 지원할 수 있는 크기를 초과하면, promise를 TypeError로 거부하고 이어서 promise를 반환합니다.

  8. 설정합니다: this.[[state]] 를 "opening"으로.

  9. 다음 단계를 수행합니다. 실행 방식은 병렬입니다.

    1. 운영체제를 호출하여 직렬 포트를 엽니다. 이때 사용할 연결 매개변수(또는 기본값)는 options에 지정된 값입니다.

    2. 어떤 이유로든 이 작업이 실패하면, 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "NetworkError" DOMException 그리고 이 단계들을 중단합니다.

    3. 설정합니다: this.[[state]] 를 "opened"로.

    4. 설정합니다: this.[[bufferSize]] 를 다음 값으로: options["bufferSize"].

    5. 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 undefined로.

  10. promise를 반환합니다.

4.4.1. SerialOptions 사전

dictionary SerialOptions {
  required [EnforceRange] unsigned long baudRate;
  [EnforceRange] octet dataBits = 8;
  [EnforceRange] octet stopBits = 1;
  ParityType parity = "none";
  [EnforceRange] unsigned long bufferSize = 255;
  FlowControlType flowControl = "none";
};
baudRate 멤버, 타입 unsigned long
보 레이트를 나타내는 0이 아닌 양수 값으로, 직렬 통신을 설정할 때 사용할 값입니다.

참고: baudRate 는 이 사전에서 유일한 필수 멤버입니다. 다른 연결 매개변수에는 일반적으로 사용되는 기본값이 있지만, 개발자는 값을 신중하게 고려하고 연결하려는 장치의 문서를 참조하여 올바른 값을 결정해야 합니다. 흔히 사용되는 값은 있지만 표준 보 레이트는 없습니다. 이 매개변수를 필수로 지정하면 임의의 기본값을 이 명세에서 선택했을 때 생길 수 있는 혼란을 줄일 수 있습니다.

dataBits 멤버, 타입 octet, 기본값 8
프레임당 데이터 비트 수입니다. 7 또는 8입니다.
stopBits 멤버, 타입 octet, 기본값 1
프레임 끝의 정지 비트 수입니다. 1 또는 2입니다.
parity 멤버, 타입 ParityType, 기본값 "none"
패리티 모드입니다.
bufferSize 멤버, 타입 unsigned long, 기본값 255
읽기 및 쓰기 버퍼의 크기를 나타내는 0이 아닌 양수 값으로, 생성할 버퍼에 적용됩니다.
flowControl 멤버, 타입 FlowControlType, 기본값 "none"
흐름 제어 모드입니다.
4.4.1.1. ParityType 열거형
enum ParityType {
  "none",
  "even",
  "odd"
};
none
각 데이터 워드에 대해 패리티 비트를 보내지 않습니다.
even
데이터 워드와 패리티 비트를 합한 전체가 짝수 패리티를 가집니다.
odd
데이터 워드와 패리티 비트를 합한 전체가 홀수 패리티를 가집니다.
4.4.1.2. FlowControlType 열거형
enum FlowControlType {
  "none",
  "hardware"
};
none
흐름 제어가 활성화되지 않습니다.
hardware
RTS 및 CTS 신호를 사용하는 하드웨어 흐름 제어가 활성화됩니다.

4.5. connected 속성

이 connected 게터의 단계는 다음과 같습니다:
  1. 반환합니다: this.[[connected]].

4.6. readable 속성

직렬 포트에서 데이터를 수신하는 애플리케이션은 일반적으로 중첩된 두 개의 루프를 다음과 같이 사용합니다.
while (port.readable) {
  const reader = port.readable.getReader();
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // |reader|가 취소되었습니다.
        break;
      }
      // |value|로 작업을 수행합니다...
    }
  } catch (error) {
    // |error|를 처리합니다...
  } finally {
    reader.releaseLock();
  }
}

내부 루프는 포트에서 데이터 청크를 읽다가 오류가 발생하면 "catch" 블록의 코드를 실행합니다. 외부 루프는 패리티 검사 실패와 같이 복구 가능한 오류를 처리하며, 이를 위해 새 리더를 엽니다. 치명적인 오류가 발생하면 readable 이 null이 되고 루프가 종료됩니다.

직렬 포트가 열려 있는 동안에는 계속 데이터를 생성할 수 있으며, 반환되는 각 청크의 데이터 양은, 반환하는 메서드인 read() 의 호출 시점에 따라 사실상 임의로 결정됩니다. 장치와 해당 장치와 통신하는 코드가 무엇을 완전한 메시지로 간주할지 결정해야 합니다. 예를 들어 장치는 ASCII 형식의 텍스트를 사용하여 호스트와 통신할 수 있으며, 각 메시지는 줄바꿈(또는 시퀀스 "\r\n")으로 끝납니다. 파이프라인을 구성하는 TransformStream들을 사용하면 다음을 자동으로 변환할 수 있습니다: Uint8Array 청크로, 이를 제공하는 것은 readable 이며, 변환 결과는 DOMString들로, 각각 완전한 텍스트 한 줄을 포함합니다.

class LineBreakTransformer {
  constructor() {
    this.container = '';
  }

  transform(chunk, controller) {
    this.container += chunk;
    const lines = this.container.split('\r\n');
    this.container = lines.pop();
    lines.forEach(line => controller.enqueue(line));
  }

  flush(controller) {
    controller.enqueue(this.container);
  }
}

const decoder = new TextDecoderStream();
const streamClosed = port.readable.pipeTo(decoder.writable);
const lineReader = decoder.readable
    .pipeThrough(new TransformStream(new LineBreakTransformer()))
    .getReader();

예제 7과 마찬가지로 파이프 체인은 pipeThrough()만으로 구성할 수 없습니다. pipeTo()를 사용해야 하는 시점은 첫 번째 TransformStream 을 SerialPort 에 연결할 때이며, 그래야 포트를 닫으려 할 때 파이프 체인이 닫힐 때까지 기다릴 수 있습니다.

lineReader.cancel();
await streamClosed;
await port.close();

메시지 경계를 인코딩하는 다른 방법으로는 각 메시지 앞에 길이를 붙이거나, 정해진 시간 동안 기다린 후 다음 메시지를 전송하는 방법이 있습니다. TransformStream 을 구현하여 이러한 유형의 메시지 경계를 처리하는 작업은 독자의 연습 과제로 남겨 둡니다.

이 read() 메서드는 비동기적이며 실행을 차단하지 않지만, async/await 구문을 사용하는 코드에서는 실행을 차단하는 것처럼 보일 수 있습니다. 이러한 상황에서는 시간 제한을 구현하면 도움이 될 수 있으며, 일정 기간 데이터를 수신하지 못했을 때 코드 실행을 계속할 수 있도록 합니다. 아래 예제는 releaseLock() 메서드를 사용하여 다음 호출을 중단합니다: read() 그 시점은 타이머가 만료된 후입니다. 이 작업은 스트림을 닫지 않으므로, 시간 제한 이후에 수신한 데이터도 나중에 읽을 수 있으며, getReader() 를 다시 호출하면 됩니다.

async function readWithTimeout(port, timeout) {
  const reader = port.readable.getReader();
  const timer = setTimeout(() => {
    reader.releaseLock();
  }, timeout);
  const result = await reader.read();
  clearTimeout(timer);
  reader.releaseLock();
  return result;
}

이 기능은 releaseLock() 에 다음 변경으로 추가되었으며, whatwg/streams#1168 브라우저에서는 최근에야 구현되었습니다.

이 readable 게터의 단계는 다음과 같습니다:
  1. 만약 this.[[readable]] 이 null이 아니면, 다음을 반환합니다: this.[[readable]].

  2. 만약 this.[[state]] 가 "opened"가 아니면, null을 반환합니다.

  3. 만약 this.[[readFatal]] 이 true이면, null을 반환합니다.

  4. stream을 다음으로 둡니다: 새로운 ReadableStream.

  5. pullAlgorithm을 다음 단계로 둡니다:

    1. desiredSize를 다음으로 둡니다: 원하는 크기(하이 워터 마크까지 채우기 위한), 그 대상은 this.[[readable]]입니다.

    2. 만약 this.[[readable]]의 현재 BYOB 요청 뷰 가 null이 아니면, desiredSize를 다음으로 설정합니다: this.[[readable]]의 현재 BYOB 요청 뷰의 바이트 길이.

    3. promise를 다음으로 둡니다: 하나의 새로운 프로미스.

    4. 다음 단계를 실행합니다. 실행 방식은 병렬입니다:

      1. 운영체제를 호출하여 최대 desiredSize바이트를 읽습니다. 읽기 대상은 포트이며, 결과를 바이트 시퀀스 bytes에 넣습니다.

        참고: this.[[state]] 가 "forgotten"이 되는 것은 the port was disconnected인 경우처럼 취급할 수 있습니다.

      2. 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:

        1. 오류가 발생하지 않았다면:

          1. 만약 this.[[readable]]의 현재 BYOB 요청 뷰가 null이 아니면, 다음을 씁니다: bytes를 다음에: this.[[readable]]의 현재 BYOB 요청 뷰. 그리고 view를 설정합니다: 다음 this.[[readable]]의 현재 BYOB 요청 뷰로.

          2. 그렇지 않으면, view를 다음 결과로 설정합니다: 생성한 Uint8Array 이며, 그 원본은 bytes이고, 생성 영역은 this의 관련 Realm입니다.

          3. 큐에 넣습니다: view를 다음에: this.[[readable]].

          4. 해결합니다: promise를 undefined로.

        2. 버퍼 오버런 상태가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "BufferOverrunError" DOMException입니다. 그리고 다음 단계를 실행합니다: 닫기 처리: 읽기 가능한 스트림.

        3. 브레이크 상태가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "BreakError" DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다.

        4. 프레이밍 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "FramingError" DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다.

        5. 패리티 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "ParityError" DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다.

        6. 운영체제 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "UnknownError" DOMException입니다. 그리고 다음 단계를 실행합니다: 해당 닫기 처리: 읽기 가능한 스트림.

        7. 만약 the port was disconnected이면, 다음 단계를 실행합니다:

          1. 설정합니다: this.[[readFatal]] 를 true로,

          2. 호출합니다: 오류 발생, 대상은 this.[[readable]] 이며, 인수는 "NetworkError" DOMException입니다.

          3. 다음 단계를 실행합니다: 처리: 읽기 가능한 스트림 닫기.

    5. promise를 반환합니다.

  6. cancelAlgorithm을 다음 단계로 둡니다:

    1. promise를 다음으로 둡니다: 하나의 새로운 프로미스.

    2. 다음 단계를 실행합니다. 실행 방식은 병렬입니다.

      1. 운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 수신 버퍼의 내용을 버립니다.

      2. 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:

        1. 다음 단계를 실행합니다: 닫기 처리: 읽기 가능한 스트림.

        2. 해결합니다: promise를 undefined로.

    3. promise를 반환합니다.

  7. 바이트 읽기를 지원하도록 설정합니다: stream에 대해, pullAlgorithm 을 설정합니다: pullAlgorithm으로, cancelAlgorithm 은 cancelAlgorithm으로 설정하고, highWaterMark 는 설정합니다: 다음 this.[[bufferSize]]로.

  8. 설정합니다: this.[[readable]] 을 stream으로.

  9. stream을 반환합니다.

다음 처리: 읽기 가능한 스트림 닫기를 수행하려면 다음 단계를 실행합니다:
  1. 설정합니다: this.[[readable]] 을 null로.

  2. 만약 this.[[writable]] 이 null이고 this.[[pendingClosePromise]] 가 null이 아니면, 해결합니다: this.[[pendingClosePromise]] 를 undefined로.

4.7. writable 속성

개별 데이터 청크를 포트에 쓰려면 WritableStreamDefaultWriter 를 필요에 따라 생성하고 해제할 수 있습니다. 이 예제는 TextEncoder를 사용하여 DOMString 을 필요한 Uint8Array 로 인코딩하여 전송합니다.
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
await writer.write(encoder.encode("PING"));
writer.releaseLock();

더 큰 청크를 쓸 때는 포트가 역방향 압력을 적용하도록 허용하는 것이 중요할 수 있습니다. 이는 직렬 송신기가 애플리케이션이 생성한 데이터를 전송하는 데 지나치게 뒤처지지 않도록 합니다. write() 메서드가 반환하는 것은 Promise 이며, 데이터가 기록되면 해결됩니다. 전송 버퍼에 어느 정도 데이터를 유지하는 것은 높은 처리량을 유지하는 데 중요하지만, 이 Promise 를 기다린 후 너무 많은 데이터 청크가 생성되지 않도록 하는 것은 과도한 버퍼링을 피하는 좋은 방법입니다.

이 writable 게터의 단계는 다음과 같습니다:
  1. 만약 this.[[writable]] 이 null이 아니면, 다음을 반환합니다: this.[[writable]].

  2. 만약 this.[[state]] 가 "opened"가 아니면, null을 반환합니다.

  3. 만약 this.[[writeFatal]] 이 true이면, null을 반환합니다.

  4. stream을 다음으로 둡니다: 새로운 WritableStream.

  5. signal을 stream의 신호로 둡니다.

  6. writeAlgorithm을 chunk가 주어졌을 때의 다음 단계로 둡니다:

    1. promise를 다음으로 둡니다: 하나의 새로운 프로미스.

    2. 단언: signal은 중단됨 상태가 아닙니다.

    3. 만약 chunk를 IDL 값으로 변환할 수 없고, 그 타입이 BufferSource이면, promise를 다음으로 거부합니다: TypeError 그리고 반환합니다: promise. 그렇지 않으면, 변환 결과를 source에 저장합니다.

    4. 버퍼 소스의 복사본을 가져옵니다. 대상은 source이며, 결과를 다음에 저장합니다: bytes.

    5. 병렬로, 다음 단계를 실행합니다:

      1. 운영체제를 호출하여 bytes를 포트에 씁니다. 또는 나중에 병합하기 위해 청크를 저장합니다.

        참고: 운영체제는 다음 시점에 이 작업에서 반환할 수 있습니다: bytes의 전송이 완료된 후가 아니라 전송 큐에 들어간 시점입니다.

        참고: this.[[state]] 가 "forgotten"이 되는 것은 the port was disconnected인 경우처럼 취급할 수 있습니다.

      2. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:

        1. 청크가 성공적으로 기록되었거나 나중에 병합하기 위해 저장되었다면, 해결합니다: promise를 undefined로.

          참고: [STREAMS]는 다음과 같이 규정합니다: writeAlgorithm은 다음 Promise 가 해결된 후에만 호출되며, 이는 이전에 이 알고리즘을 호출하여 반환된 값입니다. 효율성을 위해 구현은 이 Promise 를 일찍 해결할 수 있으며, 이는 여러 청크를 병합하기 위한 것으로, 해당 청크는 WritableStream의 내부 큐에서 대기하다가 운영체제에 대한 단일 요청으로 병합됩니다.

        2. 운영체제 오류가 발생했다면, 거부합니다: promise를 다음으로: "UnknownError" DOMException.

        3. 만약 the port was disconnected이면, 다음 단계를 실행합니다:

          1. 설정합니다: this.[[writeFatal]] 을 true로.

          2. 거부합니다: promise를 다음으로: "NetworkError" DOMException.

          3. 다음 단계를 실행합니다: 처리: 쓰기 가능한 스트림 닫기.

        4. 만약 signal이 중단됨 상태이면, 거부합니다: promise를 signal의 중단 사유로.

    6. promise를 반환합니다.

  7. abortAlgorithm을 다음 단계로 둡니다:

    1. promise를 다음으로 둡니다: 하나의 새로운 프로미스.

    2. 다음 단계를 실행합니다. 실행 방식은 병렬입니다.

      1. 운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 송신 버퍼의 내용을 버립니다.

      2. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:

        1. 다음 단계를 실행합니다: 닫기 처리: 쓰기 가능한 스트림.

        2. 해결합니다: promise를 undefined로.

    3. promise를 반환합니다.

  8. closeAlgorithm을 다음 단계로 둡니다:

    1. promise를 다음으로 둡니다: 하나의 새로운 프로미스.

    2. 다음 단계를 실행합니다. 실행 방식은 병렬입니다.

      1. 운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 송신 버퍼의 내용을 플러시합니다.

      2. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:

        1. 다음 단계를 실행합니다: 닫기 처리: 쓰기 가능한 스트림.

        2. 만약 signal이 중단됨 상태이면, 거부합니다: promise를 signal의 중단 사유로.

        3. 그렇지 않으면, 해결합니다: promise를 undefined로.

    3. promise를 반환합니다.

  9. 설정합니다: stream에 대해, writeAlgorithm은 다음으로 설정합니다: writeAlgorithm, abortAlgorithm은 다음으로 설정합니다: abortAlgorithm, closeAlgorithm은 다음으로 설정합니다: closeAlgorithm, highWaterMark는 다음으로 설정합니다: this.[[bufferSize]], 그리고 sizeAlgorithm은 바이트 수를 세는 크기 알고리즘으로 설정합니다.

  10. 추가합니다: 다음 중단 단계를 signal에.

    1. 포트에 쓰기 위한 모든 운영체제 호출이 기록된 데이터 양과 관계없이 가능한 한 빨리 반환하도록 합니다.

  11. 설정합니다: this.[[writable]] 을 stream으로.

  12. stream을 반환합니다.

다음 처리: 쓰기 가능한 스트림 닫기를 수행하려면 다음 단계를 실행합니다:
  1. 설정합니다: this.[[writable]] 을 null로.

  2. 만약 this.[[readable]] 이 null이고 this.[[pendingClosePromise]] 가 null이 아니면, 해결합니다: this.[[pendingClosePromise]] 를 undefined로.

4.8. setSignals() 메서드

직렬 포트에는 장치 감지 및 흐름 제어를 위한 여러 추가 신호가 있으며, 이를 명시적으로 조회하고 설정할 수 있습니다. 예를 들어, 일부 마이크로컨트롤러를 프로그래밍하려면 먼저 "프로그래밍" 모드에 진입해야 하며, 이를 위해 "데이터 단말 준비"(또는 DTR) 신호를 전환합니다.
await port.setSignals({ dataTerminalReady: false });
await new Promise(resolve => setTimeout(resolve, 200));
await port.setSignals({ dataTerminalReady: true });
이 setSignals() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this.[[state]] 가 "opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError" DOMException 이며, promise를 반환합니다.

  3. signals에 명시된 멤버가 모두 없으면, 거부합니다: promise를 TypeError로 그리고 promise를 반환합니다.

  4. 다음 단계를 수행합니다. 실행 방식은 병렬입니다:

    참고: 이상적으로는 다음에 지정된 변경 사항, 즉 signals의 변경 사항을 원자적으로 적용해야 하지만, 사용자 에이전트가 이 단계들을 구현하는 데 사용하는 POSIX 및 Windows API는 모두 이를 지원하지 않습니다. 따라서 아래에 제시된 순서에 애플리케이션이 의존할 가능성이 높습니다.

    1. 만약 signals["dataTerminalReady"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다( false인 경우). 대상은 직렬 포트의 "데이터 단말 준비" 또는 "DTR" 신호입니다.

    2. 만약 signals["requestToSend"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다( false인 경우). 대상은 직렬 포트의 "송신 요청" 또는 "RTS" 신호입니다.

    3. 만약 signals["break"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다( false인 경우). 대상은 직렬 포트의 "브레이크" 신호입니다.

      참고: "브레이크" 신호는 일반적으로 대역 내 신호로 구현되며, 송신선을 "마크" 전압으로 유지하므로 활성화되어 있는 동안 데이터 전송을 막습니다.

    4. 운영체제가 이러한 신호 중 어느 것의 상태라도 변경하지 못하고, 그 이유가 무엇이든, 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "NetworkError" DOMException.

    5. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 undefined로.

  5. promise를 반환합니다.

4.8.1. SerialOutputSignals 사전

dictionary SerialOutputSignals {
  boolean dataTerminalReady;
  boolean requestToSend;
  boolean break;
};
dataTerminalReady, 타입 boolean
데이터 단말 준비 (DTR)
requestToSend, 다음 타입 boolean
송신 요청 (RTS)
break, 타입 boolean
브레이크

4.9. getSignals() 메서드

이 getSignals() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this.[[state]] 가 "opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError" DOMException 이며, promise를 반환합니다.

  3. 다음 단계를 수행합니다. 실행 방식은 병렬입니다:

    1. 운영체제에 제어 신호의 상태를 조회하며, 이 신호는 직렬 포트에 연결된 장치가 활성화할 수 있는 신호입니다.

    2. 운영체제가 이러한 신호의 상태를 알아내지 못하고, 그 이유가 무엇이든, 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "NetworkError" DOMException 그리고 이 단계들을 중단합니다.

    3. dataCarrierDetect를 true로 두는 조건은 "데이터 반송파 감지" 또는 "DCD" 신호가 장치에 의해 활성화된 경우이며, 그렇지 않으면 false로 둡니다.

    4. clearToSend를 true로 두는 조건은 "송신 허가" 또는 "CTS" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면 false로 둡니다.

    5. ringIndicator를 true로 두는 조건은 "호출 표시" 또는 "RI" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면 false로 둡니다.

    6. dataSetReady를 true로 두는 조건은 "데이터 세트 준비" 또는 "DSR" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면 false로 둡니다.

    7. signals를 다음으로 둡니다: 순서 있는 맵 «[ "dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]».

    8. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 signals로.

  4. promise를 반환합니다.

4.9.1. SerialInputSignals 사전

dictionary SerialInputSignals {
  required boolean dataCarrierDetect;
  required boolean clearToSend;
  required boolean ringIndicator;
  required boolean dataSetReady;
};
dataCarrierDetect 멤버, 타입 boolean
데이터 반송파 감지 (DCD)
clearToSend 멤버, 다음 타입 boolean
송신 허가 (CTS)
ringIndicator 멤버, 타입 boolean
호출 표시 (RI)
dataSetReady 멤버, 타입 boolean
데이터 세트 준비 (DSR)

4.10. close() 메서드

포트와의 통신이 더 이상 필요하지 않으면 포트를 닫고 시스템에서 관련 리소스를 해제할 수 있습니다.

다음을 호출하면: port.close() 암묵적으로 호출됩니다: port.readable.cancel() 및 port.writable.abort() 이는 비우기 위한 것으로, 버퍼링된 모든 데이터에 적용됩니다. 애플리케이션이 다음을 호출했다면: port.readable.getReader() 또는 port.writable.getWriter() 스트림은 잠기며 포트를 닫을 수 없습니다. 따라서 개발자는 진행 중인 읽기 또는 쓰기 작업을 어떻게 처리할지 결정해야 합니다. 예를 들어, 포트가 닫히기 전에 버퍼링된 모든 데이터가 전송되도록 보장하려면 애플리케이션은 Promise 를 기다려야 하며, 이를 반환하는 것은 writer.close()입니다.

const encoder = new TextEncoder();
const writer = port.writable.getWriter();
writer.write(encoder.encode("A long message that will take..."));
await writer.close();
await port.close();

전송되지 않은 데이터를 버리려면 애플리케이션은 대신 다음을 호출할 수 있습니다: writer.abort().

만약 TransformStream 이 파이프로 연결된 대상이 port.writable 이라면, 다음 Promise 가 해결되기를 기다리는 것, 즉 writer.close() 가 반환한 값의 해결을 기다리는 것만으로는 충분하지 않습니다. 애플리케이션은 파이프 체인이 닫히기를 기다려야 하며, 이를 위해 Promise 를 기다려야 합니다. 이를 반환하는 것은 pipeTo() 이며, 대신 이 값의 해결을 기다립니다.

const encoder = new TextEncoderStream();
const writableStreamClosed = encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();
writer.write("A long message that will take...");
writer.close();
await writableStreamClosed;
await port.close();

포트에서 청크를 읽기 위해 루프를 사용하고 있다면, 예제 4에서처럼, 먼저 루프를 종료한 후 다음을 호출해야 합니다: port.close().

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|가 취소되었습니다.
          break;
        }
        // |value|로 작업을 수행합니다...
      }
    } catch (error) {
      // |error|를 처리합니다...
    } finally {
      reader.releaseLock();
    }
  }

  await port.close();
}

const closed = readUntilClosed();

// 얼마 후...
keepReading = false;
reader.cancel();
await closed;

다음을 호출하면: reader.cancel() 다음 호출이 reader.read() 즉시 반환하게 되어, 내부 루프를 종료하고 다음을 호출합니다: reader.releaseLock(). 그러면 외부 루프는 keepReading이 false로 설정되었으므로 종료되며, 스트림의 잠금이 해제되어 port.close() 가 성공적으로 완료될 수 있습니다.

다음을 호출하는 것도 가능하지만: port.close() 그 시점을 다음 Promise 를 기다린 직후로 하는 것으로, 이를 반환하는 것은 reader.cancel() 입니다. 더 나은 방법은 다음 호출을 port.close() readUntilClosed()의 마지막 단계에 두는 것입니다. 그러면 치명적인 오류가 발생하여 port.readable 이 null이 될 때도 포트가 닫힙니다.

이 close() 메서드의 단계는 다음과 같습니다:
  1. promise를 다음으로 둡니다: 새로운 프로미스.

  2. 만약 this.[[state]] 가 "opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError" DOMException 이며, promise를 반환합니다.

  3. cancelPromise를 다음 호출의 결과로 둡니다: 취소, 대상은 this.[[readable]] 이며, 또는 다음으로 해결된 프로미스로 둡니다: undefined, 만약 this.[[readable]] 이 null이면.

  4. abortPromise를 다음 호출의 결과로 둡니다: 중단, 대상은 this.[[writable]] 이며, 또는 다음으로 해결된 프로미스로 둡니다: undefined, 만약 this.[[writable]] 이 null이면.

  5. pendingClosePromise를 다음으로 둡니다: 새로운 프로미스.

  6. 만약 this.[[readable]] 및 this.[[writable]] 이 null이면, 해결합니다: pendingClosePromise를 undefined로.

  7. 설정합니다: this.[[pendingClosePromise]] 를 다음으로: pendingClosePromise.

  8. combinedPromise를 다음의 결과로 둡니다: 모두를 기다리는 프로미스 가져오기, 인수는 «cancelPromise, abortPromise, pendingClosePromise»입니다.

  9. 설정합니다: this.[[state]] 를 "closing"으로.

  10. 반응합니다. 대상은 combinedPromise입니다.

  11. promise를 반환합니다.

4.11. forget() 메서드

직렬 포트에 대한 권한을 자발적으로 철회할 수 있으며, 이는 사용자가 부여한 권한입니다.
// 직렬 포트를 요청합니다.
const port = await navigator.serial.requestPort();

// 그런 다음 나중에... 직렬 포트에 대한 권한을 철회합니다.
await port.forget();
이 forget() 메서드의 단계는 다음과 같습니다:
  1. 사용자 에이전트가 이 동작을 수행할 수 없다면(예: 권한이 관리자 정책에 의해 부여된 경우), 반환합니다: 다음으로 해결된 프로미스: undefined.

  2. 다음 단계를 실행합니다. 실행 방식은 병렬입니다:

    1. 설정합니다: this.[[state]] 를 "forgetting"으로.

    2. 제거합니다: this를 시스템의 직렬 포트 시퀀스에서. 해당 시퀀스는 이전 호출의 결과로 사용자가 사이트의 접근을 허용한 포트들이며, 호출 대상은 requestPort()입니다.

    3. 설정합니다: this.[[state]] 를 "forgotten"으로.

    4. 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 undefined로.

  3. promise를 반환합니다.

5. 차단 목록

이 명세는 다음 위치에 있는 차단 목록 파일을 사용합니다: https://github.com/whatwg/serial 저장소이며, 이를 통해 웹사이트가 접근할 수 있는 포트 집합을 제한합니다.

다음을 수행한 결과는 Bluetooth 서비스 클래스 ID 차단 목록 파싱이며, 그 위치인 URL url에서 얻는 결과는 목록으로, 그 항목은 UUID 값이며 사용자 정의 서비스 ID를 나타냅니다.

이 직렬 포트 프로파일 서비스 클래스 ID는 BluetoothServiceUUID 이며, 값은 "00001101-0000-1000-8000-00805f9b34fb"입니다.

해당 BluetoothServiceUUID serviceUuid가 차단된 Bluetooth 서비스 클래스 UUID인 조건은 다음 단계가 true를 반환하는 것입니다:
  1. uuid를 다음 호출의 결과로 둡니다: BluetoothUUID.getService() 인수는 serviceUuid입니다.

  2. blocklist를 다음의 결과로 둡니다: Bluetooth 서비스 클래스 ID 차단 목록 파싱, 대상 위치는 https://github.com/whatwg/serial/blob/main/bluetooth-service-blocklist.txt입니다.

  3. 만약 blocklist가 포함하는 값이 uuid이면, true를 반환합니다.

  4. 만약 uuid와 같은 것이 직렬 포트 프로파일 서비스 클래스 ID이면, 반환합니다: false.

  5. 만약 uuid가 끝나는 값이 "-0000-1000-8000-00805f9b34fb"이면, 반환합니다: true.

  6. 그렇지 않으면, false를 반환합니다.

6. 통합

6.1. 권한 정책

이 명세는 노출된 메서드의 사용 가능 여부를 제어하는 기능을 정의하며, 노출하는 것은 serial 속성으로, 이 속성은 Navigator 객체에 있습니다.

이 기능의 기능 이름은 "serial"입니다.

이 기본 허용 목록은 이 기능에 대해 'self'입니다.

7. 보안 고려 사항

이 절은 비규범적입니다.

이 API는 다음과 유사한 보안 위험을 수반합니다: [WEBBLUETOOTH] 및 [WEBUSB], 따라서 여기에도 이들로부터 얻은 교훈을 적용할 수 있습니다. 주요 위협은 다음과 같습니다:

이러한 모든 공격에 대한 주된 완화책은 requestPort() 패턴으로, 사용자 상호작용을 요구하며 한 번에 하나의 장치에 대한 접근만 허용합니다. 이는 사이트가 연결된 모든 장치를 열거하여 취약한 장치의 존재 여부를 알아낼 수 없게 하므로 드라이브바이 공격을 방지하며, 대신 접근을 원한다는 사실을 사용자에게 명시적으로 알려야 합니다. 구현은 사이트가 현재 장치와 통신하고 있다는 시각적 표시와 해당 권한을 언제든지 철회할 수 있는 제어 수단을 제공할 수도 있습니다.

이 명세는 사이트가 보안 컨텍스트에서 제공되어야 한다고 요구하며, 이는 네트워크 기반 공격자의 악성 코드 주입을 방지하기 위한 것입니다. 이를 통해 사용자가 권한을 결정할 때 표시되는 사이트의 신원이 정확함을 보장합니다. 또한 이 명세는 최상위 문서가 다음을 통해 명시적으로 허용한 후에만 [PERMISSIONS-POLICY], 교차 출처 iframe이 API를 사용할 수 있도록 요구합니다. 다음과 결합하면 [CSP3], 이러한 메커니즘은 악성 코드 주입 공격으로부터 보호해 줍니다.

남아 있는 우려는 연결된 장치가 피싱 공격으로 악용되는 것으로, 사용자를 설득하여 악성 사이트에 장치 접근을 허용하게 하는 공격입니다. 이러한 공격은 장치의 원래 기능을 악용하거나 장치에 악성 펌웨어를 설치하는 데 사용될 수 있으며, 이 펌웨어는 다시 호스트 컴퓨터를 공격합니다. 호스트 소프트웨어는 연결된 장치의 입력을 제대로 검증하지 않아 공격에 취약할 수 있습니다. 이 분야의 보안 연구는 소프트웨어 공급업체가 연결된 장치를 신뢰할 수 없는 것으로 취급하도록 유도해 왔습니다.

이러한 유형의 공격을 완전히 방지할 수 있는 메커니즘은 없습니다. 페이지에서 장치로 보내는 데이터는 내용을 해석할 수 없는 바이트 시퀀스이기 때문입니다. 특정 유형의 데이터 전송을 차단하려는 시도에 대해서는 그럼에도 해당 유형의 데이터를 보내려는 장치 제조업체가 우회 방법을 마련할 가능성이 높습니다.

사용자 에이전트는 장치 접근을 제어하는 추가 메커니즘을 구현할 수 있습니다:

다음의 구현들은 [WEBBLUETOOTH] 및 [WEBUSB], 이러한 완화책을 실험해 왔지만, 그 효과에는 한계가 있습니다. 첫째, 장치의 악용 가능 여부를 정의하기 어렵습니다. 예를 들어 이 API는 사이트가 마이크로컨트롤러 개발 보드에 펌웨어를 업로드할 수 있도록 합니다. 이는 이 API의 핵심 사용 사례로, 이러한 장치가 교육 및 취미 시장에서 흔히 사용되기 때문입니다. 이러한 보드는 펌웨어 서명 검증을 구현하지 않으므로 쉽게 악성 장치로 바뀔 수 있습니다. 이러한 보드는 분명히 악용 가능하지만 차단해서는 안 됩니다.

또한 취약한 장치 목록을 유지하는 방법은 USB 및 Bluetooth에 잘 적용됩니다. 이 프로토콜들이 장치 메타데이터를 수집하는 대역 외 메커니즘을 정의하기 때문입니다. 따라서 이러한 장치의 제조사와 모델은 호스트에 가상 직렬 포트로 표시되더라도 쉽게 식별할 수 있습니다. 그러나 범용 USB-직렬 또는 Bluetooth-직렬 어댑터뿐 아니라 "실제" 직렬 포트를 갖춘 시스템도 있으며, DB-25, DE-9 또는 RJ-45 커넥터를 사용합니다. 이러한 경우에는 연결된 장치의 신원을 알아내기 위해 읽을 수 있는 메타데이터가 없으므로, 해당 장치에 대한 접근을 차단할 수 없습니다.

8. 개인정보 보호 고려 사항

이 절은 비규범적입니다.

직렬 포트와 직렬 장치에는 두 종류의 민감한 정보가 포함됩니다. 포트가 USB 또는 Bluetooth 장치인 경우, 공급업체 및 제품 ID(제조사와 모델을 식별함)와 같은 식별자뿐 아니라 일련번호 또는 MAC 주소가 있습니다. 직렬 장치 자체에도 고유한 식별자가 있을 수 있으며, 직렬 포트를 통해 보낸 명령으로 이를 얻을 수 있습니다. 장치에는 식별 가능 여부와 관계없이 다른 사적인 정보가 저장되어 있을 수도 있습니다.

장치 권한을 관리하기 위해 구현은 장치 식별자, 예를 들어 USB 공급업체 ID, 제품 ID 및 일련번호를 사용자 환경설정 파일에 저장할 가능성이 높으며, 사용자가 사이트의 접근을 허용한 장치의 안정적인 식별자로 사용합니다. 이러한 값은 사이트와 직접 공유되지 않으며, 권한이 철회되거나 사이트 데이터 전체가 삭제되면 함께 삭제됩니다.

접근을 허용받은 페이지가 장치에 보낼 수 있는 명령을 통해 페이지는 장치에 저장된 다른 민감한 정보에도 접근할 수 있습니다. § 7 보안 고려 사항에서 언급한 이유로, 이를 막는 것은 비현실적이며 또한 페이지가 이 정보에 접근하지 못하도록 시도하는 것은 바람직하지 않습니다.

구현은 사이트가 접근할 수 있는 장치에 대한 완전한 제어권을 사용자에게 제공해야 하며, 사용자 상호작용 없이 장치 접근을 허용해서는 안 됩니다. 이것이 다음 requestPort() 메서드의 의도입니다. 이를 통해 사이트가 연결된 모든 장치를 몰래 열거하고 데이터를 수집하지 못하도록 합니다. 이는 파일 선택기 UI와 유사합니다. 사이트는 파일 시스템 전체를 알 수 없으며, 사용자가 선택한 파일이나 디렉터리만 알 수 있습니다. 구현은 사이트가 이러한 권한을 사용할 때 사용자에게 알릴 수 있으며, 이를 위해 표시 아이콘을 탭이나 주소 표시줄에 나타낼 수 있습니다.

"비공개" 또는 "시크릿" 탐색 모드를 제공하는 구현은 사용자의 일반 프로필에 있는 권한이 이러한 세션으로 이어지지 않도록 하고, 해당 세션에서 부여된 권한이 세션 종료 후에도 유지되지 않도록 해야 합니다. 구현은 이러한 세션에서 장치 접근을 허용할 때 사용자에게 경고할 수 있습니다. 이는 식별 정보를 직접 입력하는 것과 마찬가지로, 장치 식별자 및 기타 고유 속성을 앞서 언급한 장치와의 통신을 통해 얻을 수 있으며, 이를 사용하여 세션 간에 사용자를 식별할 수 있기 때문입니다.

사용자가 장치 접근을 허용하는 것이 웹 보안 모델의 기존 격리 경계를 어떻게 허무는지 이해하지 못하면, 이 API가 부여하는 기능에 놀랄 수 있습니다. 보안 UI와 문서는 사이트에 장치 접근을 허용하면 사이트가 장치와 그 안에 포함된 모든 데이터를 완전히 제어할 수 있게 될 수 있음을 설명해야 합니다.

감사의 말

다음 분들께 감사드립니다: Anatol Ulrich, Chris Mumford, Clément Menard, Domenic Denicola, Dominique Hazael-Massieux, Florian Loitsch, Florian Scholz, Francis Gulotta, François Beaufort, Jack Hsieh, Keavon Chambers, Kenneth Rohde Christiansen, Marcos Cáceres, Matt Reynolds, Jesse Melhuish, Michael Kohler, Ms2ger, Rick Waldron, Sankha Narayan Guria, Simon Pieters, Suz Hinton, Travis Leithead, 그리고 Vincent Scheib 여러분의 훌륭한 기여에 감사드립니다.

이 표준의 작성자는 Reilly Grant입니다. (Google, reillyg@google.com).

지식재산권

저작권 © WHATWG (Apple, Google, Mozilla, Microsoft). 이 저작물에는 크리에이티브 커먼즈 저작자표시 4.0 국제 라이선스가 적용됩니다. 이 저작물의 일부가 소스 코드에 포함되는 경우, 소스 코드에 포함된 해당 부분에는 BSD 3-Clause 라이선스가 대신 적용됩니다.

색인

용어 정의: 이 명세

용어 정의: 참조 문서

참고 문헌

규범적 참고 문헌

[DOM]
Anne van Kesteren. DOM 표준. 현행 표준. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; 외. HTML 표준. 현행 표준. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 표준. 현행 표준. URL: https://infra.spec.whatwg.org/
[PERMISSIONS-POLICY]
Ian Clelland; Ari Chivukula. 권한 정책. URL: https://w3c.github.io/webappsec-permissions-policy/
[STREAMS]
Adam Rice; 외. 스트림 표준. 현행 표준. URL: https://streams.spec.whatwg.org/
[WEBBLUETOOTH]
Reilly Grant. WebBluetooth 표준. 현행 표준. URL: https://bluetooth.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 표준. 현행 표준. URL: https://webidl.spec.whatwg.org/

비규범적 참고 문헌

[CSP3]
Mike West; Antonio Sartori. 콘텐츠 보안 정책 레벨 3. URL: https://w3c.github.io/webappsec-csp/
[WEBUSB]
Reilly Grant. WebUSB 표준. 현행 표준. URL: https://usb.spec.whatwg.org/

IDL 색인

[Exposed=Window, SecureContext]
partial interface Navigator {
  [SameObject] readonly attribute Serial serial;
};

[Exposed=DedicatedWorker, SecureContext]
partial interface WorkerNavigator {
  [SameObject] readonly attribute Serial serial;
};

[Exposed=(DedicatedWorker, Window), SecureContext]
interface Serial : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  Promise<sequence<SerialPort>> getPorts();
  [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
};

dictionary SerialPortRequestOptions {
  sequence<SerialPortFilter> filters;
  sequence<BluetoothServiceUUID> allowedBluetoothServiceClassIds;
};

dictionary SerialPortFilter {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};

[Exposed=(DedicatedWorker,Window), SecureContext]
interface SerialPort : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  readonly attribute boolean connected;
  readonly attribute ReadableStream? readable;
  readonly attribute WritableStream? writable;

  SerialPortInfo getInfo();

  Promise<undefined> open(SerialOptions options);
  Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
  Promise<SerialInputSignals> getSignals();
  Promise<undefined> close();
  Promise<undefined> forget();
};

dictionary SerialPortInfo {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};

dictionary SerialOptions {
  required [EnforceRange] unsigned long baudRate;
  [EnforceRange] octet dataBits = 8;
  [EnforceRange] octet stopBits = 1;
  ParityType parity = "none";
  [EnforceRange] unsigned long bufferSize = 255;
  FlowControlType flowControl = "none";
};

enum ParityType {
  "none",
  "even",
  "odd"
};

enum FlowControlType {
  "none",
  "hardware"
};

dictionary SerialOutputSignals {
  boolean dataTerminalReady;
  boolean requestToSend;
  boolean break;
};

dictionary SerialInputSignals {
  required boolean dataCarrierDetect;
  required boolean clearToSend;
  required boolean ringIndicator;
  required boolean dataSetReady;
};

⚠MDN

Navigator/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial/getPorts

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial/requestPort

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/close

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/connect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/connect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/disconnect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/disconnect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/forget

In only one current engine.

FirefoxNoneSafariNoneChrome103+
Opera?Edge103+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/getInfo

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/getSignals

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/open

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/readable

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/setSignals

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/writable

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

WorkerNavigator/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
OperaNoneEdge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Headers/Permissions-Policy/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?