Seeeduino XIAO で入力の変化をUSBキーボードのキー押下として伝える

Seeeduino XIAO は、現在[秋月電子通商](https://akizukidenshi.com/catalog/default.aspx) (販売コード 115178) で1個1,000円未満で買える安価なマイコンである。
安価で組み込みやすい上に、キーボードなどのUSBデバイスとしての動作に対応している。

# Seeeduino XIAO の開発環境を構築する

Seeeduino XIAO のプログラムは、[Arduino IDE](https://www.arduino.cc/en/software) を用いて開発できる。
今回は、Arduino IDE 2.3.2 を用いる。

[Getting Started with Seeed Studio XIAO SAMD21 | Seeed Studio Wiki](https://wiki.seeedstudio.com/Seeeduino-XIAO/#getting-started)
を参考に準備を行う。

まず、File メニューから Preferences... を選択する。
出てきた画面で、「Additional boards manager URLs」の右側にある四角が2個あるボタンを押す。

![Preferences の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/3e0c9818-8387-ff60-ba7b-e54ea5bfab52.png)

するとURLのリストを設定する画面になるので、

```text
https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
```

を追加し、「OK」を押す。

![URLのリストを設定する画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/9f6ac08c-3248-a26f-e805-722b0544ed49.png)

さらに、Preferences の画面の「OK」も押す。

画面左側のボタンから「BOARDS MANAGER」を開く。
「xiao samd」などで検索し、「Seeed SAMD Boards」をインストールする。
(該当の項目の「INSTALL」をクリックする)

![BOARDS MANAGER の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/a4b8ac99-0c27-1014-bfa7-6c801da3340d.png)

これにより、Seeeduino XIAO 用のプログラムをコンパイルして書き込めるようになるはずである。
あとは通常通りデバイスの接続と選択、コンパイルと書き込みを行えばよい。

例として、以下のプログラム (Lチカ) を実行してみた。

```c++
const int ledPin = 13;
const int period = 1000; // ms

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(period * 2 / 3);
  digitalWrite(ledPin, LOW);
  delay(period / 3);
}
```

13番ピンに標準搭載のLEDが接続されている。
実行してみると、消灯時間が点灯時間の約2倍になった。
このことから、Seeeduino XIAO に標準搭載のLEDは `HIGH` を出力すると消灯し、`LOW` を出力すると点灯することが確認できる。

Upload ボタンを押すだけではプログラムを Seeeduino XIAO に書き込めなかった場合、Upload ボタンを押して書き込み待機状態になっているときに Seeeduino XIAO 表面の RST と書かれた接点とその隣の接点をケーブルなどで素早く2回接続することで、書き込めることがある。
~~これでも書き込めない場合、ブートローダーを書き直さないと復活しないかも…？~~
[Getting Started with Seeed Studio XIAO SAMD21 | Seeed Studio Wiki](https://wiki.seeedstudio.com/Seeeduino-XIAO/#enter-bootloader-mode)
[Seeeduino XIAOが1日で壊れた→ブートローダを書き込んだら直った話 | Revetroniqueのアート探訪](https://blog.revetronique.com/seeduino-xiao2/)

# Seeeduino XIAO からUSBキーボードの信号を送る

Arduino の [Keyboard ライブラリ](https://www.arduino.cc/reference/en/language/functions/usb/keyboard/)を用いて、Seeeduino XIAO からUSBキーボードのキーを押したり離したりしたという情報を送信する。

まず、ライブラリをインクルードする。

```c++
#include "Keyboard.h"
```

最初 (`setup` 関数など) に、USBキーボード機能を初期化する。

```c++
Keyboard.begin();
```

`press` 関数で指定したキーを押した情報を送信でき、`release` 関数で指定したキーを離した情報を送信できる。

```c++
Keyboard.press('w');
Keyboard.release('w');
```

例として、1秒おきに20ミリ秒間 <kbd>w</kbd> キーを押すプログラムを実行してみた。

```c++
#include "Keyboard.h"

// キーを押す時間と離す時間
const unsigned long pressTime = 20;
const unsigned long releaseTime = 1000 - pressTime;

// 信号を送るタイミングの管理
unsigned long prevSentTime;
bool keyPressed;

void setup() {
  // USBキーボード機能の初期化
  Keyboard.begin();
  // タイミング状態の初期化
  prevSentTime = millis();
  keyPressed = false;
}

void loop() {
  // 指定の時間が経つまで待機する
  unsigned long currentTime = millis();
  if (keyPressed) {
    // 現在キーが押されているので、時間が経ったら離す
    if (currentTime - prevSentTime < pressTime) return;
  } else {
    // 現在キーが押されているので、時間が経ったら押す
    if (currentTime - prevSentTime < releaseTime) return;
  }
  // タイミング状態を更新する
  prevSentTime = currentTime;
  keyPressed = !keyPressed;
  // キー状態を送信する (時間により切り替えた後の状態を用いる)
  if (keyPressed) {
    Keyboard.press('w');
  } else {
    Keyboard.release('w');
  }
}
```

:::note warn
このプログラムを実行すると、勝手に1秒おきに <kbd>w</kbd> キーが押される状態になるので、操作を誤ると編集中のプログラムや文章などが消えるなどの害が発生する可能性がある。注意すること。
:::

[Wireshark](https://www.wireshark.org/) で通信の様子を確認すると、約1秒おきにキーを押した情報が送信され、その約0.02秒後にキーを離した情報が送信されていることがわかる。

![Wireshark で確認した通信の様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/42c438e5-35aa-d67d-6194-3e825139bd82.png)

# Seeeduino XIAO で入力の変化をUSBキーボードのキー押下として伝える

Seeeduino XIAO のピンからの入力が変化した際、その情報をUSBキーボードのキー押下として伝える。

`write` 関数を使うと、「キーを押す」「キーを離す」を一発で送信できる。

```c++
Keyboard.write('1');
```

これを利用して、入力ピン 2 の状態が変化したらキーを押下した情報を送信するプログラムを作成した。
入力ピン 2 と GND の間をスイッチを介して接続することを想定している。
スイッチがオフのときはプルアップにより `HIGH` が入力され、スイッチがオンのときは GND に接続されるので `LOW` が入力される。

```c++
#include "Keyboard.h"

const int inputPin = D2;

int inputStatus = -1;

void setup() {
  // USBキーボード機能の初期化
  Keyboard.begin();
  // 入力状態の初期化
  pinMode(inputPin, INPUT_PULLUP);
}

void loop() {
  // 今の入力を得る
  int currentStatus = digitalRead(inputPin);
  // 入力が変化していれば、情報を送信する
  if (currentStatus != inputStatus) {
    if (currentStatus == LOW) {
      Keyboard.write('1'); // LOW (スイッチオン) になったら、1 キーを押したことにする
    } else {
      Keyboard.write('q'); // HIGH (スイッチオフ) になったら、Q キーを押したことにする
    }
    inputStatus = currentStatus;
  }
}
```

実行し、スイッチを切り替えると、`1` や `q` が入力された。
時々1回の切り替えで複数セットの `1` や `q` が入力されることがあった。
これはスイッチの内部の構造の都合により接点が素早く触れたり離れたりすることがあるためであり、1回情報を送信したら数十ミリ秒は情報を送信しないようにする、などの対策が考えられる。

Wireshark で確認すると、`write` は押した情報の約1ミリ秒後に離した情報を送信していることがわかった。

![Wireshark で確認した write の様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/01efa6c9-cbe9-915b-210f-d50c4c1be34f.png)

テキストエディタや一部のゲームなどでは、これでもキー入力を受け取ることができることがある。
しかし、キーの状態を適当な間隔でポーリングしているタイプのゲームなどでは、このようにキーを押している時間が短いとキー入力を受け取れないことがある。
この場合に対応するには、`write` を使うのではなく、手動で `press` から少し時間が経った後に `release` を使うのがいいだろう。
ついでに、「変化の直後の変化は送信しない」処理も入れてみた。

```c++
#include "Keyboard.h"

const int inputPin = D2;
const unsigned long pressDuration = 50; // ms
const char onKey = '1', offKey = 'q';
const unsigned long noResendDuration = 50; // ms

int inputStatus = -1;
bool isOnPressed = false, isOffPressed = false, recentlyChanged = false;
unsigned long onPressTime, offPressTime, changedTime;

void setup() {
  // USBキーボード機能の初期化
  Keyboard.begin();
  // 入力状態の初期化
  pinMode(inputPin, INPUT_PULLUP);
}

void loop() {
  unsigned long currentTime = millis();
  // 今の入力を得る
  int currentStatus = digitalRead(inputPin);
  // 変化の直後でなく、入力が変化していれば、情報を送信する
  if (!recentlyChanged && currentStatus != inputStatus) {
    if (currentStatus == LOW) {
      Keyboard.press(onKey);
      isOnPressed = true;
      onPressTime = currentTime;
    } else {
      Keyboard.press(offKey);
      isOffPressed = true;
      offPressTime = currentTime;
    }
    // 変化後の状態と変化した時刻を記録する
    inputStatus = currentStatus;
    changedTime = currentTime;
    recentlyChanged = true;
  }
  // 変化の直後かの情報を更新する
  if (recentlyChanged && currentTime - changedTime >= noResendDuration) {
    recentlyChanged = false;
  }
  // キーを押した状態で、離すまでの時間が経過していれば、キーを離したことにする
  if (isOnPressed && currentTime - onPressTime >= pressDuration) {
    Keyboard.release(onKey);
    isOnPressed = false;
  }
  if (isOffPressed && currentTime - offPressTime >= pressDuration) {
    Keyboard.release(offKey);
    isOffPressed = false;
  }
}
```

# まとめ

* Seeeduino XIAO は、安価で組み込みやすく、USB デバイスとしての動作に対応している
* Seeeduino XIAO 向けのプログラムは、Arduino IDE を用いてコンパイル・書き込みができる
* Keyboard ライブラリの `write` を用いると、約1ミリ秒間だけキーを押したことになる
* Keyboard ライブラリの `press` と `release` を用いると、キーを押している時間を調整できる