Pro MicroのLEDをWindowsプログラムから制御する

概要

前回のライブラリの変更やスケッチの作成で、Pro Micro + キーパッドにキーボードLEDを実装できた。今回はWindowsプログラムからキーボードLEDを点けたり消したりしてみた。また、IMEの状態に応じたScrLock LEDの点灯/消灯も行ってみた。

A_PRO_MICRO_4x3_KEY_1
Pro Micro + KeyPad.

世の中には「お世話になっております。キーボード」という楽しい製品があるのだけど、今回作っているキーパッドで同じようなことをやってみることにした。キーパッド上のキーを押すと、挨拶文(らしきもの)を出力するところまでは同じなのだけど、ScrLock LEDの状態( IMEの状態と同期している )に応じて日本語と英語を出し分け、バイリンガル風挨拶キーパッドとした。

WindowsプログラムからのキーボードLEDの制御

SendInput() を使う

今回のWindows用プログラムの実装では、HIDデバイス連携ではなくキーストローク操作のエミュレーションを行う SendInput() というAPIを使った。以前こういうものを書いたときには keydb_event() というAPIを使っていたが、現在では SendInput() を使うそうな。

早い話、SendInput() を使ってVK_SCROLLやVK_NUMLOCK などのロックキーの仮想キーコードを1ストローク分送ってやると、キーボードからの操作と同様に入力システム全体に反映されるようだ。

とりあえず、C++でダイアログベースのシンプルなMFCアプリケーションを作成した。実行すると以下のようなウィンドウを表示する。今回作成したsend_keycode アプリケーションのプロジェクト全体は文末のリンクからダウンロードできる。

send_keycode
send_keycodeアプリケーション

NumLockボタンはNumLockキー、ScrLockボタンはScrLockキーに対応。ボタンをクリックすると、これらのキーを1ストロークした(1度押して離した)のと同じようになる。キーコードを送信する部分は以下のようになっている。

typedef enum { toggle = 1, on = 2, off = 3 } key_action_t;
void CSendkeycodeDlg::send_keycode(WORD vkey, key_action_t action) {
  if (action == on || action == off) {
    SHORT u = GetKeyState(vkey);
    if ((u & 1) && action == on)
      return;
    if (!(u & 1) && action == off)
      return;
  }
  INPUT input = { 0 };
  input.type = INPUT_KEYBOARD;
  input.ki.wVk = vkey;
  input.ki.dwFlags = KEYEVENTF_EXTENDEDKEY;
  SendInput(1, &input, sizeof(INPUT));
  input.ki.dwFlags = KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP;
  SendInput(1, &input, sizeof(INPUT));
}

vkeyに、VK_SCROLLとかVK_NUMLOCKを指定しておく。キーを押して離す(ストローク)操作をエミュレートするためには、KEYEVENTF_KEYDOWN( == 0)とKEYEVENTF_KEYUPを順に送る。

1回のストロークによるロック状態のトグル操作(toggle)なのか、ロックを掛ける(on)のか、外す(off)のか、をkey_action_t型の引数で指定している。onまたはoffのときは、 GetKeyState() で該当キーのロック状態を得て、その値に応じて動きを変えている。

IMEの状態を読む

チェックボックスのCheck IME statusにチェックを入れると、定期的に(WM_TIMERによる呼び出しで)IMEの状態を読みに行く。そして、IMEが有効かつ日本語(かな、カナ)ならばScrLockをロックし、無効または英字ならばScrLockをアンロックする。

IME状態取得については以下のようにした。このOnTimer() は約50msecに一度の頻度で呼び出される。

void CSendkeycodeDlg::OnTimer(UINT_PTR id) {
	CDialogEx::OnTimer(id);
	if (id != m_timer_id)
		return;
	GUITHREADINFO gti = { 0 };
	gti.cbSize = sizeof(GUITHREADINFO);
	GetGUIThreadInfo(NULL, &gti);
	HWND hwndime = ImmGetDefaultIMEWnd(gti.hwndFocus);
	if (!IsWindow(hwndime))
		return;
	bool status = false;
	if (::SendMessage(hwndime, WM_IME_CONTROL, IMC_GETOPENSTATUS, 0) == 1)
		status = (::SendMessage(hwndime, WM_IME_CONTROL, IMC_GETCONVERSIONMODE, 0) & 1) != 0;
	send_keycode(VK_SCROLL, status ? on : off);
}

