Help us understand the problem. What is going on with this article?

SORACOMのデバイスとArduinoを用いて省エネIoT機器を作った。

はじめに

  • SORACOM Summer Challenge 2020というハッカソンに参加し、SORACOMのデバイスとArduinoでIoT機器を製作した。
  • 使用したSORACOMデバイスはこちら。

    • SORACOM LTE-M Button Plus
      image.png

      • ボタンのクリックイベントをSORACOM Harvest等に送れる。デバイス自体がSIMとして登録されており、付属のコネクタにセンサなどを取り付けるとその出力も送れる。
    • GPS マルチユニット SORACOM Edition
      image.png

      • 温度、湿度、位置情報、加速度などが分かり、設定に応じて定期的にセンサデータを送信できる。これだけできて1万円は安い。

機器の概要

  • 最近は(2020年8月)暑い日が続いているので機会は減っているが、冷房をかけた部屋から出てみると風が意外にも涼しく「エアコンかける意味なかったな……もったいねえ……」となったことはないだろうか?
  • そのため、「エアコンをつけなくても良い時は勝手に窓を開けてくれるデバイス」を製作しようと思った。以下がイメージ図。
    image.png

  • GPS マルチユニットを屋外に配置しておく。LTE-M Button Plusを押すと温度と湿度を参照し、冷房と窓を開けることのどちらが効果的かを判断し、実行する。

  • 10分おきに送られるデータを基に、暑くなってきたことが分かれば窓を閉めてエアコンに切り替えるなどする。

API操作などPC側

  • メインプログラムはPythonで記述した。実行するとボタンのクリックを待機し、クリックを検知すると最新の温度と湿度を呼び出す。
  • それを体感温度関数に通し、閾値以上ならエアコンを作動させる信号を、以下なら窓を開けるように信号をArduinoに送信する。

