├── .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 |
--------------------------------------------------------------------------------