投稿日

【Ask SORACOM Vol.13】実測!M5Stack Basic のフラッシュメモリ使用量を 50% 削減できる SORACOM 活用術

こんにちは、CRE (Customer Reliability Engineer) の加納(ニックネーム: Kanu)です。

ソラコムの CRE チームは「SORACOM を利用する上でのお客様の不安をゼロにすること」をミッションに、お客様サポートやドキュメントの拡充に努めています。また、多くのお客様のナレッジとできるよう、いただいたお問い合わせを基にした SORACOM サービスの活用方法や利用時の注意点、CRE チームが実施した検証結果、更新したドキュメントなどについて不定期に紹介しています。SORACOM サービスのご利用にあたって気付きになれば幸いです。

はじめに

CRE チームではお客様からトラブルシューティング関連のお問い合わせをいただいた場合などに、問題箇所の切り分けのため、同じデバイス・接続先・プロトコルの通信を「SORACOM を利用する方法」「SORACOM を利用しない方法」の両方で検証することがあります。今回は、M5Stack Basic から AWS IoT Core への MQTTS 通信について検証した際に、 SORACOM Beam を使うことで「SORACOM を利用しない方法」と比較して M5Stack Basic のフラッシュメモリ使用量を 50% 削減できたので、ご紹介いたします。実測値は後述します。

SORACOM Beam とは

まずは、SORACOM Beam というサービスについて簡単にご紹介いたします。

SORACOM Beam は IoT デバイスから送信されたデータに対してプロトコル変換や TLS 暗号化などの処理を加えて任意の接続先に転送できるサービスです。2015 年 9 月 30 日に IoT プラットフォーム SORACOM のローンチと同時にリリースされました。

SORACOM Beam を利用することで、上図の右側のインターネット区間を TLS で暗号化できます。また、上図の左側の IoT デバイス〜 SORACOM プラットフォームまでの区間は SORACOM Air for セルラーが提供する閉域網を経由しています。これにより、デバイス側でプリミティブなプロトコルを利用した場合でも通信経路全体が保護された状態になります。

比較した方法

今回は M5Stack Basic から AWS IoT Core へ、X.509 証明書による認証を使用して MQTTS で接続するプログラムを以下の 2 つの方法で実装しました。

  • [SORACOM を利用しない方法] Wi-Fi
  • [SORACOM を利用する方法] SORACOM Air for セルラー + SORACOM Beam (MQTT エントリポイント)
SORACOM Beam のエントリポイント

デバイスから SORACOM Beam への接続先はエントリポイントと呼ばれます。たとえば、HTTP エントリポイント、TCP → HTTP/HTTPS エントリポイントなど、それぞれ異なるポート番号をもつエントリポイントがあります。

SORACOM を利用しない方法

SORACOM を利用しない方法では、デバイスから AWS IoT Core に Wi-Fi で MQTTS 接続します。下図のようにデバイス側で TLS 暗号化処理と X.509 証明書、証明書の秘密鍵、ルート証明書といった認証情報を保持する必要があります。

[SORACOM を利用しない方法] Wi-Fi

ソースコードはこちら

Wi-Fi 接続 + TLS 暗号化の処理に WiFiClientSecure、MQTT クライアントには PubSubClient を利用しています。

コードの内容ですが、センサーの代わりに数直線上を行ったり来たりする “PositionMachine” という仮想的な装置を模しており、PositionMachineの位置と電源のON/OFF状態をAWS IoT Coreのデバイスシャドウに同期します。
PositionMachineの位置はM5StackのA/Bボタンで-/+に移動、電源はCボタンがON/OFFのトグルです。また、IoT Coreのデバイスシャドウをクラウド上で更新することで、PositionMachineの位置や電源状態が変更できるといった双方向通信ができるデモです。

/*
   Example | AWS IoT Core's Device shadow implementation on M5Stack Basic/Gray
   Copyright SORACOM
   This software is released under the MIT License, and libraries used by these sketches 
   are subject to their respective licenses.
   
*/

static const char _VERSION_[] = "deviceshadow-wifi-0.9";

