lorawan.cpp 8.94 KB
Newer Older
1
#include "lorawan.h"
2

Eric Duminil's avatar
Eric Duminil committed
3
#if defined(ESP32)
4

Eric Duminil's avatar
Eric Duminil committed
5
#include "web_config.h"
Eric Duminil's avatar
Eric Duminil committed
6
#include "config.h" //TODO: Replace with just web_config
7
8
9
#include "led_effects.h"
#include "sensor_console.h"
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
10
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
11

12
13
14
// 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>
15
#include <SPI.h>
Eric Duminil's avatar
Eric Duminil committed
16
17
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
18

19
namespace config {
20
#if defined(CFG_eu868)
21
  const char *lorawan_frequency_plan = "Europe 868";
22
#elif defined(CFG_us915)
23
  const char *lorawan_frequency_plan = "US 915";
24
#elif defined(CFG_au915)
25
  const char *lorawan_frequency_plan = "Australia 915";
26
#elif defined(CFG_as923)
27
  const char *lorawan_frequency_plan = "Asia 923";
28
#elif defined(CFG_kr920)
29
  const char *lorawan_frequency_plan = "Korea 920";
30
#elif defined(CFG_in866)
31
  const char *lorawan_frequency_plan = "India 866";
32
33
34
#else
#  error "Region should be specified"
#endif
35
  // Values should be defined in config.h
Eric Duminil's avatar
Eric Duminil committed
36
  uint16_t lorawan_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

  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;
Eric Duminil's avatar
Eric Duminil committed
65
  bool connected = false;
66
  char last_transmission[23] = "";
67

Eric Duminil's avatar
Eric Duminil committed
68
  void initialize() {
69
70
71
    Serial.print(F("Starting LoRaWAN. Frequency plan : "));
    Serial.print(config::lorawan_frequency_plan);
    Serial.println(F(" MHz."));
Eric Duminil's avatar
Eric Duminil committed
72
73
74
75
76
77
78
79
80
81

    // 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();
82
    sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
Eric Duminil's avatar
Eric Duminil committed
83
84
85
  }

  // Checks if OTAA is connected, or if payload should be sent.
86
  // 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.
Eric Duminil's avatar
Eric Duminil committed
87
  // 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.
Eric Duminil's avatar
Eric Duminil committed
88
89
90
91
  void process() {
    os_runloop_once();
  }

92
93
94
95
96
97
98
99
  void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
      Serial.print('0');
    Serial.print(v, HEX);
  }

  void onEvent(ev_t ev) {
100
101
    char current_time[23];
    ntp::getLocalTime(current_time);
102
    Serial.print("LoRa - ");
103
    Serial.print(current_time);
104
105
106
107
108
109
110
    Serial.print(" - ");
    switch (ev) {
    case EV_JOINING:
      Serial.println(F("EV_JOINING"));
      break;
    case EV_JOINED:
      waiting_for_confirmation = false;
Eric Duminil's avatar
Eric Duminil committed
111
      connected = true;
112
      led_effects::onBoardLEDOff();
113
114
115
116
117
118
119
      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);
Eric Duminil's avatar
Eric Duminil committed
120
        //TODO: Save session keys to EEPROM?
Eric Duminil's avatar
Eric Duminil committed
121
        Serial.print(F("  netid: "));
122
        Serial.println(netid, DEC);
Eric Duminil's avatar
Eric Duminil committed
123
        Serial.print(F("  devaddr: "));
124
        Serial.println(devaddr, HEX);
Eric Duminil's avatar
Eric Duminil committed
125
        Serial.print(F("  AppSKey: "));
126
127
128
129
130
        for (size_t i = 0; i < sizeof(artKey); ++i) {
          if (i != 0)
            Serial.print("-");
          printHex2(artKey[i]);
        }
Eric Duminil's avatar
Eric Duminil committed
131
        Serial.println();
132
        Serial.print(F("  NwkSKey: "));
133
134
135
136
137
138
139
140
        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."));
Eric Duminil's avatar
Note    
Eric Duminil committed
141
      // Disable link check validation (automatically enabled during join)
142
143
144
145
146
147
148
149
150
      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:
151
      ntp::getLocalTime(last_transmission);
Eric Duminil's avatar
Eric Duminil committed
152
      Serial.println(F("EV_TXCOMPLETE"));
153
154
      break;
    case EV_TXSTART:
Eric Duminil's avatar
Eric Duminil committed
155
      waiting_for_confirmation = !connected;
156
157
158
159
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      waiting_for_confirmation = false;
160
      led_effects::onBoardLEDOff();
161
162
163
164
      Serial.println(F("EV_TXCANCELED"));
      break;
    case EV_JOIN_TXCOMPLETE:
      waiting_for_confirmation = false;
165
      led_effects::onBoardLEDOff();
166
167
168
169
170
171
172
173
174
      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) {
175
      led_effects::onBoardLEDOn();
176
177
178
179
      Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
    }
  }

Eric Duminil's avatar
Eric Duminil committed
180
  void preparePayload(int16_t co2, float temperature, float humidity) {
181
182
183
184
185
186
    // 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.
Eric Duminil's avatar
Eric Duminil committed
187
      buff[0] = (util::min(util::max(co2, 0), 5100) + 10) / 20;
188
      // Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
Eric Duminil's avatar
Eric Duminil committed
189
      buff[1] = static_cast<uint8_t>((util::min(util::max(temperature, -10), 41) + 10.1f) * 5);
190
      // Mapping humidity from [0%, 100%] to [0, 200], with 0.5°C increment (0.4°C would also be possible)
Eric Duminil's avatar
Eric Duminil committed
191
      buff[2] = static_cast<uint8_t>(util::min(util::max(humidity, 0) + 0.25f, 100) * 2);
192

193
      Serial.print(F("LoRa - Payload : '"));
194
      printHex2(buff[0]);
195
      Serial.print(" ");
196
      printHex2(buff[1]);
197
      Serial.print(" ");
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
      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
      //  };
      //}
    }
  }

Eric Duminil's avatar
Eric Duminil committed
220
  void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
221
222
    static unsigned long last_sent_at = 0;
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
223
    if (connected && (now - last_sent_at > config::lorawan_sending_interval)) {
224
      last_sent_at = now;
Eric Duminil's avatar
Eric Duminil committed
225
      preparePayload(co2, temperature, humidity);
226
227
    }
  }
Eric Duminil's avatar
Eric Duminil committed
228
229
230
231
232
233
234
235
236

  /*****************************************************************
   * 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.");
Eric Duminil's avatar
Eric Duminil committed
237
    config::save();
Eric Duminil's avatar
Eric Duminil committed
238
239
    led_effects::showKITTWheel(color::green, 1);
  }
240
241
242
243
244
}

void onEvent(ev_t ev) {
  lorawan::onEvent(ev);
}
245
#endif