1週目

注釈

以下の項目について、全員がすべてを試す必要はありません (時間が足りなくなる可能性が高いです)。役割分担をして、手分けして動作の確認と、同じ班のメンバーへの動作説明・共有をして、すすめてください。必要に応じて、検証する機能を絞り込んでください。なお、一部のサンプルプログラムは編集しないと意図通りに動作しない場合があります。

電源の切り方・入れ方

  • 電源を入れるには、電源ボタンを、2秒間 長押しします。

  • 電源を切るには、電源ボタン(M5と書かれたAボタンの左側面)を、6秒間 長押しします。その際、プログラムによっては再スタートしたように見える場合がありますが、気にせずに長押し継続してください。

  • 動画をみる→ https://youtu.be/Lo1jZbAeT8Y

  • M5StickCガイド の、4ページ目、ハードウェアの概要も参考になります。

プログラムの書き込み方

IoTP プログラムで、ソースコード(.ino)を開いて、Uploadボタン を押してください。

または、git bash で SampleSrc フォルダに cd で移動したあとで、./Upload.sh を実行してください。なお、引数にファイルを指定する(例えば ./Upload.sh lcd01.ino)と、直接コンパイル&書き込みを行います。

注釈

IoTP プログラムのUploadボタンは、Upload.bat (または Upload.sh) を実行しているだけです。このバッチファイル(またはスクリプト)は、指定したソースコードを TestBuild/TestBuild.ino にコピーしたのち、フォルダを指定してコンパイルします。実行ファイルはbuildフォルダの中に生成されます。arduino-cli では、実行ファイルを直接指定して書き込む方法もあります。(参考:_Upload_FactoryTest.bat

実機での動作検証を時間短縮・効率化したいとき

基本的にはSampleSrc 内のソースコード(.ino) を開き、コンパイル&書き込みを行って確認していくことになりますが、コンパイルに時間がかかる場合は、以下の方法で事前コンパイル済みの実行イメージファイル(.bin.img)を取得し、直接書き込むこともできます。

https://git.istlab.info/miura250/TestBuild においてある実行イメージファイルと、シェルスクリプトを用いる方法です。

  1. cd で、ホームディレクトリに移動する。

  2. git clone https://git.istlab.info/git/miura250/TestBuild.git で、ホームディレクトリに TestBuild フォルダを作成しダウンロードする。

  3. cd TestBuild/build

  4. ./DirectBinUpdate.sh sprite01.bin.img のように、シェルスクリプトを用いて、bin.img ファイルを指定して書き込み(アップロード)をする。

注釈

なお、bin.img 実行イメージファイルは、前述の Upload.sh シェルスクリプトで自動的に M5StickCPlus_FactoryTest2022/SampleSrc/TestBuild/build/ 以下に生成されます。または、時間がかかりますが、M5StickCPlus_FactoryTest2022/SampleSrc/_AllBuildTest.sh にあるシェルスクリプトですべてのサンプルプログラムをビルドし、実行イメージファイルを準備することもできます。

コードを書くときのヒントと注意

  • 以下のソースコード例は、リストの右上の□ボタンを押すと、クリップボードに簡単にコピーできます。右上のボタンが表示されていないときは、右にスクロールしてください。(ただし、一部のWeb掲載プログラムは古く動かない場合があります。リスト上部のファイル名を確認して、IoTPプログラムが一覧をだしてくれる SampleSrcフォルダ 内のファイルをつかってください。)

  • ソースコードのインデントは、CTRL+T (Macの場合はCommand+T)キーで、自動整形できます。

  • #include <M5StickC.h> は、M5StickC用のヘッダファイル指定です。 M5StickCPlus では、画面解像度が異なるので、 #include <M5StickCPlus.h> に変更しないと、表示がおかしくなることがあります。

  • 大文字と小文字は厳密に区別されます。

  • 画面(LCD)をつかうプログラムを書き込んだあと、画面を使わないプログラムを書き込むと、前のプログラムの画面が残ることがあります。

  • スケッチブック(プログラムを構成する、複数のソースコードを含むファイル、他のIDEではプロジェクトと呼ぶ場合もある)をコピーしたいときは、内包するフォルダごとコピーしてください。その際、フォルダ名と、メインのソースコードファイル名(拡張子以外の部分)は、一致している必要があります。

  • その他、よくある質問は、Frequently-Asked Questions (FAQ: よくある質問) に随時追記していきます。

Serial通信

リスト 1 は、シリアルモニタへ文字列を出力するシンプルな例です。グローバル変数 num を 0 に設定したうえで、setup()関数が1回実行されます。その後、loop()関数が繰り返し実行されます。 プログラムの出力をみるには、サンプルフォルダの ./monitor.sh またはVSCodeで 「シリアルモニタ」をひらいてください。 Serialクラスには、print()関数、println()関数、 printf()関数があります。Serial通信を用いると、デバイス側の変数の値や状況を、PC側で確認するプログラムを書くことができます。 なお、文字化けしたり、???と表示されるときは、シリアルモニタの通信速度(ボーレート)を 115200 bps に変更し、プログラムで設定した速度と一致させてください。 →動画 serial01

リスト 1 src/serial01.ino
1int num = 0;
2void setup() {
3  Serial.begin(115200); // 通信速度をbpsで設定
4}
5void loop() {
6  Serial.printf("%d \n" , num);
7  num++;
8  delay(500); // 500ミリ秒待つ
9}

シリアルモニタは、デバイスからの出力を確認するだけではなく、デバイスに文字を送信することもできます。リスト 2 は、シリアルモニタを介して、PCからデバイスに文字を送信する例です。シリアルモニタ上部のテキストエリアから 0 を送信すると、カウンタ num をリセットします。10〜14行目で、PCから送信された文字を、1文字ずつ Serial.read() で読み取って、配列 buf に格納しています。PCから複数の文字を1回で送信すると、 Serial.available() は連続して 1 (=true) を返しますので、1回に送信された文字をすべて buf に格納することができます。ちなみに、シリアルモニタで送信された文字列の最後は、改行コード(10)が付与されます。 →動画 serial02

リスト 2 src/serial02.ino
 1int num = 0;
 2char buf[100];
 3
 4void setup() {
 5  Serial.begin(115200); // 通信速度をbpsで設定
 6}
 7
 8void loop() {
 9  int pos = 0;
10  while (Serial.available()) { //PCから送信された文字があるあいだ、くりかえす
11    char c = Serial.read(); // 1バイト読み取る
12    buf[pos] = c;           // 配列buf に格納
13    pos++;    // 格納位置をひとつ右へ
14  }
15  if (pos > 0) {
16    buf[pos] = 0; // さいごに Null (文字列の終端)を追加(これを忘れるとどうなる?)
17    Serial.print("> from pc: ");
18    Serial.print( buf ); // 格納しておいた文字列を表示
19    if (buf[0] == '0' && pos == 2) { // buf={ 48(='0'), 10(=改行) } のとき
20      num = 0;  // num を 0 にする
21      Serial.println( "Reset num" );
22    }
23    delay(2000);
24  }
25
26  Serial.printf("%d \n" , num );
27  num++;
28  delay(500);
29}

注釈

16行目の処理を忘れると、どうなるでしょうか? また、buf が溢れると、なにが起きるでしょうか?プログラムを書くときは、常に例外的な事象がおきる可能性を考えておきましょう。ただし、全ての例外的な事象に対処するのは困難な場合が多いです。

本体の液晶ディスプレイ(LCD)

  • M5StickCのDisplay周り解析 <https://lang-ship.com/blog/work/m5stickc-display> が詳しいです。

  • リスト 3 のサンプルでは、外側から内側に向かって、10ピクセルずつ余白を残しながら、色を変えて塗りつぶしています。その後、8,16,26ピクセルフォント(それぞれ1,2,4番)をsetCursor()関数で指定して、print()関数で文字列を描画しています。

  • M5StickCPlusの機能である液晶ディスプレイ(LCD)を使用するため、 #include <M5StickCPlus.h> を指定し、M5.begin() を呼び出しています。

リスト 3 src/lcd01.ino
 1#include <M5StickCPlus.h>
 2
 3void setup() {
 4  M5.begin();
 5  M5.Lcd.setRotation(3);
 6  M5.Lcd.fillRect(0, 0, 240, 135, RED); 
 7  //M5StickCPlusの画面サイズは、240 x 135
 8
 9  M5.Lcd.fillRect(10, 10, 220, 115, ORANGE);
10  M5.Lcd.fillRect(20, 20, 200, 95, YELLOW);
11  M5.Lcd.fillRect(30, 30, 180, 75, GREENYELLOW);
12  M5.Lcd.fillRect(40, 40, 160, 55, CYAN);
13  M5.Lcd.fillRect(50, 50, 140, 35, BLUE);
14  M5.Lcd.fillRect(60, 60, 120, 15, MAGENTA);
15
16  M5.Lcd.setTextFont(1); // 8pixel ASCII font
17  M5.Lcd.setTextSize(1); // Magnify (x1-7)
18  M5.Lcd.setTextColor(WHITE);
19  M5.Lcd.setCursor(10,1); M5.Lcd.print("RED");
20  M5.Lcd.setTextColor(BLACK);
21  M5.Lcd.setCursor(20,11); M5.Lcd.print("ORANGE");
22  M5.Lcd.setCursor(30,21); M5.Lcd.print("YELLOW");
23  M5.Lcd.setCursor(40,31); M5.Lcd.print("GREENYELLOW");
24  M5.Lcd.setCursor(50,41); M5.Lcd.print("CYAN");
25  M5.Lcd.setTextColor(WHITE);
26  M5.Lcd.setCursor(60,51); M5.Lcd.print("BLUE");
27  M5.Lcd.setCursor(70,60,2); M5.Lcd.print("MAGENTA (2)");
28  // setCursorの第3引数は、TextFont番号
29
30  M5.Lcd.setTextColor(BLACK);
31  M5.Lcd.setCursor(30,90,4); M5.Lcd.print("26pixel ASCII (4)");
32}
33
34void loop() {
35}
_images/lcd_sample.png

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

_images/setRotation.gif

注釈

フォントの種類を設定するときは、setTextFont() や、setCursor()の第3引数をつかいます。setTextSize() は、指定したフォントを「縦横何倍に引き伸ばして表示するか?」を設定します。ここの「倍率」を2以上に増やすと、若干ギザギザが目立つようになります。

本体のボタン

本体には、3つのボタンがあります。 (参考:M5StickCガイド の、4ページ目、ハードウェアの概要)

リスト 4 に、ボタンをおしたらシリアルモニタに表示する例を示します。ボタンも、M5StickCPlus の機能なので、 #include <M5StickCPlus.h> を指定し、M5.begin() を呼び出しておく必要があります。また、ボタンの状態を読み出して、ボタンオブジェクト(BtnA,BtnB)に設定するために、 M5.update() を、ループのなかに入れておく必要があります( update()の定義 )。長押しと普通押しを区別したい場合は、ifで確認する順番(長押しを先に判定する)が重要です。

リスト 4 src/button01.ino
 1#include <M5StickCPlus.h>
 2
 3void setup() {
 4  M5.begin();
 5  M5.Lcd.setRotation(3);
 6}
 7
 8void loop() {
 9  M5.update(); // 各ボタンの状態を(読み取って)更新する:ボタンを判定するときは必須。
10
11  if (M5.BtnA.wasReleasefor(1000) ) {
12    Serial.println("[A] was Pressed longer than 1s");
13  } else if (M5.BtnA.wasReleased()) {
14    Serial.println("[A] was Pressed");
15  } else if (M5.BtnB.wasReleasefor(1000) ) {
16    Serial.println("[B] was Pressed longer than 1s");
17  } else if (M5.BtnB.wasReleased()) {
18    Serial.println("[B] was Pressed");
19  } else if (M5.Axp.GetBtnPress() == 2) {
20    Serial.println("[Power] was Pressed");
21  }
22  delay(10);
23}

文字列の扱い、16進数や10進数の変換

リスト 5 に、文字列から整数に変換したり、16進数表現の文字列を10進数に変換する例を示します。 Stringクラスを用いると、char配列 / byte配列 / 小文字変換や、空白改行の削除(ただし文字列の前後のみ)、部分文字列の取得などができます。 元のデータが保持されるメソッドと、破壊されるメソッドがあることに注意しましょう。 M5系デバイスのLCDでは、色を16ビット値(赤5bit/緑6bit/青5bit)で表現しています。 M5.Lcd.color565() という関数もありますが、ここではビット演算の理解を深めるため、関数を定義しています。 C言語のポインタや、シフト演算などを理解していると、このような処理を円滑に記述できるようになります。 →動画 str01

リスト 5 src/str01.ino
 1#include <M5StickCPlus.h>
 2
 3void setup() {
 4  Serial.begin(115200);
 5  M5.begin();
 6  M5.Lcd.setRotation(3);
 7  Serial.println("整数 または 6文字の16進数(RRGGBB)を入力してください。");
 8}
 9
10String str = ""; // String: 可変長の文字列クラス
11void loop() {
12  char buf[100] = {0};
13  int pos = 0;
14  while (Serial.available()) { //PCから送信された文字があるあいだ、くりかえす
15    char c = Serial.read(); // 1バイト読み取る
16    buf[pos] = c;           // 配列buf に格納
17    pos++;    // 格納位置をひとつ右へ
18  }
19  if (pos > 0) {
20    buf[pos] = 0;
21    String sbuf = buf;
22    sbuf.trim(); //文字列の前と後の空白と改行を取り除く(破壊的メソッド)
23    str.concat(sbuf); // 文字列の連結 (意味としては、str = str + sbuf)
24    Serial.println(str);
25    if (isDigit(sbuf)) {
26      int num = sbuf.toInt();
27      Serial.println( num * num );
28    }
29    if (sbuf.length() == 6) { // RRGGBBとして、LCD画面の背景を塗りつぶす
30      sbuf.toLowerCase();
31      uint8_t r = hexToDec( sbuf.substring(0, 2) );
32      uint8_t g = hexToDec( sbuf.substring(2, 4) );
33      uint8_t b = hexToDec( sbuf.substring(4, 6) );
34      M5.Lcd.fillScreen( getColorUInt16( r , g , b ) );
35      M5.Lcd.setCursor(0, 70, 4);
36      M5.Lcd.printf(" %02x %02x %02x \n", r, g, b);
37      M5.Lcd.printf(" %d %d %d ", r, g, b);
38    }
39  }
40  delay(50);
41}
42//全ての文字が0〜9なら、1(true)を返す
43bool isDigit(String s) {
44  bool isAllDigit = true;
45  const char *p = s.c_str(); // String.c_str() は、NULLで終端されたchar配列の先頭アドレスを返す
46  while ( *p != 0 ) {
47    if (*p < '0' || '9' < *p) isAllDigit = false;
48    p++;
49  }
50  return isAllDigit;
51}
52// M5 用の、16ビットカラー値に変換 引用:https://qiita.com/nnn112358/items/ea6b5e81623ba690343c
53uint16_t getColorUInt16(uint8_t red, uint8_t green, uint8_t blue) {
54  return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
55}
56// 引数hex が16進表記の文字列だと仮定して、10進の値を返す
57uint8_t hexToDec(String hex) {
58  int dec = 0;
59  int tmp = 0;
60  for (int p = 0; p < hex.length(); p++) {
61    tmp = int(hex.charAt(p));
62    if ( '0' <= tmp && tmp <= '9' ) tmp = tmp - '0';
63    if ( 'a' <= tmp && tmp <= 'f' ) tmp = tmp - 'a' + 10;
64    if ( 'A' <= tmp && tmp <= 'F' ) tmp = tmp - 'A' + 10;
65    tmp = constrain(tmp, 0, 15); //例外処理
66    dec = (dec * 16) + tmp;
67  }
68  return dec;
69}

注釈

変数の型一覧 uint16_t は unsigned int 16bit type の略です。参考:M5StackのLCDディスプレイの色をRGBで指定する。

ブザー(Beep)

リスト 6 は、内蔵Beepを鳴らすサンプルです。音の周波数は、最初の1オクターブ (f[0]〜f[6]) のみを配列宣言時に指定し、残りの3オクターブはsetup()内で、倍数を計算して配列に設定しています。

setBeep()beep() を使うと、同じビープ音を使い回すときは記述が短くて便利ですが、通常は tone() のほうが簡潔です。 どちらの記法でも、命令実行のあと、音の停止を待たずに、すぐに次の処理に移行します(ノンブロッキング)。 継続ミリ秒数(ms)は、 M5.Beep.update() が周期的に呼び出せるときに指定し、それ以外は省略するとよいでしょう。(結局、 delay()mute() が必要になるため)

リスト 6 src/sound01.ino
 1#include <M5StickCPlus.h>
 2
 3int f[28] = { 262, 294, 330, 349, 392, 440, 494 }; // 配列のサイズは4オクターブ分
 4void setup() {
 5  Serial.begin(115200);
 6  M5.begin();
 7  M5.Lcd.setRotation(3);
 8  for (int i = 0; i < 7; i++) {
 9    f[i + 7]  = f[i] * 2;
10    f[i + 14] = f[i] * 4;
11    f[i + 21] = f[i] * 8;
12  }
13  // setBeep( Hz, ms) でビープ音を設定して、beep()で鳴らす。
14  M5.Beep.setBeep(523, 500);  M5.Beep.beep();  delay(500);
15  // tone( Hz, ms ) で鳴らす方法
16  M5.Beep.tone(1046, 500);  delay(500);
17  // msを省略し、tone( Hz ) で鳴らす(結局、delayが必要になるので)
18  M5.Beep.tone(2092);  delay(500);
19  // ここはmsを指定するとよい (loopのM5.Beep.updateが停めてくれるので、delayを省略できる)
20  M5.Beep.tone(2092 * 2, 1000);
21}
22
23void loop() {
24  M5.Beep.update(); //tone()やsetBeep()で指定した時間を過ぎていたら音を止める。
25  //                  M5.Beep.mute() とすると、すぐに停止する。
26
27  while (Serial.available()) {
28    char c = Serial.read();
29    if (c < 'c' || 'z' < c) continue; // c〜zの文字以外なら、以降のwhile内処理をスキップ
30    M5.Beep.tone( f[c - 'c'] , 1000); // 1秒鳴らす
31    Serial.println( f[c - 'c'] );
32  }
33  delay(50);
34}

内蔵LED(赤・赤外)

M5StickCPlusには、2つのLEDランプが内蔵されています。GPIO(General Purpose Input/Output)の10番 には、赤色LED、GPIO9 には、赤外LEDが接続されています。 場所は、USBケーブルを挿す面の反対側の面にある、小さな穴2つの内側です。

リスト 7 は、赤色LEDを点滅させるシンプルなプログラムです。

リスト 7 src/led01.ino
 1int PIN = 10; // 赤色LED G10
 2//int PIN = 9; // Ir LED (カメラには映りますが、肉眼では見えません) G9
 3// その他、G0 (0) , G25 (25), G26 (26) がつかえます。
 4// https://lang-ship.com/blog/work/m5stickc-io/
 5
 6void setup() {
 7  pinMode(PIN, OUTPUT); // PINのモード設定
 8}
 9
10void loop() {
11  digitalWrite(PIN, HIGH); // HIGH = 1
12  delay(1000);
13  digitalWrite(PIN, LOW); // LOW = 0
14  delay(500);
15}

外部のLED等を接続

外部のLED等は、 G0, G25, G26 に接続します。(GROVE端子のG32, G33も利用できます。) サンプルプログラムは、リスト 7 と同様です。PIN 番号を、0 / 25 / 26 に変更してください。

ブレッドボードに、抵抗とLEDを直列に接続します。 LEDの長い足のほうに、G0 (or G25 or G26) をジャンパワイヤで接続し、反対側(短い足)のほうを、GND (グランド)に接続します。 ポートを「HIGH」にすると、3.3Vの電圧がかかります。

_images/SimpleLED_m5s.png

警告

LEDのみで回路を構成しないよう注意!一般に、LEDは抵抗をつないで使用しないと、故障したり、最悪の場合破裂したりして危険です。ただし、抵抗入りLEDであれば問題ありません。

PWM (Pulse Width Modulation)

パルス幅変調とは、信号の周期Tに対する、パルスの幅を変化させる方法です。 直流モータの速度制御や、サーボモータの制御、LEDの明度調整などに使われます。

_images/pwm.jpg

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 のパルス生成に対応します。

リスト 8 src/pwm01.ino
 1int PIN = 10; // 内蔵LED
 2int PWM_CH = 1; // 0~3で指定する。0にすると高音が鳴って耳障り?
 3
 4void setup() {
 5  pinMode(PIN, OUTPUT); // PINのモード設定
 6  ledcSetup(PWM_CH, 90, 8); // CH, 周波数(Hz) 【 90Hz より、小さくするとどうなる?】 , 分解能(bit)
 7  ledcAttachPin(PIN, PWM_CH);
 8  // https://lang-ship.com/reference/unofficial/M5StickC/Peripherals/LED_Control/
 9}
10
11void loop() {
12  int i;
13
14  for (int i = 0 ; i < 256 ; i++) {
15    ledcWrite(PWM_CH, i);
16    delay( 5 );
17  }
18  for (int i = 256 ; i > 0 ; i--) {
19    ledcWrite(PWM_CH, i);
20    delay( 5 );
21  }
22}

注釈

ソースコードのコメントにも書いていますが、周波数を小さくする(20〜30程度)と、どうなるでしょうか?ぜひやってみてください。

サーボモータ(サーボハット利用)

リスト 9 は、サーボハット のサーボモータを動かすサンプルです。サーボモータの制御はPWMですので、基本的に、上のPWMとやっていることは同じです。おまけとして、LEDも点灯させました。map関数 は、Arduinoで使える関数で、範囲に対応する値を変換するときに使います。ここでは、サーボ制御で用いる5〜33の値を、LED制御の値0〜256に変換しています。

_images/servohat.png
リスト 9 src/servohat01.ino
 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)