GetGUIThreadInfo() と ImmGetDefaultIMEWnd() 使って現在アクティブなウィンドウに付随する IMEデフォルトウィンドウのハンドルを取得し、そこに WM_IME_CONTROL メッセージを投げることで状態を得る。この実装では、IMEが有効で日本語(かな、または、カナ)のときに変数statusがtrueになる。最後にstatusの値に応じてScroll Lockをオンまたはオフにしている。こうすることで、ScrLock LEDにもロック状態が反映される。

ふだん、Windowsストアアプリには縁がないのだけど、Microsoft Edge に開いたフォームにフォーカスがある状態でも意図通りにIMEの状態が正徳できた。この方法でなんとかなっているようである。プログラムの動作はx64版のバイナリで確認したが、x86版でもおそらく大丈夫だろう。

動作状況

このプログラムとPro Micro + キーパッドの動作を動画にした。Pro Microには前回作ったスケッチが入っており、ロックキーに応じてTXLEDおよびRXLEDが点灯/消灯する。

ボタン操作については見てのとおり。IMEの状態取得については、入力フォーカスをもつウィンドウの入力コンテキストに応じてIMEの状態が変わるので、メモ帳アプリを2つ開いておき、あらかじめ直接入力とかな入力にしておいた。

メモ帳アプリ間でフォーカスを移動すると、おのおの状態に応じてPro MicroのLEDが点いたり消えたりする。また、インジケータ領域のIMEのメニューに対する操作によっても、ScrLockの状態が変わることが分かる。

バイリンガル風挨拶スケッチ

send_keycode のIME監視機能を使えば、Pro Micro側でも現在日本語入力なのかそうでないのかが分かるようになった(実際はスクロールロック中かどうかが分かるだけなのだが)。これを使って、キーパッド上のキーを押した時に、日本語と英語で挨拶文を出し分けるスケッチを書いてみた。Arduino IDE 1.8.3でビルド。

#include <avr/pgmspace.h>
#include <Keyboard.h>
// A_PROMICRO_4x3_key_hid_greeting
const int NUM_KEYS = 12;
const  uint8_t scan_port[] = { 2, 3, 4};
const  uint8_t data_port[] = { 5, 6, 7, 8};

// LEDs
#define KEY_LED_NUMLOCK    1
#define KEY_LED_CAPSLOCK  2
#define KEY_LED_SCRLOCK   4
#define KEY_LED_COMPOSE   8
#define KEY_LED_KANA    0x10
// Lock Keys.
#define KEY_SCROLL_LOCK    0xcf
#define KEY_NUMLOCK    0xdb

#define KEY_HENKAN 0xf0 // 変換キー(Keyboard International4)
#define KEY_MUHENKAN 0xf1 // 無変換キー(Keyboard International5)
#define KEY_HIRAGANA 0xf2 // ひらがな (Keyboard International2) 0x88

uint8_t led_status = 0;
void led_func(uint8_t data) {
  led_status = data; 
}

void setup() {
  for(int i = 0; i < sizeof(scan_port); i++) {
    pinMode(scan_port[i], OUTPUT);
    digitalWrite(scan_port[i], HIGH);
  }
  for(int i = 0; i < sizeof(data_port); i++)
    pinMode(data_port[i], INPUT_PULLUP);
  Keyboard.releaseAll();
  HID().set_led_callback(led_func);
}

// 全キースキャンして、押下されているキーのビットを1にしたビットマップを返す。
int key_scan() {
  int key_pressed = 0;
  for(int col = 0; col < sizeof(scan_port); col++) {
    digitalWrite(scan_port[col], LOW);
    int row_data = 0;
    for(int row = 0; row < sizeof(data_port); row++) 
      row_data |= (digitalRead(data_port[row]) ? 0 : 8 >> row);
    key_pressed |= (row_data << (sizeof(data_port) * col));
    digitalWrite(scan_port[col], HIGH);
  }
  return key_pressed;    
}
const char en1[] PROGMEM = "Good Morning.";
const char en2[] PROGMEM = "Thank you and Good bye.";
const char en3[] PROGMEM = "Thank you always for your assistance.";
const char jp1[] PROGMEM = "ohayougozaimasu.";          // おはようございます。
const char jp2[] PROGMEM = "yorosikuonegaisimasu.";     // よろしくおねがいします。
const char jp3[] PROGMEM = "itsumoosewaninatteorimasu.";// いつもおせわになっております。

