M5Stack+3G通信モジュール+NTP時刻合わせ

# はじめに
M5Stack+3G通信モジュールでNTPの時刻合わせでできずに困っていましたが、なんとか解決できたので公開します。

M5StackはWiFi環境ならば、configTime()でNTPサーバを指定して時刻合わせが可能です。
しかし、3G環境でconfigTime()を呼び出すと不正終了してしまいます。調べてみるとconfigTime()はWiFiUDPを使用しているためにWiFi環境が前提のようです。
やっぱり、M5Stackで各種センサを計測したデータには現在時刻を付けたくて調べていると、日本標準時グループへhttp/httpsで通信してJSON形式で現在時刻を返してくれます。これを利用して時刻合わせすることができました。

# 開発環境
- M5Stack Base
- [M5Stack用 3G 拡張ボード(スイッチサイエンス)](https://www.switch-science.com/catalog/4000/)
- [SORACOM 特定地域向け IoT SIM plan-D](https://soracom.jp/products/sim/#specificsim)
- [NTPサーバ(日本標準時グループ)](https://jjy.nict.go.jp/tsp/PubNtp/index.html)
- [TinyGSM(3G通信ライブラリ)](https://github.com/vshymanskyy/TinyGSM)

# NTPサーバ
日本標準時グループからJSON形式で現在時刻を取得します。
以下のようなJSONデータが取得できるので、サーバ時刻("st")を使って時刻を合わせます。

```json
{
  "id": "ntp-a1.nict.go.jp",
  "it": 0.000,
  "st": 1586670266.815,
  "leap": 36,
  "next": 1483228800,
  "step": 1
}
```

詳細は、[https/http を介してアクセスされる場合](http://www.nict.go.jp/JST/http.html)を参照してください。

# スケッチ
- https(ntp-a1.nict.go.jp:443)通信を前提としています。
- SORACOMのs1.slow(128kbps)で試しています。
- NTPサーバへリクエスト送信からレスポンス受信までの通信時間を計測します。
- レスポンスのJSONから現在時刻をに取り出して通信時間を加算します。
- settimeofday()でタイムゾーンを指定してM5Stackの時刻を設定します。
- 時刻合わせ後は、1分周期で現在時刻をログに表示します。

## 3G通信で時刻合わせ
```c
#include <time.h>
#include <M5Stack.h>
#include <WiFi.h>

#define TINY_GSM_MODEM_UBLOX
#include <TinyGsmClient.h>

// 3Gモデム定義
TinyGsm modem(Serial2);             // 3Gモデム
TinyGsmClientSecure client(modem);  // クライアント(HTTPS)

// ローカル時間(NTP)
const char* ntp_host = "ntp-a1.nict.go.jp";  // 日本標準時グループ
const char* ntp_url = "cgi-bin/json";
const String NTP_JSON_ST = "\"st\":";       // JSON内の現在時刻
#define TZ_JST "JST-9"

// 初期設定
void setup(){
  // M5Stack初期化
  M5.begin();
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(WHITE);

  // シリアルモニタ初期化
  Serial.begin(115200);
  delay(1000);

  // 3Gモデム設定
  setup_3g_modem();

  // NTPサーバからローカル時間設定(3G版)
  setup_ntp_time();
}

void loop(){
  // ローカル時間表示
  printLocalTime();

  // 1分wait
  delay(1000 * 60 * 1);
}

// 3Gモデム設定
void setup_3g_modem()
{
  M5.Lcd.println(F("M5Stack + 3G Module"));

  M5.Lcd.print(F("modem.restart()"));
  Serial2.begin(115200, SERIAL_8N1, 16, 17);
  modem.restart();
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("getModemInfo:"));
  String modemInfo = modem.getModemInfo();
  M5.Lcd.println(modemInfo);

  M5.Lcd.print(F("waitForNetwork()"));
  while (!modem.waitForNetwork()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("gprsConnect(soracom.io)"));
  modem.gprsConnect("soracom.io", "sora", "sora");
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("isNetworkConnected()"));
  while (!modem.isNetworkConnected()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("My IP addr: "));
  IPAddress ipaddr = modem.localIP();
  M5.Lcd.print(ipaddr);
  delay(2000);
}

// NTPサーバからローカル時間設定(3G版)
bool setup_ntp_time()
{
  bool result = false;
  Serial.println("setup localtime from NTP server.");

  // NTPサーバ接続
  if (!client.connect(ntp_host, 443)) {
    Serial.println(F("Connect failed."));
    return false;
  }

  // URL生成
  String req_url;
  req_url = "GET /" + String(ntp_url) + " HTTP/1.1\r\n";

  // ヘッダー生成
  String req_header;
  req_header = "Host: " + String(ntp_host) + "\r\n";
  req_header += "\r\n"; //空行

  // リクエスト送信
  client.print(req_url);
  client.print(req_header);

  // 通信開始時刻取得
  time_t timeStart = millis();

  // ログ表示
  Serial.print(req_url);
  Serial.print(req_header);
  Serial.println("");

  // レスポンス受信(ヘッダー)
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      Serial.println("headers received.");
      break;
    }
  }

  // レスポンス受信(データ)
  Serial.println("----- body -----");
  String response = client.readString();
  int bodypos =  response.indexOf("\r\n");
  String body = response.substring(bodypos + 2);
  body.trim();

  // 受信停止
  client.stop();
  M5.Lcd.println(body);
  Serial.println(body);

  // 通信終了時刻取得
  time_t timeEnd = millis();

  // JSON形式のNTP時刻のST部を抽出
  // 必要箇所はピンポイントなので簡易的な検索で良いと判断
  int posNtpSt = body.indexOf(NTP_JSON_ST);
  if (posNtpSt >= 0) {
    // NTP時刻のST部を抽出
    String temp = body.substring(posNtpSt + NTP_JSON_ST.length());
    temp = temp.substring(0, temp.indexOf(","));
    temp.trim();
    double ntpTime = temp.toDouble();
    Serial.println(String(ntpTime));

    // 通信時間を取得時間に加算
    time_t diff = timeEnd - timeStart;
    ntpTime += (double)(diff) / 1000.0;
    Serial.println(String(diff));
    
    // timeval変数へ設定
    struct timeval tv;
    tv.tv_sec = (long)ntpTime;
    tv.tv_usec = (long)((ntpTime - tv.tv_sec) * 1000000);
    // ローカル時間設定
    set_local_time(tv, TZ_JST);
  }

  Serial.println("completed.");
}

// ローカル時間設定
void set_local_time(struct timeval tv, char *time_zone)
{
  struct timezone tz;
  setenv("TZ", time_zone, 1);
  tzset();
  tz.tz_minuteswest = 0;
  tz.tz_dsttime = 0;
  settimeofday(&tv, &tz);
}

// ローカル時間表示
void printLocalTime()
{
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%Y %m %d %a %H:%M:%S");
  // 現在時刻取得
  struct timeval tv;
  gettimeofday(&tv, NULL);
  Serial.print("tv.tv_sec : "); Serial.println(String(tv.tv_sec));  
  Serial.print("tv.tv_usec: "); Serial.println(String(tv.tv_usec));  
}
```

## httpで通信する場合
- httpで通信する場合は、以下のように書き換えてください。

```c
// 3Gモデム定義
TinyGsm modem(Serial2);       // 3Gモデム
TinyGsmClient client(modem);  // クライアント(HTTP)

:

// NTPサーバからローカル時間設定(3G版)
bool setup_ntp_time()
{
  :
  // NTPサーバ接続
  if (!client.connect(ntp_host, 80)) {
    Serial.println(F("Connect failed."));
    return false;
  }
  :
}
```

# 実行ログ
- ログの左側の時刻はPC時刻です。  
タイムスタンプを表示のチェックをONにしています。
- NTPサーバの現在時刻(UNIX時間)です。  
14:44:28.543 ->  "st": 1586670266.815  
UNIX時間 -> 日本時間(JST)へ変換すると、"2020/04/12 14:44:26"です。
- NTPサーバへリクエスト送信からレスポンス受信までの通信時間(ミリ秒)です。  
14:44:28.543 -> 1615
- サーバの現在時刻へ通信時刻を加算して時刻合わせした結果です。  
14:44:28.543 -> 2020 04 12 Sun 14:44:28  
14:44:28.543 -> tv.tv_sec : 1586670268  
14:44:28.543 -> tv.tv_usec: 433041

```text
14:44:25.337 -> setup localtime from NTP server.
14:44:26.921 -> GET /cgi-bin/json HTTP/1.1
14:44:26.921 -> Host: ntp-a1.nict.go.jp
14:44:26.921 -> 
14:44:26.921 -> 
14:44:26.993 -> HTTP/1.1 200 OK
14:44:26.993 -> Date: Sun, 12 Apr 2020 05:44:26 GMT
14:44:26.993 -> Server: Apache
14:44:26.993 -> Access-Control-Allow-Origin: *
14:44:27.027 -> Cache-Control: no-cache, no-store
14:44:27.027 -> Strict-Transport-Security: max-age=0
14:44:27.027 -> X-Frame-Options: SAMEORIGIN
14:44:27.027 -> Connection: close
14:44:27.027 -> Transfer-Encoding: chunked
14:44:27.065 -> Content-Type: application/json
14:44:27.065 -> 
14:44:27.065 -> headers received.
14:44:27.065 -> ----- body -----
14:44:28.543 -> {
14:44:28.543 ->  "id": "ntp-a1.nict.go.jp",
14:44:28.543 ->  "it": 0.000,
14:44:28.543 ->  "st": 1586670266.815,
14:44:28.543 ->  "leap": 36,
14:44:28.543 ->  "next": 1483228800,
14:44:28.543 ->  "step": 1
14:44:28.543 -> }
14:44:28.543 -> 
14:44:28.543 -> 0
14:44:28.543 -> 1586670266.82
14:44:28.543 -> 1615
14:44:28.543 -> completed.
14:44:28.543 -> 2020 04 12 Sun 14:44:28
14:44:28.543 -> tv.tv_sec : 1586670268
14:44:28.543 -> tv.tv_usec: 433041
14:45:28.539 -> 2020 04 12 Sun 14:45:28
14:45:28.539 -> tv.tv_sec : 1586670328
14:45:28.539 -> tv.tv_usec: 436831
14:46:28.541 -> 2020 04 12 Sun 14:46:28
14:46:28.541 -> tv.tv_sec : 1586670388
14:46:28.541 -> tv.tv_usec: 436831
```

# 参考サイト
- [プロトタイプ向けマイコンモジュール M5Stack と 3G 拡張ボードをセットアップする](https://dev.soracom.io/jp/start/m5stack/)
- [ESP32で時間取得](https://kerikeri.top/posts/2017-04-26-esp32-time/)
- [日本標準時グループ](https://jjy.nict.go.jp/tsp/PubNtp/index.html)
- [時刻変換ツール](http://kujiira.com/tools/date)

# おわりに
3G環境でもconfgiTime()で時刻合わせができると思っていたので、不正終了したときには悩みました。
各種センサで計測したデータを定刻に送信するときには必ず時刻合わせが必要なので解決できて良かったです。