ADCは、Analog to Digital Converter の意味です。 analogRead(PIN) は、PIN番ピンの電圧(0~3.3V)を、0〜4095 の値で返します。一般に、抵抗値が変化するタイプのセンサは、この方法をつかって、読み取ることができます。

警告

ADC にかいてあるように、G26, G32, G33, G36のみ使えます。G26は、無線利用時には使えません。

リスト 10 はCdSセル(照度センサ) の値を読み取るサンプルです。

リスト 10 src/cds01.ino
 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 に設定してください) ジャイロセンサの値も取得可能です。

リスト 11 src/accgyro.ino
 1// 参考にしたサイト: https://qiita.com/kitazaki/items/1ce671532270cd44fabf
 2
 3#include <M5StickCPlus.h>
 4
 5float gyroX, gyroY, gyroZ;  // ジャイロデータ 
 6float ax, ay, az;  // 加速度データ
 7
 8void setup() {
 9  M5.begin();
10  M5.Lcd.setRotation(3);
11  M5.IMU.Init();
12  Serial.begin(115200);
13  Serial.println("");
14}
15void loop() {
16  M5.IMU.getGyroData(&gyroX, &gyroY, &gyroZ);
17  M5.IMU.getAccelData(&ax, &ay, &az);
18  M5.Lcd.setCursor(0, 10, 2);
19
20  M5.Lcd.printf("aX :%7.2f  \naY :%7.2f  \naZ :%7.2f mg  \n\n", ax , ay , az );
21  Serial.printf("%7.2f , %7.2f , %7.2f \n", ax , ay , az); //シリアルプロッタ用の出力
22
23  M5.Lcd.printf("gX :%7.2f  \ngY :%7.2f  \ngZ :%7.2f mg  ", gyroX * M5.IMU.gRes, gyroY * M5.IMU.gRes, gyroZ * M5.IMU.gRes);
24  // Serial.printf("%7.2f,%7.2f,%7.2f,", gyroX * M5.IMU.gRes, gyroY * M5.IMU.gRes, gyroZ * M5.IMU.gRes);
25
26  delay(50);
27}

