WioLTEを使ったハッカソン開発成果発表

この記事はSORACOM Advent Calendar 2018連携の記事となります

今年の振り返り

皆様こん**は、SORACOM Advent Calendar 2018、12/11担当です。
SORACOM Advent Calendarに参加するのは初めてですが、よろしくお願いします。

まず最初に、もう年末なので振り返りをさせてください。

今年(2018年)は、私にとってはIoT開発に開眼した年となりました。
きっかけは今年1月にソラコム主催のもくもく会に参加したこと。

そこで出会ったのがWioLTEでした。

従来はSORACOM Airの契約をしていたのみで『活用できていないなー』といった感じ、この状況を打開したかったのです。WioLTEはSIMカードを挿すだけでセンサーデータのアップロードなど、クラウドと連携したIoTの仕組みを簡単に開発できることに惹かれました。

その後程なくしてGoogle Homeも入手し、これを絡めたブログ記事を公開しました。

WioLTEでAWS IoT Coreのデバイスシャドウを利用する
クラウドと連携したIoTの仕組みを簡単に開発できる優れものであるWioLTEを購入し学習を進めています。 今回はAWS IoTのデバイスシャドウを用いてLED発光色のステータスを管理する仕組みを作りました。
音声でSORACOM SIMの状態を変更する
スマートスピーカーのGoogle Home(Mini)を購入しました。 クラウドと連携させ、SORACOM SIMの回線状態を音声で変更するアプリケーションを作成してみました。
音声でWioLTEのLED発光色を変更する
Google Homeからの音声操作でWioLTEのLEDの発光色を変更してみます。 (SORACOM UG #10 LT発表内容)
SORACOMの料金を音声で確認する
SORACOMの料金を音声(Google Home)で確認するアプリをActions on Google + Dialogflowで作成してみました。

また、先月にはSORACOM LTE-M Buttonの記事も公開させていただきました。

SORACOM Buttonで会議脱出ボタンをつくる
先日より発売にとなったSORACOM LTE-M ButtonとTwilioサービスを利用して電話を掛ける仕組みをつくりました。 会議脱出ボタンとして活用できます。

4月のSORACOM UG #10のLT登壇をきっかけにコミュニティの人脈も広げることができ、WioLTE購入をきっかけに人生観が変わった2018年といっても差し支えないかと思います。

この場を借りて関係者の皆様には御礼申し上げます。

社内ミニハッカソンについて

ところで今春、同僚から『社内でのミニハッカソン活動を立ち上げるので参加しないか』誘いがあり、同調し参加することになりました。

この活動の特徴をまとめると以下の通り。

  • 平日業務後に行う
  • 月2回と高頻度(1回目:アイデアソン~開発、2回目:ハッカソン)
  • 毎月テーマだけ決めてアイデアは自由
  • 完成することを必須としない
  • チームの順位は付けない

目的は参加メンバー各自の技術力向上(向上心)だと思っているので、一般的なハッカソンとは違い、もくもく会に近い緩い感じで取組んでいます。

私はそこそこ規模の大きい会社勤めをしておりますが、どうしても規模が大きくなると優秀な若手社員が入社しても組織に埋もれてしまい、技術力が伸び悩んでいると感じます。そんな現状を打開する方策の1つだと思っているのです。

なお参加者の指向からテーマはIoTに偏っていることもあり、私が経験していたLambda等AWSでのサーバレスとIoTの開発ノウハウを周囲に広げて仲間を増やしたいですね。

開発成果

さて、今年の代表的な成果物をここで紹介したいと思います。

自動水やりを実現する(7月)

テーマは『AWSからデバイスをコントロールする』で、既出の記事にあるようなDevice Shadowを用いたコントロールがリファレンスとなります。

参加者のアイディアから、ベタな話題ではありますが「植物への自動水やり」にチャレンジしてみることになりました。夏、特に長期の旅行に行く場合は植物への水やりが気になりますよね。

アーキテクチャ

考えたアーキテクチャはこんな感じ

を組み合わせれば何とかなるのではないかと考え、取り組んでみることにしました。

本来であればデバイス(エッジ)にて水分量を判定してポンプを駆動すればいいのですが、学習のためわざわざ水分量のデータをAWS IoT側に送り、LambdaからDevice Shadowを書き換えポンプの駆動を制御しようという構成になります。

水分量の取得

Grove水分センサーは簡単に動きました。
SORACOM Harvestで簡単にデータが可視化できるのはいいですね。

ただseeed社の仕様書では値が0から950で取得できると記載があるのですが、なぜか1800~2000を表示・・・なぜだろう・・・

ポンプの駆動

こちらは大いに難儀することになりました。
そもそも、WioLTEで利用できるモータードライバの選択肢が少ないこと。

結局、Grove互換DCモータードライバを利用することにしました。
こちらはPWMで制御できますので、Qiitaの記事を参考に作成することができました。

結果

何とか結合しLambda側からポンプを起動することができました。
ただ、このポンプはペットボトルから吸い上げるには力が弱く、中途半端でしたが。

どうやらモーター駆動側出力もPWMで電圧制御している模様で、モータースピードを多段階で制御することはできず、モーターON/OFFの制御のみの実現となりました。但しモータードライバ経由でモーター駆動まで実現できるようになり一定の成果だったな、といった感じです。

多数のデバイスを同調させる(8月)

テーマは7月と同じ『AWSからデバイスをコントロールする』でした。
とある男性社員の発案で「多数のデバイスを同調して光らせる」ことを目標としました。

なんでも、アイドルのコンサートにてアイドルへの愛を伝えるために光りものを同調させたいのだとか。多数のデバイスの色が同調して変われば素敵ですよね。

アーキテクチャ

アーキテクチャはこれだけ。

AWS IoT Coreでも実現できそうな気がしますが、メッセージブローカーとしてはAmazon MQを採用してみました。SORACOM Beamとの接続方法はMAXさんの記事を参考にさせていただきました。

結果

私としては音声でWioLTEのLED発光色を変更するの記事で作成したコードを流用でき簡単に実現できました。Amazon MQ側で”Red”とメッセージをPublishすると赤く光り、ちょっと嬉しい(笑)

ちなみにソースコードはこんな感じとなりました。ご参考まで

#include <WioLTEforArduino.h>
#include <WioLTEClient.h>
#include <PubSubClient.h> // https://github.com/knolleary/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)

#define ID "(device name)"
#define IN_TOPIC "colorValue"

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

void callback(char* topic, byte* payload, unsigned int length) {
  SerialUSB.println("### Subscribe");
  char subsc[length];
  for (int i = 0; i < length; i++) subsc[i]=(char)payload[i];
  subsc[length]='\0';
  SerialUSB.println(subsc);

  int r = 0;
  int g = 0;
  int b = 0;

  if (strcmp(subsc,"red") == 0) { r = 255; }
  else if (strcmp(subsc,"green") == 0) { g = 255; }
  else if (strcmp(subsc,"blue") == 0) { b = 255; }
  else if (strcmp(subsc,"yellow") == 0){ r = 255; g = 255; }
  else if (strcmp(subsc,"purple") == 0){ r = 255; b = 255; }
  else if (strcmp(subsc,"lblue") == 0) { g = 255; b = 255; }
  else if (strcmp(subsc,"white") == 0) { r = 255; g = 255; b = 255; }
  else if (strcmp(subsc,"off") == 0) { }
  else {
    SerialUSB.println("Incollect color ID");
    return;
  }
  SerialUSB.print("Change LED color to: ");
  SerialUSB.println(subsc);
  Wio.LedSetRGB(r,g,b);
  SerialUSB.println("### LED Status Sent.");
}

void setup() {
  delay(200);

  SerialUSB.println("");
  SerialUSB.println("--- START ---------------------------------------------------");

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

  SerialUSB.println("### Power supply ON.");
  Wio.PowerSupplyLTE(true);
  delay(500);

  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;
  }

  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(ID)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }
  int qos=0;
  MqttClient.subscribe(IN_TOPIC,qos);
  SerialUSB.println("### Setup completed.");
}

