#はじめに
Lambda祭りの参加に若干遅れた感じですが、せっかく作るならやはりIoT/M2M関係で何かしたいなと思い、いろいろ考えてたら時間がかかってしまいました。
2014年5月くらいにRaspberry Piに温度センサ(ADT7410)をつなげてKinesisにあげてリアリタイムモニタリングをするデモを作りました。詳細はこちら(スライド20くらい)この時は単純に温度データをストリームとして上げることをしてました。
今回は、部屋の室温をイベントとしてロジックを動作させる(つまり、Lambda Functionを実行する)ことを考えてみます。
冬になるとお布団からでるのが辛くなりますよね。外気温と室温の差が布団からでることを辛くさせているという仮説(!?)をベースにOpenWeatherMapから外気温を取得します。その外気温と比べて室温が下がっていることを検知してブザーとLCDに情報表示するシステムを作ります。
全体構成
センサデータをクラウドにあげるケースにおいて、センサデバイスから一度、中継デバイス(今回の場合はRaspberry Pi)などにためてから上げることが多いと思うので今回はあえて、Arduinoにセンサ(DS18B20)をつけてUSBで接続し定期的にRaspberry Piに上げるようにしてます。
#Arduino
今回利用したDS18B20からデータ取得するためには、One Wire Protocolを使う必要があります。OneWireライブラリを使えば簡単にデータアクセスができます。ライブラリの取得先はこちら。
#include <OneWire.h>
#include <LiquidCrystal.h>
OneWire ds(9); // on pin 9
void setup(void) {
Serial.begin(9600);
}
void loop(void) {
byte i;
byte present = 0;
byte type_s;
byte data[12];
byte addr[8];
float celsius;
if ( !ds.search(addr)) {
ds.reset_search();
delay(250);
return;
}
if (OneWire::crc8(addr, 7) != addr[7]) {
return;
}
switch (addr[0]) {
case 0x10:
type_s = 1;
break;
case 0x28:
type_s = 0;
break;
case 0x22:
type_s = 0;
break;
default:
return;
}
ds.reset();
ds.select(addr);
ds.write(0x44,1);
delay(1000);
present = ds.reset();
ds.select(addr);
ds.write(0xBE);
for ( i = 0; i < 9; i++) {
data[i] = ds.read();
}
unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3;
if (data[7] == 0x10) {
raw = (raw & 0xFFF0) + 12 - data[6];
}
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3;
else if (cfg == 0x20) raw = raw << 2;
else if (cfg == 0x40) raw = raw << 1;
}
celsius = (float)raw / 16.0;
Serial.print(celsius);
Serial.println();
}
#RaspberryPi
Raspberry Pi側は、Arduinoからデータを取得するロジックとSQSのメッセージを取得してブザーとLCDの表示するロジックの2つを動作させます。Arduinoのロジックは目覚まし代わりにcronで起動することにしました。
ちなみに、My Raspberry Piには、Grovepiをつけてます。
なので、grovepiライブラリを使って、ブザーをならしたり、LCDに表示したりしてます。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import serial
import commands
import os
s = serial.Serial('/dev/ttyACM0', 9600)
line = s.readline()
temp = line.strip()
temp = "{\"temp\":" + str(temp) + "}"
f = open('/tmp/test', 'w')
f.write(temp)
f.close()
res = commands.getoutput("/usr/local/bin/aws lambda invoke-async --function-name ifttt --region
us-east-1 --invoke-args /tmp/test")
os.remove('/tmp/test')
botoがLambda対応してなかったので、awscliを使ってLambdaのinvoke-async APIを発行しました。
import boto
import boto.sqs
import time
import grovepi
from grove_rgb_lcd import *
buzzer = 4
grovepi.pinMode(buzzer,"OUTPUT")
ACCOUNT_ID = '<ACCOUNT ID>'
IDENTITY_POOL_ID = '<IDENTITY POOL ID>'
ROLE_ARN = '<ROLE>'
cognito = boto.connect_cognito_identity()
cognito_id = cognito.get_id(ACCOUNT_ID, IDENTITY_POOL_ID)
token = cognito.get_open_id_token(cognito_id['IdentityId'])
sts = boto.connect_sts()
assumedRoleObject = sts.assume_role_with_web_identity(ROLE_ARN, "bototest", token['Token'])
client_sqs = boto.sqs.connect_to_region(
"us-east-1",
aws_access_key_id=assumedRoleObject.credentials.access_key,
aws_secret_access_key=assumedRoleObject.credentials.secret_key,
security_token=assumedRoleObject.credentials.session_token)
q = client_sqs.get_queue('lamda_ifttt')
while True:
rs = q.get_messages(message_attributes=['weather_temp'])
if len(rs) >= 1:
m = rs[0]
print m.message_attributes['weather_temp']['string_value']
grovepi.digitalWrite(buzzer,1)
time.sleep(1)
grovepi.digitalWrite(buzzer,0)
setText("Weather Info: " + m.message_attributes['weather_temp']['string_value']
)
setRGB(0,128,64)
for c in range(0,255):
setRGB(c,255-c,0)
time.sleep(0.01)
setRGB(0,255,0)
q.delete_message(m)
time.sleep(10)
#Lambda Function
初めてOpenWeathermapのAPIを使ったのですが、登録も必要なく簡単に利用できました。
現在の天気情報を取得をするAPIはこちらをご覧ください。とても簡単です。
取得したデータをもとにSNSを使ってケータイに通知すると共に、Raspberry PiのLCDとブザーを鳴らすためにSQSにキュー突っ込んでます。Nest Stepとして、メッセージにいろんな意味(LCDに表示させるとかブザーならすとか)を持たせようかなと思っております。時間があれば。。
OpenWeatherMapのAPIは安定しており、かつ、今回は、低頻度でのアクセスなので、毎回取得しに行ってますが、天気情報はそれほど変わらないので、キャッシュに入れた方がよいかなと思います。(これもNext Stepですかね。。)
console.log('Loading event');
var http = require('http');
var aws = require('aws-sdk');
exports.handler = function(event, context) {
http.get("http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp", function(res) {
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
var res = JSON.parse(body);
var wheather_temp = Math.floor(parseFloat(res.main.temp)*100 - parseFloat(273.15)*100)/100;
console.log("Wheather temp: " + wheather_temp);
console.log("Rasp temp: " + event.temp);
if((wheather_temp)*2 > parseFloat(event.temp)){
var message = 'Home:' + String(parseFloat(event.temp)) + '/ Outside:' + wheather_temp;
//SNS
var sns = new aws.SNS({params: {TopicArn: 'arn:aws:sns:us-east-1:736384702727:lambda-ifttt'}});
sns.publish({Message: message}, function (err, data) {
if (!err) {
console.log('Message published');
context.done(null,'');
}else{
console.log("Err");
}
});
//SQS
var sqs = new aws.SQS();
var params = {
MessageBody: 'Weather News',
QueueUrl: 'https://sqs.us-east-1.amazonaws.com/736384702727/lamda_ifttt',
DelaySeconds: 0,
MessageAttributes: {
'weather_temp': {
DataType: 'String',
StringValue: String(wheather_temp)
}
}
};
sqs.sendMessage(params, function(err, data) {
if (err){
console.log(err, err.stack);
context.done('error', e);
}else{
console.log(data);
context.done('done');
}
});
}
});
}).on('error', function(e) {
context.done('error', e);
});
};
#最後に
かなり簡単に試すことができました。タイトルにあるIoTのバックエンドサービスには程遠いですが、Lambda及びAWSの各種サービスを使うことでバックエンドサービスを簡単かつスケーラブルに作れる確信を得ましたw さらにいうとLambdaを使うことで、IoTが発行するイベントとクラウドに蓄積されているデータがある状態になった時に何か動作させるといった、iftttエンジンが作れる!と思いました。
コレもNest Stepとして取り組みたいテーマです。機械学習とも組み合わせたいなー。
#免責
こちらは個人の意見で会社とは関係ありません。