SeeedのLoRaWANネットワーク向けLoRa-E5モジュールを用いた風速/風向計の開発

概要

このプロジェクトでは、Seeed LoRa-E5ベースのArduino風速/風向計を開発し、Digi-Key本社ビルの屋上にある気象プラットフォームに取り付け可能な屋外筐体に収納するまでの過程を説明します。また、既存のChirpStackベースのプライベートLoRaWANネットワークとMachinechatのJEDI Pro IoTソフトウェアプラットフォームにセンサノードを追加することも取り上げます。LoRaセンサは、Seeeduino Xiao、Grove LoRa-E5無線ボード、SparkfunのWeather Meterキットの風速/風向計のハードウェアを使用しています。LoRaWANネットワークは、SeeedのIP67規格の産業用LoRaWANゲートウェイを使用して、LoRaセンサパケットをSeeedのReServer(Ubuntu linuxをインストール)上で動作するプライベートChirpStack LoRaWANネットワークサーバに転送しています。MachinechatのJEDI Pro IoTプラットフォームは、同じReServer上で動作しています。

ハードウェア

  • Seeed reServer
    reServerはODYSSEY X86 v2ボードをベースに、Intel® Core™ 第11世代 i3およびIntel UHD Graphics 48EUsを搭載しています。
  • Seeed SenseCAP Outdoor Gateway - LoRaWAN US915MHz
    SenseCAP LoRaWAN Gatewayは、LoRaWAN®プロトコルに基づき、低電力、長距離の環境データ収集とモニタリングに適用されます。
  • Seeed Grove LoRa-E5 board
    STM32WLE5JC、SX126x LoRa RF Groveプラットフォーム評価用拡張ボード
  • Seeed Seeeduino Xiao board
    SAMD21G18 Seeeduino XIAOシリーズ ARM® Cortex®-M0 MCU 32ビット組込み評価ボード
  • Sparkfun Weather Meter Kit
    このキットでは、気象計測の中核となる風速、風向、雨量の3つを提供します(注:この記事では風速計と風向計のセンサを使用しています。雨量計のセンサは今後の記事で紹介する予定です)。

ソフトウェア

  • JEDI ProまたはJEDI Pro SSE
    IoTデータの収集、可視化、モニタリング、データストレージのための適応型ソフトウェアで、IoTソリューションに組み込むことができます。センサ、デバイス、マシンからのデータ収集、直感的なリアルタイムおよび履歴データ、システムビューダッシュボードの構築、データ状態を自動的に監視し対応するためのルール作成、メールやSMSによるアラート通知の受信などの機能を備えています。JEDI Pro SSEは、JEDI ProにSeeedのSenseCAP LoRaWANセンサ用のData Collectorを追加したSeeed Studio Editionバージョンです。
  • ChirpStack
    ChirpStackはオープンソースのLoRaWANネットワークサーバスタックで、LoRaWANネットワーク用のコンポーネントをオープンソースで提供しています。モジュラーアーキテクチャにより、既存のインフラストラクチャに統合することが可能です。
  • Arduino
    Arduinoは、使いやすいハードウェアとソフトウェアをベースにした、オープンソースの電子機器プラットフォームです。

背景

この記事は、TechForumの関連投稿「MachinechatとSeeed SenseCAPによるプライベートLoRaWANセンサネットワークの構築」を基にした後続プロジェクトで、Digi-Keyで入手できる既製のハードウェアとソフトウェアを使用してプライベートLoRaWAN IoTセンサネットワークを構築する詳細が記載されています。ソフトウェアとしては、MachinechatのJEDI Proアプリケーションソフトウェア、ChirpStackのLoRaWANネットワークサーバアプリケーションが使用されました。ハードウェアは、Seeed reServer x86サーバとSenseCAP屋外用LoRaWANゲートウェイが使用されました。この記事では、Seeeduino Xiao、Grove LoRa-E5ボード、Sparkfunの気象計キットを使って、LoRaWANネットワーク経由で風速と風向を報告するように設定しています。
Sparkfun気象計キット 風速/風向計
風速は、磁石を回転させることで、接点を1回転に1回閉じるカップ式風速計で測定します。1秒間に1回接点が閉じれば、風速は1.492MPH(0.66m/s)に相当します。風向計ケーブルのRJ-11コネクタの2本の内側の導線(ピン2、3)を介して接点の閉状態を監視することができます。
風向は、風向計にある8つのスイッチとそれぞれの抵抗で決定されます。風向計に内蔵された磁石により、風向きが変わると各スイッチが閉じられます。 外部抵抗で分圧を行い、出力電圧を監視して風向計の位置を知ることができます。RJ-11コネクタの2本の外側の導線(ピン1、4)は風向計に使用されます(注:詳細な情報は、SparkfunのWeather Meter Hookup Guideをご覧ください)。
Digi-Key気象プラットフォーム
Digi-Key気象プラットフォームについての詳細は次のリンクをご覧ください。Digi-Key Weather Platform

