使用 Seeed 的 LoRa-E5 模組為 LoRaWAN 網路開發風速/風向感測器

描述

這專案涵蓋了開發基於 Seeed LoRa-E5 的 Arduino 風速/方向感測器的過程,並將其安裝在能夠連接到 DigiKey 公司總部大樓屋頂氣象平台的戶外外殼中。專案還包括將感測器節點新增至現有的基於 ChirpStack 的私有 LoRaWAN 網路和 Machinecha t的 JEDI Pro IoT 軟體平台。LoRa 感測器使用 Seeeduino Xiao, Grove LoRa-E5 無線電板,以及 Sparkfun 天氣儀表套件的風速計和風向標硬體。LoRaWAN 網路使用 Seeed的 IP67 等級工業 LoRaWAN 閘道器將 LoRa 感測器資料包轉送到 Seeed 的 ReServer 上運行的專用 ChirpStack LoRaWAN 網路伺服器(安裝了 Ubuntu linux)。Machinechat 的 JEDI Pro IoT 平台運行在同一個 ReServer 上。

image

硬體

軟體

  • JEDI Pro或JEDI Pro SSE
    適用於物聯網資料收集、視覺化、監控和資料儲存的軟體,可整合到物聯網解決方案中。功能包括:收集來自感測器、設備和機器的數據;構建直觀的實時和歷史數據以及系統視圖儀表板;創建規則,自動監控和響應數據情況;透過電子郵件和短信接收警報通知。JEDI Pro SSE 是 JEDI Pro 的 Seeed 工作室版本,為 Seeed 的 SenseCAP LoRaWAN 感測器系列增加了一個資料收集器
  • ChirpStack
    ChirpStack 開源 LoRaWAN 網路伺服器堆疊為 LoRaWAN 網路提供開源元件。模組化的架構使得在現有的基礎設施內整合成為可能。
  • Arduino
    Arduino 是一個基於易於使用的硬體和軟體的開源電子平台。

背景

這篇文章是一個後續項目,建立在相關的技術論壇文章「Set up a private LoRaWAN Sensor Network with Machinechat and Seeed SenseCAP 」的基礎上,文章中使用 Machinechat 和 Seeed SenseCAP 建立一個私有的 LoRaWAN 感測器網絡,其中詳細介紹了使用 DigiKey 上現成的硬體和軟體建立一個私有的 LoRaWAN 物聯網感測器網路。相關專案中使用的軟體包括 Machinechat 的 JEDI Pro 應用軟體和 ChirpStack 的 LoRaWAN 網路伺服器應用程式。專案中使用的硬體包括 Seeed reServer x86 伺服器和 SenseCAP 室外 LoRaWAN 閘道器。對於這篇文章,Seeeduino Xiao, Grove LoRa-E5 板和 Sparkfun 氣象儀套件被設定為在LoRaWAN 網路上報告風速和風向。

