1週目
注釈
以下の項目について、全員がすべてを試す必要はありません (時間が足りなくなる可能性が高いです)。役割分担をして、手分けして動作の確認と、同じ班のメンバーへの動作説明・共有をして、すすめてください。必要に応じて、検証する機能を絞り込んでください。なお、一部のサンプルプログラムは編集しないと意図通りに動作しない場合があります。
電源の切り方・入れ方
電源を入れるには、電源ボタンを、2秒間 長押しします。
電源を 完全に 切るには、USBを外したうえで 、電源ボタン(M5と書かれたAボタンの左側面)を、5秒間 長押しします。緑色のLEDが点灯したら、ボタンを離します。
動画をみる→ https://youtu.be/Lo1jZbAeT8Y
M5StickCガイド の、4ページ目、ハードウェアの概要も参考になります。
プログラムの書き込み方
講義システム のサンプルソースコードについて、ビルド済みバイナリをダウンロードして書き込むには、Git Bashやターミナルから、以下のように入力してください。
$ cd
$ cd m5scp2_exp (実験フォルダに移動)
$ ./upload_by_hash.sh (ハッシュ値)
例→ ./upload_by_hash.sh 2f4b280a7701
$ ./upload_by_hash.sh のように、ハッシュ値を省略すると、Enter the hash: と訊かれます。
ハッシュ値は、講義システムのHash 列をクリックすると、クリップボードにコピーされます。
自分が編集したソースコードについては、ハッシュ値を毎回指定するよりも、こちらの(方法2) を使うのが便利です。(ハッシュ値はソースコードを修正するたびに変わります。)
以下のサンプルを参考に、コードを書くときのヒントと注意
以下のサンプルソースコードのキャプション「リスト x src/XXXX.ino」のXXXX部分をみて、講義システムの該当するサンプルを参照してください。 たとえば、リスト 1 src/serial01.ino だったら、Name: serial01 になります。 - 2025年では、M5Unified.h を使用するため、
#include <M5StickCPlus.h>
とかかれているプログラムはコンパイルできません。 参考:2024以前のサイト - M5Unified.h を用いるときのM5.begin()
の書き方は以前と異なります。講義システムで、ソースコードの新規作成時に生成されるコードを参考にしてください。 - 一部のサンプルについては、需要がすくないこともあり、M5Unified.h 版のサンプルはまだ準備していません。画面(LCD)をつかうプログラムを書き込んだあと、画面を使わないプログラムを書き込むと、前のプログラムの画面が残ることがあります。また、画面を使わないプログラムは実行していることが分かりにくいので、注意してください。LCDが付いているとすこしだけ明るくなります。
Cプログラムの命令(関数等)について、大文字と小文字は厳密に区別されます。
その他、よくある質問は、Frequently-Asked Questions (FAQ: よくある質問) に随時追記していきます。
Serial通信
リスト 1 (serial01
) は、シリアルモニタおよび画面へ文字列を出力する例です。プログラムでは、まずグローバル変数 num を 0 に設定したうえで、setup()
関数が1回実行されます。その後、loop()
関数が繰り返し実行されます。このあたりは Processing言語のsetup, draw と似ています。( ChatGPTによると、Arduino側が参考にしたらしい )
プログラムの出力をみるには、「シリアルモニタ」をひらいてください。(ただし、シリアルモニタをひらきっぱなしにすると、プログラムの書き込みができません。ターミナル内であればCTRL+Cで終了してください。)
Serialクラスには、print()関数、println()関数、 printf()関数があります。Serial通信を用いると、デバイス側の変数の値や状況を、PC側で確認するプログラムを書くことができます。
なお、文字化けしたり、???と表示されるときは、シリアルモニタの通信速度(ボーレート)を 115200 bps
に変更し、プログラムで設定した速度と一致させてください。
→動画 serial01
1#include <Arduino.h>
2#include <M5Unified.h>
3
4int num = 0;
5void setup() {
6 auto cfg = M5.config();
7 cfg.serial_baudrate = 115200; // Serial通信速度(ボーレート)をbpsで設定
8 M5.begin(cfg);
9 M5.Display.setRotation(0); //縦(M5が読める向き)
10 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
11 M5.Display.println("serial01");
12 M5.Display.setTextScroll(true);
13}
14void loop() {
15 M5.Display.printf("%d \n" , num);
16 Serial.printf("%d \n" , num);
17 num++;
18 M5.delay(1000); // 1秒待つ
19}
シリアルモニタは、デバイスからの出力を確認するだけではなく、デバイスに文字を送信することもできます。リスト 2 (serial02
)は、シリアルモニタを介して、PCからデバイスに文字を送信する例です。シリアルモニタ上部のテキストエリアから 0 を送信すると、カウンタ num をリセットします。19〜23行目で、PCから送信された文字を、1文字ずつ Serial.read()
で読み取って、配列 buf
に格納しています。PCから複数の文字を1回で送信すると、 Serial.available()
は連続して 1 (=true) を返しますので、1回に送信された文字をすべて buf
に格納することができます。ちなみに、シリアルモニタで送信された文字列の最後は、改行コード(10)が付与されます。
→動画 serial02
1#include <Arduino.h>
2#include <M5Unified.h>
3
4int num = 0;
5char buf[100];
6
7void setup() {
8 auto cfg = M5.config();
9 cfg.serial_baudrate = 115200; // 通信速度をbpsで設定
10 M5.begin(cfg);
11 M5.Display.setRotation(3);
12 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
13 M5.Display.println("シリアルテスト serial02");
14 M5.Display.setTextScroll(true);
15}
16
17void loop() {
18 int pos = 0;
19 while (Serial.available()) { //PCから送信された文字があるあいだ、くりかえす
20 char c = Serial.read(); // 1バイト読み取る
21 buf[pos] = c; // 配列buf に格納
22 pos++; // 格納位置をひとつ右へ
23 }
24 if (pos > 0) {
25 buf[pos] = 0; // さいごに Null (文字列の終端)を追加(これを忘れるとどうなる?)
26 M5.Display.print("> from pc: ");
27 M5.Display.print( buf );
28 Serial.print("> from pc: ");
29 Serial.print( buf ); // 格納しておいた文字列を表示
30 if (buf[0] == '0' && pos == 2) { // buf={ 48(='0'), 10(=改行) } のとき
31 num = 0; // num を 0 にする
32 M5.Display.println("Reset num");
33 Serial.println( "Reset num" );
34 }
35 delay(2000);
36 }
37
38 Serial.printf("%d \n" , num );
39 num++;
40 delay(1000);
41}
注釈
25行目の処理を忘れると、どうなるでしょうか? また、buf が溢れると、なにが起きるでしょうか?プログラムを書くときは、常に例外的な事象がおきる可能性を考えておきましょう。ただし、全ての例外的な事象に対処するのは困難な場合が多いです。
本体の液晶ディスプレイ(LCD)
M5StickC Plus2動作確認 その1 画面周り <https://lang-ship.com/blog/work/m5stack-m5stickc-plus2-1/> が詳しいです。
リスト 3 のサンプル(
lcd01
)では、外側から内側に向かって、10ピクセルずつ余白を残しながら、色を変えて塗りつぶしています。その後、8,16,26ピクセルフォント(それぞれ1,2,4番)をsetCursor()関数で指定して、print()関数で文字列を描画しています。M5Unified.h では、M5.Lcd → M5.Display になりました。
1#include <Arduino.h>
2#include <M5Unified.h>
3
4void setup() {
5 auto cfg = M5.config();
6 M5.begin(cfg);
7 M5.Display.setRotation(3);
8 M5.Display.fillRect(0, 0, 240, 135, RED);
9 //M5StickCPlusの画面サイズは、240 x 135
10
11 M5.Display.fillRect(10, 10, 220, 115, ORANGE);
12 M5.Display.fillRect(20, 20, 200, 95, YELLOW);
13 M5.Display.fillRect(30, 30, 180, 75, GREENYELLOW);
14 M5.Display.fillRect(40, 40, 160, 55, CYAN);
15 M5.Display.fillRect(50, 50, 140, 35, BLUE);
16 M5.Display.fillRect(60, 60, 120, 15, MAGENTA);
17
18 M5.Display.setTextFont(1); // 8pixel ASCII font
19 M5.Display.setTextSize(1); // Magnify (x1-7)
20 M5.Display.setTextColor(WHITE);
21 M5.Display.setCursor(10,1); M5.Lcd.print("RED");
22 M5.Display.setTextColor(BLACK);
23 M5.Display.setCursor(20,11); M5.Lcd.print("ORANGE");
24 M5.Display.setCursor(30,21); M5.Lcd.print("YELLOW");
25 M5.Display.setCursor(40,31); M5.Lcd.print("GREENYELLOW");
26 M5.Display.setCursor(50,41); M5.Lcd.print("CYAN");
27 M5.Display.setTextColor(WHITE);
28 M5.Display.setCursor(60,51); M5.Lcd.print("BLUE");
29 M5.Display.setCursor(70,60,2); M5.Lcd.print("MAGENTA (2)");
30 // setCursorの第3引数は、TextFont番号
31
32 M5.Display.setTextColor(BLACK);
33 M5.Display.setCursor(30,90,4);
34 M5.Display.print("26pixel ASCII (4)");
35}
36
37void loop() {
38 M5.delay(100);
39}

