Arduino + Alibaba IoT Platform + Linebotで擬似スマートホームを実現する

皆さんこんにちは!HRです。
今回はタイトル通り、AlibabaクラウドのIoTプラットフォームを使って、 擬似のスマートホームを実現してみたいと思います。

実現したいシナリオは以下二つです。
1.LineBotからAlibabaIoTPlatform経由でArduinoのLEDを点滅させる
2.Arduinoの人感センサーがものの動きを検知し、 AlibabaIoTPlatform経由で、LineBotへ通知を送る

では、早速シナリオ1について作成してみたいと思います。 f:id:sbc_hr:20200303173314p:plain
まず今回利用するデバイスはこちらです。
OSOYOOの擬似スマートホームセット。
https://osoyoo.com/2019/10/18/osoyoo-smart-home-iot-learning-kit-with-mega2560-introduction/
メインデバイス:Arduino Uno
WiFiモジュール:ESP8266

他のブログもすでに紹介した通りですが、 ArduinoとAlibabaIoTPlatformと通信のためのプログラムは以下になります。

#include "WiFiEsp.h"
#include "SoftwareSerial.h"
#include "PubSubClient.h"

#define whiteLED 10
#define redLED 12

SoftwareSerial softserial(A9, A8);

char ssid[] = "";
char pass[] = "";

char username[] = "";
char password[] = "";

const char* mqtt_server = "";
const char* pubsub_topic = "";
const char* client_id = "";

int status = WL_IDLE_STATUS;

WiFiEspClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;

