M5Stack+SIM7080GでSORACOM NTPサーバーから時刻を取得する方法
M5Stack系マイコンにCAT-M Unit(SIM7080G搭載)を接続し、SORACOMのNTPサーバーから時刻を取得する方法をまとめます。
1. 背景と目的
IoTデバイス開発において、正確な時刻を扱いたい場合、NTP(Network Time Protocol)サーバーから時刻を取得すると便利です。
今回、M5stackとCAT-M Unit(SIM7080G搭載)の構成で、NTPから時刻を取得したいと思ったのですが、該当する記事が案外少なく、少々苦労したのでこの記事にまとめます。
NTPサーバーとしてSORACOMのNTPサーバーがあるようなので、これを用います。
最後にコード全文を示します。
2. NTPサーバーから時刻を取得する
SIM7080GのATコマンドマニュアルを見ると、NTPサーバーから時刻を取得する場合、下記コマンドで指定できるようです。
AT+CNTP=<ntpserver>[,<timezone>][,<cid>][,<mode>]
<ntpserver>は、SORACOMのNTPサーバーを指定します。
ntp.soracom.io
ハマったのは<timezone>です。JST(日本時刻)は協定世界時+9時間であるため、め、9を指定したらいいと思っていたのですが、9を指定すると協定世界時+2時間15分となってしまいます。ATコマンドマニュアルを見ると、30分や15分単位に対応するため4倍したタイムゾーンが使用されているようです。こんなタイムゾーンがあるんですね。知りませんでした。ということで、9ではなく9x4=36を指定する必要があります。
<cid> は0、<mode>は2で良さそうです。
結果的に、下記コードで時刻を取得できました。
// NTP時刻の設定
modem.sendAT(GF("+CNTP=\"ntp.soracom.io\",36,0,2"));
modem.waitResponse();
modem.sendAT(GF("+CCLK?")); // RTCの時刻を取得
String ntpTime = ""; // NTP時刻を格納する変数
if (modem.waitResponse(10000L, ntpTime) == 1) {
// 取得した時刻情報をSerial.printで表示
Serial.println(ntpTime);
} else {
Serial.println("NTP時刻の取得に失敗しました");
}
3. 取得した時刻を変数に代入する
modem.sendAT(GF("+CCLK?"));は時刻を取得するコマンドです。その結果、例えば下記が返ってきます。
+CCLK: "24/09/01,21:31:19+36"
マイコンで時刻を取り扱うため、取得した時刻の文字列を変数に入れたいのですが、入れ方がわからず。。ChatGPTとやり取りしながら、下記で変数に入れることができました。
// 時刻情報の開始位置を検索
int start = ntpTime.indexOf("\"");
if (start != -1) {
// 必要な部分を抽出
// 例)+CCLK: "24/09/01,21:31:19+36"
String timeStr = ntpTime.substring(start + 1, start + 20); // 先頭の " をスキップし、19桁分を抽出
Serial.print("NTP time: ");
Serial.println(timeStr);
// tm構造体に代入
now.tm_year = timeStr.substring(0, 2).toInt() + 2000 - 1900; // "24" -> 2024年
now.tm_mon = timeStr.substring(3, 5).toInt() - 1; // 月 (0から始まる)
now.tm_mday = timeStr.substring(6, 8).toInt(); // 日
now.tm_hour = timeStr.substring(9, 11).toInt(); // 時
now.tm_min = timeStr.substring(12, 14).toInt(); // 分
now.tm_sec = timeStr.substring(15, 17).toInt(); // 秒
return true; // 成功を返す
} else {
Serial.println("時刻情報の解析に失敗しました");
return false; // 解析失敗を返す
}
4. コード全文
最後に、コード全文を示します。
M5Stack Tough、M5Stack Fire、M5Stack Coreに対応したコードです。
M5Stackセットアップ
TinyGSMライブラリを用いてSORACOMに接続
NTPサーバーから時刻取得
取得した時刻を変数に代入
という流れです。
#if defined(ARDUINO_M5STACK_TOUGH) // Toughの場合
#include <M5Tough.h>
#else
#include <M5Stack.h>
#endif
#define TINY_GSM_MODEM_SIM7080
#include <TinyGsmClient.h>
#include <time.h>
#define SerialAT Serial1
#define ENDPOINT "uni.soracom.io"
TinyGsm modem(SerialAT);
TinyGsmClient client(modem);
struct tm now;
void setup() {
#if defined(ARDUINO_M5STACK_TOUGH)
M5.begin(true, true, true, true);
#else
M5.begin();
#endif
M5.Lcd.fillScreen(BLACK);
// Set GSM module baud rate
#if defined(ARDUINO_M5STACK_TOUGH)
SerialAT.begin(115200, SERIAL_8N1, 13, 14); //M5Tough
#else
SerialAT.begin(115200, SERIAL_8N1, 16, 17); //M5Stack,Fire
#endif
delay(1000);
/*
Serial.print(F("modem.restart()"));
modem.restart();
Serial.println(F(" done."));
*/
Serial.print(F("modem.init()"));
modem.init();
Serial.println(F(" done."));
Serial.print(F("modem.getModemInfo(): "));
String modemInfo = modem.getModemInfo();
Serial.println(modemInfo);
Serial.print(F("waitForNetwork()"));
while (!modem.waitForNetwork()) Serial.print(".");
Serial.println(F(" Ok."));
Serial.print(F("gprsConnect(soracom.io)"));
modem.gprsConnect("soracom.io", "sora", "sora");
Serial.println(F(" done."));
Serial.print(F("isNetworkConnected()"));
while (!modem.isNetworkConnected()) Serial.print(".");
Serial.println(F(" Ok."));
Serial.print(F("My IP addr: "));
IPAddress ipaddr = modem.localIP();
Serial.println(ipaddr);
if (getNtpTime(modem, now)) {
// 成功時の処理
Serial.println("NTP時刻の取得に成功しました:");
Serial.print("Year: ");
Serial.println(now.tm_year + 1900);
Serial.print("Month: ");
Serial.println(now.tm_mon + 1);
Serial.print("Day: ");
Serial.println(now.tm_mday);
Serial.print("Hour: ");
Serial.println(now.tm_hour);
Serial.print("Minute: ");
Serial.println(now.tm_min);
Serial.print("Second: ");
Serial.println(now.tm_sec);
} else {
// 失敗時の処理
Serial.println("NTP時刻の取得に失敗しました");
}
}
void loop() {
// ループ内では何も行わない
}
// NTP時刻を取得し、struct tmに格納する関数
bool getNtpTime(TinyGsm &modem, struct tm &now) {
// NTP時刻の設定(JSTを取得)
modem.sendAT(GF("+CNTP=\"ntp.soracom.io\",36,0,2"));
modem.waitResponse();
// RTCの時刻を取得
modem.sendAT(GF("+CCLK?"));
String ntpTime = ""; // NTP時刻を格納する変数
if (modem.waitResponse(10000L, ntpTime) == 1) {
// 取得した時刻情報をSerial.printで表示
Serial.println(ntpTime);
M5.Lcd.setTextSize(3);
M5.Lcd.println(ntpTime);
// 時刻情報の開始位置を検索
int start = ntpTime.indexOf("\"");
if (start != -1) {
// 必要な部分を抽出
// 例)+CCLK: "24/09/01,21:31:19+36"
String timeStr = ntpTime.substring(start + 1, start + 20); // 先頭の " をスキップし、19桁分を抽出
Serial.print("NTP time: ");
Serial.println(timeStr);
// tm構造体に代入
now.tm_year = timeStr.substring(0, 2).toInt() + 2000 - 1900; // "24" -> 2024年
now.tm_mon = timeStr.substring(3, 5).toInt() - 1; // 月 (0から始まる)
now.tm_mday = timeStr.substring(6, 8).toInt(); // 日
now.tm_hour = timeStr.substring(9, 11).toInt(); // 時
now.tm_min = timeStr.substring(12, 14).toInt(); // 分
now.tm_sec = timeStr.substring(15, 17).toInt(); // 秒
return true; // 成功を返す
} else {
Serial.println("時刻情報の解析に失敗しました");
return false; // 解析失敗を返す
}
} else {
Serial.println("NTP時刻の取得に失敗しました");
return false; // 取得失敗を返す
}
}
5. まとめと今後の課題
ATコマンドの<timezone>の考え方を理解するまでに苦戦しましたが、何とかNTPサーバーから時刻を取得し、変数代入することができました。
取得した時刻を、RTC UnitなどのRTCに時刻セットすれば正確な時刻を取り扱うことができますし、任意時刻までSleepさせるなどの使い方もできそうです。
ご参考になれば幸いです。
参考文献
・SIM7080GのATコマンドマニュアル
・SORACOMのNTPサーバー