M5Stack CORE2 UiFlowからArduino開発環境への移植 ～ タッチパネル × Faces(エンコーダ) × Bluetoothデバイス制御


## 説明について

以前、**M5Stack CORE2**というデバイスを使って**「M5MouseWheel」**という便利ツールを作りました。

![m5mci.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65983/9ae85e40-8dd6-d4fa-df2d-f2a2a5785c18.png)

<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vT6SWcz1KLnEZBDmtYqbbwyzALN3QQR76dL_uekL1zzBH00yb19GNTGETNAm5PIM4AVfZfnmII9mSXc/embed?start=false&loop=false&delayms=3000" frameborder="0" width="773" height="490" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>

M5Stack のファームウェア開発には何通りかの方法があります。
主には**UiFlow**というビジュアルプログラミングができる専用ツールと
本格的な**Arduino開発環境**というマイコンの汎用開発ツールがあります。
以前はUiFlowを用いてプログラミングは楽ちんだったのですが、実は本格開発だともっと色々な事が出来る事に気づいたので、プログラムをそちらに移植しました。その時のハマりどころと解決方法をご説明します。

- **M5Stack CORE2でUiFlowを用いてプロトタイプを作り、方向性が定まったらArduino開発環境(C++)に移行する**開発手法を検討されている
- **M5Stack で開発していたが、M5Stack CORE2となると途端につまづいてしまう** 

方には参考となると思います。