インプリメンテーション

このプロジェクトでは、外部USB 5V電源がSeeeduino Xiaoに電力を供給し、Xiaoの3.3V出力がGrove LoRa-E5ボードと気象センサハードウェアに電力を供給しています。XiaoのUARTはGrove LoRa-E5のTX/RXピンに、Xiaoの入力ピン1と2は風向計と風速計センサに接続されています。
XiaoとLoRa-E5ボードを屋外用筐体(Hammod Manufacturing 品番RL6225)に取り付け、M8パネルマウントコネクタとケーブルアセンブリを使って+5V電源と風速/風向計センサを接続しました。
Arduinoのアプリケーションコードは、Seeeduino Xiao上で実行され、LoRa-E5にATコマンドを送り、入力ピン1、2の監視を行っています。 アプリケーションはLoRaネットワークに接続して、入力ピンを監視して風速/方向データを計算し、データをCayenneLPPフォーマットでエンコードしてLoRaで送信し、遅延させて次の読み取りをループバックします。下図は、回路がどのように配線され、実装されているかを示したものです(Scheme-itプロジェクト:LoRaE5_Xiao_WindSensorM8cables を参照)。

モジュールと風速/風向センサコネクタJ1間の電気的接続を以下に示します。

Seeeduino Xiaoピン Grove LoRa-E5コネクタ 風速/風向センサコネクタJ1
GND GND 1
3V3 VCC 2
1 4 (風向計)
2 3 (風速計)
6 RX
7 TX

Seeeduino Xiao、Grove LoRa-E5、および風速/風向計のセットアップ

1 - Seeeduino XiaoにArduinoをセットアップします。 Getting started with Seeeduino Xiaoを参照。

2 - アプリケーションに必要なライブラリをインストールします。Arduinoのライブラリマネージャでライブラリを追加します。

3 - コードウォークスルー(ファイル名: Xiao_LoraE5CayenneLPP_WindRev1.ino)
初期設定

// below code is based on Seeed LoRaE5 example code and modified to NOT use the display on the Seeeduino Xiao expansion board (just uses a Xiao connected to a LoRaE5 
// Grove expansion board. Seeed example code at https://wiki.seeedstudio.com/Grove_LoRa_E5_New_Version/
//
//note: all Seeed LoRaE5 Grove boards have example code App key of "2B7E151628AED2A6ABF7158809CF4F3C" so needs to be changed
// this version changes out the DHT11 sensor for Sparkfun weather meter kit for wind speed and direction

// Anemometer measuring code based on below:
//More Information at: https://www.aeq-web.com/
//Version 2.0 | 11-NOV-2020
//
//SBR modifications
//SensorPin is connected to RC pulldown circuit on anemometer switch
//Wind vane pin is connected to voltage divider circuit for vane resistors


#include <Arduino.h>
#include <CayenneLPP.h>


// anemometer and direction parameters
const int RecordTime = 3; //Define Measuring Time (Seconds)
const int SensorPin = 2;     // the number of the sensorpin
const int VanePin = 1;      // pin# connected to wind vane
const int ledPin =  13;      // the number of the LED pin
int InterruptCounter;
float WindSpeed;
float DirWind;   //wind direction voltage
float angleWind; //wind vane angle
  

CayenneLPP lpp(51);   //setup Cayenne LPP (low power payload) buffer - per documentation 51 bytes is safe to send
 
static char recv_buf[512];
static bool is_exist = false;
static bool is_join = false;
static int led = 0;

