デジタルIOワープ装置を作る

# はじめに

こちらは[SORACOM Advent Calendar 2022](https://qiita.com/advent-calendar/2022/soracom) 6日目の記事です。
電気信号を離れた箇所に伝えたい時、短い距離なら線を繋げますが、距離が長いと線を繋げていくのは大変です。
そういう時に信号をA地点からB地点にワープさせる装置があれば便利だと思いました。電線そのものをワープさせるのは今のところできませんが、情報だけならいけそうです。

イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/cc522a3b-8c51-0fe7-44c5-86b0ddac5374.png)

# 構成

インターネットに接続できる機器が一組必要です。特にセルラーモデム内蔵の機器であれば、（さほど）場所を問わずに設置できるのでいいですね。
SORACOM IoTストアで購入できる[Wio LTE](https://soracom.jp/store/5301/)は小型でLTEモデムとSIMスロットが内蔵されていて自分自身で回線接続でき、さらにプログラミングできるIOを持っているのでこの用途には最適です。家に2枚あったのでこれを使いましょう。

信号はある程度リアルタイムにやりとりしたいため、[SORACOM Beam](https://soracom.jp/services/beam/)と[AWS IoT Core](https://aws.amazon.com/jp/iot-core/)を組み合わせMQTTで通信することとします。本当はSORACOMだけで完結できる構成を考えたかったのですが、思いつきませんでした。接続先はMQTTであればなんでも良いのですが、使い慣れているためAWS IoTとしました。

ボードのどのIOを使用するかなどの設定は[SORACOM Air メタデータサービス](https://users.soracom.io/ja-jp/docs/air/use-metadata/)を使うと良いでしょう。

簡単な構成図は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/6ace93d5-359f-fdec-f5e2-faa346249e4f.png)

ポイントはAWS IoTに接続するための認証情報やボードの設定などがSORACOMに集約されており、ボード側には持たないようにしているところです。どのボードをどう組み合わせるかをクラウド側で決められるので、作るのが楽なんですよね。また、ボード側にクラウドの認証情報を持たなくて良いので、セキュリティ面での安全性も高いです。SORACOMならではのメリットですね。

# コード

以下のコードをWio LTEに書き込みます。

```c
#include <WioLTEforArduino.h>
#include <WioLTEClient.h>
#include <PubSubClient.h>		// https://github.com/SeeedJP/pubsubclient
#include <stdio.h>

#define APN               "soracom.io"
#define USERNAME          "sora"
#define PASSWORD          "sora"

#define MQTT_SERVER_HOST  "beam.soracom.io"
#define MQTT_SERVER_PORT  (1883)

WioLTE Wio;
WioLTEClient WioClient(&Wio);
PubSubClient MqttClient;

char imsi[16];
char groupId[37];
char pinNames[][4] = { "D38", "D39", "D20", "D19", "A6", "A7", "A4", "A5" };
int pinNumbers[] = { WIOLTE_D38, WIOLTE_D39, WIOLTE_D20, WIOLTE_D19, WIOLTE_A6, WIOLTE_A7, WIOLTE_A4, WIOLTE_A5 };
int pinModes[] = { -1, -1, -1, -1, -1, -1, -1, -1 };
int pinValues[] = { -1, -1, -1, -1, -1, -1, -1, -1 };

void callback(char* topic, byte* payload, unsigned int length) {
  char buf[length + 1];
  for (int i = 0; i < sizeof(buf); i++) buf[i] = payload[i];
  buf[length] = 0;

  for (int i = 0; i < 8; i++){
    if (pinModes[i] == OUTPUT){
      char compTopic[128];
      sprintf(compTopic, "%s/%s", groupId, pinNames[i]);
      if (strcmp(topic, compTopic) == 0){
        int value = atoi(buf);
        char disp[128];
        sprintf(disp, "Write: %s %d", pinNames[i], value);
        digitalWrite(pinNumbers[i], value);
        SerialUSB.println(disp);
      }
    }
  }
}

void getMetadata(const char *url, char *buf, int len){
  memset(buf, 0, len);
  Wio.HttpGet(url, buf, len);
  for (int i = 0; i < len; i++){
    if (buf[i] == 0x0d || buf[i] == 0x0a){
      buf[i] = 0;
    }
  }
}

void getWatchLocal(){
  char buf[1024];
  getMetadata("http://metadata.soracom.io/v1/subscriber.tags.local", buf, sizeof(buf));
  SerialUSB.println(buf);

  char *p1 = &buf[0];
  char *p2 = p1;
  bool breakFlag = false;

  while (true){
    if (*p1 == ',' || *p1 == 0){
      if (*p1 == 0) breakFlag = true;

      *p1 = 0;
      for (int i = 0; i < 8; i++){
        if (strcmp(p2, pinNames[i]) == 0){
          pinModes[i] = INPUT;
          pinMode(pinNumbers[i], INPUT);
        }
      }
 
      if (breakFlag) break;

      p2 = p1 + 1;
    }

    p1++;
  }
}

void getWatchRemote(){
  char buf[1024];
  getMetadata("http://metadata.soracom.io/v1/subscriber.tags.remote", buf, sizeof(buf));
  SerialUSB.println(buf);

  char *p1 = &buf[0];
  char *p2 = p1;
  bool breakFlag = false;

  while (true){
    if (*p1 == ',' || *p1 == 0){
      if (*p1 == 0) breakFlag = true;

      *p1 = 0;
      for (int i = 0; i < 8; i++){
        if (strcmp(p2, pinNames[i]) == 0){
          pinModes[i] = OUTPUT;
          pinMode(pinNumbers[i], OUTPUT);
        }
      }

      if (breakFlag) break;

      p2 = p1 + 1;
    }

    p1++;
  }
}

void setup() {
  delay(200);

  SerialUSB.println("");
  SerialUSB.println("--- START ---------------------------------------------------");
  
  SerialUSB.println("### I/O Initialize.");
  Wio.Init();

  SerialUSB.println("### Power supply Grove.");
  Wio.PowerSupplyGrove(true);
  delay(1000);
  
  SerialUSB.println("### Power supply LTE.");
  Wio.PowerSupplyLTE(true);
  delay(1000);

  SerialUSB.println("### Turn on or reset.");
  if (!Wio.TurnOnOrReset()) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  SerialUSB.println("### Connecting to \""APN"\".");
  if (!Wio.Activate(APN, USERNAME, PASSWORD)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  char buf[1024];

  getMetadata("http://metadata.soracom.io/v1/subscriber.imsi", buf, sizeof(buf));
  strncpy(imsi, buf, sizeof(imsi));
  SerialUSB.println(imsi);
  delay(1000);

  getMetadata("http://metadata.soracom.io/v1/subscriber.groupId", buf, sizeof(buf));
  strncpy(groupId, buf, sizeof(groupId));
  SerialUSB.println(groupId);
  delay(1000);

  getWatchLocal();
  delay(1000);

  getWatchRemote();
  delay(1000);

  SerialUSB.println("### Connecting to MQTT server \""MQTT_SERVER_HOST"\"");
  MqttClient.setServer(MQTT_SERVER_HOST, MQTT_SERVER_PORT);
  MqttClient.setCallback(callback);
  MqttClient.setClient(WioClient);
  if (!MqttClient.connect(imsi)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  char subscribeTopic[128];
  sprintf(subscribeTopic, "%s/+", groupId);
  MqttClient.subscribe(subscribeTopic);

  SerialUSB.println("### Setup completed.");
}

void loop() {
  char data[128];
  char topic[128];
  char disp[128];
  
  for (int i = 0 ; i < 8; i++){
    if (pinModes[i] == INPUT){
      int value = digitalRead(pinNumbers[i]);
      if (pinValues[i] != value){
        pinValues[i] = value;
        sprintf(topic, "%s/%s", groupId, pinNames[i]);
        sprintf(data, "%d", value);
        sprintf(disp, "Publish: %s %s", topic, data);
        SerialUSB.println(disp);
        MqttClient.publish(topic, data);
      }
    }
  }

  MqttClient.loop();
}
```

Wio LTEには汎用IOとして利用できるポートが8つあり、これをlocal、remoteのいずれかとして割り当てます。
localはボードのIOを監視して、変化があったらMQTTでpublishするポート、remoteはsubscribeしたデータを出力するポートです。
ボードAのD38をlocal、ボードBのD38をremoteとすると、ボードAのD38に入力したらボードBのD38から出てくることになります。
remoteとlocalの割り当ては[SORACOM Air メタデータサービス](https://users.soracom.io/ja-jp/docs/air/use-metadata/)のタグを利用します。メタデータは一括で取得することもできますが、JSONをパースするのが大変な場合は[クエリ機能](https://users.soracom.io/ja-jp/docs/air/use-metadata/#%e3%83%a1%e3%82%bf%e3%83%87%e3%83%bc%e3%82%bf%e3%81%ae%e8%aa%ad%e3%81%bf%e5%8f%96%e3%82%8a%e4%be%8b-%e3%82%af%e3%82%a8%e3%83%aa%e6%a9%9f%e8%83%bd%e3%82%92%e4%bd%bf%e7%94%a8%e3%81%99%e3%82%8b)を使うと取得したい部分だけを取得することができます。
例えばlocalタグを取得したい場合は以下のURLにて取得できます。

`http://metadata.soracom.io/v1/subscriber.tags.local`

地味に便利な機能なのでおすすめです。

MQTT通信には[PubSubClient](https://github.com/SeeedJP/pubsubclient)を利用します。
publish、subscribeするトピックとして、グループIDを使っています。同じグループに属するボード間でデータのやりとりができるようにするためです。

LTE接続し、MQTT接続すると、あとはローカルポートをチェックし変更があったらpublishする、subscribeしたtopicからのデータを指定のポートから出力するだけです。

# 設定

SORACOM Beam経由でAWS IoT Coreに接続できれば良いです。以下の記事に説明されているので、この通りに設定すれば良いでしょう。(AWS IoTの画面はかなり変わってしまっています)

[multi credentials per group 機能を利用して AWS IoT に接続する](https://users.soracom.io/ja-jp/docs/beam/multi-credentials/)
(デバイスは2つ作り、ポリシーは両方PubSubToAnyTopicを適用します。ステップ3は必要ありません)

デバイスごとにグループを作成してもいいのですが、今のSORACOM Beamは1つのグループで複数のデバイスに対応できるようになっているのですね。

また、メタデータサービスを利用するため、メタデータの読み取りをONにし、local、remoteのタグを設定しましょう。

グループ設定画面にて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/a3eec692-4886-6cc2-5f35-2bca69330346.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/98294b7f-8c18-531e-f853-785486794e44.png)

SIM管理画面にて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/496b5c09-1f6f-004a-ad67-614a30fa1f66.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/41e6a372-dd9a-00af-9c83-435aef39848d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/3dd646f8-a1b4-9d01-3f33-7f3d0ce56acc.png)

# 動作確認

ボードAのD38ポートに[ボタン](https://www.seeedstudio.com/Grove-Button-P.html)を接続しlocalタグをD38に設定し、
ボードBのD38ポートに[ブザー](https://www.seeedstudio.com/Grove-Buzzer.html)を接続しremoteタグをD38に設定します。
これでボタンを押してる間だけ離れた箇所のブザーがなる仕組みとなるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/3edee97e-b65c-388d-4ce3-d830d3b49c28.png)
(写真では電源繋いでませんが、動作させる時は当然電源を繋げましょう)

ボードBを他の部屋に置き動作開始して、ボードAのボタンを押すと、、「ピーッ」鳴りました！成功です。

流石にノータイムで鳴らせるわけではないみたいですね。1秒程度のタイムラグはありそうです。
また、カチカチと高速でボタンを入り切りすると、その全てには反応しませんでした。データは届いているようなので、まとめて処理されたところブザーがなる時間がなかったのかもしれません。

今回はわかりやすくボタンとブザーでしたが、中身的にはIOを繋いでいるだけなので、磁気スイッチ+LEDの組み合わせでもいいですし、異常検知出力+映像記録開始入力とかでもいいと思います。今デジタルIOを有線で繋いでいるものであれば対応可能なはずです。

# 色々発展できる？

Q. 1対1以外にも使える？
A. 使える。デバイスはそれぞれMQTT接続しているだけでお互いを認識しているわけではないので3つ以上のボードでも使用可能。同じポートに複数のlocalが設定されていると、後に信号が変化した方が採用されるので、先に変化した方が無視されるのであまりよくない。基本的には1つのlocalを複数のremoteに繋げる方法になる。

Q. シリアル通信はできる？
A. 一応できるけど厳密なタイミングが取れないからSPIなどクロックとタイミングを合わせて転送する方法や、UARTなど速度を一定に保って送信しないといけないものはそのままでは対応できない。シリアル通信の1バイトやある程度まとまったパケットをMQTTに乗せて、remote側で再現する、とかならできそう。

Q. アナログ信号転送はできる？
A. インターネットのパケット通信を通しているので、アナログをアナログのまま飛ばすことはできない。
一旦デジタル信号に変換する必要があるが、Wio LTEにはAD変換(アナログからデジタルへの変換)機能はあるけどDA変換(デジタルからアナログへの変換)がないので、このハードウェア構成では実現できない。AD、DAの両方ができるボードがあれば実現は可能。
ただ、デジタルは変化があった時だけ送るとすれば(変化が少なければ)データ量もそこそこ少なくなるけど、アナログ信号をちゃんと再現しようとするとそこそこ速いサンプリング(数十kHz〜数MHzくらい？信号の性質による)が必要になり、それを全部飛ばすとデータ量がかなり多くなりそう。

# おわりに

デジタル信号IOをワープさせる装置を作ってみました。
なんかAに入れるとBから出てくるので、このペアは繋がってるとみなしてお手軽に使えると思います。
まあPCとかスマホとか使えばもっと高度なやりとりができるわけですが、このくらい単純な方が使える状況もあるのではないかと。

そのうち物質そのものをワープさせる技術とか出てこないかな〜、と思う今日この頃です。
明日は@0x6bさんです。よろしくお願いします！