DigiKeyboardが日本語キーボード搭載PCで記号を正しく出力しない問題を解決する

Digisparkを用いて自作キーボードを作ると，**記号が正確に出力されない＆一部の記号は出力すらできない問題**にブチ当たります．この記事ではこれを解決する方法を紹介しています．

**2019.06.27追記**
僕は使ったことないので何とも言えませんがArduino LeonardoのKeyboardライブラリでも同様の問題が起きるらしいですね．もしかしたらこの記事応用できるかも...?
#環境

 - USB2.0ポートを搭載したWindows 8.1マシン
 - Arduino IDE 1.8.4
 - 中華Digispark

#DigiKeyboardで起きる問題
DigiKeyboardをそのままの状態で使用すると，USキーボードと日本語キーボードのレイアウトの差から以下の2つの問題が起きます．
###記号が間違って出力される
以下のようなスケッチを書いたとします．

~~~c
DigiKeyboard.print("https://");
~~~

しかし実行してみると，出力される文字列は
https+//
となります．このように，`print()`関数や`sendKeyStroke()`関数で意図した記号とは違う記号が出力されてしまうことがあります．

###出力されない記号がある
今度は以下のようなスケッチを書いたとします．

~~~c
DigiKeyboard.print("C:\Users\Toramin10\Downloads");
~~~

しかし実行結果は
C+UsersToramin10Downloads
となり，先ほどのようにコロンがプラスになってしまうばかりか，このようにバックスラッシュは出力すらされていません．|と_でも同じ症状になってしまいます．

#解決法
解決法としては，

 - 誤出力を見越してあらかじめ記号を置換しておく
 - ヘッダファイルをいじって日本語環境に対応させる

という2通りの方法が考えられます．

あまり**高頻度でスケッチを書き換えない**場合や，何より**\\_|を出力させなくていい**場合は，置換させるだけの方がお手軽だと思います．この方法についてはこちらの過去記事に置換表を載せてありますので参考にしてください．

[DigisparkをUSBキーボードにして一発入力させてみた](https://qiita.com/Toramin10/items/291748d4a0c887dd8d7b)

しかし**頻繁にスケッチを書き換えたい**場合，この方法では毎回人力で置換する必要があり面倒です．また最大の問題点としてこの方法では**\\_|が依然出力できない**ままですから，パスの指定させるぞ！とかパスワード自動入力ドングルを作ろう！といった場合は絶望的です．

このようなケースでは，若干手間がかかりますが**日本語環境に合わせてライブラリをいじる**ことで解決できます．

##いじるファイルは2つ
修正が必要なファイルは以下の2つです．
**◎ライブラリに手を加える際はあくまで自己責任でお願いします◎**

 - DigiKeyboard.h：出力不可能な文字を無くすための修正が必要
 - scancode-ascii-table.h：誤出力を解消するための修正が必要

いずれもDigistumpのパッケージに含まれるDigisparkKeyboardライブラリのなかにあります．見つからないときは，Arduino IDEの **ファイル＞スケッチ例＞DigisparkKeyboard＞Keyboard** からサンプルスケッチを開いて，そこで **スケッチ＞スケッチのフォルダを表示** とやるとライブラリがあるフォルダにたどり着くことができます． 

###まずASCIIから
まず`scancode-ascii-table.h`は`DigiKeyboard.h`から参照されているので，こちらから修正していきます．これは`print()`関数で文字列を取得した時に，その各々の文字がキーボードのどのキーに当たるのか判別するのに使われます．こいつがアメリカ仕様になっているために日本語キーボード搭載のPCでは間違って出力されてしまうわけです．

~~~C++
/* ASCII:  10 */ 40,
~~~
ファイルを開くとこのようなものが並んでいます．
これは，おおまかには**「ASCIIコード10の文字に当たるキーのIDは40」**という意味です．ASCIIコードで十進数の10に当たる文字は「改行」で，日本語キーボードでUsage ID(Dec)が40のキーは「Enterキー」なのでこれは間違っていません．

~~~C++
/* ASCII:  58 */ 179,
~~~
しかしこれを見てみると，ASCIIコード58の「:」は，IDが179のキーに当たるということになっています．日本語キーボードで179は「Shift + ;」ですが，日本語キーボードで「Shift + ;」を押すと**+になってしまいます！**これが誤出力の原因です(USキーボードではShift + ;で:になります)．

~~~C++
/* ASCII:  58 */ 52,
~~~
実際にはIDが52の「:」キーに対応するべきですからこのように書き換えます．
こんなふうにして，全ての行について点検を行い，押すべきキーと違うIDになっていれば修正します．

**なんだそれ！だるい！！！**

分かります．とてもだるいですね．でも案外すぐ終わりました．
それでもだるいものはだるいので，これを置いておきますね...
![KeyBoardwithID.png](https://qiita-image-store.s3.amazonaws.com/0/284083/13975560-a3ba-f3de-0803-caa26b3ec66b.png)
本来はUSキーボードと日本語キーボードを見比べてUSBのHID Usage IDを調べるのですがだるすぎるのでまとめました．Shiftと一緒に押すときは**+128**してください．

ASCIIの文字コード表はググれば出てきます．

**2019.06.27追記**
この一覧は，USレイアウトのキーボードのキートップだけが日本仕様に付け替えられている場合を想定しています(僕のThinkPad X260がそうだったので)．もしもうまくいかなかったら自力でやってみてください．

###Internationalキー問題
ここでひと工夫が必要なのですが，`scancode-ascii-table.h`を修正する際，ASCII 92, 95, 124はそれぞれIDを素直に135, 263, 265にするとあとでうまくいかなくなってしまいます．というのも**それらこそが\\_|**にあたり，入力にはInternationalに指定された**日本語キーボード特有のキー**を使用するからです．

この例外なキーたちがDigiKeyboardと相性がバッチバチに悪いらしいので，ここではそれぞれのIDを**104, 105, 106**に設定しておきます(別のキーだと思い込ませます)．

ここまでで，今まで`print()`関数で記号が誤って出力されていた問題は解決しています．`scancode-ascii-table.h`を上書き保存して，試しにいろんな記号を出力させてみてください．

###次にDigiKeyboard.hを
今度は出力不可能な文字をなくしにかかります．`DigiKeyboard.h`を開きます．

まずここです．40行目からHIDのレポートディスクリプタの中身が書いてあります．

~~~c++
PROGMEM char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = { /* USB report descriptor */
  0x05, 0x01,                    // USAGE_PAGE (Generic Desktop) 
  0x09, 0x06,                    // USAGE (Keyboard) 
  0xa1, 0x01,                    // COLLECTION (Application) 
  0x05, 0x07,                    //   USAGE_PAGE (Keyboard) 
  0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl) 
  0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI) 
  0x15, 0x00,                    //   LOGICAL_MINIMUM (0) 
  0x25, 0x01,                    //   LOGICAL_MAXIMUM (1) 
  0x75, 0x01,                    //   REPORT_SIZE (1) 
  0x95, 0x08,                    //   REPORT_COUNT (8) 
  0x81, 0x02,                    //   INPUT (Data,Var,Abs) 
  0x95, 0x01,           //   REPORT_COUNT (simultaneous keystrokes) 
  0x75, 0x08,                    //   REPORT_SIZE (8) 
  0x25, 0x65,                    //   LOGICAL_MAXIMUM (101) 
  0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated)) 
  0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application) 
  0x81, 0x00,                    //   INPUT (Data,Ary,Abs) 
  0xc0                           // END_COLLECTION 
};
~~~

