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 となる。 これはそのデバイスのものである。そうでなければ undefined となる。
usbProductId メンバー、 型は unsigned short
ポートが USB デバイスの一部である場合、このメンバーは 16 ビットの製品 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
正の非ゼロ値で、ボーレートを示す。そのレートでシリアル 通信を確立すべきである。

注: baudRate は唯一の 必須 メンバーであり、この 辞書に属する。その他の接続パラメーターには一般的な既定値があるものの、 開発者が検討し、ドキュメントを参照することが重要である。 接続を意図するデバイスのドキュメントを参照して、正しい 値を決定する。一般的な値はいくつかあるが、標準のボーレートは存在しない。 このパラメーターを必須にすることで、任意の既定値が この仕様によって選択された場合に生じ得る混乱を軽減する。

dataBits メンバー、 型は octet、既定値は 8
フレームあたりのデータビット数。7 または 8。
stopBits メンバー、 型は octet、既定値は 1
フレーム末尾のストップビット数。1 または 2。
parity メンバー、 型は ParityType、既定値は "none"
パリティモード。
bufferSize メンバー、 型は unsigned long、既定値は 255
正の非ゼロ値で、読み取りバッファーと書き込みバッファーのサイズを示す。 これらは、そのサイズで作成すべきである。
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 属性

シリアルポートからデータを受信するアプリケーションでは、通常、入れ子になった 2 つのループを次のように使用します。
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 へ。 それぞれには、テキストの 1 行全体が含まれます。

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 を 1 つの 新しいプロミス とする。

    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 を 1 つの 新しいプロミス とする。

    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 を 1 つの 新しいプロミス とする。

    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 を 1 つの 新しいプロミス とする。

    2. 次の手順を実行する(並列に)。

      1. オペレーティングシステムを呼び出して、すべてのソフトウェア およびハードウェアの、当該ポートの送信バッファーの内容を破棄する。

      2. グローバルタスクをキューに入れる。その対象は 関連するグローバルオブジェクト であり、その所有者は this である。使用するのは シリアル ポートタスクソース とし、実行するのは次の 手順である:

        1. 次の手順を呼び出す:クローズを処理する( 書き込み可能なストリーム)。

        2. 解決する:promise を、 undefined で。

    3. promise を返す。

  8. closeAlgorithm を次の手順とする:

    1. promise を 1 つの 新しいプロミス とする。

    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 API と Windows API のいずれでもサポートされておらず、 ユーザーエージェントはこれらの 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. プライバシーに関する考慮事項

この節は非規範的である。

シリアルポートとシリアルデバイスには、2 種類の機微情報が含まれる。 ポートが USB または Bluetooth デバイスである場合、ベンダー ID や 製品 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 条項ライセンス が適用される。

索引

この 仕様で定義される用語

参照によって定義される 用語

参考文献

規範的参考文献

[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; ほか. Streams 標準. 現行 標準. 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?