lorawan.cpp 8.04 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
#include <SPI.h>
Eric Duminil's avatar
Eric Duminil committed
10
11
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
12

13
14
namespace config {
  // Values should be defined in config.h
Eric Duminil's avatar
Eric Duminil committed
15
  uint16_t lorawan_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

  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
44
  bool connected = false;
45
  char last_transmission[23] = "";
46

Eric Duminil's avatar
Eric Duminil committed
47
  void initialize() {
Eric Duminil's avatar
Eric Duminil committed
48
    Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
Eric Duminil's avatar
Eric Duminil committed
49
50
51
52
53
54
55
56
57
58

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

  // 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
64
  // 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
65
66
67
68
  void process() {
    os_runloop_once();
  }

69
70
71
72
73
74
75
76
  void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
      Serial.print('0');
    Serial.print(v, HEX);
  }

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

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

169
      Serial.print(F("LoRa - Payload : '"));
170
      printHex2(buff[0]);
171
      Serial.print(" ");
172
      printHex2(buff[1]);
173
      Serial.print(" ");
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
      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
196
  void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
197
198
    static unsigned long last_sent_at = 0;
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
199
    if (connected && (now - last_sent_at > config::lorawan_sending_interval)) {
200
      last_sent_at = now;
Eric Duminil's avatar
Eric Duminil committed
201
      preparePayload(co2, temperature, humidity);
202
203
    }
  }
Eric Duminil's avatar
Eric Duminil committed
204
205
206
207
208
209
210
211
212
213
214

  /*****************************************************************
   * 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);
  }
215
216
217
218
219
}

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