DRAGINO LoRa/GPS HAT v1.4-JP を The Things Networkにつなぐ


# やったこと

- [The Things Network](https://www.thethingsnetwork.org/)(以下TTN)をLoRaWANサーバとして利用する
- LoRaWANゲートウェイとして [DRAGINO LoRa/GPS HAT v1.4-JP](https://jp.rs-online.com/web/p/communication-wireless-development-tools/1875121/) + ラズパイ３を利用する
- LoRaWANノードとしてEASEL社の温湿度センサ [ES920LRTH2](https://easel5.com/service/products-information/products/lora-related/es920lrth2/) を繋いで、動作確認を行う

# 構成

シンプルにGateway１台×温湿度センサユニットの構成で試してみます。

## LoRaWAN Gateway
![Gateway](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/c225feb2-a7a7-1aa3-ebc1-3a9d56c5b6b4.jpeg)

## LoRa Node (ES920LRTH2: 温湿度センサ)
![Node](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/826df968-d14b-8fc8-9ba8-04b39d47bce5.jpeg)

# ざっくり進め方

- [1. LoRaWAN ゲートウェイのセットアップ](#1-lorawan-ゲートウェイのセットアップ)
    - [1-1. ラズパイ３のSPIを有効にする](#1-1-ラズパイ３のspiを有効にする)
    - [1-2. Gatewayサーバ用のプログラムDual Channel LoRaWANのセットアップ](#1-2-gatewayサーバ用のプログラムdual-channel-lorawan-gatewayのセットアップ)
- [2. LoRaWAN サーバ(TTN)のセットアップ(1)](#2-lorawan-サーバttnのセットアップ1)
    - [2-1. Gatewayのセットアップと動作確認](#2-1-gatewayのセットアップと動作確認)
    - [2-2. アプリケーションのセットアップとDeviceの登録](#2-2-アプリケーションのセットアップとdeviceの登録)
- [3. LoRa ノード(温湿度センサ)のセットアップ](#3-lora-ノードes920lrth2-温湿度センサのセットアップ)
- [4. LoRaWAN サーバ(TTN)のセットアップ(2)](#4-lorawan-サーバttnのセットアップ2)
    - [4-1. DeviceのデバイスEUIを変更](#4-1-deviceのデバイスeuiを変更)
    - [4-2. 動作確認](#4-2-動作確認)

# 1. LoRaWAN ゲートウェイのセットアップ

ネットを探すといろいろ情報が出てきますが、まとまっているページがなかなか見つからなかったので、まずは一次資料である開発元のwikiを参考にしましたが、情報がかなり古く参考となったのはラズパイのSPIを有効にする部分のみでした。

> Configuration
> 
> - Connect the Raspberry Pi to the Internet;
> - Use 'raspi-config ' to ensure that SPI can be used on RPi ;
> - Use 'git clone git://git.drogon.net/wiringPi' to install the GPIO access library written in C for the BCM2835 used in the Raspberry Pi;
> - Get the single channel LoRa Gateway source code from here ;
> - Edit the 'main.cpp' to change configuration (look for: "Configure these values!").
> 
> 引用元）
> http://wiki.dragino.com/index.php?title=Use_Lora/GPS_HAT_%2B_RaspberryPi_to_set_up_a_Lora_Node

- wiringPiは現時点のRaspberryPI OSであればすでにインストール済み
- LoRa Gatewayサーバのサンプルソースが古かったので、他サイトを参照に[Dual Channel LoRaWAN Gateway](https://github.com/bokse001/dual_chan_pkt_fwd) を利用することにしました。


## 1-1. ラズパイ３のSPIを有効にする

`raspi-config` を利用して、SPIを有効にするだけ。

```
sudo raspi-config
```

`3. Interface Options` -> `P4 SPI` から有効にします。 

## 1-2. Gatewayサーバ用のプログラム[Dual Channel LoRaWAN Gateway](https://github.com/bokse001/dual_chan_pkt_fwd)のセットアップ

現時点(2021/01/24)で動かすには日本専用の設定変更に加えていろいろと変更しなければならない点があったのでまるっと修正したソースを[github](https://github.com/y-amadatsu/dual_chan_pkt_fwd/tree/features/edit_jp_setting)に上げました。

<details><summary>参考）ソースの修正箇所詳細について</summary><div>

https://github.com/bokse001/dual_chan_pkt_fwd をcloneして日本仕様に書き換えます[^1]

[^1]: DRAGINO LoRa/GPS HAT v1.4-JPはシングルチャネルなので、fork元の https://github.com/tftelkamp/single_chan_pkt_fwd を利用すべきだと思いましたがすでにメンテナンスされていないのでこちらを使うのが正解みたいです

まずは `global_conf.json` を書き換えます。

```JSON:global_conf.json
{
  "SX127x_conf":
  {
    "freq": 923400000,   <--- 日本の周波数帯（デフォルトチャンネル）
    "freq_2": 923200000, <---
    "spread_factor": 10, <--- SF値（拡散率）ここは調整してもよい
    "pin_nss": 6,    <--- DRAGINO LoRa/GPS HATのピンに合わせる
    "pin_dio0": 7,   <---
    "pin_nss_2": 6,  <---
    "pin_dio0_2": 7, <---
    "pin_rst": 3,    <---
    "pin_led1":4,
    "pin_NetworkLED": 22,
    "pin_InternetLED": 23,
    "pin_ActivityLED_0": 21,
    "pin_ActivityLED_1": 29
  },
  "gateway_conf":
  {
    "ref_latitude": 0.0,      <-- ここはGatewayの設置場所にあわせましょう
    "ref_longitude": 0.0,     <--
    "ref_altitude": 10,       <--

    "name": "your name",                     <-- このあたりも適切に
    "email": "a@b.c",                        <--
    "desc": "Single channel pkt forwarder",

    "interface": "eth0",      <-- ラズパイでインターネット接続に利用する

    "servers":
    [
      {
        "address": "router.jp.thethings.network",  <-- 日本向けに変更
        "port": 1700,
        "enabled": true
      },
      {
        "address": "router.as1.thethings.network", <-- TODO: 周波数帯からAS1を選択してみたけど、これでよいのか？
        "port": 1700,
        "enabled": false
      }
    ]
  }
}

```

serversは [ここ](https://www.thethingsnetwork.org/docs/gateways/packet-forwarder/semtech-udp.html#connect-a-gateway_server-addresses) を参照に書き換えました。


次に `dual_chan_pkt_fwd.cpp` を書き換えます。 
まずは定数定義。#defineの定義部分（230行目付近）に追加しました。

```CPP:dual_chan_pkt_fwd.cpp(230行目付近)

// Tx Power Register add 
// Power Setting for Japan ARIB STD-T108
// 20mW=13dBm
// SX1276 RegPaConfig(0x09), Val=0x3f
// bit 7 PaSelect = 0 select RFO
// bit 6-4 MaxPower = 3 Pmax=10.8+0.6*3=12.6 < 13
// bit 3-0 OutputPower = 0x0f Pout=Pmax-(15-0x0f)=12.6
// SX1272 RegPaConfig(0x09), Val=0x0e
// bit 7 PaSelect = 0 select RFO
// bit 6-4 unused = 0
// bit 3-0 OutputPower = 0xe Pout=-1 + OutputPower = -1 + 0x0e = 13dBm
#define PWR_JPN_1276             0x3f
#define PWR_JPN_1272             0x0e
```

実際の書き込み部分は433行目付近に入れました。

```CPP:dual_chan_pkt_fwd.cpp(433行目付近)
  // Set Tx Power for Japan
  if (sx1272) {
    WriteRegister(REG_PA_CFG,PWR_JPN_1272, CE);
  } else {
  // sx1276
    WriteRegister(REG_PA_CFG,PWR_JPN_1276, CE);
  }
```
ここでmakeしたところ下記のwarningが発生しました。

```bash
include/rapidjson/document.h:1652:24: warning: ‘void* memcpy(void*, const void*, size_t)’ writing to an object of type ‘rapidjson::GenericValue<rapidjson::UTF8<> >::Member’ {aka ‘struct rapidjson::GenericMember<rapidjson::UTF8<>, rapidjson::MemoryPoolAllocator<> >’} with no trivial copy-assignment; use copy-assignment instead [-Wclass-memaccess]
             std::memcpy(data_.o.members, members, count * sizeof(Member));
```

そこで、エラーが出ていた箇所を書き換えました。

```CPP:修正前）include/rapidjson/document.h(1630行目付近)
    // Initialize this value as array with initial data, without calling destructor.
    void SetArrayRaw(GenericValue* values, SizeType count, Allocator& allocator) {
        flags_ = kArrayFlag;
        if (count) {
            data_.a.elements = (GenericValue*)allocator.Malloc(count * sizeof(GenericValue));
            std::memcpy(data_.a.elements, values, count * sizeof(GenericValue));
        }
        else
            data_.a.elements = NULL;
        data_.a.size = data_.a.capacity = count;
    }
```

```CPP:修正後）
    void SetArrayRaw(GenericValue* values, SizeType count, Allocator& allocator) {
        flags_ = kArrayFlag;
        if (count) {
            // data_.a.elements = (GenericValue*)allocator.Malloc(count * sizeof(GenericValue));
            // std::memcpy(data_.a.elements, values, count * sizeof(GenericValue));
            auto arr = static_cast<GenericValue*>(allocator.Malloc(count * sizeof(GenericValue)));
            for (SizeType idx = 0; idx < count; ++idx)
                new (arr + idx) GenericValue;
            data_.a.elements = arr;
            std::copy_n(values, count, arr);
        }
        else
            data_.a.elements = NULL;
        data_.a.size = data_.a.capacity = count;
    }
```

`SetObjectRaw` も同じように警告がでていたので書き換えました。

今度はmakeが成功したので実行してみたところ、以下のエラーでサーバの起動に失敗しました。。。

```bash
pi@gateway:~/project/dual_chan_pkt_fwd $ ./dual_chan_pkt_fwd 
dual_chan_pkt_fwd: include/rapidjson/document.h:1398: unsigned int rapidjson::GenericValue<Encoding, Allocator>::GetUint() const [with Encoding = rapidjson::UTF8<>; Allocator = rapidjson::MemoryPoolAllocator<>]: Assertion `flags_ & kUintFlag' failed.
中止
```

修正方法がちょっとわからなかったので、さくっとASSERTをコメントアウトしてみました。。。誰か正しい修正方法を教えて下さい😢

```CPP:修正前）include/rapidjson/document.h(1396行目付近)
    int GetInt() const          { RAPIDJSON_ASSERT(flags_ & kIntFlag);   return data_.n.i.i;   }
    unsigned GetUint() const    { RAPIDJSON_ASSERT(flags_ & kUintFlag);  return data_.n.u.u;   }
    int64_t GetInt64() const    { RAPIDJSON_ASSERT(flags_ & kInt64Flag); return data_.n.i64; }
    uint64_t GetUint64() const  { RAPIDJSON_ASSERT(flags_ & kUint64Flag); return data_.n.u64; }
```

```CPP：修正後
    int GetInt() const          { RAPIDJSON_ASSERT(flags_ & kIntFlag);   return data_.n.i.i;   }
    //unsigned GetUint() const    { RAPIDJSON_ASSERT(flags_ & kUintFlag);  return data_.n.u.u;   }
    unsigned GetUint() const    { return data_.n.u.u;   }
    int64_t GetInt64() const    { RAPIDJSON_ASSERT(flags_ & kInt64Flag); return data_.n.i64; }
    uint64_t GetUint64() const  { RAPIDJSON_ASSERT(flags_ & kUint64Flag); return data_.n.u64; }
```
一応これで動いているのでとりあえず良しとします😇

</div></details>

### 1-2-1. 修正箇所

上記ソースコードを元にした、個別の修正箇所を記載します。

#### 必須

`global_conf.json` の以下の箇所を修正してください

```JSON:global_conf.json
...省略...
  "gateway_conf":
  {
    "ref_latitude": 0.0,      <-- ここはGatewayの設置場所にあわせましょう
    "ref_longitude": 0.0,     <--
    "ref_altitude": 10,       <--

    "name": "your name",                     <-- このあたりも適切に
    "email": "a@b.c",                        <--
    "desc": "Single channel pkt forwarder",

    "interface": "eth0",      <-- ラズパイでインターネット接続に利用する
```


#### 任意

ソースを展開したパスに併せて `dual_chan_pkt_fwd.service` を修正してください。修正しない場合、 `make install` で失敗する場合があります。

```dual_chan_pkt_fwd.service
WorkingDirectory=/home/pi/dual_chan_pkt_fwd/
ExecStart=/home/pi/dual_chan_pkt_fwd/dual_chan_pkt_fwd 
```

### 1-2-2. ソースコードのコンパイル

普通に make & make install する流れでOKです。

```shell-session
$ cd dual_chan_pkt_fwd
$ make
$ make install
sudo cp -f ./dual_chan_pkt_fwd.service /lib/systemd/system/
sudo systemctl enable dual_chan_pkt_fwd.service
sudo systemctl daemon-reload
sudo systemctl start dual_chan_pkt_fwd
sudo systemctl status dual_chan_pkt_fwd -l
● dual_chan_pkt_fwd.service - Lora Packet Forwarder
   Loaded: loaded (/lib/systemd/system/dual_chan_pkt_fwd.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2021-01-24 14:52:03 JST; 5s ago
 Main PID: 19602 (dual_chan_pkt_f)
    Tasks: 1 (limit: 2063)
   CGroup: /system.slice/dual_chan_pkt_fwd.service
           └─19602 /home/pi/project/dual_chan_pkt_fwd/dual_chan_pkt_fwd

 1月 24 14:52:03 gateway systemd[1]: Started Lora Packet Forwarder.
```

上記のようにログが出れば起動OKです！

後々の処理のため、一旦サーバを停止しておきましょう。

```shell-session
$ sudo systemctl stop dual_chan_pkt_fwd 
```

ここで、TTNへのゲートウェイへの登録に必要なゲートウェイのEUIを確認します。直接動かすとサーバ起動時に表示されますので以下で確認してください

```
$ ./dual_chan_pkt_fwd
server: .address = router.jp.thethings.network; .port = 1700; .enable = 1
server: .address = router.as1.thethings.network; .port = 1700; .enable = 0
Gateway Configuration
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  Single channel pkt forwarder
  Latitude=xxxxxxx
  Longitude=xxxxxxxx
  Altitude=0
  Interface: eth0
Trying to detect module CE0 with NSS=6 DIO0=7 Reset=3 Led1=unused
SX1276 detected on CE0, starting.
Trying to detect module CE1 with NSS=6 DIO0=7 Reset=3 Led1=unused
SX1276 detected on CE1, starting.
Gateway ID: XX:XX:XX:XX:XX:XX:XX:XX      <---- ここがゲートウェイEUIなのでメモっておいてください！
Listening at SF10 on 923.400000 Mhz.
Listening at SF10 on 923.200000 Mhz.
-----------------------------------
stat update: 2021-01-24 09:57:17 GMT no packet received yet
```

このあとGatewayを登録しますので、起動したままにしておきましょう。

# 2. LoRaWAN サーバ(TTN)のセットアップ(1)

## 2-1. Gatewayのセットアップと動作確認

ゲートウェイを登録します。
ゲートウェイEUIには先にメモっておいたものを入力してください。

![console_thethingsnetwork_org_gateways_register__1_-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/ca116cbc-d356-6080-b717-f316e6887c9b.png)

登録後下記のようにステータスが接続済みになれば登録完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/4f0632a9-4d91-8a92-acb6-97269bf883e1.png)

## 2-2. アプリケーションのセットアップとDeviceの登録

アプリケーションを作成します。

![2-2-1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/0b8e6701-78d4-0152-0211-3c7775e5dad5.png)

アプリケーション作成後、デバイスを登録します。

![2-2-2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/34e8d6d7-3c9b-6611-049b-c4871f8eb8e1.png)

※なお、デバイスEUIは自動生成しますがい、あとで変更します。

登録後、デバイスの設定をES920LRTH2向けに変更しておきます。

![2-2-3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/66593c67-79f2-90ca-a337-aa3330229116.png)

登録完了後下記画面となりますので、

- デバイスアドレス
- ネットワークセッションキー
- Appセッションキー

をメモしておいてください。このあとES920LRTH2で利用します。

![2-2-4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/d5b744f3-83fc-5778-4f64-2450582ff2ba.png)


ちなみに独自に生成できない／しないほうがよい項目は下記の２個です。混乱の元となるのでメモしておきます。

<dl>
  <dt>DevEUI（TTNコンソール：デバイスEUI）</dt>
  <dd>ES920LRTH2の出荷時にデフォルトセットされている。変更もできるが変えないほうがよい。ES920LRTH2の値を見てTTNコンソール側を書き換える。</dd>
  <dt>DevAddr（TTNコンソール：デバイスアドレス）</dt>
  <dd>TTNコンソールでアプリケーションからデバイス登録する際にTTN側で発行する。ES920LRTH2側で設定する。</dd>
</dl>

# 3. LoRa ノード(ES920LRTH2: 温湿度センサ)のセットアップ

ES920LRTH2のドキュメントを元にシリアル接続し、下記値をセットしてください。

- Activate
    - **Activation by Personalization** をセット
- DevAddr
    - [上記でメモった](#2-2-アプリケーションのセットアップとdeviceの登録) **デバイスアドレス** をセット
- NwkSKey
    - [上記でメモった](#2-2-アプリケーションのセットアップとdeviceの登録) **ネットワークセッションキー** をセット
- AppSKey
    - [上記でメモった](#2-2-アプリケーションのセットアップとdeviceの登録) **Appセッションキー** をセット
- Acknowledge
    - **OFF** をセット（ちょっと触った限りでは、Acknowledgeが受け取れなかった為）

その他はデフォルトで構いません。また、以降の処理で使いますので、 `DevEUI` を確認しておいてください。

```console:参考)
LoRaWAN > y
  configuration setting is below
  -------------------------------------
  Class                       : Class A
  ADR                         : ON
  Activate                    : Activation by Personalization
  DevEUI                      : xxxxxxxxxxxxxxxx              <--- ここをメモっておく
  AppEUI                      : 0000000000000000
  AppKey                      : 00000000000000000000000000000000
  DevAddr                     : xxxxxxxx
  NwkSKey                     : xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  AppSKey                     : xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  Acknowledge                 : OFF
  Default Data rate           : DR2
  Output Power                : 13dBm
  Send Interval               : 60
```

# 4. LoRaWAN サーバ(TTN)のセットアップ(2)
## 4-1. DeviceのデバイスEUIを変更

上記処理で取得した `DevEUI` でデバイスEUIを更新します。

![4-1-1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/00fa5f9b-71f5-2c3f-3566-9180ff6dee74.png)

これで終了です。これで以下のようにデバイスの詳細画面でステータスに時間が表示されば登録が完了したことが確認できます。

![4-1-2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/9438a7ad-f16e-3512-189f-fc39e657f04e.png)

ES920LRTH2側がきちんと動作していればアプリケーション一覧の「データ」タブでpayloadが飛んでいることが確認できると思います。

## 4-2. 動作確認

上記手順でバイナリデータを受信できるようになりましたが、目で見えるようにPayloadのdecoder/converterを書き換えましょう。

![4-2-1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/314371bb-08e4-019f-3a11-5ffd65e812bc.png)


```javascript:decoder
function Decoder(bytes, port) {
  // Decode an uplink message from a buffer
  // (array) of bytes to an object of fields.
  //var decoded = {};

  var decoded = {
    "Temperature": (bytes[1] << 8) + bytes[0],
    "Humidity": (bytes[3] << 8) + bytes[2],
    "Voltage": (bytes[5] << 8) + bytes[4]
  };

  // if (port === 1) decoded.led = bytes[0];

  return decoded;
}
```

```javascript:converter
function Converter(decoded, port) {
  // Merge, split or otherwise
  // mutate decoded fields.
  var converted = decoded;

  // if (port === 1 && (converted.led === 0 || converted.led === 1)) {
  //   converted.led = Boolean(converted.led);
  // }
  
  roundup = function(value, digit) {
    digits = Math.pow(10, digit);
    return Math.round(value * digits) / digits;
  }
  
  converted.Temperature = -45 + (175 * converted.Temperature / (Math.pow(2, 16) - 1));
  converted.Humidity = 100 * converted.Humidity / (Math.pow(2, 16) - 1)
  converted.Voltage = 3.0 / (Math.pow(2, 12) - 1) * converted.Voltage;

  converted.Temperature = roundup(converted.Temperature, 2)
  converted.Humidity = roundup(converted.Humidity, 2)
  converted.Voltage = roundup(converted.Voltage, 2)

  return converted;
}
```

こうすることで、下記のようにアプリケーションデータで、センサー値を確認することができます。

![4-2-2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253654/88ffbfa4-1a48-5590-5dd8-4fe82d22156d.png)

（自分に向けて）お疲れさまでした😭

## その他メモ

- Cayenneにつなげてみたかったが、TTNのconverterがバイナリデータを返せないので断念
- Acknowledgeの受け取り方が知りたい

## 主な参照元

- Lora/GPS HAT
  - https://wiki.dragino.com/index.php?title=Lora/GPS_HAT

- Use Lora/GPS HAT + RaspberryPi to set up a Lora Node
  - http://wiki.dragino.com/index.php?title=Use_Lora/GPS_HAT_%2B_RaspberryPi_to_set_up_a_Lora_Node

※この他にもいっぱいあったのですがメモし忘れました…