導入
先日、初めて英字配列のキーボードを入手しました。
なにぶん初めてなものですから、「キーに書いてある記号と入力が一致しないんだろうな~」程度にしか考えていませんでした。
そんなある日、typstを用いてレポートを作成するとあることに気が付きます。
アンダーバーを入力するキーが…無い!?!?
対処方法の検討
軽く検索をかけると、「-」キーとシフトキーを同時に押すと「_」が入力されるとのことでしたが、試したところ「=」が入力されました。
enのキーボードなのでjp配列として認識させる?などの解決方法があったようですが、あえてそちらは試さず、、、
そこで「_」が入力できるキーを自作しようと思いついたのです!(天啓)
使用部品
・seeeduino xiao (Arduinoの仲間)
秋月電子通商での販売リンク
・アーケードコントローラーのボタン
・ボタンをはめ込む部品(3Dプリントした)
・マ〇ドナルドでドリンク買うと付属するやつ
これらを組み合わせるとこんな感じ。
キーの完成です!これはキーです。これはキーですね。
いざ実践…のはずが
Arduino IDEを用いて、次のプログラムを書き込みました。
#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回押す機能です。
なので、このプログラムを使用すれば「_」だけが入力できる自作キーが完成!…のはずでした。
実際打ち込まれたのは「=」でした。
原因究明
Keyboard.write()の引数に16進数を入力し、なんのキーが押されるか調べた結果、以下の通りになりました。
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コード表を見てみましょう。(農工大のサイト から引用)
ASCIIコード表を見ると、「_」は16進数で5Fであることがわかります。先程の調査した表で5Fに該当するのは「=」です。したがって、作成したプログラムに問題があるわけではなく、ライブラリに何か秘密がありそうです。
ライブラリを読む
私の場合では、C:\Users\user名\AppData\Local\Arduino15\libraries\Keyboard
というフォルダにライブラリの本体であるKeyboard.h
とKeyboard.cpp
ファイル、その他のファイルが入っていました。
中身を読んでいきましょう。
まず、先程のKeyboard.write()
関数はKeyboard.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()
関数を読みます。
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
という構造体(クラス?)にまとめて送信することにより、キー入力を行っているようです。
ところで、上記のコードのこの部分が気になります。
} 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
というファイルで定義されていました。
#define SHIFT 0x80
#define ALT_GR 0xc0
#define ISO_KEY 0x64
#define ISO_REPLACEMENT 0x32
つまり、文字コードは0x00~0x7F
の範囲で想定されていて、最上位ビットである0x80
が立っていればシフトキーも同時に入力する必要がある、ということです。
あとでわかりますが、ここが最大のつまづきポイントでした。
重要なのは文字コードは0x00~0x7F
の範囲で想定されているという部分です。
最後に、Keyboard.h
というファイルを読みます。
最後の方に、このような記述があります。
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
があるので読みます。
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()
関数だということがわかったので、この関数を「引数が何であってもアンダーバーを入力する関数」に変更しちゃいましょう(ダメ)
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()
に変更します。
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
}
今回の場合ではアンダーバーを打ちたいだけなのでキーマップは不要です。(依存を解消する意味もあり、)次のように空のキーマップを作っておき、不要な部分を消します。
+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
も変更します。
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
に戻り、次の部分を変更します。
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ボードに次のコードを書き込みます。
#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;
}
}
}
いざ、ボタンを連打すると…?
やった!念願のアンダーバーが入力できたぞ!
根本的な解決策
私の解決策ではアンダーバーがが入力可能になるだけであり、本質的な解決とは言えないでしょう。
そこで、この現象について検索してみると、すでに原因と解決策を提示してくださっている方がいらっしゃいましたので、紹介させていただきます。
以下は引用です。
Arduino LeonardoでUSBキーボードエミュレーションを行う場合、IDE付属のKeyboardライブラリを使うと記号が化けたり、「\」「|」「半角/全角キー」などの打てない文字・キーがあったりします。Keyboardライブラリは101キーボードをエミュレートするため、106/109キーボードにしかない文字やキーを打てず、キーバインドの異なる記号は化けてしまうのです。
こちらの方が作成された2つのファイルをライブラリとして使用すると、正しく「_」が入力できるようになります。
最後に
今回の記事を作成するにあたり、初めて既存のライブラリを読む作業を行いました。
ビット演算を駆使してうまいこと動作させているライブラリで、込められた意図を察するまでにかなり時間がかかってしまいました(合計6時間ぐらい…?)。
完璧な解決方法を自力で作ることはできませんでしたが、当初の目的である「アンダーバーが打てるキーを作る」は達成できたので感無量です。動けばよかろうなのだ
同様の症状に見舞われている方の一助になれば幸いです。
ここまで読んでいただき、ありがとうございました!