SORACOM Users

機器や環境に依存する設定情報を外出しするアーキテクチャ例

デバイス毎に必要な設定情報をデバイスの外部に保存し、デバイスの起動時などをきっかけに設定情報を取得し適用するアーキテクチャです。

このアーキテクチャによりデバイスの生産時において個体差を無くすことができるため、出荷前の初期設定の削減が期待できるほか、設定情報をクラウド上に保存することから、出荷後における設定変更も容易に実現できます。

機器や環境に依存する設定情報を外出しするアーキテクチャ例

何が解決できるのか?

本アーキテクチャは以下の要件を解決します。

アーキテクチャ概要

本アーキテクチャは「設定情報の保存先」と「設定情報を取得する際のキー」「通信回線」の3つの要素を利用し、通信回線を通じてキー情報を基に設定情報を入手してデバイスに適用するものです。

SORACOM を利用した場合

本アーキテクチャを SORACOM で構成した場合は「設定情報の保存先」にSORACOM Air メタデータサービスを、「設定情報を取得する際のキー」にはSORACOM IoT SIMを利用し、通信回線(IoT データ通信 SORACOM Air)を通じてタグのデータを得る構成が可能です。
設定情報の取得と適用のタイミングには、デバイスの起動時やボタンなどの割り込みがあります。詳しくは設定情報の取得と反映タイミングをご覧ください。

本アーキテクチャの動作

SORACOM では SORACOM プラットフォームを通じて通信回線(SIM)を識別できます。そして、通信回線ごとに「タグ」と呼ばれる管理情報を付与することができますが、メタデータサービスはそのタグを読み書きができる機能です。SORACOM Air を使用しているデバイスからは http://metadata.soracom.io/v1/subscriber というアドレスを通じて、その通信回線で利用しているSIMに割り当てたタグの操作ができます。また、3G/LTE 通信自体が暗号化されているため上記アドレスへのアクセスする際の暗号化は不要です。

実装例

SIMに割り当てられた “config” というキーのタグから値を読み出し、解析するところまでをそれぞれ実装しています。
値は JSON で以下のように書かれているとしています。

{"device_id": "IOT1", "interval": 5000, "sensors": {"led": 1, "temp": 0, "humi": 0, "gps": 1}} 

ここからはデバイス毎の実装例を解説します。

Raspberry Pi (Linux の systemd + Python3)

Raspberry Pi OS 起動時に実行するように systemd を利用しています。実際の取得を担当するのは Python スクリプトです。

/usr/local/sbin/get_metadata.py

# Device DI pattern using SORACOM Air metadata service for Linux
# 
# Copyright (c) 2020 SORACOM, INC.
# This software is released under the MIT License.
# http://opensource.org/licenses/mit-license.php

import urllib.request

def get_tag_value_of(tag_key_name):
  """Get value of tag from SORACOM metadata service.

  Args:
    tag_key_name (str):  The name of the tag to retrieve.

  Returns:
    str: Raw value in tag.

  Examples:
    >>> get_tag_value_of("config")
    b'{"device_id": "IOT1", "interval": 5000, "sensors": {"led": 1, "temp": 0, "humi": 0, "gps": 1}}\n'

  Note:
    An environment that can access the SORACOM Air metadata service is required.
    For example, there is a way to use a USB dongle + SORACOM IoT SIM.
  """
  req = urllib.request.Request("http://metadata.soracom.io/v1/subscriber.tags.{}".format(tag_key_name))
  with urllib.request.urlopen(req) as res:
    body = res.read()
  return body

if __name__ == '__main__':

  import json

  # Fetch value of metadata from SORACOM metadata service
  body = get_tag_value_of("config")
  print(body)

  # Parse JSON
  config = json.loads(body)
  print(config["device_id"])
  print(config["interval"])
  print(config["sensors"]["gps"])

/etc/systemd/system/get_metadata.service

[Unit]
Description = Fetch and Apply configuration from SORACOM Metadata service
After = network.target

[Service]
Type = simple
RemainAfterExit = yes
ExecStart = /usr/bin/python3 /usr/local/sbin/get_metadata.py

[Install]
WantedBy = multi-user.target

インストール

$ sudo systemctl daemon-reload
$ sudo systemctl enable get_metadata.service

動作の様子

起動後に以下を確認してみます。

