#include <Arduino.h>
#include <Wire.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal_I2C.h>
#include <DHTesp.h>
#include <math.h>
// =====================================================
// PIN DEFINITIONS — MATCHING YOUR diagram.json
// =====================================================
// Potentiometers simulating the three battery-group voltages
#define GROUP1_PIN 32
#define GROUP2_PIN 35
#define GROUP3_PIN 34
// Potentiometer simulating pack current
#define CURRENT_PIN 33
// Six DS18B20 sensors share this OneWire pin
#define ONE_WIRE_PIN 4
// Output devices
#define RELAY_PIN 23
#define NORMAL_LED_PIN 14
#define FAULT_LED_PIN 27
#define BUZZER_PIN 25
// Additional sensors
#define MQ2_DIGITAL_PIN 18
#define DHT22_PIN 19
// =====================================================
// OPERATING LIMITS
// =====================================================
// Battery-group voltage limits
const float MIN_GROUP_VOLTAGE = 3.00;
const float MAX_GROUP_VOLTAGE = 4.20;
const float IMBALANCE_LIMIT = 0.15;
// Simulated current limit
const float OVERCURRENT_LIMIT = 3.00;
// Temperature protection
const float TEMP_WARNING_LIMIT = 45.0;
const float TEMP_CRITICAL_LIMIT = 50.0;
// Ambient-temperature warning for monitoring only
const float AMBIENT_WARNING_LIMIT = 40.0;
// Number of DS18B20 sensors
const int CELL_COUNT = 6;
// Change this to false if the relay operates in reverse
const bool RELAY_ACTIVE_HIGH = true;
// =====================================================
// SENSOR AND DISPLAY OBJECTS
// =====================================================
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature cellTemperatureSensors(&oneWire);
DHTesp ambientSensor;
LiquidCrystal_I2C lcd(0x27, 20, 4);
// =====================================================
// GLOBAL VARIABLES
// =====================================================
DeviceAddress cellSensorAddresses[CELL_COUNT];
bool cellAddressAvailable[CELL_COUNT];
// =====================================================
// HELPER FUNCTIONS
// =====================================================
float mapFloat(
int rawValue,
float outputMinimum,
float outputMaximum
) {
return outputMinimum +
((float)rawValue / 4095.0) *
(outputMaximum - outputMinimum);
}
void setRelay(bool loadEnabled) {
if (RELAY_ACTIVE_HIGH) {
digitalWrite(RELAY_PIN, loadEnabled ? HIGH : LOW);
} else {
digitalWrite(RELAY_PIN, loadEnabled ? LOW : HIGH);
}
}
void setBuzzer(bool enabled) {
if (enabled) {
tone(BUZZER_PIN, 2000);
} else {
noTone(BUZZER_PIN);
}
}
// Always writes exactly 20 LCD characters
void printLCDLine(int row, String message) {
if (message.length() > 20) {
message = message.substring(0, 20);
}
while (message.length() < 20) {
message += " ";
}
lcd.setCursor(0, row);
lcd.print(message);
}
void printDeviceAddress(DeviceAddress address) {
for (uint8_t i = 0; i < 8; i++) {
if (address[i] < 16) {
Serial.print("0");
}
Serial.print(address[i], HEX);
}
}
float findMaximum(const float values[], int count) {
float maximumValue = values[0];
for (int i = 1; i < count; i++) {
if (values[i] > maximumValue) {
maximumValue = values[i];
}
}
return maximumValue;
}
float findMinimum(const float values[], int count) {
float minimumValue = values[0];
for (int i = 1; i < count; i++) {
if (values[i] < minimumValue) {
minimumValue = values[i];
}
}
return minimumValue;
}
float calculateAverage(const float values[], int count) {
float total = 0.0;
int validCount = 0;
for (int i = 0; i < count; i++) {
if (values[i] != DEVICE_DISCONNECTED_C) {
total += values[i];
validCount++;
}
}
if (validCount == 0) {
return DEVICE_DISCONNECTED_C;
}
return total / validCount;
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
analogReadResolution(12);
pinMode(RELAY_PIN, OUTPUT);
pinMode(NORMAL_LED_PIN, OUTPUT);
pinMode(FAULT_LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(MQ2_DIGITAL_PIN, INPUT);
digitalWrite(NORMAL_LED_PIN, LOW);
digitalWrite(FAULT_LED_PIN, LOW);
setRelay(false);
setBuzzer(false);
// Start I2C on the ESP32 default selected pins
Wire.begin(21, 22);
// Start LCD
lcd.init();
lcd.backlight();
// Start six battery-temperature sensors
cellTemperatureSensors.begin();
cellTemperatureSensors.setResolution(12);
// Start ambient temperature/humidity sensor
ambientSensor.setup(DHT22_PIN, DHTesp::DHT22);
int detectedCellSensors =
cellTemperatureSensors.getDeviceCount();
Serial.println();
Serial.println("EV Battery Monitoring System");
Serial.print("DS18B20 sensors detected: ");
Serial.println(detectedCellSensors);
// Read and store each DS18B20 address
for (int i = 0; i < CELL_COUNT; i++) {
cellAddressAvailable[i] =
cellTemperatureSensors.getAddress(
cellSensorAddresses[i],
i
);
Serial.print("Temperature sensor ");
Serial.print(i + 1);
Serial.print(": ");
if (cellAddressAvailable[i]) {
printDeviceAddress(cellSensorAddresses[i]);
Serial.println();
} else {
Serial.println("Not detected");
}
}
printLCDLine(0, "EV Battery Monitor");
printLCDLine(1, "3S2P Simulation");
printLCDLine(
2,
"Cell sensors: " +
String(detectedCellSensors)
);
printLCDLine(3, "Starting system...");
delay(2500);
// CSV header suitable for creating an ML dataset
Serial.println(
"time_s,"
"pack_voltage,"
"group1_voltage,"
"group2_voltage,"
"group3_voltage,"
"current,"
"temp1,"
"temp2,"
"temp3,"
"temp4,"
"temp5,"
"temp6,"
"average_cell_temp,"
"max_cell_temp,"
"min_cell_temp,"
"temp_spread,"
"ambient_temp,"
"ambient_humidity,"
"temperature_rise,"
"voltage_imbalance,"
"power,"
"soc,"
"smoke_detected,"
"relay_enabled,"
"status"
);
}
// =====================================================
// MAIN LOOP
// =====================================================
void loop() {
// ---------------------------------------------------
// READ SIMULATED VOLTAGE AND CURRENT
// ---------------------------------------------------
int rawGroup1 = analogRead(GROUP1_PIN);
int rawGroup2 = analogRead(GROUP2_PIN);
int rawGroup3 = analogRead(GROUP3_PIN);
int rawCurrent = analogRead(CURRENT_PIN);
// Each pot directly represents one parallel battery group
float group1Voltage =
mapFloat(rawGroup1, 2.70, 4.20);
float group2Voltage =
mapFloat(rawGroup2, 2.70, 4.20);
float group3Voltage =
mapFloat(rawGroup3, 2.70, 4.20);
// Potentiometer represents 0–5 A
float current =
mapFloat(rawCurrent, 0.0, 5.0);
// ---------------------------------------------------
// READ SIX BATTERY TEMPERATURE SENSORS
// ---------------------------------------------------
cellTemperatureSensors.requestTemperatures();
float cellTemperatures[CELL_COUNT];
bool cellTemperatureSensorFault = false;
for (int i = 0; i < CELL_COUNT; i++) {
if (cellAddressAvailable[i]) {
cellTemperatures[i] =
cellTemperatureSensors.getTempC(
cellSensorAddresses[i]
);
} else {
cellTemperatures[i] =
DEVICE_DISCONNECTED_C;
}
if (
cellTemperatures[i] ==
DEVICE_DISCONNECTED_C
) {
cellTemperatureSensorFault = true;
}
}
// ---------------------------------------------------
// READ AMBIENT TEMPERATURE AND HUMIDITY
// ---------------------------------------------------
TempAndHumidity ambientData =
ambientSensor.getTempAndHumidity();
float ambientTemperature =
ambientData.temperature;
float ambientHumidity =
ambientData.humidity;
bool ambientSensorFault =
isnan(ambientTemperature) ||
isnan(ambientHumidity);
if (ambientSensorFault) {
ambientTemperature = -127.0;
ambientHumidity = -1.0;
}
// ---------------------------------------------------
// READ SMOKE SENSOR
// ---------------------------------------------------
// Wokwi MQ-2 DOUT becomes LOW when gas exceeds threshold
bool smokeDetected =
digitalRead(MQ2_DIGITAL_PIN) == LOW;
// ---------------------------------------------------
// CALCULATIONS
// ---------------------------------------------------
float groupVoltages[3] = {
group1Voltage,
group2Voltage,
group3Voltage
};
float packVoltage =
group1Voltage +
group2Voltage +
group3Voltage;
float maximumGroupVoltage =
findMaximum(groupVoltages, 3);
float minimumGroupVoltage =
findMinimum(groupVoltages, 3);
float voltageImbalance =
maximumGroupVoltage -
minimumGroupVoltage;
float maximumCellTemperature =
findMaximum(cellTemperatures, CELL_COUNT);
float minimumCellTemperature =
findMinimum(cellTemperatures, CELL_COUNT);
float averageCellTemperature =
calculateAverage(
cellTemperatures,
CELL_COUNT
);
float temperatureSpread =
maximumCellTemperature -
minimumCellTemperature;
float temperatureRise = -127.0;
if (
!ambientSensorFault &&
!cellTemperatureSensorFault
) {
temperatureRise =
maximumCellTemperature -
ambientTemperature;
}
float power =
packVoltage * current;
// Basic voltage-based SOC simulation
float soc =
((packVoltage - 9.0) /
(12.6 - 9.0)) * 100.0;
soc = constrain(soc, 0.0, 100.0);
// ---------------------------------------------------
// SYSTEM STATUS AND PROTECTION LOGIC
// ---------------------------------------------------
String status = "Normal";
bool normalLEDEnabled = true;
bool faultLEDEnabled = false;
bool relayEnabled = true;
bool buzzerEnabled = false;
// Priority 1: Smoke/fire-risk condition
if (smokeDetected) {
status = "Smoke_Detected";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = false;
buzzerEnabled = true;
}
// Priority 2: Missing cell-temperature sensor
else if (cellTemperatureSensorFault) {
status = "Cell_Temp_Fault";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = false;
buzzerEnabled = true;
}
// Priority 3: Extreme cell temperature
else if (
maximumCellTemperature >=
TEMP_CRITICAL_LIMIT
) {
status = "Critical_Overheat";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = false;
buzzerEnabled = true;
}
// Priority 4: Low battery-group voltage
else if (
group1Voltage < MIN_GROUP_VOLTAGE ||
group2Voltage < MIN_GROUP_VOLTAGE ||
group3Voltage < MIN_GROUP_VOLTAGE
) {
status = "Low_Voltage";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = false;
buzzerEnabled = false;
}
// Priority 5: Overcurrent
else if (current > OVERCURRENT_LIMIT) {
status = "Overload";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = false;
buzzerEnabled = false;
}
// Priority 6: Temperature warning
// Relay remains ON, but buzzer warns the user
else if (
maximumCellTemperature >=
TEMP_WARNING_LIMIT
) {
status = "Temp_Warning";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = true;
buzzerEnabled = true;
}
// Priority 7: Cell/group voltage imbalance
else if (
voltageImbalance >
IMBALANCE_LIMIT
) {
status = "Cell_Imbalance";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = true;
buzzerEnabled = false;
}
// Ambient sensor failure does not disconnect battery
else if (ambientSensorFault) {
status = "Ambient_Sensor_Fault";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = true;
buzzerEnabled = false;
}
// High ambient temperature is a warning only
else if (
ambientTemperature >=
AMBIENT_WARNING_LIMIT
) {
status = "High_Ambient_Temp";
normalLEDEnabled = false;
faultLEDEnabled = true;
relayEnabled = true;
buzzerEnabled = false;
}
// ---------------------------------------------------
// OUTPUT CONTROL
// ---------------------------------------------------
digitalWrite(
NORMAL_LED_PIN,
normalLEDEnabled ? HIGH : LOW
);
digitalWrite(
FAULT_LED_PIN,
faultLEDEnabled ? HIGH : LOW
);
setRelay(relayEnabled);
setBuzzer(buzzerEnabled);
// ---------------------------------------------------
// LCD DISPLAY
// ---------------------------------------------------
String lcdLine1 =
"V:" +
String(packVoltage, 2) +
" I:" +
String(current, 2) +
"A";
String lcdLine2 =
"G:" +
String(group1Voltage, 1) +
"," +
String(group2Voltage, 1) +
"," +
String(group3Voltage, 1);
String lcdLine3 =
"Cell:" +
String(maximumCellTemperature, 1) +
" Amb:" +
String(ambientTemperature, 1);
String lcdLine4 =
status +
(relayEnabled ? " R:ON" : " R:OFF");
printLCDLine(0, lcdLine1);
printLCDLine(1, lcdLine2);
printLCDLine(2, lcdLine3);
printLCDLine(3, lcdLine4);
// ---------------------------------------------------
// SERIAL CSV OUTPUT FOR ML DATASET
// ---------------------------------------------------
Serial.print(millis() / 1000);
Serial.print(",");
Serial.print(packVoltage, 2);
Serial.print(",");
Serial.print(group1Voltage, 2);
Serial.print(",");
Serial.print(group2Voltage, 2);
Serial.print(",");
Serial.print(group3Voltage, 2);
Serial.print(",");
Serial.print(current, 2);
Serial.print(",");
for (int i = 0; i < CELL_COUNT; i++) {
Serial.print(cellTemperatures[i], 1);
Serial.print(",");
}
Serial.print(averageCellTemperature, 1);
Serial.print(",");
Serial.print(maximumCellTemperature, 1);
Serial.print(",");
Serial.print(minimumCellTemperature, 1);
Serial.print(",");
Serial.print(temperatureSpread, 1);
Serial.print(",");
Serial.print(ambientTemperature, 1);
Serial.print(",");
Serial.print(ambientHumidity, 1);
Serial.print(",");
Serial.print(temperatureRise, 1);
Serial.print(",");
Serial.print(voltageImbalance, 2);
Serial.print(",");
Serial.print(power, 2);
Serial.print(",");
Serial.print(soc, 1);
Serial.print(",");
Serial.print(smokeDetected ? 1 : 0);
Serial.print(",");
Serial.print(relayEnabled ? 1 : 0);
Serial.print(",");
Serial.println(status);
// DHT22 is read more reliably at a slower interval
delay(2000);
}