はじめに
- SORACOM Summer Challenge 2020というハッカソンに参加し、SORACOMのデバイスとArduinoでIoT機器を製作した。
- 使用したSORACOMデバイスはこちら。
- 
- ボタンのクリックイベントをSORACOM Harvest等に送れる。デバイス自体がSIMとして登録されており、付属のコネクタにセンサなどを取り付けるとその出力も送れる。
 
- 
- 温度、湿度、位置情報、加速度などが分かり、設定に応じて定期的にセンサデータを送信できる。これだけできて1万円は安い。
 
 
- 
機器の概要
- 
最近は(2020年8月)暑い日が続いているので機会は減っているが、冷房をかけた部屋から出てみると風が意外にも涼しく「エアコンかける意味なかったな……もったいねえ……」となったことはないだろうか? 
- 
GPS マルチユニットを屋外に配置しておく。LTE-M Button Plusを押すと温度と湿度を参照し、冷房と窓を開けることのどちらが効果的かを判断し、実行する。 
- 
10分おきに送られるデータを基に、暑くなってきたことが分かれば窓を閉めてエアコンに切り替えるなどする。 
API操作などPC側
- メインプログラムはPythonで記述した。実行するとボタンのクリックを待機し、クリックを検知すると最新の温度と湿度を呼び出す。
- それを体感温度関数に通し、閾値以上ならエアコンを作動させる信号を、以下なら窓を開けるように信号をArduinoに送信する。
自動認証
- まずはSORACOMのAPIにアクセスするためにAPI-KeyとTokenが必要となる。これはSORACOMのAPIリファレンス(https://dev.soracom.io/jp/docs/api/) などで発行できるが、時間制限があるため自動発行しての認証を行いたい。
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
- ここで以下のコードを挟み、クリックイベントを待つ。
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を経由すればクリックイベントをトリガにプログラムを走らせられたが、全てのライブラリを入れられなかったので泣く泣く諦めた。いつか試したい。
 
- 3未満にしたのは調べていく中で
- 
requests.get()で得たレスポンスを.json()することで辞書のようにアクセス可能にしている。
- 
その後タイムスタンプを取得し、比較。1秒待つ。繰り返し。 
GPSマルチユニット
- クリックイベントを読んだら温度、湿度のデータ取得に移る。
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})
  * ミスナールの計算式を用いた。これに風速の補正を加えるとより精度が上がるのだが、単に気温を見るよりはこれでかなりマシになった。
### シリアル通信
* 以下の記事を参考にしてArduinoとの通信を行った。
    * [PC-Arduino間 Python経由 シリアル通信備忘録](https://qiita.com/Acqua_Alta/items/9f19afddc6db1e4d4286)
```python: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が行っているのは、シリアル通信で送られてきた文字列に応じてエアコンの起動とステッピングモーターの駆動を行うこと。
# 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;    
    }
}
回路の概要
- 使った部品を以下に示す。
- uxcell ステップモーター 電動機 UL2003 モジュールボート 28BYJ-48 5V DC
- 赤外線リモコン受信モジュールOSRB38C9AA(2個入)
- 5mm赤外線LED 940nm OSI5FU5111C-40 (5個入)
- 学校で配られたArduino Nanoの互換品。Arduinoなら何でもいいと思う。
- 調べたらこれっぽい。安すぎない?
 
 
- 回路は後述のエアコンの記事のものにステッピングモーターを加えただけなので、エアコンの記事を参照していただきたい。
- ただ、記事では受信モジュールのGNDとVccの配線が逆になっており、自分は1つ燃やしてしまった。致命的な罠……
エアコンの起動、停止
- これは以下の記事を参考にした。
- 赤外線リモコンを作る
- エアコンのデータを保存するためにもこのプログラムを使う。
 
- まず、上の記事のプログラムをArduinoに書き込み、エアコンの起動と停止の信号を受信モジュールで受け取って記録する。
- 「s,496,444,428,440,428,436,……,0」といったような長い信号を受け取れるので、最初の「s,」と最後の「,0」を外してコピーしておく。
- kansei.inoのdata[]とoff_data[]にそれぞれ貼り付ければエアコンのオンオフができるようになる。
外の方が暑い時はエアコンをつけてくれます。 pic.twitter.com/IgVLGZy9ws
— M1YamA (@M1YamA00) August 13, 2020
モーターの駆動
- 大学でもらったコードを改変したが、以下のサイトのものが近い。
- Stepperライブラリでモータを動かす
- やっぱりライブラリ使うのが一番!
 
- 
Stepsで回転量を操作できる。2048で1回転なので、270°回したければ1536。
外が涼しい時は自動で窓を開けてくれます(自作窓頑張った) pic.twitter.com/2WHjrWSVz4
— M1YamA (@M1YamA00) August 13, 2020
おまけ
- エアコン稼動中に外が涼しくなってきたらエアコンを止めて窓を開け、窓の外が暑くなってきたら窓を閉めてエアコンを起動する機能も作った。
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らしいものを作れたらと思う。
参考ページまとめ
本文に載っているものが多いが、参考にしたページをまとめる。