$ sudo systemctl status get_metadata.service
● get_metadata.service - Fetch and Apply configuration from SORACOM Metadata service
   Loaded: loaded (/etc/systemd/system/get_metadata.service; enabled; vendor preset: enabled)
   Active: active (exited) since Fri 2020-12-11 00:59:14 JST; 6s ago
  Process: 705 ExecStart=/usr/bin/python3 /usr/local/sbin/get_metadata.py (code=exited, status=0/SUCCESS)
 Main PID: 705 (code=exited, status=0/SUCCESS)

Dec 11 00:59:14 rpi4 systemd[1]: Started Fetch and Apply configuration from SORACOM Metadata service.
Dec 11 00:59:15 rpi4 python3[705]: b'{"device_id": "IOT1", "interval": 5000, "sensors": {"led": 1, "temp": 0, "humi": 0, "gps": 1}}\n'
Dec 11 00:59:15 rpi4 python3[705]: "IOT1"
Dec 11 00:59:15 rpi4 python3[705]: 5000
Dec 11 00:59:15 rpi4 python3[705]: 1

Wio LTE JP Version

Wio LTE JP Version での実装例です。
HTTP アクセスには Wio LTE JP Version のライブラリを、 JSON 解析には ArduinoJson を利用しています。

/*
 * Device DI pattern using SORACOM Air metadata service for Wio LTE JP Version
 * 
 * Copyright (c) 2020 SORACOM, INC.
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
*/
#define SerialMon SerialUSB

#include <WioLTEforArduino.h>
WioLTE Wio;

#include <ArduinoJson.h>
DynamicJsonDocument doc(192); // Generated by https://arduinojson.org/v6/assistant/

/**
 * @brief      Get value of tag from SORACOM metadata service.
 * @param[in]  tag_key_name   The name of the tag to retrieve.
 * @param[in]  Wio            The instance of Wio.
 * @param[in]  default_value  Value to return in case of 404 or 403.
 * @return     Raw value in tag or default_value.
 * @par        Examples:
 *             String body = get_tag_value_of("config", &Wio);
 * @note       An environment that can access the SORACOM Air metadata service is required.
 *             For example, there is a way to use a USB dongle + SORACOM IoT SIM.
 */
String get_tag_value_of(const char *tag_key_name, WioLTE *wio, const char *default_value = "") {
  char url[255];
  sprintf(url, "http://metadata.soracom.io/v1/subscriber.tags.%s", tag_key_name);
  char buf[1024];
  wio->HttpGet(url, buf, sizeof(buf));
  String body = String(buf);
  // Wio.HttpGet cannot read header.
  if (body == "Specified key does not exist." || /* == 404 */
      body == "You Ware not allowed to access Metadata Server.") { /* == 403 */
    body = String(default_value);
  }
  return body;
}

void setup() {
  delay(1000);
  SerialMon.println("");
  SerialMon.println("--- START ---------------------------------------------------");

  SerialMon.println("### I/O Initialize.");
  Wio.Init();
  SerialMon.println("### Power supply ON.");
  Wio.PowerSupplyLTE(true);
  delay(500);
  SerialMon.println("### Turn on or reset.");
  if (!Wio.TurnOnOrReset()) {
    SerialMon.println("### ERROR! ###");
    return;
  }
  SerialMon.println("### Connecting...");
  if (!Wio.Activate("soracom.io", "sora", "sora")) {
    SerialMon.println("### ERROR! ###");
    return;
  }
  SerialMon.println("### Setup completed.");

  // Fetch value of metadata from SORACOM metadata service
  String body = get_tag_value_of("config", &Wio);
  SerialMon.println(body);

  // Parse JSON
  DeserializationError err = deserializeJson(doc, body);
  if (err) {
    SerialMon.print(F("deserializeJson() failed: "));
    SerialMon.println(err.c_str());
    return;
  }
  String c1 = doc["device_id"];
  SerialMon.println(c1);
  long n2 = doc["interval"];
  SerialMon.println(n2);
  long n3 = doc["sensors"]["gps"];
  SerialMon.println(n3);

  SerialMon.println("### done.");
}

void loop() {
  // Your impl.
}

動作の様子

--- START ---------------------------------------------------
### I/O Initialize.
### Power supply ON.
### Turn on or reset.
### Connecting...
### Setup completed.
{"device_id": "IOT1", "interval": 5000, "sensors": {"led": 1, "temp": 0, "humi": 0, "gps": 1}}

