LoginSignup
3

More than 5 years have passed since last update.

posted at

updated at

Organization

Raspberry Pi+PythonでLoRaWAN Deviceにチャレンジしてみる(4)

今回の内容

前回からの続きで、今回はラズパイにセンサーを取り付け、そのデータをプラットフォームへ送信し、最終的に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はそれぞれpiraspberryですが、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.pysendPayloadメソッドを以下コードのように変更し、mtype(ACKの確認/無視)とdtype(Text or Hex)のフラグを追加して、より汎用的に使えるようにしました。

RHF3M076.py
#!/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回までリトライするようにしています。

moisture.py
#!/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バケットにデータが送られているか確認します。

s3.png

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を作成します。

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に流し込むようにしたいと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3