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

3
#if defined(AMPEL_LORAWAN) && defined(ESP32)
4

5
6
7
#include "led_effects.h"
#include "sensor_console.h"
#include "util.h"
8
#include "time_util.h"
Eric Duminil's avatar
Eric Duminil committed
9

Eric Duminil's avatar
Note    
Eric Duminil committed
10
/*** Define region and transceiver type, and ignore lmic_project_config.h from lmic library ***/
11
12
13
14
15
16
17
18
19
// 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>
20
#include <SPI.h>
Eric Duminil's avatar
Eric Duminil committed
21
22
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
23

24
namespace config {
25
#if defined(CFG_eu868)
26
  const char *lorawan_frequency_plan = "Europe 868";
27
#elif defined(CFG_us915)
28
  const char *lorawan_frequency_plan = "US 915";
29
#elif defined(CFG_au915)
30
  const char *lorawan_frequency_plan = "Australia 915";
31
#elif defined(CFG_as923)
32
  const char *lorawan_frequency_plan = "Asia 923";
33
#elif defined(CFG_kr920)
34
  const char *lorawan_frequency_plan = "Korea 920";
35
#elif defined(CFG_in866)
36
  const char *lorawan_frequency_plan = "India 866";
37
38
39
#else
#  error "Region should be specified"
#endif
40
  // Values should be defined in config.h
Eric Duminil's avatar
Eric Duminil committed
41
  uint16_t lorawan_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
42
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

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

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

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

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

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

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

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

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

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