void loop() {
  MqttClient.loop();
}

余談ですが、コンサート会場のような混雑した場所ではWiFiの電波は混信して正常に動作しなさそうです。そんな場所でもLTEを利用するWioLTEであれば混信を回避して一定の動作が期待できそうですね。

動物の状態を可視化する(10月)

テーマは『AWSの分析サービスを利用する』でした。
動物園巡りが趣味という女性社員の『動物が寝ているとがっかりする。起きているかどうかわかるようにしたい』というアイディアを実現してみることになりました。

なお動物の活動状況の分析は既にIoTで実現しており、デザミス株式会社のU-motion等が有名です。(ちなみに、ガイアの夜明け[2017/12/19放送回]は必見ですよ)

アーキテクチャ

当日描いた手書きアーキテクチャ図が良くできているのでそのまま掲載(笑

  • 加速度センサーの値をWioLTEで拾って、SORACOM Beam経由でAWS IoTに送る
    (送る情報は加速度の値[30秒最大]、位置情報[緯度/経度])
  • Timestampと(SORACOM Beamで付与される)IMSIを付加して、生データはDynamo DBに格納
  • 同時に分析用としてElastic Searchに保存するのですが、そのままではTimeStampとIMSIが付与されていないので、改めてAWS IoTにRePublishして後に格納
  • DynamoDBの値を参照するLambda関数にて動物の活動状況をチェックし、来園者用Webページ(S3 Web Hosting)にページを生成

こんな感じです。

私はデバイス側からDynamoDBとElastic Searchにデータを投入するところまでを担当しました。加速度センサーには、Grove IoT Starter Kit付属のものを使いました。

デバイス側ソースコード

加速度データは100ミリ秒ごとに取得し、1分ごとに最大値をアップロードするようにしました。(通信量の削減目的)
また加速度は X,Y,Z 3軸取れるわけですが、絶対値の和としています。

ちなみに以下のソースコードにはDevice Shadowの色データが更新されたらLEDが光るロジックは残しています(未使用)

#include <WioLTEforArduino.h>
#include <WioLTEClient.h>
#include <PubSubClient.h>	// https://github.com/knolleary/pubsubclient
#include <ArduinoJson.h>
#include <stdio.h>
#include <ADXL345.h>

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

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

#define ID                "(device name)"
#define IN_TOPIC          "$aws/things/(device name)/shadow/update/delta"
#define SENSOR_TOPIC      "acceldata"

#define SEND_INTERVAL     (60000)
#define SENSOR_INTERVAL   (100)
#define RECEIVE_TIMEOUT   (10000)

WioLTE Wio;
WioLTEClient WioClient(&Wio);
PubSubClient MqttClient;
ADXL345 adxl; //variable adxl is an instance of the ADXL345 library

void callback(char* topic, byte* payload, unsigned int length) {
  SerialUSB.println("### Subscribe");
  char subsc[length];
  for (int i = 0; i < length; i++) subsc[i]=(char)payload[i];
  subsc[length]='\0';
  SerialUSB.println(subsc);

  // JSON parse
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& root = jsonBuffer.parseObject(subsc);

  if (!root.success()) {
    SerialUSB.println("parseObject() failed");
  }
  else{
    // Change LED color
    const char* ledcolor = root["state"]["LED"];
    SerialUSB.print("Change LED color to: ");
    SerialUSB.println(ledcolor);

    int r;int g;int b;

    char tmp[2];
    strncpy(tmp,ledcolor  ,2);sscanf(tmp,"%x",&r);
    strncpy(tmp,ledcolor+2,2);sscanf(tmp,"%x",&g);
    strncpy(tmp,ledcolor+4,2);sscanf(tmp,"%x",&b);

    Wio.LedSetRGB(r,g,b);
    SerialUSB.println("### LED Status Sent.");  

    // RePublish LED Status
    char data[100];
    sprintf(data,"{\"state\": {\"reported\" : {\"LED\" : \"%s\"}}}",ledcolor);
    MqttClient.publish(OUT_TOPIC, data);
  }
}

void setup() {
  delay(200);

  SerialUSB.println("");
  SerialUSB.println("--- START ---------------------------------------------------");
  
  SerialUSB.println("### I/O Initialize.");
  Wio.Init();
  
  SerialUSB.println("### Power supply ON.");
  Wio.PowerSupplyLTE(true);
  delay(500);

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

  SerialUSB.println("### Accelerometer Initialize.");
  adxl.powerOn();

  //set activity/ inactivity thresholds (0-255)
  adxl.setActivityThreshold(75); //62.5mg per increment
  adxl.setInactivityThreshold(75); //62.5mg per increment
  adxl.setTimeInactivity(10); // how many seconds of no activity is inactive?
 
  //look of activity movement on this axes - 1 == on; 0 == off 
  adxl.setActivityX(1);
  adxl.setActivityY(1);
  adxl.setActivityZ(1);
 
  //look of inactivity movement on this axes - 1 == on; 0 == off
  adxl.setInactivityX(1);
  adxl.setInactivityY(1);
  adxl.setInactivityZ(1);
 
  //look of tap movement on this axes - 1 == on; 0 == off
  adxl.setTapDetectionOnX(0);
  adxl.setTapDetectionOnY(0);
  adxl.setTapDetectionOnZ(1);
 
  //set values for what is a tap, and what is a double tap (0-255)
  adxl.setTapThreshold(50); //62.5mg per increment
  adxl.setTapDuration(15); //625us per increment
  adxl.setDoubleTapLatency(80); //1.25ms per increment
  adxl.setDoubleTapWindow(200); //1.25ms per increment
 
  //set values for what is considered freefall (0-255)
  adxl.setFreeFallThreshold(7); //(5 - 9) recommended - 62.5mg per increment
  adxl.setFreeFallDuration(45); //(20 - 70) recommended - 5ms per increment
 
  //setting all interrupts to take place on int pin 1
  //I had issues with int pin 2, was unable to reset it
  adxl.setInterruptMapping( ADXL345_INT_SINGLE_TAP_BIT,   ADXL345_INT1_PIN );
  adxl.setInterruptMapping( ADXL345_INT_DOUBLE_TAP_BIT,   ADXL345_INT1_PIN );
  adxl.setInterruptMapping( ADXL345_INT_FREE_FALL_BIT,    ADXL345_INT1_PIN );
  adxl.setInterruptMapping( ADXL345_INT_ACTIVITY_BIT,     ADXL345_INT1_PIN );
  adxl.setInterruptMapping( ADXL345_INT_INACTIVITY_BIT,   ADXL345_INT1_PIN );
 
  //register interrupt actions - 1 == on; 0 == off  
  adxl.setInterrupt( ADXL345_INT_SINGLE_TAP_BIT, 1);
  adxl.setInterrupt( ADXL345_INT_DOUBLE_TAP_BIT, 1);
  adxl.setInterrupt( ADXL345_INT_FREE_FALL_BIT,  1);
  adxl.setInterrupt( ADXL345_INT_ACTIVITY_BIT,   1);
  adxl.setInterrupt( ADXL345_INT_INACTIVITY_BIT, 1);

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

  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(ID)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }
  int qos=0;
  MqttClient.subscribe(IN_TOPIC,qos);

  // Send Initialize LED Status
  char *data = "{\"state\": {\"reported\" : {\"LED\" : \"000000\"}}}";
  MqttClient.publish(OUT_TOPIC, data);
  SerialUSB.println("### LED Status Sent.");  
}