赤外(InfraRed)リモコン

注釈

実験時間中に、送信のみを手軽に試したい場合は、付録の リスト 30 を書き込んで、教員を呼んでください。

信号の読み取り

赤外線リモコン受信モジュールが必要です。ここでは、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)を覚えておきます。

_images/ir_sensor_47owm.png

信号の送信

内蔵の赤外LEDを用いて、信号を送信する例を リスト 12 に示します。 内蔵の赤外LED光はあまり強くないため、50cm程度まで近づかないと反応しない場合があります。また、太陽光の影響を強く受けます。 単体の赤外LEDを接続して用いると、距離を伸ばすことができます。

なお、NECフォーマットではない赤外線リモコンの通信フォーマットについては、 赤外線リモコンの通信フォーマット や、スケッチ例を参考にしてください。

リスト 12 src/irsend01.ino
 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.Lcd ではじまる画面描画命令は、表示されている画面に対して、直接描画するため、画面のちらつきが生じることがあります。 TFT_eSprite クラスを用いると、画面に表示されていない仮想画面(オフスクリーン)を作成して、そこに対して描画を行っておき、さいごにまとめて仮想画面の内容をメインの画面(Lcd)に描画することができます。 これにより、画面のちらつきが抑制できます(ダブルバッファリング)。 ちらつき防止のほかに、なめらかに画面スクロールする目的で、実画面よりも大きい仮想画面を作成しておき、その一部の領域のみを実画面に描画するといった用途にも使われます。