画面の向きは、setRotation() 関数で、設定します。0〜3までの整数で、指定します。

注釈
フォントの種類を設定するときは、setFont() をつかいます。setTextSize() は、指定したフォントを「縦横何倍に引き伸ばして表示するか?」を設定します。ここの「倍率」を2以上に増やすと、若干ギザギザが目立つようになります。 M5Unified.h をつかうと、日本語も表示できるようになりました。
本体のボタン
本体には、3つのボタンがあります。 (参考:M5StickCガイド の、4ページ目、ハードウェアの概要)
リスト 4 に、ボタンをおしたらシリアルモニタに表示する例を示します。ボタンも、M5StickCPlus の機能なので、 #include <M5StickCPlus.h>
を指定し、M5.begin()
を呼び出しておく必要があります。 また、ボタンの状態を読み出して、ボタンオブジェクト(BtnA,BtnB)に設定するために、 M5.update()
を、ループのなかに入れておく必要があります ( update()の定義 )。長押しと普通押しを区別したい場合は、ifで確認する順番(長押しを先に判定する)が重要です。
1#include <Arduino.h>
2#include <M5Unified.h>
3
4void setup() {
5 auto cfg = M5.config();
6 M5.begin(cfg);
7 M5.Display.setRotation(3);
8 M5.Display.setTextScroll(true);
9 M5.Display.setTextFont(1);
10 M5.Display.println("started!!");
11}
12
13void loop() {
14 M5.update(); // 各ボタンの状態を(読み取って)更新する:ボタンを判定するときは必須。
15
16 if (M5.BtnA.wasReleasefor(1000) ) {
17 M5.Display.println("[A] was Pressed longer than 1s");
18 } else if (M5.BtnA.wasReleased()) {
19 M5.Display.println("[A] was Pressed");
20 } else if (M5.BtnB.wasReleasefor(1000) ) {
21 M5.Display.println("[B] was Pressed longer than 1s");
22 } else if (M5.BtnB.wasReleased()) {
23 M5.Display.println("[B] was Pressed");
24 } else if (M5.BtnPWR.wasReleased()) {
25 M5.Display.println("[Pwr] was Pressed");
26 }
27 delay(10);
28}
注釈
非公式リファレンスの「ボタン管理(Button)」 も参考になります。
文字列の扱い、16進数や10進数の変換
リスト 5 に、文字列から整数に変換したり、16進数表現の文字列を10進数に変換する例を示します。
Stringクラスを用いると、char配列 / byte配列 / 小文字変換や、空白改行の削除(ただし文字列の前後のみ)、部分文字列の取得などができます。
元のデータが保持されるメソッドと、破壊されるメソッドがあることに注意しましょう。
M5系デバイスのLCDでは、色を16ビット値(赤5bit/緑6bit/青5bit)で表現しています。 M5.Lcd.color565()
という関数もありますが、ここではビット演算の理解を深めるため、関数を定義しています。
C言語のポインタや、シフト演算などを理解していると、このような処理を円滑に記述できるようになります。
→動画 str01
1#include <Arduino.h>
2#include <M5Unified.h>
3
4void setup() {
5 auto cfg = M5.config();
6 cfg.serial_baudrate = 115200; // シリアル通信を利用する。通信速度(ボーレート)を設定。
7 M5.begin(cfg);
8
9 M5.Display.setRotation(3);
10 M5.Display.setTextScroll(true);
11 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
12 M5.Display.println("整数 または 6文字の16進数(RRGGBB)を入力してください。");
13 Serial.println("整数 または 6文字の16進数(RRGGBB)を入力してください。");
14}
15
16String str = ""; // String: 可変長の文字列クラス
17void loop() {
18 char buf[100] = {0};
19 int pos = 0;
20 while (Serial.available()) { //PCから送信された文字があるあいだ、くりかえす
21 char c = Serial.read(); // 1バイト読み取る
22 buf[pos] = c; // 配列buf に格納
23 pos++; // 格納位置をひとつ右へ
24 }
25 if (pos > 0) {
26 buf[pos] = 0;
27 String sbuf = buf;
28 sbuf.trim(); //文字列の前と後の空白と改行を取り除く(破壊的メソッド)
29 str.concat(sbuf); // 文字列の連結 (意味としては、str = str + sbuf)
30 Serial.println(str);
31 if (isDigit(sbuf)) {
32 int num = sbuf.toInt();
33 Serial.println( num * num );
34 }
35 if (sbuf.length() == 6) { // RRGGBBとして、LCD画面の背景を塗りつぶす
36 sbuf.toLowerCase();
37 uint8_t r = hexToDec( sbuf.substring(0, 2) );
38 uint8_t g = hexToDec( sbuf.substring(2, 4) );
39 uint8_t b = hexToDec( sbuf.substring(4, 6) );
40 M5.Display.fillScreen( getColorUInt16( r , g , b ) );
41 M5.Display.setCursor(0, 70, 4);
42 M5.Display.printf(" %02x %02x %02x \n", r, g, b);
43 M5.Display.printf(" %d %d %d ", r, g, b);
44 }
45 }
46 M5.delay(50);
47}
48//全ての文字が0〜9なら、1(true)を返す
49bool isDigit(String s) {
50 bool isAllDigit = true;
51 const char *p = s.c_str(); // String.c_str() は、NULLで終端されたchar配列の先頭アドレスを返す
52 while ( *p != 0 ) {
53 if (*p < '0' || '9' < *p) isAllDigit = false;
54 p++;
55 }
56 return isAllDigit;
57}
58// M5 用の、16ビットカラー値に変換 引用:https://qiita.com/nnn112358/items/ea6b5e81623ba690343c
59uint16_t getColorUInt16(uint8_t red, uint8_t green, uint8_t blue) {
60 return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
61}
62// 引数hex が16進表記の文字列だと仮定して、10進の値を返す
63uint8_t hexToDec(String hex) {
64 int dec = 0;
65 int tmp = 0;
66 for (int p = 0; p < hex.length(); p++) {
67 tmp = int(hex.charAt(p));
68 if ( '0' <= tmp && tmp <= '9' ) tmp = tmp - '0';
69 if ( 'a' <= tmp && tmp <= 'f' ) tmp = tmp - 'a' + 10;
70 if ( 'A' <= tmp && tmp <= 'F' ) tmp = tmp - 'A' + 10;
71 tmp = constrain(tmp, 0, 15); //例外処理
72 dec = (dec * 16) + tmp;
73 }
74 return dec;
75}
注釈
変数の型一覧 uint16_t は unsigned int 16bit type の略です。参考:M5StackのLCDディスプレイの色をRGBで指定する。
ブザー(Beep)
リスト 6 は、内蔵Beepを鳴らすサンプルです。音の周波数は、最初の1オクターブ (f[0]〜f[6]) のみを配列宣言時に指定し、残りの3オクターブはsetup()内で、倍数を計算して配列に設定しています。
Arduino標準tone関数 をつかう方法と、M5.Speaker.tone() をつかう方法があります。Lang-ship に詳しいです。Arduino標準tone関数をつかうと、画面が暗くなってしまうので、暫定的に M5.Display.setBrightness(255); をいれています。
1#include <M5Unified.h>
2int f[28] = { 262, 294, 330, 349, 392, 440, 494 }; // 配列のサイズは4オクターブ分確保する
3
4void setup() {
5 auto cfg = M5.config();
6 cfg.serial_baudrate = 115200;
7 M5.begin(cfg);
8 M5.Display.setRotation(3);
9 for (int i = 0; i < 7; i++) {
10 f[i + 7] = f[i] * 2;
11 f[i + 14] = f[i] * 4;
12 f[i + 21] = f[i] * 8;
13 }
14 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
15 M5.Display.println("サウンドテスト (tone関数)");
16 M5.Display.setTextScroll(true);
17
18 tone(GPIO_NUM_2, f[0], 200);
19 tone(GPIO_NUM_2, f[2], 200);
20 tone(GPIO_NUM_2, f[4], 200);
21 tone(GPIO_NUM_2, f[7], 400);
22 M5.Display.setBrightness(255);
23}
24
25void loop() {
26 while (Serial.available()) {
27 char c = Serial.read();
28 if (c < 'c' || 'z' < c) continue; // c〜zの文字以外なら、以降のwhile内処理をスキップ
29 tone(GPIO_NUM_2, f[c - 'c'], 500); // 0.5秒鳴らす
30 M5.Display.setBrightness(255);
31 Serial.printf(" chr=%c %d Hz\n", c, f[c - 'c']);
32 M5.Display.printf(" chr=%c %d Hz\n", c, f[c - 'c']);
33 }
34 M5.delay(50);
35}
内蔵LED(赤・赤外)
M5StickCPlus2には、2つのLEDランプが内蔵されています。GPIO(General Purpose Input/Output)の19番 には、赤色LED および 赤外LEDが接続されています。 場所は、USBケーブルを挿す面の反対側の面にある、小さな穴2つの内側です。
リスト 7 は、これらのLEDを点滅させるシンプルなプログラムです。
1#include <Arduino.h>
2#include <M5Unified.h>
3
4#define PIN 19 // 赤色LED G19
5
6void setup() {
7 auto cfg = M5.config();
8 M5.begin(cfg);
9 M5.Display.setRotation(3);
10
11 pinMode(PIN, OUTPUT); // PINのモード設定
12
13 M5.Display.setRotation(3); //横向き
14 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
15 M5.Display.println("led01 (LEDが点滅するデモ)");
16 M5.Display.setTextScroll(true);
17}
18
19void loop() {
20 digitalWrite(PIN, HIGH); // HIGH = 1
21 M5.delay(1000);
22 digitalWrite(PIN, LOW); // LOW = 0
23 M5.delay(1000);
24}
外部のLED等を接続(Plus2未確認)
外部のLED等は、 G0, G25, G26 に接続します。(GROVE端子のG32, G33も利用できます。) サンプルプログラムは、リスト 7 と同様です。PIN 番号を、0 / 25 / 26 に変更してください。
ブレッドボードに、抵抗とLEDを直列に接続します。 LEDの長い足のほうに、G0 (or G25 or G26) をジャンパワイヤで接続し、反対側(短い足)のほうを、GND (グランド)に接続します。 ポートを「HIGH」にすると、3.3Vの電圧がかかります。

