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

#はじめに

某所より、「今度`Fukuoka City LoRaWAN`ってゆう実証実験ば福岡市内を中心にやりますけん、おたくも都合ば付けてやらんですか？（注：筆者脳内訳）」というお誘いが当社にあったそうで、ふたつ返事で絶賛参加することになったようです。

そこで部署内の誰がこの件を担当するか、という事になり、「ローラ？なにそれ美味しいの？もしかして『オッケー』が口癖の女性タレント！？」状態で予備知識ゼロな私に、何故かRaspberry Piを触ったことがあるというだけで、白羽の矢が立ってしまいました。

という訳（？）で、ラズパイとLoRaWANモデムを繋いでPythonから通信し、既存DWHへデータを格納できるかチャレンジしたいと思います。

まず今回は、Raspberry PiとLoRaWAN Modemを接続し、最初はシリアルコンソール越しに、次にPythonプログラムからシリアル通信できるか確認したいと思います。

#最終目標的なトポロジ
下図のようにAWSを利用することで、アプリケーションサーバを構成したいと思います。
![Topology.png](https://qiita-image-store.s3.amazonaws.com/0/94476/60d7d12e-5499-cbcb-5a96-81d5b5889e69.png)


#用意したもの

- Raspberry Pi 3 Model B （[Complete Starter Kit](http://amzn.asia/9vutlbE)）
- LoRaWAN Modem (RigingHF社製RHF3M076)　※運営事業者（NTTネオメイト様）からお借りしました。


#開発環境

- Raspbian GNU/Linux 9 (stretch)
- Python 3.5.3

#ラズパイとモデム接続

ラズパイのUSBポートにモデムを接続して、ラズパイを起動します。
※事前にネットワーク周りは設定済みです。
![IMG_0582.JPG](https://qiita-image-store.s3.amazonaws.com/0/94476/295993ec-3142-3953-219c-8080f47eb898.jpeg)

#シリアル通信できるようにする
Raspbianでは、デフォルトで`Serial`が無効化されていますので、有効化します。

```
$ sudo raspi-config
```

`5 Interfacing Opsions`を選択
![2017-10-24_17h34_41.png](https://qiita-image-store.s3.amazonaws.com/0/94476/894bcb49-bd27-f0ad-dc9d-8d8b9c07135f.png)

`P6 Serial`を選択
![2017-10-24_17h35_51.png](https://qiita-image-store.s3.amazonaws.com/0/94476/e21ba562-adb3-75dc-1013-a10c356b94c1.png)

`<はい>`（または`<Yes>`）を選択
![2017-10-24_17h36_39.png](https://qiita-image-store.s3.amazonaws.com/0/94476/fae083ed-63b3-1a60-f469-ef73ee04700b.png)

`<了解>`（または`<Ok>`）を選択
![2017-10-24_17h37_32.png](https://qiita-image-store.s3.amazonaws.com/0/94476/6a0ebf75-e7e8-a259-128b-57082bd952bd.png)

#モデムを認識しているか確認

この時点でOSがモデムを認識しているか確認してみます。

モデムを接続していない場合

```
$ lsusb
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```

モデムを接続している場合

```
$ lsusb
Bus 001 Device 005: ID 0483:5740 STMicroelectronics STM32F407
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```

どうやら`RHF3M076`は、OS上では`STM32F407`というProduct名で認識されるようです。
恐らくSTマイクロ社製の同チップが乗ってるのでしょう。

更にSerial通信用のポート（デバイスファイル）が出来ているか確認してみます。

```
$ ls -l /dev/tty*
･･･
crw-rw---- 1 root dialout 166,  0 10月 23 11:22 /dev/ttyACM0
･･･
```

モデムを外した場合、上記デバイスが消えるので、この `/dev/ttyACM0` で間違いなさそうです。

#モデムにシリアル端末から接続してみる
## シリアル端末プログラムの準備
CUI環境でシリアルコンソールを使いたいので `Minicom`をインストールします。

```
$ sudo apt-get install minicom
```

インストールが完了したら初期設定します。

```
$ sudo LANG=c minicom -s
```

`Serial port setup`を選択
![2017-10-25_10h50_40.png](https://qiita-image-store.s3.amazonaws.com/0/94476/28aff263-8bd1-0026-ae27-f8323872ca0f.png)

`A - Serial Device`行のデバイス名を`/dev/ttyACM0`に変更
![2017-10-25_10h52_37.png](https://qiita-image-store.s3.amazonaws.com/0/94476/fdffe5ea-6047-449a-44c3-d0cdd4b65702.png)

`Screen and keyboard`を選択
![2017-10-25_10h53_53.png](https://qiita-image-store.s3.amazonaws.com/0/94476/f7d5520d-1d1a-a7b1-8995-8e5a05aaefa5.png)

デフォルトではコンソールから入力したコマンドが表示されない（エコーバックしない）ため、ローカルエコーするよう変更
`Q - Local echo`を`Yes`に変更（[Q]キーを押すごとに`Yes`、`No`が切り替わる）
![2017-10-25_10h54_56.png](https://qiita-image-store.s3.amazonaws.com/0/94476/17b6650b-af5e-ec3c-9d51-d6678b8a6037.png)

`Save setup as dfl`を選択（設定値をデフォルト値として保存）
![2017-10-25_10h55_46.png](https://qiita-image-store.s3.amazonaws.com/0/94476/50ce0ae7-68cf-2e45-31cd-b597c2ab4620.png)

`Exit`を選択（`Exit from minicom`を選択すると、Minicom自体も終了）
![2017-10-25_10h56_44.png](https://qiita-image-store.s3.amazonaws.com/0/94476/5f68244d-2522-9048-4c79-0ec663278a0f.png)

`Exit`を選択し、以下の表示に切り替わればモデムへコンソール接続された状態となります。

```
Welcome to minicom 2.7

OPTIONS: I18n
Compiled on Apr 22 2017, 09:14:19.
Port /dev/ttyACM0, 14:33:04

Press CTRL-A Z for help on special keys
```

ちなみにコンソール接続中、[ctrl]+[A]、[Z]キーでコマンドサマリが表示され、更に[Q]キーを押すとMinicomを終了（コンソール切断）することができます。

また再度コンソール接続するには、次のコマンドを入力します。

```
$ sudo LANG=c minicom
```

#ATコマンドで操作してみる
##改行コードの変更（TeraTermの場合）

RHF3M076では、ATコマンドの終端には必ず改行コード（`<LF>(\n)`、または`<CR><LF>(\r\n)`）が必要です。
※ちなみにモデムからのレスポンスメッセージには、行終端に`<CR><LF>(\r\n)`が付いていました。

そこでモデムをATコマンドで操作する前に、Raspberry Pi にSSH接続しているターミナル（今回はTeraTerm）で改行コードを設定しなおす必要があります。

`設定(S)` - `端末(T)`を選択
![2017-10-25_11h28_54.png](https://qiita-image-store.s3.amazonaws.com/0/94476/dd4ed7f5-ff0e-4bba-5d65-ea82adbfcc7f.png)

受信、送信の改行コードを、下図のように設定
![2017-10-25_11h30_46.png](https://qiita-image-store.s3.amazonaws.com/0/94476/e269455d-0585-24bd-1145-cf734b494af0.png)

##コマンドを入力してみる
それではコンソールから`AT`（エンターキー）と入力してみます。

```
AT
+INFO: Input timeout, start parse
+AT: OK
```

何故か最後のエンターキーを押す前に、レスポンスが返ってきました。
他のATコマンドを試そうとしても、最初の`AT`2文字を打った時点で、必ずレスポンスが返ってきます。
また`+AT: OK`メッセージの前に、`+INFO: Input timeout, start parse`というメッセージが返ってきていますので、直感的にこれが原因だろうと推測しました。

そこで色々調べたところ、これは「UART受信タイムアウト機能」というものらしく、モデムが`AT`文字を受信した直後から予め設定されたタイムアウト値（ミリ秒)に達すると、強制的に受信終了する機能だそうです。
（このタイムアウト設定値は`AT+UART=TIMEOUT`コマンドで確認できます。）

このままだと手動でATコマンドを試すことができないので、この機能を一時的に無効にするために次のATコマンドを入力します。
但し、コンソールから直接入力すると上記の二の舞になりますので、まずはエディタ等でコマンドを入力し、それをコンソールにコピペすることにします。

```
AT+UART=Timeout,0
```

うまくタイムアウト機能を無効化できれば、次のようなレスポンスが返ってきます。

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

これで、ATコマンドで設定や設定値を確認できるようになりました。

試しにモデムに登録されているID（DevAddr、DevEui、AppEui）を確認してみます。

```
AT+ID
+ID: DevAddr, 01:23:45:67
+ID: DevEui, aa:bb:cc:dd:ee:ff:aa:bb
+ID: AppEui, ab:cd:ef:ab:cd:ef:ab:cd
```

尚、ATコマンドの詳しいリファレンスは[ここから](https://market.thingpark.com/iot-devices/modems-bridges/risinghf-lorawan-modem-rhf3m076-us915)ダウンロードできます。

#Pythonからモデムと通信してみる

シリアル通信でモデムをATコマンドを使って操作できるようになりましたので、次はPythonからモデムとシリアル通信してみます。

##独自モジュール作成

`pySerial`モジュールを利用して、モデムをATコマンドで操作できるようにする独自モジュール`RHF3M076.py`を作成します。

尚、私の環境ではすでに`pySerial`モジュールがインストールされていましたが、もしインストールされていない場合は次のコマンドでインストールしましょう。

```
$ sudo pip install pySerial
```

そして以下が実際に作成した`RHF3M076.py`のコードです。
この時点では解りやすくするため、単にIDを取得する為だけの機能に限定したモジュールにしました。

```python:RHF3M076.py
#!/usr/bin/env python3
# coding: utf-8

import serial
import re

# 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._open()
        self._getId()

    # Open serial port
    def _open(self):
        self._modem = serial.Serial(
            port=self._port,
            baudrate=self._baud,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=self._timeout,
            xonxoff=False,
            rtscts=False,
            write_timeout=None,
            dsrdtr=False,
            inter_byte_timeout=None
        )
        return()

    # Get IDs
    def _getId(self):
        cmd = 'AT+ID'
        self._write(cmd)

        lines = self._read()
        print(lines)
        for line in lines:
            res = re.match(self._ptnDevAddr, line)
            if res:
                self._DevAddr = res.group(1)
                continue
            res = re.match(self._ptnDevEui, line)
            if res:
                self._DevEui = res.group(1)
                continue
            res = re.match(self._ptnAppEui, line)
            if res:
                self._AppEui = res.group(1)

    # Send to serial port
    def _write(self, cmd):
        cmd = cmd + self._crlf
        ret = self._modem.write(cmd.encode())
        return(ret)

    # Receive from serial port
    def _read(self):
        result = []
        lines = self._modem.readlines()
        for line in lines:
            result.append(line.decode().replace(self._crlf, ''))
        return(result)

    @property
    def DevAddr(self):
        return(self._DevAddr)

    @property
    def DevEui(self):
        return(self._DevEui)

    @property
    def AppEui(self):
        return(self._AppEui)
       
    # Destructor
    def __del__(self):
        self._modem.close()
```
尚、`_read`、`_write`メソッドの中で文字列をencode/decodeしていますが、これはプログラムからモデムと通信する場合、改行コードの関係上bytes型でやり取りする必要がある為です。

##テスト用スクリプトを作成し、動かしてみる

モデム用モジュールをテストするスクリプトを書きます。

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

from RHF3M076 import RHF3M076

def main():
    modem = RHF3M076()

    print('DevAddr: ' + modem.DevAddr)
    print('DevEui: ' + modem.DevEui)
    print('AppEui: ' + modem.AppEui)

    modem = None
    return()

if __name__ == '__main__':
    main()
```

以下がテストスクリプトを実行してみた結果です。
Pythonとモデム間で、問題なく通信できているようです。

```
$ chmod +x main.py
$ ./main.py
DevAddr: 01:23:45:67
DevEui: aa:bb:cc:dd:ee:ff:aa:bb
AppEui: ab:cd:ef:ab:cd:ef:ab:cd
```