lorawan.cpp 9.06 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"
Eric Duminil's avatar
Eric Duminil committed
8

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

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Check that a Region was defined
#if defined(CFG_eu868)
#  define LMIC_FREQUENCY_PLAN "Europe 868"
#elif defined(CFG_us915)
#  define LMIC_FREQUENCY_PLAN "US 915"
#elif defined(CFG_au915)
#  define LMIC_FREQUENCY_PLAN "Australia 915"
#elif defined(CFG_as923)
#  define LMIC_FREQUENCY_PLAN "Asia 923"
#elif defined(CFG_kr920)
#  define LMIC_FREQUENCY_PLAN "Korea 920"
#elif defined(CFG_in866)
#  define LMIC_FREQUENCY_PLAN "India 866"
#else
#  error "Region should be specified"
#endif

40
41
namespace config {
  // 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() {
Eric Duminil's avatar
Eric Duminil committed
75
    Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
Eric Duminil's avatar
Eric Duminil committed
76
77
78
79
80
81
82
83
84
85

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

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

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

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

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

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

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

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