みなさんもお気付きのことだと思いますが、最近一部のマイコン製品の価格が高騰しています。
特にM5Stack Basicは値上がりが酷く、1年半くらい前までは3,575円だったのが今は5,203円になっています。
一方、Wio Terminalは秋月電子通商では発売時の価格を維持していて、3,300円で購入できます。在庫もAAAと豊富なので、当分はこの価格で買えそうです。
当初は値段差があまりなかったこともあって慣れないWio Terminalを使おうとは思わなかったのですが、さすがに1.5倍くらいの値段差があると考えますよね。
※2022年9月6日現在、秋月電子通商でM5Stack Basicは6,380円、Wio Terminalは5,490円になっています。
仕様では見え難い2つの製品の違い
では、見た目はかなり似通った2つの製品の仕様を比べてみましょう。
仕様も似通った感じですが、M5Stack BasicがメインSoCだけで無線機能も実現しているのに対して、Wio TerminalはメインSoCよりも処理能力が高いSoCを無線用に搭載しているのが最大の違いです。
この違いがプログラムに影響を及ぼしていることを、Arduinoでコンパイルを実行すると確認することができます。
この表は、NTPで時間を取得してMachinistにデータを送信するだけの同じソースをコンパイルした結果です。
両方のスケッチの使用サイズを見てください。M5Stack BasicはWio Terminalの約9倍のサイズになっています。また、M5Stack BasicのRAMサイズは仕様では520KBになっていますが、最大RAMサイズの表示は約320KBに減少しています。
これは、M5Stack Basicは無線機能のリソースもプログラムに含まれているが、Wio Terminalは含まれていないことに起因しているものと思われます。
実際、Realtek RTL8720DNにも内蔵のSRAMが存在しているので、Wio Terminal全体のSRAMは192KBではないのですが明記されていません。
サンプルプログラム
#define WIFI_SSID "2.4GHzのSSID" #define WIFI_PASS "2.4GHzのSSIDのパスワード" #define MACHINIST_KEY "MachinistのAPIキー" #define MACHINIST_ID "MachinistのエージェントID" // コメントを外したデバイスが有効になります //#define USE_M5STACK // M5Stackを使う場合 //#define USE_WIO_TERMINAL // Wio Terminalを使う場合 // M5Stack用 #ifdef USE_M5STACK #define SERIAL M5.Lcd #define ANALOG_PIN 36 #include <WiFiClientSecure.h> #include <WiFiudp.h> #include <sys/time.h> #include <M5Stack.h> #endif // Wio Terminal用 #ifdef USE_WIO_TERMINAL #define SERIAL tft #define ANALOG_PIN A0 #include <rpcWiFi.h> #include <WiFiClientSecure.h> #include <WiFiudp.h> #include <sys/time.h> #include <TFT_eSPI.h> TFT_eSPI tft; #endif // 時刻フォーマット #define TIME_FORMAT "%04d-%02d-%02d %02d:%02d:%02d" // グローバル変数 WiFiClientSecure client; WiFiUDP udp; time_t next_day; // 次に1日1回の処理をする時刻 time_t next_time; // 次に送信処理をする時刻 int line_counter; // 画面表示行数カウンタ // Machinist GWサーバー(gw.machinist.iij.jp)のルートCAの証明書 const char* root_ca = \ "-----BEGIN CERTIFICATE-----\n" "MIIETjCCAzagAwIBAgINAe5fIh38YjvUMzqFVzANBgkqhkiG9w0BAQsFADBMMSAw" "HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFs" "U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xODExMjEwMDAwMDBaFw0yODEx" "MjEwMDAwMDBaMFAxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52" "LXNhMSYwJAYDVQQDEx1HbG9iYWxTaWduIFJTQSBPViBTU0wgQ0EgMjAxODCCASIw" "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdaydUMGCEAI9WXD+uu3Vxoa2uP" "UGATeoHLl+6OimGUSyZ59gSnKvuk2la77qCk8HuKf1UfR5NhDW5xUTolJAgvjOH3" "idaSz6+zpz8w7bXfIa7+9UQX/dhj2S/TgVprX9NHsKzyqzskeU8fxy7quRU6fBhM" "abO1IFkJXinDY+YuRluqlJBJDrnw9UqhCS98NE3QvADFBlV5Bs6i0BDxSEPouVq1" "lVW9MdIbPYa+oewNEtssmSStR8JvA+Z6cLVwzM0nLKWMjsIYPJLJLnNvBhBWk0Cq" "o8VS++XFBdZpaFwGue5RieGKDkFNm5KQConpFmvv73W+eka440eKHRwup08CAwEA" "AaOCASkwggElMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G" "A1UdDgQWBBT473/yzXhnqN5vjySNiPGHAwKz6zAfBgNVHSMEGDAWgBSP8Et/qC5F" "JK5NUPpjmove4t0bvDA+BggrBgEFBQcBAQQyMDAwLgYIKwYBBQUHMAGGImh0dHA6" "Ly9vY3NwMi5nbG9iYWxzaWduLmNvbS9yb290cjMwNgYDVR0fBC8wLTAroCmgJ4Yl" "aHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9yb290LXIzLmNybDBHBgNVHSAEQDA+" "MDwGBFUdIAAwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xvYmFsc2lnbi5j" "b20vcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAJmQyC1fQorUC2bbmANz" "EdSIhlIoU4r7rd/9c446ZwTbw1MUcBQJfMPg+NccmBqixD7b6QDjynCy8SIwIVbb" "0615XoFYC20UgDX1b10d65pHBf9ZjQCxQNqQmJYaumxtf4z1s4DfjGRzNpZ5eWl0" "6r/4ngGPoJVpjemEuunl1Ig423g7mNA2eymw0lIYkN5SQwCuaifIFJ6GlazhgDEw" "fpolu4usBCOmmQDo8dIm7A9+O4orkjgTHY+GzYZSR+Y0fFukAj6KYXwidlNalFMz" "hriSqHKvoflShx8xpfywgVcvzfTO3PYkz6fiNJBonf6q8amaEsybwMbDqKWwIX7e" "SPY=\n" "-----END CERTIFICATE-----\n"; // Machinistへ送信 void send2Machinist(time_t now) { // 現在時刻の表示 struct tm *t = localtime(&now); char date_time[20]; sprintf(date_time, TIME_FORMAT, t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); SERIAL.print(date_time); // 送信するメッセージの作成 String message = "{\"agent_id\": \"" MACHINIST_ID "\","; message += "\"metrics\": ["; message += "{\"name\": \"Analog\","; message += "\"data_point\": {"; message += "\"value\": " + String(analogRead(ANALOG_PIN)) + ","; message += "\"timestamp\": " + String((int)now); message += "}}]}"; // Machinist GWサーバーにメッセージを送信 client.setCACert(root_ca); client.connect("gw.machinist.iij.jp", 443); client.println("POST /endpoint HTTP/1.1"); client.println("Host: gw.machinist.iij.jp"); client.println("Authorization: Bearer " MACHINIST_KEY); client.println("Content-Type: application/json"); client.print("Content-Length: "); client.println(message.length()); client.println(); client.print(message.c_str()); while (!client.available()) delay(10); String response_code = client.readStringUntil('\n'); client.stop(); SERIAL.print(" : "); SERIAL.println(response_code); } // ネットワーク接続 void connectNetwork() { SERIAL.print("Connecting "); SERIAL.print(WIFI_SSID "..."); WiFi.mode(WIFI_STA); WiFi.disconnect(true); WiFi.begin(WIFI_SSID, WIFI_PASS); // WiFiに接続 while (WiFi.status() != WL_CONNECTED) { // WiFiが繋がるまでループ SERIAL.print("."); delay(1000); } SERIAL.println(" done."); SERIAL.print("IP Address: "); SERIAL.println(WiFi.localIP()); } // 時刻の同期 void syncTime() { SERIAL.print("Waiting for NTP time sync..."); // パケットデータ作成 byte packet[48]; packet[0] = 0b11100011; // LI, Version, Mode packet[1] = 0; // Stratum, or type of clock packet[2] = 6; // Polling Interval packet[3] = 0xEC; // Peer Clock Precision packet[12] = 49; packet[13] = 0x4E; packet[14] = 49; packet[15] = 52; // NTPで時刻データを取得 udp.begin(8888); udp.beginPacket("ntp.nict.jp", 123); // 123はNTPのポート番号 udp.write(packet, 48); udp.endPacket(); while (!udp.parsePacket()) delay(100); udp.read(packet, 48); // read the packet into the buffer udp.stop(); // 時刻設定 unsigned long high_word = word(packet[40], packet[41]); unsigned long low_word = word(packet[42], packet[43]); // ntpは1900年が起源でposixは1970年が起源のため70年分の時間を引く time_t posix_time = (high_word << 16 | low_word) - 2208988800UL; struct timeval tv; tv.tv_sec = posix_time; // 秒 tv.tv_usec = 0; // マイクロ秒 settimeofday(&tv, NULL); // 時間の設定 setenv("TZ", "JST-9", 1); // 環境変数TZをJST-9に設定 tzset(); // タイムゾーンの設定 SERIAL.println(" done."); } void setup() { // M5Stackの初期化 #ifdef USE_M5STACK M5.begin(true, true, false, true); // LCD, SD, UART, I2C #endif // Wio Termialの初期化 #ifdef USE_WIO_TERMINAL tft.begin(); tft.setRotation(3); tft.fillScreen(0); // 画面を黒で塗りつぶす #endif SERIAL.setTextFont(2); // 16ピクセルASCIIフォント connectNetwork(); // ネットワーク接続 syncTime(); // 時刻の同期 // その他の初期化 time_t now = time(nullptr); // 現在時刻取得 next_day = now + 24 * 60 * 60; // 1日後の時刻をセット next_time = now; // 今の時刻をセット line_counter = 4; // 繰り返し出力が始まる行数 } void loop() { // 現在時刻取得 time_t now = time(nullptr); // 送信時刻の処理 if (now > next_time) { if (line_counter > 15) { // 画面の表示行数(15)を超えた時 line_counter = 1; SERIAL.setCursor(0, 0); // カーソルを画面左上に戻す SERIAL.fillScreen(0); // 画面を黒で塗りつぶす } send2Machinist(now); // Machinistへ送信 next_time += 60; // 60秒ごと line_counter++; } // 1日1回の処理 if (now > next_day) { syncTime(); // 時刻の同期 next_day = now + 24 * 60 * 60; // 1日後の時刻をセット } }
M5Stack Basic(ESP32)はAPIが豊富でプログラムが楽
先程のサンプルプログラムはM5Stack BasicとWio Terminalのスケッチのサイズ比較のために同じソースコードを使っていましたが、M5Stack Basic(ESP32)には便利なAPIが用意されていて、楽にプログラミングをすることができます。
NTPを使った時間の取得
ESP32ではconfigTzTime()を使うことで、UDPパケットを扱うコードを書く必要がありません。
また、タイマーで定期的に時刻合わせをしてくれるため、サンプルプログラムの1日1回の処理で時刻の同期を呼び出す必要もありません。
// ESP32での時刻の同期 void syncTime() { configTzTime("JST-9", "ntp.nict.jp"); time_t now = time(nullptr); while (now < 60) { // 起動から60秒以上の時間だったら delay(500); // NTPで時間が取得できたとみなす now = time(nullptr); } }
HTTPアクセス
ESP32にはHTTPClient()が用意されていて、WiFiClientSecure()より簡単にHTTPアクセスが行えます。
サンプルグラムは次のように置き換えることができます。
http.begin("gw.machinist.iij.jp", 443, "/endpoint", root_ca); http.addHeader("Host", "gw.machinist.iij.jp"); http.addHeader("Authorization", "Bearer " MACHINIST_KEY); http.addHeader("Content-Type", "application/json"); int status_code = http.POST(message.c_str()); http.end();
Wio Terminalは購入後にファームウェアの書き換えを行う必要があって面倒
ちょっとプログラムが面倒になることはあっても、まぁまぁM5Stack Basicと同じように使えるかなっと思えるWio Terminalですが、まだ、面倒なことがあります。
出荷時のファームウェアではWiFiとBluetoothが同時に使えないんです。
次のリンクの記事を参考にして、AT-CommandベースのファームウェアからeRPCベースのファームウェアにアップデートをしましょう。
ぼくがMacで行ったときは、sudoを付けてpythonスクリプト実行しないと正しく動作しなかったので注意してくださいね。