ESP32 Gateway

PLC Monitoring System Firmware

Flash the sketch below to stream efficiency, runtime, and downtime metrics for all five PLCs. Each field maps directly to the Cloudflare ingestion schema (`time_stamp`, `shift`, `efficiency`, runtime minutes, stop counts, and downtime buckets).

↩ Back to dashboard View report

Telemetry field map

Each payload produced by the firmware includes these fields consumed by the ingestion worker:

Configuration checklist

Firmware version 2025-11-17 15:20 GMT

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <math.h>

// ====== CONFIGURE THESE VALUES ======
const char* FIRMWARE_VERSION = "2025-11-17 15:20 GMT";
const char* WIFI_SSID = "AFAZ";
const char* WIFI_PASSWORD = "122221duet";

const char* INGEST_URL = "https://ingest-subscriber.arif-173.workers.dev/";
const char* DEVICE_KEY = "plc-esp32-secret-xyz";  // Must match Cloudflare Worker DEVICE_KEY binding
const uint8_t HTTP_MAX_RETRIES = 3;
const uint32_t HTTP_RETRY_DELAY_MS = 600;

// Publish every N milliseconds per PLC (5 PLCs -> total loop interval will be PLC_COUNT * PUBLISH_INTERVAL_MS)
const uint32_t PUBLISH_INTERVAL_MS = 2'000;
// Offset applied to simulated timestamps so they look like epoch milliseconds in 2024+
const uint64_t TIMESTAMP_OFFSET = 1730600000000ULL;

// Per-PLC baseline configuration (adjust to match your machines)
struct PlcConfig {
  uint8_t id;
  const char* machineName;
  uint16_t machineNo;
  const char* shift;
  float baseMonitoredMinutes;
  float baseOperatedMinutes;
  float baseRpm;
  float baseStopEvents;
};

PlcConfig PLC_CONFIGS[] = {
  {1, "Warp Loom A1", 101, "Day", 480.0f, 420.0f, 1180.0f, 14.0f},
  {2, "Weft Loom B2", 202, "Night", 470.0f, 405.0f, 980.0f, 16.0f},
  {3, "Sizing Unit C3", 303, "Swing", 450.0f, 350.0f, 0.0f, 12.0f},
  {4, "Warping D4", 404, "Day", 510.0f, 460.0f, 1120.0f, 11.0f},
  {5, "Finishing E5", 505, "Night", 440.0f, 320.0f, 720.0f, 18.0f}
};
const size_t PLC_COUNT = sizeof(PLC_CONFIGS) / sizeof(PLC_CONFIGS[0]);
// ====== END CONFIG ======

WiFiClientSecure wifiClient;
uint32_t lastPublish = 0;

void connectWiFi();
void publishTelemetryForAllPlcs();
void populateTelemetry(const PlcConfig &plc, JsonDocument &doc);
bool postTelemetryPayload(const char* payload, size_t length, uint8_t plcId);
float modulateValue(float base, float variation);
float clampPositive(float value);
float randomRatioJitter(float baseRatio);

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

  Serial.printf("PLC ESP32 firmware build %s\n", FIRMWARE_VERSION);
  connectWiFi();
  // Cloudflare Workers require TLS. setInsecure() skips cert validation; replace with setCACert() when cert pin is available.
  wifiClient.setInsecure();
  Serial.printf("Posting telemetry to %s\n", INGEST_URL);
  randomSeed(esp_random());
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  const uint32_t now = millis();
  if (now - lastPublish >= PUBLISH_INTERVAL_MS) {
    publishTelemetryForAllPlcs();
    lastPublish = now;
  }
}

void connectWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    return;
  }

  Serial.printf("Connecting to Wi-Fi %s\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  uint8_t retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (++retries > 40) {
      Serial.println(" restarting Wi-Fi module");
      WiFi.disconnect(true);
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
      retries = 0;
    }
  }

  Serial.printf("\nWi-Fi connected: %s\n", WiFi.localIP().toString().c_str());
}

void publishTelemetryForAllPlcs() {
  for (size_t i = 0; i < PLC_COUNT; ++i) {
    const PlcConfig &plc = PLC_CONFIGS[i];

    StaticJsonDocument<512> doc;
    populateTelemetry(plc, doc);

    char payload[512];
    const size_t len = serializeJson(doc, payload, sizeof(payload));

    if (postTelemetryPayload(payload, len, plc.id)) {
      Serial.printf("Published telemetry for PLC %u\n", plc.id);
    } else {
      Serial.printf("Failed to publish telemetry for PLC %u\n", plc.id);
    }

    delay(200);  // small gap between PLC messages
  }
}