static const char DEVICE_ID_PREFIX[] = "m5stack";
static const char THING_NAME[] = "mcu1";
static const char SHADOW_NAME[] = "peripheral";

#include <M5Stack.h>
static const char TAG[] = "TAG"; // for ESP_LOG*

#include <WiFiClientSecure.h>
#include <WiFi.h>
static const char WIFI_SSID[] = "xxxxx";
static const char WIFI_PASSWORD[] = "xxxxx";
WiFiClientSecure ctx;

static const char rootCA[] PROGMEM = \
"-----BEGIN CERTIFICATE-----\n"
// ルート証明書
"-----END CERTIFICATE-----\n";

static const char certificate[] PROGMEM = \
"-----BEGIN CERTIFICATE-----\n"
// X.509 証明書
"-----END CERTIFICATE-----\n";

static const char privateKey[] PROGMEM = \
"-----BEGIN RSA PRIVATE KEY-----\n"
// 証明書の秘密鍵
"-----END RSA PRIVATE KEY-----\n";

#include <ArduinoJson.h>
#include <vector>

#include <PubSubClient.h>
PubSubClient MqttClient;
static const char mqtt_server_address[] = "xxx-ats.iot.us-west-2.amazonaws.com";
static const uint16_t mqtt_server_port = 8883;

struct Shadow {
  uint32_t version = 0;
  char reported[256];
  char update_delta[256];
  char get[256];
  char get_accepted[256];
};
struct Shadow shadow;

// Take the PositionMachine as an example.
class PositionMachine {
  public:
    int32_t position();
    void position(int32_t value);
    bool power();
    void power(bool new_condition);
  private:
    volatile int32_t current_position = 0;
    volatile bool power_condition = false;
};
int32_t PositionMachine::position() { return current_position; }
void PositionMachine::position(int32_t new_position) { current_position = new_position; }
bool PositionMachine::power() { return power_condition; }
void PositionMachine::power(bool new_condition) { power_condition = new_condition; }
PositionMachine eg_sensor;

void _draw_value(const char *title, const int32_t value, const uint16_t row = 0) {
  M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * row, M5.Lcd.textWidth("99999999999999999"), M5.Lcd.fontHeight() * 1, TFT_BLACK); // Clear the line.
  M5.Lcd.setCursor(0, M5.Lcd.fontHeight() * row);
  M5.Lcd.printf("%s%ld", title, value);
}

void report_state(const std::vector<std::string> &erase_targets = std::vector<std::string>()) {
  DynamicJsonDocument doc(2048);
  JsonObject state = doc.createNestedObject("state"); // Report store.
  
  // eg.) Implement collection of values for report. <start>");
  state["reported"]["eg_position"] = eg_sensor.position(); // eg.) Reading position of PositionMachine.
  state["reported"]["eg_power"] = eg_sensor.power(); // eg.) Reading power condition of PositionMachine.
  // <end>
  
  for (auto itr = erase_targets.begin() ; itr != erase_targets.end() ; ++itr) { // Erase applied the "desired".
    const std::string &erase_target = *itr;
    state["desired"][erase_target.c_str()] = nullptr;
  }
  char payload[2048];
  serializeJson(doc, payload, sizeof(payload));
  ESP_LOGD(TAG, "Report to: %s", shadow.reported);
  ESP_LOGD(TAG, "Payload: %s", payload);
  MqttClient.publish(shadow.reported, payload);
}

std::vector<std::string> operate_peripheral(JsonObject state) {
  String _m; serializeJson(state, _m); ESP_LOGD(TAG, "state: %s", _m.c_str());
  if (!state) {
    ESP_LOGD(TAG, "Ignored because it does not contain \"state\", force exit.");
    return std::vector<std::string>();
  }
  std::vector<std::string> successfully; // = Erasure targets store.
  
  // eg.) Implementation to operate peripherals. <start>
  if (!state["eg_position"].isNull()) { // eg.) Set position of PositionMachine.
    eg_sensor.position(state["eg_position"].as<int32_t>());
    _draw_value("Position: ", eg_sensor.position(), 8);
    successfully.push_back("eg_position"); // Successfully applied = Erase the corresponding the "desired".
  }
  if (!state["eg_power"].isNull()) { // eg.) Power condition of PositionMachine.
    eg_sensor.power(state["eg_power"].as<bool>());
    _draw_value("Power: ", eg_sensor.power(), 9);
    successfully.push_back("eg_power"); 
  }
  // <end>
  return successfully;
}

