Skip to Content

← ESP32 PLC Persistent Counters with NVS Preferences

Hydraulic moving floor (BLE app)ESP32 PLC 38RNVSDatalogging

ESP32 PLC Persistent Counters with NVS Preferences — full example

Store cycle counters, serial numbers and totals in ESP32 NVS flash so they survive reboots. Arduino Preferences example for an industrial PLC 38R.

Complete, runnable program for the ESP32 PLC 38R (nvs-persistent-counters-38r.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 — Persistent counters in non-volatile memory (Preferences)
 *
 * Hardware:  ESP32 PLC 38R (Industrial Shields)
 * Based on:  hydraulic moving floor project, AppLink.cpp (NVS)
 *
 * Wiring:
 *   I0_0  "Load cycle" push button    (simulates end of load)
 *   I0_1  "Unload cycle" push button  (simulates end of unload)
 *
 * Logic:
 *   - Serial number, load / unload / lifetime counters, oil volume and
 *     activation flag are stored in NVS ("nvdata" partition).
 *   - They survive reboots, power outages and OTA reflashes.
 *   - Writes are deferred (dirty flag + minimum interval) to avoid
 *     wearing out the flash on every pulse.
 *
 * Integration with other catalog modules:
 *   - The counters are reported to the app over BLE as [C1]loads [C2]unloads
 *     (see ejemplos/piso-movil/ble-mobile-app-control-38r.ino).
 *   - The oil volume is computed with the pulse flow meter
 *     (see ejemplos/piso-movil/pulse-flow-meter-38r.ino).
 */

#include 

#define BTN_LOAD    I0_0
#define BTN_UNLOAD  I0_1

// NVS keys (max. 15 characters)
#define KEY_SERIAL    "serial_num"
#define KEY_LOADS     "cnt_load"
#define KEY_UNLOADS   "cnt_unload"
#define KEY_LIFE      "cnt_life"
#define KEY_OIL       "oil_volume"
#define KEY_ACTIVE    "activated"

#define SAVE_INTERVAL_MS 5000   // minimum time between flash writes

Preferences prefs;

String serialNum;
int   counterLoad   = 0;
int   counterUnload = 0;
int   counterLife   = 0;     // total machine cycles
float oilVolume     = 0.0;   // liters of oil moved
bool  activated     = false;

bool     dirty      = false; // there is data pending to be saved
uint32_t lastSaveMs = 0;

void loadNVS() {
  prefs.begin("nvdata", true);                       // read-only
  serialNum     = prefs.getString(KEY_SERIAL, "SN-0000");
  counterLoad   = prefs.getInt(KEY_LOADS, 0);
  counterUnload = prefs.getInt(KEY_UNLOADS, 0);
  counterLife   = prefs.getInt(KEY_LIFE, 0);
  oilVolume     = prefs.getFloat(KEY_OIL, 0.0);
  activated     = prefs.getBool(KEY_ACTIVE, false);
  prefs.end();
}

void saveNVS() {
  prefs.begin("nvdata", false);                      // read/write
  prefs.putInt(KEY_LOADS, counterLoad);
  prefs.putInt(KEY_UNLOADS, counterUnload);
  prefs.putInt(KEY_LIFE, counterLife);
  prefs.putFloat(KEY_OIL, oilVolume);
  prefs.putBool(KEY_ACTIVE, activated);
  prefs.end();
  dirty = false;
  lastSaveMs = millis();
  Serial.println("-> NVS saved");
}

void setup() {
  Serial.begin(115200);
  pinMode(BTN_LOAD, INPUT);
  pinMode(BTN_UNLOAD, INPUT);

  loadNVS();   // the counters come back exactly as they were after every reboot

  Serial.println("=== Persistent counters (NVS) ===");
  Serial.println("Serial:    " + serialNum);
  Serial.println("Loads:     " + String(counterLoad));
  Serial.println("Unloads:   " + String(counterUnload));
  Serial.println("Lifetime:  " + String(counterLife));
  Serial.println("Oil:       " + String(oilVolume, 1) + " L");
  Serial.println("Activated: " + String(activated ? "yes" : "no"));
}

void loop() {
  // Rising edge with simple debounce
  static bool prevLoad = false, prevUnload = false;
  bool load   = digitalRead(BTN_LOAD);
  bool unload = digitalRead(BTN_UNLOAD);

  if (load && !prevLoad) {
    counterLoad++;
    counterLife++;
    oilVolume += 12.5;            // estimated liters per load cycle
    dirty = true;
    Serial.println("Load #" + String(counterLoad));
  }
  if (unload && !prevUnload) {
    counterUnload++;
    counterLife++;
    dirty = true;
    Serial.println("Unload #" + String(counterUnload));
  }
  prevLoad = load;
  prevUnload = unload;

  // Deferred write: protects the flash from a write on every pulse
  if (dirty && millis() - lastSaveMs > SAVE_INTERVAL_MS) {
    saveNVS();
  }

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