M5ATOM(ESP32)でスクリーンセイバーキラーを4パターンのインターバル処理で作ってみる

ArduinoIDE環境用の **ESP32をBLEマウスにするライブラリ** があったので、これを使って定期的にマウスポインタを動かす「スクリーンセイバーキラー」を作成しました。

# 作ったもの
デバイスは **[M5Stack](https://m5stack.com/ "M5Stack") の小型モジュールである [M5ATOM Lite](https://m5stack.com/collections/m5-atom/products/atom-lite-esp32-development-kit?variant=32259605200986 "M5ATOM Lite")** を使用しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">動作時の動画撮ったけど地味すぎるw <a href="https://t.co/xoKYCe2iHs">pic.twitter.com/xoKYCe2iHs</a></p>&mdash; tomorrow56 (@tomorrow56) <a href="https://twitter.com/tomorrow56/status/1314074110540935169?ref_src=twsrc%5Etfw">October 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

M5ATOM LiteはESP32・フルカラーLED・ボタンが2.5x2.5cmのケースにコンパクトに納められており、オフィシャルストアで$5.95、 [スイッチサイエンスで買っても968円](https://www.switch-science.com/catalog/6262/)とコストパフォーマンスが非常に良いのでお勧めです。

# 作り方

せっかくなので、**マウスポインタの移動の処理を変えて4パターン(loop()でdelay, タイマー割り込み, マルチタスク, Deep Sleepのタイマー復帰)**で作ってみます。

なお、ソースコード(sketch)はgithubで公開していますので好きにアレンジして使ってください。
[M5Atom Screen Saver Killer](https://github.com/tomorrow56/M5Atom_SSKiller "M5Atom Screen Saver Killer")

## ESP32 BLE Mouse libraryの導入
T-vk氏がgithubで公開しているので、ダウンロードしてArduino IDEのLibraryフォルダに解凍します。
[ESP32 BLE Mouse library](https://github.com/T-vK/ESP32-BLE-Mouse "ESP32 BLE Mouse library")
examplesに **ScrollAndMoveMouse** というスケッチがあるので、これを参考に作成しました。

## ざっくりした仕様
とりあえず決めた仕様はこれだけです。

- マウスポインタ移動のインターバルと移動量は変数で持たせる(デバッグしやすいので)
- PCとの接続状態やマウスポインタ移動の動作はM5ATOM Lite上のLEDの色を変えて知らせる

以降のスケッチはデバッグをしやすくするために5秒周期としています。この辺はお好みで自由に変更してください。

## パターン1: シンプルに「loop()でdelay」で周期的にマウスポインタを動かす

まずはシンプルに、loop()関数内にインターバルの時間を **delay** で記述してみました。
基本動作はこれでOKなのですが、loop()関数内でdelayさせているのでその間は処理を受け付けなくなります。
このスケッチでは、インターバル中にBLE接続が解除されてもLEDは緑のままなのでちょっと不便です。

```M5Atom_SSKiller_simple.ino
#include "M5Atom.h"
#include <BleMouse.h>  // https://github.com/T-vK/ESP32-BLE-Mouse

BleMouse bleMouse("M5Atom SSKiller");  // デバイス名

int amount = 100;  // ポインタの移動量
int interval_sec = 5;  // ポインタ移動のインターバル(秒で指定)

void setup() {
  M5.begin(true, false, true);

  // Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleMouse.begin();
}

void loop() {
  if(bleMouse.isConnected()) {

    unsigned long startTime;

    M5.dis.drawpix(0, 0x0000f0);  // ポインタ移動中はLEDを青にする

    // マウスポインタを動かす(ここでは5point分上下に往復)
    Serial.println("Move mouse pointer");
    startTime = millis();
    while(millis()<startTime + amount){
      bleMouse.move(0,-1);
      delay(20);
    }

    startTime = millis();
    while(millis()<startTime + amount) {
      bleMouse.move(0,1);
      delay(20);
    }

    M5.dis.drawpix(0, 0xf00000);  // BLEで接続されていたらLEDを緑にする
    delay(interval_sec * 1000);
  }else{
    M5.dis.drawpix(0, 0x00f000);  // BLEで接続されていない時はLEDを赤にする
  }
}
```

## パターン2: マウスポインタの移動をタイマー割り込みで実行する

インターバル期間中もBLEの接続の状態を通知するために、定期的にマウスポインタを移動する部分の処理を**タイマー割り込み**で呼び出すようにしてみました。
タイマー割り込みでマウス移動フラグを設定、Loop()関数内でフラグをチェックしてマウスポインタを動かしています。
これで、BLE接続状態はインターバル期間中にもLEDに反映されるようになります。

```M5Atom_SSKiller_timer.ino
#include "M5Atom.h"
#include <BleMouse.h>  // https://github.com/T-vK/ESP32-BLE-Mouse

BleMouse bleMouse("M5Atom SSKiller");  // デバイス名

int amount = 100;  // ポインタの移動量
int interval_sec = 5;  // ポインタ移動のインターバル(秒で指定)
volatile boolean bleEnabled = false;  // BLEの接続状態
volatile boolean moveFlag = false;  // マウス移動フラグ

// Timer Interrupt setting
hw_timer_t * timer = NULL;

volatile SemaphoreHandle_t timerSemaphore;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// タイマー割り込み発生時に呼び出される関数
void IRAM_ATTR onTimer(){
  // Set the pointer move flag when BLE is enable
  portENTER_CRITICAL_ISR(&timerMux);
  
  if(bleEnabled == true){
    moveFlag = true;  // タイマー割り込みが発生したらマウス移動フラグをセットする
  }

  portEXIT_CRITICAL_ISR(&timerMux);
  // Give a semaphore that we can check in the loop
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
  // It is safe to use digitalRead/Write here
  // if you want to toggle an output
}

// マウスポインタ移動処理を関数化
void movePointer(){
  unsigned long startTime;
  M5.dis.drawpix(0, 0x0000f0);  // LEDを青にする
  Serial.println("Move mouse pointer");

  // マウスポインタを動かす
  startTime = millis();
  while(millis()<startTime + amount){
    bleMouse.move(0,-1);
    delay(20);
  }
  startTime = millis();
  while(millis()<startTime + amount) {
    bleMouse.move(0,1);
    delay(20);
  }
  M5.dis.drawpix(0, 0xf00000); // LEDを緑にする
}

void setup(){
  M5.begin(true, false, true);

  // Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleMouse.begin();

  // Create semaphore to inform us when the timer has fired
  timerSemaphore = xSemaphoreCreateBinary();
  // Use 1st timer of 4 (counted from zero).
  // Set 80 divider(80MHz/80 = 1MHz) for prescaler
  // (see ESP32 Technical Reference Manual for more info).
  timer = timerBegin(0, 80, true);
  // Attach onTimer function to our timer.
  timerAttachInterrupt(timer, &onTimer, true);
  // Set alarm to call onTimer function(value in microseconds).
  // Repeat the alarm (third parameter)
  timerAlarmWrite(timer, interval_sec * 1000000, true);
  // Start an alarm
  timerAlarmEnable(timer);
}

void loop(){
  bleEnabled = bleMouse.isConnected();
  
  if(bleEnabled == true){
    M5.dis.drawpix(0, 0xf00000);  // BLEで接続されていたらLEDを緑にする
  }else{
    M5.dis.drawpix(0, 0x00f000);  // BLEで接続されていない時はLEDを赤にする
  }

  // マウス移動フラグがセットされていたらマウスポインタを動かす
  if(moveFlag == true){
    movePointer();
    moveFlag = false;
  }

  delay(100);
}
```

## パターン3: マウスポインタの移動とBLE接続状態表示をマルチタスク化する

次に、BLEの接続の状態の通知と、マウスポインタの移動処理を**それぞれ独立したタスク**で処理してみました。
"task0"でBLE接続状態のフラグを設定し、"task1"でこのフラグを見てBLE接続時にマウスポインタを動かしています。

```M5Atom_SSKiller_task.ino
#include "M5Atom.h"
#include <BleMouse.h>  // https://github.com/T-vK/ESP32-BLE-Mouse

BleMouse bleMouse("M5Atom SSKiller");  // デバイス名

int amount = 100;  // ポインタの移動量
int interval_sec = 5;  // ポインタ移動のインターバル(秒で指定)
volatile boolean bleEnabled = false;  // BLEの接続状態

// BLE接続状態確認
void task0(void* arg) {
  while (1) {
    bleEnabled = bleMouse.isConnected();
    if(bleEnabled == true){
      M5.dis.drawpix(0, 0xf00000);  // BLEで接続されていたらLEDを緑にする
    }else{
      M5.dis.drawpix(0, 0x00f000);  // BLEで接続されていない時はLEDを赤にする
    }
    vTaskDelay(100);
  }
}

// マウスポインタを周期的に動かす
void task1(void* arg) {
  while (1) {
    if(bleEnabled == true){
      movePointer();
    }
    vTaskDelay(interval_sec * 1000);  // マウスポインタ移動のインターバル
  }
}

void movePointer(){
  unsigned long startTime;
  M5.dis.drawpix(0, 0x0000f0);  // LED Blue
  Serial.println("Move mouse pointer");

  startTime = millis();
  while(millis()<startTime + amount){
    bleMouse.move(0,-1);
    vTaskDelay(20);
  }
  startTime = millis();
  while(millis()<startTime + amount) {
    bleMouse.move(0,1);
    vTaskDelay(20);
  }
  M5.dis.drawpix(0, 0xf00000);  // LED Green
}

void setup(){
  // M5.begin(SerialEnable, I2CEnable, DisplayEnable);
  M5.begin(true, false, true);

  // Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleMouse.begin();

  // xTaskCreatePinnedToCore(func,"name",Stuck size,NULL,priority,Task pointer,Core ID)
  // Core ID: 0 or 1 or tskNO_AFFINITY
  xTaskCreatePinnedToCore(task0, "Task0", 4096, NULL, 1, NULL, tskNO_AFFINITY);
  xTaskCreatePinnedToCore(task1, "Task1", 4096, NULL, 1, NULL, tskNO_AFFINITY);
}

void loop(){
}
```

## パターン4: Deep Sleepから定期的に復帰してマウスポインタを動かす

モバイルバッテリーで動かしたかったので、省電力のためにマウスポインタを移動させていない時はDeep Sleepに入れるようにしてみました。
Deep Sleepからの復帰時に毎回BLEを再接続に行くので、インターバルが短いと省電力効果がないので要注意です。
私の環境では一番最初にPCとペアリングする際に、BLE接続が完了する前にDeep Sleepに入ってしまってうまく動かなかったので、**最初にPCとペアリングする時にはM5ATOM Liteの前面のボタンを押すことで、Deep Sleepに入るまでの時間を5秒延長しています。**ここは、お使いの環境に合わせて調整してください。

```M5Atom_SSKiller_Sleep.ino
#include "M5Atom.h"
#include <BleMouse.h>  // https://github.com/T-vK/ESP32-BLE-Mouse

BleMouse bleMouse("M5Atom SSKiller");  // デバイス名

#define uS_TO_S_FACTOR 1000000ULL  // Conversion factor for micro seconds to seconds

int amount = 100;  // ポインタの移動量
int interval_sec = 5;  // ポインタ移動のインターバル(秒で指定)
volatile boolean bleEnabled = false;  // BLEの接続状態
volatile boolean blePearing = false;  // ペアリング実施フラグ

// マウスポインタを移動して、Deep Sleepへ入る関数
void movePointerAndSleep(){
  unsigned long startTime;
  M5.dis.drawpix(0, 0x0000f0);  // LED Blue
  Serial.println("Move mouse pointer");

  startTime = millis();
  while(millis()<startTime + amount){
    bleMouse.move(0,-1);
    delay(20);
  }
  startTime = millis();
  while(millis()<startTime + amount) {
    bleMouse.move(0,1);
    delay(20);
  }

  // PCとペアリングする時は5秒待つ
  if(blePearing == true){
    delay(5000);
  }
  M5.dis.drawpix(0, 0x000000);  // LED OFF
  delay(100);  // LEDが消えるのを待つ
  Serial.println("Going to sleep now");
  Serial.flush(); 
  esp_deep_sleep_start();
  Serial.println("This will never be printed");
}

//Print the wakeup reason for ESP32
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;

  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch(wakeup_reason)
  {
    case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
    case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
  }
}

void setup(){
  // M5.begin(SerialEnable, I2CEnable, DisplayEnable);
  M5.begin(true, false, true);

  M5.dis.drawpix(0, 0x000000);  // LED OFF

  // Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleMouse.begin();

/*    
 //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));
*/

  print_wakeup_reason();

  // Set wakeup interval
  esp_sleep_enable_timer_wakeup(interval_sec * uS_TO_S_FACTOR);
  Serial.println("Setup ESP32 to sleep for every " + String(interval_sec) + " Seconds");
}

void loop(){
  M5.update();
  bleEnabled = bleMouse.isConnected();

  // ボタンが押されたらペアリングフラグをセットする
  if (M5.Btn.wasPressed()){
    if(bleEnabled == false){
      M5.dis.drawpix(0, 0xf0f000);  // LED Yellow
      blePearing = true;
      Serial.println("Wait Pairing..");
      delay(1000);
    }
  }
  
  if(bleEnabled){
    M5.dis.drawpix(0, 0xf00000);
    movePointerAndSleep();
  }else{
    if(blePearing == true){
      M5.dis.drawpix(0, 0xf0f000);  // LED Yellow
    }else{
      M5.dis.drawpix(0, 0x00f000);
    }
  }

  delay(100);
}
```

# まとめ
M5ATOM LiteはDeep Sleepでも12mA消費しているので、個人的には応用もきくマルチタスク処理がお気に入りです。
「LGTM」や「ストック」、SNSでのシェアをしていただくと筆者の励みになるのでよろしくお願いします。