void mqtt_subscriber_callback(const char *topic, byte *payload, unsigned int length) {
  String buf_t = String(topic);
  ESP_LOGD(TAG, "Incoming: %s", buf_t.c_str());
  payload[length] = '\0'; // https://hawksnowlog.blogspot.com/2017/06/convert-byte-array-to-string.html
  String buf_p = String((char*) payload); // convert to String from char*
  ESP_LOGD(TAG, "Payload: %s", buf_p.c_str());
  
  DynamicJsonDocument doc(2048);
  DeserializationError error = deserializeJson(doc, buf_p);
  if (error) {
    ESP_LOGE(TAG, "deserializeJson() failed, force exit.: %s", error.c_str());
    return;
  }
  
  uint32_t incoming_shadow_version = doc["version"].as<uint32_t>();
  ESP_LOGD(TAG, "Shadow version: current: %lu, incoming: %lu", shadow.version, incoming_shadow_version);
  if (shadow.version > incoming_shadow_version) { // for avoiding revert.
    ESP_LOGE(TAG, "Detecting revert, force exit.");
    return;
  }
  
  if (buf_t.endsWith("/get/accepted")) { // Restore according the shadow.
    operate_peripheral(doc["state"]["reported"]); // Restore. "state.desired" is ignore here. It will be received incoming in /update/delta when the report is sent.
    report_state(); // Therefore, "desired" is not erased.
  } else if (buf_t.endsWith("/update/delta")) { // Desired from IoT Core.
    report_state(operate_peripheral(doc["state"]));
  }
  shadow.version = incoming_shadow_version; // Update for next update.
}

template <typename T> void connect_to_network(T *wifi) {
  int32_t _enter = millis();
  wifi->mode(WIFI_STA);
  ESP_LOGD(TAG, "begin():");
  wifi->begin(WIFI_SSID, WIFI_PASSWORD);
  while (wifi->status() != WL_CONNECTED) {
    ESP_LOGV(TAG, ".");
    delay(200);
  }
  ESP_LOGD(TAG, "localIP(): %s", wifi->localIP().toString().c_str());
  ESP_LOGD(TAG, "elapsed(ms): %d", millis() - _enter);
}

template <typename T> bool is_network_connected(T *wifi) {
  bool result = (wifi->status() == WL_CONNECTED);
  if (!result) ESP_LOGE(TAG, "status() != WL_CONNECTED");
  return result;
}

template <typename T> void connect_to_mqtt_broker(const char *device_id_prefix, const char *thing_name, const char *shadow_name, T *ctx) {
  int32_t _enter = millis();
  char shadow_prefix[128];
  sprintf_P(shadow_prefix, PSTR("$aws/things/%s/shadow/name/%s"), thing_name, shadow_name); // for named shadow.  
  sprintf_P(shadow.reported, PSTR("%s/update"), shadow_prefix);
  ESP_LOGD(TAG, "/reported: %s", shadow.reported);
  sprintf_P(shadow.update_delta, PSTR("%s/update/delta"), shadow_prefix);
  ESP_LOGD(TAG, "/update/delta: %s", shadow.update_delta);
  sprintf_P(shadow.get, PSTR("%s/get"), shadow_prefix);
  ESP_LOGD(TAG, "/get: %s", shadow.get);
  sprintf_P(shadow.get_accepted, PSTR("%s/get/accepted"), shadow_prefix);
  ESP_LOGD(TAG, "/get/accepted: %s", shadow.get_accepted);
  
  ctx->setCACert(rootCA);
  ctx->setCertificate(certificate);
  ctx->setPrivateKey(privateKey);
  ESP_LOGD(TAG, "Connect to: mqtts://%s:%d", mqtt_server_address, mqtt_server_port);
  MqttClient.setServer(mqtt_server_address, mqtt_server_port);
  MqttClient.setClient(*ctx);
  MqttClient.setBufferSize(2048);
  MqttClient.setKeepAlive(1200); // sec. Max of IoT Core's spec.
  MqttClient.setCallback(mqtt_subscriber_callback);
  char mqtt_id[64];
  sprintf_P(mqtt_id, PSTR("%s-%s"), device_id_prefix, String(random(1000000, 9999999)));
  ESP_LOGD(TAG, "MQTT_ID: %s", mqtt_id);
  if (!MqttClient.connect(mqtt_id)) {
    ESP_LOGE(TAG, "MqttClient.connect(): failed, force exit. (MqttClient.state(): %d)", MqttClient.state());
    return;
  }
  MqttClient.subscribe(shadow.get_accepted);
  MqttClient.subscribe(shadow.update_delta);
  ESP_LOGD(TAG, "Get updates while offline from shadow. (waiting 3 seconds.)");
  delay(3000);
  MqttClient.publish(shadow.get, "{}");
  ESP_LOGD(TAG, "Waiting shadow. (with 3 seconds.)");
  delay(3000);
  ESP_LOGD(TAG, "elapsed(ms): %d", millis() - _enter);
}

