7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

農工大Advent Calendar 2024

Day 3

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

Last updated at Posted at 2024-12-02

導入

先日、初めて英字配列のキーボードを入手しました。
なにぶん初めてなものですから、「キーに書いてある記号と入力が一致しないんだろうな~」程度にしか考えていませんでした。
そんなある日、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.hKeyboard.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.cppKeyboard.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.hKeyBoard_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時間ぐらい…?)。

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

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

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

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?