int buf_size;  //Cayenne LPP buffer payload size
int Pointer;   //pointer used in Cayenne LPP buffer
int Offset = 12;    //offset to where Cayenne LPP payload data starts
int Loop1;       //loop counter in LoRa payload builder
int Loop2;       //loop counter in LoRa payload builder
int Loop3 = 0;       //loop counter in LoRa parameter send

 
static int at_send_check_response(char *p_ack, int timeout_ms, char *p_cmd, ...)
{
    int ch;
    int num = 0;
    int index = 0;
    int startMillis = 0;
    va_list args;
    memset(recv_buf, 0, sizeof(recv_buf));
    va_start(args, p_cmd);
    Serial1.printf(p_cmd, args);
    Serial.printf(p_cmd, args);
    va_end(args);
    delay(200);
    startMillis = millis();
 
    if (p_ack == NULL)
    {
        return 0;
    }
 
    do
    {
        while (Serial1.available() > 0)
        {
            ch = Serial1.read();
            recv_buf[index++] = ch;
            Serial.print((char)ch);
            delay(2);
        }
 
        if (strstr(recv_buf, p_ack) != NULL)
        {
            return 1;
        }
 
    } while (millis() - startMillis < timeout_ms);
    return 0;
}
 
static void recv_prase(char *p_msg)
{  
    if (p_msg == NULL)
    {
        return;
    }
    char *p_start = NULL;
    int data = 0;
    int rssi = 0;
    int snr = 0;
 
    p_start = strstr(p_msg, "RX");
    if (p_start && (1 == sscanf(p_start, "RX: \"%d\"\r\n", &data)))
    {
        Serial.println(data);
        led = !!data;
        if (led)
        {
            digitalWrite(LED_BUILTIN, LOW);
        }
        else
        {
            digitalWrite(LED_BUILTIN, HIGH);
        }
    }
 
    p_start = strstr(p_msg, "RSSI");
    if (p_start && (1 == sscanf(p_start, "RSSI %d,", &rssi)))
    {
         Serial.println(rssi);
    }
    p_start = strstr(p_msg, "SNR");
    if (p_start && (1 == sscanf(p_start, "SNR %d", &snr)))
    {
        Serial.println(snr);
    }
}

 
void setup(void)
{
    Serial.begin(9600);
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, HIGH);

    // anemeometer and wind vane setup
    pinMode(SensorPin, INPUT); //use input to count interrupts of anemometer
    pinMode(VanePin, INPUT); //use input to read analog voltage of wind vane output
 
    Serial1.begin(9600);                  //UART serial1 connection to LoRaE5
    Serial.print("E5 LORAWAN TEST\r\n");
 
    if (at_send_check_response("+AT: OK", 100, "AT\r\n"))
    {
        is_exist = true;
        at_send_check_response("+ID: AppEui", 1000, "AT+ID\r\n");
        at_send_check_response("+MODE: LWOTAA", 1000, "AT+MODE=LWOTAA\r\n");
        //at_send_check_response("+DR: EU868", 1000, "AT+DR=EU868\r\n");  //original
        at_send_check_response("+DR: US915", 1000, "AT+DR=US915\r\n"); 
        //at_send_check_response("+CH: NUM", 1000, "AT+CH=NUM,0-2\r\n");  //original
        at_send_check_response("+CH: NUM", 1000, "AT+CH=NUM,8-15,65\r\n"); // configure channels to match chirpstack
        at_send_check_response("+KEY: APPKEY", 1000, "AT+KEY=APPKEY,\"2B7E151628AED2A6ABF7158809CF4F3C\"\r\n");
        at_send_check_response("+CLASS: C", 1000, "AT+CLASS=A\r\n");
        at_send_check_response("+PORT: 8", 1000, "AT+PORT=8\r\n");
        delay(200);
        is_join = true;
    }
    else
    {
        is_exist = false;
        Serial.print("No E5 module found.\r\n");
    }
 
}
 

メインループ - 風速と風向きを決定し、CayenneLPPフォーマットでエンコードしてLoRaペイロードデータとして送信