下から3段目，`USAGE_MAXIMUM(Keyboard Application)`と書いてあるところですが，このままではInternationlキーたちが使えるキーに含まれません．
以下のように書き直します．

~~~c++
  0x29, 0x8c,                    //   USAGE_MAXIMUM (Keyboard International6)
~~~
日本語キーボードではInternational6というキーまでが搭載されうる(と思う)ので，そのUsage IDまでひろげておきます．

次！！ずーっと下がって199行目から，`print()`関数で受け取った文字列に`sendKeyStroke()`をあてていく行程にかかわる部分(だと思うの)ですが，

~~~c++
size_t write(uint8_t chr) {
    uint8_t data = pgm_read_byte_near(ascii_to_scan_code_table + (chr - 8));
    sendKeyStroke(data & 0b01111111, data >> 7 ? MOD_SHIFT_RIGHT : 0);
    return 1;
  }
~~~
ここに，先ほど104, 105, 106にしておいた理由があります．
というのも，例えば\のIDを135にすると**\\と大文字DのIDが同じ**になってしまい，Dも\もDと出力されてしまいます．

このような不都合を回避するために，まず`scancode-ascii-table.h`で当該のキーのIDを「表示されないキーで」「できるだけ普通のPCのキーと被りにくい」**ダミーのIDに差し替えます．**ここではF13, F14, F15キーに充てられている104, 105, 106を使いました．

次に，上の箇所で「もしダミーにしたIDが来たらそれは\\（や_や|）だぞ！」という条件分岐を設定し，ここで初めて**正しいキーのIDを`sendKeyStroke()`に渡してやります．**(すなわちレポートディスクリプタのUsage Maximumを広げた時点で，もしこの条件分岐を設定しなくても，スケッチ内で`print("\");`の代わりに`sendKeyStroke(135);`を使えば\の出力は可能です．)

僕はこのようにしました．

~~~c++
size_t write(uint8_t chr) {
    uint8_t data = pgm_read_byte_near(ascii_to_scan_code_table + (chr - 8));

	switch (data){
	case 104:
		sendKeyStroke(135);
		break;
	case 105:
		sendKeyStroke(135, MOD_SHIFT_RIGHT);
		break;
	case 106:
		sendKeyStroke(137, MOD_SHIFT_RIGHT);
		break;
	default:
		sendKeyStroke(data & 0b01111111, data >> 7 ? MOD_SHIFT_RIGHT : 0);
	}

    return 1;
  }
~~~
ヘッダファイルの修正点はこれで以上です．
これで一応僕のDigisparkくんは`print()`関数できちんと出力してくれるようになりました！

##失敗してしまったら
もし編集中に事故が起きて，取り返しのつかない感じになった場合は [こちら](https://github.com/digistump/DigisparkArduinoIntegration/tree/master/libraries/DigisparkKeyboard) .

#まとめ
以上は僕が趣味の範囲で考えた方法になりますので，もっとこうした方がいいだろ！それは邪道すぎるぞ！等ありましたらコメントにお願いします．いくらか皆様の参考になればと思います！