TFT_eSprite クラスは、Lcdクラスとほぼ同様の描画関数を備えています。リスト 13 は、スプライトの効果あり/なしをAボタンで切り替えて確認できるサンプルプログラムです。 →動画 sprite01

リスト 13 src/sprite01.ino
 1#include <M5StickCPlus.h>
 2
 3int USE_SPRITE = 0 ; //スプライトを使用するとき1
 4
 5TFT_eSprite spu = TFT_eSprite(&M5.Lcd); // Sprite object
 6
 7void setup() {
 8  M5.begin();
 9  M5.Lcd.setRotation(3);
10
11  spu.setColorDepth(8);
12  spu.createSprite(240, 135);
13
14  randomSeed(analogRead(0));
15}
16
17void loop() {
18  int r = random(100000);
19  if ( USE_SPRITE ) {
20    spu.fillSprite( CYAN );
21    spu.setCursor(30, 34, 4);  spu.setTextSize(2);
22    spu.setTextColor( WHITE, BLUE );
23    spu.printf("  %d  \n Sprite ", r );
24    spu.pushSprite(0, 0);
25  } else {
26    M5.Lcd.fillScreen( CYAN );
27    M5.Lcd.setCursor(30, 34, 4);  M5.Lcd.setTextSize(2);
28    M5.Lcd.setTextColor( WHITE, BLUE );
29    M5.Lcd.printf("  %d  \n LCD ", r);
30  }
31
32  M5.update();
33  if (M5.BtnA.wasReleased()) {
34    USE_SPRITE = 1 - USE_SPRITE ; // スプライト使用の切り替え(1なら0、0なら1)
35    M5.Beep.tone(1000 + 1000 * USE_SPRITE, 500);
36  }
37  M5.Beep.update();
38  delay(100);
39}

Wifi 接続

リスト 14 は、Wifi接続のサンプルです。ssidpassword には、環境にあわせたものを入力してください。接続すると、m5デバイスのIPアドレスを画面に表示します。本当にWifi接続できたかどうかを、PCのターミナルからpingを打つなどして、確認してみましょう。(Windowsの場合、コマンドプロンプトをひらき、ping のあとに、半角スペースと、確認したいIPアドレスを入れます)

リスト 14 src/wifi01.ino
 1#include <M5StickCPlus.h>
 2#include <WiFi.h>
 3
 4const char* ssid = "ics-ap";
 5const char* password = "jikkenics";
 6
 7void setup() {
 8  M5.begin();
 9  M5.Lcd.setRotation(3);
10  M5.Lcd.fillScreen(ORANGE);
11  M5.Lcd.setCursor(10, 50, 4);
12
13  WiFi.begin(ssid, password); // 接続開始
14  while (WiFi.status() != WL_CONNECTED) { // 接続中...
15    M5.Beep.tone(2000);  delay(200);
16    M5.Beep.mute();  delay(300);
17    M5.Lcd.print(".");
18  }
19  // 接続完了!!
20  M5.Beep.tone(4000); 
21  M5.Lcd.fillScreen(GREEN);
22  M5.Lcd.setCursor(0, 40, 4);
23  M5.Lcd.setTextColor(BLACK, GREEN);
24  M5.Lcd.print("  Wifi Connected!\n  ");
25  String gotip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
26  M5.Lcd.println(gotip);
27  delay(1500);
28  M5.Beep.mute();
29}
30
31void loop() {
32}

Wifi接続するだけでは、あまり意味がないので、Telnetサーバを起動する例を リスト 15 に示します。 シリアルモニタを開いて、IPアドレス(10.85.xxx.xxx)を確認したら、ターミナル(コマンドプロンプト)で、telnet 10.85.xxx.xxx と入力して、接続します。(Windowsでは https://www.imamura.biz/blog/27493 を参照し、telnetを有効にしてください。) telnet から文字を入力すると、シリアルモニタに表示されます。 逆に、シリアルモニタから文字を入力すると、Telnet接続しているターミナルに、文字が表示されます。 WiFiServer server(23) で、23番ポートで待ち受けるサーバを、作成しています。 ちなみに、WiFiMulti は、複数のアクセスポイントに対して、Wifi接続を試みることができる機能(クラス)です。ただし、最終的に繋がるのは1つのアクセスポイントになります。

注釈

Telnet接続を切断するときは、まずControlキーをおしながら ] をおしてください。プロンプトに telnet> と表示されますので、quit と打ち込むと終了します。 Escape character is '^]'.^ は、Controlキーのことです。