bool is_mqtt_broker_connected() {
  bool result = MqttClient.connected();
  if (!result) ESP_LOGE(TAG, "MqttClient.connected(): failed. (MqttClient.state(): %d)", MqttClient.state());
  return result;
}

void mcu_restart() {
  ESP_LOGD(TAG, "enter.");
  delay(3000); // waiting for flush of serial buffer
  M5.Power.reset();
}

void setup() {
  delay(1000);
  
  M5.begin();
  dacWrite(25, 0); // Speaker OFF // bad known how for M5Stack ...
  Serial.begin(115200);
  ESP_LOGD(TAG, "--- START: %s", _VERSION_);
  
  M5.Lcd.wakeup();
  M5.Lcd.setBrightness(40);
  M5.Lcd.setTextSize(3);
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.clear();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(F("Connecting..."));
  
  connect_to_network(&WiFi);
  while (!is_mqtt_broker_connected()) connect_to_mqtt_broker(DEVICE_ID_PREFIX, THING_NAME, SHADOW_NAME, &ctx);
  
  M5.Lcd.clear();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(F("EXAMPLE:     WiFi"));
  M5.Lcd.println(F("    Device Shadow"));
  M5.Lcd.println(F("     by kanu"));
  M5.Lcd.println(F("BtnA    | BtnB   "));
  M5.Lcd.println(F("  -1000 |   +1000"));
  M5.Lcd.println(F("BtnC PWR toggle  "));
  _draw_value("Position: ", eg_sensor.position(), 8);
  _draw_value("Power: ", eg_sensor.power(), 9);
}

static const uint32_t LOOP_INTERVAL_MS_IN_LOOP = 50; // msec
void loop() {
  if (!is_network_connected(&WiFi)) mcu_restart();
  while (!is_mqtt_broker_connected()) connect_to_mqtt_broker(DEVICE_ID_PREFIX, THING_NAME, SHADOW_NAME, &ctx);
  
  // Examples of operated from the field <start>
  M5.update();
  
  int32_t pos = eg_sensor.position();
  if (M5.BtnA.wasPressed()) pos = pos - 1000;
  if (M5.BtnB.wasPressed()) pos = pos + 1000;
  if (pos != eg_sensor.position()) { // is modified ?
    ESP_LOGD(TAG, "Local operation");
    DynamicJsonDocument doc(128);
    JsonObject state = doc.createNestedObject("state");
    state["eg_position"] = pos;
    report_state(operate_peripheral(state));
  }
  
  bool pwr = eg_sensor.power();
  if (M5.BtnC.wasPressed()) pwr = !pwr;
  if (pwr != eg_sensor.power()) { // is modified ?
    ESP_LOGD(TAG, "Local operation");
    DynamicJsonDocument doc(128);
    JsonObject state = doc.createNestedObject("state");
    state["eg_power"] = pwr;
    report_state(operate_peripheral(state));
  }
  // <end>
  
  unsigned long next = millis();
  while (millis() < next + LOOP_INTERVAL_MS_IN_LOOP) MqttClient.loop();
}