void loop(void)
//void loop()
{
     
    // measure windspeed
    measure();
    // measure wind vane voltage
    DirWind = analogRead(VanePin)*3.33;
    // calculate wind direction in degrees
    vaneAngle();
    
    
    Serial.print("Windspeed: ");
    Serial.print(WindSpeed);
    Serial.print(" mph ");
    Serial.print("Wind Direction: ");
    Serial.print(angleWind);
    Serial.println(" degrees");


//***************** start of Cayenne LPP code **************************
    // due to character byte limitatations in LoRa payload buffer, need to alternate between 
    // the two wind parameters when sending out parameter payload
    
    Serial.print("Loop3 = ");  //debug code
    Serial.println(Loop3);    //debug code

    if (Loop3 == 0)
    {
      lpp.reset();
      lpp.addAnalogOutput(1, WindSpeed);  //channel 1, windspeed value
      Loop3 = 1;
    }
    else
    {
      lpp.reset();
      lpp.addAnalogOutput(2, angleWind);  //channel 2, wind direction value
      Loop3 = 0;
    }
   

    buf_size = lpp.getSize();
    Serial.print("Cayenne LPP buffer size = ");
    Serial.println(buf_size);

    uint8_t *payload = lpp.getBuffer();
    char cmd[128];
    
    // Lmsg is just test code that probably can be deleted?
    char Lmsg[4];
    for (unsigned char i = 0; i < lpp.getSize(); i++)
    {
      sprintf(Lmsg, "%02X", (char)payload[i]);
      Serial.print(Lmsg);
    }

    Serial.println(" payload"); 

//**************end of payload builder code**************************

    // start of LoRa communication code 
    if (is_exist)
    {
        int ret = 0;
        if (is_join)
        {
 
            ret = at_send_check_response("+JOIN: Network joined", 12000, "AT+JOIN\r\n");
            if (ret)
            {
                is_join = false;   //resets join check?
            }
            else
            {
                at_send_check_response("+ID: AppEui", 1000, "AT+ID\r\n");
                Serial.print("JOIN failed!\r\n\r\n");
                delay(5000);
            }
        }
        else
        {
            char cmd[128]; 
            //sprintf(cmd, "AT+CMSGHEX=\"%04X%04X\"\r\n", (int)temp, (int)humi); //original
            //****** add in CayenneLPP ecoding ******
            sprintf(cmd, "AT+CMSGHEX=\"%02X\"\r\n", (char)payload[0]);  //first part of confirmed message
            Serial.println("debug - print cmd at start of loop");
            Serial.println(cmd);
            // add data payload to LoRa message by looping thru Cayenne LPP data buffer
            char TestMsg[2];   
            for (Loop1 = 0; Loop1 < buf_size; Loop1++)
              {
                Pointer = (Loop1*2) + Offset;
                sprintf(TestMsg, "%02X", (char)payload[Loop1]);
                // write data buffer character to LoRa message
                for (Loop2 = 0; Loop2 < 2; Loop2++)
                  {
                     cmd[Loop2 + Pointer] = TestMsg[Loop2];
                  }
              }            
            // create end of message characters for LoRa message (need ",return,null characters)
            char EndMsg[20];
            sprintf(EndMsg, "test\"%02X\"\r\n", (char)payload[0]);
            // add ", return and null characters to end of LoRa message
            for (unsigned char i = 2; i < 7; i++)  
              {
                cmd[Pointer + i] = EndMsg[5 + i];
              }            
            Serial.println("debug - print cmd at end of loop");
            Serial.println(cmd);
            //******** end of CayenneLPP encodeding *********
            ret = at_send_check_response("Done", 12000, cmd);        //sends cmd command over LoRa - increase time from 5000 to 12000
            if (ret)
            {
                recv_prase(recv_buf);
            }
            else
            {
                Serial.print("Send failed!\r\n\r\n");
            }
            delay(5000); 
        }
    Serial.println(" in main loop checking LoRa then wait 5 minutes");   
    delay(300000); //main delay 300 seconds 
    }
    else
    {
        delay(1000);
    }
}

センサのサブルーチン

//count anemometer pulses and calculate windspeed
void measure() {
  InterruptCounter = 0;
  attachInterrupt(digitalPinToInterrupt(SensorPin), countup, RISING);
  delay(1000 * RecordTime);
  detachInterrupt(digitalPinToInterrupt(SensorPin));
  WindSpeed = (float)InterruptCounter / (float)RecordTime * 2.4;
}

void countup() {
  InterruptCounter++;
}

