今回の内容
前回からの続きで、今回はラズパイにセンサーを取り付け、そのデータをプラットフォームへ送信し、最終的にAmazon S3に保存されるかやってみます。
また設置場所ですが、「Fukuoka City LoRaWAN」の提供エリア内にあり、アクセスも良くて環境も整っている「GooDay Fab 大名店(福岡市中央区大名、FUKUOKA growth next 1F)」に設置することにしました。
更にセンサーについては、そこにある植物コーナーの鉢植の土壌水分を定期的(10分毎)に測定し、データを送信することにします。
センサー周りで準備した物
色々検討した結果、以下の物を準備すれば簡単&短時間で組み立て出来ることがわかり、通販で即買いしました。
-
GrovePi+
Raspberry Piにこのセンサーボードを付けるだけで、はんだ付けすることなくGroveブランドのセンサーを取り付けできます。
また、A/Dコンバータも搭載されているので、アナログの入出力もデジタルに変換してくれます。 - Grove 水分センサ
-
GrovePi ケース
基盤剥き出しだと取り扱いが気になっていましたが、GrovePi専用ケースが売っていました。 -
GROVE 4ピンケーブル 50cm (5本セット)
センサ付属のケーブルが短い(20cm程度)ので、50cmを別途調達しました。
組み立て
GrovePiはラズパイに載せたとき、USBポートやLANポートの金属部分がGrovePiの基盤部分に当たってしまうので、以下画像の位置にマスキングテープを貼って絶縁対策します。
GrovePiコネクタ部分をラズパイのGPIOピンに差し込んで取り付けます。ちなみに差し込み位置は下図の通りです。
最後にGrovePiケースに収めた後、センサーや各種ケーブルを取り付けます。水分センサーはアナログポートの0番(基盤にA0
と書かれています)に差し込みます。
尚、ケースへの取り付け、組み立て方法は公式動画を参考にしました。
ラズパイ起動時に不具合発生
組み立てが終わりラズパイの電源を投入しますが、ここで問題が発生しハマりました。
起動途中からレジスタやスタック情報が表示され、最後に
Fixing recursive fault but reboot is needed.
というメッセージか表示され、起動できません。
何度か、接続が間違っていないか確認して電源を再投入してみますが、やはり同じ症状です。
試しに、GrovePiを取り外して電源を投入すると、こちらは問題なく起動できます。
「もしかすると、カーネルのバージョンが関係している?」と思い、色々調べてみると、、、ありました。
GrovePiの公式サイトに「Raspbian for Robots」というページがあり、そこでGrovePi用にカスタマイズされたOSイメージが配布されていました。
試しに上記ページのUsing a PC
を展開し、表示された手順に従って別のSDカードを準備して起動してみると、問題なく起動しました。
ちなみに起動できなかったSDカードは、元々「Complete Starter Kit」に同梱されていたもので、OSのバージョンを確認すると最新バージョンのstretch
だということが解りました。
$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
NAME="Raspbian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
また起動したRaspbian for Robots
のバージョンを確認すると、
$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
NAME="Raspbian GNU/Linux"
VERSION_ID="8"
VERSION="8 (jessie)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
ひとつ前のバージョンjessie
だということが解り、現時点(2017/11現在)で、GrovePiはstretch
に未対応だということが推測できます。
ということで、切ない状況ですが前回まで使っていたOSは捨てて、このRaspbian for Robots
で構築していくことにします。
ところでこのRaspbian for Robots
ですが、幸いにしてGrovePiを利用すためのツールやモジュール、サンプルスクリプト等は一通りインストール済みですので、公式のRaspbian Jessie
を準備して環境を整えることを考えると、多少の手間は省けます。
GrovePiファームウェアのアップデート
無事に起動できましたのでログインします。
尚、公式のRaspbianのデフォルトログインID、PWはそれぞれpi
、raspberry
ですが、Raspbian for Robots
の場合、IDは同じpi
ですが、パスワードはrobots1234
となります。
ログインできたらOSの基本的な環境(ネットワーク、ロケール、タイムゾーン等々)を設定した後、GrovePiのファームウェアをアップデートします。
まずは現在のファームウェアのバージョンを、grove_firmware_version_check.py
スクリプトで確認します。
$ cd ~/Dexter/GrovePi/Software/Python
$ python grove_firmware_version_check.py
GrovePi has firmware version 1.2.2
次にfirmware_update.sh
コマンドで最新バージョンにアップデートします。
$ cd ~/Dexter/GrovePi/Firmware
$ sudo chmod +x firmware_update.sh
$ sudo ./firmware_update.sh
再度、ファームウェアのバージョンを確認します。
$ cd ~/Dexter/GrovePi/Software/Python
$ python grove_firmware_version_check.py
GrovePi has firmware version 1.2.7
また、ここまでの作業でOSを最新の状態にしていない場合、次のコマンドでアップデートします。
$ sudo apt-get update
$ sudo apt-get upgrade
更新後は念のためOSをリブートします。
センサーテスト
ここまでの作業で環境が整いましたので、センサーをテストしてみます。
まず、OSがGrovePiをi2cボードとして認識しているか、次のコマンドで確認します。
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- 04 -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
00行の4列目が04
となっていますので、きちんと認識してくれているようです。
既にインストールされているDemoスクリプトを使って、センサーデータを取得してみます。
センサーの種類別にDemoスクリプトが用意されていますので、今回は水分センサ用のDemoスクリプトgrove_moisture_sensor.py
を使います。
$ cd ~/Dexter/GrovePi/Software/Python
$ python grove_moisture_sensor.py
0
0
0
83
67
74
・・・
問題なくセンサーからデータを取ってくれているようです。
ちなみに、水分センサーの両方の金属部分をショートさせるように指で触ると通電して数値が上がり、触らなければ0になります。
またDemoスクリプトの中を見ると以下の説明があり、このセンサーは0~950までの値をとり、その数値により「乾燥(0-300)」「多湿(300-700)」「水中(700-950)」の状態を判別できることが解ります。
# NOTE:
# The wiki suggests the following sensor values:
# Min Typ Max Condition
# 0 0 0 sensor in open air
# 0 20 300 sensor in dry soil
# 300 580 700 sensor in humid soil
# 700 940 950 sensor in water
# Sensor values observer:
# Val Condition
# 0 sensor in open air
# 18 sensor in dry soil
# 425 sensor in humid soil
# 690 sensor in water
LoRaWANモデムの動作確認
OSが変わりましたので、LoRaWANモデムと問題なく通信できるか再確認します。
まずは前回までに作成した以下のPythonスクリプトを、~/Projects/LoRaWAN
直下に置きます。
- RHF3M076.py
- main.py
次に第1回の手順に従って、LoRaWANモデムのデバイスポート(/dev/ttyACM0
)を確認します。
特に違いもなく支障はなさそうですので、main.py
を実行してみます。
$ cd ~/Projects/LoRaWAN
$ ./main.py
(中略)
AttributeError: 'Serial' object has no attribute 'reset_input_buffer'
Serial
オブジェクトにreset_input_buffer
アトリビュートが無い、と叱られてしまいました。
「もしかするとインストールされているpySerial
のバージョンが古い?」と思ったので確認してみます。
$ pip3 show pyserial | grep Version
Version: 2.6
やはりバージョンが古いようですので、アップグレードしてみます。
$ sudo pip3 install --upgrade pyserial
Downloading/unpacking pyserial from https://pypi.python.org/packages/0d/e4/2a744dd9e3be04a0c0907414e2a01a7c88bb3915cbe3c8cc06e209f59c30/pyserial-3.4-py2.py3-none-any.whl#md5=0e555d61700e0b95a15d8162092c5299
Downloading pyserial-3.4-py2.py3-none-any.whl (193kB): 193kB downloaded
Installing collected packages: pyserial
Found existing installation: pyserial 2.6
Not uninstalling pyserial at /usr/lib/python3/dist-packages, owned by OS
Successfully installed pyserial
Cleaning up...
まさかのowned by OS
と表示され、アップデートできません。
pySerial
はハードウェア絡みのモジュールということもあり、無理矢理バージョンアップしてGrovePiが動作しなくなる恐怖もあるので、ここはRHF3M076.py
の該当行をコメントアウトすることで対応することにしました。
def _open(self):
(中略)
try:
(中略)
self._modem.reset_input_buffer()
↓↓↓↓↓↓
def _open(self):
(中略)
try:
(中略)
#self._modem.reset_input_buffer()
LoRaWANデバイスの設置
スクリプトを作成する前に、LoRaWANデバイスをGooDay Fab 大名店に設置します。
植物コーナーの棚に空きスペースがあり、電源も近くにあったのでそこに置かせてもらいました。
折角なので、同店で販売しているアクリル板をレーザーカッターで加工し、店ロゴ入りの上板に変更してみました。
レーザーカッターを使った作業って、意外と楽しかったりします(笑)
そばにあるポトスの鉢に水分センサーを差し込みました。ちなみに水分センサーの基盤部分が剥き出しだったので、ビニールテープでマスキングしています。
さらに大名店のスタッフが、レーザーカッターで専用プレートを作ってくれました!
センサーデータ送信用スクリプト作成
Demoスクリプトを参考に、センサーデータをプラットフォームへ送信するスクリプトを作成したいと思います。
まずはモジュールRHF3M076.py
のsendPayload
メソッドを以下コードのように変更し、mtype(ACKの確認/無視)とdtype(Text or Hex)のフラグを追加して、より汎用的に使えるようにしました。
#!/usr/bin/env python3
# coding: utf-8
#------------------------------------------------------------------------------
# LoRaWAN modem (RHF3M076) module for Fukuoka City LoRaWAN
# by Kaho Musen Holdings Co.,Ltd.
#
# Created: 2017.11.03
# Author: k.nagase (a) gooday.co.jp
#------------------------------------------------------------------------------
import serial
import re
import time
from logging import getLogger
# Class of LoRaWAN modem
class RHF3M076:
# Constructor
def __init__(self, port='/dev/ttyACM0', baud=115200, timeout=0.1):
self._port = port
self._baud = baud
self._timeout = timeout
self._crlf = '\r\n'
self._ptnDevAddr = r'\+ID: DevAddr, (([0-9A-Fa-f]{2}[:-]){3}[0-9A-Fa-f]{2})'
self._ptnDevEui = r'\+ID: DevEui, (([0-9A-Fa-f]{2}[:-]){7}[0-9A-Fa-f]{2})'
self._ptnAppEui = r'\+ID: AppEui, (([0-9A-Fa-f]{2}[:-]){7}[0-9A-Fa-f]{2})'
self._ptnKeyErr = r'\+KEY: ERROR\((.*)\)'
self._logger = getLogger(type(self).__name__)
self._open()
# Open the serial port
def _open(self):
self._modem = serial.Serial()
self._modem.port = self._port
self._modem.baudrate = self._baud
self._modem.timeout = self._timeout
try:
self._modem.open()
#self._modem.reset_input_buffer()
return()
except Exception as e:
self._logger.error('Serial port open failed.')
raise(e)
# Send key type and key value
def _setKey(self, KeyType, KeyValue):
try:
if KeyValue:
cmd = 'AT+KEY=' + KeyType + ',"' + KeyValue + '"'
ret = self._write(cmd)
self._waitResponse()
line = self._read()
ret = re.match(self._ptnKeyErr, line)
if ret:
raise Exception(KeyType + ' is invalid. (' + ret.group(1) + ')')
else:
raise Exception(KeyType + ' is empty.')
return()
except Exception as e:
self._logger.error(e)
# Send to serial port
def _write(self, cmd):
try:
self._logger.info('SEND:' + cmd)
cmd = cmd + self._crlf
ret = self._modem.write(cmd.encode())
return(ret)
except Exception as e:
print('ERROR: Send to serial port failed.')
raise(e)
# Receive from serial port
def _read(self):
len = self._waitResponse()
ret = self._modem.readline().decode().replace(self._crlf, '')
self._logger.info('RECV:' + ret)
return(ret)
def _waitResponse(self):
while self._modem.inWaiting() == 0:
time.sleep(0.1)
return(self._modem.inWaiting())
# Send payload
def sendPayload(self, mtype, dtype, payload):
#
# mtype: 0=ACK Unconfirm, 1=ACK Confirm
# dtype: 0=Text, 1=Hex
#
cmd = 'AT+'
if mtype:
cmd += 'CMSG'
else:
cmd += 'MSG'
if dtype:
cmd += 'HEX'
cmd += '="' + payload + '"'
self._write(cmd)
#time.sleep(2)
len = self._waitResponse()
if mtype:
ack = False
else:
ack = True
while True:
line = self._read()
if line.find('Done') != -1:
break
if line.find('ACK Received') != -1:
ack = True
return(ack)
@property
def DevAddr(self):
return(self._DevAddr)
@property
def DevEui(self):
return(self._DevEui)
@property
def AppEui(self):
return(self._AppEui)
@property
def NwksKey(self):
return(self._NwksKey)
@property
def AppsKey(self):
return(self._AppsKey)
@property
def AppKey(self):
return(self._AppKey)
@property
def ADR(self):
return(self._ADR)
@NwksKey.setter
def NwksKey(self, NwksKey):
self._NwksKey = NwksKey
self._setKey('NWKSKEY', self._NwksKey)
@AppsKey.setter
def AppsKey(self, AppsKey):
self._AppsKey = AppsKey
self._setKey('APPSKEY', self._AppsKey)
@AppKey.setter
def AppKey(self, AppKey):
self._AppKey = AppKey
self._setKey('APPKEY', self._AppKey)
@ADR.setter
def ADR(self, state):
self._ADR = state
strState = 'ON' if self._ADR else 'OFF'
cmd = 'AT+ADR=' + strState
ret = self._write(cmd)
len = self._waitResponse()
self._read()
# Destructor
def __del__(self):
self._modem.close()
そしてセンサーデータ取得&LoRaWAN送信用のスクリプトmoisture.py
を、以下コードのように作成しました。
尚、正確に一定間隔でデータ取得から送信まで行いたいので、こちらの投稿を参考に、システムコールとシグナルを使って一定間隔で処理させるようにしました。
また送信エラーが発生した場合、最大3回までリトライするようにしています。
#!/usr/bin/env python3
# coding: utf-8
import signal
import time
import grovepi
from RHF3M076 import RHF3M076
from logging import basicConfig, getLogger, DEBUG
# Moisture sensor port number (Analog #0)
sensor = 0
# Seconds and interval for signal
sgSecond = 0.1
sgInterval = 600.0
# Keys
NwkSKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
AppSKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
AppKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
modem = RHF3M076()
basicConfig(level=DEBUG)
logger = getLogger(__name__)
def getSensor():
try:
# Get moisture sensor data (0 - 950)
val = grovepi.analogRead(sensor)
return(str(val))
except IOError:
logger.error('Sensor input failed.')
return(False)
def task(arg1, arg2):
payload = getSensor()
#print(payload)
modem.NwksKey = NwkSKey
modem.AppsKey = AppSKey
modem.AppKey = AppKey
modem.ADR = True
# Send LoRaWAN payload (Retry at 3 times)
for i in range(3):
if modem.sendPayload(1, 0, payload): break
else:
logger.error('Payload send failed.')
time.sleep(1)
def main():
signal.signal(signal.SIGALRM, task)
signal.setitimer(signal.ITIMER_REAL, sgSecond, sgInterval)
while 1:
try:
time.sleep(1)
except KeyboardInterrupt:
modem = None
break
except Exception as e:
modem = None
raise(e)
if __name__ == '__main__':
main()
スクリプトの実行
実際にスクリプトを実行してみます。
まず初めにmoisture.py
に実行権限を付与し、その後実行します。
$ chmod +x moisture.py
$ ./moisture.py
INFO:RHF3M076:SEND:AT+KEY=NWKSKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
INFO:RHF3M076:RECV:+KEY: NWKSKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
INFO:RHF3M076:SEND:AT+KEY=APPSKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
INFO:RHF3M076:RECV:+KEY: APPSKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
INFO:RHF3M076:SEND:AT+KEY=APPKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
INFO:RHF3M076:RECV:+KEY: APPKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
INFO:RHF3M076:SEND:AT+ADR=ON
INFO:RHF3M076:RECV:+ADR: ON
INFO:RHF3M076:SEND:AT+CMSG="338"
INFO:RHF3M076:RECV:+CMSG: Start LoRaWAN transaction
INFO:RHF3M076:RECV:+CMSG: TX "338"
INFO:RHF3M076:RECV:+CMSG: Wait ACK
INFO:RHF3M076:RECV:+CMSG: ACK Received
INFO:RHF3M076:RECV:+CMSG: RXWIN1, RSSI -128, SNR -11.25
INFO:RHF3M076:RECV:+CMSG: Done
(以下同様のログ)
ログを確認する限りでは問題なく送信できているようですので、S3バケットにデータが送られているか確認します。
10分毎にデータが作成されているのが確認できます。
さらに保存されているファイルの中身も確認してみます。
{"Time":"2017-11-20T12:09:02.876+01:00","DevEUI":"47XXXXXXXXXXXXXX","FPort":"8","FCntUp":"632","ADRbit":"1","MType":"4","FCntDn":"666","payload_hex":"363132","mic_hex":"04f92577","Lrcid":"00000201","LrrRSSI":"-111.000000","LrrSNR":"-7.000000","SpFact":"10","SubBand":"G0","Channel":"LC4","DevLrrCnt":"2","Lrrid":"65XXXXXX","Late":"0","LrrLAT":"33.590340","LrrLON":"130.401535","Lrrs":{"Lrr":[{"Lrrid":"65XXXXXX","Chain":"0","LrrRSSI":"-111.000000","LrrSNR":"-7.000000","LrrESP":"-118.790100"},{"Lrrid":"65XXXXXX","Chain":"0","LrrRSSI":"-117.000000","LrrSNR":"-10.500000","LrrESP":"-127.870773"}]},"CustomerID":"100008500","CustomerData":{"alr":{"pro":"LORA/Generic","ver":"1"}},"ModelCfg":"0","DevAddr":"01XXXXXX"}
プラットフォームから送られたデータが、JSON形式で保存されていますので完璧です。
自動起動の設定
OSブート時にセンサースクリプトが自動起動されるよう、systemdに登録します。
まずはファイル/etc/systemd/system/moisture.service
を作成します。
[Unit]
Description = Moisture Sensor
[Service]
ExecStart=/home/pi/Projects/LoRaWAN/moisture.py
Restart=always
Type=simple
[Install]
WantedBy=multi-user.target
moisture
サービスの自動起動を有効化し、手動で起動します。
$ sudo systemctl enable moisture
$ sudo systemctl start moisture
次回
今回はS3バケットにデータを蓄積しましたが、次回はFirehose
の設定を変更し、データ加工後にAmazon Redshift
に流し込むようにしたいと思います。