InfluxDB Cloud にLTEモデムで送信する

### はじめに
 influxDBはデータ可視化ツールGrafanaとよく使われIoTセンサーの時系列データベースとしてよく使われています。
 https://cloud2.influxdata.com/
 
 クラウドサービスとしてのinfluxDB CloudにはESP32などからwifiでデータを送信できるように
 [InfluxDB Client for Arduino library](https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino)が用意されています
 ですが野外のセンサーなどwifiの無いところではこのライブラリは使えません。
 
 そこでAPIの仕様を見ながら簡単なサンプルを
 ・ spresense+LTE拡張ボード
 ・ ESP32（M5stickC)+[CatM UNIT(SIM7080G)](https://shop.m5stack.com/products/sim7080g-cat-m-nb-iot-unit)
 ・[IoT スターターキット for Arduino](https://soracom.jp/store/5243/)
 で書いてみました
 [https://docs.influxdata.com/influxdb/cloud/api/#operation/PostWrite](https://docs.influxdata.com/influxdb/cloud/api/#operation/PostWrite)

 ## 証明書取得
 
 まずTLS送信のために証明書をダウンロードします
 今回は上のライブラリ内にあるヘッダーファイルをそのまま使います
 
 https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino/blob/master/src/InfluxDbCloud.h

 ダウンロードして以下のスケッチと同じフォルダに入れておきます

 ## InfluxDB 設定情報
 ・host
 influxDB Cloudにログインした状態でのURLのホスト名
 例：us-east-1-1.aws.cloud2.influxdata.com
 
 ・ bucketsを作る
 Data (Load Data) > BUCKETS　>　 CREATE BUCKET 
https://docs.influxdata.com/influxdb/v2/admin/buckets/create-bucket/
 例：ltesensor

・APIトークンを取得
 Data (Load Data) > API Tokens　>　 GENERATE API TOKEN
 https://docs.influxdata.com/influxdb/v2/admin/tokens/create-token/
 
・　ラインプロトコル
送信データは以下のようにプロトコルが決められてます
https://docs.influxdata.com/influxdb/cloud/reference/syntax/line-protocol/

例: 
**sensor,device=SPRESENSE count=4u,temperature=25.0,humidity=80i** 

 **Measurement**（テーブルのようなもの）に **Point** （レコードのようなもの）をテキスト1行で追加します
 
 **tag** は省略可で検索時に高速に抽出されます
 
 **field** の値はなにもないとfloat型　後ろに"i"を付けるとint64型 ,"u"はuint64型,ダブルクオーテーションで囲うとString型になります
空白の位置にも意味があるのでよく確認しておきます

 **timestamp**　は送信時に省略するとクラウド側で自動的に付与されます（デフォルトではナノ秒のUNIX　time) 今回のサンプルでは略

参考　https://www.mikan-tech.net/entry/what-is-influxdb

https://qiita.com/NWLab/items/736ffe812757edc187f1

## サンプルスケッチ
### spresense +  LTE拡張ボード
・ライブラリ
ArduinoHttpClient
arduino-libraries/ArduinoHttpClient@^0.5.0
https://github.com/arduino-libraries/ArduinoHttpClient

 ``` c++:influxdbHttpPostWrite.ino
#include <Arduino.h>
// libraries
#include <ArduinoHttpClient.h>
#include <RTC.h>
#include <LTE.h>
#include "InfluxDbCloud.h"

// LTE setting
#define APP_LTE_APN "povo.jp" // replace your APN
#define APP_LTE_USER_NAME "" // replace with your username
#define APP_LTE_PASSWORD  "" // replace with your password
#define APP_LTE_IP_TYPE (LTE_NET_IPTYPE_V4V6) // IP : IPv4v6
#define APP_LTE_AUTH_TYPE (LTE_NET_AUTHTYPE_CHAP) // Authentication : CHAP
#define APP_LTE_RAT (LTE_NET_RAT_CATM) // RAT : LTE-M (LTE Cat-M1)

// InfluxDB setting
// E.g. http://192.168.1.48:8086 (In InfluxDB 2 UI -> Load Data -> Client Libraries), 
#define INFLUXDB_URL "*********.aws.cloud2.influxdata.com"
// InfluxDB 2 server or cloud API authentication token (Use: InfluxDB UI -> Load Data -> Tokens -> <select token>)
#define INFLUXDB_TOKEN "***YOUR TOKEN****"

// InfluxDB 2 bucket name (Use: InfluxDB UI -> Load Data -> Buckets)
#define INFLUXDB_BUCKET "ltesensor"

char postWritePath[] = "/api/v2/write?bucket=" INFLUXDB_BUCKET;
int port = 443; // port 443 is the default for HTTPS

// initialize the library instance
LTE lteAccess;
LTETLSClient tlsClient;
HttpClient client = HttpClient(tlsClient, INFLUXDB_URL, port);
uint count = 0;

void printClock(RtcTime &rtc)
{
  printf("%04d/%02d/%02d %02d:%02d:%02d\n",
         rtc.year(), rtc.month(), rtc.day(),
         rtc.hour(), rtc.minute(), rtc.second());
}

void setup()
{
  char apn[LTE_NET_APN_MAXLEN] = APP_LTE_APN;
  LTENetworkAuthType authtype = APP_LTE_AUTH_TYPE;
  char user_name[LTE_NET_USER_MAXLEN] = APP_LTE_USER_NAME;
  char password[LTE_NET_PASSWORD_MAXLEN] = APP_LTE_PASSWORD;

  // initialize serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
      ; // wait for serial port to connect. Needed for native USB port only
  }

  Serial.println("Starting secure HTTP client.");
  Serial.println("=========== APN information ===========");
  Serial.print("Access Point Name  : ");
  Serial.println(apn);
  Serial.print("Authentication Type: ");
  Serial.println(authtype == LTE_NET_AUTHTYPE_CHAP ? "CHAP" :
                 authtype == LTE_NET_AUTHTYPE_NONE ? "NONE" : "PAP");
  if (authtype != LTE_NET_AUTHTYPE_NONE) {
    Serial.print("User Name          : ");
    Serial.println(user_name);
    Serial.print("Password           : ");
    Serial.println(password);
  }

  while (true) {

    /* Power on the modem and Enable the radio function. */

    if (lteAccess.begin() != LTE_SEARCHING) {
      Serial.println("Could not transition to LTE_SEARCHING.");
      Serial.println("Please check the status of the LTE board.");
      for (;;) {
        sleep(1);
      }
    }

    /* The connection process to the APN will start.
     * If the synchronous parameter is false,
     * the return value will be returned when the connection process is started.
     */
    if (lteAccess.attach(APP_LTE_RAT,
                         apn,
                         user_name,
                         password,
                         authtype,
                         APP_LTE_IP_TYPE) == LTE_READY) {
      Serial.println("attach succeeded.");
      break;
    }

    /* If the following logs occur frequently, one of the following might be a cause:
     * - APN settings are incorrect
     * - SIM is not inserted correctly
     * - If you have specified LTE_NET_RAT_NBIOT for APP_LTE_RAT,
     *   your LTE board may not support it.
     * - Rejected from LTE network
     */
    Serial.println("An error has occurred. Shutdown and retry the network attach process after 1 second.");
    lteAccess.shutdown();
    sleep(1);
  }

  // Set local time (not UTC) obtained from the network to RTC.
  RTC.begin();
  unsigned long currentTime;
  while(0 == (currentTime = lteAccess.getTime())) {
    sleep(1);
  }
  RtcTime rtc(currentTime);
  printClock(rtc);
  RTC.setTime(rtc);

  tlsClient.setCACert(InfluxDbCloud2CACert);
}

void loop()
{
  // HTTP POST method
  Serial.println("making POST request");
  String measurement = "sensor";
  String tagSet = "device=SPRESENSE";
  String fieldSet ;
  float temp = 25.0;
  int humi = 80;
  float press = 1013.00;
  
  String contentType = "text/plain; charset=utf-8";
  fieldSet = "count=" + String(count) + "u";
  fieldSet += ",temperature=" + String(temp,1);
  fieldSet += ",humidity=" + String(humi) + "i";
  fieldSet += ",pressure=" + String(press,2);
  String postData = measurement + "," + tagSet + " " + fieldSet ;
  Serial.println(postData);
  client.beginRequest();
  client.post(postWritePath);
  client.sendHeader("Authorization: Token "INFLUXDB_TOKEN);
  client.sendHeader(HTTP_HEADER_CONTENT_TYPE, contentType);
  client.sendHeader(HTTP_HEADER_CONTENT_LENGTH,postData.length());
  client.endRequest();

  client.write((const byte*)postData.c_str(), postData.length());

  int statusCode = client.responseStatusCode();
  String response = client.responseBody();

  Serial.print("Status code: ");
  Serial.println(statusCode);
  Serial.print("Response: ");
  Serial.println(response);

  count ++;
  sleep(60);
}
 ```

 参考:SPRESENSE LTEチュートリアル
 [TLSプロトコルを使用したHTTP Clientの動作を確認する](https://developer.sony.com/spresense/development-guides/arduino_tutorials_ja.html#_tutorial_LteHttpSecureClient)
 
 https://github.com/sonydevworld/spresense-arduino-compatible/blob/master/Arduino15/packages/SPRESENSE/hardware/spresense/1.0.0/libraries/LTE/examples/LteHttpSecureClient/LteHttpSecureClient.ino


 ### M5stickC + M5Stack CAT-M UNIT（SIM7080G搭載）
・ライブラリ
- ArduinoHttpClient
arduino-libraries/ArduinoHttpClient@^0.5.0
https://github.com/arduino-libraries/ArduinoHttpClient

- TinyGSM
vshymanskyy/TinyGSM@^0.11.7
https://github.com/vshymanskyy/TinyGSM

 ``` c++:influxdbHttpPostWrite.ino
#include <Arduino.h>
// libraries
#define TINY_GSM_MODEM_SIM7080
#include <TinyGsmClient.h>
#include <ArduinoHttpClient.h>
#include "InfluxDbCloud.h"
#define SerialAT Serial1

// LTE setting
#define APP_LTE_APN "povo.jp" // replace your APN
#define APP_LTE_USER_NAME "" // replace with your username
#define APP_LTE_PASSWORD  "" // replace with your password

// InfluxDB setting
// E.g. http://192.168.1.48:8086 (In InfluxDB 2 UI -> Load Data -> Client Libraries), 
#define INFLUXDB_URL "*********.aws.cloud2.influxdata.com"
// InfluxDB 2 server or cloud API authentication token (Use: InfluxDB UI -> Load Data -> Tokens -> <select token>)
#define INFLUXDB_TOKEN "***YOUR TOKEN****"

// InfluxDB 2 bucket name (Use: InfluxDB UI -> Load Data -> Buckets)
#define INFLUXDB_BUCKET "ltesensor"

char postWritePath[] = "/api/v2/write?bucket=" INFLUXDB_BUCKET;
int port = 443; // port 443 is the default for HTTPS

// initialize the library instance
TinyGsm        modem(SerialAT);
TinyGsmClientSecure tlsClient(modem);
HttpClient client = HttpClient(tlsClient, INFLUXDB_URL, port);

uint count = 0;

void writeCaFiles(int index, const char *filename, const char *data,
                  size_t lenght)
{
    modem.sendAT("+CFSTERM");
    modem.waitResponse();


    modem.sendAT("+CFSINIT");
    if (modem.waitResponse() != 1) {
        Serial.println("INITFS FAILED");
        return;
    }
    // AT+CFSWFILE=<index>,<filename>,<mode>,<filesize>,<input time>
    // <index>
    //      Directory of AP filesystem:
    //      0 "/custapp/" 1 "/fota/" 2 "/datatx/" 3 "/customer/"
    // <mode>
    //      0 If the file already existed, write the data at the beginning of the
    //      file. 1 If the file already existed, add the data at the end o
    // <file size>
    //      File size should be less than 10240 bytes. <input time> Millisecond,
    //      should send file during this period or you can’t send file when
    //      timeout. The value should be less
    // <input time> Millisecond, should send file during this period or you can’t
    // send file when timeout. The value should be less than 10000 ms.

    size_t payloadLenght = lenght;
    size_t totalSize     = payloadLenght;
    size_t alardyWrite   = 0;

    while (totalSize > 0) {
        size_t writeSize = totalSize > 10000 ? 10000 : totalSize;

        modem.sendAT("+CFSWFILE=", index, ",", "\"", filename, "\"", ",",
                     !(totalSize == payloadLenght), ",", writeSize, ",", 10000);
        modem.waitResponse(30000UL, "DOWNLOAD");
REWRITE:
        modem.stream.write(data + alardyWrite, writeSize);
        if (modem.waitResponse(30000UL) == 1) {
            alardyWrite += writeSize;
            totalSize -= writeSize;
            Serial.printf("Writing:%d overage:%d\n", writeSize, totalSize);
        } else {
            Serial.println("Write failed!");
            delay(1000);
            goto REWRITE;
        }
    }

    Serial.println("Wirte done!!!");

    modem.sendAT("+CFSTERM");
    if (modem.waitResponse() != 1) {
        Serial.println("CFSTERM FAILED");
        return;
    }
}

void setup()
{
  // Set GSM module baud rate
  Serial.begin(115200);
  SerialAT.begin(115200, SERIAL_8N1, 33, 32);//M5StickC
  delay(1000);
  Serial.println("start");

  // Restart takes quite some time
  // To skip it, call init() instead of restart()
  Serial.println("Initializing modem...");
  // modem.restart();
  modem.init();

  String modemInfo = modem.getModemInfo();
  Serial.print("Modem Info: ");
  Serial.println(modemInfo);
  modem.gprsConnect(APP_LTE_APN, APP_LTE_USER_NAME, APP_LTE_PASSWORD );
  Serial.print("waitForNetwork()");
  while (!modem.waitForNetwork()) Serial.print(".");
  Serial.println(" Ok.");

  Serial.print("gprsConnect");
  modem.gprsConnect(APP_LTE_APN, APP_LTE_USER_NAME, APP_LTE_PASSWORD );
  Serial.println(" done.");

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

  Serial.print("My IP addr: ");
  IPAddress ipaddr = modem.localIP();
  Serial.println(ipaddr);
  //Set CACert  
  writeCaFiles(3, "root.crt", InfluxDbCloud2CACert, strlen(InfluxDbCloud2CACert));
  modem.sendAT("+CSSLCFG=\"SSLVERSION\",0,3");
  modem.waitResponse();
  modem.sendAT("+CSSLCFG=\"CONVERT\",2,\"root.crt\"");
  if (modem.waitResponse() != 1) {
      Serial.println("Convert root.crt failed!");
  }
}

void loop()
{
  // HTTP POST method
  Serial.println("making POST request");
  String measurement = "sensor";
  String tagSet = "device=SIM7080G";
  String fieldSet ;
  float temp = 25.0;
  int humi = 80;
  float press = 1013.00;
  
  String contentType = "text/plain; charset=utf-8";
  fieldSet = "count=" + String(count) + "u";
  fieldSet += ",temperature=" + String(temp,1);
  fieldSet += ",humidity=" + String(humi) + "i";
  fieldSet += ",pressure=" + String(press,2);
  String postData = measurement + "," + tagSet + " " + fieldSet ;
  Serial.println(postData);
  
  client.beginRequest();
  client.post(postWritePath);
  client.sendHeader("Authorization: Token "INFLUXDB_TOKEN);
  client.sendHeader(HTTP_HEADER_CONTENT_TYPE, contentType);
  client.sendHeader(HTTP_HEADER_CONTENT_LENGTH,postData.length());
  client.endRequest();

  client.write((const byte*)postData.c_str(), postData.length());

  int statusCode = client.responseStatusCode();
  String response = client.responseBody();

  Serial.print("Status code: ");
  Serial.println(statusCode);
  Serial.print("Response: ");
  Serial.println(response);

  count ++;
  sleep(60);
}
 ```

参考:LilyGo-T-SIM7080G　ModemMqttsExample

https://github.com/Xinyuan-LilyGO/LilyGo-T-SIM7080G/blob/master/examples/ModemMqttsExample/ModemMqttsExample.ino

 ## Response
  **204** が返ってくれば書き込み成功です
 
## SORACOM BEAM を利用する
上記はモデム自身でHTTPSの認証をやっていましたが、soracom　のデータ転送支援サービス **BEAM**(https://soracom.jp/services/beam/) 　を使えばモデム側はHTTP（TCP,UDP）で送信したものをHTTPSに変換して転送します。センサー端末側の通信コストや省電力に役立ちます

 ### BEAM設定

 SIMグループ　-　基本設定　-　SORACOM BEAM設定　- HTTPエントリポイントを以下のように設定します
 ホスト名、bucket名、Tokenは適宜入れ替えて
![スクリーンショット 2023-11-10 203434.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/b2734d63-949b-f4a5-1de0-7083f63a30cf.png)
![スクリーンショット 2023-11-10 203254.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/76e9b7f8-43e8-5937-6aa6-3f6564ffe40b.png)

https://users.soracom.io/ja-jp/docs/beam/http/

 ### スケッチ
[IoT スターターキット for Arduino（Quectel BG96搭載)](https://soracom.jp/store/5243/) を使って

[温度 (C) と湿度 (%) を 60 秒ごとに計測し JSON で Unified Endpoint へ送信する](https://users.soracom.io/ja-jp/guides/starter-kits/arduino/samples/#%e6%b8%a9%e5%ba%a6-c-%e3%81%a8%e6%b9%bf%e5%ba%a6--%e3%82%92-60-%e7%a7%92%e3%81%94%e3%81%a8%e3%81%ab%e8%a8%88%e6%b8%ac%e3%81%97-json-%e3%81%a7-unified-endpoint-%e3%81%b8%e9%80%81%e4%bf%a1%e3%81%99%e3%82%8b)

https://github.com/soracom-labs/arduino-dragino-unified/blob/main/send_temp_and_humi_with_soracom/send_temp_and_humi_with_soracom.ino

このスケッチを参考に

```C++:send_temp_and_humi_with_soracom.ino
/*
 * send_temp_and_humi_with_soracom.ino
 * Ambient temperature and humidity send to Cloud via Unified Endpoint of SORACOM
 *
 * Copyright SORACOM
 * This software is released under the MIT License, and libraries used by these sketches 
 * are subject to their respective licenses.
 * See also: https://github.com/soracom-labs/arduino-dragino-unified/README.md
*/

#define CONSOLE Serial
#define INTERVAL_MS (60000)
#define ENDPOINT "uni.soracom.io"
#define SKETCH_NAME "send_temp_and_humi_with_soracom"
#define VERSION "1.1"

/* for LTE-M Shield for Arduino */
#define RX 10
#define TX 11
#define BAUDRATE 9600
#define BG96_RESET 15

#define TINY_GSM_MODEM_BG96
#include <TinyGsmClient.h>

#include <SoftwareSerial.h>
SoftwareSerial LTE_M_shieldUART(RX, TX);
TinyGsm modem(LTE_M_shieldUART);
TinyGsmClient ctx(modem);

#include <U8x8lib.h>
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);
#define U8X8_ENABLE_180_DEGREE_ROTATION 1

#include <DHT.h>

#define USE_DHT11 // Use DHT11 (Blue)
//#define USE_DHT20 // Use DHT20 (Black)

#ifdef USE_DHT11
  #define dht11Pin 3
  DHT dht(dht11Pin, DHT11);
#endif
#ifdef USE_DHT20
  DHT dht(DHT20);
#endif

#define OLED_MAX_CHAR_LENGTH 16
void drawText(U8X8* u8x8, const char* in_str, int width = OLED_MAX_CHAR_LENGTH);
void drawText_P(U8X8* u8x8, const char* pgm_s, int width = OLED_MAX_CHAR_LENGTH);

#define RESET_DURATION 86400000UL // 1 day
void software_reset() {
  asm volatile ("  jmp 0");
}

void setup() {
  CONSOLE.begin(115200);
  LTE_M_shieldUART.begin(BAUDRATE);
  u8x8.begin();
  u8x8.setFlipMode(U8X8_ENABLE_180_DEGREE_ROTATION);
  u8x8.setFont(u8x8_font_victoriamedium8_r);

  CONSOLE.print(F("Welcome to ")); CONSOLE.print(SKETCH_NAME); CONSOLE.print(F(" ")); CONSOLE.println(VERSION);
  u8x8.clear();
  drawText_P(&u8x8, PSTR("Welcome to ")); drawText(&u8x8, SKETCH_NAME); drawText_P(&u8x8, PSTR(" ")); drawText(&u8x8, VERSION);
  delay(3000);

  CONSOLE.print(F("resetting module "));
  pinMode(BG96_RESET,OUTPUT);
  digitalWrite(BG96_RESET,LOW);
  delay(300);
  digitalWrite(BG96_RESET,HIGH);
  delay(300);
  digitalWrite(BG96_RESET,LOW);
  CONSOLE.println(F(" done."));

  u8x8.clear();
  drawText_P(&u8x8, PSTR("modem.restart()..."));
  modem.restart();
  drawText_P(&u8x8, PSTR("done."));
  delay(500);

  u8x8.clear();
  drawText_P(&u8x8, PSTR("modem.getModemInfo():"));
  String modemInfo = modem.getModemInfo();
  u8x8.println();
  int modem_info_len = modemInfo.length() + 1;
  char modem_info_buf[modem_info_len];
  modemInfo.toCharArray(modem_info_buf, modem_info_len);
  drawText(&u8x8, modem_info_buf);
  delay(2000);

  u8x8.clear();
  drawText_P(&u8x8, PSTR("waitForNetwork()..."));
  while (!modem.waitForNetwork());
  drawText_P(&u8x8, PSTR("Ok."));
  delay(500);

  u8x8.clear();
  drawText_P(&u8x8, PSTR("gprsConnect(soracom.io)..."));
  modem.gprsConnect("soracom.io", "sora", "sora");
  drawText_P(&u8x8, PSTR("done."));
  delay(500);

  u8x8.clear();
  drawText_P(&u8x8, PSTR("isNetworkConnected()..."));
  while (!modem.isNetworkConnected());
  drawText_P(&u8x8, PSTR("Ok."));
  delay(500);

  u8x8.clear();
  drawText_P(&u8x8, PSTR("My IP addr: "));
  u8x8.println();
  IPAddress ipaddr = modem.localIP();
  CONSOLE.println(ipaddr);
  char ip_addr_buf[20];
  sprintf_P(ip_addr_buf, PSTR("%d.%d.%d.%d"), ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3]);
  drawText(&u8x8, ip_addr_buf);
  delay(2000);

  dht.begin();
}

void loop() {
  // HTTP POST method
  CONSOLE.println("making POST request");
  char measurement[] = "sensor";
  char tagSet[] = "device=SORACOM";
  String fieldSet ;
  float t = dht.readTemperature();
  char t_buf1[10];
  dtostrf(t, -1, 1, t_buf1); /*  左詰め*/
  int humi = (int) dht.readHumidity();
  char payload[40];
  sprintf_P(payload, PSTR("%s,%s temperature=%s,humidity=%di"),measurement,tagSet, t_buf1, humi);

  CONSOLE.println(payload);
  u8x8.clear();
  drawText(&u8x8, payload);
  u8x8.println();

  CONSOLE.print(F("Send..."));
  drawText_P(&u8x8, PSTR("Send..."));
  /* connect */
  if (!ctx.connect(ENDPOINT, 80)) {
    CONSOLE.println(F("failed."));
    drawText_P(&u8x8, PSTR("failed."));
    delay(3000);
    return;
  }
  /* send request */
  char hdr_buf[40];
  ctx.println(F("POST / HTTP/1.1"));
//  sprintf_P(hdr_buf, PSTR("Host: %s"), ENDPOINT);
//  ctx.println(hdr_buf);
//  ctx.println(F("Content-Type: text/plain; charset=utf-8"));
  sprintf_P(hdr_buf, PSTR("Content-Length: %d"), strlen(payload));
  ctx.println(hdr_buf);
  ctx.println();
  ctx.println(payload);
  /* receive response */
  while (ctx.connected()) {
    String line = ctx.readStringUntil('\n');
    CONSOLE.println(line);
    if (line == "\r") {
      CONSOLE.println(F("Response header received."));
      break;
    }
  }
  // NOTE: response body is ignore
  ctx.stop();
  CONSOLE.println(F("done."));
  drawText_P(&u8x8, PSTR("done."));

  delay(INTERVAL_MS);
  
#ifdef RESET_DURATION
  if(millis() > RESET_DURATION )
  {
    CONSOLE.println("Execute software reset...");
    delay(1000);
    software_reset();
  }
#endif
}

void drawText(U8X8* u8x8, const char* in_str, int width = OLED_MAX_CHAR_LENGTH) {
  size_t len = strlen(in_str);
  for (int i = 0 ; i < len ; i++) {
    if (u8x8->tx > width - 1) {
      u8x8->tx = 0; // CR
      u8x8->ty++;   // LF
    }
    u8x8->print(in_str[i]);
  }
}

void drawText_P(U8X8* u8x8, const char* pgm_s, int width = OLED_MAX_CHAR_LENGTH) {
  size_t len = strlen_P(pgm_s);
  for (int i = 0 ; i < len ; i++) {
    if (u8x8->tx > width - 1) {
      u8x8->tx = 0; // CR
      u8x8->ty++;   // LF
    }
    char c = pgm_read_byte(pgm_s++);
    u8x8->print(c);
  }
}
```