M5AtomS3でスマホの操作を1ボタンで行う装置を作ってみた

![SW-WiFiCALL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33661/ea131ad8-9ba6-b270-6327-2325b7e82a34.png)

# 背景

　スマホに接続する、入力装置（キーボードなど）を自作することが可能です。簡単な方法ではArduino系でleonard,nano,UNO R4、RP2040系はUSBホスト機能があるので、キーボードのような入力装置を自作して接続できます。また、BloetoothのHIDプロファイルを使用すれば、Bluetoothでも接続できます。

　福祉用の補助入力機器でスイッチのワンプッシュのみ操作で、スマホの機能を呼び出す装置のリクエストをよくいただきますので、検討して見ました。

# スマホのキーボード入力調査

　まずは、AndroidのスマホにUSB経由でキーボードを接続してみます。
![p1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33661/13b5fdf6-b582-a024-d2ba-a60c79f5c61b.png)
　接続すると、選択中のアイコンの表示が変わり、TABキー、矢印キーで選択状態が移動できます。
![p2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33661/cd3540a0-eb5e-539f-c129-ce1f4037af03.png)

そしてそのままEnterキーを押すと、画面のタッチ動作と同じでアプリが起動（アプリ内では機能が実行）されます。

　ここで、できるだけ登録時の操作を簡単にするため、起動したいアプリをホーム画面のTOPに登録しておいたほうが良さげです。

### 簡単な操作の例（Android）
#### TOPページの先頭に選択
 Windowsキー + Enter
#### 電話をかける
 ・電話アプリ起動　→　お気に入りで相手を選択
 ・電話アプリ起動　→　連絡先リストから選択
 ・電話アプリ起動　→　ダイヤルパッドアイコン選択　→　ダイヤルキーを選択（電話番号入力）→　音声通話ボタン選択

### 電話を切る
　電話切断アイコンを選択	
### 電話を取る
　残念ながらキーボード操作ができないので、マウスの操作記録で対応

　これらの操作を、キーボードの代わりとしてUSB接続したマイコンに記録し、１つのボタン操作で１つずつキーボード操作で送られるコードを送るようにすれば、スマホの操作ができるようになります。

# プログラム作成
　今回USBホストになるマイコンはM5AtomS3を使用しまいた。rasberry-pi picoやArduno系のleonard,nanoでもUSBホストになるので対応可能です。また、今回はAndroidで試してしますが、iPhoneでもキーボードで同じような処理ができます。

### スマホ操作の記録
　スマホの操作はM5AtomS3内のEEPROM内に記憶していきます。
  操作を記録する方法はいくつか考えられます。

１．本体にカーソル移動と選択のキーを持たせ、キー操作毎にUSB経由でキーコードを送り、スマホ画面でカーソル移動を確認しながら、Enterキー入力時にキー操作を記録する

　*記録過程、内容を確認するためのLCDが必要になります。複雑な登録はわかりにくくなります。

２，マイコンでWebサーバを起動し、組み込んだWeb画面から操作、入力を行う

３．PC側のアプリを作成し、BLEまたはWiFi経由で操作コマンドを送る

製品的には、２の方法がスマートに感じますが、Webページの実装が少し面倒なので今回は１の方法で作成しました。

#### 処理
　Enterキーが押されるまでのカーソルキー操作回数を記憶し、Enterキー押下でEEPROMに書き込む（書き込みポインタ更新）。このとき書き込みポインタも更新します。書き込み領域のサイズも考慮して、書き込みポインタの上限設定が必要です。

　M5AtomS3はボタンキーと加速度センサを持っているので、本体を傾けてボタンキーをしたときカーソル移動操作（Up,Down,Right,Left）、本体を水平にしてボタンを押したときにEnter操作としました。Raspberry-pi picoなどで作成する場合には、カーソル移動とEnterキーを外部に取り付ける必要があります。

注意
＊M5AtomS3をUSBホストで動作させた場合に、再度プログラムを書き込むときは、リセットボタンを押しながら電源を入れる必要があります。
＊USBでのシリアル通信ができなくなるので、デバッグにprintは使えません


### ボタンによる記録データの呼び出し
　ボタン押下で，記録したデータを一個づつ読み出して、キーコードをUSB経由で送ります。
アプリの起動時間を考慮して、Enterのあとは2秒ほど待ちを入れます（スマホによって時間が変わるので調整必要）

