Python
RaspberryPi
LoRaWan

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

前回からの続きです。
今回はLoRaWANデバイスからネットワークサーバ(Fukuoka City LoRaWANでは、「プラットフォーム」と呼ばれています)へ、データを送信(Uplink)するところまでやりたいと思います。

プラットフォームのアクティベーション、ログイン

運営事業者へ利用申請してから待つこと数日、LoRaWAN運用基盤を提供しているThingParkから「[ThingPark Portal] Your ThingPark Account Details」というSubjectのメールが届きました。
メール内にAccount Activationというリンクがあったので、そこをクリックしてパスワードを登録すればアクティベーション完了です。

ここで落とし穴が待ち受けており、アクティベーション後にThingParkのログインページにリダイレクトされますが、そこがプラットフォームへのログインページじゃない、ということです。(私は見事に引っ掛かりました・・・)

事前に送られてきている「ユーザマニュアル」に記載されているURLが、正しいプラットフォームのアドレスとなりますので、ブラウザのアドレス欄にそれを打ち込めばプラットフォームログインページが開きます。

ログインページでアクティベーションの際に設定したパスワードにより、プラットフォームにログインします。
platform_001.png

ログインすると、以下のポータルページに遷移します。
platform_002.png

ThingParkStore
Fukuoka City LoRaWANでは利用しません。
Device Manager
デバイスやアプリケーションサーバを設定、管理できます。
Wireless Logger
デバイスの通信状況や、送られてきたデータを確認できます。

Device Manager

ポータルページのDevice Manager右下の矢印をクリックすると、別タブで以下のページが開きます。
platform_003.png

左メニューの各項目から、アプリケーションサーバやデバイスを管理できます。

Devices
デバイスの管理が行えます。
Connectivity plans
通信プランを確認できます。予め3つの通信プランが設定されており、追加/変更/削除はできません。
AS routing profiles
アプリケーションサーバへのルーティング方法を管理できます。
Application servers
アプリケーションサーバ情報の管理ができます。
Settings
利用者(Subscriber)情報の確認、アラームの設定ができます。

アプリケーションサーバの登録