**■ 移植後のプログラム（M5MouseWheel (BluetoothMouse対応版））**
当記事は経緯説明ですが、結果だけ知りたい方はこちらからご確認ください。

![スクリーンショット 2021-06-11 130133.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65983/57228f13-f86f-be96-0b45-cad57e4543d8.png)

https://github.com/RAWSEQ/M5MouseWheel-BluetoothMouse

Arduino開発環境でM5Stack CORE2への書き込みができるようになった状態（**利用ライブラリは`M5Core2`**）からのご説明なのでその点はご了承ください。

また、こちらの記事は2021/06時点での私の考察で、それ以降（または私の認識時点に）、別なアプローチで問題が解消されている可能性がありますので、予めご了承ください。

## ■０．M5Stack BASIC と M5Stack CORE2

M5Stack CORE2がリリースされてからワリと時が経ったと感じますが、Google検索で "CORE2" と指定してもなかなか望むドキュメントにたどり着きません。（私のGoogle力のせいかもしれませんが・・）恐らく、M5Stack BASIC (無印M5Stack）の威光が強い為かと愚考します。

M5Stack BASIC と M5Stack CORE2 は各々利用するライブラリが異なります。当然ながら変化したり追加されたりしている点はありますが基本的には同一の位置付けにあります。
但し、BASICに接続していた一部デバイスはCORE2では動作しなかったりする場合があります。
それには都合があるのですが、それを周りの開発貢献者様が様々な理由で読み解く（または承服する）のが難しい状態になっていると感じています。そういったもやもやがこちらの記事で少しでも晴れれば幸いです。

## ■１．タッチパネルって結局どうする？

無印M5Stackだと３ボタンが押された場合の制御を書けばよかったものの、タッチパネルとなるとハードルが高いイメージがあります。
タッチボタンを再現しようにも、「タッチ状態を取得して・タッチされたらどこがタッチされているか座標情報から範囲を特定して・範囲内ならタッチしてから指を離す間ボタンの見た目を変えて・・・」等、UiFlowでは簡単にできたものも面倒な制御が必要になるのでは？と考えておりましたが、実はそんなことありません。

```c++:宣言部
ButtonColors cl_on  = {0x7BEF, WHITE, WHITE}; // タップした時の色 (背景, 文字列, ボーダー)
ButtonColors cl_off = {BLACK, 0xC618, 0xC618}; // 指を離した時の色 (背景, 文字列, ボーダー)

// ボタン定義名( X軸, Y軸, 横幅, 高さ, 回転, ボタンのラベル, 指を離した時の色指定, タッチした時の色指定）
Button btn_x(110, 110, 100, 100, false ,"X", cl_off, cl_on);
```

```c++:スタートアップ(setup)
void setup() {
  // …
  M5.Buttons.setFont(FSSB12); // 全てのボタンのフォント指定
  btn_x.setFont(FSSB24); // 個別指定したい場合はこちら
  btn_x.addHandler(event_btn_x, E_RELEASE); // ボタンが離された時のイベント関数を指定（E_**の指定で他の操作もアサイン可能）
  M5.Buttons.draw(); // 全てのボタンを描画
  // …
}
```

```c++:クロック(loop)
void loop() {
  M5.update();
}
```

```c++:イベント関数
void event_btn_x(Event& e) {
  // ボタンが押された（指が離されたタイミング）時の処理
}
```

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65983/9fc2f3c8-728e-3ad9-cdc0-a1d8f90cebda.gif)


非常にシンプルになりましたね＾＾
ボタンの形状は変わりますが、ボタン座標の定義はUiFlowのPython記述の書き写しで問題ありませんでした。


参考：公式サイトや他ドキュメントや記事にも記載がなかったのでソースファイルを見てみると、ヘッダーファイルに親切な記述がありました。もっと深く触りたい方はこちらをご参照ください。

https://github.com/m5stack/M5Core2/blob/master/src/utility/M5Button.h

「トグルボタン」や、「選択式ボタン」等も私のプログラムに実装されていますので参考にしてみてください。

## ■２．Faces（一部I2C外部デバイス）が動かなくなった・・

正直これが一番ハマりました。M5MouseWheelはホイール部分に **Faces（Encoderモジュール）** を利用しています。
これが無印M5Stackの時は通常に制御できたのにライブラリをCORE2にした途端、まったく動かなくなります。
あらら・見捨てられたものかと思いましたが、実は・・

他のデバイス操作系のライブラリのソースを参照させていただいたところ
無印の場合、外部デバイスとの信号をやり取りする(I2C)場合は

```c++:信号取得(CORE2だと無効)
Wire.requestFrom(Faces_Encoder_I2C_ADDR, 3); // アドレスを指定して
if (Wire.available()) { // 存在すれば
  int increment = Wire.read();  // 信号取得
}
```

といった感じで `Wire`というクラスから操作するのですが、
実はCORE2から系統分岐して`Wire`(Wire0) `Wire1` `Wire2` という構成に変更されており、Faces Encoderは`Wire1`に分類されていました。

```c++:信号取得(CORE2で動作)
Wire1.requestFrom(Faces_Encoder_I2C_ADDR, 3); // アドレスを指定して
if (Wire1.available()) { // 存在すれば
  int increment = Wire1.read();  // 信号取得
}
```

`Wire`は「デバイスは存在しない」という正常状態で呼び出しされていた為、構成変更を把握していないと解明が厄介でした・・

デバイス操作系のライブラリがCORE2で軒並み動かなくなる現象のほとんどが上記の理由かと・（かといってGithubでコントリビュートするスキルが無い・・）
なので、お察しの通り、ライブラリ(`M5FacesEncoder`)は利用せずに、直接操作する記述となっています。

## 移植プログラム

結果以下の通りのプログラムとなりました。ご参考にどうぞ。
追記：後に気づいたのですが、HIDマウスコントロールに初動数ドット分のデッドゾーン（誤動作対策の為のキャンセリング）があります。避けたい場合はリンク先のOLDバージョンをご利用ください。

https://github.com/RAWSEQ/M5MouseWheel-BluetoothMouse

`M5Core2`以外のライブラリ：**ESP32-BLE-Mouse**

https://github.com/T-vK/ESP32-BLE-Mouse

**インストール方法**：最新版リリース（右側）のZIPファイルをダウンロードして、Arduino開発環境メニュー「`スケッチ > ライブラリをインクルード > .ZIP形式のライブラリをインクルード`」を選択。ダウンロードファイルを指定する。

```c++:M5MoutsWheel.ino
#include <M5Core2.h>
#include <BleMouse.h>

#define Faces_Encoder_I2C_ADDR     0X5E

#define MODE_X 1
#define MODE_Y 2
#define MODE_SCR 3
#define MODE_WX 4
#define MODE_WY 5
#define MODE_STEP 6

#define MODE_X_COLOR 0xff0000
#define MODE_Y_COLOR 0x3333ff
#define MODE_SCR_COLOR 0xff9900
#define MODE_WX_COLOR 0x33ff33
#define MODE_WY_COLOR 0x00cccc
#define MODE_STEP_COLOR 0xffffff

#define INTERVAL_TIMES_MIDDLE 500

bool ble_mode=false;
int st_mode=0;
int batt_percent=100;
int encoder_pos=0;
int m_step=1;
int st_b_l=0;
int st_b_r=0;
int st_b_m=0;
int iv_cnt_middle=0;

ButtonColors cl_on  = {0x7BEF, WHITE, WHITE};
ButtonColors cl_off = {BLACK, 0xC618, 0xC618};
ButtonColors cl_bl = {0x7BEF, 0xC618, 0xC618};
Button btn_x(110, 110, 100, 100, false ,"X", cl_off, cl_on);
Button btn_y(220, 110, 100, 100, false ,"Y", cl_off, cl_on);
Button btn_scr(0, 110, 100, 48, false ,"SCR", cl_off, cl_on);
Button btn_wx(0, 162, 48, 48, false ,"WX", cl_off, cl_on);
Button btn_wy(52, 162, 48, 48, false ,"WY", cl_off, cl_on);
Button btn_step(0, 6, 100, 100, false ,"STEP", cl_off, cl_on);
Button btn_l(170, 6, 65, 100, false ,"L", cl_off, cl_on);
Button btn_r(240, 6, 70, 48, false ,"R", cl_off, cl_on);
Button btn_m(240, 58, 70, 48, false ,"M", cl_off, cl_on);

BleMouse bleMouse("M5MouseWheel", "@RAWSEQ", batt_percent);

void setup() {
  // Setup M5
  M5.begin();
  M5.Lcd.fillScreen(BLACK);

  // Setup Buttons
  M5.Buttons.setFont(FSSB12);
  btn_x.setFont(FSSB24);
  btn_y.setFont(FSSB24);
  btn_x.addHandler(event_btn_x, E_RELEASE);
  btn_y.addHandler(event_btn_y, E_RELEASE);
  btn_scr.addHandler(event_btn_scr, E_RELEASE);
  btn_wx.addHandler(event_btn_wx, E_RELEASE);
  btn_wy.addHandler(event_btn_wy, E_RELEASE);
  btn_step.addHandler(event_btn_step, E_RELEASE);
  btn_l.addHandler(event_btn_l, E_RELEASE);
  btn_r.addHandler(event_btn_r, E_RELEASE);
  btn_m.addHandler(event_btn_m, E_RELEASE);
  M5.Buttons.draw();
  label_update_step();
  
  // Bluetooth Begin
  bleMouse.begin();
  label_change_status("Bluetooth Pairing...", RED);
}

void loop() {
  M5.update();

  // Interval Middle
  iv_cnt_middle++; if (iv_cnt_middle >= INTERVAL_TIMES_MIDDLE) { iv_cnt_middle = 0; }
  if (iv_cnt_middle == 0) {
    
    // Bluetooth
    bool diff_ble_mode = ble_mode;
    ble_mode = bleMouse.isConnected();
    if (diff_ble_mode != ble_mode) {
      if (ble_mode) {
        label_change_status("Bluetooth Connected.", BLUE);
      } else {
        label_change_status("Bluetooth Pairing...", RED);
      }
    }

    // Battery
    float batVoltage = M5.Axp.GetBatVoltage();
    int batPercent = ( batVoltage < 3.2 ) ? 0 : ( batVoltage - 3.2 ) * 100;
    if (batt_percent != batPercent) {
      bleMouse.setBatteryLevel(batPercent);
      batt_percent = batPercent;
    }
  }

  // Encoder Monitoring
  if (ble_mode) {
    Wire1.requestFrom(Faces_Encoder_I2C_ADDR, 3);
    if (Wire1.available()) {   
      int increment = Wire1.read();
      int diff_encoder_pos = encoder_pos;
      encoder_pos += ((increment > 127) ? (256 - increment) * -1 : increment);
      if (encoder_pos != diff_encoder_pos) {
        event_encoder_rotate(encoder_pos - diff_encoder_pos);
      }
    }
  }

  delay(10);
}

void label_change_status(char *msg, long color) {
  M5.Lcd.fillRect(0, 218, 320, 25, BLACK);
  M5.Lcd.setTextColor(color);
  M5.Lcd.drawString(msg, 0, 228, 1);
}

void label_update_step() {
  M5.Lcd.fillRect(105, 38, 50, 30, BLACK);
  char num_char[4];
  sprintf(num_char, "%d", m_step);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.drawString(num_char, 131, 53, 1);
}

void encoder_set_led(int i, long rgb) {
    delay(5);
    Wire1.beginTransmission(Faces_Encoder_I2C_ADDR);
    Wire1.write(i);
    Wire1.write((byte)(rgb>>16));
    Wire1.write((byte)(rgb>>8));
    Wire1.write((byte)(rgb));
    Wire1.endTransmission();
}

void encoder_set_led_all(long rgb) {
  for (int i = 0; i < 12; i++){
    encoder_set_led(i, rgb);
  }
}

void event_encoder_rotate(int incremental) {
  switch (st_mode) {
    case MODE_X:
      bleMouse.move(incremental*m_step,0);
      break;
    case MODE_Y:
      bleMouse.move(0,incremental*m_step);
      break;
    case MODE_SCR:
      bleMouse.move(0,0,incremental*m_step*-1);
      break;
    case MODE_WX:
      bleMouse.move(0,0,0,incremental*m_step);
      break;
    case MODE_WY:
      bleMouse.move(0,0,incremental*m_step);
      break;
    case MODE_STEP:
      m_step += incremental;
      label_update_step();
      break;
    default:
      break;
  }
}

void sub_set_mode(int mode, long color) {
  if (st_mode != mode) {
    btn_x.draw(cl_off);
    btn_y.draw(cl_off);
    btn_scr.draw(cl_off);
    btn_wx.draw(cl_off);
    btn_wy.draw(cl_off);
    btn_step.draw(cl_off);
    st_mode = mode;
    encoder_set_led_all(color);
  }
}

void event_btn_x(Event& e) {
  sub_set_mode(MODE_X, MODE_X_COLOR);
  btn_x.draw(cl_bl);
}
void event_btn_y(Event& e) {
  sub_set_mode(MODE_Y, MODE_Y_COLOR);
  btn_y.draw(cl_bl);
}
void event_btn_scr(Event& e) {
  sub_set_mode(MODE_SCR, MODE_SCR_COLOR);
  btn_scr.draw(cl_bl);
}
void event_btn_wx(Event& e) {
  sub_set_mode(MODE_WX, MODE_WX_COLOR);
  btn_wx.draw(cl_bl);
}
void event_btn_wy(Event& e) {
  sub_set_mode(MODE_WY, MODE_WY_COLOR);
  btn_wy.draw(cl_bl);
}
void event_btn_step(Event& e) {
  sub_set_mode(MODE_STEP, MODE_STEP_COLOR);
  btn_step.draw(cl_bl);
}
void event_btn_l(Event& e) {
  if (st_b_l) {
    bleMouse.release(MOUSE_LEFT);
    btn_l.draw(cl_off);
    st_b_l = 0;
  } else {
    bleMouse.press(MOUSE_LEFT);
    btn_l.draw(cl_bl);
    st_b_l = 1;
  }
}
void event_btn_r(Event& e) {
  if (st_b_r) {
    bleMouse.release(MOUSE_RIGHT);
    btn_r.draw(cl_off);
    st_b_r = 0;
  } else {
    bleMouse.press(MOUSE_RIGHT);
    btn_r.draw(cl_bl);
    st_b_r = 1;
  }
}
void event_btn_m(Event& e) {
  if (st_b_m) {
    bleMouse.release(MOUSE_MIDDLE);
    btn_m.draw(cl_off);
    st_b_m = 0;
  } else {
    bleMouse.press(MOUSE_MIDDLE);
    btn_m.draw(cl_bl);
    st_b_m = 1;
  }
}

```

ファイル一枚のプログラムコードで、Bluetoothマウスが作れるなんて・・いい時代が来たものです＾＾