フューチャー技術ブログ

IoTの力でワインの品質を管理する

夏休み自由研究ブログ連載の7本目です。

はじめに

こんにちは。日本ソムリエ協会認定ワインエキスパートの今泉です。
間違えました。TIG/DXユニットの今泉です。

先日、古い年式のワインセラーを我が家に迎えました。新品といえど型落ち品のため、怖いのが故障です。

ワインはとてもデリケートな飲み物で真夏の気温などでは直ぐにダメになってしまいます。また熟成に適した温度もあり、一般的には年間を通じて12~15℃程度で一日の温度変化が少ないほうが良いとされています。1
もし長期間家を空けたタイミングでセラーが壊れてしまったら……………

機械なので故障は仕方ありませんが、なんとかトラブルをすぐに検知する仕組みが欲しいところです。
そこでセラーに温度データロガーを設置することにしましたが、私の要望を満たす市販の製品は非常に高価かつ私にとっては不要な機能も含まれており、選定は難航しました。

ちょうどいい製品がないのであれば作ればよいのです。

ということでアラート機能付きの温湿度データロガーを自作することにしました。

システム構成

API Gateway + Lambda + CloudWatch + DynamoDB

実現したいこととしては以下の通りです。

  1. セラー内部の温湿度を取得
  2. API Gateway経由でLambdaにHTTP POSTし、DynamoDBにデータを蓄積
  3. CloudWatchをトリガーにDBへのデータ更新が行われているか、セラー内部の温度が基準を上回っていないか監視
  4. 3で問題があった場合、LINE messaging APIで自分のLINEアカウントに通知

MQTT & AWSIoTで実現することも考えましたが、すでに宮永さんが素敵な記事を書いているので今回は見送りました。
GoでMQTT!! ~温湿度マイスターbotの作成~(前編)

まずはAPI

まずはクラウド環境の構築ですが、今回使用するLambdaDynamoDBについては特段本記事では取り上げません。
フューチャー技術ブログでは数多く記事が投稿されていますので是非読んでみて下さい。

Lambdaから実行するコードは以下の通りです。
Node.jsでもよかったのですが、最近使い始めたGoに少しでも慣れるためにもGoで書くことにしました。

package main

import (
"encoding/json"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

var tokyo, _ = time.LoadLocation("Asia/Tokyo")

type CellarData struct {
Id int `dynamodbav:"id"`
TimeStamp string `dynamodbav:"timestamp"`
Temperature string `dynamodbav:"temperature"`
Humidity string `dynamodbav:"humidity"`
}

type RequestBody struct {
Temperature string `json:"temperature"`
Humidity string `json:"humidity"`
}

type Response struct {
RequestMethod string `json:"RequestMethod"`
}

func handler(request RequestBody) (events.APIGatewayProxyResponse, error) {

// connect DB
db := dynamodb.New(session.Must(session.NewSession()))

//set Data
cellarData := CellarData{
Id: 1,
TimeStamp: time.Now().In(tokyo).Format("2006-01-02 15:04:05"),
Temperature: request.Temperature,
Humidity: request.Humidity,
}
item, err := dynamodbattribute.MarshalMap(cellarData)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}

// execute
_, err = db.PutItem(&dynamodb.PutItemInput{
TableName: aws.String("CELLAR"),
Item: item,
})
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}

// set response
jsonBytes, _ := json.Marshal(Response{
RequestMethod: "POST",
})
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}

func main() {
lambda.Start(handler)
}

ここで話せるようなポイントはあまりないのですが、POSTのリクエストボディはAPI Gatewayのマッピングテンプレートを活用しました。

ESP-WROOM02(ESP8266)を使った温湿度センサの作成

今回はじめて電子工作にチャレンジしたのですが、どのマイコンボードを選べばいいのか、どういう機材をそろえればいいのか、全く当たりがつきません。
Arduinoが入門者にはお勧めらしいのですが、無線でのインターネット接続を行うには別途Wifi通信モジュールが必要で、その通信モジュール自体もプログラムの読み書きが可能なマイコンとして動作が可能らしく、じゃあもうそれでいいじゃんとなりました。