//determine wind direction angle based on measured wind vane voltage
void vaneAngle() {
  angleWind = 99.9;
  if (DirWind < 270)
    {
      angleWind = 112.5;
    }
  else if ((DirWind >= 270) and (DirWind <= 310))
    {
      angleWind = 67.5  ;
    } 
   else if ((DirWind > 310) and (DirWind <= 400))
    {
      angleWind = 90.0  ;
    }    
  else if ((DirWind >= 400) and (DirWind < 600))
    {
      angleWind = 157.5  ;
    }    
  else if ((DirWind >= 600) and (DirWind < 750))
    {
      angleWind = 135.0  ;
    }    
  else if ((DirWind >= 750) and (DirWind < 850))
    {
      angleWind = 202.5  ;
    }    
  else if ((DirWind >= 850) and (DirWind < 1150))
    {
      angleWind = 180.0  ;
    }    
   else if ((DirWind >= 1150) and (DirWind < 1450))
    {
      angleWind = 22.5  ;
    }    
   else if ((DirWind >= 1450) and (DirWind < 1900))
    {
      angleWind = 45.0  ;
    }    
  else if ((DirWind >= 1900) and (DirWind < 2050))
    {
      angleWind = 247.5  ;
    }    
   else if ((DirWind >= 2050) and (DirWind < 2250))
    {
      angleWind = 225.0  ;
    }    
  else if ((DirWind >= 2250) and (DirWind < 2500))
    {
      angleWind = 337.5;
    }
  else if ((DirWind >= 2500) and (DirWind < 2700))
    {
      angleWind = 0.0;
    }
  else if ((DirWind >= 2700) and (DirWind < 2850))
    {
      angleWind = 292.5;
    }
  else if ((DirWind >= 2850) and (DirWind < 3000))
    {
      angleWind = 315.0;
    }
  else if (DirWind >= 3000)
    {
      angleWind = 270.0;
    }
}  

Xiao_LoraE5CayenneLPP_WindRev1.ino アプリケーションの最新のソースコードは、以下のリンクの github にアップロードされています。

Grove LoRa-E5 ボードのデバイスEUI の決定

ArduinoでXiao_LoraE5CayenneLPP_WindRev1.inoのコードをコンパイルしてSeeeduino Xiaoにアップロードし、Serial Monitorを有効にします。Serial Monitorの出力を見て、Grove LoRa-E5デバイスのEUIを決定します。
image

LoRa-E5ベースのセンサノードをChirpStack LoRaWANネットワークサーバに追加

(注:このプロジェクトと以下のステップは、ChirpStackベースのプライベートLoRaWANネットワークがアクティブで、LoRa-E5センサノードの範囲内にあることを前提としています。そうでない場合は、TechForumの投稿「MachinechatとSeeed SenseCAPによるプライベートLoRaWANセンサネットワークの構築」をご参照ください。)

1 - ChirpStackで、Device-profileを選択し、Createを選択します。Device-profile nameに「Seeed LoRaE5」を入力し、LoRaWAN MACバージョンは「1.0.2」、LoRaWAN Regional Parametersバージョンは「A」、ADR algorithmは「Default ADR algorithm」を選択し、Uplink intervalには「3600」を入力します。JOIN(OTAA/ABP) )タブで、「Device supports OTAA」ボックスにチェックを入れます。CODEC タブで、 CODEC ドロップダウンリストから「Cayenne LPP」を選択します。

2 - ChirpStackで、Applicationsを選択し、「FarmTest」を選択し、Createを選択します。Device nameに 「LoRaE5wind」を入力し、Device descriptionに「description」を入力し、Device EUIにGrove LoRa-E5ボードのデバイスEUI(上記ステップ 「Determine Grove LoRa-E5 board device EUI」から決定)を入力し、 Device-profileに「STM32WL Sensors」を入力し、CREATE DEVICEを選択します。(注:最初のテストとデモでは、Disable frame-counter validation(フレームカウンタの検証を無効にする)ボックスにチェックを入れることをお勧めします。)

3 - DeviceのApplication keyを追加します。
Application keyに「2B7E151628AED2A6ABF7158809CF4F3C」(注:これはLoRa-E5のデフォルトキーです。変更する場合はLoRa-E5 AT Command SpecificationのKEYセクションを参照)を入力し、SET DEVICE-KEYSを選択します。

