実用的な高度計を作成する ~ M5StickCで腕時計型高度計
以前にM5StickCでBME280を使用した高度計を作成しましたが、バッテリー切れが早い欠点がありました。
M5StickCは、ディスプレイが付いてバッテリーも内蔵するため、実用性の高い電子工作が可能です。これがRaspberry Piであれば、表示機やバッテリー、ボタンなどをどうしようか、いろいろ迷ってしまいます。また、ポータブルでコンパクトでなければなりません。対してM5StickCなら多くが内蔵しているので、理想に近いマイコンです。
しかしバッテリーが付いているとは言え、何も考えないでプログラムを作成すると、1時間位でバッテリーが切れてしまいます。これでは、とても実用に耐えません。表示を暗くしたり、CPUの動作を抑える方法もありますが、やはり2~4時間程度が限度かなと思います。
そこで、バッテリーが1日は持つ高度計の作成を行ってみました。
1.DeepSleepを使う
バッテリーを節約するために、高度や時間を見たい時だけM5StickCを動作させます。それ以外は休止状態にします。そこで、DeepSleepという機能を使います。これは、以前の高度計(BME280を使わない)でもやりました。
// ディープスリープスタート
M5.Axp.SetSleep();
esp_deep_sleep_start();
今回も、DeepSleepを利用します。
2.時刻の取得
M5StickCにはRTCによる時計機能がありますが、バッテリー切れを頻繁に起こすので、電源オン時に時刻を取得して、RTCに設定する必要があります。しかし、あくまでも電源オンの時だけ時間合わせをすればいいので、DeepSleepからの復帰では時刻合わせを行いません。
esp_sleep_wakeup_cause_t wakeup ;
wakeup = esp_sleep_get_wakeup_cause();
switch(wakeup){
case ESP_SLEEP_WAKEUP_EXT0 :
// DeepSleepから復帰した場合
また時刻合わせは、ネットから取得します。そのため、ネット環境を必要とします。
struct tm timeInfo;
getLocalTime(&timeInfo) ;
3.高度補正機能を付ける
実は、一番の問題点が高度補正機能です。高度は気圧計(BME280)から取得するので、気圧によって高度が上下してしまいます。そのため、高度補正が必要です。
これをどうするか悩みました。M5StickCには、電源ボタン以外に2つボタン(ボタンA、ボタンB)があるだけです。その2つのボタンで高度を+-するのですが、DeepSleepの復帰時にもボタンを押す必要があるので、その切り分けがうまくいきませんでした。長押しと組み合わせればいいかなと思ったのですが、うまくいかなかったのです。
そこで、電源ON時に限り、2~30秒の間だけ高度補正ができるようにしました。その間は高度表示が白で表示され、 ボタンAとボタンBで高度を+-できます。時間が過ぎると高度表示は緑になり、高度5秒、時計5秒を表示したのち、DeepSleepします。
pinMode(GPIO_NUM_37, INPUT_PULLUP);
esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
DeepSleepからの復帰は、ボタンAを押します。押した後は同じく、高度5秒、時計5秒を表示したのち、DeepSleepします。
これで、バッテリーの消費を最小化しました。
4.コード
日付の処理が少し入っていますが、今回は日付の表示を省いています。ご了承ください。
#include <M5StickC.h>
#include <Wire.h>
#include "Adafruit_Sensor.h"
#include <Adafruit_BME280.h>
#include <math.h>
Adafruit_BME280 bme;
#include <WiFi.h>
#include <time.h>
const char* g_ssid = "xxxxxxxxxxxx";
const char* g_password = "xxxxxxxxxxxx";
const char* g_ntpServer = "ntp.jst.mfeed.ad.jp" ;
const long g_gmtOffset = 9 * 3600 ;
RTC_TimeTypeDef g_RTC_Time;
RTC_DateTypeDef g_RTC_Date;
bool g_isPowerON = false ;
RTC_DATA_ATTR int g_hosei = 0 ;
int g_editTimeCount = 0 ; // 修正できる時間 (500ms × 回数)
float getAltitude(float wkTemp, float wkPress) {
float wkAltitude = 0;
float seaAltitude = (float)1013.25 ;
float PressJyou = (float)1 / (float)5.257 ;
float wkPressHi = seaAltitude / wkPress ;
float wkPress2 = powf(wkPressHi, PressJyou) ;
wkPress2 = wkPress2 - (float)1 ;
float wkTemp2 = wkTemp + 273.15 ;
wkAltitude = wkPress2 * wkTemp2 / (float)0.0065 ;
return wkAltitude ;
}
void DispLCD(float wAltitude) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 10, 7);
if (g_isPowerON == false) {
M5.Lcd.setTextColor(GREEN);
M5.Lcd.printf("%04.0f", wAltitude) ;
delay(5000);
} else {
M5.Lcd.setTextColor(WHITE);
M5.Lcd.printf("%04.0f", wAltitude) ;
delay(500);
}
if (g_isPowerON == false) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(GREEN);
M5.Lcd.setCursor(0, 10, 7);
M5.Rtc.GetTime(&g_RTC_Time);
M5.Lcd.printf("%02d:%02d", g_RTC_Time.Hours, g_RTC_Time.Minutes);
delay(5000);
}
}
void setup() {
// put your setup code here, to run once:
M5.begin();
Wire.begin(0,26);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
esp_sleep_wakeup_cause_t wakeup ;
wakeup = esp_sleep_get_wakeup_cause();
switch(wakeup){
case ESP_SLEEP_WAKEUP_EXT0 :
Serial.printf("deepsleepから起動\n");
g_isPowerON = false ;
break; // スリープ復帰時はWiFiで時刻取得しない
default :
Serial.printf("電源ONから起動\n");
// Wi-Fiから時刻を取得
M5.Lcd.setCursor(0, 0, 1);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.print("Wi-Fi Connecting");
WiFi.begin(g_ssid, g_password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
M5.Lcd.print(".");
}
M5.Lcd.println("Wi-Fi Connected");
struct tm timeInfo;
if (getLocalTime(&timeInfo)) {
RTC_TimeTypeDef wTime;
wTime.Hours = timeInfo.tm_hour;
wTime.Minutes = timeInfo.tm_min;
wTime.Seconds = timeInfo.tm_sec;
M5.Rtc.SetTime(&wTime);
RTC_DateTypeDef wDate;
wDate.WeekDay = timeInfo.tm_wday;
wDate.Month = timeInfo.tm_mon + 1;
wDate.Date = timeInfo.tm_mday;
wDate.Year = timeInfo.tm_year + 1900;
M5.Rtc.SetData(&wDate);
}
g_isPowerON = true ;
break ;
} // end switch
if (!bme.begin(0x76)){
Serial.println("BME280を検出できません");
while (1);
}
pinMode(GPIO_NUM_10, OUTPUT);
digitalWrite(GPIO_NUM_10, HIGH);
}
void loop() {
float temp, altitude, pressure, humid;
// 一応、BME280では以下の取得ができます
temp = (float)bme.readTemperature();
pressure = (float)bme.readPressure() / 100;
altitude = getAltitude(temp, pressure) + g_hosei ;
humid = (float)bme.readHumidity() ;
DispLCD(altitude);
if (g_isPowerON) {
// 高度の補正が可能
if (g_editTimeCount++ > 50) {
// 修正モード終了
g_isPowerON = false ;
Serial.println("End of Edit Mode");
} else {
Serial.println("Edit Mode");
if ( digitalRead(M5_BUTTON_HOME) == LOW ) {
// +補正
g_hosei += 10 ;
Serial.print("+");
} else if (digitalRead(M5_BUTTON_RST) == LOW ) {
// -補正
g_hosei -= 10 ;
Serial.print("-");
}
}
} else {
// ボタンAでスリープ復帰
pinMode(GPIO_NUM_37, INPUT_PULLUP);
esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
// ディープスリープスタート
M5.Axp.SetSleep();
esp_deep_sleep_start();
}
}
5.実用性はあるけれど筐体を何とかしたい
使ってみると、なかなかいい感じです。高度補正ですが、delay関数を使っているので、押しても反応しない場合があります。これは、少し長押し気味に押して反応させると良いでしょう。もっといい方法にした方が良いでしょうが、これでも十分操作できます。
また、高度修正が可能な時にマイナスの高度が表示される時は3桁の表示になってしまいますが、今回のコードでは対処していません。ご了承ください。
今回やってみて一番の問題は、M5StickCとセンサー部を合わせると、横に長いことです。これで腕に着けて歩くのは、少し邪魔です。自作のセンサーではなくてEnv Hatを購入すればマシになりますが、それでも邪魔になりそうです。(今回は右手に装着することを前提にしています。左手の場合は180度回転した表示にしてください)
これはあくまでもプロトタイプで、3Dプリンタで筐体を作り、ESP32、バッテリー、センサー、ボタンを配したパッケージを作る必要があるでしょう。でも、さすがにそこまでして作成するつもりはありません。
なかなか日常で使い続ける工作はできません。これからも考えて行こうと思います。