Raspberry Pi＋PythonでLoRaWAN Deviceにチャレンジしてみる（２）

[前回](https://qiita.com/nagase/items/230b03ef09cbc5bbbd0c)からの続きです。
今回はLoRaWANデバイスからネットワークサーバ（`Fukuoka City LoRaWAN`では、「プラットフォーム」と呼ばれています）へ、データを送信（Uplink）するところまでやりたいと思います。

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

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

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

ログインページでアクティベーションの際に設定したパスワードにより、プラットフォームにログインします。
![platform_001.png](https://qiita-image-store.s3.amazonaws.com/0/94476/3453144a-4414-4ed5-d8ad-bd1230cf8e52.png)

ログインすると、以下のポータルページに遷移します。
![platform_002.png](https://qiita-image-store.s3.amazonaws.com/0/94476/53e76c77-dc4f-ceee-db4a-9aabb354dd71.png)

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


#Device Manager
ポータルページの`Device Manager`右下の矢印をクリックすると、別タブで以下のページが開きます。
![platform_003.png](https://qiita-image-store.s3.amazonaws.com/0/94476/a8451649-681c-4347-410f-e4232c8b44c7.png)

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

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

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

1. 左メニューの`Application servers`をクリックし、`Create`ボタンをクリック
![platform_004.png](https://qiita-image-store.s3.amazonaws.com/0/94476/15c8da95-31b4-eaa1-831d-85e35a10fd83.png)

2. `Name`に任意のアプリケーションサーバ名を入力、`Type`は`HTTP Application Server`を選択し、`Create`ボタンをクリック
![platform_005.png](https://qiita-image-store.s3.amazonaws.com/0/94476/047b6c71-dcb5-29f9-971f-327ae610f32f.png)

3. `Content Type`に`XML`または`JSON`を選択（今回は`JSON`を選択）し、`Add a route`にある`Add`ボタンをクリック
![platform_006.png](https://qiita-image-store.s3.amazonaws.com/0/94476/2a420155-2caf-a645-cd98-f0e36bb2907e.png)

4. `Source ports`にポート番号（HTTP≒80、HTTPS≒443、*≒全てのポート）を入力、`Routing strategy`に`Blast`（複数サーバ指定時、同時送信）、`Sequential`（複数サーバ指定時、順次送信）のいずれかを選択し、`Add`ボタンをクリック
![platform_007.png](https://qiita-image-store.s3.amazonaws.com/0/94476/0ab7085e-ff2d-0f5d-5bff-0e9b13725e5f.png)

5. `Destination`にアプリケーションサーバ側エンドポイント（https://～）を入力し、`Add`ボタンをクリック<br />※今回は予め準備しておいたAmazon API GatewayのURLを入力（API Gateway側の手順は次回以降に投稿）
![platform_008.png](https://qiita-image-store.s3.amazonaws.com/0/94476/dfa854b3-56df-4efe-142b-aeb8b86fa407.png)

6. `Save`ボタンをクリック
![platform_009.png](https://qiita-image-store.s3.amazonaws.com/0/94476/f30c6e94-b945-0842-eb3c-f34f7bb69d79.png)


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

1. 左メニューの`AS routing profiles`をクリックし、`Create`ボタンをクリック
![platform_010.png](https://qiita-image-store.s3.amazonaws.com/0/94476/f88dbf74-31d4-e0c3-45a8-95457ed2ebb8.png)

2. `Name`に任意のプロファイル名を入力し、`Create`ボタンをクリック
![platform_011.png](https://qiita-image-store.s3.amazonaws.com/0/94476/932307ba-b106-9e6b-f927-d6bcc88ba186.png)

3. `Destinations`にある`Add`ボタンをクリック
![platform_012.png](https://qiita-image-store.s3.amazonaws.com/0/94476/94b829f1-f9d9-ecdf-0bc8-73f359982b26.png)

4. `Type`に`Local application server`を選択、`Destination`に前述で作成したアプリケーションサーバ名を選択し、`Add`ボタンをクリック
![platform_013.png](https://qiita-image-store.s3.amazonaws.com/0/94476/27e6071e-3720-aa4f-076c-84fa25793e3f.png)

5. `Save`ボタンをクリック
![platform_014.png](https://qiita-image-store.s3.amazonaws.com/0/94476/a152ac21-dc00-4851-faca-1462db376254.png)


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

1. 左メニューの`Devices`をクリックし、`Create`ボタンをクリック
![platform_015.png](https://qiita-image-store.s3.amazonaws.com/0/94476/a147d1dc-175b-3716-6c84-83351a2a94a0.png)

2. デバイス情報①～⑩を入力し、`Create`ボタン（⑪）をクリック
![platform_016.png](https://qiita-image-store.s3.amazonaws.com/0/94476/3af08951-5721-a74d-dc0f-71df293dece5.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](https://qiita-image-store.s3.amazonaws.com/0/94476/959d1a12-09f1-cdaf-ddc3-9bce7f126e27.png)

※注
AppSKeyが未登録であったり、間違った（デバイス側に登録されているものと異なる）キーを登録してしまうと、デバイスが送信しているデータとプラットフォームで受信したデータが違う、という事象が発生します。（データの暗号/復号で異なるキーを利用しているので、まあ当然の結果ですが）
実は私自身もこの事象で半日程悩んでおり、本実証実験を先行して取り組んでいるUeno氏に相談したところ、[全く同じ事象でハマった](https://www.slideshare.net/KazUeno/lorawantm)らしく直ぐに解決することができました。
（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を確認/無視するコマンドも用意されています。

<dl>
<dt>MSG<dt>
<dd>テキスト送信用、ACKを無視</dd>
<dt>CMSG</dt>
<dd>テキスト送信用、ACKを確認</dd>
<dt>MSGHEX</dt>
<dd>HEX送信用、ACKを無視</dd>
<dt>CMSGHEX</dt>
<dd>HEX送信用、ACKを確認</dd>
</dl>

#通信試験結果の確認
プラットフォームに再度ログインし、ポータルページにある「Wireless Logger」を開きます。

![platform_018.png](https://qiita-image-store.s3.amazonaws.com/0/94476/06ebb152-6283-411a-79dd-eca4ec2cad90.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送信メソッド、ロギング処理等を追加しました。

```python: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()
```

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

```python: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」で、送信データを確認することができました。