MFT2017 ゴミ箱残量モニタリングのWio LTE

Seeedで開発を担当している松岡です。
年末はイベントやワークショップで出張三昧、年始は深センのSeeedへ出張とバタバタしていましたが、ようやく落ち着いてきました。

今回はMaker Faire Tokyo 2017で発売前にデモンストレーションしていた、Wio LTEの実装を紹介したいと思います。

Seeedブースの様子

Seeedは、新発売のGrove Inventor Kit for micro:bit、発売前のGrove Zero、FusionPCBサービスPropagateサービスなどを展示・紹介していました。(写真からは分かりにくいですが)このブースの一番右端で、Wio LTEを使ってゴミ箱残量モニタリングするデモンストレーションしていました。

ゴミ箱残量モニタリング

左の写真が改造したゴミ箱、右の写真がモニタリング画面です。モニタリング画面でゴミの残量(Capacity)とゴミ箱の蓋の開閉状態(Lid)をリアルタイムに表示しています。また、ゴミの残量に応じて、地図上のマークの色が3段階に変化することで、パッと見で満タンかどうか分かるようにしました。実は、画面下部の地図は実装する時間が無くてハメコミ画像で済ませていました。

ゴミ箱の改造

ゴミ箱で取るべき情報はゴミ残量蓋の開閉でしたので、Wio LTEにGrove – 超音波センサーGrove – 加速度センサーを接続して、超音波センサーの距離測定でゴミ残量を算出、加速度センサーの重力方向の角度で蓋の開閉を検知することにしました。ゴミ箱に穴あけて、Groveモジュールを両面テープで貼り付けしてGroveケーブルでWio LTEに接続。とっても簡単。
調子に乗ってWio LTEを固定するためにアクリルの土台を作ったのですが、アクリルをレーザーでカット、ホットプレートで曲げ、ネジ部はタップ立てと、ちょっとやり過ぎでした。ここまでやる必要は無かったですね。
電源は展示途中で電池切れが怖かったので、AnkerのUSB電源から長いUSBケーブルでUSBコネクターに接続して給電にしました。

(写真は後日USBモバイルバッテリーに変更して撮影したものです。)

Wio LTEからモニタリングまでの通信経路

Wio LTEからモニタリング画面までは、SORACOM AirとSORACOM FunnelでAzure Event Hubへ転送して、モニタリング画面へ通知するようにしました。Azure Event HubからAzure Stream Analytics、Power BIも試してみましたが、社内から「モニタリング画面の見栄え大事」という強い声があり、モニタリング画面はWPFアプリケーションで作って、Azure Event Hubからデータ取得するように変更しました。

Wio LTEのプログラム

Wio LTEが誤動作をしているように見えないよう、Wio LTEのプログラムにはいくつかの実用的なコードを加えました。

1. センサーのノイズ、例外値の除去

センサーからは様々な理由でノイズ、例外値が含まれることが多いので、アプリケーションに合わせてコードで除去します。

距離センサーは値がばらつくということはなく、距離が測れないときに極端に大きな値が返ってきたので、そのようなときは最後に範囲内で取れた値を使うようにしました。

if (RangeInCentimeters < 0 || 200 < RangeInCentimeters) {
return LastDistance;
}
LastDistance = RangeInCentimeters;
return RangeInCentimeters;

加速度センサーは角度がばらつく感じだったので移動平均しました。

// キューイング
for (int i = 0; i < LID_AVERAGE_COUNT – 1; i++) {
LidDegrees[i] = LidDegrees[i + 1];
}
LidDegrees[LID_AVERAGE_COUNT – 1] = degree;
// 平均値を算出
int degreeSum = 0;
for (int i = 0; i < LID_AVERAGE_COUNT; i++) {
degreeSum += LidDegrees[i];
}
degree = degreeSum / (int)LID_AVERAGE_COUNT;

2. 通信量の削減

ゴミの残量をリアルタイムに伝えたいが、1秒ごとに送信する方法だと通信コストが高くつきます。そこで、蓋が開→閉や閉→開に変化したタイミングで送信するようにしました。開→閉や閉→開を検知した瞬間だと蓋が閉まりかけている途中などで安定してゴミ残量を測れない可能性があるので、検知してから1秒後(WAIT_TIME)に送信としました。

if (LastLidClosed != lidClosed) {
StartTicks = millis() + WAIT_TIME;
}
LastLidClosed = lidClosed;