IOT1
5000
1
### done.

M5Stack や Arduino UNO (TinyGSM)

M5Stack 用 3G 拡張ボードLTE-M Shield for Arduino では 3G/LTE 通信に TinyGSM を、 HTTP アクセスに ArduinoHttpClient を、 JSON 解析には ArduinoJson を利用します。

※ Arduino UNO + LTE-M Shield for Arduino においては、実行時メモリ不足になるため ArduinoJson による JSON 解析は行わないようにしています。

/*
 * Device DI pattern using SORACOM Air metadata service for M5Stack(with 3G ext. board)/Arduino UNO(with LTE-M Shield for Arduino)
 * 
 * Copyright (c) 2020 SORACOM, INC.
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
*/
#define SerialMon Serial

#ifdef ARDUINO_M5Stack_Core_ESP32
  #include <M5Stack.h>
  #include <HTTPClient.h> /* Why? see https://qiita.com/ma2shita/items/97bf1a0c3158b848019a */
#endif

#ifdef ARDUINO_M5Stack_Core_ESP32
  #define SerialAT Serial2 // `Serial2` is 3G Extension board for M5Stack Basic/Gray
  #define TINY_GSM_MODEM_UBLOX
#elif defined(ARDUINO_AVR_UNO)
  #include <SoftwareSerial.h>
  SoftwareSerial SerialAT(10, 11); // for LTE-M Shield for Arduino with Arduino UNO
  #define TINY_GSM_MODEM_BG96
#endif

#include <TinyGsmClient.h>
TinyGsm modem(SerialAT);
TinyGsmClient socket(modem);

#include <ArduinoHttpClient.h>

#ifdef ARDUINO_M5Stack_Core_ESP32
  #include <ArduinoJson.h>
  DynamicJsonDocument doc(192); // Generated by https://arduinojson.org/v6/assistant/
#endif

/**
 * @brief      Get value of tag from SORACOM metadata service.
 * @param[in]  tag_key_name   The name of the tag to retrieve.
 * @param[in]  socket         The instance of TinyGsmClient.
 * @param[in]  default_value  Value to return in case of 404 or 403.
 * @return     Raw value in tag or default_value.
 * @par        Examples:
 *             String body = get_tag_value_of("config", &Wio);
 * @note       An environment that can access the SORACOM Air metadata service is required.
 *             For example, there is a way to use a USB dongle + SORACOM IoT SIM.
 */
String get_tag_value_of(const char *tag_key_name, TinyGsmClient *socket, const char *default_value = "") {
  char path[255];
  sprintf_P(path, PSTR("/v1/subscriber.tags.%s"), tag_key_name);
  HttpClient http = HttpClient(*socket, "metadata.soracom.io", 80);
  http.get(path);
  int rc = http.responseStatusCode();
  String body = http.responseBody();
  http.stop();
  if (rc != 200) body = String(default_value);
  return body;
}

void setup() {
  delay(1000);
  SerialMon.begin(115200);
  SerialMon.println("");
  SerialMon.println("--- START ---------------------------------------------------");

#ifdef ARDUINO_M5Stack_Core_ESP32
  M5.begin();
  SerialAT.begin(115200, SERIAL_8N1, 16, 17);
#elif defined(ARDUINO_AVR_UNO)
  SerialAT.begin(9600);
#endif

  SerialMon.println(F("modem.restart()"));
  modem.restart();
  SerialMon.println(F("waitForNetwork()"));
  while (!modem.waitForNetwork()) SerialMon.print(".");
  SerialMon.println(F("gprsConnect(soracom.io)"));
  modem.gprsConnect("soracom.io", "sora", "sora");
  SerialMon.println("### Setup completed.");

  // Fetch value of metadata from SORACOM metadata service
  String body = get_tag_value_of("config", &socket);
  SerialMon.println(body);

#ifdef ARDUINO_M5Stack_Core_ESP32
  // Parse JSON
  DeserializationError err = deserializeJson(doc, body);
  if (err) {
    SerialMon.print(F("deserializeJson() failed: "));
    SerialMon.println(err.c_str());
    return;
  }
  String c1 = doc["device_id"];
  SerialMon.println(c1);
  long n2 = doc["interval"];
  SerialMon.println(n2);
  long n3 = doc["sensors"]["gps"];
  SerialMon.println(n3);
#endif

  SerialMon.println("### done.");
}

