Skip to Content

← FreeRTOS Tasks on an ESP32 PLC with a LoRaWAN Watchdog

Water pumping (sanitation)ESP32 PLC 14 / 38ARLoRaWANResilience / OTA

FreeRTOS Tasks on an ESP32 PLC with a LoRaWAN Watchdog — full example

Run pump control, LoRaWAN telemetry and an hourly OTAA re-join as separate FreeRTOS tasks on an ESP32 PLC. Full Arduino code from a real station.

Complete, runnable program for the ESP32 PLC 14 / 38AR (freertos-multitasking-rejoin.ino): wiring header, requirements and integration notes included.

Download the full project pack — freeThis example + the related ones + bill of materials

Read-only preview.

/*
 * COMPLETE EXAMPLE — FreeRTOS multitasking with periodic reconnection (comms watchdog)
 *
 * Hardware: ESP32 PLC 14 (Industrial Shields) + LoRa RN2xx3 module
 * Based on: water pumping project (sanitation), bombament-1b-14.ino
 *
 * Wiring:
 *   I0_0  Minimum-level float switch  (NO, closed = water above minimum)
 *   I0_1  Maximum-level float switch  (NO, closed = water above maximum)
 *   I0_2  Overlevel / alarm float switch (NO)
 *   I0_3  Thermal relay contact       (closed = fault)
 *   Q0_0  Pump contactor command
 *   Q0_1  Alarm pilot light
 *
 * Logic:
 *   Three FreeRTOS tasks with separate responsibilities and different priorities:
 *     1. vTaskControl (prio 3, 100 ms): reads float switches and drives the pump.
 *        It never waits on the radio: control is not blocked even if LoRa hangs.
 *     2. vTaskLora    (prio 1, 60 s):   packs the state and transmits it.
 *     3. vTaskRejoin  (prio 2, 1 h):    periodic OTAA re-join. If the LoRaWAN
 *        link has dropped (gateway rebooted, session drift), the station
 *        recovers on its own without intervention — communications watchdog.
 *
 * Integration: the control logic can be replaced by the full state machine
 * (see ejemplos/bombeo-agua/pump-state-machine-float-switches.ino) and the frame
 * by the bit packing from the catalog (lorawan-telemetry-bitpacking.ino).
 */

// --- I/O map (ESP32 PLC 14 pins, industrialshields-arduino library)
#define I_FLOAT_MIN  I0_0
#define I_FLOAT_MAX  I0_1
#define I_OVERLEVEL  I0_2
#define I_THERMAL    I0_3
#define Q_PUMP       Q0_0
#define Q_PILOT      Q0_1

// --- Period of each task (the whole point is that they are independent)
const TickType_t T_CONTROL = pdMS_TO_TICKS(100);      // control: 100 ms
const TickType_t T_LORA    = pdMS_TO_TICKS(60000);    // telemetry: 60 s
const TickType_t T_REJOIN  = pdMS_TO_TICKS(3600000);  // OTAA re-join: 1 h

// --- State shared between tasks (written by control, read by LoRa).
//     1-byte variables: reads are atomic, no mutex needed.
volatile bool pump = false, overlevel = false, fault = false;

// ------------------------------------------------- Task 1: control (100 ms)
// Highest priority: the process rules. Even while the radio is transmitting
// or re-joining, this task keeps running every 100 ms.
void vTaskControl(void *pv) {
  TickType_t wakeTime = xTaskGetTickCount();
  for (;;) {
    bool minLvl = digitalRead(I_FLOAT_MIN);
    bool maxLvl = digitalRead(I_FLOAT_MAX);
    overlevel = digitalRead(I_OVERLEVEL);
    fault     = digitalRead(I_THERMAL);

    if (fault)                  pump = false;          // thermal relay: stop
    else if (minLvl && maxLvl)  pump = true;           // tank full: empty it
    else if (!minLvl)           pump = false;          // emptying finished

    digitalWrite(Q_PUMP, pump);
    digitalWrite(Q_PILOT, fault || overlevel);

    vTaskDelayUntil(&wakeTime, T_CONTROL);             // exact period, no drift
  }
}

// ------------------------------------------------- Task 2: telemetry (60 s)
// Low priority: if it collides with control, it waits. A LoRa send can take
// seconds (duty cycle, high SF) and here it bothers no one.
void vTaskLora(void *pv) {
  for (;;) {
    byte msg = 0;
    msg |= digitalRead(I_FLOAT_MIN) << 7;
    msg |= digitalRead(I_FLOAT_MAX) << 6;
    msg |= overlevel << 5;
    msg |= pump      << 4;
    msg |= fault     << 3;
    lora_send_bytes(&msg, 1);          // see LoRaWAN module in the catalog
    vTaskDelay(T_LORA);
  }
}

// ------------------------------------------------- Task 3: OTAA re-join (1 h)
// The "communications watchdog": in remote stations with nobody watching,
// renegotiating the session hourly guarantees a dropped link recovers on its own.
void vTaskRejoin(void *pv) {
  for (;;) {
    vTaskDelay(T_REJOIN);                               // wait first, then re-join
    Serial.println("[rejoin] renegotiating OTAA session...");
    lora_join_otaa("APP_EUI", "APP_KEY");               // credentials: placeholders
  }
}

void setup() {
  Serial.begin(115200);

  pinMode(I_FLOAT_MIN, INPUT);
  pinMode(I_FLOAT_MAX, INPUT);
  pinMode(I_OVERLEVEL, INPUT);
  pinMode(I_THERMAL, INPUT);
  pinMode(Q_PUMP, OUTPUT);
  pinMode(Q_PILOT, OUTPUT);
  digitalWrite(Q_PUMP, LOW);
  digitalWrite(Q_PILOT, LOW);

  lora_init(57600);                                     // RN2xx3 via SerialSC1
  lora_join_otaa("APP_EUI", "APP_KEY");                 // initial join

  // 4096 bytes of stack per task: plenty for digitalRead + AT commands
  xTaskCreate(vTaskControl, "control", 4096, NULL, 3, NULL);  // 100 ms
  xTaskCreate(vTaskLora,    "lora",    4096, NULL, 1, NULL);  // 60 s
  xTaskCreate(vTaskRejoin,  "rejoin",  4096, NULL, 2, NULL);  // 1 h
}

void loop() { vTaskDelay(portMAX_DELAY); }              // everything lives in FreeRTOS tasks
Download the full project pack — freeThis example + the related ones + bill of materials