警告
LEDのみで回路を構成しないよう注意!一般に、LEDは抵抗をつないで使用しないと、故障したり、最悪の場合破裂したりして危険です。ただし、抵抗入りLEDであれば問題ありません。
PWM (Pulse Width Modulation)
パルス幅変調とは、信号の周期Tに対する、パルスの幅を変化させる方法です。 直流モータの速度制御や、サーボモータの制御、LEDの明度調整などに使われます。

Copyright (C) 2021 by Junichi Akita, CC BY-SA
リスト 8 は、赤色LEDを点滅させるシンプルなプログラムです。 ledcSetup(チャンネル, 周波数, 分解能)
で、PWMチャンネル(0〜3)に対する周波数と分解能設定をし、 lecAttachPin(ピン番号, チャンネル)
で、PWMチャンネルを適用する出力ピンを選択します。そのあと、 ledcWrite(チャンネル, 値)
で、分解能で設定したビットで表現できる最大値+1以下の値を指定することで、デューティー比を設定します。リスト 8 の場合、分解能で8bitを指定したので、0〜256 の値が、デューティー比 0〜1 のパルス生成に対応します。
1#include <M5Unified.h>
2#include <esp32-hal-ledc.h>
3
4#define LED_PIN 19 // 赤色LED G19
5
6#define PWM_CH 1 // PWMチャンネル(0~15)
7#define PWM_FREQ 90 // PWM周波数 (Hz)
8#define PWM_RES 8 // 分解能 (bit) → 0~255 のデューティ比
9
10void setup() {
11 M5.begin();
12 M5.Display.setRotation(3);
13
14 pinMode(LED_PIN, OUTPUT); // PINのモード設定
15 // PWM のセットアップ
16 ledcAttachChannel(LED_PIN, PWM_FREQ, PWM_RES, PWM_CH);
17 // PWM_FREQ=周波数Frequency(Hz) 【 90Hz より、小さくするとどうなる?】
18 // PWM_RES=分解能Resolution(bit)
19
20 M5.Display.setRotation(3); //横向き
21 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
22 M5.Display.println("pwm01 LED点滅サンプル (PWM=Pulse Width Modulation パルス幅変調)");
23 M5.Display.setTextScroll(true);
24}
25
26void loop() {
27 for (int duty = 0; duty <= 255; duty += 5) {
28 ledcWrite(LED_PIN, duty);
29 delay(20);
30 }
31 delay(500);
32 for (int duty = 255; duty >= 0; duty -= 5) {
33 ledcWrite(LED_PIN, duty);
34 delay(20);
35 }
36 delay(500);
37 // 別の方法
38 // ledcFade(LED_PIN, 255, 10, 1000);
39 // ledcWrite(LED_PIN, 10); // これがないと切り替わらない
40 // ledcFade(LED_PIN, 10, 255, 1000);
41 // ledcWrite(LED_PIN, 255); // これがないと切り替わらない
42 // delay(500); // 一瞬消灯状態を維持
43}
注釈
ソースコードのコメントにも書いていますが、周波数を小さくする(20〜30程度)と、どうなるでしょうか?ぜひやってみてください。
サーボモータ(サーボハット利用)(Plus2未確認)
リスト 9 は、サーボハット のサーボモータを動かすサンプルです。サーボモータの制御はPWMですので、基本的に、上のPWMとやっていることは同じです。おまけとして、LEDも点灯させました。map関数
は、Arduinoで使える関数で、範囲に対応する値を変換するときに使います。ここでは、サーボ制御で用いる5〜33の値を、LED制御の値0〜256に変換しています。

1#define MLOW 5
2#define MHIGH 33
3
4void setup() {
5 ledcSetup(1, 50, 8); //サーボハット CH1
6 ledcAttachPin(26, 1);
7
8 pinMode(10, OUTPUT);
9 ledcSetup(2, 100, 8);//LED CH2
10 ledcAttachPin(10, 2);
11}
12
13void loop() {
14 for (int i = MLOW; i <= MHIGH; i = i + 1) {
15 ledcWrite(1, i);
16 int v = map(i, MLOW, MHIGH, 0, 256); // 範囲5〜33に対するiの値を、範囲0〜256に変換
17 ledcWrite(2, v);
18 delay(50);
19 }
20 delay(300);
21 for (int i = MHIGH; i >= MLOW; i = i - 1) {
22 ledcWrite(1, i);
23 int v = map(i, MLOW, MHIGH, 0, 256);
24 ledcWrite(2, v);
25 delay(20);
26 }
27 delay(300);
28}
アナログセンサを接続 (ADC)(Plus2未確認)
ADCは、Analog to Digital Converter の意味です。
analogRead(PIN)
は、PIN番ピンの電圧(0~3.3V)を、0〜4095 の値で返します。一般に、抵抗値が変化するタイプのセンサは、この方法をつかって、読み取ることができます。
警告
ADC にかいてあるように、G26, G32, G33, G36のみ使えます。G26は、無線利用時には使えません。
リスト 10 はCdSセル(照度センサ) の値を読み取るサンプルです。
1int PIN = 26;
2
3// +--- 15kΩ抵抗 --+-- CdSセル --+
4// | | |
5// GND G26 3V3
6// https://www.storange.jp/2012/03/arduinocds.html
7
8// 注意点: https://lang-ship.com/reference/unofficial/M5StickC/Peripherals/ADC/
9
10void setup() {
11 Serial.begin(115200);
12 pinMode(PIN, ANALOG); // PINのモード設定
13 // https://lang-ship.com/blog/work/m5stickc-io/
14}
15
16void loop() {
17 Serial.printf("%04d\n", analogRead(PIN) ); // 暗 0 〜 4095 明
18 delay(1000);
19}
圧力センサや、曲げセンサも、同様の方法で利用することができます。
加速度センサ
リスト 11 は、内蔵されている加速度センサ(MPU6886) の値を表示するサンプルです。ちなみに、IMUとは、Inertial Measurement Unit: 慣性計測装置 の略です。 「ツール」→「シリアルプロッタ」をひらくと、値をグラフでみることができます。(通信速度設定を、115200bps に設定してください) ジャイロセンサの値も取得可能です。
1#include <M5Unified.h>
2
3void setup() {
4 auto cfg = M5.config();
5 cfg.serial_baudrate = 115200;
6 M5.begin(cfg);
7 M5.Display.setRotation(3); //横向き
8 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
9 M5.Display.println("IMU Inertial Measurement Unit: 慣性計測装置");
10}
11
12void loop() {
13 M5.update();
14 auto imu_update = M5.Imu.update();
15 if (imu_update) {
16 m5::IMU_Class::imu_data_t data = M5.Imu.getImuData();
17 M5.Display.setCursor(0, 40); // x,y,fonttype
18
19 M5.Display.printf("加速度 傾↑↓:%7.2f ←→:%7.2f 垂直:%7.2f\n", data.accel.x , data.accel.y , data.accel.z );
20 Serial.printf("%7.2f , %7.2f , %7.2f \n", data.accel.x , data.accel.y , data.accel.z); //ArduinoIDE シリアルプロッタ用の出力
21
22 M5.Display.printf("ジャイロ X:%7.2f Y:%7.2f Z:%7.2f \n", data.gyro.x, data.gyro.y, data.gyro.z);
23 // Serial.printf("%7.2f,%7.2f,%7.2f,", gyroX * M5.IMU.gRes, gyroY * M5.IMU.gRes, gyroZ * M5.IMU.gRes);
24 }
25 M5.delay(10);
26}
赤外(InfraRed)リモコン(Plus2未確認)
注釈
実験時間中に、送信のみを手軽に試したい場合は、付録の リスト 28 を書き込んで、教員を呼んでください。
信号の読み取り
赤外線リモコン受信モジュールが必要です。ここでは、GP1UXC41QS を前提に、話をすすめます。
また、準備として、ライブラリマネージャにて、IRremoteESP8266 をインストールします。ちなみに、テストしたバージョンは2.7.15でした。 2.7.15
より新しいバージョンだと、失敗する場合があります。もし最新版を入れてうまくいかない場合はダウングレードしてください。
注釈
ライブラリマネージャは、「スケッチ」→「ライブラリをインクルード」→「ライブラリを管理…」で、ひらきます。
ファイルメニュー → スケッチ例 → IRremoteESP8266 → IRrecvDumpV2 を選択します。
ブレッドボードに、以下の図のように配線します。Pとかいてある面が、受光器が出っ張っている面だとおもってください。47Ωの抵抗を、5Vとの間に入れます。
赤外線リモコン受信モジュールに接続したピンを const uint16_t kRecvPin = 36;
として設定します。
受光器にリモコンを向けて、ボタンを押すと、シリアルモニタに情報が表示されます。ここでは、Protocol : NEC , Code 0x2FD48B7 (32 Bits) と表示されたとします。この数値(uint32_t)を覚えておきます。

