##1. はじめに
M5Stack CORE2 UiFlowからArduino開発環境への移植 ~ タッチパネル × Faces(エンコーダ) × Bluetoothデバイス制御を拝読しM5StackでBLE(Bluetooth Low Energy)のキーボードやマウスを手軽に作れることが分かりました。そこで、コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作します。
##2. BLE Keyboard&Mouse
M5Stackでコマンド操作のコマンドインターフェースプログラムにArduino Leonardoで多目的ツールの製作のキーボードおよびマウスのコマンドを移植します。BLE Keyboard & Mouseはblackketter/ESP32-BLE-ComboのESP32 BLE Combo Keyboard & Mouse libraryを利用させていただきます。
###2.1 ビルド環境
- Arduino IDE 1.8.15(Windows Store 1.8.49.0)
- esp32 by Espressif Systems Ver. 1.0.4
###2.2 ESP32 BLE Combo Keyboard & Mouse library
- blackketter/ESP32-BLE-Combo→緑色の"Code"ボタン→Download ZIP
- Arduino IDEでスケッチ→ライブラリをインクルード→.ZIP形式のライブラリをインストール...とたどり、ダウンロードしたZIPファイルをインストール
###2.3 コマンド
表1および表2にコマンドを示します。
表1 コマンド一覧
コマンド | 引数1 | 引数2 | 機能 |
---|---|---|---|
help | なし | なし | コマンドの一覧と簡単な説明を表示する |
? | なし | なし | コマンドの一覧と簡単な説明を表示する |
ver | なし | なし | ファイル名とビルド日時を表示する |
typesize | なし | なし | 型のサイズをバイト数で表示する |
str | 文字列 | なし | 文字列を打鍵する(ASCII CODE 0x20-0x7Fのみ) |
key | 押下したいキー(表2) | なし | キーを打鍵(press&release)する |
key | alt | 押下したいキー(表2) | 左Altキーとの同時押しでキーを打鍵する |
key | ctrl | 押下したいキー(表2) | 左Ctrlキーとの同時押しでキーを打鍵する |
key | shift | 押下したいキー(表2) | 左Shiftキーとの同時押しでキーを打鍵する |
key | win | 押下したいキー(表2) | Windowsロゴキーとの同時押しでキーを打鍵する |
key | ctrlalt | 押下したいキー(表2) | 左Ctrlキー、左Altキーとの同時押しでキーを打鍵する |
mouse | sc | なし | マウス左ボタンのシングルクリック |
mouse | wc | なし | マウス左ボタンのダブルクリック |
mouse | rc | なし | マウス右ボタンのシングルクリック |
mouse | p | なし | マウス左ボタンのプレス |
mouse | r | なし | マウス左ボタンのリリース |
mouse | scr | スクロール量 | マウスホイールの回転 |
mouse | 水平方向の移動量 | 垂直方向の移動量 | マウスカーソルの移動 |
表2 keyコマンドの引数
キー | 引数の与え方 |
---|---|
ASCII文字(1文字) | そのまま入力します |
Enterキー | ent |
Spaceキー | sp |
Tabキー | tab |
Backspaceキー | bs |
Deleteキー | del |
Escapeキー | esc |
PrintScreenキー | ps |
↑キー | ua |
↓キー | da |
←キー | la |
→キー | ra |
PAGE UPキー | pu |
PAGE DOWNキー | pd |
- スペースを含む文字列はstrコマンドとkey spコマンドに分けて入力します。
###2.4 スケッチ
/*
* M5Stack Command Controled BLE Keyboard&Mouse Combo Device
* by ka's 2021
*
* This software includes the work that is distributed
* in the Apache License 2.0
*
* M5Stackでコマンド操作
* https://qiita.com/pbjpkas/items/c8c26837a1a5a243f4b6
*
* Arduino Leonardoで多目的ツールの製作
* https://qiita.com/pbjpkas/items/97dbf835b0aab6725e94
*
* BleCombo.h
* ESP32 BLE Combo Keyboard & Mouse library
* https://github.com/blackketter/ESP32-BLE-Combo
*/
#include <stdio.h>
#include <string.h>
#include <BleCombo.h>
#include <M5Stack.h>
// wait
#define ANTI_CHATTER 50 //50ms
// keycode
#define KEY_NUL 0x00
#define KEY_PRINTSCREEN 0xCE
#define KEY_SCROLLLOCK 0xCF
#define KEY_PAUSE 0xD0
void setup();
void loop();
// Keyboard
void keyboard_key(int primary_key, int secondary_key, int key_code);
// Mouse
void mouse_single_click(void);
void mouse_double_click(void);
// Command Mode
void cmd_print_help(void);
void cmd_print_ver(void);
int cmd_str(char *str);
int cmd_str_to_char(char *str);
int cmd_key(int primary_key, int secondary_key, char *str);
int cmd_execute(char *buf);
void cmd_rx_data(void);
// the setup routine runs once when M5Stack starts up
void setup()
{
// Initialize the M5Stack object
M5.begin();
Serial.begin(115200);
// LCD display
M5.Lcd.print("M5Stack BLE Combo Device Ver.001");
// BLE Mouse, Keyboard
Mouse.begin();
Keyboard.begin();
}
// the loop routine runs over and over again forever
void loop()
{
if(Serial.available())
{
cmd_rx_data();
}
}
/***********************************************************************
Keyboard
***********************************************************************/
void keyboard_key(int primary_key, int secondary_key, int key_code)
{
if(primary_key ){ Keyboard.press(primary_key ); }
if(secondary_key){ Keyboard.press(secondary_key); }
Keyboard.press(key_code);
delay(ANTI_CHATTER);
Keyboard.releaseAll();
delay(ANTI_CHATTER);
}
/***********************************************************************
Mouse
***********************************************************************/
void mouse_single_click(int button)
{
Mouse.click(button);
delay(ANTI_CHATTER);
}
void mouse_double_click(int button)
{
mouse_single_click(button);
mouse_single_click(button);
}
/***********************************************************************
Command Mode
***********************************************************************/
#define CMD_OK 0
#define CMD_INVALID -1
#define CMD_MALLOC_ERR -2
#define CMD_BUF_LENGTH 64 // 63+1
#define CMD_MAX_LENGTH 64 // 63+1
#define ARG_MAX_LENGTH 64 // 63+1
void cmd_print_help(void)
{
Serial.print("Available Command:\n");
Serial.print("help, ? : Print Help Messages\n");
Serial.print("ver : Print Version Information\n");
Serial.print("typesize : print type's size\n");
}
void cmd_print_ver(void)
{
Serial.print(__FILE__);
Serial.print(" ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.print(__TIME__);
Serial.print("\n");
}
int cmd_str(char *str)
{
int i;
int len;
len = strlen(str);
for(i=0; i<len; i++)
{
if(str[i]<0x20)
{
return CMD_INVALID;
}
}
for(i=0; i<len; i++){ keyboard_key(KEY_NUL, KEY_NUL, str[i]); }
return CMD_OK;
}
int cmd_str_to_char(char *str)
{
int keycode;
char buf[2];
if (strcmp(str, "ent")==0){ keycode = KEY_RETURN; }
else if(strcmp(str, "sp" )==0){ keycode = 0x20; }
else if(strcmp(str, "tab")==0){ keycode = KEY_TAB; }
else if(strcmp(str, "bs" )==0){ keycode = KEY_BACKSPACE; }
else if(strcmp(str, "del")==0){ keycode = KEY_DELETE; }
else if(strcmp(str, "esc")==0){ keycode = KEY_ESC; }
else if(strcmp(str, "ps" )==0){ keycode = KEY_PRINTSCREEN; }
else if(strcmp(str, "ua" )==0){ keycode = KEY_UP_ARROW; }
else if(strcmp(str, "da" )==0){ keycode = KEY_DOWN_ARROW; }
else if(strcmp(str, "la" )==0){ keycode = KEY_LEFT_ARROW; }
else if(strcmp(str, "ra" )==0){ keycode = KEY_RIGHT_ARROW; }
else if(strcmp(str, "pu" )==0){ keycode = KEY_PAGE_UP; }
else if(strcmp(str, "pd" )==0){ keycode = KEY_PAGE_DOWN; }
else
{
if(strlen(str)>1) { return CMD_INVALID; }
sscanf(str, "%c", buf);
keycode = buf[0];
if((keycode<0x20) || (keycode>0x7f)){ return CMD_INVALID; }
}
return keycode;
}
int cmd_key(int primary_key, int secondary_key, char *str)
{
int keycode;
keycode = cmd_str_to_char(str);
if(keycode<0)
{
return CMD_INVALID;
}
keyboard_key(primary_key, secondary_key, keycode);
return CMD_OK;
}
int cmd_execute(char *buf)
{
int i, x, y;
unsigned int ux;
int return_val = CMD_OK;
char cmd[CMD_MAX_LENGTH];
char arg1[ARG_MAX_LENGTH];
char arg2[ARG_MAX_LENGTH];
strcpy(cmd, "");
strcpy(arg1, "");
strcpy(arg2, "");
sscanf(buf, "%s %s %s", &cmd, &arg1, &arg2);
if (strcmp(cmd, "help")==0){ cmd_print_help(); }
else if(strcmp(cmd, "?" )==0){ cmd_print_help(); }
else if(strcmp(cmd, "ver" )==0){ cmd_print_ver(); }
else if(strcmp(cmd, "typesize")==0)
{
Serial.print( "char : "); Serial.print(sizeof(char));
Serial.print("\nshort : "); Serial.print(sizeof(short));
Serial.print("\nint : "); Serial.print(sizeof(int));
Serial.print("\nlong : "); Serial.print(sizeof(long));
Serial.print("\nlong long : "); Serial.print(sizeof(long long));
Serial.print("\n");
}
else if(strcmp(cmd, "str")==0){ return_val = cmd_str(&arg1[0]); }
else if(strcmp(cmd, "key")==0)
{
if (strcmp(arg1, "alt" )==0){ return_val = cmd_key(KEY_LEFT_ALT, KEY_NUL, &arg2[0]); }
else if(strcmp(arg1, "ctrl" )==0){ return_val = cmd_key(KEY_LEFT_CTRL, KEY_NUL, &arg2[0]); }
else if(strcmp(arg1, "shift" )==0){ return_val = cmd_key(KEY_LEFT_SHIFT, KEY_NUL, &arg2[0]); }
else if(strcmp(arg1, "win" )==0){ return_val = cmd_key(KEY_LEFT_GUI, KEY_NUL, &arg2[0]); }
else if(strcmp(arg1, "ctrlalt")==0){ return_val = cmd_key(KEY_LEFT_CTRL, KEY_LEFT_ALT, &arg2[0]); }
else if(strlen(arg1) != 0 ){ return_val = cmd_key(KEY_NUL, KEY_NUL, &arg1[0]); }
else
{
return CMD_INVALID;
}
}
else if(strcmp(cmd, "mouse")==0)
{
if (strcmp(arg1, "sc" )==0){ mouse_single_click(MOUSE_LEFT ); }
else if(strcmp(arg1, "wc" )==0){ mouse_double_click(MOUSE_LEFT ); }
else if(strcmp(arg1, "rc" )==0){ mouse_single_click(MOUSE_RIGHT); }
else if(strcmp(arg1, "p" )==0){ if(!Mouse.isPressed(MOUSE_LEFT)){ Mouse.press (MOUSE_LEFT); } }
else if(strcmp(arg1, "r" )==0){ if( Mouse.isPressed(MOUSE_LEFT)){ Mouse.release(MOUSE_LEFT); } }
else if(strcmp(arg1, "scr")==0)
{
x = atoi(arg2);
if(!x){ return CMD_INVALID; }
for(i=0; i<abs(x); i++)
{
if(x>=0){ Mouse.move(0,0, 1); }
else { Mouse.move(0,0,-1); }
delay(25);
}
}
else
{
x = atoi(arg1);
y = atoi(arg2);
for(i=0; i<abs(x); i++)
{
if(x>=0){ Mouse.move( 1,0); }
else { Mouse.move(-1,0); }
delay(5);
}
for(i=0; i<abs(y); i++)
{
if(y>=0){ Mouse.move(0, 1); }
else { Mouse.move(0,-1); }
delay(5);
}
}
}
else
{
return CMD_INVALID;
}
return return_val;
}
void cmd_rx_data(void)
{
static int i=0;
static char buf[CMD_BUF_LENGTH];
int return_val = CMD_OK;
if(Serial.available())
{
buf[i] = Serial.read();
Serial.print(buf[i]); //echo-back
if ( (buf[i] == 0x08) or (buf[i] == 0x7f) ) //BackSpace, Delete
{
buf[i] = '\0';
if(i) i--;
}
else if( (buf[i] == '\r') or (buf[i] == '\n') )
{
buf[i] = '\0';
return_val = cmd_execute(&buf[0]);
for(i=0; i<CMD_BUF_LENGTH; i++) buf[i] = '\0';
i=0;
if(return_val == CMD_INVALID)
{
Serial.print("?\n");
}
else
{
Serial.print("OK\n");
}
}
else
{
i++;
if(i>=CMD_BUF_LENGTH)
{
Serial.print("### CMD BUFFER FULL, CLEAR. ###\n");
for(i=0; i<CMD_BUF_LENGTH; i++) buf[i] = '\0';
i=0;
}
}
}
}
##3. iOSアプリの操作
###3.1 動作デモ
iPad(iPad OS 14.6)のSafariでQiitaのトップページをあらかじめ開いておきます。
また、自動大文字入力の設定をオフにして入力した文字の一文字目が自動で大文字になるのを抑止します1。
- 検索窓にマウスポインタを移動する
- 検索窓でマウスを左クリックする
- "user:pbjpkas"およびEnterキーを入力しページを遷移させる
- マウスポインタを元の場所へ移動する
###3.2 テストランナー
Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定するに掲載のtest-runner.pyをテストランナーとして利用しWindows 10のPython 3.6.9で実行します。
C:\hoge>python test-runner.py blecombo
###3.3 テストスクリプト
キーボードやマウスをコマンド操作するスクリプトは下記4行でワンセットです。
send,mouse 200 -150 ←M5Stackに"mouse 200 -150"というコマンドを送る
rcvd ←M5Stackのコマンドのエコーバックを読み捨てる
sleep,4 ←マウスポインタの移動が終わるまで待つ
rcvd ←M5Stackのコマンドの実行結果(OK or ?)を読み捨てる
テストスクリプト全体を以下に示します。
#open UART
open_uart,COM3,0
sleep,4
rcvd
#
send,mouse 200 -150
rcvd
sleep,4
rcvd
#
send,mouse sc
rcvd
sleep,2
rcvd
#
send,str user:pbjpkas
rcvd
sleep,4
rcvd
send,key ent
rcvd
sleep,4
rcvd
#
send,mouse -200 150
rcvd
sleep,4
rcvd
テスト実行結果を以下に示します。
2021/06/16 00:10:44,#open UART,OK
2021/06/16 00:10:44,open_uart,COM3,0,OK
2021/06/16 00:10:44,sleep,4,OK
2021/06/16 00:10:48,rcvd,,OK
2021/06/16 00:10:49,#,OK
2021/06/16 00:10:49,send,mouse 200 -150,OK
2021/06/16 00:10:49,rcvd,mouse 200 -150,OK
2021/06/16 00:10:49,sleep,4,OK
2021/06/16 00:10:53,rcvd,OK,OK
2021/06/16 00:10:53,#,OK
2021/06/16 00:10:53,send,mouse sc,OK
2021/06/16 00:10:53,rcvd,mouse sc,OK
2021/06/16 00:10:53,sleep,2,OK
2021/06/16 00:10:55,rcvd,OK,OK
2021/06/16 00:10:55,#,OK
2021/06/16 00:10:55,send,str user:pbjpkas,OK
2021/06/16 00:10:55,rcvd,str user:pbjpkas,OK
2021/06/16 00:10:55,sleep,4,OK
2021/06/16 00:10:59,rcvd,OK,OK
2021/06/16 00:10:59,send,key ent,OK
2021/06/16 00:10:59,rcvd,key ent,OK
2021/06/16 00:10:59,sleep,4,OK
2021/06/16 00:11:03,rcvd,OK,OK
2021/06/16 00:11:03,#,OK
2021/06/16 00:11:03,send,mouse -200 150,OK
2021/06/16 00:11:03,rcvd,mouse -200 150,OK
2021/06/16 00:11:03,sleep,4,OK
2021/06/16 00:11:07,rcvd,OK,OK
###3.4 known issues
- テストスクリプトが終了してUARTをクローズする際にM5Stackがリセットします。
- マウスポインタを滑らかに動かすようなチューニングは特に行っていません。
##4. おわりに
- コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作できました。
- 2019年9月に品質技術を知る、学ぶ、作るでBluetoothのキーボードを作ったときはC/C++で1000行くらいかかっていたのが今はライブラリをインストールしてBleCombo.hをインクルードすればサクッと作れるようになっていて、とってもありがたいです。
##付録. Lチカで始めるテスト自動化・記事一覧
電子書籍化したものを技術書典で頒布しています。
- Lチカで始めるテスト自動化
- Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上
- Lチカで始めるテスト自動化(3)オシロスコープの組込み
- Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)
- Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み
- Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定
- Lチカで始めるテスト自動化(7)タイムスタンプの保存
- Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み
- Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み
- Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上
- Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作
- Lチカで始めるテスト自動化(12)書籍化の作業メモ
- Lチカで始めるテスト自動化(13)外部プログラムの呼出し
- Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する
- Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する
- Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する