見出し画像

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サーバーがあるようなので、これを用います。

最後にコード全文を示します。

M5Stack ToughのPortCにCAT-M Unit(SIM7080G搭載)を接続し、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を指定する必要があります。

Local time zone, the range is (-47 to 48), in fact, time zone range (-12 to 12), but taking into account that some countries and regions will use half time zone, or even fourth time zone, so the entire extended four time zones X, so that when the time zone of the input integers are used, without the need for decimal. Time zone in front of the West if it is a negative number indicates the time zone.
---以下、機械翻訳---
現地のタイムゾーンの範囲は(-47から48)です。実際のタイムゾーンの範囲は(-12から12)ですが、一部の国や地域では30分単位、さらには15分単位のタイムゾーンを使用するため、全体で4倍に拡張されたタイムゾーンが存在します。このため、タイムゾーンを整数として入力する際には小数点を必要としません。負の数は西側のタイムゾーンを示します。

SIM7070_SIM7080_SIM7090 Series_AT Command Manual p.309

<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時刻の取得に失敗しました");
  }
SIM7070_SIM7080_SIM7090 Series_AT Command Manual p.309

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コマンドマニュアル

https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/sim7080g/en/SIM7070_SIM7080_SIM7090%20Series_AT%20Command%20Manual_V1.04.pdf

・SORACOMのNTPサーバー


この記事が気に入ったらサポートをしてみませんか?