#include "lorawan.h" #if defined(ESP32) #include "web_config.h" #include "led_effects.h" #include "sensor_console.h" #include "util.h" #include "ntp.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 #include #include #include namespace config { #if defined(CFG_eu868) const char *lorawan_frequency_plan = "Europe 868"; #elif defined(CFG_us915) const char *lorawan_frequency_plan = "US 915"; #elif defined(CFG_au915) const char *lorawan_frequency_plan = "Australia 915"; #elif defined(CFG_as923) const char *lorawan_frequency_plan = "Asia 923"; #elif defined(CFG_kr920) const char *lorawan_frequency_plan = "Korea 920"; #elif defined(CFG_in866) const char *lorawan_frequency_plan = "India 866"; #else # error "Region should be specified" #endif } // 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 namespace lorawan { bool waiting_for_confirmation = false; bool connected = false; char last_transmission[23] = ""; void initialize() { Serial.print(F("Starting LoRaWAN. Frequency plan : ")); Serial.print(config::lorawan_frequency_plan); Serial.println(F(" 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. if (config::lora_session_saved) { LMIC_setSession(config::lora_netid, config::lora_devaddr, (unsigned char*) config::lorawan_nwk_key, (unsigned char*) config::lorawan_art_key); connected = true; // Well, hopefully. } else { 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(F("LoRa - ")); Serial.print(current_time); Serial.print(F(" - ")); 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); /** * NOTE: This is experimental. Saving session to EEPROM seems like a good idea at first, * to avoid joining every time the Ampel is started. * But many other infos are needed too (e.g. a counter), and it wouldn't be wise * to save the whole configuration every time data is sent via EEPROM. * * config::lora_session_saved can be set to false, and the Ampel will try to join again next time. */ config::lora_session_saved = true; config::lora_netid = netid; config::lora_devaddr = devaddr; memcpy(config::lorawan_nwk_key, nwkKey, 16); memcpy(config::lorawan_art_key, artKey, 16); config::save(); 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 decodeUplink(input) { // return { // data: { // co2: input.bytes[0] * 20, // temp: input.bytes[1] / 5.0 - 10, // rh: input.bytes[2] / 2.0 // }, // warnings: [], // errors: [] // }; // } } } 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."); config::save(); led_effects::showKITTWheel(color::green, 1); } } void onEvent(ev_t ev) { lorawan::onEvent(ev); } // 'A' -> 10, 'F' -> 15, 'f' -> 15, 'z' -> -1 int8_t hexCharToInt(char c) { int8_t v = -1; if ((c >= '0') && (c <= '9')) { v = (c - '0'); } else if ((c >= 'A') && (c <= 'F')) { v = (c - 'A' + 10); } else if ((c >= 'a') && (c <= 'f')) { v = (c - 'a' + 10); } return v; } /** * Parses hex string and saves the corresponding bytes in buf. * msb is true for most-significant-byte, false for least-significant-byte. * * "112233" will be loaded into {0x11, 0x22, 0x33} in MSB, {0x33, 0x22, 0x11} in LSB. */ void hexStringToByteArray(uint8_t *buf, const char *hex, uint max_n, bool msb) { int n = util::min(strlen(hex) / 2, max_n); for (int i = 0; i < n; i++) { int j; if (msb) { j = i; } else { j = n - 1 - i; } uint8_t r = hexCharToInt(hex[j * 2]) * 16 + hexCharToInt(hex[j * 2 + 1]); buf[i] = r; } } // Load config into LMIC byte arrays. void os_getArtEui(u1_t *buf) { hexStringToByteArray(buf, config::lorawan_app_eui, 8, false); } void os_getDevEui(u1_t *buf) { hexStringToByteArray(buf, config::lorawan_device_eui, 8, false); } void os_getDevKey(u1_t *buf) { hexStringToByteArray(buf, config::lorawan_app_key, 16, true); } #endif