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()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this의 관련 전역 객체의 연관된 Document가 사용하도록 허용되지 않은 정책으로 제어되는 기능의 이름이 "serial"이면, 거부합니다: promise를 다음으로: "
SecurityError"DOMException그리고 promise를 반환합니다. -
만약 관련 전역 객체, 즉 this의 해당 객체에 일시적 활성화가 없으면, 거부합니다: promise를 다음으로: "
SecurityError"DOMException그리고 promise를 반환합니다. -
만약 options["
filters"] 가 있으면, 각 filter에 대해, 그 대상은 options["filters"] 에 속하는 항목이며, 다음 단계를 실행합니다:-
만약 filter["
bluetoothServiceClassId"] 가 있으면:-
만약 filter["
usbVendorId"] 가 있으면, 거부합니다: promise를TypeError로 그리고 promise를 반환합니다. -
만약 filter["
usbProductId"] 가 있으면, 거부합니다: promise를TypeError로 그리고 promise를 반환합니다.
-
-
만약 filter["
usbVendorId"] 가 없으면, 거부합니다: promise를TypeError로 그리고 promise를 반환합니다.참고: 이 검사는 다음의 결합된 규칙을 구현합니다:
SerialPortFilter는 비어 있을 수 없으며, 만약usbProductId가 지정되었다면usbVendorId도 반드시 함께 지정해야 합니다.
-
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다:
-
allPorts를 빈 목록으로 둡니다.
-
각각에 대해, 그 대상은 시스템에 등록된 Bluetooth 장치입니다:
-
각각에 대해, 그 대상은
BluetoothServiceUUIDuuid이며, 이를 지원하는 것은 해당 장치입니다:-
만약 uuid가 차단된 Bluetooth 서비스 클래스 UUID가 아니면:
-
만약 uuid가 직렬 포트 프로파일 서비스 클래스 ID와 같거나, 또는
-
options["
allowedBluetoothServiceClassIds"] 가 있고 포함하는 값이 uuid이면:-
port를 다음으로 둡니다:
SerialPort이는 서비스를 나타내며, 해당 Bluetooth 장치의 서비스입니다. -
추가합니다: port를 allPorts에.
-
-
-
-
-
각각에 대해, 그 대상은 사용 가능한 비 Bluetooth 직렬 포트입니다:
-
port를 다음으로 둡니다:
SerialPort이는 해당 포트를 나타냅니다. -
추가합니다: port를 allPorts에.
-
-
사용자에게 포트 목록을 표시하여 사이트의 직렬 포트 접근을 허용하도록 요청합니다. 표시되는 것은 allPorts에 있는 포트 중 하나 이상의 필터와 일치하는 포트이며, 필터는 options["
filters"] 가 있으면 해당 값에 있는 필터이고, 없으면 allPorts를 표시합니다. -
사용자가 포트를 선택하지 않으면, 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "
NotFoundError"DOMException그리고 이 단계들을 중단합니다. -
port를 다음으로 둡니다:
SerialPort이는 사용자가 선택한 포트를 나타냅니다. -
전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 port로.
-
-
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
true를 반환한다는 뜻입니다:
-
info를 다음 호출의 결과로 둡니다: port.
getInfo(). -
만약 filter["
bluetoothServiceClassId"] 가 있으면:-
직렬 포트가 Bluetooth 장치의 일부가 아니면,
false를 반환합니다. -
만약 filter["
bluetoothServiceClassId"] 가 다음과 같으면: info["bluetoothServiceClassId"],true를 반환합니다. -
그렇지 않으면,
false를 반환합니다.
-
-
만약 filter["
usbVendorId"] 가 없으면, 다음을 반환합니다:true. -
직렬 포트가 USB 장치의 일부가 아니면,
false를 반환합니다. -
만약 info["
usbVendorId"] 가 다음과 같지 않으면: filter["usbVendorId"],false를 반환합니다. -
만약 filter["
usbProductId"] 가 없으면, 다음을 반환합니다:true. -
만약 info["
usbProductId"] 가 다음과 같지 않으면: filter["usbProductId"],false를 반환합니다. -
그렇지 않으면,
true를 반환합니다.
SerialPortFilter의
시퀀스이며,
다음 단계가 true를 반환한다는 뜻입니다:
-
시퀀스의 각 filter에 대해, 다음 하위 단계를 실행합니다:
-
만약 port가 필터와 일치하며 그 필터가 filter이면, 다음을 반환합니다:
true.
-
-
false를 반환합니다.
3.2. getPorts() 메서드
// 페이지가 로드될 때 사용 가능한 포트를 확인합니다. 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()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this의 관련 전역 객체의 연관된 Document가 사용하도록 허용되지 않은 정책으로 제어되는 기능의 이름이
"serial"이면, 거부합니다: promise를 다음으로: "SecurityError"DOMException그리고 다음을 반환합니다: promise. -
다음 단계를 실행합니다. 실행 방식은 병렬입니다:
-
availablePorts를 다음의 시퀀스로 둡니다: 사용 가능한 직렬 포트 중에서, 이전 호출의 결과로 사용자가 사이트에 접근을 허용한 포트이며, 호출 대상은
requestPort()입니다. -
ports를 다음의 시퀀스로 둡니다:
SerialPort객체들로, 이들이 나타내는 것은 availablePorts에 있는 포트입니다. -
전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 ports로.
-
-
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()
해당 포트가 논리적으로
연결됨 상태가 되면, 실행합니다:
다음 단계를.
-
port를 다음으로 둡니다:
SerialPort이는 해당 포트를 나타냅니다. -
설정합니다: port.
[[connected]]를true로. -
이벤트를 발생시킵니다. 그 이름은
connect이고, 대상은 port이며, 이벤트의bubbles속성은true로 초기화합니다.
직렬 포트가 논리적으로 연결됨 상태인 조건은, 유선 직렬 포트이면서 시스템에 물리적으로 연결되어 있거나, 무선 직렬 포트이면서 시스템이 무선 장치에 대한 활성 연결을 가지고 있는 경우입니다(예: 열린 Bluetooth L2CAP 채널).
4.2. ondisconnect 속성
ondisconnect
는 이벤트 처리기 IDL 속성이며,
disconnect
이벤트 유형에 해당합니다.
requestPort()
해당 포트가 더 이상 논리적으로
연결됨 상태가 아니면, 다음 단계를 실행합니다:
-
port를 다음으로 둡니다:
SerialPort이는 해당 포트를 나타냅니다. -
설정합니다: port.
[[connected]]를false로. -
이벤트를 발생시킵니다. 그 이름은
disconnect이고, 대상은 port이며, 이벤트의bubbles속성은true로 초기화합니다.
4.3. getInfo() 메서드
getInfo()
메서드의 단계는 다음과 같습니다:
-
info를 빈 순서 있는 맵으로 둡니다.
-
포트가 USB 장치의 일부이면, 다음 단계를 수행합니다:
-
설정합니다: info["
usbVendorId"] 를 공급업체 ID로, 그 대상은 해당 장치입니다. -
설정합니다: info["
usbProductId"] 를 제품 ID로, 그 대상은 해당 장치입니다.
-
-
포트가 Bluetooth 장치의 서비스이면, 다음 단계를 수행합니다:
-
설정합니다: info["
bluetoothServiceClassId"] 를 서비스 클래스 UUID로, 이는 해당 Bluetooth 서비스의 값입니다.
-
-
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()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this.
[[state]]가"closed"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError"DOMException이며, promise를 반환합니다. -
만약 options["
baudRate"] 가 0이면, promise를 다음으로 거부합니다:TypeError그리고 promise를 반환합니다. -
만약 options["
dataBits"] 가 7도 8도 아니면, promise를 거부합니다. 거부 사유는TypeError이며, promise를 반환합니다. -
만약 options["
stopBits"] 가 1도 2도 아니면, promise를 거부합니다. 거부 사유는TypeError이며, promise를 반환합니다. -
만약 options["
bufferSize"] 가 0이면, promise를 다음으로 거부합니다:TypeError그리고 promise를 반환합니다. -
선택적으로, 만약 options["
bufferSize"] 가 다음보다 크면, 즉 구현이 지원할 수 있는 크기를 초과하면, promise를TypeError로 거부하고 이어서 promise를 반환합니다. -
다음 단계를 수행합니다. 실행 방식은 병렬입니다.
-
운영체제를 호출하여 직렬 포트를 엽니다. 이때 사용할 연결 매개변수(또는 기본값)는 options에 지정된 값입니다.
-
어떤 이유로든 이 작업이 실패하면, 전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "
NetworkError"DOMException그리고 이 단계들을 중단합니다. -
설정합니다: this.
[[bufferSize]]를 다음 값으로: options["bufferSize"]. -
전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를
undefined로.
-
-
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
게터의 단계는 다음과 같습니다:
-
반환합니다: 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
게터의 단계는 다음과 같습니다:
-
만약 this.
[[readable]]이null이 아니면, 다음을 반환합니다: this.[[readable]]. -
만약 this.
[[readFatal]]이true이면,null을 반환합니다. -
stream을 다음으로 둡니다: 새로운
ReadableStream. -
pullAlgorithm을 다음 단계로 둡니다:
-
desiredSize를 다음으로 둡니다: 원하는 크기(하이 워터 마크까지 채우기 위한), 그 대상은 this.
[[readable]]입니다. -
만약 this.
[[readable]]의 현재 BYOB 요청 뷰 가 null이 아니면, desiredSize를 다음으로 설정합니다: this.[[readable]]의 현재 BYOB 요청 뷰의 바이트 길이. -
promise를 다음으로 둡니다: 하나의 새로운 프로미스.
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다:
-
운영체제를 호출하여 최대 desiredSize바이트를 읽습니다. 읽기 대상은 포트이며, 결과를 바이트 시퀀스 bytes에 넣습니다.
참고: this.
[[state]]가"forgotten"이 되는 것은 the port was disconnected인 경우처럼 취급할 수 있습니다. -
전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:
-
오류가 발생하지 않았다면:
-
만약 this.
[[readable]]의 현재 BYOB 요청 뷰가 null이 아니면, 다음을 씁니다: bytes를 다음에: this.[[readable]]의 현재 BYOB 요청 뷰. 그리고 view를 설정합니다: 다음 this.[[readable]]의 현재 BYOB 요청 뷰로. -
그렇지 않으면, view를 다음 결과로 설정합니다: 생성한
Uint8Array이며, 그 원본은 bytes이고, 생성 영역은 this의 관련 Realm입니다. -
큐에 넣습니다: view를 다음에: this.
[[readable]]. -
해결합니다: promise를
undefined로.
-
-
버퍼 오버런 상태가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "BufferOverrunError"DOMException입니다. 그리고 다음 단계를 실행합니다: 닫기 처리: 읽기 가능한 스트림. -
브레이크 상태가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "BreakError"DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다. -
프레이밍 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "FramingError"DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다. -
패리티 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "ParityError"DOMException입니다. 그리고 실행할 단계는 닫기 처리: 읽기 가능한 스트림입니다. -
운영체제 오류가 발생하면, 다음을 호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "UnknownError"DOMException입니다. 그리고 다음 단계를 실행합니다: 해당 닫기 처리: 읽기 가능한 스트림. -
만약 the port was disconnected이면, 다음 단계를 실행합니다:
-
설정합니다: this.
[[readFatal]]를true로, -
호출합니다: 오류 발생, 대상은 this.
[[readable]]이며, 인수는 "NetworkError"DOMException입니다. -
다음 단계를 실행합니다: 처리: 읽기 가능한 스트림 닫기.
-
-
-
-
promise를 반환합니다.
-
-
cancelAlgorithm을 다음 단계로 둡니다:
-
promise를 다음으로 둡니다: 하나의 새로운 프로미스.
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다.
-
운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 수신 버퍼의 내용을 버립니다.
-
전역 작업을 큐에 넣습니다. 그 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:
-
다음 단계를 실행합니다: 닫기 처리: 읽기 가능한 스트림.
-
해결합니다: promise를
undefined로.
-
-
-
promise를 반환합니다.
-
-
바이트 읽기를 지원하도록 설정합니다: stream에 대해, pullAlgorithm 을 설정합니다: pullAlgorithm으로, cancelAlgorithm 은 cancelAlgorithm으로 설정하고, highWaterMark 는 설정합니다: 다음 this.
[[bufferSize]]로. -
설정합니다: this.
[[readable]]을 stream으로. -
stream을 반환합니다.
-
설정합니다: this.
[[readable]]을null로. -
만약 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
게터의 단계는 다음과 같습니다:
-
만약 this.
[[writable]]이null이 아니면, 다음을 반환합니다: this.[[writable]]. -
만약 this.
[[writeFatal]]이true이면,null을 반환합니다. -
stream을 다음으로 둡니다: 새로운
WritableStream. -
signal을 stream의 신호로 둡니다.
-
writeAlgorithm을 chunk가 주어졌을 때의 다음 단계로 둡니다:
-
promise를 다음으로 둡니다: 하나의 새로운 프로미스.
-
단언: signal은 중단됨 상태가 아닙니다.
-
만약 chunk를 IDL 값으로 변환할 수 없고, 그 타입이
BufferSource이면, promise를 다음으로 거부합니다:TypeError그리고 반환합니다: promise. 그렇지 않으면, 변환 결과를 source에 저장합니다. -
버퍼 소스의 복사본을 가져옵니다. 대상은 source이며, 결과를 다음에 저장합니다: bytes.
-
병렬로, 다음 단계를 실행합니다:
-
운영체제를 호출하여 bytes를 포트에 씁니다. 또는 나중에 병합하기 위해 청크를 저장합니다.
참고: 운영체제는 다음 시점에 이 작업에서 반환할 수 있습니다: bytes의 전송이 완료된 후가 아니라 전송 큐에 들어간 시점입니다.
참고: this.
[[state]]가"forgotten"이 되는 것은 the port was disconnected인 경우처럼 취급할 수 있습니다. -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:
-
청크가 성공적으로 기록되었거나 나중에 병합하기 위해 저장되었다면, 해결합니다: promise를
undefined로.참고: [STREAMS]는 다음과 같이 규정합니다: writeAlgorithm은 다음
Promise가 해결된 후에만 호출되며, 이는 이전에 이 알고리즘을 호출하여 반환된 값입니다. 효율성을 위해 구현은 이Promise를 일찍 해결할 수 있으며, 이는 여러 청크를 병합하기 위한 것으로, 해당 청크는WritableStream의 내부 큐에서 대기하다가 운영체제에 대한 단일 요청으로 병합됩니다. -
운영체제 오류가 발생했다면, 거부합니다: promise를 다음으로: "
UnknownError"DOMException. -
만약 the port was disconnected이면, 다음 단계를 실행합니다:
-
설정합니다: this.
[[writeFatal]]을true로. -
거부합니다: promise를 다음으로: "
NetworkError"DOMException. -
다음 단계를 실행합니다: 처리: 쓰기 가능한 스트림 닫기.
-
-
-
-
promise를 반환합니다.
-
-
abortAlgorithm을 다음 단계로 둡니다:
-
promise를 다음으로 둡니다: 하나의 새로운 프로미스.
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다.
-
운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 송신 버퍼의 내용을 버립니다.
-
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:
-
다음 단계를 실행합니다: 닫기 처리: 쓰기 가능한 스트림.
-
해결합니다: promise를
undefined로.
-
-
-
promise를 반환합니다.
-
-
closeAlgorithm을 다음 단계로 둡니다:
-
promise를 다음으로 둡니다: 하나의 새로운 프로미스.
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다.
-
운영체제를 호출하여 포트의 모든 소프트웨어 및 하드웨어 송신 버퍼의 내용을 플러시합니다.
-
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 다음 단계를 실행합니다:
-
다음 단계를 실행합니다: 닫기 처리: 쓰기 가능한 스트림.
-
그렇지 않으면, 해결합니다: promise를
undefined로.
-
-
-
promise를 반환합니다.
-
-
설정합니다: stream에 대해, writeAlgorithm은 다음으로 설정합니다: writeAlgorithm, abortAlgorithm은 다음으로 설정합니다: abortAlgorithm, closeAlgorithm은 다음으로 설정합니다: closeAlgorithm, highWaterMark는 다음으로 설정합니다: this.
[[bufferSize]], 그리고 sizeAlgorithm은 바이트 수를 세는 크기 알고리즘으로 설정합니다. -
추가합니다: 다음 중단 단계를 signal에.
-
포트에 쓰기 위한 모든 운영체제 호출이 기록된 데이터 양과 관계없이 가능한 한 빨리 반환하도록 합니다.
-
-
설정합니다: this.
[[writable]]을 stream으로. -
stream을 반환합니다.
-
설정합니다: this.
[[writable]]을null로. -
만약 this.
[[readable]]이null이고 this.[[pendingClosePromise]]가null이 아니면, 해결합니다: this.[[pendingClosePromise]]를undefined로.
4.8. setSignals() 메서드
await port. setSignals({ dataTerminalReady: false }); await new Promise( resolve=> setTimeout( resolve, 200 )); await port. setSignals({ dataTerminalReady: true });
setSignals()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this.
[[state]]가"opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError"DOMException이며, promise를 반환합니다. -
signals에 명시된 멤버가 모두 없으면, 거부합니다: promise를
TypeError로 그리고 promise를 반환합니다. -
다음 단계를 수행합니다. 실행 방식은 병렬입니다:
참고: 이상적으로는 다음에 지정된 변경 사항, 즉 signals의 변경 사항을 원자적으로 적용해야 하지만, 사용자 에이전트가 이 단계들을 구현하는 데 사용하는 POSIX 및 Windows API는 모두 이를 지원하지 않습니다. 따라서 아래에 제시된 순서에 애플리케이션이 의존할 가능성이 높습니다.
-
만약 signals["
dataTerminalReady"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다(false인 경우). 대상은 직렬 포트의 "데이터 단말 준비" 또는 "DTR" 신호입니다. -
만약 signals["
requestToSend"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다(false인 경우). 대상은 직렬 포트의 "송신 요청" 또는 "RTS" 신호입니다. -
만약 signals["
break"] 가 있으면, 운영체제를 호출하여 활성화하거나(true인 경우) 비활성화합니다(false인 경우). 대상은 직렬 포트의 "브레이크" 신호입니다.참고: "브레이크" 신호는 일반적으로 대역 내 신호로 구현되며, 송신선을 "마크" 전압으로 유지하므로 활성화되어 있는 동안 데이터 전송을 막습니다.
-
운영체제가 이러한 신호 중 어느 것의 상태라도 변경하지 못하고, 그 이유가 무엇이든, 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "
NetworkError"DOMException. -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를
undefined로.
-
-
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()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this.
[[state]]가"opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError"DOMException이며, promise를 반환합니다. -
다음 단계를 수행합니다. 실행 방식은 병렬입니다:
-
운영체제에 제어 신호의 상태를 조회하며, 이 신호는 직렬 포트에 연결된 장치가 활성화할 수 있는 신호입니다.
-
운영체제가 이러한 신호의 상태를 알아내지 못하고, 그 이유가 무엇이든, 전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 다음으로: "
NetworkError"DOMException그리고 이 단계들을 중단합니다. -
dataCarrierDetect를
true로 두는 조건은 "데이터 반송파 감지" 또는 "DCD" 신호가 장치에 의해 활성화된 경우이며, 그렇지 않으면false로 둡니다. -
clearToSend를
true로 두는 조건은 "송신 허가" 또는 "CTS" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면false로 둡니다. -
ringIndicator를
true로 두는 조건은 "호출 표시" 또는 "RI" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면false로 둡니다. -
dataSetReady를
true로 두는 조건은 "데이터 세트 준비" 또는 "DSR" 신호가 이미 장치에 의해 활성화된 경우이며, 그렇지 않으면false로 둡니다. -
signals를 다음으로 둡니다: 순서 있는 맵 «[ "
dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]». -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를 signals로.
-
-
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()
메서드의 단계는 다음과 같습니다:
-
promise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this.
[[state]]가"opened"가 아니면, promise를 거부합니다. 거부 사유는 "InvalidStateError"DOMException이며, promise를 반환합니다. -
cancelPromise를 다음 호출의 결과로 둡니다: 취소, 대상은 this.
[[readable]]이며, 또는 다음으로 해결된 프로미스로 둡니다:undefined, 만약 this.[[readable]]이null이면. -
abortPromise를 다음 호출의 결과로 둡니다: 중단, 대상은 this.
[[writable]]이며, 또는 다음으로 해결된 프로미스로 둡니다:undefined, 만약 this.[[writable]]이null이면. -
pendingClosePromise를 다음으로 둡니다: 새로운 프로미스.
-
만약 this.
[[readable]]및 this.[[writable]]이null이면, 해결합니다: pendingClosePromise를undefined로. -
설정합니다: this.
[[pendingClosePromise]]를 다음으로: pendingClosePromise. -
combinedPromise를 다음의 결과로 둡니다: 모두를 기다리는 프로미스 가져오기, 인수는 «cancelPromise, abortPromise, pendingClosePromise»입니다.
-
반응합니다. 대상은 combinedPromise입니다.
-
combinedPromise가 이행되었다면:
-
다음 단계를 실행합니다. 실행 방식은 병렬입니다:
-
운영체제를 호출하여 직렬 포트를 닫고 관련된 모든 리소스를 해제합니다.
-
설정합니다: this.
[[readFatal]]및 this.[[writeFatal]]을false로. -
설정합니다: this.
[[pendingClosePromise]]를null로. -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스 를 사용하여 해결합니다: promise를
undefined로.
-
-
-
combinedPromise가 r을 사유로 거부되었다면:
-
설정합니다: this.
[[pendingClosePromise]]를null로. -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 거부합니다: promise를 r로.
-
-
-
promise를 반환합니다.
4.11. forget() 메서드
// 직렬 포트를 요청합니다. const port= await navigator. serial. requestPort(); // 그런 다음 나중에... 직렬 포트에 대한 권한을 철회합니다. await port. forget();
forget()
메서드의 단계는 다음과 같습니다:
-
사용자 에이전트가 이 동작을 수행할 수 없다면(예: 권한이 관리자 정책에 의해 부여된 경우), 반환합니다: 다음으로 해결된 프로미스:
undefined. -
다음 단계를 실행합니다. 실행 방식은 병렬입니다:
-
제거합니다: this를 시스템의 직렬 포트 시퀀스에서. 해당 시퀀스는 이전 호출의 결과로 사용자가 사이트의 접근을 허용한 포트들이며, 호출 대상은
requestPort()입니다. -
전역 작업을 큐에 넣습니다. 대상은 관련 전역 객체이며, 이는 this의 객체입니다. 이때 직렬 포트 작업 소스를 사용하여 해결합니다: promise를
undefined로.
-
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를 반환하는 것입니다:
-
uuid를 다음 호출의 결과로 둡니다:
BluetoothUUID.getService()인수는 serviceUuid입니다. -
blocklist를 다음의 결과로 둡니다: Bluetooth 서비스 클래스 ID 차단 목록 파싱, 대상 위치는 https://github.com/whatwg/serial/blob/main/bluetooth-service-blocklist.txt입니다.
-
만약 blocklist가 포함하는 값이 uuid이면,
true를 반환합니다. -
만약 uuid와 같은 것이 직렬 포트 프로파일 서비스 클래스 ID이면, 반환합니다:
false. -
만약 uuid가 끝나는 값이 "
-0000-1000-8000-00805f9b34fb"이면, 반환합니다:true. -
그렇지 않으면,
false를 반환합니다.
6. 통합
6.1. 권한 정책
이 명세는 노출된 메서드의 사용 가능 여부를 제어하는 기능을 정의하며,
노출하는 것은 serial
속성으로, 이 속성은 Navigator
객체에 있습니다.
이 기능의 기능 이름은 "serial"입니다.
이 기본 허용 목록은 이 기능에 대해
'self'입니다.
7. 보안 고려 사항
이 절은 비규범적입니다.
이 API는 다음과 유사한 보안 위험을 수반합니다: [WEBBLUETOOTH] 및 [WEBUSB], 따라서 여기에도 이들로부터 얻은 교훈을 적용할 수 있습니다. 주요 위협은 다음과 같습니다:
-
악성 사이트가 사용자를 속여 접근 권한을 얻은 장치의 원래 기능을 악의적인 목적으로 사용하는 경우입니다. 예를 들어 로봇이 물리적 피해를 일으킬 수 있습니다.
-
악성 사이트가 사용자를 속여 접근 권한을 얻은 장치에 자체 펌웨어를 설치하여, 장치의 원래 기능을 악의적인 목적으로 변경하거나 연결된 호스트를 공격하는 경우입니다. 예를 들어 다른 호스트 소프트웨어가 장치와 통신할 때 버퍼 오버플로를 일으킬 수 있습니다.
-
접근 권한을 부여받은 신뢰할 수 있는 사이트에 악성 코드가 주입되어, 해당 장치에 대해 위의 동작 중 하나를 수행하는 경우입니다. 예를 들어 온라인 펌웨어 업데이트 유틸리티가 해킹되어 악성 펌웨어를 전달할 수 있습니다.
-
공격자가 사용자를 설득하여 악성 장치를 시스템에 연결하게 하고, 이 장치가 악성 사이트나 침해된 사이트와 공모하여 공격자에게 다시 통신할 수 있는 웹 기반 채널을 만드는 경우입니다.
이러한 모든 공격에 대한 주된 완화책은 requestPort()
패턴으로, 사용자 상호작용을 요구하며 한 번에
하나의 장치에 대한 접근만 허용합니다. 이는 사이트가
연결된 모든 장치를 열거하여 취약한 장치의 존재 여부를 알아낼 수 없게 하므로 드라이브바이 공격을 방지하며,
대신 접근을 원한다는 사실을 사용자에게 명시적으로 알려야 합니다.
구현은 사이트가 현재 장치와 통신하고 있다는
시각적 표시와 해당 권한을 언제든지 철회할 수 있는
제어 수단을 제공할 수도 있습니다.
이 명세는 사이트가 보안 컨텍스트에서 제공되어야 한다고 요구하며, 이는 네트워크 기반 공격자의 악성 코드 주입을 방지하기 위한 것입니다. 이를 통해 사용자가 권한을 결정할 때 표시되는 사이트의 신원이 정확함을 보장합니다. 또한 이 명세는 최상위 문서가 다음을 통해 명시적으로 허용한 후에만 [PERMISSIONS-POLICY], 교차 출처 iframe이 API를 사용할 수 있도록 요구합니다. 다음과 결합하면 [CSP3], 이러한 메커니즘은 악성 코드 주입 공격으로부터 보호해 줍니다.
남아 있는 우려는 연결된 장치가 피싱 공격으로 악용되는 것으로, 사용자를 설득하여 악성 사이트에 장치 접근을 허용하게 하는 공격입니다. 이러한 공격은 장치의 원래 기능을 악용하거나 장치에 악성 펌웨어를 설치하는 데 사용될 수 있으며, 이 펌웨어는 다시 호스트 컴퓨터를 공격합니다. 호스트 소프트웨어는 연결된 장치의 입력을 제대로 검증하지 않아 공격에 취약할 수 있습니다. 이 분야의 보안 연구는 소프트웨어 공급업체가 연결된 장치를 신뢰할 수 없는 것으로 취급하도록 유도해 왔습니다.
이러한 유형의 공격을 완전히 방지할 수 있는 메커니즘은 없습니다. 페이지에서 장치로 보내는 데이터는 내용을 해석할 수 없는 바이트 시퀀스이기 때문입니다. 특정 유형의 데이터 전송을 차단하려는 시도에 대해서는 그럼에도 해당 유형의 데이터를 보내려는 장치 제조업체가 우회 방법을 마련할 가능성이 높습니다.
사용자 에이전트는 장치 접근을 제어하는 추가 메커니즘을 구현할 수 있습니다:
-
사이트가 다음을 호출하지 못하도록 하는 설정입니다:
requestPort()단, 명시적 허용 목록에 추가된 경우는 제외합니다.시스템 관리자는 관리하는 모든 기기에 이러한 설정을 적용할 수 있으며, 이를 위해 기업 정책 제어를 사용할 수 있습니다. 이러한 제어는 관리자가 선택한 사이트에 특정 장치의 접근 권한만 자동으로 부여하고 다른 장치의 접근은 허용하지 않도록 할 수 있습니다.
-
악용 가능한 것으로 알려진 하드웨어의 장치 ID 목록을 사용자 에이전트와 함께 배포할 수 있습니다. 목록에 있는 장치로의 연결은 차단됩니다. 구현은 자동 업데이트 또는 실험 관리 시스템을 사용하여 이 목록의 업데이트를 즉시 배포함으로써 진행 중인 공격을 차단할 수 있습니다.
다음의 구현들은 [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 라이선스가 대신 적용됩니다.