ESP32 Gateway
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).
Each payload produced by the firmware includes these fields consumed by the ingestion worker:
plc_id, machine_name, machine_no, shift.time_stamp (epoch milliseconds).efficiency (percent) and rpm.monitoredtime, operatedtime, totalstoptime, and stop counter totalstoptimes.filling, warpp, warpg, others, beamp, beamg, knotting, drawing, mechanical, and electrical (e.g. fillingdowntime + fillingtimes).WIFI_SSID and WIFI_PASSWORD with plant network credentials.INGEST_URL at your Cloudflare Worker endpoint and copy the same DEVICE_KEY secret configured via Wrangler.PLC_CONFIGS entry (machine name/number, shift, monitored & operated minutes, stop events).PUBLISH_INTERVAL_MS, HTTP retry values, and baseline stop ratios for site-specific cadence.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;
}