diff --git a/lorawan.cpp b/lorawan.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b85fcbc446eff5c228d82785258a217c9635ec09
--- /dev/null
+++ b/lorawan.cpp
@@ -0,0 +1,204 @@
+#include "lorawan.h"
+
+namespace config {
+  // Values should be defined in config.h
+  uint16_t lora_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
+
+  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;
+}
+// Pin mapping for TTGO LoRa32 V1
+// More info : https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436
+const lmic_pinmap lmic_pins = { .nss = 18, .rxtx = LMIC_UNUSED_PIN, .rst = 14, .dio = { 26, 33, 32 } };
+
+// 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
+
+//TODO: Add infos to webserver (last timestamp, sending interval, connected or not?)
+//TODO: Merge back to other branch (and other git)
+//TODO: compile for both boards, and check ESP32 && LORA
+
+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;
+
+  void printHex2(unsigned v) {
+    v &= 0xff;
+    if (v < 16)
+      Serial.print('0');
+    Serial.print(v, HEX);
+  }
+
+  void onEvent(ev_t ev) {
+    static bool trying_to_join = false;
+    Serial.print("LoRa - ");
+    Serial.print(ntp::getLocalTime());
+    Serial.print(" - ");
+    switch (ev) {
+    case EV_JOINING:
+      trying_to_join = true;
+      Serial.println(F("EV_JOINING"));
+      break;
+    case EV_JOINED:
+      trying_to_join = false;
+      waiting_for_confirmation = false;
+      LedEffects::onBoardLEDOff();
+      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);
+        Serial.print(F("netid: "));
+        Serial.println(netid, DEC);
+        Serial.print(F("devaddr: "));
+        Serial.println(devaddr, HEX);
+        Serial.print(F("AppSKey: "));
+        for (size_t i = 0; i < sizeof(artKey); ++i) {
+          if (i != 0)
+            Serial.print("-");
+          printHex2(artKey[i]);
+        }
+        Serial.println("");
+        Serial.print("NwkSKey: ");
+        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."));
+      // Disable link check validation (automatically enabled
+      // during join, but because slow data rates change max TX
+      // size, we don't use it in this example.
+      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:
+      Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
+      if (LMIC.txrxFlags & TXRX_ACK)
+        Serial.println(F("Received ack"));
+      if (LMIC.dataLen) {
+        Serial.print(F("Received "));
+        Serial.print(LMIC.dataLen);
+        Serial.println(F(" bytes of payload"));
+      }
+      break;
+    case EV_TXSTART:
+      waiting_for_confirmation = trying_to_join;
+      Serial.println(F("EV_TXSTART"));
+      break;
+    case EV_TXCANCELED:
+      waiting_for_confirmation = false;
+      LedEffects::onBoardLEDOff();
+      Serial.println(F("EV_TXCANCELED"));
+      break;
+    case EV_JOIN_TXCOMPLETE:
+      waiting_for_confirmation = false;
+      LedEffects::onBoardLEDOff();
+      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) {
+      LedEffects::onBoardLEDOn();
+      Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
+    }
+  }
+
+  void preparePayload() {
+    // 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.
+      buff[0] = (min(max(sensor::co2, 0), 5100) + 10) / 20;
+      // Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
+      buff[1] = static_cast<uint8_t>((min(max(sensor::temperature, -10), 41) + 10.1f) * 5);
+      // Mapping humidity from [0%, 100%] to [0, 200], with 0.5°C increment (0.4°C would also be possible)
+      buff[2] = static_cast<uint8_t>(min(max(sensor::humidity, 0) + 0.25f, 100) * 2);
+
+      Serial.print(F("LoRa - Payload : '0x"));
+      printHex2(buff[0]);
+      printHex2(buff[1]);
+      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
+      //  };
+      //}
+    }
+  }
+
+  void initialize() {
+    Serial.println(F("Starting LoRaWAN."));
+    // LMIC init.
+    os_init();
+    // Reset the MAC state. Session and pending data transfers will be discarded.
+    LMIC_reset();
+    // Join, but don't send anything yet.
+    LMIC_startJoining();
+  }
+
+  // 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.
+  // 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.
+  void process() {
+    os_runloop_once();
+  }
+
+  void preparePayloadIfTimehasCome() {
+    static unsigned long last_sent_at = 0;
+    unsigned long now = seconds();
+    if (now - last_sent_at > config::lora_sending_interval) {
+      last_sent_at = now;
+      preparePayload();
+    }
+  }
+}
+
+void onEvent(ev_t ev) {
+  lorawan::onEvent(ev);
+}
diff --git a/lorawan.h b/lorawan.h
new file mode 100644
index 0000000000000000000000000000000000000000..19282930f01f5b8a54ddba39a24e696cb6cacc73
--- /dev/null
+++ b/lorawan.h
@@ -0,0 +1,24 @@
+#ifndef AMPEL_LORAWAN_H_
+#define AMPEL_LORAWAN_H_
+
+#include <Arduino.h>
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+typedef uint8_t u1_t;
+//#include <lmic_project_config.h>
+
+#include "co2_sensor.h"
+#include "led_effects.h"
+#include "config.h"
+
+#include "util.h"
+
+namespace lorawan {
+  extern bool waiting_for_confirmation;
+  void initialize();
+  void process();
+  void preparePayloadIfTimehasCome();
+}
+
+#endif