void loop() {
  char data[100];
  double xyz[3];
  double ax,ay,az,am,mv=0;
  unsigned long next = millis();

  while (millis() < next + SEND_INTERVAL) { // Read Sensor Data adxl.getAcceleration(xyz); ax = xyz[0]; ay = xyz[1]; az = xyz[2]; // Check Maximum Data am = fabs(ax)+fabs(ay)+fabs(az)-1; if (am > mv) {
      mv = am;
      SerialUSB.print("Maximum Accelate : ");SerialUSB.print(am);SerialUSB.println(" g");
    }

    // Check Subscribe
    MqttClient.loop();
    delay(SENSOR_INTERVAL);
  }

  // Get GeoLocation via LTE
  double lat, lng;
  SerialUSB.println("### Get GeoLocation via LTE.");
  Wio.GetLocation(&lng, &lat);
  SerialUSB.println(lng);
  SerialUSB.println(lat); 

  SerialUSB.println("### SensorData Send.");
  sprintf(data,"{\"accel\":%8.2lf,\"lat\":%lf,\"lng\":%lf}",mv,lat,lng);

  // Send To SORACOM Beam
  MqttClient.publish(SENSOR_TOPIC, data);

  // Send To SORACOM Harvest
  int connectId;
  connectId = Wio.SocketOpen("harvest.soracom.io", 8514, WIOLTE_UDP);
  if (connectId < 0) {
    SerialUSB.println("### ERROR! ###");
  }else{
    Wio.SocketSend(connectId, data);
    Wio.SocketClose(connectId);
  }
}

