├── .gitignore ├── 3D ├── sensor_mounting.f3d └── sensor_mounting.stl ├── README.md ├── SD011_Test.py ├── Tools └── platformio_versioning.py ├── img ├── TP4056_board_led.jpg ├── case_open.jpg ├── dashboard.jpg ├── dust_sensor_outdoor.jpg ├── power_no_solar.jpg ├── power_operation.jpg ├── rfm95_breadboard.jpg ├── schematic.png ├── sensor_mounting.jpg ├── sensor_mounting_3d.jpg ├── sensor_mounting_case.jpg ├── solar_panel.jpg └── solar_panel_bad.jpg ├── include ├── io_pins.h ├── lora_credentials.h.example ├── settings.h └── version.h ├── platformio.ini └── src ├── bme280.cpp ├── bme280.h ├── functions.cpp ├── functions.h ├── global.cpp ├── global.h ├── lorawan.cpp ├── lorawan.h ├── main.cpp ├── particle.cpp ├── particle.h ├── power.cpp ├── power.h ├── tsl2591.cpp ├── tsl2591.h ├── veml6075.cpp └── veml6075.h /.gitignore: -------------------------------------------------------------------------------- 1 | ### PlatformIO ### 2 | .pioenvs 3 | .piolibdeps 4 | .clang_complete 5 | .gcc-flags.json 6 | .pio 7 | 8 | ### VisualStudioCode ### 9 | .vscode/* 10 | #!.vscode/settings.json 11 | !.vscode/tasks.json 12 | #!.vscode/launch.json 13 | #!.vscode/extensions.json 14 | *.bak 15 | 16 | ### VisualStudioCode Patch ### 17 | # Ignore all local history of files 18 | .history 19 | 20 | # credentials 21 | *_credentials.h 22 | 23 | include/version_build.h -------------------------------------------------------------------------------- /3D/sensor_mounting.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/3D/sensor_mounting.f3d -------------------------------------------------------------------------------- /3D/sensor_mounting.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/3D/sensor_mounting.stl -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 battery/solar powerd environmental sensor 2 | 3 | Battery/solar-powered ESP32 TTN (LoraWAN) sensor node for measuring temperature, humidity, air pressure, fine dust, UV and LUX. 4 | 5 | 6 | 7 | The microcontroller is an ESP32, but I don't use a ready-made module, because they need too much power even in DeepSleep. Instead I use a WROOM32 module. 8 | 9 | The following sensors are used: SDS011 (Fine dust), BME280 (Temperature / Humidity / Air pressure), VEML6075 (UV Index) and TSL2591 (LUX). 10 | 11 | For power supply a 18650 battery is used, which is charged via solar pannel and TP4056. Since the ESP32 and the sensors require 3.3 V, the MCP1700-3302E voltage regulator is used, because the a full loaded 18650 provieds 4.2V and the ESP32 is only rated for 3.0 V~3.6 V. Only the SD011 needs 5V, these are generated with a step-up converter. 12 | 13 | Since the SDS011 also needs too much current during the DeepSleep phases of the ESP32, it is switched on and off by N-channel MOSFET (IRL3103PBF). 14 | 15 | Every 5 minutes the measurements are sent via LoRaWAN. I retrieve the values via MQTT and Telegraf and visualize them in a Grafana dashboard. 16 | 17 | The whole thing was then installed in a weatherproof housing, the cables and the AAA are led down by Cable Glands to the outside to keep the housing as waterproof as possible. Only the BME280 was mounted outside in a Stevenson Screen. 18 | 19 | ## Components / BOM 20 | 21 | * 1x WROOM32 module 22 | * 1x TP4056 module 23 | * 1x MCP1700-3302E 24 | * 1x Solar panel 6V 3W (180x150 mm) 25 | * 1x 18650 battery 26 | * 1x 18650 battery holder 27 | * 1x 5V DC-DC boost converter module 28 | * 1x RFM95 module 29 | * 1x RFM95 breadboard 30 | * 1x BME280 sensor 31 | * 1x SDS011 module 32 | * 1x TSL2591 sensor 33 | * 1x IRL3103PBF 34 | * 1x VEML6075 sensor 35 | * 1x Housing IP65 171 x 121 x 55 mm 36 | * 1x Cable Gland PG7 37 | * 2x Cable Gland PG11 38 | * 7x Standoff/Spacer 20 mm 39 | * Aquarium hose 40 | * ... 41 | 42 | ## Schematic 43 | 44 | 45 | 46 | ### TP4056 modification 47 | 48 | The charging LED lights up as soon as the solarpannel supplies some current. However, this will cause the LED to discharge the battery when there is little sunshine. 49 | 50 | To prevent this, I have soldered out the charging LED. 51 | 52 | 53 | 54 | ### Solar panel 55 | 56 | The simple solar panels with plastic covers do not withstand UV radiation and wether conditions very well outdoors and the performance decreases drastically. 57 | 58 | 59 | 60 | Therefore, I now use a slightly better one with a glass cover. 61 | 62 | 63 | 64 | ### RFM95 Breadboard 65 | 66 | I have use the [iBrick / RFM95LORA Breadboard](https://github.com/iBrick/RFM95LORA_Breadboard) to make the RFM95 module breadboard/hole matrix board friendly. You can direct download the [Gerberfile](https://github.com/iBrick/RFM95LORA_Breadboard/blob/master/RFM95Breadboard.rar) from the Git repro. 67 | 68 | 69 | 70 | ## Power consumption 71 | 72 | | Mode | Power consumption | 73 | | ---- | ----------------- | 74 | | SDS011 measurement (30 sec) | 175 mA | 75 | | LoRa activity | 7 mA | 76 | | DeepSleep | 52 uA | 77 | 78 | ### Battery levels overview (Measuring and data send interval every 5 minutes) 79 | 80 | The data are still from the use with the 2W solar pannel! 81 | 82 | #### No charge current 83 | 84 | 85 | 86 | `1` = Disconnect solar pannel, `2` = Last data packet sent 87 | 88 | #### Normal operation 89 | 90 | 91 | 92 | `1` = First data packet sent after after approx. 6 houre of charge, `2` = Rainy days 93 | 94 | ## TTN payload decoder 95 | 96 | ```javascript 97 | function decodeUplink(input) { 98 | var decoded = {}; 99 | 100 | decoded.vcc = (input.bytes[0] + 200)/100; 101 | 102 | if(input.bytes[1] != 255 || input.bytes[2] != 255) 103 | { 104 | decoded.pm25 = ((input.bytes[1] << (8*1) | input.bytes[2] << (8*0)) / 10); 105 | } 106 | 107 | if(input.bytes[3] != 255 || input.bytes[4] != 255) 108 | { 109 | decoded.pm10 = ((input.bytes[3] << (8*1) | input.bytes[4] << (8*0)) / 10); 110 | } 111 | 112 | if(input.bytes[5] != 255 || input.bytes[6] != 255) 113 | { 114 | decoded.temperature = ((input.bytes[5]<<24>>16 | input.bytes[6]) / 10); 115 | } 116 | 117 | if(input.bytes[7] != 255) 118 | { 119 | decoded.humidity = input.bytes[7]; 120 | decoded.humidity &= ~(1 << 7); 121 | if(input.bytes[7] >> 7 == 1) { decoded.humidity +=0.5 } 122 | } 123 | 124 | pressure = (input.bytes[8] << (8*0) | input.bytes[9] << (8*1) | input.bytes[10] << (8*2)) / 100; 125 | if(pressure >= 300 && pressure <= 1100) { decoded.pressure = pressure } 126 | 127 | if(input.bytes[11] != 255) decoded.uvi = input.bytes[11] / 10; 128 | 129 | if(input.bytes[14] != 255) 130 | { 131 | decoded.lux = (input.bytes[12] << (8*0) | input.bytes[13] << (8*1) | input.bytes[14] << (8*2)) / 100; 132 | } 133 | 134 | return { 135 | data: decoded, 136 | warnings: [], 137 | errors: [] 138 | }; 139 | } 140 | ``` 141 | 142 | ## 3D Printing 143 | 144 | ### PCB and SDS011 mounting 145 | 146 | The STL and Fusion 360 Archive files are included in the 3D folder 147 | 148 | 149 | 150 | 151 | ### Stevenson Screen 152 | 153 | For the Stevenson Screen, I have used a modle from [Thingiverse](https://www.thingiverse.com/thing:3044446/files) 154 | 155 | ## Pictures 156 | 157 | Grafana Dashboard for the visualization of the measured values. 158 | 159 | 160 | 161 | The whole mounted outside on the windowsill. 162 | 163 | 164 | 165 | 166 | ## Links 167 | 168 | * [Thingiverse Stevenson Screen](https://www.thingiverse.com/thing:3044446/files) 169 | * [iBrick / RFM95LORA Breadboard](https://github.com/iBrick/RFM95LORA_Breadboard) 170 | -------------------------------------------------------------------------------- /SD011_Test.py: -------------------------------------------------------------------------------- 1 | import sds011 2 | import time 3 | 4 | sensor = sds011.SDS011("COM18", use_query_mode=True) 5 | 6 | 7 | sensor.sleep( sleep=False ) 8 | time.sleep( 15 ) 9 | pm25, pm10 = sensor.query() 10 | sensor.sleep() 11 | print( "PM2.5: %6.1f" % pm25 ) 12 | print( "PM10: %6.1f" % pm10 ) -------------------------------------------------------------------------------- /Tools/platformio_versioning.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # Optional: Define Path to your local verison of https://github.com/JackGruber/auto_buildnumber 5 | # If it is not located at a position same folder as `platformio_versioning.py` 6 | autoversion = 'C:\\git\\meine\\auto_buildnumber' 7 | 8 | try: 9 | import versioning 10 | autver_ok = True 11 | except: 12 | if os.path.exists(autoversion): 13 | sys.path.insert(0, autoversion) 14 | try: 15 | import versioning 16 | autver_ok = True 17 | except: 18 | autver_ok = False 19 | else: 20 | autver_ok = False 21 | 22 | if autver_ok == True: 23 | versioning.UpdateVersionFile("include/version.h", "DEFINEHEADER", False, "include/version_build.h") 24 | -------------------------------------------------------------------------------- /img/TP4056_board_led.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/TP4056_board_led.jpg -------------------------------------------------------------------------------- /img/case_open.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/case_open.jpg -------------------------------------------------------------------------------- /img/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/dashboard.jpg -------------------------------------------------------------------------------- /img/dust_sensor_outdoor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/dust_sensor_outdoor.jpg -------------------------------------------------------------------------------- /img/power_no_solar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/power_no_solar.jpg -------------------------------------------------------------------------------- /img/power_operation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/power_operation.jpg -------------------------------------------------------------------------------- /img/rfm95_breadboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/rfm95_breadboard.jpg -------------------------------------------------------------------------------- /img/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/schematic.png -------------------------------------------------------------------------------- /img/sensor_mounting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/sensor_mounting.jpg -------------------------------------------------------------------------------- /img/sensor_mounting_3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/sensor_mounting_3d.jpg -------------------------------------------------------------------------------- /img/sensor_mounting_case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/sensor_mounting_case.jpg -------------------------------------------------------------------------------- /img/solar_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/solar_panel.jpg -------------------------------------------------------------------------------- /img/solar_panel_bad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/esp32_ttn_environmental_sensor/8a4e9b5e04c6dcab6494b2b920e180d8643a1e09/img/solar_panel_bad.jpg -------------------------------------------------------------------------------- /include/io_pins.h: -------------------------------------------------------------------------------- 1 | // PIN definition for module 2 | #define PIN_LMIC_NSS 14 3 | #define PIN_LMIC_RST 26 4 | #define PIN_LMIC_DIO0 13 5 | #define PIN_LMIC_DIO1 12 6 | #define PIN_LMIC_DIO2 25 7 | 8 | #define PIN_ENABLE_SD011 5 9 | #define PIN_VBAT 27 -------------------------------------------------------------------------------- /include/lora_credentials.h.example: -------------------------------------------------------------------------------- 1 | #define TTN_APPEUI { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } // TTN Application EUI with "lsb" 2 | #define TTN_DEVEUI { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } // TTN Device EUI with "lsb" 3 | #define TTN_APPKEY { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } // TTN App Key with "msb" 4 | -------------------------------------------------------------------------------- /include/settings.h: -------------------------------------------------------------------------------- 1 | #define LORA_TX_INTERVAL 300 2 | #define LMIC_LORA_SF DR_SF7 // LORA Data rate 3 | #define SDS011_SERIAL Serial2 // Pin 16/17 4 | #define REF_VCCRAW_ESP32 931 // VCCraw read from ESP32 5 | #define REF_VBAT 3496 // Voltage in mV at battery 6 | #define MAX_LUX 88001 // Value that is sent when the maximum LUX value is reached that the sensor supports. (Output from the lib is in this case -1) 7 | #define PRINTDEBUGS -------------------------------------------------------------------------------- /include/version.h: -------------------------------------------------------------------------------- 1 | #define VERSION_MAJOR "0" 2 | #define VERSION_MINOR "1" 3 | #define VERSION_PATCH "1" 4 | #define VERSION_BUILD "1" 5 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ;PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | board = esp32dev 14 | framework = arduino 15 | monitor_speed = 115200 16 | upload_protocol = esptool 17 | 18 | lib_deps = 19 | https://github.com/lewapek/sds-dust-sensors-arduino-library 20 | mcci-catena/MCCI LoRaWAN LMIC library @ ^4.0.0 21 | Adafruit BME280 Library 22 | adafruit/Adafruit VEML6075 Library 23 | adafruit/Adafruit TSL2591 Library 24 | 25 | build_flags = 26 | -D hal_init=LMICHAL_init 27 | -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS 28 | -D CFG_eu868=1 29 | -D CFG_sx1276_radio=1 30 | -D LMIC_PRINTF_TO=Serial 31 | -D LMIC_DEBUG_LEVEL=0 32 | -D DISABLE_PING=1 33 | -D DISABLE_BEACONS=1 34 | -D ARDUINO_SAMD_VARIANT_COMPLIANCE=1 35 | 36 | extra_scripts = 37 | pre:tools/platformio_versioning.py -------------------------------------------------------------------------------- /src/bme280.cpp: -------------------------------------------------------------------------------- 1 | #include "bme280.h" 2 | #include 3 | #include 4 | #include 5 | 6 | void BME280Setup(void) 7 | { 8 | if (!BME280.begin(0x76, &Wire)) 9 | { 10 | Serial.println("Could not find a valid BME280 sensor, check wiring!"); 11 | while (1) 12 | ; 13 | } 14 | 15 | BME280.setSampling(Adafruit_BME280::MODE_FORCED, 16 | Adafruit_BME280::SAMPLING_X1, // temperature 17 | Adafruit_BME280::SAMPLING_X1, // pressure 18 | Adafruit_BME280::SAMPLING_X1, // humidity 19 | Adafruit_BME280::FILTER_OFF); 20 | } 21 | 22 | void BME280PrintValues(void) 23 | { 24 | Serial.print("Temperature = "); 25 | Serial.print(BME280.readTemperature()); 26 | Serial.println(" *C"); 27 | 28 | Serial.print("Pressure = "); 29 | 30 | Serial.print(BME280.readPressure() / 100.0F); 31 | Serial.println(" hPa"); 32 | 33 | Serial.print("Humidity = "); 34 | Serial.print(BME280.readHumidity()); 35 | Serial.println(" %"); 36 | 37 | Serial.println(); 38 | } 39 | 40 | float BME280ReadTemperature(void) 41 | { 42 | float val = BME280.readTemperature(); 43 | Serial.print("Temperature = "); 44 | Serial.print(val); 45 | Serial.println(" °C"); 46 | return val; 47 | } 48 | 49 | float BME280ReadHumidity(void) 50 | { 51 | float val = BME280.readHumidity(); 52 | Serial.print("Humidity = "); 53 | Serial.print(val); 54 | Serial.println(" %"); 55 | return val; 56 | } 57 | 58 | float BME280ReadPressure(void) 59 | { 60 | float val = BME280.readPressure() / 100.0F; 61 | Serial.print("Pressure = "); 62 | Serial.print(val); 63 | Serial.println(" Pa"); 64 | return val; 65 | } -------------------------------------------------------------------------------- /src/bme280.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _BME280_H 3 | #define _BME280_H 4 | 5 | #include 6 | 7 | void BME280Setup(void); 8 | void BME280PrintValues(void); 9 | float BME280ReadTemperature(void); 10 | float BME280ReadHumidity(void); 11 | float BME280ReadPressure(void); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/functions.cpp: -------------------------------------------------------------------------------- 1 | #include "functions.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void PrintResetReason() 9 | { 10 | Serial.print("MCUSR: "); 11 | switch (rtc_get_reset_reason(0)) 12 | { 13 | case 1: 14 | Serial.println("POWERON_RESET"); 15 | break; /**<1, Vbat power on reset*/ 16 | case 3: 17 | Serial.println("SW_RESET"); 18 | break; /**<3, Software reset digital core*/ 19 | case 4: 20 | Serial.println("OWDT_RESET"); 21 | break; /**<4, Legacy watch dog reset digital core*/ 22 | case 5: 23 | Serial.println("DEEPSLEEP_RESET"); 24 | break; /**<5, Deep Sleep reset digital core*/ 25 | case 6: 26 | Serial.println("SDIO_RESET"); 27 | break; /**<6, Reset by SLC module, reset digital core*/ 28 | case 7: 29 | Serial.println("TG0WDT_SYS_RESET"); 30 | break; /**<7, Timer Group0 Watch dog reset digital core*/ 31 | case 8: 32 | Serial.println("TG1WDT_SYS_RESET"); 33 | break; /**<8, Timer Group1 Watch dog reset digital core*/ 34 | case 9: 35 | Serial.println("RTCWDT_SYS_RESET"); 36 | break; /**<9, RTC Watch dog Reset digital core*/ 37 | case 10: 38 | Serial.println("INTRUSION_RESET"); 39 | break; /**<10, Instrusion tested to reset CPU*/ 40 | case 11: 41 | Serial.println("TGWDT_CPU_RESET"); 42 | break; /**<11, Time Group reset CPU*/ 43 | case 12: 44 | Serial.println("SW_CPU_RESET"); 45 | break; /**<12, Software reset CPU*/ 46 | case 13: 47 | Serial.println("RTCWDT_CPU_RESET"); 48 | break; /**<13, RTC Watch dog Reset CPU*/ 49 | case 14: 50 | Serial.println("EXT_CPU_RESET"); 51 | break; /**<14, for APP CPU, reseted by PRO CPU*/ 52 | case 15: 53 | Serial.println("RTCWDT_BROWN_OUT_RESET"); 54 | break; /**<15, Reset when the vdd voltage is not stable*/ 55 | case 16: 56 | Serial.println("RTCWDT_RTC_RESET"); 57 | break; /**<16, RTC Watch dog reset digital core and rtc module*/ 58 | default: 59 | Serial.println("NO_MEAN"); 60 | } 61 | Serial.println(); 62 | } 63 | 64 | void SetupPins() 65 | { 66 | pinMode(PIN_ENABLE_SD011, OUTPUT); 67 | pinMode(PIN_VBAT, INPUT); 68 | } 69 | 70 | long ReadVBat() 71 | { 72 | Serial.print("ReadVBat = "); 73 | long vccraw = 0; 74 | int read_raw; 75 | int smaples_ok = 0; 76 | long vbat = 0; 77 | long raw = 0; 78 | 79 | for (int smaples = 1; smaples <= 50; smaples++) 80 | { 81 | adc2_config_channel_atten(ADC2_CHANNEL_7, ADC_ATTEN_0db); 82 | esp_err_t r = adc2_get_raw(ADC2_CHANNEL_7, ADC_WIDTH_12Bit, &read_raw); 83 | if (r == ESP_OK) 84 | { 85 | smaples_ok++; 86 | raw += read_raw; 87 | delay(10); 88 | } 89 | else if (r == ESP_ERR_TIMEOUT) 90 | { 91 | Serial.println("ADC2 used by Wi-Fi."); 92 | } 93 | } 94 | if (smaples_ok > 0) 95 | { 96 | vccraw = ((1.1 / 4095) * raw * 1000) / smaples_ok; 97 | vbat = vccraw * ((float)REF_VBAT / (float)REF_VCCRAW_ESP32); 98 | Serial.print("VCCraw: "); 99 | Serial.print(vccraw); 100 | Serial.print(" mV, vBat: "); 101 | Serial.print(vbat); 102 | Serial.println(" mV"); 103 | 104 | return vbat; // vbat in millivolts 105 | } 106 | else 107 | return 0; 108 | } 109 | 110 | void I2CScanner() 111 | { 112 | byte error, address; 113 | int nDevices; 114 | 115 | Serial.println("Scanning I2C Bus ..."); 116 | 117 | nDevices = 0; 118 | for (address = 1; address < 127; address++) 119 | { 120 | // The i2c_scanner uses the return value of 121 | // the Write.endTransmisstion to see if 122 | // a device did acknowledge to the address. 123 | Wire.beginTransmission(address); 124 | error = Wire.endTransmission(); 125 | 126 | if (error == 0) 127 | { 128 | Serial.print("I2C device found at address 0x"); 129 | if (address < 16) 130 | Serial.print("0"); 131 | Serial.print(address, HEX); 132 | Serial.println(" !"); 133 | 134 | nDevices++; 135 | } 136 | else if (error == 4) 137 | { 138 | Serial.print("Unknown error at address 0x"); 139 | if (address < 16) 140 | Serial.print("0"); 141 | Serial.println(address, HEX); 142 | } 143 | } 144 | if (nDevices == 0) 145 | Serial.println("No I2C devices found\n"); 146 | else 147 | Serial.println("done\n"); 148 | } 149 | 150 | bool I2CCheckAddress(byte address) 151 | { 152 | byte error; 153 | Wire.beginTransmission(address); 154 | error = Wire.endTransmission(); 155 | if (error == 0) 156 | return true; 157 | else 158 | return false; 159 | } -------------------------------------------------------------------------------- /src/functions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _FUNCTIONS_H 3 | #define _FUNCTIONS_H 4 | 5 | #include 6 | 7 | void PrintResetReason(void); 8 | void SetupPins(); 9 | long ReadVBat(); 10 | void I2CScanner(void); 11 | bool I2CCheckAddress(byte); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/global.cpp: -------------------------------------------------------------------------------- 1 | #include "global.h" 2 | 3 | float PM25 = NAN; 4 | float PM10 = NAN; 5 | Adafruit_BME280 BME280; -------------------------------------------------------------------------------- /src/global.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _GLOBAL_H 3 | #define _GLOBAL_H 4 | 5 | #include 6 | #include 7 | 8 | extern float PM25; 9 | extern float PM10; 10 | extern Adafruit_BME280 BME280; 11 | 12 | #endif -------------------------------------------------------------------------------- /src/lorawan.cpp: -------------------------------------------------------------------------------- 1 | #include "lorawan.h" 2 | #include "settings.h" 3 | #include 4 | #include 5 | #include "io_pins.h" 6 | #include "functions.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // Pin mapping 15 | const lmic_pinmap lmic_pins = { 16 | .nss = PIN_LMIC_NSS, 17 | .rxtx = LMIC_UNUSED_PIN, 18 | .rst = PIN_LMIC_RST, 19 | .dio = {PIN_LMIC_DIO0, PIN_LMIC_DIO1, PIN_LMIC_DIO2}, 20 | }; 21 | 22 | static uint8_t LORA_DATA[15]; 23 | 24 | // Schedule TX every this many seconds (might become longer due to duty cycle limitations). 25 | const unsigned TX_INTERVAL = LORA_TX_INTERVAL; 26 | 27 | void os_getArtEui(u1_t *buf) { memcpy_P(buf, APPEUI, 8); } 28 | void os_getDevEui(u1_t *buf) { memcpy_P(buf, DEVEUI, 8); } 29 | void os_getDevKey(u1_t *buf) { memcpy_P(buf, APPKEY, 16); } 30 | 31 | bool GO_DEEP_SLEEP = false; 32 | 33 | RTC_DATA_ATTR lmic_t RTC_LMIC; 34 | 35 | void LoRaWANSetup() 36 | { 37 | Serial.println(F("LoRaWAN_Setup ...")); 38 | 39 | Serial.print(F("Saved seqnoUp: ")); 40 | Serial.println(LMIC.seqnoUp); 41 | 42 | // LMIC init 43 | os_init(); 44 | 45 | // Let LMIC compensate for +/- 1% clock error 46 | LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); 47 | 48 | // Reset the MAC state. Session and pending data transfers will be discarded. 49 | LMIC_reset(); 50 | 51 | if (RTC_LMIC.seqnoUp != 0) 52 | { 53 | LoraWANLoadLMICFromRTC(); 54 | } 55 | 56 | // Start job 57 | LoraWANDo_send(&sendjob); 58 | } 59 | 60 | void LoraWANDo_send(osjob_t *j) 61 | { 62 | // Check if there is not a current TX/RX job running 63 | if (LMIC.opmode & OP_TXRXPEND) { 64 | Serial.println(F("OP_TXRXPEND, not sending")); 65 | } else if (LMIC.opmode & OP_TXDATA) { 66 | Serial.println(F("OP_TXDATA, not sending")); 67 | } else { 68 | LoraWANGetData(); 69 | 70 | // Prepare upstream data transmission at the next possible time. 71 | LMIC_setTxData2(1, LORA_DATA, sizeof(LORA_DATA), 0); 72 | Serial.println(F("Packet queued")); 73 | } 74 | // Next TX is scheduled after TX_COMPLETE event. 75 | } 76 | 77 | void onEvent(ev_t ev) 78 | { 79 | Serial.print(os_getTime()); 80 | Serial.print(": "); 81 | switch (ev) 82 | { 83 | case EV_SCAN_TIMEOUT: 84 | Serial.println(F("EV_SCAN_TIMEOUT")); 85 | break; 86 | case EV_BEACON_FOUND: 87 | Serial.println(F("EV_BEACON_FOUND")); 88 | break; 89 | case EV_BEACON_MISSED: 90 | Serial.println(F("EV_BEACON_MISSED")); 91 | break; 92 | case EV_BEACON_TRACKED: 93 | Serial.println(F("EV_BEACON_TRACKED")); 94 | break; 95 | case EV_JOINING: 96 | Serial.println(F("EV_JOINING")); 97 | break; 98 | case EV_JOINED: 99 | Serial.println(F("EV_JOINED")); 100 | #ifndef DISABLE_JOIN 101 | { 102 | u4_t netid = 0; 103 | devaddr_t devaddr = 0; 104 | u1_t nwkKey[16]; 105 | u1_t artKey[16]; 106 | LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey); 107 | Serial.print("netid: "); 108 | Serial.println(netid, DEC); 109 | Serial.print("devaddr: "); 110 | Serial.println(devaddr, HEX); 111 | Serial.print("artKey: "); 112 | for (size_t i = 0; i < sizeof(artKey); ++i) 113 | { 114 | Serial.print(artKey[i], HEX); 115 | } 116 | Serial.println(""); 117 | Serial.print("nwkKey: "); 118 | for (size_t i = 0; i < sizeof(nwkKey); ++i) 119 | { 120 | Serial.print(nwkKey[i], HEX); 121 | } 122 | Serial.println(""); 123 | } 124 | // Disable link check validation (automatically enabled 125 | // during join, but because slow data rates change max TX 126 | // size, we don't use it in this example. 127 | LMIC_setLinkCheckMode(0); 128 | #endif 129 | break; 130 | /* 131 | || This event is defined but not used in the code. No 132 | || point in wasting codespace on it. 133 | || 134 | || case EV_SCAN_FOUND: 135 | || Serial.println(F("EV_SCAN_FOUND")); 136 | || break; 137 | */ 138 | case EV_JOIN_FAILED: 139 | Serial.println(F("EV_JOIN_FAILED")); 140 | break; 141 | case EV_REJOIN_FAILED: 142 | Serial.println(F("EV_REJOIN_FAILED")); 143 | break; 144 | case EV_TXCOMPLETE: 145 | Serial.println(F("EV_TXCOMPLETE")); 146 | 147 | if (LMIC.txrxFlags & TXRX_ACK) 148 | { 149 | Serial.println(F("Received ack")); 150 | } 151 | 152 | if (LMIC.dataLen) 153 | { 154 | Serial.print(LMIC.dataLen); 155 | Serial.println(F(" bytes of payload")); 156 | } 157 | 158 | GO_DEEP_SLEEP = true; 159 | 160 | break; 161 | case EV_LOST_TSYNC: 162 | Serial.println(F("EV_LOST_TSYNC")); 163 | break; 164 | case EV_RESET: 165 | Serial.println(F("EV_RESET")); 166 | break; 167 | case EV_RXCOMPLETE: 168 | // data received in ping slot 169 | Serial.println(F("EV_RXCOMPLETE")); 170 | break; 171 | case EV_LINK_DEAD: 172 | Serial.println(F("EV_LINK_DEAD")); 173 | break; 174 | case EV_LINK_ALIVE: 175 | Serial.println(F("EV_LINK_ALIVE")); 176 | break; 177 | /* This event is defined but not used in the code. 178 | case EV_SCAN_FOUND: 179 | DisplayPrintln(F("EV_SCAN_FOUND"), LORAWAN_STATE_DISPLAY_LINE); 180 | break; 181 | */ 182 | case EV_TXSTART: 183 | Serial.println(F("EV_TXSTART")); 184 | break; 185 | case EV_TXCANCELED: 186 | Serial.println(F("EV_TXCANCELED")); 187 | break; 188 | case EV_RXSTART: 189 | /* do not print anything -- it wrecks timing */ 190 | break; 191 | case EV_JOIN_TXCOMPLETE: 192 | Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept")); 193 | break; 194 | default: 195 | Serial.print(F("Unknown event: ")); 196 | Serial.println((unsigned)ev); 197 | break; 198 | } 199 | } 200 | 201 | void LoraWANDo(void) 202 | { 203 | long seconds = millis() / 1000; 204 | static unsigned long last_runntime_info = 0; 205 | static unsigned long runntime_info_ever_ms = 5000; 206 | 207 | if (GO_DEEP_SLEEP == true && !os_queryTimeCriticalJobs(ms2osticksRound((LORA_TX_INTERVAL * 1000)))) 208 | { 209 | Serial.println(F("Go to DeepSleep ...")); 210 | Serial.print(F("Runtime was: ")); 211 | Serial.print(seconds); 212 | Serial.println(F(" seconds")); 213 | 214 | LoraWANSaveLMICToRTC(LORA_TX_INTERVAL); 215 | Serial.flush(); 216 | 217 | PowerDeepSleepTimer(LORA_TX_INTERVAL - 30 - 8); // 30sec for SDS011, 8 sec for remaining code 218 | } 219 | else 220 | { 221 | if(last_runntime_info + runntime_info_ever_ms < millis()) 222 | { 223 | Serial.print("Runtime: "); 224 | Serial.print(seconds); 225 | Serial.println(" seconds"); 226 | #ifndef PRINTDEBUGS 227 | LoraWANDebug(LMIC); 228 | #endif 229 | last_runntime_info = millis(); 230 | } 231 | 232 | os_runloop_once(); 233 | } 234 | } 235 | 236 | /* 237 | Byte 1: VCC 238 | Byte 2: PM25 239 | Byte 3: PM25 240 | Byte 4: PM10 241 | Byte 5: PM10 242 | Byte 6: Temperature 243 | Byte 7: Temperature 244 | Byte 8: Humidity 245 | Byte 9: Pressure 246 | Byte 10: Pressure 247 | Byte 11: Pressure (first Bit), the remaining not used 248 | Byte 12: UV Index 249 | Byte 13: LUX (Value: 0-8800000) 250 | Byte 14: LUX 251 | Byte 15: LUX 252 | */ 253 | void LoraWANGetData() 254 | { 255 | uint8_t tmp_u8; 256 | uint16_t tmp_u16; 257 | uint32_t tmp_u32; 258 | float tmp_float; 259 | 260 | // VCC 261 | /**************************************************************************/ 262 | tmp_u8 = (ReadVBat() / 10) - 200; 263 | LORA_DATA[0] = tmp_u8; 264 | 265 | // PM25 266 | /**************************************************************************/ 267 | tmp_u16 = (PM25 * 10); 268 | if (isnan(PM25)) 269 | { 270 | LORA_DATA[1] = 255; 271 | LORA_DATA[2] = 255; 272 | } 273 | else 274 | { 275 | LORA_DATA[1] = tmp_u16 >> 8; 276 | LORA_DATA[2] = tmp_u16 & 0xFF; 277 | } 278 | 279 | // PM10 280 | /**************************************************************************/ 281 | tmp_u16 = (PM10 * 10); 282 | if (isnan(PM10)) 283 | { 284 | LORA_DATA[3] = 255; 285 | LORA_DATA[4] = 255; 286 | } 287 | else 288 | { 289 | LORA_DATA[3] = tmp_u16 >> 8; 290 | LORA_DATA[4] = tmp_u16 & 0xFF; 291 | } 292 | 293 | if(I2CCheckAddress(0x76)) 294 | { 295 | // Temperature 296 | /**************************************************************************/ 297 | tmp_u16 = (BME280ReadTemperature() * 10); 298 | LORA_DATA[5] = tmp_u16 >> 8; 299 | LORA_DATA[6] = tmp_u16 & 0xFF; 300 | 301 | // Humidity 302 | /**************************************************************************/ 303 | tmp_float = BME280ReadHumidity(); 304 | tmp_u8 = tmp_float; 305 | LORA_DATA[7] = tmp_u8; 306 | // Bit 8 for decimal 1 = 0.5 307 | if ((tmp_float - tmp_u8) > 0.251 && (tmp_float - tmp_u8) < 0.751) 308 | { 309 | LORA_DATA[7] |= (1 << 7); 310 | } 311 | else if ((tmp_float - tmp_u8) > 0.751) 312 | { 313 | LORA_DATA[7] = LORA_DATA[7] + 1; 314 | } 315 | 316 | // Pressure 317 | /**************************************************************************/ 318 | tmp_float = BME280ReadPressure(); 319 | tmp_u32 = (tmp_float * 100); 320 | LORA_DATA[8] = (tmp_u32 >> (8 * 0)) & 0xff; 321 | LORA_DATA[9] = (tmp_u32 >> (8 * 1)) & 0xff; 322 | LORA_DATA[10] = (tmp_u32 >> (8 * 2)) & 0xff; 323 | } 324 | else 325 | { 326 | LORA_DATA[5] = 255; 327 | LORA_DATA[6] = 255; 328 | LORA_DATA[7] = 255; 329 | LORA_DATA[8] = 255; 330 | LORA_DATA[9] = 255; 331 | } 332 | 333 | // UV Index 334 | if(I2CCheckAddress(0x10)) 335 | { 336 | tmp_float = VEML6075GetUVI(); 337 | tmp_u8 = (tmp_float * 10); 338 | LORA_DATA[11] = tmp_u8; 339 | } 340 | else 341 | { 342 | LORA_DATA[11] = 255; 343 | } 344 | 345 | // Lux 346 | if (I2CCheckAddress(0x29)) 347 | { 348 | tmp_float = TSL2591GetLux(true, true); 349 | if(tmp_float < 0) 350 | { 351 | Serial.println("MAX LUX reached!"); 352 | tmp_float = MAX_LUX; 353 | } 354 | tmp_u32 = tmp_float * 100; 355 | 356 | LORA_DATA[12] = (tmp_u32 >> (8 * 0)) & 0xff; 357 | LORA_DATA[13] = (tmp_u32 >> (8 * 1)) & 0xff; 358 | LORA_DATA[14] = (tmp_u32 >> (8 * 2)) & 0xff; 359 | } 360 | else 361 | { 362 | LORA_DATA[12] = 255; 363 | LORA_DATA[13] = 255; 364 | LORA_DATA[14] = 255; 365 | } 366 | } 367 | 368 | void LoraWANSaveLMICToRTC(int deepsleep_sec) 369 | { 370 | Serial.println(F("Save LMIC to RTC ...")); 371 | RTC_LMIC = LMIC; 372 | 373 | //System time is resetted after sleep. So we need to calculate the dutycycle with a resetted system time 374 | unsigned long now = millis(); 375 | 376 | // EU Like Bands 377 | #if defined(CFG_LMIC_EU_like) 378 | for(int i = 0; i < MAX_BANDS; i++) { 379 | ostime_t correctedAvail = RTC_LMIC.bands[i].avail - ((now/1000.0 + deepsleep_sec ) * OSTICKS_PER_SEC); 380 | if(correctedAvail < 0) { 381 | correctedAvail = 0; 382 | } 383 | RTC_LMIC.bands[i].avail = correctedAvail; 384 | } 385 | 386 | RTC_LMIC.globalDutyAvail = RTC_LMIC.globalDutyAvail - ((now/1000.0 + deepsleep_sec ) * OSTICKS_PER_SEC); 387 | if(RTC_LMIC.globalDutyAvail < 0) 388 | { 389 | RTC_LMIC.globalDutyAvail = 0; 390 | } 391 | #else 392 | Serial.println("No DutyCycle recalculation function!") 393 | #endif 394 | 395 | #ifndef PRINTDEBUGS 396 | LoraWANDebug(RTC_LMIC); 397 | #endif 398 | } 399 | 400 | void LoraWANLoadLMICFromRTC() 401 | { 402 | Serial.println(F("Load LMIC vars from RTC ...")); 403 | LMIC = RTC_LMIC; 404 | 405 | #ifndef PRINTDEBUGS 406 | LoraWANDebug(RTC_LMIC); 407 | #endif 408 | } 409 | 410 | void LoraWANPrintVersion(void) 411 | { 412 | Serial.print(F("LMIC Version: ")); 413 | Serial.print(ARDUINO_LMIC_VERSION_GET_MAJOR (ARDUINO_LMIC_VERSION) ); 414 | Serial.print(F(".")); 415 | Serial.print(ARDUINO_LMIC_VERSION_GET_MINOR (ARDUINO_LMIC_VERSION) ); 416 | Serial.print(F(".")); 417 | Serial.print(ARDUINO_LMIC_VERSION_GET_PATCH (ARDUINO_LMIC_VERSION) ); 418 | Serial.print(F(".")); 419 | Serial.println(ARDUINO_LMIC_VERSION_GET_LOCAL (ARDUINO_LMIC_VERSION) ); 420 | } 421 | 422 | // opmode def 423 | // https://github.com/mcci-catena/arduino-lmic/blob/89c28c5888338f8fc851851bb64968f2a493462f/src/lmic/lmic.h#L233 424 | void LoraWANPrintLMICOpmode(void) 425 | { 426 | Serial.print(F("LMIC.opmode: ")); 427 | if (LMIC.opmode & OP_NONE) { Serial.print(F("OP_NONE ")); } 428 | if (LMIC.opmode & OP_SCAN) { Serial.print(F("OP_SCAN ")); } 429 | if (LMIC.opmode & OP_TRACK) { Serial.print(F("OP_TRACK ")); } 430 | if (LMIC.opmode & OP_JOINING) { Serial.print(F("OP_JOINING ")); } 431 | if (LMIC.opmode & OP_TXDATA) { Serial.print(F("OP_TXDATA ")); } 432 | if (LMIC.opmode & OP_POLL) { Serial.print(F("OP_POLL ")); } 433 | if (LMIC.opmode & OP_REJOIN) { Serial.print(F("OP_REJOIN ")); } 434 | if (LMIC.opmode & OP_SHUTDOWN) { Serial.print(F("OP_SHUTDOWN ")); } 435 | if (LMIC.opmode & OP_TXRXPEND) { Serial.print(F("OP_TXRXPEND ")); } 436 | if (LMIC.opmode & OP_RNDTX) { Serial.print(F("OP_RNDTX ")); } 437 | if (LMIC.opmode & OP_PINGINI) { Serial.print(F("OP_PINGINI ")); } 438 | if (LMIC.opmode & OP_PINGABLE) { Serial.print(F("OP_PINGABLE ")); } 439 | if (LMIC.opmode & OP_NEXTCHNL) { Serial.print(F("OP_NEXTCHNL ")); } 440 | if (LMIC.opmode & OP_LINKDEAD) { Serial.print(F("OP_LINKDEAD ")); } 441 | if (LMIC.opmode & OP_LINKDEAD) { Serial.print(F("OP_LINKDEAD ")); } 442 | if (LMIC.opmode & OP_TESTMODE) { Serial.print(F("OP_TESTMODE ")); } 443 | if (LMIC.opmode & OP_UNJOIN) { Serial.print(F("OP_UNJOIN ")); } 444 | Serial.println(""); 445 | } 446 | 447 | void LoraWANDebug(lmic_t lmic_to_check) 448 | { 449 | Serial.println(""); 450 | Serial.println(""); 451 | 452 | LoraWANPrintLMICOpmode(); 453 | 454 | Serial.print("LMIC.seqnoUp = "); 455 | Serial.println(lmic_to_check.seqnoUp); 456 | 457 | Serial.print("LMIC.globalDutyRate = "); 458 | Serial.print(lmic_to_check.globalDutyRate); 459 | Serial.print(" osTicks, "); 460 | Serial.print(osticks2ms(lmic_to_check.globalDutyRate)/1000); 461 | Serial.println(" sec"); 462 | 463 | Serial.print("LMIC.globalDutyAvail = "); 464 | Serial.print(lmic_to_check.globalDutyAvail); 465 | Serial.print(" osTicks, "); 466 | Serial.print(osticks2ms(lmic_to_check.globalDutyAvail)/1000); 467 | Serial.println(" sec"); 468 | 469 | Serial.print("LMICbandplan_nextTx = "); 470 | Serial.print(LMICbandplan_nextTx(os_getTime())); 471 | Serial.print(" osTicks, "); 472 | Serial.print(osticks2ms(LMICbandplan_nextTx(os_getTime()))/1000); 473 | Serial.println(" sec"); 474 | 475 | Serial.print("os_getTime = "); 476 | Serial.print(os_getTime()); 477 | Serial.print(" osTicks, "); 478 | Serial.print(osticks2ms(os_getTime()) / 1000); 479 | Serial.println(" sec"); 480 | 481 | Serial.print("LMIC.txend = "); 482 | Serial.println(lmic_to_check.txend); 483 | Serial.print("LMIC.txChnl = "); 484 | Serial.println(lmic_to_check.txChnl); 485 | 486 | Serial.println("Band \tavail \t\tavail_sec\tlastchnl \ttxcap"); 487 | for (u1_t bi = 0; bi < MAX_BANDS; bi++) 488 | { 489 | Serial.print(bi); 490 | Serial.print("\t"); 491 | Serial.print(lmic_to_check.bands[bi].avail); 492 | Serial.print("\t\t"); 493 | Serial.print(osticks2ms(lmic_to_check.bands[bi].avail)/1000); 494 | Serial.print("\t\t"); 495 | Serial.print(lmic_to_check.bands[bi].lastchnl); 496 | Serial.print("\t\t"); 497 | Serial.println(lmic_to_check.bands[bi].txcap); 498 | 499 | } 500 | Serial.println(""); 501 | Serial.println(""); 502 | } -------------------------------------------------------------------------------- /src/lorawan.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _LORAWAN_H 3 | #define _LORAWAN_H 4 | 5 | #include 6 | #include 7 | #include "lora_credentials.h" 8 | 9 | // This EUI must be in little-endian format, so least-significant-byte 10 | // first. When copying an EUI from ttnctl output, this means to reverse 11 | // the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 12 | // 0x70. 13 | static const u1_t PROGMEM APPEUI[8] = TTN_APPEUI; 14 | 15 | // This should also be in little endian format, see above. 16 | static const u1_t PROGMEM DEVEUI[8] = TTN_DEVEUI; 17 | 18 | // This key should be in big endian format (or, since it is not really a 19 | // number but a block of memory, endianness does not really apply). In 20 | // practice, a key taken from ttnctl can be copied as-is. 21 | static const u1_t PROGMEM APPKEY[16] = TTN_APPKEY; 22 | 23 | static osjob_t sendjob; 24 | 25 | // LMIC State save for reboot 26 | extern RTC_DATA_ATTR lmic_t RTC_LMIC; 27 | 28 | void os_getArtEui(u1_t *buf); 29 | void os_getDevEui(u1_t *buf); 30 | void os_getDevKey(u1_t *buf); 31 | 32 | void LoRaWANSetup(void); 33 | void LoraWANDo_send(osjob_t *j); 34 | void LoraWANDo(void); 35 | void LoraWANGetData(void); 36 | void LoraWANSaveLMICToRTC(int deepsleep_sec); 37 | void LoraWANLoadLMICFromRTC(void); 38 | void LoraWANPrintVersion(void); 39 | void LoraWANPrintLMICOpmode(void); 40 | void LoraWANDebug(lmic_t lmic_to_check); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "soc/soc.h" 7 | #include "soc/rtc_cntl_reg.h" 8 | #include "version.h" 9 | #include "version_build.h" 10 | #include "bme280.h" 11 | #include "esp32-hal-cpu.h" 12 | #include 13 | #include 14 | #include "settings.h" 15 | #include 16 | 17 | void setup() 18 | { 19 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Disable Brownout Detector 20 | setCpuFrequencyMhz(10); 21 | Serial.begin(115200); 22 | Serial.println(F("Starting environment sensor ...")); 23 | Serial.println("Sketch: " VERSION_MAJOR "." VERSION_MINOR "." VERSION_PATCH "." BUILD_COMMIT "-" BUILD_BRANCH); 24 | Serial.println("Builddate: " BUILD_DATE " " BUILD_TIME); 25 | LoraWANPrintVersion(); 26 | PrintResetReason(); 27 | 28 | Serial.print("CPU Speed: "); 29 | Serial.print(getCpuFrequencyMhz()); 30 | Serial.println(" MHz"); 31 | 32 | SetupPins(); 33 | 34 | Wire.begin(); 35 | I2CScanner(); 36 | 37 | if (ReadVBat() < 3300) 38 | { 39 | Serial.println("Goto DeepSleep (VBat to low)"); 40 | PowerDeepSleepTimer(LORA_TX_INTERVAL); 41 | } 42 | 43 | // Setup BME280 and print values 44 | if (I2CCheckAddress(0x76)) 45 | { 46 | BME280Setup(); 47 | BME280PrintValues(); 48 | } 49 | else 50 | { 51 | Serial.println("No BME280 found!"); 52 | } 53 | 54 | // Setup VEML6075 55 | if (I2CCheckAddress(0x10)) 56 | { 57 | VEML6075Setup(); 58 | VEML6075GetUVI(); 59 | } 60 | else 61 | { 62 | Serial.println("No VEML6075 found!"); 63 | } 64 | 65 | // Setup TSL2591 66 | if (I2CCheckAddress(0x29)) 67 | { 68 | TSL2591Setup(); 69 | TSL2591GetLux(true, true); 70 | } 71 | else 72 | { 73 | Serial.println("No TSL2591 found!"); 74 | } 75 | 76 | // Setup SD011 and read values 77 | ParticlePower(true); 78 | ParticleSetup(); 79 | ParticleRead(true, true, PM25, PM10); 80 | ParticlePower(false); 81 | 82 | LoRaWANSetup(); 83 | } 84 | 85 | void loop() 86 | { 87 | LoraWANDo(); 88 | } -------------------------------------------------------------------------------- /src/particle.cpp: -------------------------------------------------------------------------------- 1 | #include "particle.h" 2 | #include "SdsDustSensor.h" 3 | #include 4 | #include 5 | #include 6 | 7 | SdsDustSensor sds(SDS011_SERIAL); 8 | 9 | void ParticleSetup(void) 10 | { 11 | delay(600); 12 | 13 | sds.begin(); 14 | sds.setQueryReportingMode(); // ensures sensor is in 'query' reporting mode 15 | 16 | Serial.println(sds.queryFirmwareVersion().toString()); 17 | Serial.println(sds.queryReportingMode().toString()); 18 | Serial.println(sds.queryWorkingState().toString()); 19 | Serial.println(sds.queryWorkingPeriod().toString()); 20 | } 21 | 22 | void ParticleRead(bool wakeup, bool sleep, float &pm25, float &pm10) 23 | { 24 | // Wakeup and wait 25 | if (wakeup == true) 26 | { 27 | ParticleWakeup(true); 28 | } 29 | 30 | PmResult pm = sds.queryPm(); 31 | if (pm.isOk()) 32 | { 33 | Serial.print("PM2.5 = "); 34 | Serial.print(pm.pm25); 35 | pm25 = pm.pm25; 36 | Serial.print(", PM10 = "); 37 | pm10 = pm.pm10; 38 | Serial.println(pm.pm10); 39 | } 40 | else 41 | { 42 | Serial.print("Could not read values from sensor, reason: "); 43 | Serial.println(pm.statusToString()); 44 | } 45 | 46 | if (sleep == true) 47 | { 48 | ParticleSleep(); 49 | } 50 | } 51 | 52 | void ParticleSleep(void) 53 | { 54 | WorkingStateResult state = sds.sleep(); 55 | if (state.isWorking()) 56 | { 57 | Serial.println("Problem with sleeping the sensor."); 58 | } 59 | else 60 | { 61 | Serial.println("Sensor is sleeping"); 62 | } 63 | } 64 | 65 | void ParticleWakeup(bool wait = true) 66 | { 67 | sds.wakeup(); 68 | 69 | if (wait == true) 70 | { 71 | PowerLightSleepTimer(30); 72 | } 73 | } 74 | 75 | void ParticlePower(bool power) 76 | { 77 | digitalWrite(PIN_ENABLE_SD011, power); 78 | if (power == true) 79 | { 80 | delay(600); 81 | } 82 | } -------------------------------------------------------------------------------- /src/particle.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _PARTICLE_H 3 | #define _PARTICLE_H 4 | 5 | #include 6 | 7 | void ParticleSetup(void); 8 | void ParticleRead(bool wakeup, bool sleep, float &pm25, float &pm10); 9 | void ParticleSleep(void); 10 | void ParticleWakeup(bool wait); 11 | void ParticlePower(bool power); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/power.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void PowerDeepSleepTimer(int seconds) 5 | { 6 | esp_sleep_enable_timer_wakeup(seconds * 1000000); 7 | esp_deep_sleep_start(); 8 | } 9 | 10 | void PowerLightSleepTimer(int seconds) 11 | { 12 | adc_power_off(); 13 | 14 | esp_sleep_enable_timer_wakeup(seconds * 1000000); 15 | esp_light_sleep_start(); 16 | } -------------------------------------------------------------------------------- /src/power.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _POWER_H 3 | #define _POWER_H 4 | 5 | #include 6 | 7 | void PowerDeepSleepTimer(int); 8 | void PowerLightSleepTimer(int); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/tsl2591.cpp: -------------------------------------------------------------------------------- 1 | #include "tsl2591.h" 2 | #include 3 | #include 4 | 5 | Adafruit_TSL2591 TSL2591 = Adafruit_TSL2591(2591); 6 | 7 | void TSL2591ShowGain() 8 | { 9 | tsl2591Gain_t gain = TSL2591.getGain(); 10 | switch (gain) 11 | { 12 | case TSL2591_GAIN_LOW: 13 | Serial.print(F("1x (Low)")); 14 | break; 15 | case TSL2591_GAIN_MED: 16 | Serial.print(F("25x (Medium)")); 17 | break; 18 | case TSL2591_GAIN_HIGH: 19 | Serial.print(F("428x (High)")); 20 | break; 21 | case TSL2591_GAIN_MAX: 22 | Serial.print(F("9876x (Max)")); 23 | break; 24 | } 25 | } 26 | 27 | bool TSL2591UpperGain() 28 | { 29 | tsl2591Gain_t gain = TSL2591.getGain(); 30 | bool update = false; 31 | switch (gain) 32 | { 33 | case TSL2591_GAIN_LOW: 34 | TSL2591.setGain(TSL2591_GAIN_MED); 35 | update = true; 36 | break; 37 | case TSL2591_GAIN_MED: 38 | TSL2591.setGain(TSL2591_GAIN_HIGH); 39 | update = true; 40 | break; 41 | case TSL2591_GAIN_HIGH: 42 | TSL2591.setGain(TSL2591_GAIN_MAX); 43 | update = true; 44 | break; 45 | case TSL2591_GAIN_MAX: 46 | update = false; 47 | break; 48 | } 49 | 50 | return update; 51 | } 52 | 53 | bool TSL2591LowerGain() 54 | { 55 | tsl2591Gain_t gain = TSL2591.getGain(); 56 | bool update = false; 57 | switch (gain) 58 | { 59 | case TSL2591_GAIN_LOW: 60 | update = false; 61 | break; 62 | case TSL2591_GAIN_MED: 63 | TSL2591.setGain(TSL2591_GAIN_LOW); 64 | update = true; 65 | break; 66 | case TSL2591_GAIN_HIGH: 67 | TSL2591.setGain(TSL2591_GAIN_MED); 68 | update = true; 69 | break; 70 | case TSL2591_GAIN_MAX: 71 | TSL2591.setGain(TSL2591_GAIN_HIGH); 72 | update = true; 73 | break; 74 | } 75 | 76 | return update; 77 | } 78 | 79 | void TSL2591AutoGain() 80 | { 81 | TSL2591.setGain(TSL2591_GAIN_LOW); 82 | float lux; 83 | bool gain_ok = false; 84 | 85 | while (gain_ok == false) 86 | { 87 | lux = TSL2591GetLux(false, false); 88 | 89 | if (lux > 0) 90 | { 91 | // Max gain and reading OK 92 | if (TSL2591UpperGain() == false && TSL2591GetLux(false, true) > 0) 93 | { 94 | gain_ok = true; 95 | } 96 | } 97 | else 98 | { 99 | TSL2591LowerGain(); 100 | gain_ok = true; 101 | } 102 | } 103 | } 104 | 105 | float TSL2591GetLux(bool autogain = false, bool print = true) 106 | { 107 | uint32_t lum; 108 | uint16_t ir, full; 109 | 110 | if (autogain == true) 111 | { 112 | TSL2591AutoGain(); 113 | } 114 | 115 | lum = TSL2591.getFullLuminosity(); 116 | ir = lum >> 16; 117 | full = lum & 0xFFFF; 118 | float lux = TSL2591.calculateLux(full, ir); 119 | 120 | if (print == true) 121 | { 122 | Serial.print(F("IR: ")); 123 | Serial.print(ir); 124 | Serial.print(F(" ")); 125 | Serial.print(F("Full: ")); 126 | Serial.print(full); 127 | Serial.print(F(" ")); 128 | Serial.print(F("Visible: ")); 129 | Serial.print(full - ir); 130 | Serial.print(F(" ")); 131 | Serial.print(F("Lux: ")); 132 | Serial.print(lux, 6); 133 | Serial.print(F(" ")); 134 | Serial.print(F("Auto Gain: ")); 135 | if (autogain == true) 136 | { 137 | Serial.print(F("yes - ")); 138 | } 139 | else 140 | { 141 | Serial.print(F("no - ")); 142 | } 143 | TSL2591ShowGain(); 144 | Serial.println(F(" ")); 145 | } 146 | 147 | return lux; 148 | } 149 | 150 | void TSL2591Setup() 151 | { 152 | Serial.println(F("Setup TSL2591 sensor")); 153 | 154 | if (TSL2591.begin()) 155 | { 156 | Serial.println(F("Found a TSL2591 sensor")); 157 | 158 | TSL2591.setGain(TSL2591_GAIN_MED); 159 | TSL2591.setTiming(TSL2591_INTEGRATIONTIME_300MS); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/tsl2591.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _TSL2591_H 3 | #define _TSL2591_H 4 | 5 | #include 6 | 7 | void TSL2591ShowGain(void); 8 | bool TSL2591UpperGain(void); 9 | bool TSL2591LowerGain(void); 10 | void TSL2591AutoGain(void); 11 | float TSL2591GetLux(bool, bool); 12 | void TSL2591Setup(void); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /src/veml6075.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Adafruit_VEML6075.h" 4 | 5 | Adafruit_VEML6075 UVSENSOR = Adafruit_VEML6075(); 6 | 7 | void VEML6075Setup(void) 8 | { 9 | Serial.println(F("Setup VEML6075 sensor")); 10 | if (!UVSENSOR.begin()) 11 | { 12 | Serial.println("Failed to communicate with VEML6075 sensor!"); 13 | } 14 | else 15 | { 16 | Serial.println("Found VEML6075 sensor"); 17 | 18 | UVSENSOR.setIntegrationTime(VEML6075_100MS); 19 | Serial.print("Integration time set to "); 20 | switch (UVSENSOR.getIntegrationTime()) 21 | { 22 | case VEML6075_50MS: 23 | Serial.print("50"); 24 | break; 25 | case VEML6075_100MS: 26 | Serial.print("100"); 27 | break; 28 | case VEML6075_200MS: 29 | Serial.print("200"); 30 | break; 31 | case VEML6075_400MS: 32 | Serial.print("400"); 33 | break; 34 | case VEML6075_800MS: 35 | Serial.print("800"); 36 | break; 37 | } 38 | Serial.println(" ms"); 39 | 40 | // Set the high dynamic mode 41 | UVSENSOR.setHighDynamic(true); 42 | if (UVSENSOR.getHighDynamic()) 43 | { 44 | Serial.println("High dynamic reading mode"); 45 | } 46 | else 47 | { 48 | Serial.println("Normal dynamic reading mode"); 49 | } 50 | 51 | // Set the mode 52 | UVSENSOR.setForcedMode(false); 53 | if (UVSENSOR.getForcedMode()) 54 | { 55 | Serial.println("Forced reading mode"); 56 | } 57 | else 58 | { 59 | Serial.println("Continuous reading mode"); 60 | } 61 | 62 | // Set the calibration coefficients 63 | // https://learn.adafruit.com/adafruit-veml6075-uva-uvb-uv-index-sensor/arduino-test 64 | // https://en.wikipedia.org/wiki/Ultraviolet_index 65 | UVSENSOR.setCoefficients(2.22, 1.33, // UVA_A and UVA_B coefficients 66 | 2.95, 1.74, // UVB_C and UVB_D coefficients 67 | 0.001461, 0.002591); // UVA and UVB responses 68 | 69 | UVSENSOR.shutdown(true); 70 | } 71 | } 72 | 73 | float VEML6075GetUVI(void) 74 | { 75 | UVSENSOR.shutdown(false); 76 | delay(200); // Wait for wakeup 77 | 78 | float uva = UVSENSOR.readUVA(); 79 | float uvb = UVSENSOR.readUVB(); 80 | float uvi = UVSENSOR.readUVI(); 81 | 82 | UVSENSOR.shutdown(true); 83 | 84 | Serial.print("UVA: "); 85 | Serial.println(uva); 86 | Serial.print("UVB: "); 87 | Serial.println(uvb); 88 | Serial.print("UVI: "); 89 | Serial.println(uvi); 90 | 91 | return uvi; 92 | } 93 | -------------------------------------------------------------------------------- /src/veml6075.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _VEML6075_H 3 | #define _VEML6075_H 4 | 5 | #include 6 | 7 | void VEML6075Setup(void); 8 | float VEML6075GetUVI(void); 9 | 10 | #endif 11 | --------------------------------------------------------------------------------