lorawan.cpp 10.4 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"
6
7
8
#include "led_effects.h"
#include "sensor_console.h"
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
9
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
10

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

18
namespace config {
19
#if defined(CFG_eu868)
20
  const char *lorawan_frequency_plan = "Europe 868";
21
#elif defined(CFG_us915)
22
  const char *lorawan_frequency_plan = "US 915";
23
#elif defined(CFG_au915)
24
  const char *lorawan_frequency_plan = "Australia 915";
25
#elif defined(CFG_as923)
26
  const char *lorawan_frequency_plan = "Asia 923";
27
#elif defined(CFG_kr920)
28
  const char *lorawan_frequency_plan = "Korea 920";
29
#elif defined(CFG_in866)
30
  const char *lorawan_frequency_plan = "India 866";
31
32
33
#else
#  error "Region should be specified"
#endif
34
35
36
37
38
39
40
41
42
43
44
45
}

// 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;
Eric Duminil's avatar
Eric Duminil committed
46
  bool connected = false;
47
  char last_transmission[23] = "";
48

Eric Duminil's avatar
Eric Duminil committed
49
  void initialize() {
50
51
52
    Serial.print(F("Starting LoRaWAN. Frequency plan : "));
    Serial.print(config::lorawan_frequency_plan);
    Serial.println(F(" MHz."));
Eric Duminil's avatar
Eric Duminil committed
53
54
55
56
57
58
59
60
61

    // 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.
Eric Duminil's avatar
Eric Duminil committed
62
63
64
65
66
67
68
    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();
    }
69
    sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
Eric Duminil's avatar
Eric Duminil committed
70
71
72
  }

  // Checks if OTAA is connected, or if payload should be sent.
73
  // 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
74
  // 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
75
76
77
78
  void process() {
    os_runloop_once();
  }

79
80
81
82
83
84
85
86
  void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
      Serial.print('0');
    Serial.print(v, HEX);
  }

  void onEvent(ev_t ev) {
87
88
    char current_time[23];
    ntp::getLocalTime(current_time);
Eric Duminil's avatar
Eric Duminil committed
89
    Serial.print(F("LoRa - "));
90
    Serial.print(current_time);
Eric Duminil's avatar
Eric Duminil committed
91
    Serial.print(F(" - "));
92
93
94
95
96
97
    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
98
      connected = true;
99
      led_effects::onBoardLEDOff();
100
101
102
103
104
105
106
      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
107
108
109
110
111
112
        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();
Eric Duminil's avatar
Note    
Eric Duminil committed
113
        //NOTE: Saving session to EEPROM seems like a good idea at first, but unfortunately: too much info is needed, and a counter would need to be save every single time data is sent.
Eric Duminil's avatar
Eric Duminil committed
114
        Serial.print(F("  netid: "));
115
        Serial.println(netid, DEC);
Eric Duminil's avatar
Eric Duminil committed
116
        Serial.print(F("  devaddr: "));
117
        Serial.println(devaddr, HEX);
Eric Duminil's avatar
Eric Duminil committed
118
        Serial.print(F("  AppSKey: "));
119
120
121
122
123
        for (size_t i = 0; i < sizeof(artKey); ++i) {
          if (i != 0)
            Serial.print("-");
          printHex2(artKey[i]);
        }
Eric Duminil's avatar
Eric Duminil committed
124
        Serial.println();
125
        Serial.print(F("  NwkSKey: "));
126
127
128
129
130
131
132
133
        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
134
      // Disable link check validation (automatically enabled during join)
135
136
137
138
139
140
141
142
143
      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:
144
      ntp::getLocalTime(last_transmission);
Eric Duminil's avatar
Eric Duminil committed
145
      Serial.println(F("EV_TXCOMPLETE"));
146
147
      break;
    case EV_TXSTART:
Eric Duminil's avatar
Eric Duminil committed
148
      waiting_for_confirmation = !connected;
149
150
151
152
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      waiting_for_confirmation = false;
153
      led_effects::onBoardLEDOff();
154
155
156
157
      Serial.println(F("EV_TXCANCELED"));
      break;
    case EV_JOIN_TXCOMPLETE:
      waiting_for_confirmation = false;
158
      led_effects::onBoardLEDOff();
159
160
161
162
163
164
165
166
167
      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) {
168
      led_effects::onBoardLEDOn();
169
170
171
172
      Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
    }
  }

Eric Duminil's avatar
Eric Duminil committed
173
  void preparePayload(int16_t co2, float temperature, float humidity) {
174
175
176
177
178
179
    // 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
180
      buff[0] = (util::min(util::max(co2, 0), 5100) + 10) / 20;
181
      // Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
Eric Duminil's avatar
Eric Duminil committed
182
      buff[1] = static_cast<uint8_t>((util::min(util::max(temperature, -10), 41) + 10.1f) * 5);
183
      // 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
184
      buff[2] = static_cast<uint8_t>(util::min(util::max(humidity, 0) + 0.25f, 100) * 2);
185

186
      Serial.print(F("LoRa - Payload : '"));
187
      printHex2(buff[0]);
188
      Serial.print(" ");
189
      printHex2(buff[1]);
190
      Serial.print(" ");
191
192
193
194
195
196
197
198
199
200
201
      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);
Eric Duminil's avatar
Eric Duminil committed
202

203
      //NOTE: To decode in TheThingsNetwork:
Eric Duminil's avatar
Eric Duminil committed
204
205
206
207
208
209
210
211
212
213
214
      //        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: []
      //          };
      //        }
215
216
217
    }
  }

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

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

void onEvent(ev_t ev) {
  lorawan::onEvent(ev);
}
Eric Duminil's avatar
Doc    
Eric Duminil committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

// '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);
}

292
#endif