SORACOM を利用する方法

一方、SORACOM を利用する方法では上述した内容 (TLS 暗号化処理と認証情報の保持) を SORACOM Beam が肩代わりしてくれますので、M5Stack Basic 側の実装に組み込む必要がありません。なお、SORACOM Beam を利用して AWS IoT Core に MQTTS 接続する際の SORACOM, AWS の設定方法についてはユーザードキュメントに記載しています。

[SORACOM を利用する方法] SORACOM Air for セルラー + SORACOM Beam (MQTT エントリポイント)

ソースコードはこちら

M5Stack Basic 3G 拡張ボードで利用できるライブラリ TinyGSM を利用しています。MQTT クライアントライブラリは Wi-Fi 接続と同様に PubSubClient を利用しています。

コードの内容自体は、Wi-Fiのみの時と同様です。

/*
   Example | AWS IoT Core's Device shadow implementation using SORACOM Beam on M5Stack Basic/Gray + SORACOM Air(3G ext. board)
   Copyright SORACOM
   This software is released under the MIT License, and libraries used by these sketches 
   are subject to their respective licenses.
   
*/

static const char _VERSION_[] = "deviceshadow-3G+Beam-0.9";

static const char DEVICE_ID_PREFIX[] = "m5stack";
static const char THING_NAME[] = "mcu1";
static const char SHADOW_NAME[] = "peripheral";

#include <M5Stack.h>
static const char TAG[] = "TAG"; // for ESP_LOG*

#define SerialAT Serial2 // Serial2 is 3G ext. module
#define TINY_GSM_MODEM_UBLOX
#include <TinyGsmClient.h>
TinyGsm modem(SerialAT);
TinyGsmClient ctx(modem);

#include <ArduinoJson.h>
#include <vector>

#include <PubSubClient.h>
PubSubClient MqttClient;
static const char mqtt_server_address[] = "beam.soracom.io";
static const uint16_t mqtt_server_port = 1883;

struct Shadow {
  uint32_t version = 0;
  char reported[256];
  char update_delta[256];
  char get[256];
  char get_accepted[256];
};
struct Shadow shadow;

// Take the PositionMachine as an example.
class PositionMachine {
  public:
    int32_t position();
    void position(int32_t value);
    bool power();
    void power(bool new_condition);
  private:
    volatile int32_t current_position = 0;
    volatile bool power_condition = false;
};
int32_t PositionMachine::position() { return current_position; }
void PositionMachine::position(int32_t new_position) { current_position = new_position; }
bool PositionMachine::power() { return power_condition; }
void PositionMachine::power(bool new_condition) { power_condition = new_condition; }
PositionMachine eg_sensor;

void _draw_value(const char *title, const int32_t value, const uint16_t row = 0) {
  M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * row, M5.Lcd.textWidth("99999999999999999"), M5.Lcd.fontHeight() * 1, TFT_BLACK); // Clear the line.
  M5.Lcd.setCursor(0, M5.Lcd.fontHeight() * row);
  M5.Lcd.printf("%s%ld", title, value);
}

void report_state(const std::vector<std::string> &erase_targets = std::vector<std::string>()) {
  DynamicJsonDocument doc(2048);
  JsonObject state = doc.createNestedObject("state"); // Report store.
  
  // eg.) Implement collection of values for report. <start>");
  state["reported"]["eg_position"] = eg_sensor.position(); // eg.) Reading position of PositionMachine.
  state["reported"]["eg_power"] = eg_sensor.power(); // eg.) Reading power condition of PositionMachine.
  // <end>
  
  for (auto itr = erase_targets.begin() ; itr != erase_targets.end() ; ++itr) { // Erase applied the "desired".
    const std::string &erase_target = *itr;
    state["desired"][erase_target.c_str()] = nullptr;
  }
  char payload[2048];
  serializeJson(doc, payload, sizeof(payload));
  ESP_LOGD(TAG, "Report to: %s", shadow.reported);
  ESP_LOGD(TAG, "Payload: %s", payload);
  MqttClient.publish(shadow.reported, payload);
}