データの可視化

さて分析用のデータ収集のためにデバイス側の実装を先行して済ませた訳ですが、Elastic Searchが実装されるまでデータが正しいかどうか解らない。
そんなときにSORACOM Lagoonが利用でき、開発メンバーへのデータ共有に大活躍でした。

その状態で可視化のためにWioLTE+加速度センサーを”裸族”で持ち運ぶ日課が始まったのですが、1つ気になる現象が。WioLTEの関数、Wio.GetLocationで取得できる基地局の位置情報が地域によって取得できないのです。私の通勤経路周辺では東京23区内しか取得できないようでした。

結果

結局、他開発メンバーの実装と結合し、DynamoDBのデータを分析し S3 Web Hosting経由で値を見せることまで実現することができました。

ただ本来の目的である”データの分析”までは時間切れで検討できず。
今後はそこまで踏み込んでチャレンジしたいですね。

まとめ

我々のハッカソンは時間が無い中で活動しています。
(アイデアソン+ハッカソンで合計5-6時間程度)

そんな中でもWioLTEはプロトタイピング開発に最適で、とにかくアイディアを実現化することに対して最も最短経路で開発できるのが本当に素晴らしいです。
今後も成果が出た際には発表できればと思っていますし、更にハッカソン活動を広げ布教活動(笑)に取り組んで行ければと思っています。
(ちなみに次回の社内のハッカソンのテーマは”SORACOMサービスを使う”です)

seeedの皆様、ソラコムの皆様、来年も期待しています。

コメント