Skip to Content

← 4-20 mA Level Sensor Reading with an ESP32 PLC

Water pumping (sanitation)ESP32 PLC 38ARGPIOAcquisition

4-20 mA Level Sensor Reading with an ESP32 PLC — full example

Read a 4-20 mA level probe on an ESP32 PLC: scaling to cm, moving-average filtering, broken-wire detection and hysteresis pump control in Arduino.

Complete, runnable program for the ESP32 PLC 38AR (level-sensor-4-20ma.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 — Reading a 4-20 mA analog level sensor
 *
 * Hardware: ESP32 PLC 38AR (Industrial Shields)
 * Based on: water pumping project (sanitation), bombament-2b-38ar.ino
 *
 * Wiring:
 *   I0_11  4-20 mA hydrostatic level sensor (2-wire, powered at 24 Vdc;
 *          the PLC analog input converts the current loop to 0-1023)
 *   I0_0   Overlevel float switch (NO) — backup independent of the sensor
 *   Q0_0   Drain pump contactor
 *   Q0_1   Alarm pilot light
 *
 * Logic:
 *   1. Raw 0-1023 reading of I0_11 (4 mA ~ 0%, 20 mA ~ 100% of the scale).
 *   2. Moving-average filter (8 samples) to remove current-loop ripple.
 *   3. Broken-wire detection: below ~3.5 mA the loop is open (sensor
 *      disconnected or cut cable) — alarm and safe mode.
 *   4. Conversion to centimeters of water column and hysteresis control:
 *      the pump starts above LEVEL_START and stops below LEVEL_STOP.
 *      The overlevel float switch acts as a hardwired backup.
 *
 * Integration: `level_cm` and `filtered_sensor` (0-1023) are ready to go into
 * the 4-byte LoRaWAN frame — see ejemplos/bombeo-agua/lorawan-telemetry-bitpacking.ino
 */

// --- I/O map (ESP32 PLC 38AR pins, industrialshields-arduino library)
#define I_LEVEL_SENSOR  I0_11
#define I_OVERLEVEL     I0_0
#define Q_PUMP          Q0_0
#define Q_PILOT         Q0_1

// --- Sensor scale: transmitter from 0 to 250 cm of water column
const float    SCALE_CM        = 250.0;  // level at 20 mA
const uint16_t RAW_4MA         = 205;    // approx. reading at 4 mA  (1023 * 4/20)
const uint16_t RAW_20MA        = 1023;   // reading at 20 mA
const uint16_t RAW_BROKEN_WIRE = 180;    // < ~3.5 mA: open loop

// --- Hysteresis control setpoints (in cm)
const float LEVEL_START = 180.0;         // start the drain pump
const float LEVEL_STOP  = 60.0;          // stop the pump

// --- Moving-average filter
const uint8_t N_SAMPLES = 8;
uint16_t samples[N_SAMPLES];
uint8_t  sample_index = 0;

uint16_t filtered_sensor = 0;            // 0-1023, ready for telemetry
float    level_cm        = 0.0;
bool     broken_wire     = false;
bool     pump            = false;

// ------------------------------------------------- Reading + filtering
uint16_t read_filtered_sensor() {
  samples[sample_index] = analogRead(I_LEVEL_SENSOR);  // I0_11, 4-20mA -> 0-1023
  sample_index = (sample_index + 1) % N_SAMPLES;

  uint32_t sum = 0;
  for (uint8_t i = 0; i < N_SAMPLES; i++) sum += samples[i];
  return sum / N_SAMPLES;
}

// ------------------------------------------------- Scaling to physical units
float raw_to_cm(uint16_t raw) {
  if (raw <= RAW_4MA) return 0.0;        // below 4 mA there is no valid reading
  return (raw - RAW_4MA) * SCALE_CM / (RAW_20MA - RAW_4MA);
}

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

  pinMode(I_LEVEL_SENSOR, INPUT);
  pinMode(I_OVERLEVEL, INPUT);
  pinMode(Q_PUMP, OUTPUT);
  pinMode(Q_PILOT, OUTPUT);
  digitalWrite(Q_PUMP, LOW);
  digitalWrite(Q_PILOT, LOW);

  // Preload the filter so it does not start with a zero average
  uint16_t first = analogRead(I_LEVEL_SENSOR);
  for (uint8_t i = 0; i < N_SAMPLES; i++) samples[i] = first;
}

void loop() {
  filtered_sensor = read_filtered_sensor();

  // --- Loop diagnostics: broken wire / disconnected sensor
  broken_wire = (filtered_sensor < RAW_BROKEN_WIRE);

  if (broken_wire) {
    // Safe mode: with no reliable reading, the pump only obeys the overlevel
    // float switch (hardwired backup independent of the sensor).
    pump = digitalRead(I_OVERLEVEL);
    digitalWrite(Q_PILOT, HIGH);                 // warn the operator
  } else {
    level_cm = raw_to_cm(filtered_sensor);

    // Hysteresis control: two separate setpoints keep the pump from
    // cycling in and out continuously around a single threshold.
    if (level_cm > LEVEL_START)     pump = true;
    else if (level_cm < LEVEL_STOP) pump = false;

    digitalWrite(Q_PILOT, digitalRead(I_OVERLEVEL));  // overlevel = alarm
  }

  digitalWrite(Q_PUMP, pump);

  // Trace every second: raw, cm and state — useful to calibrate the scale on site
  static uint32_t t_trace = 0;
  if (millis() - t_trace >= 1000) {
    t_trace = millis();
    Serial.printf("raw=%u  level=%.1f cm  pump=%d  broken_wire=%d\n",
                  filtered_sensor, level_cm, pump, broken_wire);
  }

  delay(100);
}
Download the full project pack — freeThis example + the related ones + bill of materials