#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <DHT.h>
#include <WiFi.h>
#include <PubSubClient.h>
// --- WIFI & MQTT CONFIG ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.hivemq.com";
WiFiClient espClient;
PubSubClient client(espClient);
const char* topic_telemetry = "sprint2026/myname/telemetry";
const char* topic_control = "sprint2026/myname/control";
// --- ILI9341 TFT DISPLAY CONFIG ---
#define TFT_CS 5
#define TFT_DC 15
#define TFT_RST 2
#define TFT_SCK 17
#define TFT_MOSI 23
#define TFT_MISO 19
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST, TFT_MISO);
// --- PIN DEFINITIONS ---
#define PIN_VOLT_ADC 34
#define PIN_DHT 4
#define DHTTYPE DHT22
#define PIN_LED_GREEN 25
#define PIN_LED_YELLOW 26
#define PIN_LED_RED 27
#define PIN_BUZZER 14
#define PIN_RELAY_1 32
#define PIN_RELAY_2 33
#define PIN_STEPPER_DIR 19
#define PIN_STEPPER_STEP 18
DHT dht(PIN_DHT, DHTTYPE);
// --- THRESHOLDS & STATES ---
const float VOLT_MIN_LIMIT = 180.0;
const float VOLT_MAX_LIMIT = 250.0;
const float TEMP_WARN_LIMIT = 50.0;
const float TEMP_CRITICAL_LIMIT = 75.0;
unsigned long previousMicros = 0;
const unsigned long stepInterval = 2000;
bool stepperState = LOW;
bool stepperActive = false;
// Trackers initialized to force first render
float lastVoltage = -999.0;
float lastTemp = -999.0;
float lastCurrent = -999.0;
bool lastFault = false;
bool lastFanActive = false;
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
}
void callback(char* topic, byte* payload, unsigned int length) {
// Handle incoming control packets if needed
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
String clientId = "GridNovaESP32-";
clientId += String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("connected");
client.subscribe(topic_control);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_VOLT_ADC, INPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_LED_YELLOW, OUTPUT);
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_RELAY_1, OUTPUT);
pinMode(PIN_RELAY_2, OUTPUT);
pinMode(PIN_STEPPER_DIR, OUTPUT);
pinMode(PIN_STEPPER_STEP, OUTPUT);
dht.begin();
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
tft.begin();
tft.setRotation(0);
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(10, 10);
tft.println(F("GRIDNOVA HUB"));
tft.drawFastHLine(5, 35, 230, ILI9341_WHITE);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(10, 55); tft.print(F("VOLT:"));
tft.setCursor(10, 95); tft.print(F("TEMP:"));
tft.setCursor(10, 135); tft.print(F("L1(Main):"));
tft.setCursor(10, 175); tft.print(F("L2(Fan):"));
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
int adcValue = analogRead(PIN_VOLT_ADC);
float voltage = map(adcValue, 0, 4095, 120, 300);
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (isnan(temperature) || isnan(humidity)) {
temperature = 35.0;
humidity = 50.0;
}
bool voltageFault = false;
digitalWrite(PIN_BUZZER, LOW);
if (voltage < VOLT_MIN_LIMIT) {
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_YELLOW, HIGH);
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_BUZZER, HIGH);
voltageFault = true;
}
else if (voltage > VOLT_MAX_LIMIT) {
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_YELLOW, LOW);
digitalWrite(PIN_LED_RED, HIGH);
digitalWrite(PIN_BUZZER, HIGH);
voltageFault = true;
}
else {
digitalWrite(PIN_LED_GREEN, HIGH);
digitalWrite(PIN_LED_YELLOW, LOW);
digitalWrite(PIN_LED_RED, LOW);
}
if (voltageFault) {
digitalWrite(PIN_RELAY_1, LOW);
digitalWrite(PIN_RELAY_2, LOW);
stepperActive = false;
}
else {
if (temperature >= TEMP_CRITICAL_LIMIT) {
digitalWrite(PIN_RELAY_1, LOW);
digitalWrite(PIN_RELAY_2, HIGH);
stepperActive = true;
}
else if (temperature >= TEMP_WARN_LIMIT) {
digitalWrite(PIN_RELAY_1, HIGH);
digitalWrite(PIN_RELAY_2, HIGH);
stepperActive = true;
}
else if (temperature <= (TEMP_WARN_LIMIT - 2.0)) {
digitalWrite(PIN_RELAY_1, HIGH);
digitalWrite(PIN_RELAY_2, HIGH);
stepperActive = false;
}
}
if (stepperActive) {
unsigned long currentMicros = micros();
if (currentMicros - previousMicros >= stepInterval) {
previousMicros = currentMicros;
digitalWrite(PIN_STEPPER_DIR, HIGH);
stepperState = !stepperState;
digitalWrite(PIN_STEPPER_STEP, stepperState);
}
} else {
digitalWrite(PIN_STEPPER_STEP, LOW);
}
// --- DYNAMIC OHM'S LAW CURRENT & POWER CALCULATIONS ---
float effectiveResistance = 62.8;
float current = 0.0;
if (voltage >= VOLT_MIN_LIMIT && voltage <= VOLT_MAX_LIMIT && !voltageFault) {
current = voltage / effectiveResistance;
}
float power = voltage * current * 0.95;
float frequency = 50.0;
float powerFactor = 0.95;
// --- CONDITIONAL CHANGE-DETECTION TELEMETRY PUBLISH ---
if (abs(voltage - lastVoltage) > 0.5 || abs(temperature - lastTemp) > 0.2 || abs(current - lastCurrent) > 0.05) {
String payload = "{";
payload += "\"voltage\":" + String(voltage, 1) + ",";
payload += "\"current\":" + String(current, 2) + ",";
payload += "\"power\":" + String(power, 1) + ",";
payload += "\"frequency\":" + String(frequency, 1) + ",";
payload += "\"temperature\":" + String(temperature, 1) + ",";
payload += "\"pf\":" + String(powerFactor, 2);
payload += "}";
client.publish(topic_telemetry, payload.c_str());
}
// Update Display & Sync Trackers
updateDisplayDynamic(voltage, temperature, voltageFault, stepperActive);
lastVoltage = voltage;
lastTemp = temperature;
lastCurrent = current;
delay(500);
}
void updateDisplayDynamic(float volt, float temp, bool vFault, bool fanActive) {
tft.setTextSize(2);
if (volt != lastVoltage) {
tft.fillRect(70, 55, 150, 20, ILI9341_BLACK);
tft.setCursor(70, 55);
tft.setTextColor(ILI9341_WHITE);
tft.print(volt, 1); tft.print(F(" V"));
}
if (temp != lastTemp) {
tft.fillRect(70, 95, 150, 20, ILI9341_BLACK);
tft.setCursor(70, 95);
tft.setTextColor(ILI9341_WHITE);
tft.print(temp, 1); tft.print(F(" C"));
}
bool mainShed = (vFault || temp >= TEMP_CRITICAL_LIMIT);
if (mainShed != lastFault) {
tft.fillRect(110, 135, 100, 20, ILI9341_BLACK);
tft.setCursor(110, 135);
if (mainShed) {
tft.setTextColor(ILI9341_RED); tft.print(F("SHED"));
} else {
tft.setTextColor(ILI9341_GREEN); tft.print(F("ON"));
}
lastFault = mainShed;
}
if (fanActive != lastFanActive) {
tft.fillRect(100, 175, 120, 20, ILI9341_BLACK);
tft.setCursor(100, 175);
if (fanActive) {
tft.setTextColor(ILI9341_YELLOW); tft.print(F("ACTIVE"));
} else {
tft.setTextColor(ILI9341_DARKGREY); tft.print(F("OFF"));
}
lastFanActive = fanActive;
}
}