3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-11-21

今回の内容

前回からの続きで、今回はラズパイにセンサーを取り付け、そのデータをプラットフォームへ送信し、最終的に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に流し込むようにしたいと思います。

3
3
0

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
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?