2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する

Posted at

##1. はじめに
M5Stack CORE2 UiFlowからArduino開発環境への移植 ~ タッチパネル × Faces(エンコーダ) × Bluetoothデバイス制御を拝読しM5StackでBLE(Bluetooth Low Energy)のキーボードやマウスを手軽に作れることが分かりました。そこで、コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作します。

bleHID.png

##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

  1. blackketter/ESP32-BLE-Combo→緑色の"Code"ボタン→Download ZIP
  2. 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_bleHID.ino
/*
 * 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

  1. 検索窓にマウスポインタを移動する
  2. 検索窓でマウスを左クリックする
  3. "user:pbjpkas"およびEnterキーを入力しページを遷移させる
  4. マウスポインタを元の場所へ移動する

動作デモ

###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 ?)を読み捨てる

テストスクリプト全体を以下に示します。

blecombo.csv
#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

テスト実行結果を以下に示します。

blecombo_result.csv
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チカで始めるテスト自動化・記事一覧
電子書籍化したものを技術書典で頒布しています。

  1. Lチカで始めるテスト自動化
  2. Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上
  3. Lチカで始めるテスト自動化(3)オシロスコープの組込み
  4. Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)
  5. Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み
  6. Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定
  7. Lチカで始めるテスト自動化(7)タイムスタンプの保存
  8. Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み
  9. Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み
  10. Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上
  11. Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作
  12. Lチカで始めるテスト自動化(12)書籍化の作業メモ
  13. Lチカで始めるテスト自動化(13)外部プログラムの呼出し
  14. Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する
  15. Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する
  16. Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する
  1. iPadのアルファベット自動大文字入力をオフにする

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?