software/ampel-firmware/lorawan.cpp
2021-11-15 13:45:36 +01:00

212 lines
7.9 KiB
C++

#include "lorawan.h"
#if defined(AMPEL_LORAWAN) && 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;
char last_transmission[23] = "";
void initialize() {
Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
// 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();
sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
}
// 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 printHex2(unsigned v) {
v &= 0xff;
if (v < 16)
Serial.print('0');
Serial.print(v, HEX);
}
void onEvent(ev_t ev) {
char current_time[23];
ntp::getLocalTime(current_time);
Serial.print("LoRa - ");
Serial.print(current_time);
Serial.print(" - ");
switch (ev) {
case EV_JOINING:
Serial.println(F("EV_JOINING"));
break;
case EV_JOINED:
waiting_for_confirmation = false;
connected = true;
led_effects::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(F(" 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)
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:
ntp::getLocalTime(last_transmission);
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;
led_effects::onBoardLEDOff();
Serial.println(F("EV_TXCANCELED"));
break;
case EV_JOIN_TXCOMPLETE:
waiting_for_confirmation = false;
led_effects::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) {
led_effects::onBoardLEDOn();
Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
}
}
void preparePayload(int16_t co2, float temperature, float humidity) {
// 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(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(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(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 preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
static unsigned long last_sent_at = 0;
unsigned long now = seconds();
if (connected && (now - last_sent_at > config::lorawan_sending_interval)) {
last_sent_at = now;
preparePayload(co2, temperature, humidity);
}
}
/*****************************************************************
* Callbacks for sensor commands *
*****************************************************************/
void setLoRaInterval(int32_t sending_interval) {
config::lorawan_sending_interval = sending_interval;
Serial.print(F("Setting LoRa sending interval to : "));
Serial.print(config::lorawan_sending_interval);
Serial.println("s.");
led_effects::showKITTWheel(color::green, 1);
}
}
void onEvent(ev_t ev) {
lorawan::onEvent(ev);
}
#endif