← 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