lorawan.cpp 9.19 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 "config.h" //TODO: Replace with just web_config
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

Eric Duminil's avatar
Note    
Eric Duminil committed
11
/*** Define region and transceiver type, and ignore lmic_project_config.h from lmic library ***/
12
13
14
15
16
17
18
19
20
// Those values are probably okay if you're in Europe.
#define ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
#define CFG_eu868 1
#define CFG_sx1276_radio 1
/****************************************************************************************/

// 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>
21
#include <SPI.h>
Eric Duminil's avatar
Eric Duminil committed
22
23
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
24

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

  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
71
  bool connected = false;
72
  char last_transmission[23] = "";
73

Eric Duminil's avatar
Eric Duminil committed
74
  void initialize() {
75
76
77
    Serial.print(F("Starting LoRaWAN. Frequency plan : "));
    Serial.print(config::lorawan_frequency_plan);
    Serial.println(F(" MHz."));
Eric Duminil's avatar
Eric Duminil committed
78
79
80
81
82
83
84
85
86
87

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

  // Checks if OTAA is connected, or if payload should be sent.
92
  // 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
93
  // 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
94
95
96
97
  void process() {
    os_runloop_once();
  }

98
99
100
101
102
103
104
105
  void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
      Serial.print('0');
    Serial.print(v, HEX);
  }

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

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

198
      Serial.print(F("LoRa - Payload : '"));
199
      printHex2(buff[0]);
200
      Serial.print(" ");
201
      printHex2(buff[1]);
202
      Serial.print(" ");
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
      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
225
  void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
226
227
    static unsigned long last_sent_at = 0;
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
228
    if (connected && (now - last_sent_at > config::lorawan_sending_interval)) {
229
      last_sent_at = now;
Eric Duminil's avatar
Eric Duminil committed
230
      preparePayload(co2, temperature, humidity);
231
232
    }
  }
Eric Duminil's avatar
Eric Duminil committed
233
234
235
236
237
238
239
240
241
242
243

  /*****************************************************************
   * 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);
  }
244
245
246
247
248
}

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