void loop() {
  // Your impl.
}

動作の様子 (M5Stack Basic + M5Stack 用 3G 拡張ボード)


--- START ---------------------------------------------------
modem.restart()
waitForNetwork()
gprsConnect(soracom.io)
### Setup completed.
{"device_id": "IOT1", "interval": 5000, "sensors": {"led": 1, "temp": 0, "humi": 0, "gps": 1}}

IOT1
5000
1
### done.

アーキテクチャの適用ポイントと注意点

本アーキテクチャを適用する際に押さえておきたいポイントと注意点です。

外出しする「設定情報」の見つけ方

「設定情報」は個体毎に異なった設定を行う値です。例えば2台目のデバイスにプログラムを書き込む際に変更する値や情報は、全て設定情報と考えて良いでしょう。実装上では #defineconst 等の定義や定数が外出しの候補となります。

メタデータサービスに保存する設定情報のフォーマット

ここで紹介した設定情報のフォーマットは JSON でしたが、本アーキテクチャの目的は設定情報を読み込み反映させる手段の一つであるため、必ずしも JSON である必要はありません。

フォーマットはデバイス側での処理のしやすさと、性能とのバランスで決定します。例えば、Raspberry Pi のようなハードウェアであれば JSON や XML のような構造化テキストも処理可能ですが、メモリが小さいマイコンでの JSON の取扱いはメモリを圧迫します。バイナリは省メモリで扱える反面、クラウド側(メタデータサービス)での取扱いに適していないので、CSVや固定長テキストといったフォーマットが検討できるでしょう。

構造化されていないフォーマットにおいては、文法や構造のチェックを実装する手間も存在します。これらの要素を加味したうえでフォーマットを決めていきます。

ユーザーデータの利用

SORACOM Air のメタデータサービスには「ユーザーデータ」という領域があります。メタデータはタグとしてSIM毎に異なる値を設定できますが、ユーザーデータはSIMグループ(SIMを束ねたグループ)毎に設定可能ですので、共通する設定情報を保存しておくといった活用方法があります。

詳細はSORACOM Air メタデータサービス機能 / ユーザーデータにアクセスするをご覧ください。

設定情報の取得と反映タイミング

これまで、設定情報の取得と反映はデバイスの起動時として解説してきましたが、実装によってはデバイスの稼働中においても可能です。タイミングをまとめると2つとなります。

  1. デバイス起動時
  2. デバイス稼働中の割り込み時

デバイス起動時における設定情報の取得と反映は、本アーキテクチャにおいての基本実装で、もっとも簡単なものとなります。これに加えて、デバイス稼働中の「割り込み」を利用した設定情報の取得と反映をする方法があります。これには2つの方法があります。

  1. スイッチやボタン等の信号による割り込み
  2. 時間による割り込み

信号による割り込みは、例えば「設定取得ボタン」といった形です。基本的に有人運用向けですが、必要なタイミングで反映ができることから、運用としてもわかりやすい方法となります。時間による割り込みは定時実行とも言われ、「1日に1回、設定情報を取得し反映させる」というものです。無人で運用する際に有用ですが、間隔によっては反映までに時間がかかるという面もあります。

また、割り込み発生時の実装方法も2通り存在します。

  1. デバイスの電源OFF/ON、もしくは再起動を行い、次回の起動時で設定情報の取得と反映をする
  2. 再起動せずに設定情報を取得し、デバイス稼働中でも反映ができるようにする

デバイスの電源OFF/ONや再起動は実装が容易です。特に電源のOFF/ONは複雑な作業が無い事から、手間なく運用できる方法です。一般的には内部メモリが消失することから、保存が必要であれば外部記憶が必要となります。本アーキテクチャは、設定情報を保存しておく仕組みがデバイス上に無くとも個別適用が可能であることがメリットの1つであるため、例えばデータ収集・蓄積サービス SORACOM Harvest Files といったクラウドストレージの利用を組み合わせる事で、本アーキテクチャを活かす事が可能です。

一点注意したいのは、いつ電源がOFFになっても電源ONで復旧できる実装が不可欠です。またソフトウェアリセットによる再起動は、マイコンがリセットできてもセンサーや周辺機器のリセットは別に行う必要があるため、マイコンとセンサーや周辺機器との状態が不一致になり動作に支障をきたす場合もあります。全体的なリセットを行う実装や、電源のOFF/ONを行う運用を行う事、もしくはもう一つの考え方「デバイス稼働中における設定情報の反映」を検討します。

