lorawan.cpp 10.7 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
Note    
Eric Duminil committed
107
108
109
110
111
112
113
114
        /**
         * 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.
         */
Eric Duminil's avatar
Eric Duminil committed
115
116
117
118
119
120
        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
121

Eric Duminil's avatar
Eric Duminil committed
122
        Serial.print(F("  netid: "));
123
        Serial.println(netid, DEC);
Eric Duminil's avatar
Eric Duminil committed
124
        Serial.print(F("  devaddr: "));
125
        Serial.println(devaddr, HEX);
Eric Duminil's avatar
Eric Duminil committed
126
        Serial.print(F("  AppSKey: "));
127
128
129
130
131
        for (size_t i = 0; i < sizeof(artKey); ++i) {
          if (i != 0)
            Serial.print("-");
          printHex2(artKey[i]);
        }
Eric Duminil's avatar
Eric Duminil committed
132
        Serial.println();
133
        Serial.print(F("  NwkSKey: "));
134
135
136
137
138
139
140
141
        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
142
      // Disable link check validation (automatically enabled during join)
143
144
145
146
147
148
149
150
151
      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:
152
      ntp::getLocalTime(last_transmission);
Eric Duminil's avatar
Eric Duminil committed
153
      Serial.println(F("EV_TXCOMPLETE"));
154
155
      break;
    case EV_TXSTART:
Eric Duminil's avatar
Eric Duminil committed
156
      waiting_for_confirmation = !connected;
157
158
159
160
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      waiting_for_confirmation = false;
161
      led_effects::onBoardLEDOff();
162
163
164
165
      Serial.println(F("EV_TXCANCELED"));
      break;
    case EV_JOIN_TXCOMPLETE:
      waiting_for_confirmation = false;
166
      led_effects::onBoardLEDOff();
167
168
169
170
171
172
173
174
175
      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) {
176
      led_effects::onBoardLEDOn();
177
178
179
180
      Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
    }
  }

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

194
      Serial.print(F("LoRa - Payload : '"));
195
      printHex2(buff[0]);
196
      Serial.print(" ");
197
      printHex2(buff[1]);
198
      Serial.print(" ");
199
200
201
202
203
204
205
206
207
208
209
      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
210

211
      //NOTE: To decode in TheThingsNetwork:
Eric Duminil's avatar
Eric Duminil committed
212
213
214
215
216
217
218
219
220
221
222
      //        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: []
      //          };
      //        }
223
224
225
    }
  }

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

  /*****************************************************************
   * 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
243
    config::save();
Eric Duminil's avatar
Eric Duminil committed
244
245
    led_effects::showKITTWheel(color::green, 1);
  }
246
247
248
249
250
}

void onEvent(ev_t ev) {
  lorawan::onEvent(ev);
}
Eric Duminil's avatar
Doc    
Eric Duminil committed
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
292
293
294
295
296
297
298
299

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

300
#endif