Commit d0fe348e authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'feature/lorawan' into develop

parents b0fc3f96 5e4c7e78
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/***************************************************************** /*****************************************************************
* Libraries * * Libraries *
*****************************************************************/ *****************************************************************/
//TODO: check header dependencies, and simplify if possible.
#include "config.h" #include "config.h"
#ifndef MEASUREMENT_TIMESTEP #ifndef MEASUREMENT_TIMESTEP
# error Missing config.h file. Please copy config.example.h to config.h. # error Missing config.h file. Please copy config.example.h to config.h.
...@@ -10,6 +11,9 @@ ...@@ -10,6 +11,9 @@
#ifdef MQTT #ifdef MQTT
# include "mqtt.h" # include "mqtt.h"
#endif #endif
#ifdef LORAWAN
# include "lorawan.h"
#endif
#include "util.h" #include "util.h"
#include "wifi_util.h" #include "wifi_util.h"
......
...@@ -99,7 +99,12 @@ void setup() { ...@@ -99,7 +99,12 @@ void setup() {
mqtt::initialize("CO2sensors/" + SENSOR_ID); mqtt::initialize("CO2sensors/" + SENSOR_ID);
#endif #endif
} }
csv_writer::initialize(); csv_writer::initialize();
#if defined(LORAWAN) && defined(ESP32)
lorawan::initialize();
#endif
} }
/***************************************************************** /*****************************************************************
...@@ -107,6 +112,15 @@ void setup() { ...@@ -107,6 +112,15 @@ void setup() {
*****************************************************************/ *****************************************************************/
void loop() { void loop() {
#if defined(LORAWAN) && defined(ESP32)
//LMIC Library seems to be very sensitive to timing issues, so run it first.
lorawan::process();
if (lorawan::waiting_for_confirmation) {
// If node is waiting for join confirmation from Gateway, nothing else should run.
return;
}
#endif
//NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed. //NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed.
//TODO: Restart every day or week, in order to not let t0 overflow? //TODO: Restart every day or week, in order to not let t0 overflow?
uint32_t t0 = millis(); uint32_t t0 = millis();
......
...@@ -175,11 +175,17 @@ namespace sensor { ...@@ -175,11 +175,17 @@ namespace sensor {
} }
logToSerial(); logToSerial();
//TODO: Move the 3 back to ampel-firmware.ino and remove headers from co2_sensor.h
csv_writer::logIfTimeHasCome(timestamp, co2, temperature, humidity); csv_writer::logIfTimeHasCome(timestamp, co2, temperature, humidity);
#ifdef MQTT #ifdef MQTT
mqtt::publishIfTimeHasCome(timestamp, co2, temperature, humidity); mqtt::publishIfTimeHasCome(timestamp, co2, temperature, humidity);
#endif #endif
#if defined(LORAWAN) && defined(ESP32)
lorawan::preparePayloadIfTimehasCome();
#endif
} }
if (should_calibrate) { if (should_calibrate) {
......
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
#ifdef MQTT #ifdef MQTT
# include "mqtt.h" # include "mqtt.h"
#endif #endif
#ifdef LORAWAN
# include "lorawan.h"
#endif
namespace config { namespace config {
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor) extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
......
...@@ -104,6 +104,31 @@ ...@@ -104,6 +104,31 @@
# define MQTT_PASSWORD "" # define MQTT_PASSWORD ""
# define MQTT_SERVER_FINGERPRINT "EE BC 4B F8 57 E3 D3 E4 07 54 23 1E F0 C8 A1 56 E0 D3 1A 1C" // SHA1 for test.mosquitto.org # define MQTT_SERVER_FINGERPRINT "EE BC 4B F8 57 E3 D3 E4 07 54 23 1E F0 C8 A1 56 E0 D3 1A 1C" // SHA1 for test.mosquitto.org
/**
* LoRaWAN
*/
// Region and transceiver type should be specified in:
// * Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h for Arduino IDE
// * platformio.ini for PlatformIO
// See https://github.com/mcci-catena/arduino-lmic#configuration for more information
// Should data be sent over LoRaWAN?
// It has been tested with "TTGO ESP32 SX1276 LoRa 868" and will only work with an ESP32 + LoRa modem
# define LORAWAN
// How often should measurements be sent over LoRaWAN?
# define LORAWAN_SENDING_INTERVAL 300 // [s] This value should not be too low. See https://www.thethingsnetwork.org/docs/lorawan/duty-cycle.html#maximum-duty-cycle
// This EUI must be in little-endian format, so least-significant-byte first.
// When copying an EUI from ttnctl output, this means to reverse the bytes.
# define LORAWAN_DEVICE_EUI {0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11}
// This should also be in little endian format, see above.
// For TheThingsNetwork issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
# define LORAWAN_APPLICATION_EUI {0x00, 0x00, 0x00, 0x00, 0x00, 0xD5, 0xB3, 0x70}
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
# define LORAWAN_APPLICATION_KEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
/** /**
* NTP * NTP
*/ */
......
...@@ -43,11 +43,20 @@ namespace LedEffects { ...@@ -43,11 +43,20 @@ namespace LedEffects {
} }
void onBoardLEDOff() { void onBoardLEDOff() {
//NOTE: OFF is LOW on ESP32 and HIGH on ESP8266 :-/
#ifdef ESP8266
digitalWrite(ONBOARD_LED_PIN, HIGH); digitalWrite(ONBOARD_LED_PIN, HIGH);
#else
digitalWrite(ONBOARD_LED_PIN, LOW);
#endif
} }
void onBoardLEDOn() { void onBoardLEDOn() {
#ifdef ESP8266
digitalWrite(ONBOARD_LED_PIN, LOW); digitalWrite(ONBOARD_LED_PIN, LOW);
#else
digitalWrite(ONBOARD_LED_PIN, HIGH);
#endif
} }
void LEDsOff() { void LEDsOff() {
......
#include "lorawan.h"
#if defined(ESP32)
namespace config {
// Values should be defined in config.h
uint16_t lorawan_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
static const u1_t PROGMEM APPEUI[8] = LORAWAN_APPLICATION_EUI;
static const u1_t PROGMEM DEVEUI[8] = LORAWAN_DEVICE_EUI;
static const u1_t PROGMEM APPKEY[16] = LORAWAN_APPLICATION_KEY;
}
// Payloads will be automatically sent via MQTT by TheThingsNetwork, and can be seen with:
// mosquitto_sub -h eu.thethings.network -t '+/devices/+/up' -u 'APPLICATION-NAME' -P 'ttn-account-v2.4xxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx' -v
// or encrypted:
// mosquitto_sub -h eu.thethings.network -t '+/devices/+/up' -u 'APPLICATION-NAME' -P 'ttn-account-v2.4xxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx' -v --cafile mqtt-ca.pem -p 8883
// ->
// co2ampel-test/devices/esp3a7c94/up {"app_id":"co2ampel-test","dev_id":"esp3a7c94","hardware_serial":"00xxxxxxxx","port":1,"counter":5,"payload_raw":"TJd7","payload_fields":{"co2":760,"rh":61.5,"temp":20.2},"metadata":{"time":"2020-12-23T23:00:51.44020438Z","frequency":867.5,"modulation":"LORA","data_rate":"SF7BW125","airtime":51456000,"coding_rate":"4/5","gateways":[{"gtw_id":"eui-xxxxxxxxxxxxxxxxxx","timestamp":1765406908,"time":"2020-12-23T23:00:51.402519Z","channel":5,"rssi":-64,"snr":7.5,"rf_chain":0,"latitude":22.7,"longitude":114.24,"altitude":450}]}}
// More info : https://www.thethingsnetwork.org/docs/applications/mqtt/quick-start.html
void os_getArtEui(u1_t *buf) {
memcpy_P(buf, config::APPEUI, 8);
}
void os_getDevEui(u1_t *buf) {
memcpy_P(buf, config::DEVEUI, 8);
}
void os_getDevKey(u1_t *buf) {
memcpy_P(buf, config::APPKEY, 16);
}
namespace lorawan {
bool waiting_for_confirmation = false;
bool connected = false;
String last_transmission = "";
void printHex2(unsigned v) {
v &= 0xff;
if (v < 16)
Serial.print('0');
Serial.print(v, HEX);
}
void onEvent(ev_t ev) {
Serial.print("LoRa - ");
Serial.print(ntp::getLocalTime());
Serial.print(" - ");
switch (ev) {
case EV_JOINING:
Serial.println(F("EV_JOINING"));
break;
case EV_JOINED:
waiting_for_confirmation = false;
connected = true;
LedEffects::onBoardLEDOff();
Serial.println(F("EV_JOINED"));
{
u4_t netid = 0;
devaddr_t devaddr = 0;
u1_t nwkKey[16];
u1_t artKey[16];
LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey);
Serial.print(F("netid: "));
Serial.println(netid, DEC);
Serial.print(F("devaddr: "));
Serial.println(devaddr, HEX);
Serial.print(F("AppSKey: "));
for (size_t i = 0; i < sizeof(artKey); ++i) {
if (i != 0)
Serial.print("-");
printHex2(artKey[i]);
}
Serial.println("");
Serial.print("NwkSKey: ");
for (size_t i = 0; i < sizeof(nwkKey); ++i) {
if (i != 0)
Serial.print("-");
printHex2(nwkKey[i]);
}
Serial.println();
}
Serial.println(F("Other services may resume, and will not be frozen anymore."));
// Disable link check validation (automatically enabled
// during join, but because slow data rates change max TX
// size, we don't use it in this example.
LMIC_setLinkCheckMode(0);
break;
case EV_JOIN_FAILED:
Serial.println(F("EV_JOIN_FAILED"));
break;
case EV_REJOIN_FAILED:
Serial.println(F("EV_REJOIN_FAILED"));
break;
case EV_TXCOMPLETE:
last_transmission = ntp::getLocalTime();
Serial.println(F("EV_TXCOMPLETE"));
break;
case EV_TXSTART:
waiting_for_confirmation = !connected;
Serial.println(F("EV_TXSTART"));
break;
case EV_TXCANCELED:
waiting_for_confirmation = false;
LedEffects::onBoardLEDOff();
Serial.println(F("EV_TXCANCELED"));
break;
case EV_JOIN_TXCOMPLETE:
waiting_for_confirmation = false;
LedEffects::onBoardLEDOff();
Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept."));
Serial.println(F("Other services may resume."));
break;
default:
Serial.print(F("LoRa event: "));
Serial.println((unsigned) ev);
break;
}
if (waiting_for_confirmation) {
LedEffects::onBoardLEDOn();
Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
}
}
void preparePayload() {
// Check if there is not a current TX/RX job running
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, not sending"));
} else {
uint8_t buff[3];
// Mapping CO2 from 0ppm to 5100ppm to [0, 255], with 20ppm increments.
buff[0] = (util::min(util::max(sensor::co2, 0), 5100) + 10) / 20;
// Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
buff[1] = static_cast<uint8_t>((util::min(util::max(sensor::temperature, -10), 41) + 10.1f) * 5);
// Mapping humidity from [0%, 100%] to [0, 200], with 0.5°C increment (0.4°C would also be possible)
buff[2] = static_cast<uint8_t>(util::min(util::max(sensor::humidity, 0) + 0.25f, 100) * 2);
Serial.print(F("LoRa - Payload : '"));
printHex2(buff[0]);
Serial.print(" ");
printHex2(buff[1]);
Serial.print(" ");
printHex2(buff[2]);
Serial.print(F("', "));
Serial.print(buff[0] * 20);
Serial.print(F(" ppm, "));
Serial.print(buff[1] * 0.2 - 10);
Serial.print(F(" °C, "));
Serial.print(buff[2] * 0.5);
Serial.println(F(" %."));
// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, buff, sizeof(buff), 0);
//NOTE: To decode in TheThingsNetwork:
//function Decoder(bytes, port) {
// return {
// co2: bytes[0] * 20,
// temp: bytes[1] / 5.0 - 10,
// rh: bytes[2] / 2.0
// };
//}
}
}
void initialize() {
Serial.println(F("Starting LoRaWAN."));
// More info about pin mapping : https://github.com/mcci-catena/arduino-lmic#pin-mapping
// Has been tested successfully with ESP32 TTGO LoRa32 V1, and might work with other ESP32+LoRa boards.
const lmic_pinmap *pPinMap = Arduino_LMIC::GetPinmap_ThisBoard();
// LMIC init.
os_init_ex(pPinMap);
// Reset the MAC state. Session and pending data transfers will be discarded.
LMIC_reset();
// Join, but don't send anything yet.
LMIC_startJoining();
}
// Checks if OTAA is connected, or if payload should be sent.
// NOTE: while a transaction is in process (i.e. until the TXcomplete event has been received, no blocking code (e.g. delay loops etc.) are allowed, otherwise the LMIC/OS code might miss the event.
// If this rule is not followed, a typical symptom is, that the first send is ok and all following ones end with the TX not complete failure.
void process() {
os_runloop_once();
}
void preparePayloadIfTimehasCome() {
static unsigned long last_sent_at = 0;
unsigned long now = seconds();
if (now - last_sent_at > config::lorawan_sending_interval) {
last_sent_at = now;
preparePayload();
}
}
}
void onEvent(ev_t ev) {
lorawan::onEvent(ev);
}
#endif
#ifndef AMPEL_LORAWAN_H_
#define AMPEL_LORAWAN_H_
#if defined(ESP32)
#include <Arduino.h>
// Requires "MCCI LoRaWAN LMIC library", which will be automatically used with PlatformIO but should be added in "Arduino IDE".
// Tested successfully with v3.2.0 and connected to a thethingsnetwork.org app.
#include <lmic.h>
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
#include <SPI.h>
typedef uint8_t u1_t;
#include "co2_sensor.h"
#include "led_effects.h"
#include "config.h"
#include "util.h"
namespace config {
extern uint16_t lorawan_sending_interval; // [s]
}
namespace lorawan {
extern bool waiting_for_confirmation;
extern bool connected;
extern String last_transmission;
void initialize();
void process();
void preparePayloadIfTimehasCome();
}
#endif
#endif
...@@ -46,10 +46,10 @@ namespace mqtt { ...@@ -46,10 +46,10 @@ namespace mqtt {
snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity); snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity);
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da' // Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
if (mqttClient.publish(publish_topic.c_str(), payload)) { if (mqttClient.publish(publish_topic.c_str(), payload)) {
Serial.println("OK"); Serial.println(F("OK"));
last_successful_publish = ntp::getLocalTime(); last_successful_publish = ntp::getLocalTime();
} else { } else {
Serial.println("Failed."); Serial.println(F("Failed."));
} }
LedEffects::onBoardLEDOff(); LedEffects::onBoardLEDOff();
} }
......
...@@ -21,3 +21,11 @@ platform = espressif32 ...@@ -21,3 +21,11 @@ platform = espressif32
board = ttgo-lora32-v1 board = ttgo-lora32-v1
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
lib_deps =
MCCI LoRaWAN LMIC library
build_flags =
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-D CFG_eu868=1
-D CFG_sx1276_radio=1
...@@ -24,6 +24,18 @@ namespace ntp { ...@@ -24,6 +24,18 @@ namespace ntp {
String getLocalTime(); String getLocalTime();
} }
namespace util {
template<typename Tpa, typename Tpb>
inline auto min(const Tpa &a, const Tpb &b) -> decltype(a < b ? a : b) {
return b < a ? b : a;
}
template<typename Tpa, typename Tpb>
inline auto max(const Tpa &a, const Tpb &b) -> decltype(b > a ? b : a) {
return b > a ? b : a;
}
}
#define seconds() (millis() / 1000UL) #define seconds() (millis() / 1000UL)
extern uint32_t max_loop_duration; extern uint32_t max_loop_duration;
const extern String SENSOR_ID; const extern String SENSOR_ID;
......
...@@ -87,6 +87,11 @@ namespace web_server { ...@@ -87,6 +87,11 @@ namespace web_server {
#ifdef MQTT #ifdef MQTT
"<tr><td>Last MQTT publish</td><td>%s</td></tr>\n" "<tr><td>Last MQTT publish</td><td>%s</td></tr>\n"
"<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n" "<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n"
#endif
#if defined(LORAWAN) && defined(ESP32)
"<tr><td>Connected to LoRaWAN?</td><td>%s</td></tr>\n"
"<tr><td>Last LoRaWAN transmission</td><td>%s</td></tr>\n"
"<tr><td>LoRaWAN publish timestep</td><td>%5d s</td></tr>\n"
#endif #endif
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor? "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n" "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
...@@ -196,6 +201,9 @@ namespace web_server { ...@@ -196,6 +201,9 @@ namespace web_server {
csv_writer::last_successful_write.c_str(), config::csv_interval, csv_writer::last_successful_write.c_str(), config::csv_interval,
#ifdef MQTT #ifdef MQTT
mqtt::last_successful_publish.c_str(), config::sending_interval, mqtt::last_successful_publish.c_str(), config::sending_interval,
#endif
#if defined(LORAWAN) && defined(ESP32)
lorawan::connected ? "Yes" : "No", lorawan::last_transmission.c_str(), config::lorawan_sending_interval,
#endif #endif
config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(), config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(),
WiFi.localIP().toString().c_str(), get_free_heap_size(), available_fs_space, max_loop_duration, BOARD, hh, mm, WiFi.localIP().toString().c_str(), get_free_heap_size(), available_fs_space, max_loop_duration, BOARD, hh, mm,
......
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
#ifdef MQTT #ifdef MQTT
# include "mqtt.h" # include "mqtt.h"
#endif #endif
#ifdef LORAWAN
# include "lorawan.h"
#endif
namespace web_server { namespace web_server {
void initialize(); void initialize();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment