アンダーバーが打てるキー

# 導入

先日、初めて英字配列のキーボードを入手しました。
なにぶん初めてなものですから、「キーに書いてある記号と入力が一致しないんだろうな～」程度にしか考えていませんでした。
そんなある日、typstを用いてレポートを作成するとあることに気が付きます。



<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/bd604349-8777-a4a3-8d1f-73e660fd03f1.jpeg" width=50%>

**アンダーバーを入力するキーが…無い！？！？**








# 対処方法の検討
軽く検索をかけると、「-」キーとシフトキーを同時に押すと「\_」が入力されるとのことでしたが、試したところ「=」が入力されました。

enのキーボードなのでjp配列として認識させる？などの解決方法があったようですが、あえてそちらは試さず、、、
<br>
<br>

<br>
<br><br>
<br><br>
<br>
そこで「_」が入力できるキーを自作しようと思いついたのです！（天啓）


# 使用部品
・seeeduino xiao （Arduinoの仲間）
[秋月電子通商での販売リンク](https://akizukidenshi.com/catalog/g/g115178/)

・アーケードコントローラーのボタン

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/6637277a-ab36-ef14-38b7-7e5c12653f29.jpeg" width=50%>
<br>
<br>

・ボタンをはめ込む部品（3Dプリントした）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/0cc0c999-0a95-2e66-095c-4cc52ec0180d.jpeg" width=50%>


<br>
<br>
・マ〇ドナルドでドリンク買うと付属するやつ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/0e4bd6c7-b497-4c37-3a45-c00fbfbb6c29.jpeg" width=50%>

これらを組み合わせるとこんな感じ。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/6eb0daf0-1aec-fb66-16e1-fefe226fbaaf.jpeg" width=50%>

キーの完成です！これはキーです。これはキーですね。

# いざ実践…のはずが
Arduino IDEを用いて、次のプログラムを書き込みました。

```c
#include "Keyboard.h"

const int buttonPin1 = 1; // ボタンが接続されているピン

void setup() {
  pinMode(buttonPin1, INPUT_PULLUP); // ボタンピンを入力として設定する
  Keyboard.begin(); // キーボードの初期化
}

void loop() {
  bool Pin1_pressed = false;
  while (true){
  int buttonState1 = digitalRead(buttonPin1); // ボタンの状態を読み取る

  // ボタンが押された場合、"_"キーのASCIIコードを送信する
  if (buttonState1 == LOW and not Pin1_pressed) {
    Keyboard.write('_');
    Pin1_pressed = true;
    delay(100); // ウェイト
  }else if (buttonState1 == HIGH and Pin1_pressed){
    Pin1_pressed = false;
  }
  }
}
```

「Keyboard.h」は、ArduinoボードをUSBキーボードとして使えるようにするためのライブラリです。Keyboard.write()はキーを1回押す機能です。
なので、このプログラムを使用すれば「\_」だけが入力できる自作キーが完成！…のはずでした。


<div style="display: flex; justify-content: center; gap: 20px;">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/5bc7104c-fa60-6824-5d6b-ad6ac7bd8cbf.gif" alt="イコール">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/1f079081-6873-fbef-8293-11d7e51afb80.gif"width=50% alt="ボタン押す">
</div>


実際打ち込まれたのは「=」でした。

# 原因究明
Keyboard.write()の引数に16進数を入力し、なんのキーが押されるか調べた結果、以下の通りになりました。

```text
1d なし?
1e なし?
1f なし?
20 空白
21 !!!
22 ***
23 ###
24 $$$
25 %%%
26 '''
27 :::
28 )))
29 なし?
2a (((
2b ~~~
2c ,,,
2d ---
2e ...
2f ///

30 000
3a +++
3b ;;;
3c <<<
3d ^^^
3e >>>
3f ???

40 """
41 AAA
4a JJJ

5a ZZZ
5b @@@
5c @@@
5c ]]]
5d [[[
5e &&&
5f ===

60 なし?
61 aaa
62 bbb
6f ooo
77 www
7a zzz
7b ```
7c }}}
7d {{{
7e なし?
7f なし?
80 コントロール
81 なし?
82 なし?
83 windowsキー?
84 windowsキー?
87 windowsキー?
```
アンダーバーに対応する16進数がなさそうです。



ここで、ASCIIコード表を見てみましょう。([農工大のサイト](https://web.tuat.ac.jp/~simizlab/cp1/ascii.html) から引用)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/761eecae-4910-cb58-2cb6-852c63a005d3.png" width=50%>

ASCIIコード表を見ると、「\_」は16進数で5Fであることがわかります。先程の調査した表で5Fに該当するのは「=」です。したがって、作成したプログラムに問題があるわけではなく、ライブラリに何か秘密がありそうです。

# ライブラリを読む
私の場合では、`C:\Users\user名\AppData\Local\Arduino15\libraries\Keyboard` というフォルダにライブラリの本体である`Keyboard.h`と`Keyboard.cpp`ファイル、その他のファイルが入っていました。

中身を読んでいきましょう。
まず、先程の`Keyboard.write()`関数は`Keyboard.cpp`に書かれています。

```cpp
size_t Keyboard_::write(uint8_t c)
{
	uint8_t p = press(c);	// Keydown
	release(c);		// Keyup
	return p;		// just return the result of press() since release() almost always returns 1
}
```
`press()`,`release()`という関数を連続して呼び出すことで、キーが押されて離されるまでの一連の動作を再現しているようです。


では次に、`press()`関数を読みます。
```cpp
size_t Keyboard_::press(uint8_t k)
{
	uint8_t i;
	if (k >= 136) {			// it's a non-printing key (not a modifier)
		k = k - 136;
	} else if (k >= 128) {	// it's a modifier key
		_keyReport.modifiers |= (1<<(k-128));
		k = 0;
	} else {				// it's a printing key
		k = pgm_read_byte(_asciimap + k);
		if (!k) {
			setWriteError();
			return 0;
		}
		if ((k & ALT_GR) == ALT_GR) {
			_keyReport.modifiers |= 0x40;   // AltGr = right Alt
			k &= 0x3F;
		} else if ((k & SHIFT) == SHIFT) {
			_keyReport.modifiers |= 0x02;	// the left shift modifier
			k &= 0x7F;
		}
		if (k == ISO_REPLACEMENT) {
			k = ISO_KEY;
		}
	}
	k = 0x87;
	_keyReport.modifiers |= 0x02;
	
	// Add k to the key report only if it's not already present
	// and if there is an empty slot.
	if (_keyReport.keys[0] != k && _keyReport.keys[1] != k &&
		_keyReport.keys[2] != k && _keyReport.keys[3] != k &&
		_keyReport.keys[4] != k && _keyReport.keys[5] != k) {

		for (i=0; i<6; i++) {
			if (_keyReport.keys[i] == 0x00) {
				_keyReport.keys[i] = k;
				break;
			}
		}
		if (i == 6) {
			setWriteError();
			return 0;
		}
	}
	_keyReport.modifiers |= 0x02;
	sendReport(&_keyReport);
	return 1;
}
```

引数を文字コードとして受け取り、情報を整形したのちに `keyReport`という構造体（クラス？）にまとめて送信することにより、キー入力を行っているようです。
ところで、上記のコードのこの部分が気になります。

```c++
} else if ((k & SHIFT) == SHIFT) {
			_keyReport.modifiers |= 0x02;	// the left shift modifier
			k &= 0x7F;
			
		}
```
kというのはこの関数の引数、つまり何らかの文字コードとなる数値が期待されています。
`K & SHIFT`というのは、ビットの積演算です。この値がSHIFTキーのフラグとなり、SHIFTキーの併用が必要なキー入力の場合、`_KeyReport.modifiers`にSHIFTキーの入力フラグをオンであると伝える…といった感じでしょうか。
ここに登場するSHIFTという定数は、`KeyBoardLayout.h`というファイルで定義されていました。
```c
#define SHIFT 0x80
#define ALT_GR 0xc0
#define ISO_KEY 0x64
#define ISO_REPLACEMENT 0x32
```
つまり、文字コードは`0x00~0x7F`の範囲で想定されていて、最上位ビットである`0x80`が立っていればシフトキーも同時に入力する必要がある、ということです。

あとでわかりますが、ここが最大のつまづきポイントでした。
重要なのは*文字コードは`0x00~0x7F`の範囲で想定されている*という部分です。

最後に、`Keyboard.h`というファイルを読みます。
最後の方に、このような記述があります。
```c
class Keyboard_ : public Print
{
private:
  KeyReport _keyReport;
  const uint8_t *_asciimap;
  void sendReport(KeyReport* keys);
public:
  Keyboard_(void);
  void begin(const uint8_t *layout = KeyboardLayout_en_US);
  void end(void);
  size_t write(uint8_t k);
  size_t write(const uint8_t *buffer, size_t size);
  size_t press(uint8_t k);
  size_t release(uint8_t k);
  void releaseAll(void);
};
```

`void begin(const uint8_t *layout = KeyboardLayout_en_US);`が気になります。
フォルダに`KeyboardLayout_en_US.cpp`があるので読みます。

```c
extern const uint8_t KeyboardLayout_en_US[128] PROGMEM =
{
	0x00,          // NUL
	0x00,          // SOH
	0x00,          // STX
	0x00,          // ETX
	0x00,          // EOT
	0x00,          // ENQ
	0x00,          // ACK
	0x00,          // BEL
	0x2a,          // BS  Backspace
	0x2b,          // TAB Tab
	0x28,          // LF  Enter
	0x00,          // VT
	0x00,          // FF
	0x00,          // CR
	0x00,          // SO
	0x00,          // SI
	…
```

128個のキーコードが書かれていました。このキーコードと日本語のキーコードが同一でないためにアンダーバーば入力できなかったようです。

おおかた読み終わりました。それでは、とりあえず動くものを作りましょう。

# 解決策
キー入力を実際に担っているのは`KeyBoard.cpp`の`Keyboard.press()`関数だということがわかったので、この関数を「引数が何であってもアンダーバーを入力する関数」に変更しちゃいましょう（ダメ）

```diff_cpp
size_t Keyboard_::press(uint8_t k)
{
	uint8_t i;
-	if (k >= 136) {			// it's a non-printing key (not a modifier)
-		k = k - 136;
-	} else if (k >= 128) {	// it's a modifier key
-		_keyReport.modifiers |= (1<<(k-128));
-		k = 0;
-	} else {				// it's a printing key
-		k = pgm_read_byte(_asciimap + k);
-		if (!k) {
-			setWriteError();
-			return 0;
-		}
-		if ((k & ALT_GR) == ALT_GR) {
-			_keyReport.modifiers |= 0x40;   // AltGr = right Alt
-			k &= 0x3F;
-		} else if ((k & SHIFT) == SHIFT) {
-			_keyReport.modifiers |= 0x02;	// the left shift modifier
-			k &= 0x7F;
-		}
-		if (k == ISO_REPLACEMENT) {
-			k = ISO_KEY;
-		}
-	}
+	k = 0x87;                     // 日本語の「ろ」キー
+	_keyReport.modifiers |= 0x02; // シフトキーを同時に押すフラグをオン

	// Add k to the key report only if it's not already present
	// and if there is an empty slot.
	if (_keyReport.keys[0] != k && _keyReport.keys[1] != k &&
		_keyReport.keys[2] != k && _keyReport.keys[3] != k &&
		_keyReport.keys[4] != k && _keyReport.keys[5] != k) {

		for (i=0; i<6; i++) {
			if (_keyReport.keys[i] == 0x00) {
				_keyReport.keys[i] = k;
				break;
			}
		}
		if (i == 6) {
			setWriteError();
			return 0;
		}
	}
	sendReport(&_keyReport);
	return 1;
}
```
この変更に伴って、呼び出しもとの`Keyboard.write()`関数も変更します。
`release()`関数は文字コードを指定してキーを離すので不都合です。
`releaseAll()`に変更します。
```diff_cpp
size_t Keyboard_::write(uint8_t c)
{
	uint8_t p = press(c);	// Keydown
-   release(c)    
+	releaseAll();		// KeyupAll
	return p;		// just return the result of press() since release() almost always returns 1
}
```
今回の場合ではアンダーバーを打ちたいだけなのでキーマップは不要です。（依存を解消する意味もあり、）次のように空のキーマップを作っておき、不要な部分を消します。

```diff_cpp
+extern
+const uint8_t _asciimap[128] PROGMEM;

+const uint8_t _asciimap[128] =
+{
+
+};


Keyboard_::Keyboard_(void)
{
	static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
	HID().AppendDescriptor(&node);
-	_asciimap = KeyboardLayout_en_US;	
}

-void Keyboard_::begin(const uint8_t *layout)	
+void Keyboard_::begin(void)
{
-   _asciimap = layout;
}
```

`Keyboard.begin()`の引数を変更してしまったため、`Keyboard.h`との整合性が取れなくなってしまいます。そのため、`Keyboard.h`も変更します。
```diff_c
class Keyboard_ : public Print
{
private:
  KeyReport _keyReport;
  const uint8_t *_asciimap;
  void sendReport(KeyReport* keys);
public:
  Keyboard_(void);
- void begin(const uint8_t *layout = KeyboardLayout_en_US);
+ void begin(void);
  void end(void);
  size_t write(uint8_t k);
  size_t write(const uint8_t *buffer, size_t size);
  size_t press(uint8_t k);
  size_t release(uint8_t k);
  void releaseAll(void);
};
```
そして、今回私が一番引っかかったのが、次のポイントです。
もう1度`Keyboard.cpp`に戻り、次の部分を変更します。

```diff_cpp
    0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
-   0x29, 0x73,                    //   USAGE_MAXIMUM (Keyboard Application)
+   0x29, 0xff,                    //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
    0xc0,                          // END_COLLECTION
```
[#ライブラリを読む](#ライブラリを読む)のなかで少し触れましたが、プログラム内で渡されるキーコードはおそらく`0x00~0x7F`が想定され、少なくとも`0x80`にはならないようにしたいはずです（SHIFTキーのフラグと衝突するため）。
ここを`0x73`から`0xff`にすることでやり取りする値の上限を開放し、日本語の「ろ」キーに該当する`0x87`というキーコードが正常にやりとりされます。

# 自作ライブラリを動作検証
最後に、ファイル名を変更しましょう。`KeyBoard_under.h`と`KeyBoard_under.cpp`に変更します。`C:\Users\user名\AppData\Local\Arduino15\libraries\`に`KeyBoard_under`というフォルダを作り、そこに2ファイルを保存します。
seeeduinoボードに次のコードを書き込みます。

```c
#include <Keyboard_under.h>

const int buttonPin1 = 1; // ボタンが接続されているピン

void setup() {
  pinMode(buttonPin1, INPUT_PULLUP); // ボタンピンを入力として設定する
  Keyboard.begin(); // キーボードの初期化
}

void loop() {
  bool Pin1_pressed = false;
  while (true){
  int buttonState1 = digitalRead(buttonPin1); // ボタンの状態を読み取る

  if (buttonState1 == LOW and not Pin1_pressed) {
    Keyboard.write('_'); // 引数は何でもよい
    Pin1_pressed = true;
    delay(100); // ウェイト
  }else if (buttonState1 == HIGH and Pin1_pressed){
    Pin1_pressed = false;
  }
  }
}

```

いざ、ボタンを連打すると…？
<div style="display: flex; justify-content: center; gap: 20px;">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/0d1a4ae0-6467-9858-1f20-e5d67b66162a.gif" width=50% alt="イコール">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/1f079081-6873-fbef-8293-11d7e51afb80.gif"width=50% alt="ボタン押す">
</div>

やった！念願のアンダーバーが入力できたぞ！

# 根本的な解決策
私の解決策ではアンダーバーがが入力可能になるだけであり、本質的な解決とは言えないでしょう。
そこで、この現象について検索してみると、すでに原因と解決策を提示してくださっている方がいらっしゃいましたので、紹介させていただきます。

https://mgt.blog.ss-blog.jp/2016-01-14

以下は引用です。
> Arduino LeonardoでUSBキーボードエミュレーションを行う場合、IDE付属のKeyboardライブラリを使うと記号が化けたり、「\」「|」「半角/全角キー」などの打てない文字・キーがあったりします。Keyboardライブラリは101キーボードをエミュレートするため、106/109キーボードにしかない文字やキーを打てず、キーバインドの異なる記号は化けてしまうのです。

こちらの方が作成された2つのファイルをライブラリとして使用すると、正しく「\_」が入力できるようになります。






# 最後に
今回の記事を作成するにあたり、初めて既存のライブラリを読む作業を行いました。
ビット演算を駆使してうまいこと動作させているライブラリで、込められた意図を察するまでにかなり時間がかかってしまいました（合計6時間ぐらい…？）。

完璧な解決方法を自力で作ることはできませんでしたが、当初の目的である「アンダーバーが打てるキーを作る」は達成できたので感無量です。~~動けばよかろうなのだ~~

同様の症状に見舞われている方の一助になれば幸いです。

ここまで読んでいただき、ありがとうございました！