ทำ Weather Station ด้วย Heltec LoRa V2 กับ Sensor AHT10 ใช้ PlatformIO
เราจะมาทำ Weather Station ด้วย Heltec LoRa V2 กัน โดยการวัดค่าอุณหภูมิ ความชื้นแล้วส่งผ่าน LoRaWAN ขึ้น TTN (TheThingsNetwork.org), Chirpstack หรือ เครือข่าย Helium People Network
ปรกติผู้เขียนจะใช้ Sensor BME280 แต่หลังๆ มานี่ราคา BME280 ขึ้นไปค่อนข้างมาก และผู้ขายยังส่ง BMP280 ปนมาแทน BME280 ทำให้วัดความชื้นไม่ได ้ก็เลยจะเปลี่ยนไปใช้ AHT10 แทน เพื่อลดปัญหาสับสนตอนซื้อ BME280 มาใช้งาน
ผู้สนใจจะต้องติดตั้ง VSCode และ Plugin PlatformIO ให้เรียบร้อยและมีประสบการณ์ใช้งานมาบ้างเล็กน้อย
อุปกรณ์ที่ใช้
- Heltec Wifi LoRa OLED V.2 เลือกเอาที่บัดกรีขามาเสร็จก็จะสดวกเพราะบัดกรีเองไม่ง่ายนักเพราะมันมีสายแพรของ OLED บังขาอยู่
ราคาประมาณ 900 บาท - Sensor AHT10 ราคาประมาณ 100 บาท
- Breadboard ขนาด 8.2x5.5x1ซม. ราคาประมาณ 65 บาท
- สาย Jumper Wire
การเชื่อมต่อสายไฟตามภาพด้านล่าง เป็นการเชื่อมเข้าขา I2C ของ Heltec
เปิด VSCode และ คลิก ICO หน้ามนุษย์ต่างดาว PlatformIO ตามภาพ
คลิกสร้าง + New Project ใหม ใต้ข้อความ Quick Access ตามภาพ
ตั้งชื่อ Name ตามต้องการ เลือก Board เป็น Heltec Wifi LoRa 32 (V2) (Heltec Automation) และ Framework เป็น Arduino Framework
// Modified from Github proffalken/HeltecGPS.ino
// @MBConsultingUK"
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include <U8x8lib.h>
#include <CayenneLPP.h>
#include <Wire.h>
#include <AHT10.h>
#define V2
uint8_t readStatus = 0;
AHT10 myAHT10(AHT10_ADDRESS_0X38);
float temp,pa,hum,alt;
int cnt=0;
// the OLED used
U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16);
// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 10;
// Create the LPP object
CayenneLPP lpp(51);
// LoRa Pins
#define LoRa_RST 14 // GPIO 14
#define LoRa_CS 18 // GPIO 18
#define LoRa_DIO0 26 // GPIO 26
#define LoRa_DIO2 32 // GPIO 32
#ifdef V2 //WIFI Kit series V1 not support Vext control
#define LoRa_DIO1 35 // GPIO35 -- SX127x's IRQ(Interrupt Request) V2
#else
#define LoRa_DIO1 33 // GPIO33 -- SX127x's IRQ(Interrupt Request) V1
#endif
#define USE_JOINING
#ifdef USE_JOINING
// OTAA join keys
// This EUI must be in little-endian format, so least-significant-byte
// first. When copying an EUI from ttnctl output, this means to reverse
// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3,
// 0x70.
//(lsb)
//static const u1_t PROGMEM APPEUI[8] = { 0x8B, 0x65, 0x00, 0xF0, 0x7E, 0xD5, 0xB3, 0x70 };
static const u1_t PROGMEM APPEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
void os_getArtEui (u1_t* buf) {
memcpy_P(buf, APPEUI, 8);
}
// This should also be in little endian format, see above.
//(lsb) Example Chirstack DevEUI F18FDABBDD703F00
//static const u1_t PROGMEM DEVEUI[8] = { 0xF1, 0x8F, 0xDA, 0xBB, 0xDD, 0x70, 0x3F, 0x00 };
static const u1_t PROGMEM DEVEUI[8] = { 0x00, 0x3F, 0x70, 0xDD, 0xBB, 0xDA, 0x8F, 0xF1 };
void os_getDevEui (u1_t* buf) {
memcpy_P(buf, DEVEUI, 8);
}
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
// The key shown here is the semtech default key.
// (msb)
static const u1_t PROGMEM APPKEY[16] = { 0xDA, 0x5A, 0x42, 0xB8, 0x71, 0x50, 0x2E, 0xBA, 0x68, 0x38, 0xC5, 0x28, 0xDE, 0x07, 0xC2, 0x26 };
void os_getDevKey (u1_t* buf) {
memcpy_P(buf, APPKEY, 16);
}
#else
// ABP keys
//UNO1
// LoRaWAN NwkSKey, network session key (msb)
static const PROGMEM u1_t NWKSKEY[] = { 0xAA, 0xC3, 0x0F, 0xB2, 0x91, 0xDB, 0x55, 0xC5, 0x31, 0x82, 0x53, 0xD4, 0x08, 0x08, 0x7A, 0x00 };
// LoRaWAN AppSKey, application session key (msb)
static const u1_t PROGMEM APPSKEY[] = { 0xAA, 0xBE, 0x2D, 0xE6, 0xB6, 0xB3, 0xF7, 0xC2, 0xD0, 0x33, 0x72, 0xB5, 0x27, 0x20, 0xD6, 0x00 };
// LoRaWAN end-device address (DevAddr)
static const u4_t DEVADDR = 0x26011511;
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
#endif
//static uint8_t mydata[] = {13, 37};
static osjob_t sendjob;
// Pin mapping
const lmic_pinmap lmic_pins = {
.nss = LoRa_CS,
.rxtx = LMIC_UNUSED_PIN,
.rst = LoRa_RST,
.dio = { LoRa_DIO0, LoRa_DIO1, LoRa_DIO2 },
};
void do_send(osjob_t* j) {
// Check if there is not a current TX/RX job running
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, not sending"));
u8x8.drawString(0, 7, "OP_TXRXPEND, not sent");
} else {
// Read the sensors and pack up the data
temp = myAHT10.readTemperature();
//pa = bme.readPressure() / 100.0F;
hum = myAHT10.readHumidity();
//alt = bme.readAltitude(SEALEVELPRESSURE_HPA);
Serial.print("Temp:");
Serial.print(temp);
Serial.println(" C");
Serial.print("Hum:");
Serial.print(hum);
Serial.println(" %");
//display sensor
u8x8.setCursor(0, 3);
u8x8.printf("T %.2f,H %.2f", temp, hum);
lpp.reset();
lpp.addTemperature(2, temp);
lpp.addRelativeHumidity(3, hum);
// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(2, lpp.getBuffer(), lpp.getSize(), 0);
Serial.println(F("Packet queued"));
u8x8.drawString(0, 7, "PACKET QUEUED");
digitalWrite(BUILTIN_LED, HIGH);
}
// Next TX is scheduled after TX_COMPLETE event.
}
void onEvent (ev_t ev) {
Serial.print(os_getTime());
u8x8.setCursor(0, 2);
u8x8.printf("TIME %i", os_getTime());
Serial.print(": ");
switch (ev) {
case EV_SCAN_TIMEOUT:
Serial.println(F("EV_SCAN_TIMEOUT"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_SCAN_TIMEOUT");
break;
case EV_BEACON_FOUND:
Serial.println(F("EV_BEACON_FOUND"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_BEACON_FOUND");
break;
case EV_BEACON_MISSED:
Serial.println(F("EV_BEACON_MISSED"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_BEACON_MISSED");
break;
case EV_BEACON_TRACKED:
Serial.println(F("EV_BEACON_TRACKED"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_BEACON_TRACKED");
break;
case EV_JOINING:
Serial.println(F("EV_JOINING"));
u8x8.clearLine(7);
//u8x8.drawString(0, 7, " ");
u8x8.drawString(0, 7, "EV_JOINING");
break;
case EV_JOINED:
Serial.println(F("EV_JOINED"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_JOINED ");
LMIC_setDrTxpow(DR_SF7, 14); //added fixed SF after join for longer range messages
// Disable link check validation (automatically enabled
// during join, but not supported by TTN at this time).
LMIC_setLinkCheckMode(0);
break;
case EV_RFU1:
Serial.println(F("EV_RFU1"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_RFUI");
break;
case EV_TXSTART:
Serial.println(F("EV_TXSTART"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_TXSTART");
break;
case EV_JOIN_FAILED:
Serial.println(F("EV_JOIN_FAILED"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_JOIN_FAILED");
break;
case EV_REJOIN_FAILED:
Serial.println(F("EV_REJOIN_FAILED"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_REJOIN_FAILED");
//break;
break;
case EV_TXCOMPLETE:
u8x8.setCursor(0, 6);
u8x8.printf("PACKET# %i",cnt);
Serial.print("PACKET#");
Serial.println(cnt);
cnt=cnt+1;
Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_TXCOMPLETE");
digitalWrite(BUILTIN_LED, LOW);
if (LMIC.txrxFlags & TXRX_ACK) {
Serial.println(F("Received ack"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "Received ACK");
}
if (LMIC.dataLen) {
Serial.print(F("Data Received "));
u8x8.drawString(0, 5, "RX ");
Serial.print(LMIC.dataLen);
u8x8.setCursor(4, 5);
u8x8.printf("%i bytes", LMIC.dataLen);
Serial.print(F(" bytes of payload 0x"));
for (int i = 0; i < LMIC.dataLen; i++) {
if (LMIC.frame[LMIC.dataBeg + i] < 0x10) {
Serial.print(F("0"));
}
Serial.print(LMIC.frame[LMIC.dataBeg + i], HEX);
}
Serial.println();
u8x8.setCursor(0, 6);
u8x8.printf("RSSI %d SNR %.1d", LMIC.rssi, LMIC.snr);
}
// Schedule next transmission
os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
break;
case EV_LOST_TSYNC:
Serial.println(F("EV_LOST_TSYNC"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_LOST_TSYNC");
break;
case EV_RESET:
Serial.println(F("EV_RESET"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_RESET");
break;
case EV_RXCOMPLETE:
// data received in ping slot
Serial.println(F("EV_RXCOMPLETE"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_RXCOMPLETE");
break;
case EV_LINK_DEAD:
Serial.println(F("EV_LINK_DEAD"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_LINK_DEAD");
break;
case EV_LINK_ALIVE:
Serial.println(F("EV_LINK_ALIVE"));
u8x8.clearLine(7);
u8x8.drawString(0, 7, "EV_LINK_ALIVE");
break;
default:
Serial.println(F("Unknown event"));
u8x8.clearLine(7);
u8x8.setCursor(0, 7);
u8x8.printf("UNKNOWN EVENT %d", ev);
break;
}
}
void setup() {
Serial.begin(115200);
if (!myAHT10.begin()) {
Serial.println("Could not find a valid AHT10 sensor, check wiring!");
while (1);
}
u8x8.begin();
u8x8.setFont(u8x8_font_chroma48medium8_r);
u8x8.drawString(0, 1, "LoRaWAN Thailand");
if (!myAHT10.begin()) {
Serial.println("No sensor device found, check line or address!");
while (1);
}
SPI.begin(5, 19, 27);
// LMIC init
os_init();
// Reset the MAC state. Session and pending data transfers will be discarded.
LMIC_reset();
#ifndef USE_JOINING
#ifdef PROGMEM
// On AVR, these values are stored in flash and only copied to RAM
// once. Copy them to a temporary buffer here, LMIC_setSession will
// copy them into a buffer of its own again.
uint8_t appskey[sizeof(APPSKEY)];
uint8_t nwkskey[sizeof(NWKSKEY)];
memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
#else
// If not running an AVR with PROGMEM, just use the arrays directly
LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
#endif
#endif
LMIC_setDrTxpow(DR_SF8, 14); //set join at SF8 with power 14
pinMode(BUILTIN_LED, OUTPUT);
digitalWrite(BUILTIN_LED, LOW);
// Start job (sending automatically starts OTAA too)
do_send(&sendjob);
}
void loop() {
os_runloop_once();
}
Copy โปรแกรม main.cpp ด้านบนแทนข้อความโปรแกรม main.cpp ใน Directory src
แก้คีย์ OTAA คือ APPEUI, DEVEUI, APPKEY ให้ตรงกับที่เราลงทะเบียนไว้ใน TTN Console หรือ Chirpstack Console หรือ Helium Console
DEV EUI ต้องเรียงกลับด้าน เช่น
Device EUI บน Chirpstack f18fdabbdd703f00 หรือ บน TTS eui-f18fdabbdd703f00
ในโปรแกรมให้ใช้ lsb ดังนี้
static const u1_t PROGMEM DEVEUI[8] = { 0x00, 0x3F, 0x70, 0xDD, 0xBB, 0xDA, 0x8F, 0xF1 };
กรณี Chirpstack APPEUI ไม่ต้องใช้แล้ว กำหนดเป็น 0000000000000000
static const u1_t PROGMEM APPEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
ชื่อ Device EUI เรียงตรงข้ามกับในโปรแกรมภาษา C
ตอนเพิ่ม Device ใน Console กำหนด LoRaWAN MAC version 1.0.2 และ LoRaWAN Regional Parameters revision เป็น A
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:heltec_wifi_lora_32_V2]
platform = espressif32
board = heltec_wifi_lora_32_V2
framework = arduino
lib_deps =
mcci-catena/MCCI LoRaWAN LMIC library@^4.1.1
sabas1080/CayenneLPP@^1.1.0
olikraus/U8g2@^2.33.2
SPI
enjoyneering/AHT10@^1.1.0
monitor_speed = 115200
build_flags = -Wall
-Wextra
-Wno-missing-field-initializers -O3
-D CFG_as923=1
-D CFG_sx1276_radio=1
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-D hal_init=LMICHAL_init
-D LMIC_DEBUG_LEVEL=2
แก้ไข ไฟล์ platformio.ini ให้ตรงตามตัวอย่างข้างบน
อาจจะเพิ่ม build_flags บรรทัด
-D LMIC_DEBUG_LEVEL=2
เพื่อให้ Serial Monitor แสดง Debug Message ออกมามากขึ้น
กด Upload อาจจะมี Warning สีเหลืองขึ้นมาบ้างบางบรรทัด (หากแก้ Code ให้กดรูปถังขยะเพื่อล้างข้อมูลก่อนหนึ่งครั้ง)
Tip!
หากมีปัญหาเกี่ยวกับ
collect2.exe: error: ld returned 1 exit status
*** [.pio\build\heltec_wifi_lora_32_V2\firmware.elf] Error 1
และมีข้อความ multiple definition of `hal_init’ แสดงว่า Platform LoRa V2 ที่เพิ่มมี Lib Hal ซ้ำกับของ MCCI LMIC
ให้ เพิ่ม -Dhal_init=LMICHAL_init ตรง build_flags = ใน File Platformio.ini
หรือ ถ้าใช้ ArduinoIDE ให้เพิ่ม
#define hal_init LMICHAL_init
ในไฟล์ lmic_project_config.h ใน Dir project_config ในที่เก็บ Libraries
เช่น ตัวอย่าง file ที่แก้เก็บไว้ที่ User ให้ดู User ที่ใช้งาน ArduinoIDE
C:\Users\User\Documents\Arduino14\libraries\MCCI_LoRaWAN_LMIC_library\project_config\lmic_project_config.h
เมื่อ Upload เสร็จ Heltec จะถูก Reboot และจะเริ่มทำงาน Join Request หาก Join ได้ ก็จะเริ่มส่งข้อมูล Temp และ Humid ตามภาพ ตัวเลข Packet จะขึ้นแสดงบนหน้าจอ OLED ตัวเลขนี้จะตรงตามตัวเลข fCnt บนหน้า Console
กรณี Join ไม่สำเร็จเพราะ key ผิดหรือไม่มี Hotspot Helium ให้ส่งข้อมูลผ่านจะขึ้น UNKNOWN EVENT 20 และตามด้วย EV_JOIN_FAILED
หากสำเร็จและเริ่มมีการส่งข้อมูลตัวเลข Fcnt จะเพิ่มขึ้นทีละหนึ่ง
ตัวอย่างข้อมูล Live Data บน Console Chirpstack โดยค่า อุณหภูมิและความชื้นจะอยู่ใต้บรรทัด objectJSON
หน้า Console สามารถกำหนดรูปแบบข้อมูลที่ส่งมาเป็น CayenneLLP ได้เลยไม่ต้องกำหนด Decoder ภาษา JS และหากเราตั้งค่า Integration ไปยัง Influxdb V.2 เราสามารถ Visualize ข้อมูลในรูป graph ได้ง่ายๆ ด้วย Influxdb Dashboard
สรุป
การใช้ PlatformIO ทำให้การติดตั้ง Lib เป็นไปอย่างอัตโนมัติและสามารถกำหนด Env ตอน Comply ได้ด้วย เช่น การกำหนดความถี่ใช้งานเป็น AS923 ได้ด้วยและเมื่อใช้กับ LIB MCCI LMIC ทำให้ง่ายขึ้นมาก
Question!
- แก้ความถี่เป็น AS923 ตรงไหน?
Ans
Platformio.inibuild_flags = -Wall -Wextra -Wno-missing-field-initializers -O3
-D CFG_as923=1
-D CFG_sx1276_radio=1
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
2.ถ้าไม่ใช้ขา SCL SDA จะใช้ขาอื่น กำหนดอย่างไร?
Ans ????
Reference