// Complete ESP8266 JamesBon Supervisor with MQTT Status Publishing
// Copy and paste this entire code to Arduino IDE
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ESP8266WebServer.h>
#include <Preferences.h>
#include <SSD1306Wire.h>
#include <ESP8266HTTPClient.h>
// OLED setup
SSD1306Wire display(0x3C, D5, D6);
// WiFi credentials
const char* ssid = "JamesBon";
const char* password = "jamesbon123";
// MQTT settings
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
const char* mqtt_topic = "jamesbon/pulse";
const char* status_request_topic = "jamesbon/status/request";
const char* status_response_topic = "jamesbon/status/response";
WiFiClient espClient;
PubSubClient mqtt_client(espClient);
// Telegram settings
const String TELEGRAM_TOKEN = "7909609422:AAGWlUDPn9SyFzeyavPfxsWA29klpm_HyTA";
const String CHAT_ID = "1084601333";
ESP8266WebServer server(80);
Preferences preferences;
// Buzzer and button
const int buzzerPin = D1;
const int muteButtonPin = D3;
bool buzzerActive = false;
bool isMuted = false;
unsigned long lastButtonPress = 0;
const unsigned long BUTTON_DEBOUNCE = 200;
// Display timing
unsigned long lastDisplayUpdate = 0;
const unsigned long DISPLAY_UPDATE_INTERVAL = 5000;
int currentDisplayIndex = 0;
const unsigned long DEFAULT_TIMEOUT = 60 * 60 * 1000;
// WiFi reconnection settings
unsigned long lastWifiReconnect = 0;
const unsigned long WIFI_RETRY_INTERVAL = 10000;
int wifiRetryCount = 0;
const int MAX_WIFI_RETRIES = 3;
// MQTT reconnection settings
unsigned long lastMqttReconnect = 0;
const unsigned long MQTT_RETRY_INTERVAL = 5000;
int mqttRetryCount = 0;
const int MAX_MQTT_RETRIES = 5;
// Status publishing
unsigned long lastStatusPublish = 0;
const unsigned long STATUS_PUBLISH_INTERVAL = 30000; // 30 seconds auto-publish
struct ConnectivityTracker {
unsigned long lastConnectAttempt = 0;
unsigned long connectionCheckInterval = 20 * 60 * 1000;
int failedAttempts = 0;
int maxAttemptsPerHour = 3;
unsigned long hourStartTime = 0;
bool isOffline = false;
bool telegramSent = false;
};
ConnectivityTracker connectivity;
struct MessageTracker {
String code;
String description;
unsigned long lastReceived;
unsigned long timeoutInterval;
bool alertSent;
bool newAlert;
bool telegramSent;
};
MessageTracker messages[] = {
{"JMBR1", "Modem 1 @Fab. Lambung", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMBR2", "Modem 2 @Goliath Crane", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB1", "Shot Blasting Machine", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB2", "Plasma Cutting Safpro", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB3", "Flame Planner Cutting", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB4", "OHC 10T Barat Lambung", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB5", "Goliath Crane 300 T", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB6", "Plasma Cutting Victory", 0, DEFAULT_TIMEOUT, false, false, false},
{"JMB7", "Plasma Cutting Dama", 0, DEFAULT_TIMEOUT, false, false, false},
};
const int numMessages = sizeof(messages) / sizeof(messages[0]);
bool testInternetConnection() {
WiFiClient client;
return client.connect("8.8.8.8", 53);
}
// NEW FUNCTION: Publish status to MQTT for Kodular app
void publishStatusToMQTT() {
if (!mqtt_client.connected()) {
Serial.println("MQTT not connected - cannot publish status");
return;
}
String statusMessage = "";
unsigned long now = millis();
// Build status message for all machines
for (int i = 0; i < numMessages; i++) {
unsigned long elapsed = (now - messages[i].lastReceived) / 60000; // minutes
bool isActive = elapsed < (messages[i].timeoutInterval / 60000);
statusMessage += messages[i].code + ",";
statusMessage += messages[i].description + ",";
statusMessage += String(elapsed) + ",";
statusMessage += (isActive ? "Active" : "Not Active");
// Add newline except for last item
if (i < numMessages - 1) {
statusMessage += "\n";
}
}
// Publish the complete status
bool published = mqtt_client.publish(status_response_topic, statusMessage.c_str());
if (published) {
Serial.println("Status published to MQTT:");
Serial.println(statusMessage);
Serial.println("---");
} else {
Serial.println("Failed to publish status to MQTT");
}
}
bool hasActiveAlarms() {
unsigned long now = millis();
for (int i = 0; i < numMessages; i++) {
if (now - messages[i].lastReceived > messages[i].timeoutInterval) {
return true;
}
}
return false;
}
void reconnectWiFi() {
if (WiFi.status() == WL_CONNECTED) {
wifiRetryCount = 0;
return;
}
unsigned long now = millis();
if (now - lastWifiReconnect < WIFI_RETRY_INTERVAL) return;
lastWifiReconnect = now;
if (wifiRetryCount >= MAX_WIFI_RETRIES) {
Serial.println("Max WiFi retries reached. Restarting ESP...");
ESP.restart();
return;
}
wifiRetryCount++;
Serial.print("WiFi reconnection attempt: ");
Serial.println(wifiRetryCount);
WiFi.disconnect();
delay(1000);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts++ < 20) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi reconnected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
wifiRetryCount = 0;
} else {
Serial.println("\nWiFi reconnection failed!");
}
}
void sendTelegramAlert(String message) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Cannot send Telegram - WiFi not connected");
return;
}
const int MAX_RETRIES = 3;
int retryCount = 0;
while (retryCount < MAX_RETRIES) {
WiFiClientSecure client;
HTTPClient http;
client.setInsecure();
client.setTimeout(15000);
String url = "https://api.telegram.org/bot" + TELEGRAM_TOKEN + "/sendMessage";
String postData = "chat_id=" + CHAT_ID + "&text=" + message;
Serial.println("Telegram attempt " + String(retryCount + 1) + ": " + message);
http.begin(client, url);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
http.setTimeout(15000);
int httpCode = http.POST(postData);
if (httpCode == 200) {
Serial.println("Telegram message sent successfully!");
http.end();
return;
} else {
Serial.println("HTTP Error: " + String(httpCode));
retryCount++;
http.end();
if (retryCount < MAX_RETRIES) {
delay(2000);
}
}
}
Serial.println("Failed to send Telegram message after " + String(MAX_RETRIES) + " attempts");
}
void reconnectMQTT() {
if (mqtt_client.connected()) {
mqttRetryCount = 0;
return;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Cannot reconnect MQTT - WiFi not connected");
return;
}
unsigned long now = millis();
if (now - lastMqttReconnect < MQTT_RETRY_INTERVAL) return;
lastMqttReconnect = now;
if (mqttRetryCount >= MAX_MQTT_RETRIES) {
Serial.println("Max MQTT retries reached. Will keep trying...");
mqttRetryCount = 0;
}
mqttRetryCount++;
Serial.print("MQTT reconnection attempt: ");
Serial.println(mqttRetryCount);
String clientId = "ESP8266Client-" + String(random(0xffff), HEX);
if (mqtt_client.connect(clientId.c_str())) {
Serial.println("MQTT connected!");
// Subscribe to both original heartbeat topic and new status request topic
mqtt_client.subscribe(mqtt_topic);
mqtt_client.subscribe(status_request_topic);
Serial.print("Subscribed to topics: ");
Serial.print(mqtt_topic);
Serial.print(" and ");
Serial.println(status_request_topic);
connectivity.isOffline = false;
connectivity.telegramSent = false;
mqttRetryCount = 0;
if (!hasActiveAlarms()) {
digitalWrite(buzzerPin, LOW);
buzzerActive = false;
}
} else {
Serial.print("MQTT connection failed, rc=");
Serial.println(mqtt_client.state());
connectivity.isOffline = true;
if (!buzzerActive) {
digitalWrite(buzzerPin, HIGH);
buzzerActive = true;
Serial.println("BUZZER ON");
}
if (!connectivity.telegramSent) {
sendTelegramAlert("JAMESBON SUPERVISOR: MQTT Offline Detected!");
connectivity.telegramSent = true;
}
}
}
void checkConnectivity() {
unsigned long now = millis();
if (now - connectivity.hourStartTime >= 3600000) {
connectivity.failedAttempts = 0;
connectivity.hourStartTime = now;
}
if (now - connectivity.lastConnectAttempt >= connectivity.connectionCheckInterval) {
connectivity.lastConnectAttempt = now;
bool wifiOK = WiFi.status() == WL_CONNECTED;
bool internetOK = wifiOK && testInternetConnection();
bool mqttOK = mqtt_client.connected();
if (wifiOK && internetOK && mqttOK) {
connectivity.isOffline = false;
connectivity.failedAttempts = 0;
connectivity.telegramSent = false;
if (!hasActiveAlarms()) {
digitalWrite(buzzerPin, LOW);
buzzerActive = false;
}
} else {
connectivity.failedAttempts++;
connectivity.isOffline = true;
if (!buzzerActive) {
digitalWrite(buzzerPin, HIGH);
buzzerActive = true;
}
}
}
}
// MODIFIED CALLBACK: Handle both machine heartbeats and status requests
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) message += (char)payload[i];
String topicStr = String(topic);
// Handle status request from Kodular app
if (topicStr == status_request_topic && message == "GET_STATUS") {
Serial.println("Status request received from Kodular app");
publishStatusToMQTT();
return;
}
// Handle machine heartbeats (original functionality)
for (int i = 0; i < numMessages; i++) {
if (message == messages[i].code) {
messages[i].lastReceived = millis();
messages[i].alertSent = false;
messages[i].newAlert = false;
messages[i].telegramSent = false;
if (!hasActiveAlarms() && !connectivity.isOffline) {
digitalWrite(buzzerPin, LOW);
buzzerActive = false;
isMuted = false;
}
}
}
}
void handleAPIStatus() {
String response = WiFi.localIP().toString() + "\n";
unsigned long now = millis();
for (int i = 0; i < numMessages; i++) {
unsigned long elapsed = (now - messages[i].lastReceived) / 60000;
bool active = elapsed < (messages[i].timeoutInterval / 60000);
response += messages[i].code + "," + messages[i].description + "," + String(elapsed) + "," + (active ? "Active" : "Not Active") + "\n";
}
server.send(200, "text/plain", response);
}
void handleUpdateTimeout() {
if (!server.hasArg("code") || !server.hasArg("timeout")) {
server.send(400, "text/plain", "Missing parameters");
return;
}
String code = server.arg("code");
unsigned long timeout = server.arg("timeout").toInt() * 60 * 1000;
for (int i = 0; i < numMessages; i++) {
if (messages[i].code == code) {
messages[i].timeoutInterval = timeout;
preferences.putULong(code.c_str(), timeout);
server.send(200, "text/plain", "Updated and saved timeout for " + code);
return;
}
}
server.send(404, "text/plain", "Code not found");
}
void handleResetTimeouts() {
preferences.clear();
for (int i = 0; i < numMessages; i++) messages[i].timeoutInterval = DEFAULT_TIMEOUT;
server.send(200, "text/plain", "All timeouts reset to default");
}
void setupAPI() {
server.on("/api/status", HTTP_GET, handleAPIStatus);
server.on("/api/update", HTTP_GET, handleUpdateTimeout);
server.on("/api/reset", HTTP_GET, handleResetTimeouts);
server.begin();
}
void loadTimeoutsFromPreferences() {
for (int i = 0; i < numMessages; i++) {
String key = messages[i].code;
if (preferences.isKey(key.c_str())) {
unsigned long timeout = preferences.getULong(key.c_str(), DEFAULT_TIMEOUT);
if (timeout >= 60000 && timeout <= 24UL * 60 * 60000) {
messages[i].timeoutInterval = timeout;
}
}
}
}
void setup_wifi() {
Serial.print("Connecting to WiFi: ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts++ < 20) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi connection failed!");
}
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
connectivity.hourStartTime = millis();
connectivity.lastConnectAttempt = millis();
connectivity.telegramSent = false;
wifiRetryCount = 0;
mqttRetryCount = 0;
}
void setup() {
Serial.begin(115200);
pinMode(buzzerPin, OUTPUT);
pinMode(muteButtonPin, INPUT_PULLUP);
digitalWrite(buzzerPin, HIGH);
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_16);
display.drawString(18, 0, "JAMESBON");
display.setFont(ArialMT_Plain_24);
display.drawString(6, 20, "SuperVisor");
display.display();
preferences.begin("timeouts", false);
setup_wifi();
mqtt_client.setServer(mqtt_server, mqtt_port);
mqtt_client.setCallback(callback);
setupAPI();
loadTimeoutsFromPreferences();
unsigned long now = millis();
for (int i = 0; i < numMessages; i++) {
messages[i].lastReceived = now;
messages[i].telegramSent = false;
}
lastDisplayUpdate = now;
digitalWrite(buzzerPin, LOW);
Serial.println("=== JAMESBON SUPERVISOR READY ===");
Serial.println("MQTT Topics:");
Serial.println("- Heartbeat: " + String(mqtt_topic));
Serial.println("- Status Request: " + String(status_request_topic));
Serial.println("- Status Response: " + String(status_response_topic));
Serial.println("================================");
}
void loop() {
reconnectWiFi();
reconnectMQTT();
checkConnectivity();
if (mqtt_client.connected()) mqtt_client.loop();
server.handleClient();
unsigned long now = millis();
// Check for new alarms
for (int i = 0; i < numMessages; i++) {
if (now - messages[i].lastReceived > messages[i].timeoutInterval && !messages[i].alertSent) {
messages[i].alertSent = true;
messages[i].newAlert = true;
if (!messages[i].telegramSent) {
String alertMessage = "JAMESBON SUPERVISOR :%0A" + messages[i].code + " - " + messages[i].description + " is OFFLINE!";
sendTelegramAlert(alertMessage);
messages[i].telegramSent = true;
}
isMuted = false;
buzzerActive = true;
digitalWrite(buzzerPin, HIGH);
}
}
// Handle mute button
if (digitalRead(muteButtonPin) == LOW && (now - lastButtonPress) > BUTTON_DEBOUNCE) {
lastButtonPress = now;
if (buzzerActive) {
buzzerActive = false;
isMuted = true;
digitalWrite(buzzerPin, LOW);
Serial.println("Buzzer muted");
} else if (isMuted) {
isMuted = false;
Serial.println("Unmuted");
} else if (connectivity.isOffline) {
Serial.println("Restarting due to offline status");
ESP.restart();
}
}
// Display update logic
if (now - lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL || lastDisplayUpdate == 0) {
display.clear();
if (connectivity.isOffline) {
display.setFont(ArialMT_Plain_16);
display.drawString(24, 0, "STATUS :");
display.setFont(ArialMT_Plain_24);
display.drawString(10, 22, "OFFLINE");
display.display();
} else {
if (currentDisplayIndex < numMessages) {
int i = currentDisplayIndex;
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, messages[i].code);
unsigned long elapsed = (now - messages[i].lastReceived) / 60000;
bool isActive = elapsed < (messages[i].timeoutInterval / 60000);
display.setFont(ArialMT_Plain_10);
display.drawString(0, 20, messages[i].description);
display.setFont(ArialMT_Plain_16);
String timeDisplay;
if (elapsed >= 60) {
unsigned long hours = elapsed / 60;
timeDisplay = "Last : " + String(hours) + "hrs";
} else {
timeDisplay = "Last : " + String(elapsed) + " mnt";
}
display.drawString(0, 40, timeDisplay);
display.drawString(60, 0, String(isActive ? "Online" : "Offline"));
} else {
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "IP Address:");
display.drawString(0, 19, WiFi.localIP().toString());
}
display.display();
currentDisplayIndex++;
if (currentDisplayIndex > numMessages) {
currentDisplayIndex = 0;
}
}
lastDisplayUpdate = now;
}
// Auto-publish status every 30 seconds (optional)
if (now - lastStatusPublish > STATUS_PUBLISH_INTERVAL) {
lastStatusPublish = now;
if (mqtt_client.connected()) {
publishStatusToMQTT();
}
}
}