Sparkfun 氣象儀表套件風速計和風向標
風速是由杯式風速計測量,旋轉磁鐵導致接觸關閉一次每轉。觸點每秒關閉一次,對應的風速為1.492MPH(2.4km/h)。觸點閉合可以透過風向標電纜上 RJ-11 連接器(接腳 2 和 3)的兩個內導體進行監控。
風向由風向標上的 8 個開關決定,每個開關都有自己的電阻。隨著風向標方向的改變,風向標上的磁鐵會使各個開關閉合。外部電阻器用來設定分壓器,這樣就可以監控電壓輸出來決定風向標的位置。RJ-11 連接器的兩個外部導體(引腳 1 和引腳 4)用於風向標。(註:詳細資料請參閱Sparkfun 氣象儀連接指南

DigiKey 天氣平台
有關 DigiKey 天氣平台的詳細資訊,請訪問另一文章「DigiKey Weather Platform」。

實現

對於該項目,外部 USB 5V 電源為 Seeeduino Xiao 供電,Xiao 的 3V3 輸出為 Grove LoRa-E5 板和天氣感測器硬體供電。Xiao UART 連接到 Grove LoRa-E5 的 TX/RX 接腳,Xiao 輸入接腳 1 和 2 連接到風感測器風速計和風向標。
Xiao 和 LoRa-E5 板安裝在室外外殼(Hammod 型號 RL6225),+5V 電源和風感測器風速計/風向標使用 M8 面板安裝連接器和電纜組件連接。
Arduino 應用程式碼在 Seeeduino Xiao 上運行,向 LoRa-E5 發送 AT 命令,並監控輸入引腳 1 和 2。應用程式加入 LoRa 網絡,監控輸入引腳以計算風速/風向數據,以 CayenneLPP 格式編碼數據,透過 LoRa 發送數據,延遲並循環返回下一次讀取。下面的原理圖說明了電路是如何連接和實現的(參考 Scheme-it 項目:LoRaE5_Xiao_WindSensorM8cables)。

image

模組與風感測器連接器 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。參考連結 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。進入 Seeeduino Xiao 並啟用串行監視器。檢查串行監視器輸出以確定 Grove LoRa-E5 設備的 EUI。

image

將 LoRa-E5 感測器節點加入到 ChirpStack LoRaWAN 網路伺服器中

(注意:這個專案和下面的步驟假設一個基於 ChirpStack 的私有 LoRaWAN 網路是活躍的,並且在 LoRa-E5 感測器節點的範圍內,如果沒有,請參考TechForum 另一文章「Set up a private LoRaWAN Sensor Network with Machinechat and Seeed SenseCAP」。

1 - 在 ChirpStack 中,選擇設備設定檔並建立。名稱設備設定檔「Seeed LoRaE5」,LoRaWAN MAC 版本選擇「1.0.2」,LoRaWAN 區域參數版本選擇「A」,ADR 演算法選擇「Default ADR algorithm」,上行間隔輸入「3600」。在JOIN(OTAA/ABP)標籤中,選取「裝置支援OTAA」。在CODEC 標籤中,在CODEC下拉清單中選擇「Cayenne LPP」。

image

2 - 在 ChirpStack 中,選擇應用程序,然後選擇「FarmTest」,然後選擇建立。裝置名稱輸入「LoRaE5wind」,裝置描述輸入「description」,Grove LoRa-E5 板輸入「Device EUI」(由上一步驟「Determine Grove LoRa-E5 board device EUI 」確定),裝置設定檔輸入「STM32WL Sensors」,選擇 CREATE DEVICE。(注意:對於初始測試和演示,您可能需要勾選「Disable frame-counter validation」)

image

3 - 新增設備的應用密鑰。輸入應用鍵「2B7E151628AED2A6ABF7158809CF4F3C」(注意:這是 LoRa-E5 中的預設鍵,更改請參考 LoRa-E5 AT 命令規範的 key 部分)並選擇 SET DEVICE-KEYS。

image

設定和測試 ChirpStack HTTP整合與 JEDI Pro 通用 LoRaWAN 自訂資料收集器

1 - 在 ChirpStack 中啟用 HTTP 整合。
在 ChirpStack 整合介面中選擇「Add」。

image

2 - 配置 HTTP 集成
選擇「JSON」為有效載荷編組器(Payload Marshaler),為 Endpoint URL 新增 IP 位址(使用相同的 IP 配置config.yml 檔),然後選擇 ADD INTEGRATION

image

3 - 拷貝「lorawan-linux.bin 」 和「config.yml 」 到 Ubuntu Linux Mini-PC 上安裝 JEDI Pro 的 ~/jedi/plugins 目錄下。修改配置。 yml 檔案啟用調試,並指定 IP 監聽位址。
(注意:如果你之前已經安裝了 lorawan-linux.binconfig. yml 檔案。yml 檔案對於不同的感測器,你所需要做的就是編輯 config.yml 如步驟 5 所示,添加風速和風角度參數的信息)

image

4 - 在命令列中使用「./lorawan-linux.bin ./config.yml」運行自訂插件。在 Ubuntu Linux Mini-PC 的終端機上運行 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 來關閉偵錯並儲存檔案。

image

設定 JEDI Pro 自訂資料收集器和資料儀表板

在 JEDI Pro 中,選擇「Settings 設定」選項卡,然後選擇「Data Collectors」,然後選擇「ADD COLLECTOR」。(注意:如果您之前已經將 LoRaWAN 自訂資料收集器新增至 JEDI 應用程式中,則無需執行此步驟)

image

如下圖所示配置 Collector。將資料收集器命名為「LoRaWAN」(或您喜歡的任何名稱),選擇「收集器類型」為「自訂插件」,選擇「LoRaWAN -linux.bin」作為插件可執行文件,輸入config.yml 文件(例如:「/home/scottr/jedi/plugins/config.yml」為 Plug-in 選項,勾選「Run As Background Process and Monitor」複選框,然後選擇「VALIDATE PLUG-IN」以驗證功能。

image

在 JEDI Pro 中,選擇「Data Dashboards」,然後選擇「+」來新增一個圖表。

image

為風速和風角度配置資料圖表,並選擇「Add」以包含在資料儀表板中(參見下面的範例)

image

結論

Arduino、Seeeduino Xiao、LoRa-E5 無線電和 Sparkfun 的氣象儀套件的組合提供了一個靈活的風感測器平台,可以透過 LoRa 提供風速和風向資料。接著配置 ChirpStack 的 HTTP 整合和 Machinechat 的通用 LoRaWAN 自訂資料擷取器,將風感測器資料匯入 JEDI Pro,用於物聯網資料收集、視覺化、監控和資料儲存。可以根據需要輕鬆修改範例程式碼以用於其他感測器。

參考資料

Seeed - Getting Started with reServer
Seeed - SenseCAP室外閘道器 - LoRaWAN US915MHz
Seeed - 102991155 4G LoRa 閘道器 乙太網路
Seeed - WIO-E5 開發板
Seeed - LoRa-E5 AT命令
Machinechat 的 JEDI One 物聯網平台入門
Machinechat - Set up a private LoRaWAN Sensor Network with Machinechat and Seeed SenseCAP
Sparkfun - 氣象儀表連接指南