リスト 15 src/telnet01.ino
  1// オリジナルのWifiTelnetToSerial を、改変しました。
  2/*
  3  WiFiTelnetToSerial - Example Transparent UART to Telnet Server for ESP32
  4
  5  Copyright (c) 2017 Hristo Gochkov. All rights reserved.
  6  This file is part of the ESP32 WiFi library for Arduino environment.
  7
  8  This library is free software; you can redistribute it and/or
  9  modify it under the terms of the GNU Lesser General Public
 10  License as published by the Free Software Foundation; either
 11  version 2.1 of the License, or (at your option) any later version.
 12
 13  This library is distributed in the hope that it will be useful,
 14  but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 16  Lesser General Public License for more details.
 17
 18  You should have received a copy of the GNU Lesser General Public
 19  License along with this library; if not, write to the Free Software
 20  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 21*/
 22#include <WiFi.h>
 23#include <WiFiMulti.h>
 24
 25WiFiMulti wifiMulti;
 26
 27//how many clients should be able to telnet to this ESP32
 28#define MAX_SRV_CLIENTS 3
 29const char* ssid = "ics-ap";
 30const char* password = "jikkenics";
 31
 32WiFiServer server(23);
 33WiFiClient serverClients[MAX_SRV_CLIENTS];
 34
 35void setup() {
 36  Serial.begin(115200);
 37  Serial.println("\nConnecting");
 38
 39  wifiMulti.addAP(ssid, password);
 40  //  wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
 41  //  wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");
 42
 43  Serial.println("Connecting Wifi ");
 44  for (int loops = 10; loops > 0; loops--) {
 45    if (wifiMulti.run() == WL_CONNECTED) {
 46      Serial.println("");
 47      Serial.print("WiFi connected ");
 48      Serial.print("IP address: ");
 49      Serial.println(WiFi.localIP());
 50      break;
 51    }
 52    else {
 53      Serial.println(loops);
 54      delay(1000);
 55    }
 56  }
 57  if (wifiMulti.run() != WL_CONNECTED) {
 58    Serial.println("WiFi connect failed");
 59    delay(1000);
 60    ESP.restart();
 61  }
 62
 63  //start UART and the server
 64  //  Serial2.begin(9600);
 65  server.begin();
 66  server.setNoDelay(true);
 67
 68  Serial.print("Ready! Use 'telnet ");
 69  Serial.print(WiFi.localIP());
 70  Serial.println(" 23' to connect");
 71}
 72
 73void loop() {
 74  uint8_t i;
 75  if (wifiMulti.run() == WL_CONNECTED) {
 76    //check if there are any new clients
 77    if (server.hasClient()) {
 78      for (i = 0; i < MAX_SRV_CLIENTS; i++) {
 79        //find free/disconnected spot
 80        if (!serverClients[i] || !serverClients[i].connected()) {
 81          if (serverClients[i]) serverClients[i].stop();
 82          serverClients[i] = server.available();
 83          if (!serverClients[i]) Serial.println("available broken");
 84          Serial.print("New client: ");
 85          Serial.print(i); Serial.print(' ');
 86          Serial.println(serverClients[i].remoteIP());
 87          break;
 88        }
 89      }
 90      if (i >= MAX_SRV_CLIENTS) {
 91        //no free/disconnected spot so reject
 92        server.available().stop();
 93      }
 94    }
 95    //check clients for data
 96    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
 97      if (serverClients[i] && serverClients[i].connected()) {
 98        if (serverClients[i].available()) {
 99          //get data from the telnet client and push it to the UART
100          while (serverClients[i].available()) Serial.write(serverClients[i].read());
101        }
102      }
103      else {
104        if (serverClients[i]) {
105          serverClients[i].stop();
106        }
107      }
108    }
109    //check ==UART== => Serial for data
110    if (Serial.available()) {
111      size_t len = Serial.available();
112      uint8_t sbuf[len];
113      Serial.readBytes(sbuf, len);
114      //push UART data to all connected telnet clients
115      for (i = 0; i < MAX_SRV_CLIENTS; i++) {
116        if (serverClients[i] && serverClients[i].connected()) {
117          serverClients[i].write(sbuf, len);
118          delay(1);
119        }
120      }
121    }
122  }
123  else {
124    Serial.println("WiFi not connected!");
125    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
126      if (serverClients[i]) serverClients[i].stop();
127    }
128    delay(1000);
129  }
130}

NTPサーバ (Network Time Protocol) と時刻の取得

M5StickCPlusには、システム時間(localTime)と、RTC(リアルタイムクロック:時計の機能を備えたICのこと)の2種類の時計があります。システム時間は、システムリセット(再起動)のたびに、時刻もリセットされますが、後者のRTCはリセットされません。

リスト 16 に、NTPサーバを使ってシステム時間の修正をしたのち、システム時間を1秒ごとに取得して、シリアルモニタに表示する例を示します。こちらは、M5のライブラリは不要です。configTime()でNTPサーバを設定しておくと、1時間に1回、NTPサーバに接続して、時刻修正します。 →動画 ntp01

リスト 16 src/ntp01.ino
 1#include <WiFi.h>
 2
 3const char* ssid = "ics-ap";
 4const char* password = "jikkenics";
 5
 6void setup() {
 7  Serial.begin(115200);
 8  WiFi.begin(ssid, password);
 9  while (WiFi.status() != WL_CONNECTED) { // 接続中...
10    Serial.print(".");
11  }
12//  char* ntpserver = "ntp.nict.jp"; // 学内のNetworkを利用するときは、10.64.7.184 にする
13  char* ntpserver = "10.64.7.184"; // 学内のNetworkを利用するときは、10.64.7.184 にする
14  configTime(9 * 3600, 0, ntpserver);//GMTとの時差(秒) が9*3600, サマータイムで進める時間(秒)が0
15}
16
17void loop() {
18  struct tm localTime;
19  char buf[30];
20  getLocalTime(&localTime);
21  sprintf(buf, ">> %04d/%02d/%02d  %02d:%02d:%02d",
22          localTime.tm_year + 1900,
23          localTime.tm_mon + 1,
24          localTime.tm_mday,
25          localTime.tm_hour,
26          localTime.tm_min,
27          localTime.tm_sec
28         );
29  Serial.println(buf);
30  delay(1000);
31}

警告

configTime() を行わない状況で、システム時間(localTime)を取得しようとすると、取得に数秒ほど時間がかかります。

リスト 17 は、RTCの時刻を表示するサンプルです。なお、5行目で USE_NTP に 1 が設定してあれば、RTCに時刻を設定します。常時Wifiネットワークに接続できない場合は、RTCを利用することが望ましいです。

リスト 17 src/rtc01.ino
 1#include <M5StickCPlus.h>
 2#include <WiFi.h>
 3// #include "time.h"
 4
 5#define USE_NTP 1   // NTPからRTCに時刻設定するなら1
 6
 7void setRTCfromLT(struct tm lt) {
 8  RTC_DateTypeDef DateStruct;
 9  DateStruct.Year = lt.tm_year + 1900;
10  DateStruct.Month = lt.tm_mon + 1;
11  DateStruct.Date = lt.tm_mday;
12  DateStruct.WeekDay = lt.tm_wday;
13  M5.Rtc.SetData(&DateStruct);
14
15  RTC_TimeTypeDef TimeStruct;
16  TimeStruct.Hours   = lt.tm_hour;
17  TimeStruct.Minutes = lt.tm_min;
18  TimeStruct.Seconds = lt.tm_sec+1;
19  M5.Rtc.SetTime(&TimeStruct);
20}
21
22void getRTC(char* buf) {
23  RTC_DateTypeDef DateStruct;
24  RTC_TimeTypeDef TimeStruct;
25  M5.Rtc.GetData(&DateStruct);
26  M5.Rtc.GetTime(&TimeStruct);
27  sprintf(buf, "%04d/%02d/%02d %02d:%02d:%02d",
28          DateStruct.Year,  DateStruct.Month,   DateStruct.Date,
29          TimeStruct.Hours, TimeStruct.Minutes, TimeStruct.Seconds
30         );
31}
32
33void setup() {
34  M5.begin();
35  M5.Lcd.setRotation(3);
36  Serial.begin(115200);
37
38  if (USE_NTP) {
39    const char* ssid = "ics-ap";
40    const char* password = "jikkenics";
41    const char* ntpserver = "ntp.nict.jp"; // or 10.64.7.184 for CIT-ap1x
42    WiFi.begin(ssid, password);
43    while (WiFi.status() != WL_CONNECTED) { // 接続中...
44      Serial.print(".");
45    }
46    configTime(9 * 3600, 0, ntpserver);
47
48    struct tm localTime;
49    while (localTime.tm_year < 80) {
50      getLocalTime(&localTime); delay(50);
51    }
52    setRTCfromLT(localTime);
53  }
54}
55
56void loop() {
57  char buf[30];
58  getRTC(buf); // bufに、日時文字列を書き込む
59  Serial.println(buf);
60  M5.Lcd.fillScreen(BLUE);
61  M5.Lcd.setCursor(0, 50, 4);
62  M5.Lcd.println(buf);
63  delay(1000);
64}

注釈

RTCに一旦時刻を設定しておくと、初期状態で書き込まれているプログラムFactoryTest の「BMP8563 RTC Time」でも、その時刻が表示されるようになります。

WebClient