デバイス稼働中における設定情報の反映はデバイスとクラウド間の「双方向通信」とも言われます。利点は内部メモリやセンサー、周辺機器のリセットといった課題を解消できます。
双方向通信を実現するデザインパターンはデバイス-クラウドの双方向通信 デザインパターンと実装として、別途公開していますのでご覧ください。

適用結果のフィードバック先

本アーキテクチャは設定情報を取得して適用する部分にフォーカスを当てて解説していますが、適用の結果を知る方法があるとトラブルシュートに役立ちます。

結果の保存先にもメタデータサービスは利用可能です。
あらかじめメタデータサービスの設定で「読み取り専用」を OFF にした上で metadata.soracom.io/v1/subscriber/update_tags に HTTP POST することで、タグと値を書き込むことができます。

以下の例は config_version4 としてメタデータサービスに書き込んでいます。

$ curl -s -X POST -H "Content-Type: application/json" \
  -d "{\"config_version\":\"4\"}" \
  metadata.soracom.io/v1/subscriber/update_tags

書き込んだ後のタグの状態は以下のようになります。値はキーが存在していれば値は上書き、存在していなければキーと共に新規作成されます。

$ curl -s metadata.soracom.io/v1/subscriber.tags | jq .
{
  "name": "RPi4/MS2131i",
  "config": "{\"inverval\": 5000, \"sensors\": {\"led\": 1, \"temp\": 0, \"humi\": 0, \"gps\": 1}}",
  "config_version": "4"
}

※タグの値は文字形式のみ使用可能です。例えば {"config_version": 10} と値を数値形式にした場合は失敗しますのでご注意ください。

本アーキテクチャ適用時の注意点

設定情報の取得失敗に備えましょう

電波状況などの要因から設定情報の取得を失敗する可能性があるため、その場合の挙動を考慮しておく必要があります。例としては再起動をしつづける、あらかじめデバイス内に設定情報した標準値で起動するといった実装が考えられます。

設定情報を保存しているシステムへのアクセスは分散を検討しましょう

設定情報を保存しているシステム(メタデータサービス)へ短時間でのアクセスが集中すると、システム保護のために Too many requests といった措置がされる場合があります。これは SORACOM のみならずクラウドサービス全般に言えることで、例えば「毎時0分に実行」といった実装は大量のデバイスになった場合に本問題に遭遇する可能性が高まります。また、取得失敗後の再取得タイミングも同じにしてしまうと同様の問題が続くため、ランダムで数秒ずらすといった分散アクセスを検討してください。

この分散に関する実装はexponential backoff(外部サイトAWS様の解説)も参考になります。

#define#ifdef 等によるコンパイル前の設定には利用できません

特にマイコン向けの開発で用いられるC/C++では、プログラム本体ではなくコンパイル前の定義命令(プリプロセッサ)を利用して設定情報を書き込む手法が用いられます。本アーキテクチャは稼働中において動的に適用する仕組みとなっているため、プリプロセッサの挙動を本アーキテクチャで制御する事はできません。

活用事例

本アーキテクチャは GPS マルチユニット SORACOM Edition で活用しています。

GPS マルチユニット SORACOM Edition

GPS マルチユニット SORACOM Edition(以下、GPSマルチユニット)は4種のセンサーを搭載しており、それぞれのセンサーのON/OFFやデータ送信頻度といった設定が可能です。この設定情報はSORACOM ユーザーコンソールの「ガジェット管理」メニューを通じて行われますが、そこで設定された情報はメタデータサービスのユーザーデータに保存されています。

以下は、ガジェット設定で設定された後のユーザーデータの例で、GPS マルチユニットに割り当てられた SIM グループの “SORACOM Air for Cellular 設定” の「ユーザーデータ」で確認できます。GPS マルチユニット内のセンサーのON/OFFや送信頻度といった設定情報が JSON となっています。

ガジェット設定で設定された後のユーザーデータの例

GPSマルチユニットの設定情報の取得・反映のタイミングは、デバイス起動時に加えてボタン押下時、また定期的にも取得して反映されるようになっており、出荷後の設定変更を Web を通じて行えるようになっています。

pagetop