JEDI Pro Generic LoRaWAN Custom Data CollectorとChirpStackとのHTTP Integrationのセットアップおよびテスト

ChirpStackが変更され、LoRaWANメタデータおよびセンサーデータを指定されたIPアドレスに転送するための HTTP integrationが追加されました。MachinechatのGeneric LoRaWAN Custom Data Collector Plug-inは、指定されたIPアドレスをリッスンし、LoRaWANデータを解析し、レビュー(デバッグ有効時)およびJEDI Proプラットフォームで使用するために使用されます。
(注:Custom Data Collectorは、lorawan-linux.binとconfig.ymlの2つのファイルで構成されており、Machinechatから以下のサイトで入手可能です。https://support.machinechat.io/hc/en-us/articles/6046199010327-Generic-LoRaWAN-Custom-Data-Collector-Beta-for-JEDI-PRO-Linux

1 - ChirpStackでHTTP Integrationを有効にする。
ChirpStack Integrations画面で「Add」を選択します。

2 - HTTP Integrationの構成
Payload marshalerで「JSON」を選択し、Endpoint URLにIPアドレス(config.ymlファイルと同じIPを使用)を追加し、ADD INTEGRATIONを選択します。

3 - Ubuntu Linux Mini-PCのJEDI Proがインストールされている~/jedi/pluginsディレクトリにlorawan-linux.binconfig.ymlファイルをコピーします。config.ymlファイルを修正して、デバッグを有効にし、IPリスニングアドレスを指定します。
(注: もし、以前に別のセンサ用にlorawan-linux.binconfig.ymlをインストールしたことがあれば、手順5のようにconfig.ymlを編集して風速と風向のパラメータ情報を追加するだけです。)

4 - Ubuntu Linux Mini-PCの端末で、「./lorawan-linux.bin ./config.yml」 を使ってコマンドラインからカスタムプラグインを実行し、出力データをモニタしてください。データは以下のようになるはずです(注:lorawan-linux.binファイルを実行可能にしておくことを忘れないでください)。

5 - config.ymlファイルを編集し、LoRaWANデータをJEDI Proのデータパラメータにマッピングし、デバッグを無効にします。このプロジェクトの例では、propertyNamesを、LoRaWANのcSproperty: “anslogOutput.1”mcProperty: “WindSpeed” にマッピングされるように、LoRaWANのcSproperty: “analogOutput.2”mcProperty: “WindAngle” にマッピングされるように編集して下さい。「setDebug:」をfalseにしてデバッグを無効にし、ファイルを保存します。

JEDI Pro Custom Data CollectorとData Dashboardのセットアップ

JEDI Proで、「Settings」タブを選択し、「Data Collectors」を選択し、「Add Collector」を選択します。(注:JEDIアプリケーションにLoRaWAN Custom Data Collectorを追加したことがある場合、このステップは必要ありません)

Collectorの設定は以下のように行います。Data Collectorの名前を「LoRaWAN」(またはお好きな名前)とし、Collector Typeとして「Custom Plug-In」を選択し、Plug-In Executable fileとして「lorawan-linux.bin」を選択して、config.ymlファイルの場所を入力します。(例: プラグインオプションに「/home/scottr/jedi/plugins/config.yml」を入力し、「Run As Background Process and Monitor」にチェックを入れ、 「VALIDATE PLUG-IN」を選択して機能を確認します。)

JEDI Proで「Data Dashboards」を選択し、「+」を選択して新しいチャートを追加します

風速(WindSpeed)と風向(WindAngle)のデータグラフを設定し、「追加」を選択すると、Data Dashboardに表示されます(以下の例を参照してください)。

まとめ

Arduino、Seeeduino Xiao、LoRa-E5無線ボード、およびSparkfunのWeather Meterキットを組み合わせることで、LoRaで風速や風向のデータを提供する柔軟な風速/風向センサプラットフォームとなります。ChirpStackのHTTP IntegrationとMachinechatのGeneric LoRaWAN Custom Data Collectorを設定して、風センサのデータをJEDI Proに取り込み、IoTデータの収集、可視化、モニタリング、およびデータ保存を行います。このサンプルコードは、必要に応じて他のセンサ用に簡単に変更することができます。

参考資料




オリジナル・ソース(English)