const char* const PROGMEM en[] = { en1, 0, 0, 0, en2, 0, 0, 0, en3, 0, 0, 0 };
const char* const PROGMEM jp[] = { jp1, 0, 0, 0, jp2, 0, 0, 0, jp3, 0, 0, 0 };

bool key_change(uint8_t code, bool on) {
  uint16_t pp = pgm_read_word(led_status & KEY_LED_SCRLOCK ? &jp[code] : &en[code]);
  if (pp) {
    if (!on)
      return true;
    Keyboard.print((const __FlashStringHelper*) pp);
    if (led_status & KEY_LED_SCRLOCK) {
      Keyboard.write(KEY_HENKAN);
      Keyboard.write(KEY_RETURN);
    }
    Keyboard.write(KEY_RETURN);
    return true;
  }
  uint8_t k = 0;
  if (code == 3)  
    k = KEY_SCROLL_LOCK;
  else if (code == 11)  
    k = KEY_NUMLOCK;
  if (k)
      on ? Keyboard.press(k) : Keyboard.release(k);  
  return false;
}

void loop() {
  static int last_key_pressed = 0;
  int key_pressed = key_scan();
  int change = key_pressed ^ last_key_pressed;
  if (change) {
    int mask = 1;
    uint8_t code;
    for(int i = 0; i < NUM_KEYS; i++) {
      if (change & mask)
        key_change(i, (key_pressed & mask) != 0);
      mask = mask << 1;          
    }
    last_key_pressed = key_pressed;
  }
  digitalWrite(LED_BUILTIN_TX, led_status & KEY_LED_NUMLOCK ? 0 : 1);
  digitalWrite(LED_BUILTIN_RX, led_status & KEY_LED_SCRLOCK ? 0 : 1);
  delay(50);
}

全般

今回のスケッチでは、SW1~SW12の内部キーコードを0~11とし、配列を引く時のインデックスとして使っている。

setup();key_scan(); については前と同じなのでこちらの投稿を参照のこと。loop() 内では前回スキャン時と変化のあったキーについて、key_change() を呼び出して挨拶文を出力する。key_change()には、キーコードとキーが押されたのか離されたのかを識別するためのパラメータを渡している。

キーパッドの上段の3つのキーにあたる、キーコード#0, #4, #8に挨拶文を割り当て、#3をScroll Lock、#11をNumLockとした。

挨拶文はPROGMEMに

挨拶文は文字列として持つ必要があるので、データ領域を圧迫しないよう、文字列実体 ( en1[]~jp3[] ) をプログラムメモリエリア(フラッシュメモリ、以下PGM)にもつことにした。そして、PGM内での各文字列のアドレスを、やはりPGMにおいたポインタ配列(en[]およびjp[])に格納するようにした。PGM領域にアクセスする関数を使うため、先頭で #include <avr/pgmspace.h>  を指定している。
このサイズのスケッチならばデータ領域もスカスカだからPROGMEM指定はなくてもいいのだけど、調子に乗って拡張していったときのことを考えてPGMに置くことにした。

void key_change(uint8_t code, bool on)

led_statusに反映されているスクロールロックLEDの状態に応じてポインタ配列を選択し、内部キーコードをインデックスとしてPGM内の文字列のアドレス(16ビット長)を pp = pgm_read_word() で得る。pp == 0 となる要素は、対象のキーが挨拶用ではないことを表している。

pp != 0 ならば、Keyboard.print((const __FlashStringHelper*) pp); によってキーボード入力として出力する。__FlashStringHelper をつけておくと、Printクラスの中で1バイトずつ pgm_read_byte() と write() が繰り返されることになるので、スケッチ側でデータをコピーする領域を確保したり strncpy_P() でコピーする必要がない。文字列出力は、キーの押下時(on == true ) のときにだけ行う。

スクロールロックされている(この場合、日本語になっている)ならば、文字列の出力後に変換キーをKeyboard::write() を使って1ストローク送ることでIMEによる変換を促し、KEY_RETURNも1ストローク送って確定している。そして最後に、日本語英語共にEnterキー(KEY_RETURN) を1ストローク送って改行させている。

日本語入力IMEの設定によっては、想定している動作と異なることがあるだろう。

文字列が定義されていないキー( pp == 0) については、キーコード#3ならばScroll Lockキーを、#11ならばNumLockキーを出力している。その他のキーには何も定義しなかった。

変換/無変換キーについて

