M5 / プログラミング

M5StickCでガーデニングの水やりセンサを作る ~その3 【スリープで長時間稼働】

こんにちは、Lineaです 

前回は計測した水分量データをAmbientというIoTログサービスに送信してデータの可視化まで行いました。ですが、課題としてM5StickCの内蔵バッテリーで常時ネットワーク接続して稼働させ続けると1時間弱しか保ちませんでした。
今回はM5StickCのスリープ機能を使って解決してみようと思います。計測してデータ送信⇒15分スリープ⇒計測してデータ送信…と繰り返す動作を作っていきます。
スリープ機能はUIFlowでは使用できなかったので、今回からArduino環境(C/C++)でつくっていきます

前回の記事はこちらです

M5StickCのスリープ機能

M5StickC(に内蔵されるマイコン)には2段階のスリープがあります。

  • ディープスリープ
  • ライトスリープ

どちらも復帰にはタイマーやボタンの入力が使えます。一定時間後やボタンが押されたらといった条件で復帰させることができます。
2つのスリープの差は電力以外に復帰時の挙動に大きな違いがあります。

ディープスリープ

名前の通り機能をほぼ停止させることで電力消費を大きく抑えます。
復帰時にはプログラムを最初から実行し直すことになります。また、データもスローメモリという特殊領域に保存しておかないと維持されないです。スリープから復帰したかどうかを判別する事はできるのでプログラムの先頭でスリープ復帰時に処理を切り替える事はできます。

ライトスリープ

ディープスリープより色々停止しないので消費電力は少し大きくなります。その代わりスリープの復帰時に継続してプログラムが動作します。変数なども普通に維持された状態で復帰します。どちらかというとこっちの方が「スリープ」っぽい動作ですね。

今回は実行する機能としては数十分おきに測って送信するだけなので、状態を維持する必要がないのでディープスリープを使います。

プログラミング

最初にも書きましたがUIFlowではスリープが使えないのでC言語でプログラミングします。
M5StickCはArduino環境でC言語でプログラミングできます。ArduinoIDEでもプログラミングできますが、自分はVSCode+PlatformIOを使用しています。そのうちPlatformIOでのスタート手順も書かないとですね。

今回作成したプログラムがこちらです。

#include <M5StickCPlus.h>
#include <WiFi.h>
#include <Ambient.h>

//define
//pin定義
const uint8_t kEarthPin = 33;
const uint8_t kEarthDigiPin = 32;
//スリープ時間
const uint sleepMin = 15; //15分
//wifi情報
const char wifi_ssid[32]="SSID";
const char wifi_key[64]="アクセスポイントのキー";
//ambient情報
const unsigned int channelId = 0; // AmbientのチャネルID
const char* writeKey = "ライトキー"
//static変数
WiFiClient client;
Ambient ambient;

//functions
//測定
void measureEarth(){
  //濡れているほうが値が低くなるので反転させる
  uint16_t earthVal = 1024 - analogRead(kEarthPin);
  int earthDigi = digitalRead(kEarthDigiPin);

  //表示
  M5.lcd.fillScreen(BLACK);//クリア
  M5.Lcd.setTextColor(WHITE, BLACK);
  int32_t drawX=4 ,drawY = 16;
  char valStr[32];
  M5.lcd.drawString("Earth Sensor",drawX,drawY);
  drawY += 24;
  sprintf(valStr,"value : %d",earthVal);
  M5.lcd.drawString(valStr,drawX,drawY);
  drawY += 24;
  M5.lcd.fillRect(drawX,drawY,(earthVal/1024.0)*(M5.Lcd.width()-32),16,BLUE);
  drawY += 24;
  sprintf(valStr,"wet? : %d",earthDigi);
  M5.lcd.drawString(valStr,drawX,drawY);
  drawY += 24;
  sprintf(valStr,"wifi? : %d",WiFi.status());
  M5.lcd.drawString(valStr,drawX,drawY);

  //Ambient送信
  ambient.set(1,earthVal);
  ambient.set(2,earthDigi);
  ambient.send();
  
}