信号の送信
内蔵の赤外LEDを用いて、信号を送信する例を リスト 12 に示します。 内蔵の赤外LED光はあまり強くないため、50cm程度まで近づかないと反応しない場合があります。また、太陽光の影響を強く受けます。 単体の赤外LEDを接続して用いると、距離を伸ばすことができます。
なお、NECフォーマットではない赤外線リモコンの通信フォーマットについては、 赤外線リモコンの通信フォーマット や、スケッチ例を参考にしてください。
1#include <IRremoteESP8266.h>
2#include <IRsend.h>
3
4uint16_t kIrLed = 9; // 内蔵IrLED
5IRsend irsend(kIrLed);
6
7uint32_t code = 0x2FD48B7; // Toshiba TV Power
8void setup() {
9 irsend.begin();
10}
11
12void loop() {
13 irsend.sendNEC( code , 32);
14 delay(5000);
15}
スプライト表示(TFT_eSprite クラス)
M5.Display ではじまる画面描画命令は、表示されている画面に対して、直接描画するため、画面のちらつきが生じることがあります。 LGFX_Sprite クラスを用いると、画面に表示されていない仮想画面(オフスクリーン)を作成して、そこに対して描画を行っておき、さいごにまとめて仮想画面の内容をメインの画面(Lcd)に描画することができます。 これにより、画面のちらつきが抑制できます(ダブルバッファリング)。 ちらつき防止のほかに、なめらかに画面スクロールする目的で、実画面よりも大きい仮想画面を作成しておき、その一部の領域のみを実画面に描画するといった用途にも使われます。
LGFX_Sprite クラスは、M5.Displayクラスとほぼ同様の描画関数を備えています。リスト 13 は、スプライトの効果あり/なしをAボタンで切り替えて確認できるサンプルプログラムです。 →動画 sprite01
1#include <M5Unified.h>
2
3int USE_SPRITE = 0 ; //スプライトを使用するとき1
4
5// スプライト用のオブジェクト
6LGFX_Sprite spu(&M5.Display);
7
8void setup() {
9 M5.begin();
10 M5.Display.setRotation(3);
11
12 // スプライトの初期化、サイズを設定
13 spu.setColorDepth(8);
14 spu.createSprite(240, 135);
15 randomSeed(analogRead(0));
16
17}
18
19void loop() {
20 int r = random(100000);
21 if ( USE_SPRITE ) {
22 spu.fillSprite( CYAN );
23 spu.setCursor(30, 34, 4); spu.setTextSize(2);
24 spu.setTextColor( WHITE, BLUE );
25 spu.printf(" %d \n Sprite ", r );
26 spu.pushSprite(0, 0);
27 } else {
28 M5.Display.fillScreen( TFT_GREENYELLOW );
29 M5.Display.setCursor(30, 34, 4); M5.Lcd.setTextSize(2);
30 M5.Display.setTextColor( WHITE, TFT_DARKGREEN );
31 M5.Display.printf(" %d \n LCD ", r);
32 }
33
34 M5.update();
35 if (M5.BtnA.wasReleased()) {
36 USE_SPRITE = 1 - USE_SPRITE ; // スプライト使用の切り替え(1なら0、0なら1)
37 M5.Speaker.tone(1000 + 1000 * USE_SPRITE, 500);
38 }
39 delay(100);
40}
Wifi 接続
リスト 14 は、Wifi接続のサンプルです。ssid
と password
には、環境にあわせたものを入力してください。接続すると、m5デバイスのIPアドレスを画面に表示します。本当にWifi接続できたかどうかを、PCのターミナルからpingを打つなどして、確認してみましょう。(Windowsの場合、コマンドプロンプトをひらき、ping のあとに、半角スペースと、確認したいIPアドレスを入れます)
1#include <M5Unified.h>
2#include <WiFi.h>
3
4const char* ssid = "ics-ap";
5const char* password = "jikkenics";
6
7void setup() {
8 auto cfg = M5.config();
9 M5.begin(cfg);
10 M5.Display.setRotation(3);
11 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
12 M5.Display.println("WIFI 01");
13 M5.Display.setTextScroll(true);
14 // Serial.begin(115200);
15 // M5.Display.println("シリアル開始");
16
17 WiFi.begin(ssid, password); // 接続開始
18 while (WiFi.status() != WL_CONNECTED) { // 接続中...
19 M5.Display.print(".");
20 M5.delay(500);
21 }
22 // 接続完了!!
23 M5.Display.fillScreen(GREEN);
24 M5.Display.setCursor(0, 40, 4);
25 M5.Display.setTextColor(BLACK, GREEN);
26 M5.Display.print(" Wifi Connected!\n ");
27 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
28 M5.Display.println(gotip);
29 M5.delay(1500);
30}
31
32void loop() {
33 M5.update();
34 if (M5.BtnA.wasReleased()) {
35 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
36 M5.Display.println(gotip);
37 }
38 M5.delay(20);
39}
注:以前掲載していた、Telnetサーバを起動する例 については省略しました。
NTPサーバ (Network Time Protocol) と時刻の取得
M5StickCPlusには、システム時間(localTime)と、RTC(リアルタイムクロック:時計の機能を備えたICのこと)の2種類の時計があります。システム時間は、システムリセット(再起動)のたびに、時刻もリセットされますが、後者のRTCはリセットされません。
リスト 15 に、NTPサーバを使ってシステム時間の修正をしたのち、システム時間を1秒ごとに取得して、シリアルモニタに表示する例を示します。configTime()でNTPサーバを設定しておくと、1時間に1回、NTPサーバに接続して、時刻修正します。 →動画 ntp01
1#include <M5Unified.h>
2#include <WiFi.h>
3
4const char* ssid = "ics-ap";
5const char* password = "jikkenics";
6
7void setup() {
8 auto cfg = M5.config();
9 cfg.serial_baudrate = 115200; // ボーレートを115200に設定
10 M5.begin(cfg);
11 M5.Display.setRotation(3);
12 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
13 M5.Display.println("NTP 01");
14 M5.Display.setTextScroll(true);
15 // Serial.begin(115200);
16 // M5.Display.println("シリアル開始");
17
18 WiFi.begin(ssid, password); // 接続開始
19 while (WiFi.status() != WL_CONNECTED) { // 接続中...
20 M5.Display.print(".");
21 M5.delay(500);
22 }
23 // 接続完了!!
24 char* ntpserver = "ntp.nict.jp" ;
25 configTime(9 * 3600, 0, ntpserver) ; //GMTとの時差(秒) が9*3600, サマータイムで進める時間(秒)が0
26
27
28 M5.Display.fillScreen(GREEN);
29 M5.Display.setCursor(0, 40);
30 M5.Display.setTextColor(BLACK, GREEN);
31 M5.Display.print(" Wifi Connected!\n ");
32 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
33 M5.Display.println(gotip);
34 M5.delay(1500);
35}
36
37void loop() {
38 M5.update();
39
40 struct tm localTime;
41 char buf[30];
42 getLocalTime(&localTime);
43 sprintf(buf, "%04d/%02d/%02d %02d:%02d:%02d\n",
44 localTime.tm_year + 1900,
45 localTime.tm_mon + 1,
46 localTime.tm_mday,
47 localTime.tm_hour,
48 localTime.tm_min,
49 localTime.tm_sec
50 );
51 M5.Display.setTextColor(GREEN,BLACK);
52 M5.Display.print(buf);
53 Serial.print(buf);
54
55 if (M5.BtnA.wasReleased()) {
56 M5.Display.print(WiFi.localIP().toString());
57 }
58 M5.delay(1000);
59}
警告
configTime() を行わない状況で、システム時間(localTime)を取得しようとすると、取得に数秒ほど時間がかかります。
リスト 16 は、RTCに時刻を設定し、表示するサンプルです。ここではWifi&NTPで時刻同期してから、RTCに設定していますが、Wifiネットワークに接続できない場合は、RTCを利用することが望ましいです。
1#include <M5Unified.h>
2#include <WiFi.h>
3
4const char* ssid = "ics-ap";
5const char* password = "jikkenics";
6
7void setup() {
8 auto cfg = M5.config();
9 cfg.serial_baudrate = 115200; // ボーレートを115200に設定
10 M5.begin(cfg);
11 M5.Display.setRotation(3);
12 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
13 M5.Display.println("NTP 01");
14 M5.Display.setTextScroll(true);
15
16 WiFi.begin(ssid, password); // 接続開始
17 while (WiFi.status() != WL_CONNECTED) { // 接続中...
18 M5.Display.print(".");
19 M5.delay(500);
20 }
21 char* ntpserver = "ntp.nict.jp" ;
22 configTime(9 * 3600, 0, ntpserver) ; //GMTとの時差(秒) が9*3600, サマータイムで進める時間(秒)が0
23
24 M5.Display.fillScreen(CYAN);
25 M5.Display.setCursor(0, 40);
26 M5.Display.setTextColor(BLACK, CYAN);
27 M5.Display.print(" Wifi Connected!\n ");
28 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
29 M5.Display.println(gotip);
30 M5.delay(1500);
31}
32
33void loop() {
34 M5.update();
35 char buf[30];
36
37 getLocalC(buf); // bufに、Local(NTP)日時文字列を書き込む
38 M5.Display.setTextColor(GREEN,BLACK);
39 M5.Display.println(buf);
40 Serial.println(buf);
41
42 getRTC(buf); // bufに、日時文字列を書き込む
43 M5.Display.setTextColor(CYAN,BLACK);
44 M5.Display.println(buf);
45 Serial.println(buf);
46
47
48 if (M5.BtnA.wasPressed()) {
49 // NTPからRTCに時刻設定する
50 struct tm localTime;
51 getLocalTime(&localTime);
52 setRTCfromLT(localTime);
53 M5.Display.setTextColor(WHITE,RED);
54 M5.Display.println(" LocalTimeをRTCに設定! ");
55 }
56 M5.delay(1000);
57}
58
59void setRTCfromLT(struct tm lt) {
60 m5::rtc_date_t DateStruct;
61 DateStruct.year = lt.tm_year + 1900;
62 DateStruct.month = lt.tm_mon + 1;
63 DateStruct.date = lt.tm_mday;
64 DateStruct.weekDay = lt.tm_wday;
65 M5.Rtc.setDate(&DateStruct);
66
67 m5::rtc_time_t TimeStruct;
68 TimeStruct.hours = lt.tm_hour;
69 TimeStruct.minutes = lt.tm_min;
70 TimeStruct.seconds = lt.tm_sec+1;
71 M5.Rtc.setTime(&TimeStruct);
72}
73
74void getRTC(char* buf) {
75 m5::rtc_date_t DateStruct;
76 m5::rtc_time_t TimeStruct;
77 M5.Rtc.getDate(&DateStruct);
78 M5.Rtc.getTime(&TimeStruct);
79 sprintf(buf, "RTC %04d/%02d/%02d %02d:%02d:%02d",
80 DateStruct.year, DateStruct.month, DateStruct.date,
81 TimeStruct.hours, TimeStruct.minutes, TimeStruct.seconds
82 );
83}
84
85void getLocalC(char* buf) {
86 struct tm localTime;
87 getLocalTime(&localTime);
88 sprintf(buf, "LOC %04d/%02d/%02d %02d:%02d:%02d",
89 localTime.tm_year + 1900,
90 localTime.tm_mon + 1,
91 localTime.tm_mday,
92 localTime.tm_hour,
93 localTime.tm_min,
94 localTime.tm_sec
95 );
96}
注釈
RTCに一旦時刻を設定しておくと、初期状態で書き込まれているプログラムFactoryTest の「BMP8563 RTC Time」でも、その時刻が表示されるようになります。
WebClient
リスト 17 は、HTTP通信で天気予報Web APIに接続するサンプルです。HTTPClientクラスを用いると、ブラウザでURLを指定してWebページを開くように、WebサーバにGETメソッドやPOSTメソッドでリクエストを送信して、ステータスコードやレスポンスを取得することができます。このサンプルでは、シリアルコンソールに、天気予報をJSON形式で表示します。JSON(ジェイソン)とは、Javascriptのオブジェクトの形式でデータを表現する記法です。
1#include <M5Unified.h>
2#include <WiFi.h>
3#include <HTTPClient.h> // ステータスコードの定義もここにある
4
5const char* ssid = "ics-ap";
6const char* password = "jikkenics";
7// 天気予報API https://weather.tsukumijima.net/ から、千葉の天気を取得
8const char* weatherapi_url = "https://weather.tsukumijima.net/api/forecast/city/120010";
9
10void setup() {
11 auto cfg = M5.config();
12 cfg.serial_baudrate = 115200;
13 M5.begin(cfg);
14 M5.Display.setRotation(3); //横向き
15 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
16 M5.Display.println("webclient01 ボタンを押すと天気を取得");
17 M5.Display.setTextScroll(true);
18 M5.Display.fillScreen(BLACK);
19 M5.Display.setTextColor(WHITE,BLACK);
20
21 WiFi.begin(ssid, password); // 接続開始
22 while (WiFi.status() != WL_CONNECTED) { // 接続中...
23 M5.Display.print(".");
24 M5.delay(500);
25 }
26 // 接続完了!!
27 M5.Display.fillScreen(GREEN);
28 M5.Display.setCursor(0, 30, 4);//x,y,fonttype
29 M5.Display.setTextColor(BLACK, GREEN);
30 M5.Display.print(" Wifi Connected!\n");
31 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
32 M5.Display.println(gotip);
33 M5.Display.println("BtnA to get weather");
34 M5.delay(1500);
35}
36
37void loop() {
38 M5.update();
39 if (M5.BtnA.wasReleased()) {
40 HTTPClient http; // クライアント作成
41 http.begin(weatherapi_url); // HTTPでサーバに接続
42 int httpCode = http.GET(); // ステータスコードを取得
43 if (httpCode > 0) {
44 Serial.println(httpCode);
45 M5.Display.setTextColor(GREEN,BLACK);
46 M5.Display.printf("status code = %d\n ", httpCode);
47 if (httpCode == HTTP_CODE_OK) { // ステータスコードが「成功」(200) なら
48 String payload = http.getString();
49 Serial.println(payload);
50 }
51 }
52 http.end();
53 }
54 M5.delay(20);
55}
警告
https (SSL) 通信でPOSTメソッドで送信する場合は、付録→LINE Notify (リスト 31)を参考にしてください。(注:Plus2未確認)
WebServer
リスト 18 は、80番ポートでHTTPでの通信を待ち受け(listenし)て、クライアントからの接続情報(ヘッダ情報)を返すWebサーバの例です。クライアント(ブラウザ)からのリクエスト行のうち、 GET
または POST
ではじまる行があれば、変数 meth
に格納します。つまり、変数 meth
には、ブラウザで発行したリクエストのURLが含まれることになります。(methはmethodの略。)POSTメソッドで送信されていれば、リクエストボディに記述されたデータを変数 post
に格納します。 クライアント(ブラウザ)に返す「レスポンス」は、 client.println()
で送信します。最後に、client.stop()
で、サーバ側から接続を切断します。
→動画 httpserver01
1#include <M5Unified.h>
2#include <WiFi.h>
3#include <HTTPClient.h> // ステータスコードの定義もここにある
4
5const char* ssid = "ics-ap";
6const char* password = "jikkenics";
7
8WiFiServer server(80);
9
10void setup() {
11 auto cfg = M5.config();
12 cfg.serial_baudrate = 115200;
13 M5.begin(cfg);
14 M5.Display.setRotation(3); //横向き
15 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
16 M5.Display.println("webserver01");
17 M5.Display.setTextScroll(true);
18
19 WiFi.begin(ssid, password); // 接続開始
20 while (WiFi.status() != WL_CONNECTED) { // 接続中...
21 M5.Display.print(".");
22 M5.delay(500);
23 }
24 // 接続完了!!
25 M5.Display.fillScreen(GREEN);
26 M5.Display.setCursor(0, 30, 4);//x,y,fonttype
27 M5.Display.setTextColor(BLACK, GREEN);
28 M5.Display.print(" Wifi Connected!\n");
29 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
30 M5.Display.println(gotip);
31 M5.Display.println("open Web via HTTP");
32
33 server.begin(); // Webサーバを開始
34}
35
36void loop() {
37 M5.update();
38 WiFiClient client = server.available();
39 if (client) {
40 String req = "" ;
41 String tmp = "" , meth = "" ;
42 while (client.connected()) { // loop while the client's connected
43 if (client.available()) { // if there's bytes to read from the client,
44 char c = client.read(); // read a byte, then
45 req += c;
46 if (c == '\n') { // if the byte is a newline character
47 if (tmp.length() == 0) { // end of request, break while loop
48 break;
49 } else { //まだ継続
50 if (tmp.startsWith("GET ") || tmp.startsWith("POST ") ) {
51 meth = tmp;
52 }
53 tmp = "";
54 }
55 } else if (c != '\r') { // if you got anything else but a carriage return character,
56 tmp += c; // add it to the end of the currentLine
57 }
58 }
59 } // end of while
60
61 Serial.println(meth);
62 if ( meth.startsWith("GET /") ) {
63 client.println("HTTP/1.1 200 OK"); // header (with response code)
64 client.println("Content-Type:text/plain");
65 client.println(""); // HTTPでは、header と body の区切りは改行
66 client.println(meth);
67 client.println("-- request --");
68 client.println(req);
69 }
70
71 if ( meth.startsWith("POST ") ) {
72 String post = "";
73 char buf[257];
74 int n;
75 while ((n = client.available()) > 0) {
76 if (n < 256) {
77 client.readBytes(buf, n) ;
78 buf[n] = 0 ;
79 } else {
80 client.readBytes(buf, 256) ;
81 buf[256] = 0 ;
82 }
83 }
84 post += buf ;
85
86 client.println("HTTP/1.1 200 OK");
87 client.println("Content-Type:text/plain");
88 client.println(""); // HTTPでは、header と body の区切りは改行
89 client.println(meth);
90 client.println("-- request --");
91 client.println(req);
92 client.println("-- post data --");
93 client.println(post);
94 }
95 // close the connection:
96 client.stop();
97 Serial.println(" --- Client Disconnected.");
98 }
99 M5.delay(20);
100}
センサデータを返すだけなら問題ありませんが、クライアントからのデータを GET / POST で受信して処理する場合は、 key1=val1&key2=val2
のような文字列を要素に分解する必要がでてきます。あまり深入りしませんが、正規表現で文字列を照合・抽出する Regexp
や、抽出した結果をハッシュ/辞書として保存する Dictionary
ライブラリを導入すると、複雑なリクエストやデータを扱いやすくなるでしょう。
リスト 19 に、RGBの色指定文字列などのリクエスト文字列をパースして辞書に追加するプログラムの断片を示します。注意:このプログラム( リスト 19 )は、単体では動作しません。また、Plus2未確認です。
1#include <Regexp.h>
2#include <Dictionary.h>
3
4Dictionary *dict = new Dictionary(); // 辞書/ハッシュ。キー文字列→値 を保持するデータ構造
5
6void match_callback (const char * match, // matching string (not null-terminated)
7 const unsigned int length, // length of matching string
8 const MatchState & ms) // MatchState in use (to get captures)
9{
10 char k [10]; // size must be large enough to hold captures
11 char v [10]; // size must be large enough to hold captures
12 ms.GetCapture(k, 0);
13 ms.GetCapture(v, 1);
14 dict->insert(k, v); // 辞書に追加 (たとえば、 red→120 を追加)
15} // end of match_callback
16
17// たとえば、*cbuf = "red=120&green=255&blue=9" のような文字列を想定する
18void param2dict(char *cbuf){
19 MatchState ms (cbuf); //正規表現マッチャーの作成
20 ms.GlobalMatch ("([a-z]+)=([0-9]+)", match_callback); // (key)=(value) で複数回マッチングする。match_callback は別関数。
21 int r = dict->search("red").toInt(); // 辞書 dict
22 int g = dict->search("green").toInt();
23 int b = dict->search("blue").toInt();
24 int32_t bgcolor = (int(r * 31 / 255) << 11) | (int(g * 63 / 255) << 5) | (int(b * 31 / 255));
25}
MQTT
MQTT(Message Queue Telemetry Transport) は、センサデータをデバイス間で共有・流通させるときなどに使われる、軽量のメッセージ送受信プロトコルです。 ここで、「軽量」とは、HTTPに比べて、ヘッダ部分のデータが少ない、という意味です。 MQTTでは、サーバのことを「ブローカ」と呼びます。ここでは、データを送信するPublisherと、データを受信するSubscriberの2つのデバイスと、ブローカの3つの構成要素で説明します。
Publisherは、ブローカに接続しデータを送信します。このとき、 「トピック」 と呼ばれる、データの登録先を文字列で指定します。また、retainデータとして送信するかどうかも指定します。retainとは「保持・維持」の意味で、最後に送ったデータをブローカに残しておきたい場合、rateinデータとします。 Subscriberは、ブローカに接続し、トピックをサブスクライブ(購読)します。このとき、トピックに書き込まれているretainデータがあれば、最初にそのデータを受信します。retainデータがなければ、Publisherが新しいデータをトピックに送信したタイミングで、データを受信します。 このような通信手段(プロトコル)を、パブリッシュ/サブスクライブモデル と呼びます。参考サイト: IoT初心者向け!「MQTT」について簡単にまとめてみる
MQTT Publish
リスト 20 は、MQTT Publisherのサンプルです。実験用ブローカ(mqtt.istlab.info)に接続して、 jikken
というトピックにデータ(シリアルコンソールで送信した文字列)を書き込みます。
1#include <M5Unified.h>
2#include <WiFi.h>
3// ライブラリで PubSubClient をいれておく ★★★ 重要 ★★★
4#include <PubSubClient.h>
5
6const char* ssid = "ics-ap";
7const char* password = "jikkenics";
8
9const char* server = "mqtt.istlab.info";
10const int port = 1883;
11const char* pubTopic = "jikken"; // 班名を入れれば他の班とかぶらない。+にはしない。ex. jikken/A4han
12
13WiFiClient wifiClient;
14char* clientid = "m5scp2_001"; //デバイス個別に設定すればなんでもよい
15PubSubClient mqttClient(wifiClient); // MQTT Client
16
17char macaddr[100];
18
19void setup() {
20 auto cfg = M5.config();
21 cfg.serial_baudrate = 115200;
22 M5.begin(cfg);
23 M5.Display.setRotation(3); //横向き
24 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
25 M5.Display.println("mqtt01pub");
26 M5.Display.setTextScroll(false);
27
28 WiFi.begin(ssid, password); // 接続開始
29 while (WiFi.status() != WL_CONNECTED) { // 接続中...
30 M5.Display.print(".");
31 M5.delay(500);
32 }
33 // 接続完了!!
34 M5.Display.fillScreen(CYAN);
35 M5.Display.setCursor(0, 8, 4);//x,y,fonttype
36 M5.Display.setTextColor(BLACK, CYAN);
37 M5.Display.print(" Wifi Connected!\n");
38 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
39 M5.Display.println(gotip);
40
41 mqttClient.setServer(server, port);
42 if ( !mqttClient.connected() ) {
43 Serial.println("Try connecting MQTT...");
44 reconnect();
45 }
46 // 参考:WiFiデバイスのMACアドレスを取得し、送信する
47 uint8_t mac[6];
48 WiFi.macAddress(mac);
49 sprintf(macaddr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] );
50 M5.Display.println(macaddr);
51
52 M5.delay(2000);
53 Serial.println("publishするメッセージをSerialから送信してください。またはAボタンを押してください。");
54 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
55 M5.Display.println("publishするメッセージをSerialから送信してください。またはAボタンを押してください。");
56}
57
58void loop() {
59 // シリアルコンソールから書き込みがあれば, publishする
60 char mbuf[100]; int pos = 0;
61 while ( Serial.available() ) { // ノンブロッキング
62 mbuf[pos] = Serial.read();
63 pos++;
64 }
65 if (pos > 0) {
66 mbuf[pos-1] = 0; //改行をNULLに置き換える
67 Serial.println(mbuf);
68 if ( !mqttClient.connected() ) reconnect();
69 // ブローカにデータを送信する。最後の false を true にすると、retained になる。
70 // boolean ret = mqttClient.publish( pubTopic , mbuf, size, false );
71 boolean ret = mqttClient.publish( pubTopic , mbuf );
72 if ( !ret ){
73 Serial.println("publish failed.");
74 }
75 }
76 M5.update(); // ボタン読み取りの前にはupdateが必要
77 if (M5.BtnA.wasReleased()) {
78 if ( !mqttClient.connected() ) reconnect();
79 boolean ret = mqttClient.publish( pubTopic , macaddr );
80 if ( !ret ){
81 Serial.println("publish failed.");
82 }
83 }
84 M5.delay(50);
85}
86
87void reconnect() {
88 while ( !mqttClient.connected() ) {
89 if ( mqttClient.connect(clientid) ) {
90 Serial.println("Connected to MQTT Broker.");
91 } else {
92 Serial.printf("Connect Failed. state=%d", mqttClient.state());
93 }
94 }
95}
MQTT Subscribe
リスト 21 は、MQTT Subscriberのサンプルです。実験用ブローカ(mqtt.istlab.info)に接続して、 ワイルドカード文字 +
(プラス) は、任意の単一トピック階層にマッチするすべてのトピックを購読します。ワイルドカード文字 #
は / で区切られた下位トピック階層も含めてマッチします。 JSON形式のデータを処理するときは、ArduinoJsonをつかってパージング(deserialize) すると便利です(90行目以降にサンプルがあります)。
1#include <M5Unified.h>
2#include <WiFi.h>
3#include <PubSubClient.h>
4#include <ArduinoJson.h>
5
6const char* ssid = "ics-ap";
7const char* password = "jikkenics";
8
9const char* server = "mqtt.istlab.info";
10const int port = 1883;
11const char* pubTopic = "+"; // 班名を入れれば他の班とかぶらない。+にはしない。ex. jikken/A4han
12
13WiFiClient wifiClient;
14char* clientid = "m5scp2_002"; //デバイス個別に設定すればなんでもよい
15PubSubClient mqttClient(wifiClient); // MQTT Client
16
17char macaddr[100];
18
19void setup() {
20 auto cfg = M5.config();
21 cfg.serial_baudrate = 115200;
22 M5.begin(cfg);
23 M5.Display.setRotation(3); //横向き
24 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
25 M5.Display.println("mqtt01sub");
26 M5.Display.setTextScroll(true);
27
28 WiFi.begin(ssid, password); // 接続開始
29 while (WiFi.status() != WL_CONNECTED) { // 接続中...
30 M5.Display.print(".");
31 M5.delay(500);
32 }
33 // 接続完了!!
34 M5.Display.fillScreen(BLUE);
35 M5.Display.setCursor(0, 8, 4);//x,y,fonttype
36 M5.Display.setTextColor(WHITE, BLUE);
37 M5.Display.print(" Wifi Connected!\n");
38 String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
39 M5.Display.println(gotip);
40
41 // 参考:WiFiデバイスのMACアドレスを取得し、送信する
42 uint8_t mac[6];
43 WiFi.macAddress(mac);
44 sprintf(macaddr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] );
45 M5.Display.println(macaddr);
46
47 mqttClient.setServer(server, port);
48 mqttClient.setCallback(callback_on_subscribe);
49
50 if (!mqttClient.connected()) {
51 Serial.println("Try (re)connecting...");
52 reconnect();
53 }
54 mqttClient.subscribe(pubTopic);
55
56 M5.delay(2000);
57 Serial.println("自動的にsubscribeします。Serialと画面に出力します。");
58 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
59 M5.Display.println("自動的にsubscribeします。Serialと画面に出力します。");
60}
61
62void loop() {
63 mqttClient.loop(); // データがpublishされたら、callback_on_subscribe が呼ばれる
64 M5.delay(50);
65}
66
67void reconnect() {
68 while ( !mqttClient.connected() ) {
69 if ( mqttClient.connect(clientid) ) {
70 Serial.println("Connected to MQTT Broker.");
71 } else {
72 Serial.printf("Connect Failed. state=%d", mqttClient.state());
73 }
74 }
75}
76
77// データがPublishされたら、ここが実行される。(39行目でコールバック関数を設定しているため)
78void callback_on_subscribe(char* topic, byte* payload, unsigned int len) {
79 char buf[100];
80 Serial.print("Message arrived [");
81 Serial.print(topic);
82 Serial.print("] ");
83 for (int i = 0; i < len ; i++) {
84 buf[i] = (char)payload[i];
85 }
86 buf[len] = 0;
87 Serial.println(buf);
88 M5.Display.println(buf);
89 return;
90 // 参考:JSON Parsing example
91 // StaticJsonDocument<200> sjdoc;
92 // deserializeJson(sjdoc, buf);
93 // int intval = sjdoc["intval"];
94 // const char* str = sjdoc["string"];
95}
トピック名とワイルドカード
上述のとおり、サブスクライブするときのトピック名には、ワイルドカード文字が指定できます。例えば、 office/+
と指定すると、 office/temp
にも office/humid
にもマッチします。参考:MQTT トピック
mosquitto コマンド例
mosquitto は、オープンソースのMQTT Broker/Client 実装の1つです。参考までに、mosquitto クライアントを使用するコマンド例を示します。:
#(パブリッシュ。retainなし: -m "メッセージ" )
mosquitto_pub -h mqtt.istlab.info -u ex1 -P PASS -t office/temp -m "data or message"
#(パブリッシュ。retainあり: -r )
mosquitto_pub -h mqtt.istlab.info -u ex1 -P PASS -t office/temp -m "data or message" -r
#(retainデータを削除。-n : send a null (zero length) message.)
mosquitto_pub -h mqtt.istlab.info -u ex1 -P PASS -t office/temp -n -r
#(サブスクライブ)
mosquitto_sub -h mqtt.istlab.info -u ex1 -P PASS -t office/temp
3つ目の例に示すように、長さ0のretainデータを送信すると、retainデータを削除できます。
注釈
サンプルでは文字列(ASCII)データを送受信していますが、値をbyte配列・バイナリで送受信することもできます。
Bluetooth Serial Protocol Profile (SPP) (Plus2未確認)
「Serial通信」では、開発用PCのシリアルコンソールをつかって、USB接続を介して、文字列を送受信しました。 「Wifi接続」のTelnetサーバでは、TCP/IP通信上でのTelnet接続を介して、文字列を送受信しました。
ここでは、Bluetoothを用いて、上記と同様、2台のデバイス間で文字列を送受信する例を示します。Bluetoth では、機器同士は同一の「プロファイル」という通信方式に対応している必要があります。Serial Protocol Profile は、Bluetooth無線通信で仮想シリアル接続を可能にするプロファイルです。
注釈
基本的に ESP32同士をBluetoothシリアルでつないでみる と同じです。その他、一般的なBluetoothとBLEについての解説は、ESP32による近距離無線通信の実験② BLE通信 が参考になります。
(1) スレーブ用デバイスのMACアドレスを調べる
リスト 22 を、1台目のM5StickCPlusで実行し、BluetoothのMACアドレスを調べます。
1#include <M5StickCPlus.h>
2#include "BluetoothSerial.h"
3
4BluetoothSerial SerialBT;
5
6const char* dname = "BT_Serial_00"; // Slave device name
7
8void setup() {
9 M5.begin();
10 M5.Lcd.setRotation(3);
11 M5.Lcd.fillScreen(YELLOW);
12 M5.Lcd.setTextColor(BLACK, YELLOW);
13 M5.Lcd.setCursor(0, 0, 1);
14 M5.Lcd.setTextSize(2);
15
16 Serial.begin(115200);
17 SerialBT.begin(dname); //Bluetooth device name
18 Serial.println("The device started, now you can pair it with bluetooth!");
19
20 uint8_t macBT[6];
21 esp_read_mac(macBT, ESP_MAC_BT);
22 char mac[20];
23 sprintf(mac, "%02X:%02X:%02X:%02X:%02X:%02X", macBT[0], macBT[1], macBT[2], macBT[3], macBT[4], macBT[5]);
24 Serial.println(mac);
25 M5.Lcd.println(mac);
26
27}
28
29int line = 0;
30void loop() {
31 char c;
32 if (Serial.available()) {
33 SerialBT.write(c = Serial.read());
34 Serial.printf("read:%c\n", c);
35 }
36 if (SerialBT.available()) {
37 Serial.write(c = SerialBT.read());
38 SerialBT.write(c + 0);
39 M5.Lcd.print(c);
40 if (c == 0xa) line++;
41 if (line > 7) {
42 M5.Lcd.fillScreen(YELLOW); M5.Lcd.setCursor(0, 0, 1);
43 line = 0;
44 }
45 }
46
47 M5.update();
48 if (M5.BtnA.wasReleasefor(1000) ) {
49 SerialBT.println("[A] was Pressed longer than 1s");
50 } else if (M5.BtnA.wasReleased()) {
51 SerialBT.println("[A] was Pressed");
52 } else if (M5.BtnB.wasReleasefor(1000) ) {
53 SerialBT.println("[B] was Pressed longer than 1s");
54 } else if (M5.BtnB.wasReleased()) {
55 SerialBT.println("[B] was Pressed");
56 } else if (M5.Axp.GetBtnPress() == 2) {
57 SerialBT.println("[Power] was Pressed");
58 }
59
60 delay(20);
61}
(2) マスタ用デバイスにサンプルを書き込む
リスト 23 を、2台目のM5StickCPlusで実行します。このとき、ハイライトした行のところに、(1)で調べたMACアドレスを指定します。少し時間がかかりますが、デバイス名で接続先を指定することもできます。起動するときは、スレーブ側を先に起動しておき、あとでマスタ側を起動してください。
1#include <M5StickCPlus.h>
2#include <BluetoothSerial.h>
3
4BluetoothSerial SerialBT;
5
6bool willConnect = true;
7bool connected;
8
9const char* dname = "BT_Serial_00"; // Slave device name
10
11void setup() {
12 M5.begin();
13 M5.Lcd.setRotation(3);
14 M5.Lcd.fillScreen(BLUE);
15
16 M5.Lcd.setTextColor(BLACK, CYAN);
17 M5.Lcd.setCursor(0, 0, 1);
18 M5.Lcd.setTextSize(2);
19
20 Serial.begin(115200);
21 SerialBT.begin("BT_Serial_01", true);
22 Serial.println("The device started in master mode, make sure remote BT device is on!");
23
24 // connect(address) is fast (upto 10 secs max), connect(name) is slow (upto 30 secs max) as it needs
25 // to resolve name to address first, but it allows to connect to different devices with the same name.
26 // Set CoreDebugLevel to Info to view devices bluetooth address and device names
27 if (false) {
28 connected = SerialBT.connect(dname);
29 } else {
30 // uint8_t address[6] = {0x24, 0xA1, 0x60, 0x47, 0x84, 0x4E}; // MACアドレスが 24:A1:60:47:84:4E のとき
31 uint8_t address[6] = {0x94, 0xB9, 0x7e, 0xad, 0x53, 0x92};
32 connected = SerialBT.connect(address);
33 }
34 if (connected) {
35 Serial.println("Connected Succesfully!");
36 M5.Beep.tone(2000, 1000);
37 M5.Lcd.fillScreen(CYAN);
38 }
39 // disconnect() may take upto 10 secs max
40 // if (SerialBT.disconnect()) {
41 // connected = false;
42 // Serial.println("Disconnected Succesfully!");
43 // }
44 // this would reconnect to the name(will use address, if resolved) or address used with connect(name/address).
45 //SerialBT.connect();
46}
47
48void connectOrDisconnect(bool willCon) {
49 if (willCon && connected) return;
50 if (willCon) {
51 if (connected) {
52 Serial.println("Connected Succesfully!");
53 M5.Beep.tone(2000, 1000);
54 M5.Lcd.fillScreen(CYAN);
55 } else {
56 while (!SerialBT.connected(10000)) {
57 Serial.println("Failed to connect. Make sure remote device is available and in range, then restart app.");
58 M5.Lcd.fillScreen(PURPLE);
59 }
60 }
61 } else {
62 if ( SerialBT.disconnect() ) {
63 connected = false;
64 Serial.println("Disconnected Succesfully!");
65 M5.Lcd.fillScreen(BLUE);
66 }
67 }
68}
69int line = 0;
70void loop() {
71 connectOrDisconnect( willConnect );
72
73 char c;
74 if (Serial.available()) {
75 SerialBT.write(c = Serial.read());
76 Serial.printf("read:%c\n", c);
77 }
78 if (SerialBT.available()) {
79 Serial.write(c = SerialBT.read());
80 M5.Lcd.print(c);
81 if (c == 0xa) line++;
82 if (line > 7) {
83 M5.Lcd.fillScreen(CYAN); M5.Lcd.setCursor(0, 0, 1);
84 line = 0;
85 }
86 }
87
88 M5.update(); M5.Beep.update();
89 if (M5.BtnA.wasReleasefor(1000) ) {
90 SerialBT.println("[A] was Pressed longer than 1s");
91 } else if (M5.BtnA.wasReleased()) {
92 SerialBT.println("[A] was Pressed");
93 } else if (M5.BtnB.wasReleasefor(1000) ) {
94 SerialBT.println("[B] was Pressed longer than 1s");
95 if (SerialBT.disconnect()) {
96 connected = false;
97 M5.Lcd.fillScreen(PURPLE);
98 }
99 } else if (M5.BtnB.wasReleased()) {
100 SerialBT.println("[B] was Pressed");
101 } else if (M5.Axp.GetBtnPress() == 2) {
102 SerialBT.println("[Power] was Pressed");
103 }
104 delay(20);
105}
その他のBluetooth利用例
BLEHIDDeviceを用いると、Human Interface Device(HID) Profileを導入して、マウスやキーボードの代用品が作成できます。(詳細は省略します。)
ESP-NOW による通信
Wifi (802.11) を使いますが、基地局に接続せず、MACアドレス宛に直接通信する方式です。詳細は https://lang-ship.com/blog/work/m5stickc-esp-now-1/ を参照してください。
リスト 24 に、MACアドレスにFF:FF:FF:FF:FF:FF を指定し、ブロードキャストする例を示します。
ブロードキャストの場合、送信先のMACアドレスを指定しなくてよいので楽ですが、他の班のデバイスからも受信できます。espnow02
は、数値データのみを送受信する例です。
1#include <M5Unified.h>
2#include <esp_now.h>
3#include <WiFi.h>
4
5esp_now_peer_info_t peerInfo;
6
7void setup() {
8 auto cfg = M5.config();
9 cfg.serial_baudrate = 115200; // ボーレートを115200に設定
10 M5.begin(cfg);
11 M5.Display.setRotation(3);
12 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
13 M5.Display.println("ESPNOW 02");
14 M5.Display.setTextScroll(true);
15 Init_ESPNOW();
16}
17
18void loop() {
19 M5.update();
20 if (M5.BtnA.wasReleased()) {
21 uint8_t data[2] = {100, 254}; // 送信データ 100, 254に深い意味はない。uint8_tは0〜255の数値
22 esp_err_t result = esp_now_send(peerInfo.peer_addr, data, sizeof(data));
23 }
24 M5.delay(20);
25}
26
27void Init_ESPNOW()
28{ // ESPNowの初期化
29 // 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
30 WiFi.mode(WIFI_STA);
31 WiFi.disconnect();
32 if (esp_now_init() == ESP_OK)
33 {
34 M5.Display.println("ESP-Now Init Success");
35 }
36 else
37 {
38 M5.Display.println("ESP-Now Init failed");
39 }
40 // マルチキャスト用Slave登録
41 memset(&peerInfo, 0, sizeof(peerInfo));
42 for (int i = 0; i < 6; ++i)
43 {
44 peerInfo.peer_addr[i] = (uint8_t)0xff;
45 }
46 esp_err_t addStatus = esp_now_add_peer(&peerInfo);
47 if (addStatus == ESP_OK)
48 {
49 // Pair success
50 M5.Display.println("Pair success");
51 esp_now_register_send_cb(onESPNOWSent); //送信後のコールバック関数を指定する
52 esp_now_register_recv_cb(onESPNOWReceive); ///受信時のコールバック関数を指定する
53 }
54}
55// データを送信した後に実行されるコールバック関数
56// 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
57void onESPNOWSent(const uint8_t *mac_addr, esp_now_send_status_t status)
58{
59 // char macStr[18];
60 // snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
61 // mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
62 M5.Display.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
63}
64
65// データを受け取った時に実行されるコールバック関数
66void onESPNOWReceive(const esp_now_recv_info_t *recv_info, const uint8_t *data, int data_len)
67{
68 char macStr[18];
69 snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
70 recv_info->src_addr[0], recv_info->src_addr[1], recv_info->src_addr[2],
71 recv_info->src_addr[3], recv_info->src_addr[4], recv_info->src_addr[5]);
72
73 if (data[0] == 100 && data[1] == 254) // 100,254に深い意味はない。uint8_tは0〜255の数値
74 {
75 M5.Display.printf(" %s\n", macStr);
76 M5.Display.printf("Data: %d %d\n", data[0], data[1]);
77 }
78}
なお、espnow03
は、整数・小数・文字列をまとめてbyte (uint8_t) 配列に埋め込んで送信し、あとで取り出して利用する例です。(講義システムで確認してください。)
Preference・電源制御
再起動をすると、プログラムがリセットされて、通常の変数や配列データは消えてしまいますが、Preferenceを用いると、不揮発性のフラッシュ領域を使ってデータを保存・復元することができます。
すこし下にある pref01
(リスト 25)を参照してください。ボタンを押した回数が、本体に記録され、再起動しても継続されます。
以下に、電源に関連する命令を示します。
ESP.restart()
で、再起動します。M5.Power.powerOff()
で、電源OFFします。(ただし、USB給電中だとフリーズします)esp_sleep_enable_timer_wakeup(60*60*24 * 1000000ULL); // 1日後に復帰
のようにしたうえで、esp_deep_sleep_start()
で、タイマー設定Deep Sleepします。こちらはUSB給電中でも大丈夫です。setCpuFrequencyMhz(240)
で、CPUのクロック数を変更できます。設定できる値は、240、160、80、40、20、10ですが、無線通信するなら80以上を設定する必要があります。M5.Display.setBrightness(255)
で、画面の明るさを調整できます。
リスト 25 に、Preferenceと電源制御のサンプルを示します。Aボタンでcountを増やし、Aボタン長押しでcountをリセットします。Bボタンで再起動、電源ボタンで電源OFFします。 ただし、35行目のM5.Power.isCharging() はPlus2の場合、充電中かどうかを正しく検出できませんので注意してください。 (一応、Plus用に残しておきます。)
1#include <M5Unified.h>
2#include <Preferences.h>
3
4Preferences pref;
5int count = 0;
6
7void setup() {
8 auto cfg = M5.config(); // M5Unified の設定
9 M5.begin(cfg);
10
11 M5.Display.setRotation(0); // 縦型
12 M5.Display.fillScreen(CYAN);
13 M5.Display.setTextSize(2);
14 M5.Display.setTextScroll(true);
15 loadCount(&count);
16 M5.Speaker.tone(2000, 500);
17 M5.Display.printf("count = %d\n", count);
18}
19
20void loop() {
21 M5.update();
22
23 if (M5.BtnA.wasReleaseFor(1000)) { // Aボタン長押し
24 count = 0;
25 saveCount(&count);
26 M5.Speaker.tone(1200, 300);
27 M5.Display.printf("count = %d\n", count);
28 } else if (M5.BtnA.wasReleased()) { // Aボタン押し
29 count++;
30 saveCount(&count);
31 M5.Speaker.tone(2000, 300);
32 M5.Display.printf("count = %d\n", count);
33 } else if (M5.BtnB.wasReleased()) { // Bボタン押し
34 reboot();
35 } else if (M5.Power.isCharging() == false && M5.BtnPWR.wasReleased()) { // 電源ボタン押し
36 poweroff();
37 }
38
39 delay(50);
40}
41
42void loadCount(int *c) {
43 pref.begin("mydata", false);
44 *c = pref.getInt("count", 0); // 初回時に 0 をデフォルト値とする
45 pref.end();
46}
47
48void saveCount(int *c) {
49 pref.begin("mydata", false);
50 pref.putInt("count", *c);
51 pref.end();
52}
53
54void poweroff() {
55 M5.Power.powerOff();
56}
57
58void reboot() {
59 ESP.restart();
60}