std::vector<std::string> operate_peripheral(JsonObject state) {
  String _m; serializeJson(state, _m); ESP_LOGD(TAG, "state: %s", _m.c_str());
  if (!state) {
    ESP_LOGD(TAG, "Ignored because it does not contain \"state\", force exit.");
    return std::vector<std::string>();
  }
  std::vector<std::string> successfully; // = Erasure targets store.
  
  // eg.) Implementation to operate peripherals. <start>
  if (!state["eg_position"].isNull()) { // eg.) Set position of PositionMachine.
    eg_sensor.position(state["eg_position"].as<int32_t>());
    _draw_value("Position: ", eg_sensor.position(), 8);
    successfully.push_back("eg_position"); // Successfully applied = Erase the corresponding the "desired".
  }
  if (!state["eg_power"].isNull()) { // eg.) Power condition of PositionMachine.
    eg_sensor.power(state["eg_power"].as<bool>());
    _draw_value("Power: ", eg_sensor.power(), 9);
    successfully.push_back("eg_power"); 
  }
  // <end>
  return successfully;
}

void mqtt_subscriber_callback(const char *topic, byte *payload, unsigned int length) {
  String buf_t = String(topic);
  ESP_LOGD(TAG, "Incoming: %s", buf_t.c_str());
  payload[length] = '\0'; // https://hawksnowlog.blogspot.com/2017/06/convert-byte-array-to-string.html
  String buf_p = String((char*) payload); // convert to String from char*
  ESP_LOGD(TAG, "Payload: %s", buf_p.c_str());
  
  DynamicJsonDocument doc(2048);
  DeserializationError error = deserializeJson(doc, buf_p);
  if (error) {
    ESP_LOGE(TAG, "deserializeJson() failed, force exit.: %s", error.c_str());
    return;
  }
  
  uint32_t incoming_shadow_version = doc["version"].as<uint32_t>();
  ESP_LOGD(TAG, "Shadow version: current: %lu, incoming: %lu", shadow.version, incoming_shadow_version);
  if (shadow.version > incoming_shadow_version) { // for avoiding revert.
    ESP_LOGE(TAG, "Detecting revert, force exit.");
    return;
  }
  
  if (buf_t.endsWith("/get/accepted")) { // Restore according the shadow.
    operate_peripheral(doc["state"]["reported"]); // Restore. "state.desired" is ignore here. It will be received incoming in /update/delta when the report is sent.
    report_state(); // Therefore, "desired" is not erased.
  } else if (buf_t.endsWith("/update/delta")) { // Desired from IoT Core.
    report_state(operate_peripheral(doc["state"]));
  }
  shadow.version = incoming_shadow_version; // Update for next update.
}

void connect_to_network(TinyGsm *modem) {
  int32_t _enter = millis();
  ESP_LOGD(TAG, "restart():");
  modem->restart();
  ESP_LOGD(TAG, "getModemInfo(): %s", modem->getModemInfo().c_str());
  ESP_LOGD(TAG, "getIMEI(): ***********%s", modem->getIMEI().substring(11).c_str());
  ESP_LOGD(TAG, "getIMSI(): ***********%s", modem->getIMSI().substring(11).c_str());
  ESP_LOGD(TAG, "waitForNetwork():");
  while (!modem->waitForNetwork()) ESP_LOGV(TAG, ".");
  ESP_LOGD(TAG, "gprsConnect(soracom.io):");
  modem->gprsConnect("soracom.io", "sora", "sora");
  ESP_LOGD(TAG, "isNetworkConnected():");
  while (!modem->isNetworkConnected()) ESP_LOGV(TAG, ".");
  ESP_LOGD(TAG, "localIP(): %s", modem->localIP().toString().c_str());
  ESP_LOGD(TAG, "elapsed(ms): %d", millis() - _enter);
}

bool is_network_connected(TinyGsm *modem) {
  bool result = modem->isGprsConnected();
  if (!result) ESP_LOGE(TAG, "isGprsConnected(): false");
  return result;
}