リスト 18 は、HTTP通信で天気予報Web APIに接続するサンプルです。HTTPClientクラスを用いると、ブラウザでURLを指定してWebページを開くように、WebサーバにGETメソッドやPOSTメソッドでリクエストを送信して、ステータスコードやレスポンスを取得することができます。このサンプルでは、シリアルコンソールに、天気予報をJSON形式で表示します。JSON(ジェイソン)とは、Javascriptのオブジェクトの形式でデータを表現する記法です。

リスト 18 src/httpclient01.ino
 1#include <WiFi.h>
 2#include <HTTPClient.h> // ステータスコードの定義もここにある
 3
 4const char* ssid = "ics-ap";
 5const char* password = "jikkenics";
 6
 7// 天気予報API https://weather.tsukumijima.net/ から、千葉の天気を取得 
 8const char* weatherapi_url = "http://weather.tsukumijima.net/api/forecast/city/120010";
 9
10void setup() {
11  Serial.begin(115200);
12  WiFi.begin(ssid, password);
13  while (WiFi.status() != WL_CONNECTED) { // 接続中...
14    delay(50);
15    Serial.print(".");
16  }
17  delay(1000);
18  
19  HTTPClient http;            // クライアント作成
20  http.begin(weatherapi_url); // HTTPでサーバに接続
21  int httpCode = http.GET();  // ステータスコードを取得
22  if (httpCode > 0) {
23      Serial.println(httpCode);
24    if (httpCode == HTTP_CODE_OK) { // ステータスコードが「成功」(200) なら
25      String payload = http.getString();
26      Serial.println(payload);
27    }
28  }
29  http.end();
30}
31
32void loop() {
33}

警告

https (SSL) 通信でPOSTメソッドで送信する場合は、付録→LINE Notify (リスト 33)を参考にしてください。以前は「スケッチ例→HTTPClient→BasicHttpsClient を参照して、WiFiClientSecure クラスを使用してください。」と書いていましたが、WifiClientSecureは必要ありません。

Google Spreadsheet にデータを送信する

HTTP通信で、サーバにデータを送信して格納したいとおもっても、適当なサーバを準備するのは手間がかかることがあります。

Google Apps Scriptを用いると、HTTP通信で Google Spreadsheet にデータを書き込んだり、読み取ったりするWebサービスを作成し、WebAPIとして公開することができます。

(Google Spreadsheetのメニューで) 拡張機能→Apps Script で、リスト 19 を「コード.gs」に書き込みます。14行目で「シート1」の一番下の行に、配列array の要素を追加します。18行目のgetRange(1,3).getValue()は、スプレッドシートのC1(3列1行)の値を取得しています。ここに「=average(C2:C200)」のようにしておくと、データの平均値を取得することもできます。

作成した「コード.gs」について、「デプロイ」→「新しいデプロイ」で、アクセスできるユーザを「全員」にし、「デプロイ」を押します。すると、ウェブアプリのURLが表示されますのでコピーします。このあと、Googleアカウント権限について聞かれる場合があります。

作成できたかどうかをパソコンから確認するには、コマンドラインから、以下のように入力します(ただし、curlが必要です。val1=10 と val2=20 のあいだは&記号ですので、シェルでバックグラウンド処理されないように、URLをシングルクォートで囲っています。-L (--location)は、リダイレクト先にリクエストを発行するcurlのオプションです。) exec のあとに ? をいれて、名前1=値1&名前2=値2 のようにしてデータを渡します。

curl -L  'https://script.google.com/macros/s/XxXxXxXxXx/exec?val1=10&val2=20'
リスト 19 src/doget.js
 1function doGet(e) {
 2  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 3  var sheet = spreadsheet.getSheetByName('シート1');
 4  var now = new Date();
 5  var array = [];
 6  array.push(now);
 7  
 8  for(var k in e['parameter']) {
 9    array.push(k);
10    array.push(e['parameter'][k]);
11  }
12  var params = JSON.stringify(e);
13  array.push(params);
14  sheet.appendRow( array );
15  
16  var lastRow = sheet.getLastRow();
17
18  var output = ContentService.createTextOutput(sheet.getRange(1,3).getValue()+" "+lastRow);
19  output.setMimeType(ContentService.MimeType.TEXT);
20  return output;
21}

警告

doGet関数の返却値は、本来はHTTPのレスポンスとして受け取れるのですが、現状の HTTPClient ではリダイレクトをうまく処理できない?ため、Google App Scriptの出力を受け取れない可能性があります。

WebServer

すこし長いですが、リスト 20 は、80番ポートでHTTPでの通信を待ち受け(listenし)て、クライアントからの接続情報(ヘッダ情報)を返すWebサーバのシンプルな例です。クライアント(ブラウザ)からのリクエスト行のうち、 GET または POST ではじまる行があれば、変数 meth に格納します。つまり、変数 meth には、ブラウザで発行したリクエストのURLが含まれることになります。(methはmethodの略。)POSTメソッドで送信されていれば、リクエストボディに記述されたデータを変数 post に格納します。 クライアント(ブラウザ)に返す「レスポンス」は、 client.println() で送信します。最後に、client.stop() で、サーバ側から接続を切断します。 →動画 httpserver01

リスト 20 src/httpserver01.ino
 1#include <WiFi.h>
 2
 3const char* ssid = "ics-ap";
 4const char* password = "jikkenics";
 5
 6WiFiServer server(80);
 7
 8void setup() {
 9  Serial.begin(115200);
10  WiFi.begin(ssid, password);
11  while (WiFi.status() != WL_CONNECTED) { // 接続中...
12    delay(50);
13    Serial.print(".");
14  }
15  String ip = WiFi.localIP().toString(); // m5デバイスのIPアドレス
16  Serial.printf("\nopen http://%s\n\n", ip.c_str() );
17
18  server.begin(); // Webサーバを開始
19}
20
21void loop() {
22  WiFiClient client = server.available();
23  if (client) {
24    String req = "" ;
25    String tmp = "" , meth = "" ;
26    while (client.connected()) {            // loop while the client's connected
27      if (client.available()) {             // if there's bytes to read from the client,
28        char c = client.read();             // read a byte, then
29        req += c;
30        if (c == '\n') {                    // if the byte is a newline character
31          if (tmp.length() == 0) {  // end of request, break while loop
32            break;
33          } else { //まだ継続
34            if (tmp.startsWith("GET ") || tmp.startsWith("POST ") ) {
35              meth = tmp;
36            }
37            tmp = "";
38          }
39        } else if (c != '\r') {  // if you got anything else but a carriage return character,
40          tmp += c;      // add it to the end of the currentLine
41        }
42      }
43    } // end of while
44
45    Serial.println(meth);
46    if ( meth.startsWith("GET /") ) {
47      client.println("HTTP/1.1 200 OK"); // header (with response code)
48      client.println("Content-Type:text/plain");
49      client.println(""); // HTTPでは、header と body の区切りは改行
50      client.println(meth);
51      client.println("-- request --");
52      client.println(req);
53    }
54
55    if ( meth.startsWith("POST ") ) {
56      String post = "";
57      char buf[257];
58      int n;
59      while ((n = client.available()) > 0) {
60        if (n < 256) {
61          client.readBytes(buf, n) ;
62          buf[n] = 0 ;
63        } else {
64          client.readBytes(buf, 256) ;
65          buf[256] = 0 ;
66        }
67      }
68      post += buf ;
69
70      client.println("HTTP/1.1 200 OK");
71      client.println("Content-Type:text/plain");
72      client.println(""); // HTTPでは、header と body の区切りは改行
73      client.println(meth);
74      client.println("-- request --");
75      client.println(req);
76      client.println("-- post data --");
77      client.println(post);
78    }
79    // close the connection:
80    client.stop();
81    Serial.println(" --- Client Disconnected.");
82  }
83  delay(100);
84}

