なでしこさんで USB-IO2.0 を操作してみた

[なでしこさん](https://nadesi.com/doc3/)で[USB-IO2.0](https://km2net.com/usb-io2.0/index.shtml)を操作してみた。

# 用いる技術

## USB-IO2.0

**USB-IO2.0** は、USB HID インターフェイスでデジタルポートの入出力を操作できるデバイスで、Km2Net で販売されている。
また、対応しているコマンドが少ないバージョン **USB-IO2.0(AKI)** が[秋月電子通商](https://akizukidenshi.com/catalog/default.aspx)で販売されている。(M-05131)

コマンドは、以下のページで公開されている。
[Km2Net Inc. (Try! USB-IO2.0 USB-FSIO)](https://km2net.com/usb-fsio/command.shtml)
HID インターフェイスで「送信データ」をOutputレポートとして送信すると、「受信データ」がInputレポートとして送られてくるようである。

## WebHID API

**WebHID API** は、ウェブブラウザ上のJavaScriptからHIDデバイスを操作できるAPIである。
[WebHID API - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebHID_API)

以下の流れで通信を行える。

1. ユーザにデバイスを選択させる ([`HID.requestDevice()`](https://developer.mozilla.org/ja/docs/Web/API/HID/requestDevice))
2. 選択されたデバイスを開く ([`HIDDevice.open()`](https://developer.mozilla.org/ja/docs/Web/API/HIDDevice/open))
3. データ (レポート) の送受信を行う
   * 送信 (Outputレポート) : [`HIDDevice.sendReport()`](https://developer.mozilla.org/ja/docs/Web/API/HIDDevice/sendReport)
   * 受信 (Inputレポート) : [`inputreport` イベント](https://developer.mozilla.org/ja/docs/Web/API/HIDDevice/inputreport_event)

## なでしこさんとJavaScriptの連携

なでしこさんから、以下の関数によりJavaScriptの機能を呼び出せる。

* [JS実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E5%AE%9F%E8%A1%8C&show)
  * 文字列で指定したJavaScriptのコードを実行し、評価結果を返す。
* [JS関数実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E9%96%A2%E6%95%B0%E5%AE%9F%E8%A1%8C&show)
  * 文字列で指定したJavaScriptのコードを関数として指定した引数とともに呼び出し、評価結果を返す。
* [JSメソッド実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9F%E8%A1%8C&show)
  * 呼び出す対象のJavaScriptのオブジェクトと呼び出すメソッド、および引数を指定して呼び出し、評価結果を返す。
* [AWAIT実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FAWAIT%E5%AE%9F%E8%A1%8C&show)
  * 関数と引数を指定し、関数に `await` をつけて呼び出し、評価結果を返す。

# 実装

今回は簡単のため「とりあえず動かす」ことを目的とし、接続済のデバイスへの再接続(毎回選択しなくていいようにする)や、デバイスの切断への対応などのエラー処理は行わない。

なでしこ3貯蔵庫で実行できる。
[USB-IO2.0 でナイトライダー (WebHID API)](https://n3s.nadesi.com/id.php?1934)

## WebHID API 対応チェック

`navigator.hid` が定義されているかをチェックする。
(現在、WebHID API はFirefoxでは使えないようである)

```
「navigator」をJS実行。
もし、(それに「hid」が辞書キー存在)がいいえならば
	「WebHID API 未対応のブラウザのようです。」を表示。
	「Google Chrome なら動くかもしれません。」を表示。
	終わり。
ここまで。
```

## WebHID API 禁止チェック

なでしこ3貯蔵庫に投稿すると、デフォルトの状態では動かなかった。
調べると、`iframe` に設定された[機能ポリシー](https://developer.mozilla.org/ja/docs/Web/HTTP/Feature_Policy)でHIDの使用が禁止されているらしく、`iframe` の中のURLをウェブブラウザで直接開けば動くことがわかった。
そこで、[`FeaturePolicy.allowsFeature()`](https://developer.mozilla.org/en-US/docs/Web/API/FeaturePolicy/allowsFeature)によるチェックを追加した。

```
「document」をJS実行。
もし、それに「featurePolicy」が辞書キー存在ならば
	もし、(「document.featurePolicy」の「allowsFeature」を["hid"]でJSメソッド実行)がいいえならば
		「WebHID API の実行が禁止されているようです。」と表示。
		「iframe の設定で禁止される可能性があるので、iframe 内ではなく直接開いてください。」と表示。
		URLは「location.href」をJS実行。
		リンクテキストノードは「document」の「createTextNode」を["直接開く"]でJSメソッド実行。
		「a」のDOM部品作成。
		それの「href」にURLをDOM属性設定。
		それの「target」に「_top」をDOM属性設定。
		それにリンクテキストノードをDOM子要素追加。
		終わり。
	ここまで。
ここまで。
```

## デバイスを選択させる

USB-IO2.0 の VID/PID によるフィルタを設定し、デバイスを選択させる。

```
デバイスフィルタは{"filters":[
	{"vendorId": 0x1352, "productId": 0x0120}, # USB-IO2.0
	{"vendorId": 0x1352, "productId": 0x0121} # USB-IO2.0(AKI)
]}。
「(a) => navigator.hid.requestDevice(a)」をJS実行。
デバイス配列はそれを[デバイスフィルタ]でAWAIT実行。
もし、デバイス配列がNULLと等しいならば
	「デバイス選択で例外が発生しました。」と表示。
	終わり。
ここまで。
もし、デバイス配列の配列要素数が0と等しいならば
	「デバイスが選択されませんでした。」と表示。
	終わり。
ここまで。
デバイスはデバイス配列[0]。
「{デバイス["productName"]} に接続しました。」と表示。
```

## レポート送受信の準備

デバイスを開き、送信と受信の関数を定義する。
受信は、以下の方法で行う。

* 受信イベントが発生した時
  * 受信したデータを読み込もうとしている `Promise` がキューに存在すれば、デキューして受信したデータを渡す
  * それが存在しなければ、受信したデータをキューに入れる
* 受信したデータを読み込もうとした時
  * `Promise` を作成し、以下を行う
    * 受信したデータがキューに存在すれば、デキューして受信したデータを返す
    * それが存在しなければ、自分を受信待ちキューに入れる
 
```
デバイスオープンは『(function(device) {
	return async function() {
		return device.open();
	};
})』を[デバイス]でJS関数実行。
もし、デバイス["opened"]がいいえならば
	デバイスオープンを[]でAWAIT実行。
ここまで。
レポート送信は『(function(device) {
	return async function(reportId, data) {
		return device.sendReport(reportId, data);
	};
})』を[デバイス]でJS関数実行。
レポート受信は『(function(device) {
	const resolveQueue = [];
	const dataQueue = [];
	device.oninputreport = function(event) {
		if(resolveQueue.length > 0) {
			resolveQueue.shift()(event.data);
		} else {
			dataQueue.push(event.data);
		}
	};
	return function() {
		return new Promise(function(resolve, reject) {
			if (dataQueue.length > 0) {
				resolve(dataQueue.shift());
			} else {
				resolveQueue.push(resolve);
			}
		});
	};
})』を[デバイス]でJS関数実行。
```

## USB-IO入出力の準備

USB-IOの入出力コマンドを発行する関数を定義する。
今回は、ポート1へのデジタル出力のみ対応する。

```
● (出力値を) ポート1出力とは
	入出力コマンド配列は0を64だけ配列要素作成。
	入出力コマンド配列[0]は0x20。 # デジタル入出力
	入出力コマンド配列[1]は1。 # ポート1に対する出力を指示
	入出力コマンド配列[2]は出力値。 # ポート1に出力する値

	入出力コマンドバッファは「(a) => new Uint8Array(a)」を[入出力コマンド配列]でJS関数実行。
	レポート送信を[0, 入出力コマンドバッファ]でAWAIT実行。
	レポート受信を[]でAWAIT実行。
ここまで。
```

## ナイトライダーの実行

「1」を出力するビットの位置を往復させる処理を行う。

```
「ナイトライダーを実行します。」を表示。
「低速」のラベル作成。
速度バーは[1,100,20]の値指定バー作成。
「高速」のラベル作成。

点灯位置は0。
移動方向は1。
永遠の間
	(1<<点灯位置)をポート1出力。
	点灯位置は点灯位置+移動方向。
	もし、点灯位置<0 または 7<点灯位置 ならば
		移動方向は-移動方向。
		点灯位置は点灯位置+移動方向×2。
	ここまで。
	1÷(速度バーのテキスト取得)秒待つ。
ここまで。
```

# 実行結果

<iframe width="560" height="315" src="https://www.youtube.com/embed/l3JnU3SnCok" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

ポート1に [Pmod 8LD](https://digilent.com/shop/pmod-8ld-eight-high-brightness-leds/) を接続し、プログラムを実行した。
スライダーによる移動速度の変更ができている。

# NG集

## WebHID API 対応チェック

```
もし、(navigatorに「hid」が辞書キー存在)がいいえならば
	「WebHID API 未対応のブラウザのようです。」を表示。
	「Google Chrome なら動くかもしれません。」を表示。
	終わり。
ここまで。
```

> TypeError: Cannot use 'in' operator to search for 'hid' in undefined

JavaScriptの変数を直接は使えず、「JS実行」などを経由して取得しないといけないようだ。

## デバイスを選択させる

```
デバイス配列は「navigator.hid.requestDevice」を[デバイスフィルタ]でAWAIT実行。
```

> 『AWAIT実行』に実行できない関数が指定されました。

なぜ実行できないかは教えてくれず、不親切だ。
そもそも「AWAIT実行」には関数を表す文字列ではなく関数を渡さなければならず、今回指定したのは関数ではないため「実行できない関数が指定されました」というのは嘘であるという疑いがある。

```
「navigator.hid.requestDevice」をJS実行。
デバイス配列はそれを[デバイスフィルタ]でAWAIT実行。
```

> TypeError: Failed to execute 'requestDevice' on 'HID': Illegal invocation

APIの関数を一旦変数に格納して実行しようとした時に出ることがあるエラーと同じ感じだ。

## USB-IO入出力の準備

```
入出力コマンドバッファは「new Uint8Array」を[入出力コマンド配列]でJS関数実行。
```

> JS関数取得で実行できません。

コンストラクタは関数とは別扱いのようである。