自動認証

  • まずはSORACOMのAPIにアクセスするためにAPI-KeyとTokenが必要となる。これはSORACOMのAPIリファレンス(https://dev.soracom.io/jp/docs/api/) などで発行できるが、時間制限があるため自動発行しての認証を行いたい。
soracom.py
import requests
import time

headers = {'Content-Type': 'application/json'}
params = '{"email": "ここにメールアドレス","password": "ここにパスワードを"}'
api = "https://api.soracom.io/v1/auth"

r = requests.post(api, headers=headers,data=params)
d = r.json()
data = {'X-Soracom-API-Key':d["apiKey"], 'X-Soracom-Token':d["token"]}

APIリファレンスによると、

API アクセスの認証を行います。ルートアカウントで認証する場合は email と password、認証キーでの認証の場合は authKeyId と authKey、SAM ユーザー認証の場合はoperatorId, userName, password をリクエストに含めてください。認証が成功した場合、API キーと API トークンが返されるので、以降の認証が必要なリクエスト時にはこの API キーと API トークンをヘッダーに付与してリクエストを行ってください。

らしいので適宜お好みの認証方法に応じてparamsの部分を変えよう。

pip install requests

をしてparamsのemailとpasswordなどを自分のものに変更すれば認証はスキップできるようになる。
これからはdataを用いてAPIをいじっていく。

SORACOMデバイスのデータ取得

SORACOM LTE-M Button

  • ここで以下のコードを挟み、クリックイベントを待つ。
soracom.py
call = 'https://api.soracom.io/v1/data/Subscriber/IMSI?sort=desc&limit=1'
while(True):
    now = time.time()
    ans = requests.get(url=call, headers=data)
    ans = ans.json()
    t = ans[0]["time"]
    if abs(t/1000-now)<3:break
    time.sleep(1)
  • IMSIのところは使用したいButtonのデータを見て入力する。
  • 1秒ごとにButtonのデータに変化がないかを確認し、timestampの変化が3未満ならループを抜けるようにした。

    • 3未満にしたのは調べていく中でt/1000-nowが2.5くらいまで振れることがあったから。
    • 通信量が凄そう。AWS labmdaを経由すればクリックイベントをトリガにプログラムを走らせられたが、全てのライブラリを入れられなかったので泣く泣く諦めた。いつか試したい。
  • requests.get()で得たレスポンスを.json()することで辞書のようにアクセス可能にしている。

  • その後タイムスタンプを取得し、比較。1秒待つ。繰り返し。

GPSマルチユニット

  • クリックイベントを読んだら温度、湿度のデータ取得に移る。
soracom.py
url = 'https://api.soracom.io/v1/data/Subscriber/マルチユニットに挿入したSIMの番号?sort=desc&limit=1' 
ans = requests.get(url=url, headers=data)
ans = ans.json()
e = ans[0]["content"]
e = json.loads(e)
decoded = base64.b64decode(e["payload"]).decode('utf-8')
decoded = json.loads(decoded)
temp = decoded["temp"]
humi = decoded["humi"]
M = ((temp - 1/2.3 * (temp-10) * (0.8-humi/100)))
  • e = ans[0]["content"]として辞書データのように扱うところまではButtonの時と同じ。このeはJSON文字列データであるため、辞書型として扱うためにjson.loads()をする。
  • 温度と湿度を取得したら体感温度の関数に通す。

    M=T-\frac{1}{2.3}(T-10)(0.8-\frac{H}{100})
    
    • ミスナールの計算式を用いた。これに風速の補正を加えるとより精度が上がるのだが、単に気温を見るよりはこれでかなりマシになった。

シリアル通信

soracom.py
if M>thre:
    flag = "h"
else:
    flag = "c"
count = 0    
with serial.Serial('COM4',57600,timeout=None) as ser:
    while(True):
        send=bytes(flag,'utf-8')
        print(send)
        ser.write(send)
        time.sleep(2)
        count+=1
        if count>1:break
    ser.close()
  • 体感温度が閾値より大きければhを、低ければcを準備する。
  • with serial.Serialでポートを開き、送信する。time.sleep(2)が入っていたりwhileで囲まれていたりするのはそうしない場合はなぜか見逃されるから(ダメなやつ)。
    • ポートを開いた直後にはラグがあるんじゃないかと思っている。検証が足りていない。
    • 通信レートを57600にしているのは後述のエアコンスイッチングのため。
  • PC側は以上。あとはフラグによる場合分けくらい。(おまけ参照)

Arduino側

  • Arduinoが行っているのは、シリアル通信で送られてきた文字列に応じてエアコンの起動とステッピングモーターの駆動を行うこと。
kansei.ino
#include "types.h"
#include <Stepper.h>
#define MOTOR_PIN1 5 
#define MOTOR_PIN2 6
#define MOTOR_PIN3 7
#define MOTOR_PIN4 8
#define IR_OUT                 (2) 
#define TIMEOUT_SEND           (2000000)
#define STATE_OK               (0)
const int StepsPerRotate = 2048;
int rpm = 15; 
int Steps = 1536; //自作の窓とステッピングモーターに合わせただけ
Stepper myStepper(StepsPerRotate, MOTOR_PIN1, MOTOR_PIN3, MOTOR_PIN2, MOTOR_PIN4);

u2 data[] = {レシーバで取得したものをコピペ};
//クーラーをオンする関数
void cooler(){
  u2 time = 0;
  u4 index = sizeof(data)/sizeof(data[1]);
  u4 count = 0;

  u4 us = 0;
  us = micros();

    for(count = 0; count < index; count++){
      time = data[count];
      us = micros();
      do {
        digitalWrite(IR_OUT, !(count&1));
        delayMicroseconds(8);
        digitalWrite(IR_OUT, 0);
        delayMicroseconds(7);
      }while(s4(us + time - micros()) > 0);
    }
}
u2 = off_data = {これも受け取ったものをメモしてコピペ}
//クーラーをオフする関数
void off_cooler(){
  u2 time = 0;
  u4 index = sizeof(off_data)/sizeof(off_data[1]);
  u4 count = 0;
  u4 us = 0;
  us = micros();

    for(count = 0; count < index; count++){
      time = off_data[count];
      us = micros();
      do {
        digitalWrite(IR_OUT, !(count&1));
        delayMicroseconds(8);
        digitalWrite(IR_OUT, 0);
        delayMicroseconds(7);
      }while(s4(us + time - micros()) > 0);
    }
}

//窓を開ける関数
void mortor(){
 Serial.println("Forward");
 myStepper.step(-Steps);
 Serial.println();
 delay(500);
}

//窓を閉める関数
void closing(){
  Serial.println("Forward");
 myStepper.step(Steps);
 Serial.println();
 delay(500);
}

void setup(){
  Serial.begin(57600);
  pinMode(IR_OUT, OUTPUT);
  myStepper.setSpeed(rpm);
  Serial.println();
}

void loop(){

  int inputchar;      //入力状態の読み取りに使う

  inputchar = Serial.read();  //シリアル通信で送信された値を読み取る
  Serial.println(inputchar);
  switch(inputchar){
    case 'h':
      cooler();
      break;
    case 'c':
      mortor();
      break;
    case 'o':
      closing();
      cooler();
      break;
    case 'l':
      off_cooler();
      mortor();
      break;    
    }
}

回路の概要

IMG_20200813_152043.jpg

エアコンの起動、停止

  • これは以下の記事を参考にした。
  • まず、上の記事のプログラムをArduinoに書き込み、エアコンの起動と停止の信号を受信モジュールで受け取って記録する。
  • 「s,496,444,428,440,428,436,……,0」といったような長い信号を受け取れるので、最初の「s,」と最後の「,0」を外してコピーしておく。
  • kansei.inoのdata[]off_data[]にそれぞれ貼り付ければエアコンのオンオフができるようになる。

モーターの駆動

  • 大学でもらったコードを改変したが、以下のサイトのものが近い。
  • Stepsで回転量を操作できる。2048で1回転なので、270°回したければ1536。

おまけ

  • エアコン稼動中に外が涼しくなってきたらエアコンを止めて窓を開け、窓の外が暑くなってきたら窓を閉めてエアコンを起動する機能も作った。
soracom.py
state = "init"
while(True):
    count = 0
    ans = requests.get(url=url, headers=data)
    ans = ans.json()
    e = ans[0]["content"]
    e = json.loads(e)
    print(e)
    decoded = base64.b64decode(e["payload"]).decode('utf-8')
    decoded = json.loads(decoded)
    temp = decoded["temp"]
    humi = decoded["humi"]
    M = ((temp - 1/2.3 * (temp-10) * (0.8-humi/100)))
    print(M)
        if state=="init":
        if M>thre:
            flag = "h"
            state="cooler"
        else:
            flag = "c"
            state="window"
        count = 0    
        with serial.Serial('COM4',57600,timeout=None) as ser:
            while(True):
                send=bytes(flag,'utf-8')
                print(send)
                ser.write(send)
                time.sleep(2)
                count+=1
                if count>1:break
            ser.close()

        elif state=="cooler":
        if M<thre:
            with serial.Serial('COM4',57600,timeout=1) as ser:
                while True:
                    send=bytes('o','utf-8')
                    print(send)
                    ser.write(send)
                    time.sleep(2)
                    count+=1
                    if count>1:break
                ser.close()
        elif state=="window":
        if M>thre:
            with serial.Serial('COM4',57600,timeout=1) as ser:
                while True:
                    send=bytes('l','utf-8')
                    print(send)
                    ser.write(send)
                    time.sleep(2)
                    count+=1
                    if count>1:break
                ser.close()
        sec = 0
        for i in range(660):
            now = time.time()
            ans = requests.get(url=call, headers=data)
            ans = ans.json()
            t = ans[0]["time"]
            if abs(t/1000-now)<3:
                state="init"
                break
            time.sleep(1)
  • ボタンのクリックを検知した後のプログラム。最初にstate="init"として、体感温度が閾値以上なら(外が暑いなら)state="cooler"に切り替えてクーラーを起動する。
  • 閾値以下なら(外が涼しければ)state="window"に切り替えて窓を開ける。
  • その後、660秒間のforループに入る。これはクリックイベントを待つプログラムと同様で、もしクリックイベントが途中で発生したら再度温度と湿度を参照してクーラーを起動するか窓を開けるかするようにしている。
  • 660秒後に外のデータが更新されるので再参照し、窓が開いているのに外が暑くなっていれば即座に閉めてクーラーを起動するなどする。
    • SORACOMの設定で10分毎にデータ更新を行っているが、実際厳密に10分では更新されないので余裕をもって11分毎に再参照をする。

感想、反省

  • IoT初体験だったのでかなり至らない部分があるとは思うが、ちゃんと動くものにはなった。
  • AWS Lambdaを経由するとLINEやSlackなどとリンクしてより多くの機能を詰め込めたと思うので、そこは少し残念。
  • Arduinoとのシリアル通信が簡単にできる言語で書く必要があるという制限があったので、JSなどは自分の力不足で使えなかった……
  • 次回は多くのコンテンツと接続してよりIoTらしいものを作れたらと思う。

参考ページまとめ

本文に載っているものが多いが、参考にしたページをまとめる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした