M5Stick-Cネタです。せっかく入手したので、活用しましょう。
M5Stick-Cに、複数のログインパスワードを記憶させて、パスワード地獄から解放されましょう。また、キーボードとして認識させて、パスワードをPCに自動入力させましょう。
なぜそれができるか。
- M5StickCにBLEがあるため、HOGP (HID-over-GATT Profile)、すなわちHIDのBLEデバイスにすれば、ワイヤレスキーボードとしてふるまうことができる。
- M5StickCで採用しているCPU ESP32の不揮発メモリに保存する機能があるので、電源を切っても、パスワードを記憶してくれる。
- M5StickCにLCDとボタンがあるので、複数のパスワードを切り替えることができる。
- M5StickCにはバッテリとボタンがついているので、有線ケーブルで給電することなく、無線でボタン一つでパスワード入力することができる。
そしてなにより、以下のような有用な情報を提供してくださる有志の方々がいる。
・ESP-WROOM-32のセットアップについて
https://trac.switch-science.com/wiki/esp32_setup
・ESP32_HID.ino
https://gist.github.com/sabas1080/93115fb66e09c9b40e5857a19f3e7787
・Arduino-ESP32 Preference ライブラリ <不揮発性メモリへの読み書き>
https://qiita.com/T-YOSH/items/f388b4d7cbc829829aae
※ 念のためですが、パスワードが紛失したり漏洩したりしても、責任は負いません。
使い方編
実装内容に入る前に、気になる使い方から説明します。
M5Stick-Cにパスワードを登録
パスワードの登録は、USBケーブルでM5Stick-Cと接続するとでてくる仮想COMポートから、TeraTermなどのコンソールで入力します。
入力すると以下のようなメニューが表示されます。
Input Command Number
1: List
2: New
3: Update
4: Remove
5: Check
2を入力し、タイトルとパスワードの入力が促されますので入力すると、不揮発メモリに保存されます。
たとえば、タイトルとして「Test」、パスワードとして「1234」と入力します。
保存されたパスワードは、1を入力することで、パスワードと一緒に入力したタイトルが一覧で表示されます。
3のUpdateは、パスワードを更新します。
4のRemoveは、パスワードを削除します。
5のCheckは、保存されているパスワードが、入力するパスワードと合っているかを確認します。
PCにM5Stick-CをBluetoothキーボードとして登録
PCにパスワードを入力させる前に、M5Stick-CをBluetoothキーボードとしてペアリングしておく必要があります。
Bluetoothまたはその他のデバイスを追加するの「+」ボタンを押下します。
Bluetoothを選択します。
そうすると、「Password-Reminder」という名前のデバイスが見つかります。
それを選択すると、PIN入力画面になります。
一方の、M5Stick-CのLCDには以下のように表示されているかと思います。
PIN
12345678
このPINをPCに表示されたPIN入力画面に入力し接続ボタンを押下します。(PINの値は毎回違います)
これで、「マウス、キーボード、ペン」のところに、Password-Reminderが増え、接続済みになっているのがわかります。
PCにM5Stick-Cからパスワードを入力する。
LCDには以下のようになっていますでしょうか?
上段が複数パスワードを登録した場合のためのインデックス番号、下段がパスワード登録したときのタイトルです。
0
Test
もし以下のようになっていたら、まだパスワードを登録していないことを表しています。
Not Found
それでは、パスワードを入力する先として、例えば、適当にメモ帳を開きます。
メモ帳の適当な場所にカーソルを合わせた状態で、おもむろに、M5Stick-Cの表のボタン(M5と書いているところ)を押下します。
そうすると、「1234」と入力されましたでしょうか?!
これが、BluetoothキーボードとしてつながっているM5Stick-Cから入力されたものになります。
もし複数のパスワードを記録していた場合には、右わきのボタンを押すと、インデックス番号とタイトルとともに表示が切り替わります。
同じように、表のボタンを押下すると、選択されたインデックス番号のパスワードがメモ帳に入力されます。
使い勝手はいかがでしょうか。
実装編
環境設定と実装をしていきます。
Arduinoのセットアップ
それでは、ソースをコンパイルするための環境をセットアップしていきます。
ESP32の機能をフルに使うので、Arduinoにarduino-esp32をセットアップします。
Arduino IDEから、「追加のボードマネージャーのURL」に https://dl.espressif.com/dl/package_esp32_index.json を追加します。
それから、ボードマネージャから「esp32」と入力すると表示される「esp32 by Espressif Systems」をインストールします。(以下の画像はインストール済みの状態)
プロジェクトの準備
PCにM5Stick-Cを接続しておきます。
Arduino IDEから新規ファイルを作成します。
「ツール」→「ボード」から「M5Stick-C」を選択します。ここらへんは、ボードマネージャとライブラリマネージャよりM5StickCがインストール済みの前提です。
まだの場合は以下もご参考にしてください。
M5Stick-CでJsonをPOSTする
シリアルポートの番号は、接続されているM5Stick-Cのものを選択しておきます。
実装
以下がソースコードです。むちゃくちゃ長いです!
#include <M5StickC.h>
#include <Preferences.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include "BLE2902.h"
#include "BLEHIDDevice.h"
#include "HIDTypes.h"
#include "HIDKeyboardTypes.h"
// 不揮発メモリアクセス用
Preferences pref;
/*
* パスワード管理
*/
enum COMMAND_STATE {
TOP, /* トップメニュー */
NEW_TITLE, /* 新規(タイトル入力) */
NEW_VALUE, /* 新規(パスワード入力) */
UPDATE_SELECT, /* 更新(選択) */
UPDATE_VALUE, /* 更新(新パスワード入力) */
REMOVE_SELECT, /* 削除(選択) */
CHECK_SELECT, /* 確認(選択) */
CHECK_VALUE /* 確認(パスワード入力) */
};
enum COMMAND_STATE state = TOP; /* メニューの状態 */
#define MAX_TITLE_LENGTH (16 + 1) /* タイトルの最大長 */
#define NUM_OF_TITLE 10 /* 保存可能なタイトル数 */
char title_list[MAX_TITLE_LENGTH * NUM_OF_TITLE] = { 0 }; /* タイトルリスト(オンメモリ) */
char input_buf[255]; /* シリアル入力バッファ */
unsigned char buf_index = 0; /* シリアル入力バッファのポインタ */
char backup_buf[255]; /* シリアル入力バッファのバックアップ */
unsigned char backup_number; /* シリアル入力値(1文字)のバックアップ */
// タイトルの削除
void remove_title(unsigned char index){
memmove(&title_list[index * MAX_TITLE_LENGTH], &title_list[(index + 1) * MAX_TITLE_LENGTH], (NUM_OF_TITLE - (index + 1)) * MAX_TITLE_LENGTH);
memset(&title_list[(NUM_OF_TITLE - 1) * MAX_TITLE_LENGTH], '\0', MAX_TITLE_LENGTH);
}
// タイトルの追加
long add_title(const char *title){
unsigned char len = strlen(title);
if( len == 0 || len > (MAX_TITLE_LENGTH - 1) )
return -1;
for( unsigned char i = 0 ; i < NUM_OF_TITLE ; i++ ){
if( title_list[i * MAX_TITLE_LENGTH] == '\0' ){
strcpy( &title_list[i * MAX_TITLE_LENGTH], title );
return i;
}
}
return -1;
}
// タイトルリストの表示
void print_title_list(void){
for( unsigned char i = 0 ; i < NUM_OF_TITLE ; i++ ){
if( title_list[i * MAX_TITLE_LENGTH] == '\0' )
return;
Serial.print("[");
Serial.print(i);
Serial.print("] ");
Serial.println(&title_list[i * MAX_TITLE_LENGTH]);
}
}
// タイトルの取得
char* get_title(unsigned char index){
if( title_list[index * MAX_TITLE_LENGTH] == '\0' )
return NULL;
return &title_list[index * MAX_TITLE_LENGTH];
}
/*
* シリアル入力処理
*/
enum SERIAL_MODE {
CHAR, /* 1文字入力待ち */
CHARED, /* 1文字入力完了 */
BUFFERING, /* 文字列入力待ち */
BUFFERED, /* 文字列入力完了 */
ABORT /* 中断(Ctrl-C) */
};
enum SERIAL_MODE mode = CHAR; /* シリアル入力処理の状態 */
enum SERIAL_MODE process_serial(void);
// シリアル受信の処理
enum SERIAL_MODE process_serial(void){
if( mode == CHAR ){
if( Serial.available() > 0 ){
unsigned char c = Serial.read();
if( c == 0x03 ){
mode = ABORT;
return mode;
}
input_buf[0] = c;
input_buf[1] = '\0';
mode = CHARED;
return mode;
}
}else{
if( mode == BUFFERING ){
while( Serial.available() > 0 ){
unsigned char c = Serial.read();
if( c == 0x03 ){
mode = ABORT;
return mode;
}
input_buf[buf_index] = c;
if( c == '\r' || buf_index >= (sizeof(input_buf) - 1) ){
input_buf[buf_index] = '\0';
mode = BUFFERED;
return mode;
}
buf_index++;
}
}
}
return mode;
}
/*
* シリアル表示処理
*/
// 次への状態遷移とコンソール表示
void prompt(void){
if( state == TOP ){
Serial.println("");
Serial.println("Input Command Number");
Serial.println(" 1: List");
Serial.println(" 2: New");
Serial.println(" 3: Update");
Serial.println(" 4: Remove");
Serial.println(" 5: Check");
mode = CHAR;
}else if( state == NEW_TITLE ){
Serial.println("Input Title");
buf_index = 0;
mode = BUFFERING;
}else if( state == NEW_VALUE ){
Serial.println("Input Password");
buf_index = 0;
mode = BUFFERING;
}else if( state == UPDATE_SELECT ){
Serial.println("Input Index Number");
mode = CHAR;
}else if( state == UPDATE_VALUE ){
Serial.println("Input Password");
buf_index = 0;
mode = BUFFERING;
}else if( state == REMOVE_SELECT ){
Serial.println("Input Index Number");
mode = CHAR;
}else if( state == CHECK_SELECT ){
Serial.println("Input Index Number");
mode = CHAR;
}else if( state == CHECK_VALUE ){
Serial.println("Input Password");
buf_index = 0;
mode = BUFFERING;
}
}
/*
* BLEデバイス処理
*/
BLEHIDDevice* hid;
BLECharacteristic* input;
BLECharacteristic* output;
bool connected = false;
class MyCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer){
connected = true;
BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
desc->setNotifications(true);
}
void onDisconnect(BLEServer* pServer){
connected = false;
BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
desc->setNotifications(false);
}
};
// ペアリング処理用
class MySecurity : public BLESecurityCallbacks {
bool onConfirmPIN(uint32_t pin){
return false;
}
uint32_t onPassKeyRequest(){
Serial.println("ONPassKeyRequest");
return 123456;
}
void onPassKeyNotify(uint32_t pass_key){
// ペアリング時のPINの表示
Serial.println("onPassKeyNotify number");
Serial.println(pass_key);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(2);
M5.Lcd.println("PIN");
M5.Lcd.println(pass_key);
}
bool onSecurityRequest(){
Serial.println("onSecurityRequest");
return true;
}
void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl){
Serial.println("onAuthenticationComplete");
if(cmpl.success){
// ペアリング完了
Serial.println("auth success");
print_screen();
}else{
// ペアリング失敗
Serial.println("auth failed");
}
}
};
// BLEデバイスの起動
void taskServer(void*){
BLEDevice::init("Password-Reminder");
BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
BLEDevice::setSecurityCallbacks(new MySecurity());
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyCallbacks());
hid = new BLEHIDDevice(pServer);
input = hid->inputReport(1); // <-- input REPORTID from report map
output = hid->outputReport(1); // <-- output REPORTID from report map
std::string name = "Poruruba";
hid->manufacturer()->setValue(name);
hid->pnp(0x02, 0xe502, 0xa111, 0x0210);
hid->hidInfo(0x00,0x02);
BLESecurity *pSecurity = new BLESecurity();
// pSecurity->setKeySize();
// pSecurity->setAuthenticationMode(ESP_LE_AUTH_NO_BOND); // NO Bond
// AndroidではうまくPIN入力が機能しない場合有り
pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
pSecurity->setCapability(ESP_IO_CAP_OUT);
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
const uint8_t report[] = {
USAGE_PAGE(1), 0x01, // Generic Desktop Ctrls
USAGE(1), 0x06, // Keyboard
COLLECTION(1), 0x01, // Application
REPORT_ID(1), 0x01, // Report ID (1)
USAGE_PAGE(1), 0x07, // Kbrd/Keypad
USAGE_MINIMUM(1), 0xE0,
USAGE_MAXIMUM(1), 0xE7,
LOGICAL_MINIMUM(1), 0x00,
LOGICAL_MAXIMUM(1), 0x01,
REPORT_SIZE(1), 0x01, // 1 byte (Modifier)
REPORT_COUNT(1), 0x08,
HIDINPUT(1), 0x02, // Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position
REPORT_COUNT(1), 0x01, // 1 byte (Reserved)
REPORT_SIZE(1), 0x08,
HIDINPUT(1), 0x01, // Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
REPORT_COUNT(1), 0x06, // 6 bytes (Keys)
REPORT_SIZE(1), 0x08,
LOGICAL_MINIMUM(1), 0x00,
LOGICAL_MAXIMUM(1), 0x65, // 101 keys
USAGE_MINIMUM(1), 0x00,
USAGE_MAXIMUM(1), 0x65,
HIDINPUT(1), 0x00, // Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
REPORT_COUNT(1), 0x05, // 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
REPORT_SIZE(1), 0x01,
USAGE_PAGE(1), 0x08, // LEDs
USAGE_MINIMUM(1), 0x01, // Num Lock
USAGE_MAXIMUM(1), 0x05, // Kana
HIDOUTPUT(1), 0x02, // Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
REPORT_COUNT(1), 0x01, // 3 bits (Padding)
REPORT_SIZE(1), 0x03,
HIDOUTPUT(1), 0x01, // Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
END_COLLECTION(0)
};
hid->reportMap((uint8_t*)report, sizeof(report));
hid->startServices();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->setAppearance(HID_KEYBOARD);
pAdvertising->addServiceUUID(hid->hidService()->getUUID());
pAdvertising->start();
hid->setBatteryLevel(7);
// Serial.println("Advertising started!");
delay(portMAX_DELAY);
};
/*
* LCD表示処理
*/
unsigned char current_index = 0xff; // 現在選択中のタイトルの番号。初期は未選択状態
// M5StickCのLCD表示
// 現在選択中の番号とタイトルの表示
void print_screen(void){
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(3);
char *title;
if( current_index == 0xff ){
M5.Lcd.println("");
title = "not found";
}else{
M5.Lcd.println(current_index);
title = get_title(current_index);
}
M5.Lcd.setTextSize(2);
M5.Lcd.println(title);
}
/*
* Arduinoメイン処理
*/
void setup() {
M5.begin();
M5.IMU.Init();
M5.Axp.ScreenBreath(9);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.println("[M5StickC]");
delay(1000);
M5.Lcd.println("start Serial");
Serial.begin(9600);
Serial.println("Starting Password-Reminder!");
M5.Lcd.println("start BLE");
// BLEデバイスの起動処理の開始
xTaskCreate(taskServer, "server", 20000, NULL, 5, NULL);
// 不揮発メモリライブラリの初期化
pref.begin("password_list", false);
// 不揮発メモリからタイトルリストの読み出し
pref.getBytes("title_list", title_list, sizeof(title_list));
// デフォルト(インデックス=0)のタイトルの存在確認
if( get_title(0) != NULL )
current_index = 0;
// LCDの表示
print_screen();
}
void loop() {
M5.update();
// ButtonBが押されたとき
if( M5.BtnB.wasReleased() ){
char* title = NULL;
if( current_index != 0xff ){
// いずれかのタイトルが選択されている状態の場合
current_index++; // 次のタイトルへ
title = get_title(current_index);
}
if( title == NULL ){
title = get_title(0);
if( title == NULL )
current_index = 0xff;
else
current_index = 0;
}
// LCD表示の更新
print_screen();
}
// ButtonAが押されたとき
if( M5.BtnA.wasReleased() ){
if(connected){
// BLEキーボードとしてPCに接続されている状態の場合
if( current_index != 0xff ){
// いずれかのタイトルが選択されている状態の場合
char* title = get_title(current_index);
if( title != NULL ){
char value_buffer[255];
// 不揮発メモリからパスワードを読み出し
pref.getString( title, value_buffer, sizeof(value_buffer) );
// 1文字ずつHID(BLE)で送信
char *ptr = value_buffer;
while(*ptr){
KEYMAP map = keymap[(uint8_t)*ptr];
uint8_t msg[] = {map.modifier, 0x0, map.usage, 0x0,0x0,0x0,0x0,0x0};
input->setValue(msg, sizeof(msg));
input->notify();
ptr++;
uint8_t msg1[] = {0x0, 0x0, 0x0, 0x0,0x0,0x0,0x0,0x0};
input->setValue(msg1, sizeof(msg1));
input->notify();
delay(20);
}
}
}
}
}
// シリアル入力処理と受信完了後の処理
switch(process_serial()){
// 処理中断(Ctrl-C)
case ABORT:{
state = TOP;
prompt();
break;
}
// 1文字入力の完了時
case CHARED:{
Serial.print("->");
Serial.println(input_buf);
if( state == TOP ){
if( input_buf[0] == '1' ){
print_title_list();
state = TOP;
}else if( input_buf[0] == '2' ){
state = NEW_TITLE;
}else if( input_buf[0] == '3'){
state = UPDATE_SELECT;
}else if( input_buf[0] == '4'){
state = REMOVE_SELECT;
}else if( input_buf[0] == '5'){
state = CHECK_SELECT;
}else{
Serial.println("Invalid Input");
state = TOP;
}
}else if( state == UPDATE_SELECT ){
if( !(input_buf[0] >= '0' && input_buf[0] <= '9') ){
Serial.println("Invalid Input");
state = TOP;
}else{
backup_number = input_buf[0] - '0';
if( get_title(backup_number) == NULL ){
Serial.println("Not Found");
state = TOP;
}else{
state = UPDATE_VALUE;
}
}
}else if( state == REMOVE_SELECT ){
if( !(input_buf[0] >= '0' && input_buf[0] <= '9') ){
Serial.println("Invalid Input");
state = TOP;
}else{
backup_number = input_buf[0] - '0';
char *title = get_title(backup_number);
if( title == NULL ){
Serial.println("Not Found");
}else{
// タイトル・パスワードの削除、不揮発メモリからも削除
Serial.println(title);
pref.remove(title);
remove_title(backup_number);
pref.putBytes("title_list", title_list, sizeof(title_list));
Serial.println(" Removed");
}
state = TOP;
}
}else if( state == CHECK_SELECT ){
if( !(input_buf[0] >= '0' && input_buf[0] <= '9') ){
Serial.println("Invalid Input");
state = TOP;
}else{
backup_number = input_buf[0] - '0';
if( get_title(backup_number) == NULL ){
Serial.println("Not Found");
state = TOP;
}else{
state = CHECK_VALUE;
}
}
}else{
state = TOP;
}
prompt();
break;
}
// 文字列入力の完了時
case BUFFERED:{
if( state == NEW_TITLE ){
unsigned char len = strlen(input_buf);
if( len == 0 || len >= (MAX_TITLE_LENGTH - 1)){
Serial.println("Invalid Input");
state = TOP;
}else{
strcpy(backup_buf, input_buf);
state = NEW_VALUE;
}
}else if( state == NEW_VALUE ){
unsigned char len = strlen(input_buf);
if( len == 0 ){
Serial.println("Invalid Input");
state = TOP;
}else{
long ret = add_title(backup_buf);
if( ret < 0 ){
Serial.println("Not Enough");
}else{
// タイトル・パスワードの追加、不揮発メモリへも追加
pref.putString(backup_buf, input_buf);
pref.putBytes("title_list", title_list, sizeof(title_list));
Serial.println(backup_buf);
Serial.println(" Added");
}
state = TOP;
}
}else if( state == UPDATE_VALUE ){
// パスワードの更新、不揮発メモリを更新
unsigned char len = strlen(input_buf);
if( len == 0 ){
Serial.println("Invalid Input");
state = TOP;
}else{
char *title = get_title(backup_number);
pref.putString(title, input_buf);
Serial.println(title);
Serial.println(" Updated");
state = TOP;
}
}else if( state == CHECK_VALUE ){
unsigned char len = strlen(input_buf);
if( len == 0 ){
Serial.println("Invalid Input");
state = TOP;
}else{
// パスワードの不揮発メモリからの読み出しと、値の確認
char* title = get_title(backup_number);
pref.getString(title , backup_buf, sizeof(backup_buf) );
Serial.println(title);
if( strcmp(input_buf, backup_buf) != 0 ){
Serial.println(" Mismatch!");
}else{
Serial.println(" Correct!");
}
state = TOP;
}
}else{
state = TOP;
}
prompt();
break;
}
}
}
補足
大きく分けて5の処理に分かれます。
-
パスワード管理
不揮発メモリを操作して、パスワードを登録したり削除したりします。 -
シリアル入力処理
シリアル入出力処理を行います。1文字入力や文字列入力のためのバッファリングをします。 -
シリアル表示処理
状態に合わせて、シリアルにメニューを出力します。 -
BLEデバイス処理
BLEデバイスとしてふるまうための処理です。HIDのクラスもあるので、ありがたく使わせていただきました。 -
LCD表示処理
状態に合わせて、LCDの表示を切り替えます。 -
Arduinoメイン処理
arduinoでお決まりの、setupとloopがあります。
setup()
M5StickCの初期、LCDの初期化、シリアルの初期化、BLEデバイスの起動、不揮発メモリライブラリの初期化を行います。
loop
ボタンの押下と、シリアル入力を監視します。
ボタン押下を検知すると、HIDでパスワードを送信します。
シリアル入力を検知すると、メニューの状態遷移とパスワード処理を行います。
制限事項
-
Androidでは、HIDキーボードとして認識するときのPIN入力がうまくいかず、ペアリングできない場合があります。
その時にはあきらめて、PIN入力無しとなるようにソースコードを修正しかないのか。。。 -
パスワード登録などで、不揮発メモリを更新すると、なぜかBLEのペアリングの鍵が壊れるようで、次回ペアリングに失敗してしまいます。Bondingはあきらめないといけないのか。。。(素直に、EEPROM.hを使うべきだったか??)
-
BLEまわりが不安定です。。。おしい。。。
以上