template <typename T> void connect_to_mqtt_broker(const char *device_id_prefix, const char *thing_name, const char *shadow_name, T *ctx) {
  int32_t _enter = millis();
  char shadow_prefix[128];
  sprintf_P(shadow_prefix, PSTR("$aws/things/%s/shadow/name/%s"), thing_name, shadow_name); // for named shadow.  
  sprintf_P(shadow.reported, PSTR("%s/update"), shadow_prefix);
  ESP_LOGD(TAG, "/reported: %s", shadow.reported);
  sprintf_P(shadow.update_delta, PSTR("%s/update/delta"), shadow_prefix);
  ESP_LOGD(TAG, "/update/delta: %s", shadow.update_delta);
  sprintf_P(shadow.get, PSTR("%s/get"), shadow_prefix);
  ESP_LOGD(TAG, "/get: %s", shadow.get);
  sprintf_P(shadow.get_accepted, PSTR("%s/get/accepted"), shadow_prefix);
  ESP_LOGD(TAG, "/get/accepted: %s", shadow.get_accepted);
  
  ESP_LOGD(TAG, "Connect to: mqtt://%s:%d", mqtt_server_address, mqtt_server_port);
  MqttClient.setServer(mqtt_server_address, mqtt_server_port);
  MqttClient.setClient(*ctx);
  MqttClient.setBufferSize(2048);
  MqttClient.setKeepAlive(1200); // sec. Max of IoT Core's spec.
  MqttClient.setCallback(mqtt_subscriber_callback);
  char mqtt_id[64];
  sprintf_P(mqtt_id, PSTR("%s-%s"), device_id_prefix, String(random(1000000, 9999999)));
  ESP_LOGD(TAG, "MQTT_ID: %s", mqtt_id);
  if (!MqttClient.connect(mqtt_id)) {
    ESP_LOGE(TAG, "MqttClient.connect(): failed, force exit. (MqttClient.state(): %d)", MqttClient.state());
    return;
  }
  MqttClient.subscribe(shadow.get_accepted);
  MqttClient.subscribe(shadow.update_delta);
  ESP_LOGD(TAG, "Get updates while offline from shadow. (waiting 3 seconds.)");
  delay(3000);
  MqttClient.publish(shadow.get, "{}");
  ESP_LOGD(TAG, "Waiting shadow. (with 3 seconds.)");
  delay(3000);
  ESP_LOGD(TAG, "elapsed(ms): %d", millis() - _enter);
}

bool is_mqtt_broker_connected() {
  bool result = MqttClient.connected();
  if (!result) ESP_LOGE(TAG, "MqttClient.connected(): failed. (MqttClient.state(): %d)", MqttClient.state());
  return result;
}

void mcu_restart() {
  ESP_LOGD(TAG, "enter.");
  delay(3000); // waiting for flush of serial buffer
  M5.Power.reset();
}

void setup() {
  delay(1000);
  
  M5.begin();
  dacWrite(25, 0); // Speaker OFF // bad known how for M5Stack ...
  Serial.begin(115200);
  ESP_LOGD(TAG, "--- START: %s", _VERSION_);
  
  M5.Lcd.wakeup();
  M5.Lcd.setBrightness(40);
  M5.Lcd.setTextSize(3);
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.clear();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(F("Connecting..."));
  
  SerialAT.begin(115200, SERIAL_8N1, 16, 17); // 3G ext. module
  connect_to_network(&modem);
  while (!is_mqtt_broker_connected()) connect_to_mqtt_broker(DEVICE_ID_PREFIX, THING_NAME, SHADOW_NAME, &ctx);
  
  M5.Lcd.clear();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(F("EXAMPLE:  3G+Beam"));
  M5.Lcd.println(F("    Device Shadow"));
  M5.Lcd.println(F("     by @ma2shita"));
  M5.Lcd.println(F("BtnA    | BtnB   "));
  M5.Lcd.println(F("  -1000 |   +1000"));
  M5.Lcd.println(F("BtnC PWR toggle  "));
  _draw_value("Position: ", eg_sensor.position(), 8);
  _draw_value("Power: ", eg_sensor.power(), 9);
}

