前回まで/次回以降の取り組みはこちらから。
今回はセンサで測定したデータをRaspberry PiからAmbientというIoTサービスに送信して記録していきます。
Ambientへのデータ送信については後半にあります。(というか今回前半が長いです。)
AmbientとRaspberry Piの連携について知りたい方はRaspberry PiからAmbientにデータ送信するのセクションから見てください。
もくじ
Ambientとは
公式サイトより。
AmbientはIoTデーターの可視化サービスです。
マイコンなどから送られるセンサーデーターを受信し、蓄積し、可視化(グラフ化)します。
今回のpythonアプリ完成図
こんな感じ。
Ambientにデータ送信するためのGUIパーツを追加しました。
Arduinoスケッチ
asagao_iot_0_1_0.ino
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | // あさがおIoT観察日記 Arduino側プログラム // 2020.05.21 V0.0.1 DHT11による温度・湿度取得(DHT11サンプルスケッチより作成) // V0.0.2 BME280気圧取得(BME280サンプルスケッチより作成) // 2020.05.23 V0.0.3 TEMT6000による照度取得 // 2020.05.24 V0.0.4 KeyStudio製センサで土の水分量取得 // 2020.06.02 V0.0.5 RaspberryPi側通信制御開発のため、一度シリアル送信するのを照度データだけに変更 // 今後の準備としてLED照明、ポンプ、水切れセンサ、ボタンSWに関する制御パートを追加 // 2020.06.06 V0.0.6 センサの数値をすべてシリアル送信するように変更 // データ「d」をシリアルで受信したらセンサデータを送信する // 2020.06.08 V0.0.7 LEDガーデンライトの制御に対応 // 2020.06.11 V0.0.8 水やり制御に対応 // 2020.06.12 V1.0.0 Ambientデータ送信に対応 // フリーズ対策 boolean l = false; #define LED_pin 13 #define pump_pin 12 #define SW_B_pin 11 #define SW_R_pin 10 #define sensor_tank 2 #define sensor_water 1 #define sensor_bright 0 #include <Adafruit_Sensor.h> #include <DHT.h> #include <DHT_U.h> #define DHTPIN 2 // Digital pin connected to the DHT sensor #define DHTTYPE DHT11 // DHT 11 DHT_Unified dht(DHTPIN, DHTTYPE); #include <Wire.h> #include <SPI.h> #include <Adafruit_BME280.h> #define SEALEVELPRESSURE_HPA (1013.25) Adafruit_BME280 bme; // I2C uint32_t delayMS; int Alt = 120; void serial_send(double temp,double humid,double pres_0,int bright,int water,int tank){ Serial.println(temp); Serial.println(humid); Serial.println(pres_0); Serial.println(bright); Serial.println(water); Serial.println(tank); } void setup() { Serial.begin(9600); pinMode(LED_pin, OUTPUT); pinMode(pump_pin,OUTPUT); pinMode(SW_B_pin,INPUT_PULLUP); pinMode(SW_R_pin,INPUT_PULLUP); dht.begin(); delayMS = 100; bme.begin(0x76); } void loop() { static double temp; static double temp_0; static double humid; static double pres; static double pres_0; static int bright; static int water; static int tank; static boolean LED_Flag = false; static int LED_Th = 999; static boolean Water_Flag = false; static int Water_Th = 999; static long Water_time; static int th_lebel = 0; //シリアル送信 if(Serial.available()>=4){ //しきい値受信用行列 int th[] = {0,0,0}; char val = Serial.read(); th[0]= int(Serial.read())-48; th[1]= int(Serial.read())-48; th[2]= int(Serial.read())-48; //しきい値計算 if(th[0]>=0 && th[0]<10 && th[1]>=0 && th[1]<10 && th[2]>=0 && th[2]<10){ th_lebel = 100*th[0] + 10*th[1] + 1*th[2]; } if(val=='L'){ if(th_lebel==999){ LED_Flag = false; }else if(th_lebel==0){ LED_Th = 1024; LED_Flag = true; }else{ LED_Th = th_lebel; LED_Flag = true; } }else if(val=='W'){ if(th_lebel==999){ Water_Flag = false; digitalWrite(LED_pin,LOW); }else if(th_lebel==0){ Water_Th = 1024; Water_Flag = true; }else{ Water_Th = th_lebel; Water_Flag = true; } }else if(val=='d'){ serial_send(temp,humid,pres_0,bright,water,tank); } } //センサ読み取り sensors_event_t event; //温度取得 dht.temperature().getEvent(&event); if (!isnan(event.temperature)) { temp = event.temperature; } //湿度取得 dht.humidity().getEvent(&event); if (!isnan(event.relative_humidity)) { humid = event.relative_humidity; } //気圧取得 pres = bme.readPressure() / 100; pres_0 = pres/(pow((1-((0.0065*Alt)/(bme.readTemperature()+0.0065*Alt+273.15))),5.257)); //海面更生 //照度取得 bright = analogRead(sensor_bright); //LED照明制御 if(LED_Flag==true && bright<LED_Th){ digitalWrite(LED_pin,HIGH); }else{ digitalWrite(LED_pin,LOW); } //水分量取得 water = analogRead(sensor_water); //水やり判断 if(Water_Flag==true && water<Water_Th){ digitalWrite(pump_pin,HIGH); Water_Flag = false; Water_time = millis(); } if(millis()>Water_time+30000){ digitalWrite(pump_pin,LOW); } //タンク水切れ判断 tank = analogRead(sensor_tank); delay(10); } |
pythonプログラム
main_0_1_0.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | from PyQt5.QtWidgets import QApplication from ui.mainwindow_0_1_0 import MainWindow from PyQt5.QtCore import * import time import datetime import serial import sys sys.path.append('/usr/local/lib/python2.7/dist-packages') import ambient serialport = "/dev/ttyACM0" LED_TH = 999 LED_TH_P = 999 Water_TH = 999 Water_TH_P =999 temp = 0 humid = 0 press = 0 bright = 0 water = 0 tank = 0 class SerialCom: def __init__(self, serialport): self.sc = serial.Serial(serialport,9600, timeout=1) time.sleep(3) def data_read(self): rc_data = self.sc.readline() return rc_data def start_com(self): self.sc.write(b'd') self.sc.write(b'd') self.sc.write(b'd') self.sc.write(b'd') def light_set(self, th): self.sc.write(b'L') light1 = th//100+48 light2 = (th%100)//10+48 light3 = (th%100%10)+48 self.sc.write(chr(light1).encode()) self.sc.write(chr(light2).encode()) self.sc.write(chr(light3).encode()) def water_set(self, th): self.sc.write(b'W') water1 = th//100+48 water2 = (th%100)//10+48 water3 = (th%100%10)+48 self.sc.write(chr(water1).encode()) self.sc.write(chr(water2).encode()) self.sc.write(chr(water3).encode()) def close(self): self.sc.close() def refresh_window(): #時刻更新 now = datetime.datetime.now() ct = "現在時刻:" + now.strftime('%Y/%m/%d %H:%M:%S') ui.labeltime.setText(ct) #センサデータ表示更新 try: #温度 ui.labeltemp.setText("気温:\t" + str("{0:.2f}".format(float(temp))) + " [℃]") #湿度更新 ui.labelhumid.setText("湿度:\t" + str("{0:.2f}".format(float(humid)))+ " [%]") #気圧更新 ui.labelpress.setText("大気圧:" + str("{0:.2f}".format(float(press))) + " [hPa]" ) #照度更新 ui.labelbright.setText("照度:\t" + str("{0:4}".format(int(bright))) + "[Lx]") #水分量更新 ui.labelwater.setText( "水分量:\t" + str("{0:4}".format(int(water)))) #水タンク満水検知 ui.labeltank.setText("タンク内水検知:" + str("{0:4}".format(int(tank)))) except: pass #LED制御 n = now.time() global LED_TH #時刻制御1 cb_t1 = ui.checkBoxLightTime1.checkState() t = ui.timeEditLedStart1.time() LedStartTime1 = datetime.time(t.hour(), t.minute(), 0) t = ui.timeEditLedEnd1.time() LedEndTime1 = datetime.time(t.hour(), t.minute(), 0) #時刻制御2 cb_t2 = ui.checkBoxLightTime2.checkState() t = ui.timeEditLedStart2.time() LedStartTime2 = datetime.time(t.hour(), t.minute(), 0) t = ui.timeEditLedEnd2.time() LedEndTime2 = datetime.time(t.hour(), t.minute(), 0) #時刻制御有効の場合 if((cb_t1>0 and LedStartTime1<=n<LedEndTime1) or (cb_t2>0 and LedStartTime2<=n<LedEndTime2)): ui.progressBarLight.setValue(100) if(ui.checkBoxLightTh.checkState()): LED_TH = ui.spinBoxLed.value() else: LED_TH = 0 else: ui.progressBarLight.setValue(0) LED_TH = 999 #時刻制御無効の場合、センサーレベルだけ見る #すべてのチェックが入っていなければ、常時ON if(cb_t1==0 and cb_t2==0): if(ui.checkBoxLightTh.checkState()): LED_TH = ui.spinBoxLed.value() else: LED_TH = 0 #水やり制御 n = now.time() global Water_TH #時刻制御1 cb_t1 = ui.checkBoxWaterTime1.checkState() t = ui.timeEditWaterStart1.time() WaterStartTime1 = datetime.time(t.hour(), t.minute(), 0) t = ui.timeEditWaterEnd1.time() WaterEndTime1 = datetime.time(t.hour(), t.minute(), 0) #時刻制御2 cb_t2 = ui.checkBoxWaterTime2.checkState() t = ui.timeEditWaterStart2.time() WaterStartTime2 = datetime.time(t.hour(), t.minute(), 0) t = ui.timeEditWaterEnd2.time() WaterEndTime2 = datetime.time(t.hour(), t.minute(), 0) #時刻制御有効の場合 if((cb_t1>0 and WaterStartTime1<=n<WaterEndTime1) or (cb_t2>0 and WaterStartTime2<=n<WaterEndTime2)): ui.progressBarWater.setValue(100) if(ui.checkBoxWaterTh.checkState()): Water_TH = ui.spinBoxWater.value() else: Water_TH = 0 else: ui.progressBarWater.setValue(0) Water_TH = 999 #時刻制御無効の場合、センサーレベルだけ見る #すべてのチェックが入っていなければ、常時ON if(cb_t1==0 and cb_t2==0): if(ui.checkBoxWaterTh.checkState()): Water_TH = ui.spinBoxLed.value() else: Water_TH = 0 #Ambientデータ送信 if(now.second==0): r = amSend.send({'d1': temp, 'd2':humid, 'd3':press, 'd4':bright, 'd5':water, 'd6':tank}) def refresh_sensor(): myserial.start_com() global LED_TH global LED_TH_P global Water_TH global Water_TH_P global temp global humid global press global bright global water global tank #温度更新 try: temp = myserial.data_read() ui.labeltemp.setText("気温:\t" + str("{0:.2f}".format(float(temp))) + " [℃]") except: pass #湿度更新 try: humid = myserial.data_read() except: pass #気圧更新 try: press = myserial.data_read() except: pass #照度更新 try: bright = myserial.data_read() except: pass #水分量更新 try: water= myserial.data_read() except: pass #水タンク満水検知 try: tank= myserial.data_read() except: pass #LED制御支持送信 if(LED_TH != LED_TH_P): myserial.light_set(LED_TH) LED_TH_P = LED_TH #ポンプ制御支持送信 if(Water_TH != Water_TH_P): myserial.water_set(Water_TH) Water_TH_P = Water_TH if __name__=="__main__": import sys app = QApplication(sys.argv) ui = MainWindow() amSend = ambient.Ambient(22337,'73eeb109651b6a93') #ui.showFullScreen() myserial = SerialCom(serialport) refresh_sensor() timer1 = QTimer() timer1.timeout.connect(refresh_window) timer1.start(500) timer2 = QTimer() timer2.timeout.connect(refresh_sensor) timer2.start(2000) ui.show() sys.exit(app.exec_()) |
Ambientについて、GUIではチャネルID、ライトキー、送信間隔、送信するかを決めれるようにしてますが、いったんソフトにベタテキストで書き込んで1分間隔でデータ送信してみました。
(あまりむやみやたらにグローバル変数使わない方が良いのは知ってるんですけどね…)
あと、時々アプリそのものの動作が止まることがあり、改善方法がなかなか見つからず苦労しました。
その痕跡で随所にtry~except文があったり、refresh_time関数やrefresh_sensor関数のタイマーが変わってたりします。
根本的な理由は別の場所にあったんですが、それは後述。。。
mainwondow_0_1_0.py は前回の mainwindow_0_0_8.py から変更なしなので省略します。
謎のフリーズに悩まされる
時々、アプリの画面更新が止まってしまう不具合に悩まされました。
時計やセンサーの値などの画面の数字が変わらなくなり、Ambientへのデータ送信も止まります。
フリーズするタイミングは都度バラバラです。
時計の秒が毎回同じ、とかならまだわかりやすいんですけどね…。
もっとも、これまでもたまに起こっていたんですが、ここにきて頻発。
(Ambientにデータを送信することで記録が残るので見えるようになっただけか?)
解決するためにあれこれ試しました。
- シリアル通信に関連する部分にtry~except文を入れる → 効果なし
- refresh_time関数とrefresh_sensor関数の実行タイミング見直し → 効果なし
- refresh_time関数とrefresh_sensor関数をくっつけてみる → 効果なし
(タイミングがかち合っている?) - Arduinoから不必要なデータの返信をやめてみる → 効果なし
- 電源をUSB充電器(一応QI対応のやつ)から純正電源に交換 → 効果なし
- グローバル変数のセンサデータを同時に読み&書きしてしまっている? → 未トライ
(未トライというか、ここに対する解決策を思いつかない)
結論:センサデータ送信指示の文字列を変えた
ら、とりあえず改善したみたいです。
当初、Raspberry PiからArduinoにセンサデータの送信指示(「d」という文字を送信)したら、センサの値を返事するようにしていました。
その後、LED照明やポンプの制御をするために「L100」や「W500」といった4文字分のデータを送るようになったので、データの長さを合わせて処理を簡単にするためにセンサデータの送信指示を「d000」に変更しました。ここで、フリーズ多発。
そこで、センサデータの送信指示を「dddd」にしてみたところフリーズが収まりました。
ArduinoIDEのシリアルモニタで確認してもそんな兆候が見られるので、Arduino側で送信指示の内容によって動作が変わっているようです。
「d000」を送ると返事するときとしないとき(正確にはセンサのデータを返事するときと「0」と一文字だけ返事するとき)がありますが、「dddd」を送ると毎回センサのデータを返事してくれます。
現段階でこれ以上の理由は分かっていませんが、今後、分かったら報告します。
気を取り直して、
Raspberry PiからAmbientにデータ送信する
Ambient公式でやり方解説されているのでその通りでOKです。
が、せっかくなのでAmbientの登録からデータアップ・確認まで解説します。
①Ambientに登録する
Ambient公式サイトを開き、右上の「ユーザー登録(無料)」をクリックします。
メールアドレスを入力し、パスワードを決めて「ユーザー登録(無料)」をクリックします。
登録したアドレスにメールが届きます。
届いたメールに記載されたリンクをクリックします。
webサービスやECサイトの登録でよくある流れですね。
メールに記載されたリンクを踏めば登録完了です。
②Ambientでチャネルを作る
チャネルというのはひとまとまりのデータをまとめる箱のようなイメージです。
1つのチャネルに8種類のデータを登録できます。
ログインして使っていきます。
トップページ右上の「ログイン」でもOK。
メールアドレスとパスワードを入力して「ログイン」をクリックします。
自分の「チャネル」が表示されます。
「チャネルを作る」をクリックして、新しいチャネルを作ります。
新しいチャネルが作られました。
この「チャネルID」(5桁の数字)と「ライトキー」(16桁の英数字)がデータのアップロードに必要です。
どこかにコピペしておくか、このページを開いたままにしておきます。
③pythonプログラムからデータを送る
pythonプログラムにデータアップロード用のコマンドを追加します。
1.ライブラリ追加
LXTerminalでライブラリをインストールします。公式サイトの通り。
1 | sudo pip install git+https://github.com/AmbientDataInc/ambient-python-lib.git |
2.pythonプログラムで使う
プログラムにライブラリを読み込みます。
1 | import ambient |
ライブラリがないよ!と怒られます。どうして…。
(ericで動かしてみたらライブラリがないよ!と怒られたので、インストールできたか確認するためにThonnyIDEでも動かしてみたところです。やっぱり怒られました。)
3.パスを通す…?
ググってみたところ、どうやらこのライブラリはpython2系のライブラリらしく、python3系を使っているericやThonnyIDEからは保存場所を見つけられないようです…。
なので、保存場所を教えてあげる必要があります。
この作業を「パスを通す」というみたいです。
まずは保存場所を突き止めます。LXTerminalで
1 | pip show ambient |
を実行します。
「Location:」に続く部分がAmbientライブラリの保存されている場所です。
確かに保存場所にも「python2.7」って入ってますね。
pythonでAmbientのライブラリをインポートする前にこの保存場所を教えてあげます。
1 2 3 | import sys sys.path.append('/usr/local/lib/python2.7/dist-packages') import ambient |
エラーが出なくなりました。ちゃんと読み込めてるみたいです。
もちろん、ericでもエラー出なくなりました。
このあたりちゃんと公式で書いといてくれないと。無駄に悩んだじゃんかよー…(-ε-)*ブー
4.気を取り直してpythonプログラムで使う
Ambientのライブラリをインポートします。
1 2 3 | import sys sys.path.append('/usr/local/lib/python2.7/dist-packages') import ambient |
インスタンス化します。
ここで、チャネルIDとライトキーを入力しておきます。
1 | amSend = ambient.Ambient(チャネルId, ライトキー) |
データを送信したいタイミングで.send命令を実行します。
データは’d1’~’d8’までです。
1 | r = amSend.send({'d1': 数値, 'd2': 数値}) |
上のpythonプログラムを見てみてください。
④送ったデータを見てみる
Ambientにログインし、チャネルを開きます。
チャネルや各データの名前を付けたり、データを一般公開するか設定したり、位置情報を登録したりできます。
チャネル名を登録したので、上に表示される名前も変わりました。
次に、チャート設定をクリックしてグラフの設定をします。
グラフの名前や種類、8つのデータのうちどのデータを表示するか、メモリの縦軸を左側に取るか右側に取るか、それぞれの縦軸の最大値最小値、グラフの表示件数が設定できます。
例えば、温度・湿度・大気圧をこんな設定で1つのチャートに、
照度・土の水分量・タンクの水検知状況を2つめのチャートに登録してみます。
すると、こんな表示になりました。
これで、センサのデータがAmbientのサーバーに送信・記録されていきます。
今回は測定したデータをAmbientに送信して、いよいよIoTらしくなってきました。
まだまだ続きます。
コメント