if (millis() >= StartTicks) {

3. 定期的に通信

「通信量」の削減と相反するのですが、Wio LTEがきちんと動作しているか否かを遠隔で確認できるよう、蓋の開閉が無い場合は1分ごとに送信するようにしました。

StartTicks += SEND_INTERVAL;

4. 動作状況をLED表示

開発途中は、SerialUSBを使ってUSB接続したパソコンのシリアルモニタで動作状況が把握できますが、展示のときはWio LTEそれぞれにパソコンを接続するわけにはいきません。そこで、プログラムの動作状況をフルカラーLEDで表示しました。初期化は緑、測定は青、送信は赤で点灯します。

Wio.LedSetRGB(COLOR_MEASURE);

最後に

ゴミ箱残量モニタリングの紹介、いかがでしたでしょうか。
Maker Faire Tokyo 2017の2日間のうち、来場者が出入りする日中は連続稼働させていたのですが、電波混み混みでWi-FiやBluetoothが通信トラブル発生している環境でも、ノントラブル・ノンストップで動き続けていました。(この詳細はまた機会があれば書きたいと思います)

Wio LTEの登場でセンサーの接続やLTE通信を簡単にしていますが、次のフェーズにステップアップするには使用してるセンサーや状況、アプリケーションに合わせたプログラミングが必要と考えています。今後も実用的な事例を紹介していきたいと思います。

Wio LTEプログラム全文

#include <WioLTEforArduino.h>
#include <ADXL345.h>
#include <stdio.h>

#define LID_AVERAGE_COUNT (5)
#define ULTRASONIC_RANGER_PIN (20)
#define FULL_LENGTH (12)
#define EMPTY_LENGTH (60)
#define WAIT_TIME (1000)
#define SEND_INTERVAL (60000)

#define COLOR_SETUP 0, 10, 0
#define COLOR_MEASURE 0, 0, 10
#define COLOR_SEND 10, 0, 0
#define COLOR_NONE 0, 0, 0

WioLTE Wio;
int ConnectId;
unsigned long StartTicks;

////////////////////////////////////////////////////////////////////////////////////////
//

ADXL345 Adxl;
int LidDegrees[LID_AVERAGE_COUNT];

void LidInit()
{
for (int i = 0; i < LID_AVERAGE_COUNT; i++) {
LidDegrees[i] = 0;
}

Adxl.powerOn();
}

bool LidClosed()
{
// 加速度センサーから蓋の角度を取得
int x;
int y;
int z;
Adxl.readXYZ(&x, &y, &z);
int degree = (int)(atan2(z, x) * 180 / 3.14);

// キューイング
for (int i = 0; i < LID_AVERAGE_COUNT – 1; i++) {
LidDegrees[i] = LidDegrees[i + 1];
}
LidDegrees[LID_AVERAGE_COUNT – 1] = degree;

// 平均値を算出
int degreeSum = 0;
for (int i = 0; i < LID_AVERAGE_COUNT; i++) {
degreeSum += LidDegrees[i];
}
degree = degreeSum / (int)LID_AVERAGE_COUNT;

return degree >= 45 ? true : false;
}

////////////////////////////////////////////////////////////////////////////////////////
//

double LastDistance = 0;

double MeasureInCentimeters(int _pin)
{
// Trigger
pinMode(_pin, OUTPUT);
digitalWrite(_pin, LOW);
delayMicroseconds(2);
digitalWrite(_pin, HIGH);
delayMicroseconds(5);
digitalWrite(_pin,LOW);

// Duration
pinMode(_pin,INPUT);
// long duration;
double duration;
// duration = pulseIn(_pin,HIGH);
while (digitalRead(_pin)) ;
while (!digitalRead(_pin)) ;
unsigned long t = micros();
while (digitalRead(_pin)) ;
duration = micros() – t;

// Calculate
// int RangeInCentimeters;
double RangeInCentimeters;
RangeInCentimeters = duration/29/2;

if (RangeInCentimeters < 0 || 200 < RangeInCentimeters) {
return LastDistance;
}

LastDistance = RangeInCentimeters;
return RangeInCentimeters;
}

////////////////////////////////////////////////////////////////////////////////////////
//

void setup() {
delay(200);

SerialUSB.println(“”);
SerialUSB.println(“— START —————————————————“);

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

Wio.LedSetRGB(COLOR_SETUP);

SerialUSB.println(“### Power supply ON.”);
Wio.PowerSupplyLTE(true);
Wio.PowerSupplyGrove(true);
delay(5000);

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

SerialUSB.println(“### Connect the \”soracom.io\”.”);
delay(5000);
if (!Wio.Activate(“soracom.io”, “sora”, “sora”)) {
SerialUSB.println(“### ERROR! ###”);
return;
}

SerialUSB.println(“### Open.”);
ConnectId = Wio.SocketOpen(“funnel.soracom.io”, 23080, WIOLTE_UDP);
if (ConnectId < 0) {
SerialUSB.println(“### ERROR! ###”);
return;
}

LidInit();
StartTicks = millis() + 500000;

Wio.LedSetRGB(COLOR_NONE);
}

bool LastLidClosed = false;

void loop() {
Wio.LedSetRGB(COLOR_MEASURE);
bool lidClosed = LidClosed();
double distance = MeasureInCentimeters(ULTRASONIC_RANGER_PIN);
int capacity = 100 – (distance – FULL_LENGTH) / (EMPTY_LENGTH – FULL_LENGTH) * 100;
if (capacity < 0) capacity = 0;
if (capacity > 100) capacity = 100;
SerialUSB.print(“distance = “);
SerialUSB.print(distance);
SerialUSB.print(“, capacity = “);
SerialUSB.print(capacity);
SerialUSB.print(“, Lid = “);
SerialUSB.println(lidClosed ? “Closed” : “OPEN”);
Wio.LedSetRGB(COLOR_NONE);

if (LastLidClosed != lidClosed) {
StartTicks = millis() + WAIT_TIME;
}
LastLidClosed = lidClosed;

if (millis() >= StartTicks) {
Wio.LedSetRGB(COLOR_SEND);

char data[100];

SerialUSB.println(“### Send.”);
sprintf(data, “{\”capacity\”:%d,\”lid\”:%d}”, capacity, lidClosed ? 1 : 0);
SerialUSB.println(data);
if (!Wio.SocketSend(ConnectId, data)) {
SerialUSB.println(“### ERROR! ###”);
goto err;
}

SerialUSB.println(“### Receive.”);
int length;
do {
length = Wio.SocketReceive(ConnectId, data, sizeof (data));
if (length < 0) {
SerialUSB.println(“### ERROR! ###”);
goto err;
}
} while (length == 0);
SerialUSB.print(“Receive:”);
SerialUSB.print(data);
SerialUSB.println(“”);

err:
Wio.LedSetRGB(COLOR_NONE);
StartTicks += SEND_INTERVAL;
}

delay(500);
}

////////////////////////////////////////////////////////////////////////////////////////