#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((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(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