ダイソーのBluetoothリモートシャッター(330円)を２ボタンBluetoothキーボードにしてみよう（Windows）

# はじめに
![remote-shutter.png](https://qiita-image-store.s3.amazonaws.com/0/203949/4945dd9a-6ee9-102d-b53b-a5336515575e.png)

このリモートシャッターを[前回](https://qiita.com/nori-dev-akg/items/96584d9591d329f9dcb2)は、RaspberryPi3 のシャットダウンボタンにしましたが、今回は Windows の Bluetooth 2ボタンキーボードにしてみようと。
色々ネットを探してみたのですが、Windows で使用している記事が無くて結構ハマりました。

**※ABShutter3の仕様が変わったらしく、2つのボタンで送信キーの差がなくなったようです。どちらのボタンを押しても同じになってしまいました。 1ボタンとしては使えます。2020/10/26更新**

![test1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/4b9273e1-776d-d7a6-3bf9-f7800da0d80c.gif)


# Bluetoothリモートシャッターとは
- そもそもこれは、スマホのシャッター用ボタン
- スマホとBluetooth接続して使う
- iOSボタンとAndroidボタンの２つある
- 使わないと自動的に数分で電源OFFになる
- ボタン電池のRS2032
- HIDキーボードとして認識される

# 用途
キーボードフックを使って、別のキーにねじ曲げてしまおう！

# ハード
- Bluetoothリモートシャッター(AB Shutter3)（ダイソー 324円）
- Windows10 Pro 1909

# 開発用IDE
- [Visual Studio Community 2019 (無料)](https://visualstudio.microsoft.com/ja/downloads/)

## ソース
作成したソース：[ABShutter3Key(github)](https://github.com/nori-dev-akg/ABShutter3Key)

# Windowsとペアリング
このリモートシャッターは、Windowsとペアリングすれば HIDキーボードとして認識される。ドライバーなどのインストールは必要ない。

**リモートシャッターの電源をOFFにしておく**
`Windows10 > 設定 > デバイス > Bluetoothとその他のデバイス`
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/a833474d-67cc-9548-8a7f-bec5030b2098.png" width=50%>
`＋Bluetooth またはその他のデバイスを追加する` を押下
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/0ad63c5e-4298-0131-12d0-ecf80b151ac5.png" width=40%>
`Bluetooth` を押下。**リモートシャッターの電源をONにする**。
**通信距離が短いので、近くで行う。1m以内**。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/555b3dfa-44eb-697e-4fc5-42244e9083fd.png" width=40%>
`AB Shutter3` を押下
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/b47f693f-36e4-915b-eeae-ef93badb4863.png" width=40%>

##ペアリング済み：
先程の`Windows10 > 設定 > デバイス > Bluetoothとその他のデバイス`画面に戻ると以下のようになっているはず。もしくは「接続済み」。
デバイスがWindowsに登録された状態。次回以降は、リモートシャッターの電源を入れるとすぐに接続される。この状態はまだ使用できない。リモートシャッターは数分使用しないと電源がOFFになりPCとはこの状態になる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/25ed4b3b-1194-3f21-6e4a-0bbdb11a512a.png" width=40%>

##接続済み：
ペアリングが完了して、リモートシャッターと通信できる状態にある。この状態でないと使用できない。
もしリモートシャッターの電源がONで「ペアリング済み」状態ならiOSボタンを押してみよう「接続済み」になるはず。もしならなけれは、一度電源をOFF→ONにすればいい。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/700de9f4-b9f4-dca7-3e70-64c8c5c28256.png" width=40%>

## 接続動作確認
「接続済み」の状態で iOS/Androidボタンを押して、デスクトップ画面の左上に下図のようボリューム表示が出てれくればOK。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/674c2726-aa7c-5d5c-5f3a-222d77d6888f.png" width=10%>

# 開発メモ
「能書きはいいから使いたんじゃ」というひとは **[使い方](#使い方)**へ

## iOS/Androidボタン
- iOSボタンは、キー`VK_VOLUME_UP` にマッピングされている。なので先程のボリューム表示される。
- Androidボタンは、キー`VK_ENTER` `VK_VOLUME_UP` にマッピングされている。`VOLUME_UP` の前に `ENTER` が送信されるので、エディタを開いた状態で押すと、改行とボリューム表示がされる。

今回はこのキー`VK_ENTER` `VK_VOLUME_UP`をねじ曲げて、別のボタンにしてしまおうということです。

**※新しいモノだと仕様が変わったらしく、Androidボタンで`VK_ENTER`キーが送信されなくなっています。2020/10/26更新**

## RawInput API
キー`VK_ENTER` `VK_VOLUME_UP` は、当たり前だが通常使用しているキーボードからも送信さる。なので通常のキーボードとリモートシャッターの判定をしないといけない。判定方法として **RawInput API** を使用する。**RawInput API**を使うと HID デバイスの一覧と入力されたHIDデバイスが取得できる。だが**RawInput API**では入力されたキーの変更は一切できない、参照のみ。

##VID
HIDデバイスには、VID(ベンダーID)とPID(プロダクトID)があって、これを**RawInput API**で取得できるのでリモートシャッターの判定として使う。以下がリモートシャッターのVIDとPID（デバイスドライバ）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/a5ccf4bb-a6f6-9407-d7d1-0adff70f668a.png" width=70%>

違うようであれば `setting.ini` を変更する。

```:setting.ini
[ABShutter3Key]
VID="_VID&02248a_PID&8266"
```

##システムフックAPI SetWindowsHookEx()
昔からあるWindowsシステムAPIで user32.dll にあります。これを使うと色々なイベントをフックできる。[SetWindowsHookExW function(winuser.h)](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw)
今回はこの中でも、 `WH_KEYBOARD` と `WH_KEYBOARD_LL` を使う。昔は `WH_KEYBOARD` しかなかったと思う。`WH_KEYBOARD_LL` は 低レベルキーボードフックと言って `WH_KEYBOARD` よりも低レベルでフックするよう。どちらもグローバルフック(システム全体にフック)することは可能だが、`WH_KEYBOARD_LL` は実行ファイルだけでグローバルフックできるのに対し、`WH_KEYBOARD` は DLL にフックプロシージャを書かないといけない決まりがある。**そして両フックともフックプロシージャで 戻り値 を 0 以外にすると残りのフックチェーンにメッセージが渡らなくなり、その入力キーをブロックできるはずなのだが `VK_VOLUME_UP` が `WH_KEYBOARD` ではブロックできない・・・。仕方がないので両方使う**

低レベルである`WH_KEYBOARD_LL`のイベントが先に来る。さきほどの **RawInput API** を含めると
`WH_KEYBOARD_LL` → **RawInput API** → `WH_KEYBOARD`
（※あくまでも私の環境での実測値。公式ではないので、動かなかった場合はゴメンナサイ。）

- `VK_ENTER` は **RawInput API** で HIDデバイス判定をして、`WH_KEYBOARD`でその入力がリモートシャッターならブロックする
- `VK_VOLUME_UP` は `WH_KEYBOARD_LL` でブロックする

ということは、`VK_VOLUME_UP` は HIDデバイス判定をしていないので、すべてのHIDデバイスでブロックされてしまうことになる。しかし、どのみち **RawInput API** で `VK_VOLUME_UP` は判定できないようだった。`VK_VOLUME_UP`時のHIDデバイスハンドルがNULLなのだ。
まぁ `VK_VOLUME_UP` キーなんて普段使わないしブロックされても特に日常で困ることはないのでOKとする。

##SendKeys
キーボードフックして、`VK_ENTER` `VK_VOLUME_UP` キーをブロックしたタイミングで、代替したいキー（もしくは文字列）を送信する。送信は WindowsAPIの[SendInput()](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput) を使うのだが、ライブラリとして完成度の高い[こちら](https://www.codeproject.com/Articles/6819/SendKeys-in-C)を使わせて頂いた。

#使い方
1. [ここから(ABShutter3Key)](https://github.com/nori-dev-akg/ABShutter3Key)一式をダウンロードする。`(Code > Download Zip)`
2. ダウンロード＞解凍後に、`x64/Release` フォルダにある `settings.ini` をエディタで開く
3. ボンタンに割り当てたいキーを iOS="任意のキー"、Andoroid="任意のキー" に記述し保存(※)
4. `x64/Release` フォルダにある `ABShutter3Key.exe` を実行する
5. iOS/Androidボタンを押して任意のキーが出力できたら成功
6. 終了する場合は、タスクトレイ（システムトレイ）の `ABShutter3Key`アイコンをクリックして `EXIT`をクリック
7. 任意のキーが出力できずに、ボリューム表示されるようならばキーボードフックに失敗してかも。 `ABShutter3Key` を再起動する
8. それでもダメなら、`setting.ini` の [VIDを確認する](#vid)
8. 任意のキーが出力できずに、ボリューム表示もされないようならば  「[接続済み：](#接続済み)」か再確認する

```:setting.ini
[ABShutter3Key]
VID="_VID&02248a_PID&8266"
iOS="iOS{ENTER}"
Android="Android{ENTER}"
```

(※)キーの記述は、一文字だけであれば "A"。その他は

| KEY | CODE | 記述例 |
|:-:|:-:|:-:|
| WIN  | @  | @d ：デスクトップ表示  |
| SHIFT  | +  | +2 ："ダブルクォーテーション  |
| CTRL  | ^  | ^c ：コピー  |
| ALT  | %  | %{TAB} ：タスク切換え  |

なお、[こちら(SendKeys in C++)](https://www.codeproject.com/Articles/6819/SendKeys-in-C) の記述が使用可能。ただし、DELAYは使えない。
画面ロック(WIN+L) は {LOCKWORKSTATION}。

(※) `settings.ini`を編集したら 再起動が必要

#最後に
- 私の持っているリモートシャッターが数年前のモノなので [VID](#vid) などが変わっている可能性大です。
- 2つ持っているのですが、個体差があります。
- チャタリングが凄いです(特にAndroidボタン)、連続して表示されるかもしれません。
- SendKeysのDELAYコマンドが使えないので、そのうち使えるように改良したい。
- チャタリング対応もソフト的にしたい。

もし「動いたよ！」とかあればコメントして頂けると励みになります。
良い IoT ライフを！

#SpecialThanks!!
[SendKeys-in-C (CodeProject)](https://www.codeproject.com/Articles/6819/SendKeys-in-C)
[Combining Raw Input and keyboard Hook to selectively block input from multiple keyboards (CodeProject)
](https://www.codeproject.com/Articles/716591/Combining-Raw-Input-and-keyboard-Hook-to-selective)