//カウントダウン
void countDown(const char *dispTxt,const int sec)
{
  int count = sec;
  char countStr[16];
  M5.Lcd.setTextColor(WHITE, GREEN);
  M5.Lcd.fillRect(24,24,M5.Lcd.width()-48,M5.Lcd.height()-48,GREEN);
  while (count >= 0)
  {
    sprintf(countStr,"%s %ds...",dispTxt,count);
    M5.lcd.drawCentreString(countStr,M5.Lcd.width()/2,M5.Lcd.height()/2,2);
    count--;
    //1秒待つ
    delay(1000);
  }
}

//setup関数 ディープスリープ復帰時もここから再開する
void setup() {
  //M5スタート
  M5.begin();
  M5.Lcd.setRotation(1);
  // 画面をつける(スリープ復帰用)
  M5.Axp.ScreenBreath(10);

  //水分センサのピンをセットアップ
  pinMode(kEarthPin,ANALOG);
  pinMode(kEarthDigiPin,INPUT);

  //アナログ値の解像度を1024段階に設定
  analogSetWidth(10); //10bit(1024)

  //文字サイズを少し大きくしておく
  M5.lcd.setTextFont(2);

  // WiFi接続
  Serial.printf("Connecting to %s ", wifi_ssid);
  WiFi.begin(wifi_ssid, wifi_key);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("CONNECTED");

  //Ambient setup
  ambient.begin(channelId, writeKey, &client); // チャネルIDとライトキーを指定してAmbientの初期化
}

void loop() {
  //計測カウントダウン
  countDown("measure",5);

  //計測
  measureEarth();
  
  //wait
  delay(5000);

  //スリープカウントダウン
  countDown("sleep",5);

  //スリープ
  Serial.println("Enter sleep");
  // 画面を消す
  M5.Axp.ScreenBreath(0);
  esp_sleep_enable_timer_wakeup(SLEEP_MIN(sleepMin));
  esp_deep_sleep_start();
}

重要なところをいくつか解説します

スリープ処理
//スリープ時間
const uint sleepMin = 15; //15分

//setup関数 ディープスリープ復帰時もここから再開する
void setup() {
  //中略...
  // 画面をつける(スリープ復帰用)
  M5.Axp.ScreenBreath(10);
}

void loop() {
  //計測
  measureEarth();
 //中略...

  //スリープ
  Serial.println("Enter sleep");
  // 画面を消す スリープに入るだけでは画面は点いたままになる
  M5.Axp.ScreenBreath(0);
  //スリープ復帰方法を設定 タイマー版
  esp_sleep_enable_timer_wakeup(SLEEP_MIN(sleepMin));
 //スリープ開始
  esp_deep_sleep_start();
}

スリープはesp_sleep_enable_xxx_wakeup()で復帰方法を指定後にesp_deep_sleep_start()で開始します。xxxの部分は復帰方法毎に関数が分かれています。今回は復帰方法にタイマーを使用します。タイマーの時間はマイクロ秒で指定するので注意が必要です。SLEEP_MIN()など変換マクロ関数が用意されているのでこちらを使うと指定が楽です。

ディープスリープからの復帰はsetup()からやり直されることになるのでこの中に復帰した際の処理はここに書く必要があります。

スリープでは液晶画面のオンオフは管理されないので、スリープ突入時に画面をオフにする場合はM5.Axp.ScreenBreath()で画面の明るさを0にして消す必要があります。また、スリープからの復帰時には再度ScreenBreath()で明るさを設定してオンにする必要がありますのでsetup()に書いてあります。

Wifi & Ambient
//ヘッダ
#include <WiFi.h>
#include <Ambient.h>
//wifi情報
const char wifi_ssid[32]="SSID";
const char wifi_key[64]="アクセスポイントのキー";
//ambient情報
const unsigned int channelId = 0; // AmbientのチャネルID
const char* writeKey = "ライトキー"
//static変数
WiFiClient client; //Ambientで使うので必要
Ambient ambient;

void setup() {
  // WiFi接続
  WiFi.begin(wifi_ssid, wifi_key);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  //Ambient setup
  ambient.begin(channelId, writeKey, &client); // チャネルIDとライトキーを指定してAmbientの初期化
}