センサデータを返すだけなら問題ありませんが、クライアントからのデータを GET / POST で受信して処理する場合は、 key1=val1&key2=val2 のような文字列を要素に分解する必要がでてきます。あまり深入りしませんが、正規表現で文字列を照合・抽出する Regexp や、抽出した結果をハッシュ/辞書として保存する Dictionary ライブラリを導入すると、複雑なリクエストやデータを扱いやすくなるでしょう。 リスト 21 に、RGBの色指定文字列などのリクエスト文字列をパースして辞書に追加するプログラムの断片を示します。注意:このプログラム( リスト 21 )は、単体では動作しません

リスト 21 src/regexp01.ino
 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

リスト 22 は、MQTT Publisherのサンプルです。実験用ブローカ(mqtt.istlab.info)に接続して、 office/temp というトピックにデータ(シリアルコンソールで送信した文字列)を書き込みます。

リスト 22 src/mqtt01pub.ino
 1// MQTT Publisher example
 2//#include <M5StickCPlus.h>
 3#include <WiFi.h>
 4// ライブラリで PubSubClient をいれておく ★★★ 重要 ★★★
 5#include <PubSubClient.h>
 6
 7const char* ssid = "ics-ap";
 8const char* password = "jikkenics";
 9
10const char* server = "mqtt.istlab.info";
11const int   port     = 1883;   
12const char* pubTopic = "jikken"; // 班名を入れれば他の班とかぶらない。+にはしない。ex. jikken/A4han
13
14WiFiClient wifiClient;
15char* clientid = "m5stickc01_00000002"; //デバイス個別に設定すればなんでもよい
16PubSubClient mqttClient(wifiClient); // MQTT Client
17
18void setup() {
19  // M5.begin();
20  Serial.begin(115200);
21  WiFi.begin(ssid, password);
22  while ( WiFi.status() != WL_CONNECTED ) { // 接続中...
23    Serial.print(".");
24    delay(200);
25  }
26  Serial.println( WiFi.localIP().toString() ); //取得したIPアドレス
27  
28  mqttClient.setServer(server, port);
29  
30  if ( !mqttClient.connected() ) {
31    Serial.println("Try connecting MQTT ...");
32    reconnect();
33  }
34  
35  // 参考:WiFiデバイスのMACアドレスを取得し、送信する
36  uint8_t mac[6];
37  esp_read_mac(mac, ESP_MAC_WIFI_STA);
38  char mbuf[100];
39  sprintf(mbuf, "Message from %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] );
40  
41  mqttClient.publish( pubTopic , mbuf ); // MQTT publish message
42  Serial.println(mbuf); //送信した内容をシリアルに表示して確認
43  
44  Serial.println("publishするメッセージをSerialから送信してください");
45}
46
47void loop() {
48  // シリアルコンソールから書き込みがあれば, publishする
49  char mbuf[100];  int pos = 0;
50  while ( Serial.available() ) { // ノンブロッキング
51    mbuf[pos]  = Serial.read();
52    pos++;
53  }
54  if (pos > 0) {
55    mbuf[pos-1] = 0; //改行をNULLに置き換える
56    Serial.println(mbuf);
57    if ( !mqttClient.connected() )  reconnect();
58    // ブローカにデータを送信する。最後の false を true にすると、retained になる。
59    //    boolean ret = mqttClient.publish( pubTopic , mbuf, size, false );
60    mqttClient.publish( pubTopic , mbuf  );
61    //    if ( !ret ){
62    //      Serial.println("publish failed.");
63    //    }
64  }
65  delay(100);
66}
67
68void reconnect() {
69  while ( !mqttClient.connected() ) {
70    if ( mqttClient.connect(clientid) ) {
71      Serial.println("Connected to MQTT Broker.");
72    } else {
73      Serial.printf("Connect Failed. state=%d", mqttClient.state());
74    }
75  }
76}

警告

学内ネットワークからは、ポート制限のため、実験用ブローカ(mqtt.istlab.info) に接続できません。実験室に仮設するWifi基地局を利用してください。

MQTT Subscribe

リスト 23 は、MQTT Subscriberのサンプルです。実験用ブローカ(mqtt.istlab.info)に接続して、 ワイルドカード文字 + (プラス) は、任意の単一トピック階層にマッチするすべてのトピックを購読します。ワイルドカード文字 # は / で区切られた下位トピック階層も含めてマッチします。 JSON形式のデータを処理するときは、ArduinoJsonをつかってパージング(deserialize) すると便利です(76行目以降にサンプルがあります)。

リスト 23 src/mqtt01sub.ino
 1// MQTT Subscriber example
 2// #include <M5StickCPlus.h>
 3#include <WiFi.h>
 4// ライブラリで PubSubClient をいれておく ★★★ 重要 ★★★
 5#include <PubSubClient.h>
 6// ライブラリで ArduinoJson v6.xをいれておく ★★★ 重要 ★★★
 7//        (注意:Arduino_Json v0.1.0 は別物)
 8#include <ArduinoJson.h>
 9
10const char* ssid = "ics-ap";
11const char* password = "jikkenics";
12
13const char* server = "mqtt.istlab.info";
14const int   port     = 1883;   
15const char* pubTopic = "+"; // 例: ex1/groupXX/sensor
16
17WiFiClient wifiClient;
18char* clientid = "m5stickc01_00000001"; //デバイス個別に設定すればなんでもよい
19PubSubClient mqttClient(wifiClient); // MQTT Client
20
21void setup() {
22  //  M5.begin();
23  Serial.begin(115200);
24  WiFi.begin(ssid, password);
25  while (WiFi.status() != WL_CONNECTED) { // 接続中...
26    Serial.print(".");
27    delay(200);
28  }
29  Serial.println( WiFi.localIP().toString() ); //取得したIPアドレス
30  // 参考:WiFiデバイスのMACアドレスを取得し、clientid として用いる
31  // (18行目のclientidの定義を変更し、char clientid[20]とするのがのぞましい)
32  //  uint8_t mac[6];
33  //  esp_read_mac(mac, ESP_MAC_WIFI_STA);
34  //  sprintf(clientid, "%02X:%02X:%02X:%02X:%02X:%02X",
35  //          mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] );
36  mqttClient.setServer(server, port);
37  mqttClient.setCallback(callback_on_subscribe);
38  
39  if (!mqttClient.connected()) {
40    Serial.println("Try (re)connecting...");
41    reconnect();
42  }
43  mqttClient.subscribe(pubTopic);
44  
45}
46
47void loop() {
48  
49  mqttClient.loop(); // データがpublishされたら、callback_on_subscribe が呼ばれる
50  
51  delay(100);
52}
53
54void reconnect() {
55  while (!mqttClient.connected()) {
56    if (mqttClient.connect(clientid)) {
57      Serial.println("Connected to MQTT Broker.");
58    } else {
59      Serial.printf("Connect Failed. state=%d", mqttClient.state());
60    }
61  }
62}
63
64// データがPublishされたら、ここが実行される。(39行目でコールバック関数を設定しているため)
65void callback_on_subscribe(char* topic, byte* payload, unsigned int len) {
66  char buf[100];
67  Serial.print("Message arrived [");
68  Serial.print(topic);
69  Serial.print("] ");
70  for (int i = 0; i < len ; i++) {
71    buf[i] = (char)payload[i];
72  }
73  buf[len] = 0;
74  Serial.println(buf);
75  return;
76  //  参考:JSON Parsing example
77  //  StaticJsonDocument<200> sjdoc;
78  //  deserializeJson(sjdoc, buf);
79  //  int intval = sjdoc["intval"];
80  //  const char* str = sjdoc["string"];
81}

トピック名とワイルドカード

上述のとおり、サブスクライブするときのトピック名には、ワイルドカード文字が指定できます。例えば、 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)

「Serial通信」では、開発用PCのシリアルコンソールをつかって、USB接続を介して、文字列を送受信しました。 「Wifi接続」のTelnetサーバでは、TCP/IP通信上でのTelnet接続を介して、文字列を送受信しました。

ここでは、Bluetoothを用いて、上記と同様、2台のデバイス間で文字列を送受信する例を示します。Bluetoth では、機器同士は同一の「プロファイル」という通信方式に対応している必要があります。Serial Protocol Profile は、Bluetooth無線通信で仮想シリアル接続を可能にするプロファイルです。

