lorawan.cpp 9.87 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
62

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

  // Checks if OTAA is connected, or if payload should be sent.
67
  // 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
68
  // 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
69
70
71
72
  void process() {
    os_runloop_once();
  }

73
74
75
76
77
78
79
80
  void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
      Serial.print('0');
    Serial.print(v, HEX);
  }

  void onEvent(ev_t ev) {
81
82
    char current_time[23];
    ntp::getLocalTime(current_time);
Eric Duminil's avatar
Eric Duminil committed
83
    Serial.print(F("LoRa - "));
84
    Serial.print(current_time);
Eric Duminil's avatar
Eric Duminil committed
85
    Serial.print(F(" - "));
86
87
88
89
90
91
    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
92
      connected = true;
93
      led_effects::onBoardLEDOff();
94
95
96
97
98
99
100
      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
101
        //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
102
        Serial.print(F("  netid: "));
103
        Serial.println(netid, DEC);
Eric Duminil's avatar
Eric Duminil committed
104
        Serial.print(F("  devaddr: "));
105
        Serial.println(devaddr, HEX);
Eric Duminil's avatar
Eric Duminil committed
106
        Serial.print(F("  AppSKey: "));
107
108
109
110
111
        for (size_t i = 0; i < sizeof(artKey); ++i) {
          if (i != 0)
            Serial.print("-");
          printHex2(artKey[i]);
        }
Eric Duminil's avatar
Eric Duminil committed
112
        Serial.println();
113
        Serial.print(F("  NwkSKey: "));
114
115
116
117
118
119
120
121
        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
122
      // Disable link check validation (automatically enabled during join)
123
124
125
126
127
128
129
130
131
      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:
132
      ntp::getLocalTime(last_transmission);
Eric Duminil's avatar
Eric Duminil committed
133
      Serial.println(F("EV_TXCOMPLETE"));
134
135
      break;
    case EV_TXSTART:
Eric Duminil's avatar
Eric Duminil committed
136
      waiting_for_confirmation = !connected;
137
138
139
140
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      waiting_for_confirmation = false;
141
      led_effects::onBoardLEDOff();
142
143
144
145
      Serial.println(F("EV_TXCANCELED"));
      break;
    case EV_JOIN_TXCOMPLETE:
      waiting_for_confirmation = false;
146
      led_effects::onBoardLEDOff();
147
148
149
150
151
152
153
154
155
      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) {
156
      led_effects::onBoardLEDOn();
157
158
159
160
      Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
    }
  }

Eric Duminil's avatar
Eric Duminil committed
161
  void preparePayload(int16_t co2, float temperature, float humidity) {
162
163
164
165
166
167
    // 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
168
      buff[0] = (util::min(util::max(co2, 0), 5100) + 10) / 20;
169
      // Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
Eric Duminil's avatar
Eric Duminil committed
170
      buff[1] = static_cast<uint8_t>((util::min(util::max(temperature, -10), 41) + 10.1f) * 5);
171
      // 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
172
      buff[2] = static_cast<uint8_t>(util::min(util::max(humidity, 0) + 0.25f, 100) * 2);
173

174
      Serial.print(F("LoRa - Payload : '"));
175
      printHex2(buff[0]);
176
      Serial.print(" ");
177
      printHex2(buff[1]);
178
      Serial.print(" ");
179
180
181
182
183
184
185
186
187
188
189
      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
190

191
      //NOTE: To decode in TheThingsNetwork:
Eric Duminil's avatar
Eric Duminil committed
192
193
194
195
196
197
198
199
200
201
202
      //        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: []
      //          };
      //        }
203
204
205
    }
  }

Eric Duminil's avatar
Eric Duminil committed
206
  void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
207
208
    static unsigned long last_sent_at = 0;
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
209
    if (connected && (now - last_sent_at > config::lorawan_sending_interval)) {
210
      last_sent_at = now;
Eric Duminil's avatar
Eric Duminil committed
211
      preparePayload(co2, temperature, humidity);
212
213
    }
  }
Eric Duminil's avatar
Eric Duminil committed
214
215
216
217
218
219
220
221
222
223
224

  /*****************************************************************
   * 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);
  }
225
226
227
228
229
}

void onEvent(ev_t ev) {
  lorawan::onEvent(ev);
}
Eric Duminil's avatar
Doc    
Eric Duminil committed
230
231
232
233
234
235
236
237
238
239
240
241
242
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

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

279
#endif