回路の構築やはんだ付にやや不安があったのである程度構築済みのものを探した結果、以下の製品を利用することにしました。

ESPr® DeveloperはWifiモジュールであるESP-WROOM-02(RSP8266)の開発ボードで、こちらの製品はすでにピンソケットが実装されています。温湿度センサーとしてはBME280とフォトトランジスタを搭載したESPr® Developer用環境センサシールドを利用したのですがこちらもピンヘッダがすでに付いているので手間が大幅に削減できます。

ESP-WROOM02はArduinoと同様、C/C++をベースとしたArduino言語を使用することができるため、キャッチアップなどは不要でプログラミングに取り掛かることができました。
開発環境構築に当たっては以下記事を参考にさせていただきました。

ESP-WROOM-02開発ボードをArduino IDEで開発する方法

ソースコードは次の通りです。

#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <BME280I2C.h>
#include <Wire.h>
#include <ArduinoJson.h>

#define SERIAL_BAUD 115200
#define API_KEY "your api_key"
#define WIFI_SSID "your ssid"
#define PWD "your password"

BME280I2C bme;

void setup()
{
Serial.begin(SERIAL_BAUD);
delay(10);

Serial.println("Connecting to Wi-Fi");
WiFi.begin(WIFI_SSID, PWD);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.println(".");
}
Serial.println("");
Serial.println("Wi-Fi connected");
Serial.println(WiFi.localIP());

Wire.begin();

while (!bme.begin()){
Serial.println("BME280 is not available");
delay(1000);
}

}

void loop()
{
float temp(NAN), hum(NAN), pres(NAN);

BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
BME280::PresUnit presUnit(BME280::PresUnit_Pa);

bme.read(pres, temp, hum, tempUnit, presUnit);

Serial.print("temp: ");
Serial.print(temp);
Serial.print(", humid: ");
Serial.print(hum);
Serial.print(", pressure: ");
Serial.println(pres);

registerSensor(temp,hum);

//30min
ESP.deepSleep(30 * 60 * 1000000 );

}

void registerSensor(float temp,float hum)
{
if (WiFi.status() == WL_CONNECTED) {

HTTPClient http;
WiFiClientSecure client;
const char* path = "your endpoint";
http.setTimeout(500); // 500ms
client.setInsecure();

http.begin(client,path);
//set header
http.addHeader("x-api-key",API_KEY);
http.addHeader("Content-Type", "application/json");

//set requestbody
StaticJsonDocument<JSON_OBJECT_SIZE(20)> jsonArray;
char tempstr[10];
char humstr[10];
dtostrf(temp, 6, 2, tempstr);
dtostrf(hum, 6, 2, humstr);

jsonArray["temperature"] = tempstr;
jsonArray["humidity"] = humstr;

char jsonString[255];
serializeJson(jsonArray,jsonString, sizeof(jsonString));

// post
int httpCode = http.POST((uint8_t *)jsonString, strlen(jsonString));

if (httpCode > 0) {
Serial.printf("[HTTPS] POST... code: %d\n", httpCode);
} else {
Serial.println("[HTTPS] no connection or no HTTP server.");
}
http.end();
}
}

環境センサシールドはI2C通信のみ対応しているため、温湿度の取得はBME280I2Cをimportしました。

コードはBME280ライブラリのサンプルを参考にしています。
組み込み系ではJSON形式にするのは面倒なのでは…?と思っていましたが、ArduinoJsonを利用することで手間なくJson化することができました。

給電

今回、セラー内に設置する必要があるためPCやコンセント以外での電源確保が必要です。

たまたまスマートフォン用の電池交換式充填機があったため使ってみましたが、問題なく稼働しました。
IMG_4153.jpg

長時間駆動させることを考えるとセンサを常に稼働させておくのは望ましくありません。
ESP-WROOM02にはDeepSleepモードがあるため、これを活用して30分間隔で温湿度を取得するようにしました。

//30min
ESP.deepSleep(30 * 60 * 1000000 );

sleepの単位はμ秒のため秒を1000000倍する必要があります。

なお、以下記事によると最大で71.5分ほどスリープが可能なようです。