static const uint32_t LOOP_INTERVAL_MS_IN_LOOP = 50; // msec
void loop() {
  if (!is_network_connected(&modem)) mcu_restart();
  while (!is_mqtt_broker_connected()) connect_to_mqtt_broker(DEVICE_ID_PREFIX, THING_NAME, SHADOW_NAME, &ctx);
  
  // Examples of operated from the field <start>
  M5.update();
  
  int32_t pos = eg_sensor.position();
  if (M5.BtnA.wasPressed()) pos = pos - 1000;
  if (M5.BtnB.wasPressed()) pos = pos + 1000;
  if (pos != eg_sensor.position()) { // is modified ?
    ESP_LOGD(TAG, "Local operation");
    DynamicJsonDocument doc(128);
    JsonObject state = doc.createNestedObject("state");
    state["eg_position"] = pos;
    report_state(operate_peripheral(state));
  }
  
  bool pwr = eg_sensor.power();
  if (M5.BtnC.wasPressed()) pwr = !pwr;
  if (pwr != eg_sensor.power()) { // is modified ?
    ESP_LOGD(TAG, "Local operation");
    DynamicJsonDocument doc(128);
    JsonObject state = doc.createNestedObject("state");
    state["eg_power"] = pwr;
    report_state(operate_peripheral(state));
  }
  // <end>
  
  unsigned long next = millis();
  while (millis() < next + LOOP_INTERVAL_MS_IN_LOOP) MqttClient.loop();
}

また、上記で説明した IoT デバイスが TLS 通信をするにあたり必要となる要件や SORACOM を利用した解決方法については、ソリューションアーキテクトの渡邊 (dai) が執筆した「IoT機器のTLS通信で立ちはだかる証明書の運用」で詳しく説明しています。あわせてご覧いただけますと幸いです。

実測値の共有!

実測値は以下のとおりです。

実装方法フラッシュメモリ
[SORACOM を利用しない方法] Wi-Fi948,317 Byte
[SORACOM を利用する方法] SORACOM Air for セルラー + SORACOM Beam395,041 Byte
差分553,276 Byte

SORACOM を活用することで M5Stack Basic のフラッシュメモリを 540KB ほど削減できることが確認できました。

なお、両プログラムの差は以下のとおりです。

No差分Wi-FiSORACOM Air for セルラー + SORACOM Beam
1データの送信先 xxx-ats.iot.us-west-2.amazonaws.com:8883 (AWS IoT Core のデバイスデータエンドポイント)beam.soracom.io:1883 (SORACOM Beam MQTT エントリポイント)
2通信方式Wi-Fiセルラー通信 (3G)
3認証情報の保持有り (X.509 証明書、証明書の秘密鍵、ルート証明書)無し
4TLS 暗号化有り無し

フラッシュメモリ使用量への影響が大きいのは 3,4 です。ライブラリを使用しているため実装量自体はそれほど変わりませんが、認証情報 (X.509 証明書、証明書の秘密鍵、ルート証明書 の 3 つ) はそれぞれ文字列型で 1.2 〜 1.6 KB ほどあるため 3 つ合わせると 4 KB ほどの差があります。更に 4 のライブラリの実行コードが読み込まれることで合計 540KB ほどの削減に繋がっています。

今回は M5Stack Basic で検証しましたが、例えば Arduino Uno を利用する場合は認証情報分のフラッシュメモリ削減量 (約 4 KB) だけでも大きな差となります。つまり、SORACOM Beam を利用することで、低容量のマイコンを利用する場合においてもより安心な通信が実現しやすくなります。

また、SORACOM Beam を利用することでデータ通信量の削減も期待できます。具体的にどの程度削減されるのか知りたい方は、ソリューションアーキテクトの松永 (taketo) が執筆した「実測!IoT通信プロトコルのオーバーヘッドの実態と削減方法」も参考になると思います。

最後に

如何でしたでしょうか。デバイスの開発にあたっては、上記のように処理の一部を SORACOM 側にオフロードする方法も選択肢として検討いただけますと幸いです。

また、Ask SORACOM の過去記事はこちらをご覧ください。

それでは、次回もお楽しみに!

― ソラコム加納 (Kanu)