注釈

基本的に ESP32同士をBluetoothシリアルでつないでみる と同じです。その他、一般的なBluetoothとBLEについての解説は、ESP32による近距離無線通信の実験② BLE通信 が参考になります。

(1) スレーブ用デバイスのMACアドレスを調べる

リスト 24 を、1台目のM5StickCPlusで実行し、BluetoothのMACアドレスを調べます。

リスト 24 src/bts01_slave.ino
 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) マスタ用デバイスにサンプルを書き込む

リスト 25 を、2台目のM5StickCPlusで実行します。このとき、ハイライトした行のところに、(1)で調べたMACアドレスを指定します。少し時間がかかりますが、デバイス名で接続先を指定することもできます。起動するときは、スレーブ側を先に起動しておき、あとでマスタ側を起動してください。

リスト 25 src/bts01_master.ino
  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/ を参照してください。

リスト 26 に、MACアドレスにFF:FF:FF:FF:FF:FF を指定し、ブロードキャストする例を示します。 ブロードキャストの場合、送信先のMACアドレスを指定しなくてよいので楽ですが、他の班のデバイスからも受信できます。espnow02.ino は、数値データのみを送受信する例です。espnow03.ino は、整数・小数・文字列をまとめてbyte (uint8_t) 配列に埋め込んで送受信する例です。

リスト 26 src/espnow02.ino
  1#include "M5StickCPlus.h"
  2#include <esp_now.h>
  3#include <WiFi.h>
  4
  5esp_now_peer_info_t peerInfo;
  6int line = 0;
  7
  8void Init_ESPNOW()
  9{ // ESPNowの初期化
 10    // 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
 11    WiFi.mode(WIFI_STA);
 12    WiFi.disconnect();
 13    if (esp_now_init() == ESP_OK)
 14    {
 15        M5.Lcd.println("ESP-Now Init Success");
 16    }
 17    else
 18    {
 19        M5.Lcd.println("ESP-Now Init failed");
 20        ESP.restart();
 21    }
 22    // マルチキャスト用Slave登録
 23    memset(&peerInfo, 0, sizeof(peerInfo));
 24    for (int i = 0; i < 6; ++i)
 25    {
 26        peerInfo.peer_addr[i] = (uint8_t)0xff;
 27    }
 28    esp_err_t addStatus = esp_now_add_peer(&peerInfo);
 29    if (addStatus == ESP_OK)
 30    {
 31        // Pair success
 32        M5.Lcd.println("Pair success");
 33        esp_now_register_send_cb(onESPNOWSent);    //送信後のコールバック関数を指定する
 34        esp_now_register_recv_cb(onESPNOWReceive); ///受信時のコールバック関数を指定する
 35    }
 36}
 37// データを送信した後に実行されるコールバック関数
 38// 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
 39void onESPNOWSent(const uint8_t *mac_addr, esp_now_send_status_t status)
 40{
 41    // char macStr[18];
 42    // snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
 43    //          mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
 44    line++;
 45    if (line > 3)
 46    {
 47        M5.Lcd.fillScreen(YELLOW);
 48        M5.Lcd.setCursor(0, 0, 1);
 49        line = 0;
 50        M5.Beep.tone(2000, 500);
 51    }
 52    else
 53    {
 54        M5.Beep.tone(1000, 500);
 55    }
 56    M5.Lcd.setTextColor(BLACK, GREEN);
 57
 58    M5.Lcd.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
 59}
 60
 61// データを受け取った時に実行されるコールバック関数
 62void onESPNOWReceive(const uint8_t *mac_addr, const uint8_t *data, int data_len)
 63{
 64    char macStr[18];
 65    snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
 66             mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
 67    // Serial.println();
 68    // Serial.printf("Last Packet Recv from: %s\n", macStr);
 69    // Serial.printf("Last Packet Recv Data(%d): ", data_len);
 70    if (data[0] == 100 && data[1] == 254) // 100,254に深い意味はない。uint8_tは0〜255の数値
 71    {
 72        line++;
 73        if (line > 3)
 74        {
 75            M5.Beep.tone(2000, 1000);
 76            M5.Lcd.fillScreen(YELLOW);
 77            M5.Lcd.setCursor(0, 0, 1);
 78            line = 0;
 79        }
 80        M5.Lcd.setTextColor(BLACK, CYAN);
 81
 82        M5.Lcd.printf(" %s\n", macStr);
 83        M5.Lcd.printf("Data: %d %d\n", data[0], data[1]);
 84    }
 85}
 86
 87void setup()
 88{
 89    M5.begin();
 90    M5.Lcd.setRotation(3);
 91    M5.Lcd.fillScreen(YELLOW);
 92
 93    M5.Lcd.setTextColor(BLACK, YELLOW);
 94    M5.Lcd.setCursor(0, 0, 1);
 95    M5.Lcd.setTextSize(2);
 96
 97    Init_ESPNOW();
 98}
 99void loop()
100{
101    M5.update();
102    M5.Beep.update();
103    if (M5.BtnA.wasReleased())
104    {
105        uint8_t data[2] = {100, 254}; // 送信データ    100, 254に深い意味はない。uint8_tは0〜255の数値
106        esp_err_t result = esp_now_send(peerInfo.peer_addr, data, sizeof(data));
107    }
108    delay(20);
109}

Preference

再起動をすると、プログラムがリセットされて、通常の変数や配列データは消えてしまいます。Preferenceを用いると、不揮発性のフラッシュ領域を使ってデータを保存・復元することができます。

https://github.com/espressif/arduino-esp32/blob/master/libraries/Preferences/examples/StartCounter/StartCounter.ino

電源制御・電力制御

  • ESP.restart() で、再起動します。

  • M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80); で、電源OFFします。

  • setCpuFrequencyMhz(240) で、CPUのクロック数を変更できます。設定できる値は、240、160、80、40、20、10ですが、無線通信するなら80以上を設定する必要があります。

  • M5.Axp.ScreenBreath(8) で、画面の明るさを調整できます。8〜15が標準的ですが、7でも微かに読めます。

リスト 27 に、Preferenceと電源制御のサンプルを示します。Aボタンでcountを増やし、Aボタン長押しでcountをリセットします。Bボタンで再起動、電源ボタンで電源OFFします。

リスト 27 src/pref01.ino
 1#include <M5StickCPlus.h>
 2#include <Preferences.h>
 3
 4Preferences pref;
 5
 6int count = 0;
 7void setup() {
 8  M5.begin();
 9  M5.Lcd.setRotation(0); //縦型
10  M5.Lcd.fillScreen(CYAN);
11  M5.Lcd.setTextSize(2);
12  loadCount(&count);
13  M5.Beep.tone(2000, 500);
14  M5.Lcd.printf("count = %d\n", count);
15}
16
17void loop() {
18  M5.Beep.update();
19  M5.update();
20  if (M5.BtnA.wasReleasefor(1000) ) { // Aボタン長押し
21    count = 0;
22    saveCount(&count);
23    M5.Beep.tone(1200, 300);
24    M5.Lcd.printf("count = %d\n", count);
25  } else if (M5.BtnA.wasReleased()) { // Aボタン押し
26    count++ ;
27    saveCount(&count);
28    M5.Beep.tone(2000, 300);
29    M5.Lcd.printf("count = %d\n", count);
30  } else if (M5.BtnB.wasReleased()) { // Bボタン押し
31    reboot();
32  } else if (M5.Axp.GetBtnPress() == 2) { // 電源ボタン押し
33    poweroff();
34  }
35  delay(50);
36}
37
38void loadCount(int *c) {
39  pref.begin("mydata", false);
40  *c = pref.getInt("count");
41  pref.end();
42}
43void saveCount(int *c) {
44  pref.begin("mydata", false);
45  pref.putInt("count", *c);
46  pref.end();
47}
48
49void poweroff() {
50  M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80);//PowerOff
51}
52
53void reboot() {
54  ESP.restart();
55}