Ambientに送信するにはWifi接続も必要なので合わせて解説します。Ambientを使うにはライブラリをインストールする必要があります。ArduinoIDEやPlatformIOのライブラリ検索で「Ambient ESP32 ESP8266 lib」を検索してインストールします。インストールできたらWifi.hAmbient.hをインクルードしておきます。

ENVⅡHAT
https://www.switch-science.com/catalog/6559/

WiFi.begin()の引数にSSIDとパスワードを渡して呼ぶことで接続します。接続には時間がかかるので、すぐに次の処理を始めてしまうとエラーの原因になります。WiFi.status()で状態を取得して、接続状態WL_CONNECTEDになるまでループして接続できるまで待機します。

AmbientはAmbient型のクラスから制御します。static変数などでインスタンスを作っておき、Ambient::begin()にチャネルIDとライトキーを渡して初期化します。 Ambient::begin() にはWifiClientのポインタも渡す必要がありますのでWifi接続後に呼ぶ必要があります。

測定 & データ送信

//pin定義
const uint8_t kEarthPin = 33; //アナログ値ピン
const uint8_t kEarthDigiPin = 32; //デジタル値ピン

void setup() {
  //水分センサのピンをセットアップ
  pinMode(kEarthPin,ANALOG);
  pinMode(kEarthDigiPin,INPUT);
  //アナログ値の解像度を1024段階に設定
  analogSetWidth(10); //10bit(1024)
}

void measureEarth(){
 //計測
  //濡れているほうが値が低くなるので反転させる
  uint16_t earthVal = 1024 - analogRead(kEarthPin); //アナログ値
  int earthDigi = digitalRead(kEarthDigiPin); //デジタル値

  //Ambient送信
  ambient.set(1,earthVal); //データスロット1
  ambient.set(2,earthDigi); //データスロット2
  ambient.send();
  
}

水分センサの値は接続しているピンの値を直接読み取ります。32,33ピンが対応するピンです。32はデジタル値で一定値を境に0-1で取得できます。33はアナログ値で細かい数値が取得できます。基本的には細かく取れる33ピンの方を使用すると思います。pinMode()でピンとモードを適したものに設定します。
アナログ値はanalogSetWidth()でAD変換の解像度を設定することで値の範囲を変更できます。デフォルトでは0-4096になっていますが、UIFlowでは0-1024だったのでそれに合わせて変更しています。

センサからの値の取得はanalogRead(),digitalRead()でそれぞれ設定したピンから取得します。
Ambientへのデータ送信はAmbient::set()にデータのスロットと値を渡してセットします。Ambient::send()でセットした値を送信します。

動作

動かしてみたところをGif動画にしてみました。

Wifiの接続完了後に計測までのカウントダウンが表示されます。Wifi接続までは数秒かかります。接続中の表示を足しても良かったかもしれないです。
測定後にカウントダウンしてスリープに入ります。画面もオフにしているので見た目では電源が切れているのと差がわからないのが難点ですね。運用上はプランターに挿して放置するので目視確認することもないので気にしない方針。

さて、肝心の稼働時間です。Ambientのデータにどれだけデータ送信が続けられたかで確認します。

満充電で17:30ごろから動かして24:30ごろまでデータが送信されていました。内蔵バッテリーだけで約7時間ほど稼働できました。前回の1時間に比べれば7倍ですのでかなり伸ばすことができました!!
これならモバイルバッテリーを接続することで数日は動かし続けられそうです。計測の間隔を伸ばせば実行時間が減るので更に稼働時間を増やせるかもしれないですね。

今後

今回で計測のプログラムとしては目的が達成できました。
課題としてはM5StickCやセンサーが雨風に耐えられるような対策ですね。今の所むき出しなので、晴れた日にしか計測できないのでIoTなシステムとしてはまだまだです。密閉できるケースをDIYしようと思います。

実際に外で動かす際は画面も特にいらないので画面のないM5Atomを買い足してそちらで動かそうかなとも思っています。システムとして動かすとその間M5StickCで遊べないのでそれも困りますしね。

雨風対策なども記事にできたらと思います。それではまた!

投稿者

Linea
本業はゲームプログラマー プログラミングやIT系の記事を書いていきます アイコンは愛車のシトロエン DS3です

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA