#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include "RTClib.h"
#include "DHT.h"
#include <PID_v1.h>
#include <WiFi.h>
#include <HTTPClient.h>
// ==========================================
// CONFIGURATION: TOGGLE SIMULATION MODE HERE
// ==========================================
bool SIMULATION_MODE = true;
// ==========================================
// --- CLOUD API CONFIGURATION ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* apiEndpoint = "https://biohatch.vercel.app/api/telemetry";
const char* apiKey = "7b29e1d8-4f5c-4972-8f6a-847e2f5b3a1c";
const String DEVICE_ID = "BH_UNIZIK_001";
#define DHTPIN 5
#define DHTTYPE DHT22
#define VALVE_PIN 12
#define FAN_PIN 18
#define SERVO_PIN 13
#define RESET_BTN 4
#define LED_BLUE 14
#define LED_RED 27
#define LED_GREEN 26
#define BUILTIN_LED 2
#define GSM_TX 17
#define GSM_RX 16
#define FLAME_PIN 15
// --- PID & THRESHOLDS ---
double Setpoint = 37.5;
double Input, Output;
double Kp = 40, Ki = 0.5, Kd = 120;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
int WindowSize = 5000;
unsigned long windowStartTime;
unsigned long lastSmsTime = 0;
const unsigned long smsCooldown = 180000;
// --- CLOUD SYNC TIMER ---
unsigned long lastCloudSync = 0;
const unsigned long CLOUD_SYNC_INTERVAL = 1000; // Sync every 2 seconds
// --- OBJECTS ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
DHT dht(DHTPIN, DHTTYPE);
RTC_DS1307 rtc;
Servo turner;
// --- VARIABLES ---
int currentDay = 1;
unsigned long lastTurnTime = 0;
unsigned long simulationStartMillis = 0;
bool isLockdown = false;
bool day18AlertSent = false;
bool isHeating = false;
bool gasLeakRisk = false;
float targetHumid = 52.0;
unsigned long heartbeatMillis = 0;
// --- FLAME SAFETY TIMER ---
unsigned long valveOnTime = 0;
const unsigned long flameGracePeriod = 3000;
unsigned long DAY_LENGTH;
unsigned long TURN_INTERVAL;
// --- CUSTOM LCD CHARACTER: FLAME ---
byte flameChar[8] = {
0b00100, 0b00110, 0b01110, 0b01111, 0b11111, 0b11111, 0b01110, 0b00000
};
// --- FEEDBACK & COMMUNICATION FUNCTIONS ---
void blinkSuccess() {
for(int i = 0; i < 2; i++) {
digitalWrite(BUILTIN_LED, HIGH);
delay(100);
digitalWrite(BUILTIN_LED, LOW);
delay(100);
}
}
void sendSMS(String message) {
Serial.println("\n--- GSM DEBUG ---");
Serial2.println("AT+CMGF=1");
delay(200);
Serial2.println("AT+CMGS=\"+2349074234884\"");
delay(200);
Serial2.print(message);
delay(200);
Serial2.write(26);
Serial.println("SMS Sent Successfully");
blinkSuccess();
}
// --- NEW: CLOUD SYNC FUNCTION ---
void syncTelemetryData(float temp, float humid) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(apiEndpoint);
http.addHeader("Content-Type", "application/json");
http.addHeader("x-api-key", apiKey);
// Mocking battery and gas flow for now until actual sensors are wired
float mockBatteryV = gasLeakRisk ? 100.0 : 0.0;
float flameStatusPct = isHeating ? 100.0 : 0.0;
String statusStr = isLockdown ? "HATCHING" : "INCUBATING";
// Build the exact JSON schema the Next.js API expects
String jsonPayload = "{";
jsonPayload += "\"device_id\":\"" + DEVICE_ID + "\",";
jsonPayload += "\"current_day\":" + String(currentDay) + ",";
jsonPayload += "\"water_temp\":" + String(temp, 1) + ",";
jsonPayload += "\"chamber_humid\":" + String(humid, 1) + ",";
jsonPayload += "\"gas_flow_pct\":" + String(flameStatusPct, 1) + ",";
jsonPayload += "\"battery_v\":" + String(mockBatteryV, 1) + ",";
jsonPayload += "\"status\":\"" + statusStr + "\"";
jsonPayload += "}";
int httpResponseCode = http.POST(jsonPayload);
if (httpResponseCode > 0) {
Serial.print("HTTP POST Success, Code: ");
Serial.println(httpResponseCode);
} else {
Serial.print("HTTP POST Failed, Error: ");
Serial.println(http.errorToString(httpResponseCode).c_str());
}
http.end();
} else {
Serial.println("Wi-Fi Disconnected. Skipping cloud sync.");
}
}
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, GSM_RX, GSM_TX);
pinMode(VALVE_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(BUILTIN_LED, OUTPUT);
pinMode(RESET_BTN, INPUT_PULLUP);
pinMode(FLAME_PIN, INPUT);
lcd.init();
lcd.backlight();
delay(300);
lcd.createChar(0, flameChar); // Register flame icon
//lcd.print("BIOHATCH STARTING...");
lcd.setCursor(3,0);
lcd.print("BIOHATCH");
lcd.setCursor(4,1);
lcd.print("STARTING...");
// Connect to Wi-Fi
Serial.print("Connecting to Wi-Fi...");
WiFi.begin(ssid, password);
int wifiAttempts = 0;
while (WiFi.status() != WL_CONNECTED && wifiAttempts < 20) {
delay(500);
Serial.print(".");
wifiAttempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" Connected!");
} else {
Serial.println(" Wi-Fi Failed.");
}
dht.begin();
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
}
windowStartTime = millis();
myPID.SetOutputLimits(0, WindowSize);
myPID.SetMode(AUTOMATIC);
ESP32PWM::allocateTimer(0);
turner.setPeriodHertz(50);
turner.attach(SERVO_PIN, 500, 2400);
if (SIMULATION_MODE) {
DAY_LENGTH = 30000;
TURN_INTERVAL = 5000;
} else {
DAY_LENGTH = 86400000;
TURN_INTERVAL = 14400000;
}
simulationStartMillis = millis();
delay(500);
lcd.clear();
}
void loop() {
// --- WIFI AUTO RECONNECT ---
if (WiFi.status() != WL_CONNECTED) {
WiFi.begin(ssid, password);
}
DateTime now = rtc.now();
// HW-072 logic: LOW = Flame Detected
isHeating = (digitalRead(FLAME_PIN) == LOW);
if (isHeating) {
Serial.println("Flame Detected...");
}
// --- FLAME GRACE PERIOD LOGIC ---
if (digitalRead(VALVE_PIN) == HIGH) {
if (valveOnTime == 0) {
valveOnTime = millis();
}
if ((millis() - valveOnTime > flameGracePeriod) && !isHeating) {
gasLeakRisk = true;
} else {
gasLeakRisk = false;
}
} else {
valveOnTime = 0;
gasLeakRisk = false;
}
// 1. HEARTBEAT
if (millis() - heartbeatMillis >= 500) {
digitalWrite(LED_GREEN, !digitalRead(LED_GREEN));
heartbeatMillis = millis();
}
// 2. DAY CALCULATION
currentDay = ((millis() - simulationStartMillis) / DAY_LENGTH) + 1;
float temp = dht.readTemperature();
float h = dht.readHumidity();
// --- DHT VALIDATION ---
if (isnan(temp) || isnan(h)) {
Serial.println("DHT Read Failed");
return;
}
Input = temp;
// 3. CLOUD SYNC (Non-blocking check)
if (millis() - lastCloudSync >= CLOUD_SYNC_INTERVAL) {
syncTelemetryData(Input, h);
lastCloudSync = millis();
}
// 4. PID THERMAL CONTROL
myPID.Compute();
if (millis() - windowStartTime > WindowSize) windowStartTime += WindowSize;
if (Output > millis() - windowStartTime) digitalWrite(VALVE_PIN, HIGH);
else digitalWrite(VALVE_PIN, LOW);
// 5. STAGE LOGIC
if (currentDay >= 18) {
isLockdown = true;
targetHumid = 68.0;
if (!day18AlertSent) {
sendSMS("BioHatch: Day 18 Lockdown. Humid: 68%");
day18AlertSent = true;
}
}
// 6. HUMIDITY CONTROL
if (h > targetHumid + 3) {
digitalWrite(FAN_PIN, HIGH);
digitalWrite(LED_BLUE, HIGH);
} else {
digitalWrite(FAN_PIN, LOW);
digitalWrite(LED_BLUE, LOW);
}
// 7. EGG TURNING
if (!isLockdown) {
if (millis() - lastTurnTime > TURN_INTERVAL) {
static bool side = false;
turner.write(side ? 45 : 135);
side = !side;
lastTurnTime = millis();
blinkSuccess();
}
} else {
turner.write(90);
}
// 8. CRITICAL ALERTS
if ((Input > 39.0 || isnan(Input) || gasLeakRisk)) {
digitalWrite(LED_RED, HIGH);
if (millis() - lastSmsTime > smsCooldown) {
String msg = gasLeakRisk ?
"SAFETY ALERT: Valve Open, NO FLAME!" :
"ALERT: High Temperature " + String(Input) + "C";
sendSMS(msg);
lastSmsTime = millis();
}
} else {
digitalWrite(LED_RED, LOW);
}
// 9. LCD DISPLAY UPDATE
lcd.setCursor(0, 0);
lcd.print("D:"); lcd.print(currentDay);
lcd.print(" "); lcd.print(Input, 1); lcd.print("C ");
if(now.hour() < 10) lcd.print('0');
lcd.print(now.hour());
lcd.print(':');
if(now.minute() < 10) lcd.print('0');
lcd.print(now.minute()); lcd.print(' ');
lcd.setCursor(0, 1);
lcd.print("H:"); lcd.print(h, 0); lcd.print("% ");
if(isHeating) lcd.write(0); else lcd.print(" ");
lcd.print(isLockdown ? " HATCHING" : " TURNING ");
// 10. RESET BUTTON
if (digitalRead(RESET_BTN) == LOW) {
delay(200); // debounce
if (digitalRead(RESET_BTN) == LOW) {
simulationStartMillis = millis();
day18AlertSent = false;
currentDay = 1;
isLockdown = false;
targetHumid = 52.0;
lcd.clear();
lcd.print("RESTARTING...");
blinkSuccess();
delay(1000);
}
}
}