ESP-WROOM-02のDeep-sleepはどれだけ寝ていられるのか
運用から4日程度経ちますが、今のところ駆動に問題はありません。あまりにも電池の持ちが短かったら別の給電方法を考えたいと思います。

さて、プログラムが組み終わったのでセラーに設置してみます。
ワインセラー

なんだか電子工作感がまるでありませんが気にしないことにします。

通知機能をつける

Lambdaに実装した関数は次の通りです。

package main

import (
"strconv"
"time"

"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"github.com/line/line-bot-sdk-go/linebot"
)

type CellarData struct {
Id int `dynamodbav:"id"`
TimeStamp string `dynamodbav:"timestamp"`
Temperature string `dynamodbav:"temperature"`
Humidity string `dynamodbav:"humidity"`
}

func handler() error {
const threshold = 25.00

// connect DB
db := dynamodb.New(session.Must(session.NewSession()))

result, err := db.Query(&dynamodb.QueryInput{
TableName: aws.String("CELLAR"),
Limit: aws.Int(1),
ScanIndexForward: aws.Bool(false),
ExpressionAttributeNames: map[string]*string{
"#id": aws.String("id"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":id": {
N: aws.String("1"),
},
},
KeyConditionExpression: aws.String("#id = :id"),
})
if err != nil {
return err
}

var cellarData []CellarData
err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &cellarData)
if err != nil {
return err
}

t1, _ := time.Parse("2006-01-02 15:04:05", cellarData[0].TimeStamp)
t2 := time.Now().Add(time.Hour + 7)

f, _ := strconv.ParseFloat(cellarData[0].Temperature, 64)
if f > threshold {
return toNotify("セラーの現在の温度は" + cellarData[0].Temperature + "度です")
}
if t2.After(t1) {
return toNotify(cellarData[0].TimeStamp + "以降更新がありません")
}
return nil
}

func toNotify(str string) error {
const LINE_BOT_CHANNEL_SECRET = "your channel"
const LINE_CHANNEL_ACCESS_TOKEN = "your token"

bot, err := linebot.New(LINE_BOT_CHANNEL_SECRET, LINE_CHANNEL_ACCESS_TOKEN)
if err != nil {
return err
}

message := linebot.NewTextMessage(str)
if _, err := bot.BroadcastMessage(message).Do(); err != nil {
return err
}
}

func main() {
lambda.Start(handler)
}

データの取得はQueryInputで最新のレコードを1件のみ取得しています。
2時間以上、更新がなければ通知するようにしたかったのですが、DBにはJSTで時刻が登録されているもののtimestamp型にparseする際、時刻はそのままにUTCに戻ってしまってしまうため、今回は 9(時差) -2 =7で現在時刻をずらして無理やり比較しました。(なにをしてるのか意味不明かと思い補足)

LINE messaging API

今回初めて利用しましたが、非常に簡単に実装することができました。

LINE DevelopersでMessaging APIのチャネルを作成し、払い出されたLINEシークレットとトークンを設定するのみでBotが作成することができます。
通知には友達登録をしている人全員にメッセージを送信するBroadcastMessageを使用します。

実際にテストしてみた結果です。
LINEからの通知

これで夏場でも安心して外出できるようになりました。

おわりに

今回の仕組みを導入し終えてから1本ワインを開けましたが、いつもより美味しく感じました。

これは新しいワインの楽しみ方を見出してしまったかもしれません……。

さて、実際に通知が来てしまった場合ですが、外出先であればとんぼ返りして、セラー内のワインを冷蔵庫に退避させるか、セラー内に氷を置いて温度を下げるなどの対応が必要になります。自分で実装した手前、通知が来てほしい気持ちがありつつも活躍する場面がないと嬉しいですね。複雑です。

案外手軽に実装することができたので、次はデータだけは蓄積できている湿度2も活用して、より厳格な品質管理にチャレンジしてみたいと思います。


  1. 1.より厳格にはブルゴーニュ地方のもので11~12℃、ボルドー地方のもので13~15℃、カルフォルニアで14~18℃程度が望ましく、どの産地のものを保管するかによって温度をコントロールするのが良いです。
  2. 2.湿度は70~75%程度が望ましいとされています。