void populateTelemetry(const PlcConfig &plc, JsonDocument &doc) {
  const uint64_t epochMs = static_cast<uint64_t>(millis()) + TIMESTAMP_OFFSET;
  const float monitored = clampPositive(modulateValue(plc.baseMonitoredMinutes, 20.0f));
  float operated = clampPositive(modulateValue(plc.baseOperatedMinutes, 25.0f));
  if (operated > monitored) {
    operated = monitored * 0.97f;
  }
  const float totalStop = clampPositive(monitored - operated);
  float stopEvents = clampPositive(modulateValue(plc.baseStopEvents, 3.0f));
  if (stopEvents < 1.0f) {
    stopEvents = 1.0f;
  }

  float rpmVariation = plc.baseRpm * 0.05f;
  if (rpmVariation < 5.0f) {
    rpmVariation = 5.0f;
  }
  const float rpm = clampPositive(modulateValue(plc.baseRpm, rpmVariation));
  const float efficiency = monitored > 0.0f ? (operated / monitored) * 100.0f : 0.0f;

  doc["time_stamp"] = epochMs;
  doc["plc_id"] = plc.id;
  doc["machine_name"] = plc.machineName;
  doc["machine_no"] = plc.machineNo;
  doc["shift"] = plc.shift;
  doc["efficiency"] = efficiency;
  doc["rpm"] = rpm;
  doc["monitoredtime"] = monitored;
  doc["operatedtime"] = operated;
  doc["totalstoptime"] = totalStop;
  doc["totalstoptimes"] = roundf(stopEvents);

  struct Category {
    const char* downtimeKey;
    const char* eventKey;
    float baseRatio;
  };

  Category categories[] = {
    {"fillingdowntime", "fillingtimes", 0.10f},
    {"warppdowntime", "warpptimes", 0.08f},
    {"warpgdowntime", "warpgtimes", 0.08f},
    {"othersdowntime", "others_times", 0.07f},
    {"beampdowntime", "beamptimes", 0.07f},
    {"beamgdowntime", "beamgtimes", 0.07f},
    {"knottingdowntime", "knottingtimes", 0.09f},
    {"drawingdowntime", "drawingtimes", 0.12f},
    {"mechanicaldowntime", "mechanicaltimes", 0.17f},
    {"electricaldowntime", "electricaltimes", 0.15f},
  };

  float remainingStop = totalStop;
  float remainingEvents = stopEvents;
  const size_t categoryCount = sizeof(categories) / sizeof(Category);

  for (size_t idx = 0; idx < categoryCount; ++idx) {
    const bool isLast = idx == categoryCount - 1;
    float ratio = randomRatioJitter(categories[idx].baseRatio);
    float minutes = isLast ? remainingStop : min(remainingStop, max(0.0f, totalStop * ratio));
    remainingStop -= minutes;

    float events = isLast ? remainingEvents : min(remainingEvents, max(0.0f, stopEvents * ratio));
    remainingEvents -= events;

    doc[categories[idx].downtimeKey] = minutes;
    doc[categories[idx].eventKey] = roundf(events);
  }
}

bool postTelemetryPayload(const char* payload, size_t length, uint8_t plcId) {
  for (uint8_t attempt = 1; attempt <= HTTP_MAX_RETRIES; ++attempt) {
    HTTPClient https;
    if (!https.begin(wifiClient, INGEST_URL)) {
      Serial.println("Unable to init HTTPS client");
      delay(HTTP_RETRY_DELAY_MS);
      continue;
    }

    https.setTimeout(8000);
    https.addHeader("Content-Type", "application/json");
    if (DEVICE_KEY[0] != '\0') {
      https.addHeader("X-Device-Key", DEVICE_KEY);
    }

    const int httpCode = https.POST(reinterpret_cast<const uint8_t*>(payload), length);
    if (httpCode > 0) {
      if (httpCode >= 200 && httpCode < 300) {
        https.end();
        return true;
      }
      Serial.printf("Worker replied HTTP %d for PLC %u\n", httpCode, plcId);
      const String response = https.getString();
      if (response.length() > 0) {
        Serial.println(response);
      }
    } else {
      Serial.printf("HTTPS POST failed for PLC %u (error %d)\n", plcId, httpCode);
    }

    https.end();
    delay(HTTP_RETRY_DELAY_MS);
  }

  return false;
}

float modulateValue(float base, float variation) {
  float delta = (random(-1000, 1000) / 1000.0f) * variation;
  return base + delta;
}

float clampPositive(float value) {
  return value < 0.0f ? 0.0f : value;
}

float randomRatioJitter(float baseRatio) {
  float jitter = static_cast<float>(random(-50, 51)) / 1000.0f;  // ±0.05
  float ratio = baseRatio + jitter;
  if (ratio < 0.02f) {
    ratio = 0.02f;
  }
  return ratio;
}