「ユーザマニュアル」に従い、以下の手順でアプリケーションサーバ情報を登録します。
(デバイスとプラットフォーム間の通信試験であれば、この手順は省略できます)

  1. 左メニューのApplication serversをクリックし、Createボタンをクリック
    platform_004.png

  2. Nameに任意のアプリケーションサーバ名を入力、TypeHTTP Application Serverを選択し、Createボタンをクリック
    platform_005.png

  3. Content TypeXMLまたはJSONを選択(今回はJSONを選択)し、Add a routeにあるAddボタンをクリック
    platform_006.png

  4. Source portsにポート番号(HTTP≒80、HTTPS≒443、*≒全てのポート)を入力、Routing strategyBlast(複数サーバ指定時、同時送信)、Sequential(複数サーバ指定時、順次送信)のいずれかを選択し、Addボタンをクリック
    platform_007.png

  5. Destinationにアプリケーションサーバ側エンドポイント(https://~)を入力し、Addボタンをクリック
    ※今回は予め準備しておいたAmazon API GatewayのURLを入力(API Gateway側の手順は次回以降に投稿)
    platform_008.png

  6. Saveボタンをクリック
    platform_009.png

ルーティング方法登録

センサーデータがアプリケーションサーバに送られる際の、ルーティング方法を登録します。
(デバイスとプラットフォーム間の通信試験であれば、この手順は省略できます)

  1. 左メニューのAS routing profilesをクリックし、Createボタンをクリック
    platform_010.png

  2. Nameに任意のプロファイル名を入力し、Createボタンをクリック
    platform_011.png

  3. DestinationsにあるAddボタンをクリック
    platform_012.png

  4. TypeLocal application serverを選択、Destinationに前述で作成したアプリケーションサーバ名を選択し、Addボタンをクリック
    platform_013.png

  5. Saveボタンをクリック
    platform_014.png

デバイスの登録

以下の手順で、通信に利用するデバイス情報を登録します。
尚、今回はABPモードでの通信を想定していますので、OTAAモードで通信する際は設定項目が異なります。
⇒「ユーザマニュアル」参照

  1. 左メニューのDevicesをクリックし、Createボタンをクリック
    platform_015.png

  2. デバイス情報①~⑩を入力し、Createボタン(⑪)をクリック
    platform_016.png

    項目 説明
    Device Name 任意のデバイス名
    Device activation ABP、またはOTAAを選択(今回はABP
    DevEUI Device EUI を入力
    DevAddr Device Address を入力
    NwkSKey データ通信用の共通鍵を入力
    Device profile 利用するデバイスを選択(リストに無い場合は「LoRaWAN 1.0 - class A - ETSI - Rx2_SF9」を選択)
    Connectivity plan 実証実験利用申込時に申請したプランを選択
    Application server routing profile 作成済のルーティングプロファイルを選択
    Addボタン クリックすることで⑩のAppsKey欄に行が追加され、設定できるようになる
    AppSkeys データ通信用の共通鍵を入力(注)
  3. Devicesページを開き、Listタブをクリックすると登録したデバイス情報を確認することができる
    platform_017.png

※注
AppSKeyが未登録であったり、間違った(デバイス側に登録されているものと異なる)キーを登録してしまうと、デバイスが送信しているデータとプラットフォームで受信したデータが違う、という事象が発生します。(データの暗号/復号で異なるキーを利用しているので、まあ当然の結果ですが)
実は私自身もこの事象で半日程悩んでおり、本実証実験を先行して取り組んでいるUeno氏に相談したところ、全く同じ事象でハマったらしく直ぐに解決することができました。
(Uenoさん、ありがとうございました!)

手動による通信試験

ここまでの手順でプラットフォーム側の準備ができましたので、以下の手順によりデバイス側のコンソールから手動で通信試験を行ってみます。

ラズパイにSSH接続し、更にMinicomを起動してモデムへSerial接続します。

$ sudo LANG=c minicom

Welcome to minicom 2.7

OPTIONS: I18n
Compiled on Apr 22 2017, 09:14:19.
Port /dev/ttyACM0, 16:17:29

Press CTRL-A Z for help on special keys

「UART受信タイムアウト機能」を無効化します。

AT+UART=Timeout,0

+INFO: Input timeout, start parse
+UART: TIMEOUT is disabled

NwkSKey、AppSKey、AppKeyをデバイスに設定します。

(NwkSKey)

AT+KEY=NWKSKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

+KEY: NWKSKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX

(AppSKey)

AT+KEY=APPSKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

+KEY: APPSKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX

(AppKey)

AT+KEY=APPKEY,"2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

+KEY: APPKEY 2B XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX

通信効率を上げる為、ADR機能をONにします。

AT+ADR=ON

+ADR: ON

これで設定周りが整ったので、テキストデータ(Hello LoRaWAN!)を送信してみます。

AT+CMSG="Hello LoRaWAN!"

+CMSG: Start LoRaWAN transaction
+CMSG: TX "Hello LoRaWAN!"
+CMSG: Wait ACK
+CMSG: ACK Received
+CMSG: RXWIN2, RSSI -122, SNR -9.5
+CMSG: Done

+CMSG: Wait ACK返答から、数秒程度経った後に+CMSG: ACK Receivedが返されましたので、どうやら無事プラットフォームに送信できたようです。

尚、送信コマンドにはテキストデータの他に16進(HEX)で送るコマンドも用意されており、それぞれにプラットフォームから返されるACKを確認/無視するコマンドも用意されています。

MSG
テキスト送信用、ACKを無視
CMSG
テキスト送信用、ACKを確認
MSGHEX
HEX送信用、ACKを無視
CMSGHEX
HEX送信用、ACKを確認

通信試験結果の確認

プラットフォームに再度ログインし、ポータルページにある「Wireless Logger」を開きます。

platform_018.png

表内の2列目が上矢印(Uplink)で、且つ3列目がdataと表示されている行が、デバイスからのデータ送信ログになります。
その行の1列目の[+]をクリックして展開し、Data(hex)に記載されているものが送信されたデータ(Payload)です。
また、デバイスからテキストデータを送信した場合でも、Wireless Logger上ではHEX表示されます。

ちなみにこのHEXデータ48656c6c6f204c6f526157414e21をascii文字に変換すれば、Hello LoRaWAN!になります。

Pythonスクリプトの作成

手動での通信試験がうまくいったので、いよいよPythonスクリプトから送信してみたいと思います。

基本的には手動でやったことを、今度はスクリプトからモデムへシリアル通信することになります。

まずは前回のLoRaWANモデム用モジュールRHF3M076.pyに手を加え、各Keyのsetterやgetter、Peyload送信メソッド、ロギング処理等を追加しました。

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 for RHF3M076
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, Payload):
        cmd = 'AT+CMSG="' + Payload + '"'
        self._write(cmd)
        #time.sleep(2)
        len = self._waitResponse()
        ack = False
        while True:
            line = self._read()
            if line == '+CMSG: Done': break
            if line == '+CMSG: ACK Received':
                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()

以下のように、テストスクリプトを書いてみます。

main.py
#!/usr/bin/env python3
# coding: utf-8

from RHF3M076 import RHF3M076
from logging import basicConfig, getLogger, DEBUG

def main():
    basicConfig(level=DEBUG)
    logger = getLogger(__name__)
    modem = RHF3M076()
    modem.NwksKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    modem.AppsKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    modem.AppKey = '2BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    modem.ADR = True
    if not modem.sendPayload('Hello LoRaWAN!'):
        logger.error('Payload send failed.')
    modem = None
    return()

if __name__ == '__main__':
    main()

Pythonから通信試験

テストスクリプトを実行し、送信試験を行ってみます。

$ ./main.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="Hello LoRaWAN!"
INFO:RHF3M076:RECV:+CMSG: Start LoRaWAN transaction
INFO:RHF3M076:RECV:+CMSG: TX "Hello LoRaWAN!"
INFO:RHF3M076:RECV:+CMSG: Wait ACK
INFO:RHF3M076:RECV:+CMSG: ACK Received
INFO:RHF3M076:RECV:+CMSG: RXWIN1, RSSI -116, SNR -5
INFO:RHF3M076:RECV:+CMSG: Done

ACKも返ってきており、問題なく送信できているようです。

この後は、手動通信試験の時と同様にプラットフォーム側の「Wireless Logger」で、送信データを確認することができました。