なお、記録モードと実行モードは起動時のボタン操作で判定しています。（ボタンを押したまま起動で登録モード）
```c:usb_key.ino
#include "M5AtomS3.h"
#include <M5Unified.h>
![p1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33661/3b3d553a-6146-483f-166a-d37ca20a2592.png)

#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;

#include "image.h" 

byte run_mode = 0;     //動作モード（0:設定, 1:実行）
byte state = 0;        //状態番号

float accX = 0.0f;
float accY = 0.0f;
float accZ = 0.0f;

int dsp_cur = 0;
byte log_index = 0;
byte curX = 0;
byte curY = 0;

struct ICON_POS{
  byte x;
  byte y;
};

int btnst = 0;
unsigned long cktime;
unsigned long now;
unsigned long ck;
int count = 0;

//---------------------------------------------------
// テキスト表示
//---------------------------------------------------
void dsp_text(bool select, String text, int posY){
  if(select){
    M5.Lcd.setTextColor(TFT_WHITE,TFT_BLUE);
  }else{
    M5.Lcd.setTextColor(TFT_WHITE,TFT_WHITE);
  }
  M5.Lcd.drawString(text, 2, posY);
}

//---------------------------------------------------
// M5AtomS3へのアクション検出
//---------------------------------------------------
String getAction(){
  auto imu_update = M5.Imu.update();
  if (imu_update) {

    //加速度データ取得
    auto data = M5.Imu.getImuData();

    data.accel.x;
    data.accel.y;
    data.accel.z;
    data.accel.value;

    data.value;
    accX = data.accel.x;
    accY = data.accel.y;
    accZ = data.accel.z;
  }
  String act = "";

  //ボタン押下時の傾きにより操作を決定
  if (AtomS3.BtnA.wasPressed()) {
      if(accY > 0.5f){
        act = "D";
      }else if(accY < -0.5f){
        act = "U";
      }else if(accX > 0.5f){
        act = "L";
      }else if(accX < -0.5f){
        act = "R";
      }else if(accZ < 0.0f){
        act = "B";
      }else{
        act = "S";
      }
  }
  return act;
}
//---------------------------------------------------
// KeyBoard操作（キーコード送信）
//---------------------------------------------------
void pressKey(int code, int code2){
  if(code == 0){
    Keyboard.press(code);
    delay(100);
    Keyboard.release(code);
  }else{
    Keyboard.press(code);
    Keyboard.press(code2);
    delay(100);
    Keyboard.release(code);
    Keyboard.release(code2);
  }
}
//---------------------------------------------------
// HOME画面のTOPにコーソルを移す 
//---------------------------------------------------
void homePos(){
  //Android
  pressKey(KEY_LEFT_GUI,KEY_RETURN);
  delay(300);
  //先頭のページにもどす処理(１ページ目でない場合)
  //pressKey(KEY_LEFT_ARROW,0);

  //最初のアイコン選択状態からスタート（電話から戻る場合　１回）
  pressKey(KEY_UP_ARROW,0);
  delay(50);
  pressKey(KEY_UP_ARROW,0);
  delay(50);
}

//--------------------------------------------
//データ保存
// X  , y アプリ選択
// 100, 0 HOMEに戻る
//--------------------------------------------
void save_data(byte x, byte y){
  if(log_index >= 40) {  //最大40個まで
    M5.Lcd.fillRect(0, 32, 180, 16, BLACK);
    dsp_text(false,"Data Over!",32);
    return;
  }
  AtomS3.Display.clear();
  AtomS3.Display.drawString("Save", 2, 2);
  AtomS3.Display.drawString("Index=" + String(log_index), 2, 22);

  //データ書き込み
  int startPos = log_index*2;
  EEPROM.write(startPos, x);
  EEPROM.write(startPos + 1, y);

  AtomS3.Display.drawString("Pos=" + String(x) + "," + String(y) ,2, 42);
  
  //同時に終了コードを書き込んでおく
  log_index++;
  startPos = log_index*2;
  
  EEPROM.write(startPos, 100);
  EEPROM.write(startPos + 1, 100);

  EEPROM.commit();

}
//--------------------------------------------
//データ読み出し
//--------------------------------------------
ICON_POS read_data(){
  int startPos = log_index*2;
  byte x = EEPROM.read(startPos);
  byte y = EEPROM.read(startPos+1);

  AtomS3.Display.clear();
  AtomS3.Display.drawString("Read", 2, 2);
  AtomS3.Display.drawString("Index=" + String(log_index), 2, 22);
  AtomS3.Display.drawString("Pos=" + String(x) + "," + String(y) ,2, 42);
  if(log_index < 40){
    log_index++;
  }
  ICON_POS pos = {x,y};
  return pos;
}


void setup(void) {
  auto cfg = M5.config();
  cfg.internal_imu = true;
  M5.begin(cfg);

  Keyboard.begin();
  USB.begin();
  pinMode(2, INPUT_PULLUP);

  EEPROM.begin(402);  //EEPROM領域確保
  M5.Lcd.init();

  delay(1000);

  homePos();

  //設定モード判定
  int sw = digitalRead(2);
  if (sw == LOW || AtomS3.BtnA.wasPressed()) {
    run_mode = 0;
    state = 0;
    dsp_text(true,"Setting mode",1);
  }else{
    run_mode = 1;
    state = 0;
    dsp_text(true,"Run mode",1);
  }
  //run_mode = 0;
}

void loop(void) {
  AtomS3.update();

  String keyStr = getAction();
  now = millis();
  ck = now - cktime;
  
  //設定モード
  if(run_mode == 0){
    if(keyStr != ""){
      M5.Lcd.fillRect(0, 16, 32, 16, BLACK);
      dsp_text(false,keyStr,16);

      if(keyStr == "U"){
        //UP
        if(curY > 0){
          pressKey(KEY_UP_ARROW,0);
          curY --;
        }
      }else if(keyStr == "D"){
        //DOWN
        curY ++;
        pressKey(KEY_DOWN_ARROW,0);

      }else if(keyStr == "L"){
        //LEFT
        if(curX > 0){
          pressKey(KEY_LEFT_ARROW,0);
          curX --;
        }

      }else if(keyStr == "R"){
        //RIGHT
        pressKey(KEY_RIGHT_ARROW,0);
        curX ++;

      }else if(keyStr == "S"){
        pressKey(KEY_RETURN,0);
        //Save
        save_data(curX, curY);
        curX = 0;
        curY = 0;
      }
      M5.Lcd.fillRect(0, 32, 32, 16, BLACK);
      dsp_text(false,String(curX) + "," + String(curY),32);
    }
  //実行モード
  }else {
    if(state == 0){
      if(keyStr != ""){
        if(keyStr == "S"){
          log_index = 0;
          state = 1;
          homePos();
        }
      }
    }else if(state == 1){
      //データ読み込み
      ICON_POS iPos = read_data();

      if((iPos.x == 100 && iPos.y == 100) || (iPos.x == 0 && iPos.y == 0)){
        //終了
        state = 0;
        log_index = 0;
        AtomS3.Display.clear();
        dsp_text(true,"Run mode",1);

      }else{
        //アイコン移動
        for(int lp = 0; lp < iPos.y; lp++){
          pressKey(KEY_DOWN_ARROW,0);
          delay(100);
        }
        for(int lp = 0; lp < iPos.x; lp++){
          pressKey(KEY_RIGHT_ARROW,0);
          delay(100);
        }
        pressKey(KEY_RETURN,0);
        //アプリ（画面）起動待ち
        delay(2000);
      }
    }
  }
  delay(100);
}
```
　今回は１個の操作のみを登録しましたが、ボタン操作のパターン（単押し　長押し　２度押し）で複数の機能を割り当てることも可能です。

　また表示がある場合は、ボタン押し　数字カウント開始　ボタン押しで複数登録した機能を番号指定で呼び出すことも可能です。

# おまけ
　自作のアプリが必要ですが、Google Smartの呼び出し方法を発見しました。

ボタン操作でTTS音声読み上げアプリ（自作アプリ）起動
　「OK,Google　ランプを点けて」発声命令
→　すぐに（発声が始まる前に）Google Home起動の操作（キーコード送信）

→　スマホ自信の発声内容を、自身が解釈して実行される

これで、ボタン一つでGoogle Smartの機能が呼び出せます。