void setup() {
  
  pinMode(whiteLED, OUTPUT);
  pinMode(redLED, OUTPUT);
  
  // initialize serial for debugging
  Serial.begin(9600);
  softserial.begin(115200);
  softserial.write("AT+CIOBAUD=9600\r\n");
  softserial.write("AT+RST\r\n");
  softserial.begin(9600);
  
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {
  
  WiFi.init(&softserial);

  // check for the presence of the shield
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue
    while (true);
  }

  // attempt to connect to WiFi network
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network
    status = WiFi.begin(ssid, pass);
  }

  Serial.println("You're connected to the network");
  printWifiStatus();
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  if ((char)payload[0] == '1') {
    digitalWrite(whiteLED, HIGH);
  } else if ((char)payload[0] == '0') {
    digitalWrite(whiteLED, LOW);
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(client_id,username,password)) {
      Serial.println("connected");
      client.subscribe(pubsub_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void loop() {
  long now = millis();
  
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

}

void printWifiStatus()
{
  // print the SSID of the network you're attached to
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());
  
  // print your WiFi shield's IP address
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print where to go in the browser
  Serial.println();
  Serial.print("To see this page in action, open a browser to http://");
  Serial.println(ip);
  Serial.println();
}


コードの解説は検索すればいろんなところから出ているので、 ここではLineBotのからのメッセージを受け取る部分のみ解説したいと思います。

  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  if ((char)payload[0] == '1') {
    digitalWrite(whiteLED, HIGH);
  } else if ((char)payload[0] == '0') {
    digitalWrite(whiteLED, LOW);
  }

上記の部分はサブスクライブのTopicからのメッセージを受け取り、
1の場合はLEDのを点灯し、0の場合はLEDを消灯します。
それではLineBotの部分はみていきましょう。

import os
import base64

from flask import Flask
from flask import request
from flask import abort

from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkiot.request.v20180120.PubRequest import PubRequest

from linebot import LineBotApi
from linebot import WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent
from linebot.models import TextMessage
from linebot.models import TextSendMessage

app = Flask(__name__)

# LineBot Access Token
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
CHANNEL_SECRET = os.environ["CHANNEL_SECRET"]

# AliCloud Access Token
ALICLOUD_ACCESS_KEY = os.environ["ALICLOUD_ACCESS_KEY"]
ALICLOUD_ACCESS_SECRET = os.environ["ALICLOUD_ACCESS_SECRET"]
ALICLOUD_REGION = os.environ["ALICLOUD_REGION"]
ALICLOUD_IOT_PRODUCTKEY = os.environ["ALICLOUD_IOT_PRODUCTKEY"]
ALICLOUD_IOT_TOPIC = os.environ["ALICLOUD_IOT_TOPIC"]

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

# create AcsClient instance
client = AcsClient(ALICLOUD_ACCESS_KEY, ALICLOUD_ACCESS_SECRET, ALICLOUD_REGION)

# callback()
@app.route("/callback", methods=['POST'])
def callback():
  # Get X-Line-Signature header value
  signature = request.headers['X-Line-Signature']

  # Get request body as text
  body = request.get_data(as_text=True)
  app.logger.info("Request body: " + body)

  # Handle webhook body
  try:
    handler.handle(body, signature)
  except InvalidSignatureError:
    abort(400)

  return 'OK'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
  
  message = event.message.text
  message_bytes = message.encode('utf-8')
  base64_bytes = base64.b64encode(message_bytes)
  base64_message = base64_bytes.decode('utf-8')

  request = PubRequest()
  request.set_accept_format('json')
  request.set_TopicFullName(ALICLOUD_IOT_TOPIC)
  request.set_MessageContent(base64_message)
  request.set_ProductKey(ALICLOUD_IOT_PRODUCTKEY)
  request.set_Qos("0")
  client.do_action_with_exception(request)
  #result = str(response)
  if message == "1":
    line_bot_api.reply_message(
      event.reply_token,
      TextSendMessage(text="電気をつけました")
    )
  elif message == "0":
    line_bot_api.reply_message(
      event.reply_token,
      TextSendMessage(text="電気を消しました")
    )
  else:
    line_bot_api.reply_message(
      event.reply_token,
      TextSendMessage(text=message)
    )

if __name__ == "__main__":
  callback()
  app.run()

LineBotの部分の説明は前回のブログ

www.sbcloud.co.jp

をご参考ください。
具体的な説明を省いて、AlibabaIoTPlatformと通信の際の注意点のみ紹介させていただきます。

message = event.message.text
  message_bytes = message.encode('utf-8')
  base64_bytes = base64.b64encode(message_bytes)
  base64_message = base64_bytes.decode('utf-8')

Topicにメッセージを送信する際、base64でエンコード必要があります。
ここでbase64のエンコードを行います。

では、実際通信する際のイメージをみていきましょう。
まずLineBotからメッセージ「1」を送信すると、 f:id:sbc_hr:20200303114435p:plain Arduinoにてメッセージを受けれることを確認できます。 デバイスのLEDも点灯されることが確認できます。 f:id:sbc_hr:20200303114129p:plain

次LineBotからメッセージ「0」を送信してみます。 f:id:sbc_hr:20200303115036p:plain Arduinoにてメッセージを受けれることを確認できます。 デバイスのLEDも消灯されることが確認できます。

これで、LineBotからどこからでもArduinoのLEDをコントロールすることができました。
次は、シナリオ②の部分を作成します。
f:id:sbc_hr:20200303173449p:plain

まず、Arduinoに以下のコードを追加します。

#define motion_sensor 11
#define buzzer 

void loop() {
  long now = millis();
  
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  motionStatus=digitalRead(motion_sensor);
  if (motionStatus==0) {
    if (now - lastMsg > 3000) {
      lastMsg = now;
      //digitalWrite(buzzer,LOW);
      digitalWrite(redLED,LOW);
      client.publish(pubsub_topic, "{Intruder:\"None\"}");
    }
  } else
  {
    if (now - lastMsg > 3000) {
      lastMsg = now;
      //digitalWrite(buzzer,HIGH);
      digitalWrite(redLED,HIGH);
      client.publish(pubsub_topic, "{Intruder:\"Detected\"}");
    }
  }
}

クラウド側の画面をみてみましょう。
f:id:sbc_hr:20200303171724p:plain これで、人感センサーが人の動きを検知したら、AlibabaIoTPlatformにメッセージを送信することができました。

ここからルールエンジンを使ってアップロードされたメッセージをFunctionComputeに転送し、 FunctionComputeからLineBotへアラートを送信しますが、
具体的な設定方法はまた次回のブログにて紹介させていただきます。

以上