上記のように、日本語の場合は文字列出力のあとで変換キーのストロークを送ってからリターンキーを送っている。そのために変換キー(および無変換キー)のコードを追加定義しようとしたが、前回書いたようにArduino のKeyboardライブラリでは、ファンクションキーや矢印キーなどの表示不能キーについて、HIDのUsage ID(キーコード)に136を加えるという独特の方法でキーコードが定義されている。変換キーと無変換キーに対応するUsage IDは、Keyboard International4(0x8a)とKeyboard International5(0x8b) なので136を加えると8ビット範囲に収まらない。しょうがないのでスケッチ内に定義を追加し、Keyboard.cppにまたちょっと手をいれることにした。半端な追加を繰り返すのもなんなので、そのうち全面的に書き直すことになると思うのだが。

Keyboard.cppの変更箇所

このスケッチ専用に、変換キーを0xf0、無変換キーを0xf1とした。

size_t Keyboard_::press(uint8_t k) 
{
uint8_t i;
#if 0
//変更前
  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));
...
#else
//変更後
  if (k >= 136) {			// it's a non-printing key (not a modifier)
    if (k == 0xf0) k = 0x8a  // HENKAN
    else if (k == 0xf1) k = 0x8b  // MU-HENKAN
    else k = k - 136;
  } else if (k >= 128) {	// it's a modifier key
    _keyReport.modifiers |= (1<<(k-128));
...
#endif
}

同じ変更を、size_t Keyboard_::release(uint8_t k) の同じような箇所に対しても行った。今回の変更を反映したKeyboard.cppをダウンロードするためのリンクを文末に用意した。

動作状況

先ほどの動画と同様に、入力コンテキストの異なるメモ帳で使ったときの様子を動画にした。IMEは「ローマ字かな入力」に設定してある。

最初の動画と同様に、send_keycodeの操作がLEDに反映される様子と入力コンテキストの異なるアプリケーションを選択したときのScrLock LEDの様子など。そして、キーパッドの上段3キーを、IMEが日本語になっている方のメモ帳(左側)と、英語になっている方(右側)とでそれぞれ押している。Pro Microが生成するキーストロークは、人間の打鍵に比べるととても高速です。

自分で見ているうちに、こんな挨拶を英語で言うのかねとか思い始めたのだけど、深く考えないことにした。再現される方がいらっしゃるなら、もうちょっと気の利いた文言を書いてください。

最初の動画のときより照明を追加してみたのだけど、色の再現が難しい。

きょうのまとめ

PCからHIDデバイス化したPro Microに何か情報を伝達するならば、Pro Micro側のアウトプットレポートを拡張するか、情報伝達専用にエンドポイントを追加した上でHIDデバイス連携機能を使うのが本筋と思う。今回は安易な実装としてLEDレポートを使ってPCからPro MicroにIMEの状態を伝達した。
Excelというソフトを使うとき、ScrLockキーはけっこうよく使う(入力対象セルを固定したまま、ワークシート全体を上下左右にスクロールする)から、ちょっと困ることがあるかもしれない。既存の機能に迷惑をかけない本筋のやり方についてはそのうちに。

Windows用のプログラムはVisual Studio 2017 Community版を使った。たまにしか使わないのでとまどうことが多いが、こういう有用なソフトウェアを無償で提供してくれていることに感謝です。
Arduinoスケッチの開発については、しょうしょうの不便さ(クラスのメンバや引数を教えてくれないとか、マクロの定義元を見せてくれないとか、インクルードファイルをサッと開けないとか)はあるものの、ビルドから書き込みまでサクッと動くArduino IDEが気に入って使っている。

秋月電子のキーパッドは十分に(?)活躍した。そろそろキーの数を増やしつつ実用性が欲しくなってきたので、次回はそのあたりの話になる予定です。

send_inputアプリケーション

MFC用のプロジェクト全体を収めた send_keycode.zip はこのリンクからダウンロードできますMITライセンス。プロジェクトファイルなどはVisual Studio 2017 Community版が自動的に作成したものなので、他のビルド環境で使えるかどうかは分かりません。

変更したKeyboardライブラリ

HID.cpp, HID.h, Keyboard.cpp, Keyboard.h を格納したzipファイル hid_and_keyboard.zip はこちらからダウンロードできます。前回の修正ファイルを入れたzipファイルに、今回の修正を追加し同じ名前のzipファイルとしました。前回のスケッチでも利用できます。

関連する投稿