first commit

This commit is contained in:
developer 2026-07-28 13:13:00 +07:00
commit 46b505af15
10 changed files with 3453 additions and 0 deletions

120
battLib.h Normal file
View File

@ -0,0 +1,120 @@
//LiFePO4 4S battery soc and level calculation library
//todo:
//- battery level will forced after 60-seconds if not passing battPercentageLevelThreshold (optional)
static uint8_t battPercentageLevelThreshold = 5; //threshold for switch discrete level 0-4
static uint8_t battLastLevel = 0;
static uint8_t battLastPercentageForLevel = 0;
//variables
static float battVoltageFullChargeMinimal = 14.4; //used for longevity charge
static float battVoltageFullChargeMaximal = 14.6; //3.60V - 3.65V * 4S = 14.4V - 14.6V (can be used for "topped-off" charge mode, )
static float battVoltageFullRest = 13.6; //~3.40V - 3.50V * 4S = ~13.6V - 13.7V (can be used for "optimal" charge mode, preferred for longevity)
static float battVoltageUpperZone = 13.3; //Also can be used for "Storage/Long-term Maintenance" charge mode, or after long usage without load ~13.2V - 13.3V (50-60% state of charge)
static float battVoltageLowerZone = 12.9;
//float battVoltageHalf = 13.1; //~3.25V - 3.28V * 4S = ~13.0V - 13.1V, known as nomimal
static float battVoltageEmpty = 10.0; //~2.50V - 2.80V * 4S = ~10.0V - 11.2V
static float battVoltageCritical = 12.0; //Default ~10% => 3.0V * 4S = 12V
static float battVoltageForceShutdown = 11.2; //Default ~5% => 2.8V * 4S = 11.2V
static float battVoltageResumeChargingDefault = 13.4; //Default ~90% = ~3.35V *4S = 13.4V
//error condition
static float battOverVoltage = 15.3; //3.75V - 3.80V * 4S = 15V - 15.3V
static float battVoltageDetected = 8;
static float battVoltageNotDetected = 7;
static void resetBatteryLevel() {
battLastLevel = 0;
battLastPercentageForLevel = 0;
}
static void setBatteryPercentage(uint8_t soc) {
if (soc <= 0) {
battLastLevel = 0;
battLastPercentageForLevel = 0;
return;
} else if (soc >= 100) {
battLastLevel = 4;
battLastPercentageForLevel = 100;
return;
}
if (soc > battLastPercentageForLevel) {
//charging
if (soc - battLastPercentageForLevel > battPercentageLevelThreshold) {
battLastPercentageForLevel = soc;
if (battLastPercentageForLevel > 75) {
battLastLevel = 4;
} else if (battLastPercentageForLevel > 50) {
battLastLevel = 3;
} else if (battLastPercentageForLevel > 25) {
battLastLevel = 2;
} else if (battLastPercentageForLevel > 10) {
battLastLevel = 1;
} else {
battLastLevel = 0;
}
}
} else {
//depleted or equal
if (battLastPercentageForLevel - soc > battPercentageLevelThreshold) {
battLastPercentageForLevel = soc;
if (battLastPercentageForLevel < 10) {
battLastLevel = 0;
} else if (battLastPercentageForLevel < 25) {
battLastLevel = 1;
} else if (battLastPercentageForLevel < 50) {
battLastLevel = 2;
} else if (battLastPercentageForLevel < 75) {
battLastLevel = 3;
} else {
battLastLevel = 4;
}
}
}
}
static uint8_t getBatteryPercentage(float voltage) {
float soc = 0;
if (voltage > battVoltageUpperZone) {
soc = 90.0 + (10.0 * ((voltage - battVoltageUpperZone) / (battVoltageFullRest - battVoltageUpperZone)));
if (soc > 100) soc = 100;
} else if (voltage > battVoltageLowerZone && voltage <= battVoltageUpperZone) {
soc = 20.0 + (70.0 * ((voltage - battVoltageLowerZone) / (battVoltageUpperZone - battVoltageLowerZone)));
} else if (voltage > battVoltageEmpty) {
soc = (20.0 * ((voltage - battVoltageEmpty) / (battVoltageLowerZone - battVoltageEmpty)));
}
setBatteryPercentage(soc);
return (uint8_t) soc;
}
static uint8_t getRawBatteryLevelFromPercentage(uint8_t soc) {
if (soc <= 0) {
return 0;
} else if (soc >= 100) {
return 4;
}
if (soc > 75) {
return 4;
} else if (soc > 50) {
return 3;
} else if (soc > 25) {
return 2;
} else if (soc > 5) {
return 1;
} else {
return 0;
}
return 0;
}
static uint8_t getBatteryLevel() {
return battLastLevel;
}
static uint8_t getBatteryLevel(float voltage) {
getBatteryPercentage(voltage);
return battLastLevel;
}

277
bitmap.h Normal file
View File

@ -0,0 +1,277 @@
static const unsigned char PROGMEM splash_logo_bitmap[512] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0xE0,0xFF,0xE7,0x01,0x80,0xEF,0x03,0xF0,0x39,0xE0,0xFF,0x07,0x00,0x00,
0x00,0x00,0xF8,0xFF,0xEF,0x03,0x80,0xC7,0x07,0xF8,0x78,0xF8,0xFF,0x1F,0x00,0x00,
0x00,0x00,0xFC,0xFF,0xCF,0x03,0xC0,0x87,0x0F,0x7C,0x78,0xFC,0xFF,0x3F,0x00,0x00,
0x00,0x00,0xFC,0xFF,0xC7,0x07,0xC0,0x83,0x1F,0x3E,0x78,0xFC,0xFF,0x3F,0x00,0x00,
0x00,0x00,0x3E,0x00,0x80,0x07,0xE0,0x03,0x1F,0x1F,0x78,0x3E,0x00,0x3C,0x00,0x00,
0x00,0x00,0x1E,0x00,0x80,0x0F,0xF0,0x01,0xBE,0x0F,0x78,0x1E,0x00,0x3C,0x00,0x00,
0x00,0x00,0x1E,0x00,0x00,0x0F,0xF0,0x00,0xFC,0x07,0x78,0x1E,0x00,0x7C,0x00,0x00,
0x00,0x00,0xFE,0xFF,0x07,0x1F,0xF8,0x00,0xF8,0x03,0x78,0x1E,0x00,0x7C,0x00,0x00,
0x00,0x00,0xFE,0xFF,0x07,0x1E,0x78,0x00,0xF0,0x03,0x78,0x1E,0x00,0x7C,0x00,0x00,
0x00,0x00,0xFE,0xFF,0x07,0x3E,0x7C,0x00,0xF8,0x03,0x78,0x1E,0x00,0x7C,0x00,0x00,
0x00,0x00,0x1E,0x00,0x00,0x3C,0x3C,0x00,0xFC,0x07,0x78,0x1E,0x00,0x7C,0x00,0x00,
0x00,0x00,0x1E,0x00,0x00,0x78,0x3E,0x00,0xBE,0x0F,0x78,0x1E,0x00,0x3C,0x00,0x00,
0x00,0x00,0x3E,0x00,0x00,0xF8,0x1E,0x00,0x1F,0x1F,0x78,0x3E,0x00,0x3C,0x00,0x00,
0x00,0x00,0xFC,0xFF,0x07,0xF0,0x1F,0x80,0x0F,0x3E,0x78,0xFC,0xFF,0x3F,0x00,0x00,
0x00,0x00,0xFC,0xFF,0x07,0xF0,0x0F,0xC0,0x07,0x7C,0x78,0xFC,0xFF,0x3F,0x00,0x00,
0x00,0x00,0xF8,0xFF,0x0F,0xE0,0x07,0xE0,0x03,0xF8,0x78,0xF8,0xFF,0x1F,0x00,0x00,
0x00,0x00,0xF0,0xFF,0x07,0xC0,0x07,0xF0,0x01,0xF0,0x79,0xF0,0xFF,0x07,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xC0,0x9F,0x83,0x83,0xC1,0x3F,0xFF,0xC1,0x61,0xFE,0xFE,0x83,0x81,0xFC,0xF1,0x07,
0xE0,0xDF,0xC3,0xC3,0xC3,0x7F,0xFF,0xE1,0x61,0xFF,0xFF,0x83,0xC1,0xFE,0xFB,0x07,
0x60,0xC0,0xC7,0xC3,0xC7,0x60,0x18,0xE0,0x63,0x03,0x60,0x80,0xC1,0x0E,0x1B,0x00,
0xE0,0xCF,0xC6,0x63,0xC6,0x60,0x18,0x60,0x67,0xFF,0x60,0x80,0xC1,0x0E,0xFB,0x03,
0xE0,0xDF,0x6E,0x63,0xCC,0x7F,0x18,0x60,0x6E,0xFF,0x60,0x80,0xC1,0xFE,0xF3,0x0F,
0x00,0xD8,0x6C,0xF3,0xCF,0x3F,0x18,0x60,0x7C,0x03,0x60,0x80,0xC1,0xFE,0x01,0x0C,
0xE0,0xDF,0x3C,0xFB,0xDF,0x30,0x18,0x60,0x78,0xFF,0x60,0x80,0xFF,0x0E,0xF8,0x0F,
0xE0,0xDF,0x38,0x1B,0xD8,0x60,0x18,0x60,0x70,0xFE,0x61,0x00,0xFF,0x0E,0xF8,0x07,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};
static const unsigned char PROGMEM splash_logo_bitmap_inverted[512] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0x1F,0x00,0x18,0xFE,0x7F,0x10,0xFC,0x0F,0xC6,0x1F,0x00,0xF8,0xFF,0xFF,
0xFF,0xFF,0x07,0x00,0x10,0xFC,0x7F,0x38,0xF8,0x07,0x87,0x07,0x00,0xE0,0xFF,0xFF,
0xFF,0xFF,0x03,0x00,0x30,0xFC,0x3F,0x78,0xF0,0x83,0x87,0x03,0x00,0xC0,0xFF,0xFF,
0xFF,0xFF,0x03,0x00,0x38,0xF8,0x3F,0x7C,0xE0,0xC1,0x87,0x03,0x00,0xC0,0xFF,0xFF,
0xFF,0xFF,0xC1,0xFF,0x7F,0xF8,0x1F,0xFC,0xE0,0xE0,0x87,0xC1,0xFF,0xC3,0xFF,0xFF,
0xFF,0xFF,0xE1,0xFF,0x7F,0xF0,0x0F,0xFE,0x41,0xF0,0x87,0xE1,0xFF,0xC3,0xFF,0xFF,
0xFF,0xFF,0xE1,0xFF,0xFF,0xF0,0x0F,0xFF,0x03,0xF8,0x87,0xE1,0xFF,0x83,0xFF,0xFF,
0xFF,0xFF,0x01,0x00,0xF8,0xE0,0x07,0xFF,0x07,0xFC,0x87,0xE1,0xFF,0x83,0xFF,0xFF,
0xFF,0xFF,0x01,0x00,0xF8,0xE1,0x87,0xFF,0x0F,0xFC,0x87,0xE1,0xFF,0x83,0xFF,0xFF,
0xFF,0xFF,0x01,0x00,0xF8,0xC1,0x83,0xFF,0x07,0xFC,0x87,0xE1,0xFF,0x83,0xFF,0xFF,
0xFF,0xFF,0xE1,0xFF,0xFF,0xC3,0xC3,0xFF,0x03,0xF8,0x87,0xE1,0xFF,0x83,0xFF,0xFF,
0xFF,0xFF,0xE1,0xFF,0xFF,0x87,0xC1,0xFF,0x41,0xF0,0x87,0xE1,0xFF,0xC3,0xFF,0xFF,
0xFF,0xFF,0xC1,0xFF,0xFF,0x07,0xE1,0xFF,0xE0,0xE0,0x87,0xC1,0xFF,0xC3,0xFF,0xFF,
0xFF,0xFF,0x03,0x00,0xF8,0x0F,0xE0,0x7F,0xF0,0xC1,0x87,0x03,0x00,0xC0,0xFF,0xFF,
0xFF,0xFF,0x03,0x00,0xF8,0x0F,0xF0,0x3F,0xF8,0x83,0x87,0x03,0x00,0xC0,0xFF,0xFF,
0xFF,0xFF,0x07,0x00,0xF0,0x1F,0xF8,0x1F,0xFC,0x07,0x87,0x07,0x00,0xE0,0xFF,0xFF,
0xFF,0xFF,0x0F,0x00,0xF8,0x3F,0xF8,0x0F,0xFE,0x0F,0x86,0x0F,0x00,0xF8,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x3F,0x60,0x7C,0x7C,0x3E,0xC0,0x00,0x3E,0x9E,0x01,0x01,0x7C,0x7E,0x03,0x0E,0xF8,
0x1F,0x20,0x3C,0x3C,0x3C,0x80,0x00,0x1E,0x9E,0x00,0x00,0x7C,0x3E,0x01,0x04,0xF8,
0x9F,0x3F,0x38,0x3C,0x38,0x9F,0xE7,0x1F,0x9C,0xFC,0x9F,0x7F,0x3E,0xF1,0xE4,0xFF,
0x1F,0x30,0x39,0x9C,0x39,0x9F,0xE7,0x9F,0x98,0x00,0x9F,0x7F,0x3E,0xF1,0x04,0xFC,
0x1F,0x20,0x91,0x9C,0x33,0x80,0xE7,0x9F,0x91,0x00,0x9F,0x7F,0x3E,0x01,0x0C,0xF0,
0xFF,0x27,0x93,0x0C,0x30,0xC0,0xE7,0x9F,0x83,0xFC,0x9F,0x7F,0x3E,0x01,0xFE,0xF3,
0x1F,0x20,0xC3,0x04,0x20,0xCF,0xE7,0x9F,0x87,0x00,0x9F,0x7F,0x00,0xF1,0x07,0xF0,
0x1F,0x20,0xC7,0xE4,0x27,0x9F,0xE7,0x9F,0x8F,0x01,0x9E,0xFF,0x00,0xF1,0x07,0xF8,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
};
static const unsigned char PROGMEM active_data_2x4_in_bitmap[4] = {
0x02,
0x02,
0x03,
0x02
};
static const unsigned char PROGMEM active_data_2x4_out_bitmap[4] = {
0x01,
0x03,
0x01,
0x01
};
static const unsigned char PROGMEM power_bitmap[32] = {
0x80,0x01,
0x80,0x01,
0x90,0x09,
0xB8,0x1D,
0x9C,0x39,
0x8E,0x71,
0x86,0x61,
0x86,0x61,
0x86,0x61,
0x06,0x60,
0x06,0x60,
0x0E,0x70,
0x1C,0x38,
0x78,0x1E,
0xF0,0x0F,
0xE0,0x07
};
static const unsigned char PROGMEM wifi2_lv1_bitmap[32] = {
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x80,0x01,
0xE0,0x07,
0x70,0x0E,
0x00,0x00,
0x80,0x01,
0x80,0x01,
0x00,0x00,
0x00,0x00
};
static const unsigned char PROGMEM wifi2_lv2_bitmap[32] = {
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0x00,0x00,
0xC0,0x03,
0xF0,0x0F,
0x3C,0x3C,
0x8C,0x31,
0xE0,0x07,
0x70,0x0E,
0x00,0x00,
0x80,0x01,
0x80,0x01,
0x00,0x00,
0x00,0x00
};
static const unsigned char PROGMEM wifi2_lv3_bitmap[32] = {
0x00,0x00,
0x00,0x00,
0xF0,0x0F,
0xFC,0x3F,
0x1E,0x78,
0xC7,0xE3,
0xF0,0x0F,
0x3C,0x3C,
0x8C,0x31,
0xE0,0x07,
0x70,0x0E,
0x00,0x00,
0x80,0x01,
0x80,0x01,
0x00,0x00,
0x00,0x00
};
static const unsigned char PROGMEM bolt_16x16_bitmap[32] = {
0x00,0x00,
0x00,0x01,
0x00,0x01,
0x80,0x01,
0xC0,0x01,
0xC0,0x01,
0xE0,0x1F,
0xE0,0x0F,
0xF0,0x0F,
0xF8,0x07,
0x80,0x03,
0x80,0x03,
0x80,0x01,
0x80,0x01,
0x80,0x00,
0x00,0x00
};
static const unsigned char PROGMEM charging_7x8_bitmap[8] = {
0x3E,
0x41,
0x5D,
0x45,
0x45,
0x5D,
0x41,
0x3E
};
static const unsigned char PROGMEM charge_fast_7x8_bitmap[8] = {
0x3E,
0x7F,
0x63,
0x7B,
0x7B,
0x63,
0x7F,
0x3E
};
static const unsigned char PROGMEM warn_16x16_bitmap[32] = {
0x00,0x00,
0x80,0x01,
0xC0,0x03,
0xC0,0x07,
0xE0,0x07,
0x70,0x0E,
0x70,0x0E,
0x78,0x1E,
0x78,0x1E,
0x7C,0x3E,
0xFC,0x3F,
0x7E,0x7E,
0x7E,0x7E,
0xFF,0xFF,
0xFF,0xFF,
0xFF,0xFF
};
static const unsigned char PROGMEM error_16x16_bitmap[32] = {
0xF0,0x0F,
0xF8,0x1F,
0xFC,0x3F,
0x7E,0x7E,
0x7F,0xFE,
0x7F,0xFE,
0x7F,0xFE,
0x7F,0xFE,
0x7F,0xFE,
0x7F,0xFE,
0xFF,0xFF,
0x7F,0xFE,
0x7E,0x7E,
0xFC,0x3F,
0xF8,0x1F,
0xF0,0x0F
};
static const unsigned char PROGMEM fatal_16x16_bitmap[32] = {
0xF0,0x0F,
0xF8,0x1F,
0xFC,0x3F,
0xFE,0x7F,
0xDF,0xFB,
0x8F,0xF1,
0x1F,0xF8,
0x3F,0xFC,
0x3F,0xFC,
0x1F,0xF8,
0x8F,0xF1,
0xDF,0xFB,
0xFE,0x7F,
0xFC,0x3F,
0xF8,0x1F,
0xF0,0x0F
};
static const unsigned char PROGMEM timer_16x16_bitmap[32] = {
0x80,0x01,
0x80,0x19,
0x00,0x38,
0x08,0x70,
0x10,0x60,
0x73,0xC0,
0xE3,0xC0,
0xC3,0xC1,
0x83,0xC1,
0x03,0xC0,
0x03,0xC0,
0x06,0x60,
0x06,0x60,
0x1C,0x38,
0xF8,0x1F,
0xE0,0x07
};

BIN
data/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

743
data/index.html Normal file
View File

@ -0,0 +1,743 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPS Dashboard</title>
<link rel="icon" href="favicon.ico">
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
margin-top: -20px;
background-color: #121212; /* Dark background */
color: #e0e0e0; /* Light text color */
}
hr {
margin-bottom: 20px;
border-color: #333; /* Darker border color */
}
h1 {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
color: #ffffff; /* White text for headings */
}
.form-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
}
button {
background-color: #333; /* Dark button background */
color: #e0e0e0; /* Light text color */
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background-color: #444; /* Slightly lighter on hover */
}
button.disabled {
background-color: #0046a3; /* Gray color for disabled button */
color: #a0a0a0;
cursor: default;
pointer-events: none; /* Prevents hover effect */
transition: none; /* Removes transition effect on hover */
}
button[disabled] {
background-color: #333; /* Gray color for disabled button */
color: #555;
cursor: default;
pointer-events: none; /* Prevents hover effect */
transition: none; /* Removes transition effect on hover */
}
button[disabled]:hover {
background-color: #333;
}
label {
color: #e0e0e0; /* Light text color for labels */
}
/* for styled on off using checkbox */
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .2s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 17px;
width: 17px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .2s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #0078d7;
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Disabled state */
input:disabled + .slider {
background-color: #e6e6e6;
cursor: default;
pointer-events: none; /* Prevents hover effect */
}
input:disabled:checked + .slider {
background-color: #a0d1f6;
}
input:disabled + .slider:before {
background-color: #f5f5f5;
}
/* Optional: Add focus style */
input:focus + .slider {
box-shadow: 0 0 1px #0078d7;
}
/* Selec/dropdown style */
select {
/* width: calc(100% - 22px); */
background-color: #333333;
border: none;
color: #ffffff;
font-size: 16px;
border-radius: 5px;
box-sizing: border-box;
margin-right: 5px;
}
</style>
</head>
<body>
<h1>
<label>Smart Network Controlled UPS V1.1</label>
<!-- ⭕Hollow Red Circle⚫Black Circle⚪White Circle · 🟢Green Circle🔵Blue Circle🟡Yellow Circle🟠Orange Circle🟤Brown Circle 🔴Red Circle 🟣 Purple Circle -->
<label id="connect-state-label" title="Unavailable"></label>
</h1>
<hr>
<h1>Power Control:</h1>
<div class="form-group">
<label>Auto ON</label>
<!-- <button id="toggleAutoBtn"
onclick="toggleAutoOn()"
style="position: absolute;
left: 140px;width:50px;">OFF</button> -->
<select id="auto-mode" style="position: absolute; left: 150px;width: 150px;">
<option value="0">Disabled</option>
<option value="1">Line Powered</option>
<option value="2">Persistent on Line</option>
</select>
</div>
<!-- <div class="form-group">
<label>State</label>
<button id="switchBtn"
onclick="toggleSwitch()"
style="position: absolute;
left: 140px;width:50px;">OFF</button>
</div> -->
<div class="form-group">
<label>Power</label>
<label class="toggle-switch" style="position: absolute; left: 150px;">
<input type="checkbox" id="switchToggle" disabled >
<span class="slider"></span>
</label>
</div>
<div class="form-group">
<label>Shutdown Cast</label>
<select id="shutcast-mode" style="position: absolute; left: 150px;width: 150px;">
<option value="0">Disabled</option>
<option value="1">On Battery</option>
<option value="2">≤75% Battery</option>
<option value="3">≤50% Battery</option>
<option value="4">≤25% Battery</option>
<option value="5">Battery Critical</option>
</select>
<label id="shutdown-cast-label" style="position: absolute; left: 310px;width: 50px;"> </label>
</div>
<div class="form-group">
<div class="form-group">
<div class="container">
<label style="position: absolute; align-items: center;" title="RestAPI: htttp://<IP>/state?shutdown=<seconds>" >Shutdown</label>
<label id="shutdown-countdown" style="position: absolute; left: 150px; width: 150px;">999</label>
</div>
<div class="container" id="shutdown-form" style="position: absolute; left: 150px; align-items: center;">
<input type="number" id="shut-timer-input" placeholder="0-999" value="120" min="10" max="999" step="1" style="position: absolute; left: 0px; width: 50px; align-items: center;" maxlength="3">
<label style="position: absolute; left: 65px; width: 50px; align-items: center;">sec(s)</label>
<button id="shutdownBtn"
onclick="shutdownTriggered()"
style="position: absolute;
left: 120px;width:64px; align-items:center; padding: 0px 0px; height: 22px; margin: -1px -0px">START</button>
</div>
<!-- <div class="container" id="shutdown-countdown" style="position: absolute; left: 150px; align-items: center;">
<label style="position: absolute; left: 0px; width: 50px;"></label>
</div> -->
</div>
</div>
<hr>
<h1>Charge Control:</h1>
<div class="form-group">
<label>Charge Mode</label>
<select id="charge-mode" style="position: absolute; left: 150px;width: 150px;">
<option value="0">Always Charge</option>
<option value="1">90-100%</option>
<option value="2">85-100%</option>
<option value="3">80-100%</option>
<option value="4">90-95%</option>
<option value="5">85-95%</option>
<option value="6">80-95%</option>
<option value="7">85-90%</option>
<option value="8">80-90%</option>
<option value="9">80-85%</option>
</select>
</div>
<div class="form-group">
<label title="Automatic Voltage Reducer">AVR</label>
<label class="toggle-switch" style="position: absolute; left: 150px;">
<input type="checkbox" id="switchToggleAutoReducer" disabled >
<span class="slider"></span>
</label>
</div>
<div class="form-group">
<label title="Overvoltage Protection">OVP</label>
<label class="toggle-switch" style="position: absolute; left: 150px;">
<input type="checkbox" id="switchToggleOVP" disabled >
<span class="slider"></span>
</label>
</div>
<div class="form-group">
<label title="Full Charge on Power Lost">FCoPL</label>
<select id="fcopl-mode" style="position: absolute; left: 150px;width: 150px;">
<option value="0">Disabled</option>
<option value="1">Always</option>
<option value="2">At Battery ≤95%</option>
<option value="3">At Battery ≤90%</option>
<option value="4">At Battery ≤85%</option>
<option value="5">At Battery ≤80%</option>
</select>
<button id="resetFCoPLBtn"
onclick="resetFCoPLTriggered()"
style="position: absolute;
left: 310px;width:64px; align-items:center; padding: 0px 0px; height: 22px; margin: -1px -0px">Reset</button>
</div>
<hr>
<h1>Monitor:</h1>
<div class="form-group">
<label>Status</label>
<label id="status-label"
style="position: absolute;
left: 150px;width:400px;"
>-</label>
</div>
<div class="form-group">
<label>Power Source</label>
<label id="power-label"
style="position: absolute;
left: 150px;width:400px;"
>-</label>
</div>
<div class="form-group">
<label>Battery Voltage</label>
<label id="input-label"
style="position: absolute;
left: 150px;width:100px;"
>-</label>
</div>
<div class="form-group">
<label>Battery Level</label>
<label id="batt-label"
style="position: absolute;
left: 150px;width:400px;"
>⬛⬛⬛⬛</label>
</div>
<script>
// ⬛ 🟥 (red square), 🟦 (blue square), 🟩 (green square), 🟨 (yellow square), 🟧 (orange square), and 🟫 (brown square)
// ◼️ Black Medium Square: A medium-sized black square.
// ⬛ Black Large Square: A large black square.
// ◾ Black Medium-Small Square: A smaller black square.
// ▪️ Black Small Square: The smallest size of a black square.
// 🔲 Black Square Button: A square version of the radio button emoji
var labelStatus = document.getElementById('status-label');
var labelPower = document.getElementById('power-label');
var labelInput = document.getElementById('input-label');
var labelBatt = document.getElementById('batt-label');
var dropdownAutoOnSelect = document.getElementById('auto-mode');
var toggleSwitch = document.getElementById('switchToggle');
var shutTimerInput = document.getElementById('shut-timer-input');
var shutdownForm = document.getElementById('shutdown-form');
var shutTimer = document.getElementById('shutdown-countdown');
var dropdownShutLevelSelect = document.getElementById('shutcast-mode');
var shutFlagLabel = document.getElementById('shutdown-cast-label');
var connectStateLabel = document.getElementById('connect-state-label');
var dropdownChargeModeSelect = document.getElementById('charge-mode');
var dropdownFullChargeOnPowerLostModeSelect = document.getElementById('fcopl-mode');
var btnResetFCOPL = document.getElementById('resetFCoPLBtn');
var toggleAutomaticVoltageReducer = document.getElementById('switchToggleAutoReducer');
var toggleOvervoltageProtection = document.getElementById('switchToggleOVP');
var lastConnectTick = 0;
var autoOnMode = 0;
var stateTarget = false;
var stateValue = false;
var lineFrom = 0; //0 = None, 1 = AC, 2 = Battery
var onChange = false;
var checkRefresh;
var battLevel = 0;
var battMaxLevel = 0;
var battPercentage = 0;
var isChargingOrBlink = false;
var chargeLevel = 0;
var isBattCritical = false;
var chargeTickTock = false;
var timedShutdown = false;
var timedShutRemaining = 0;
var everReceiveStat = false;
var shutdownSuggestionLevel = 0;
//var shutdownSuggestionLevelPrev = 0;
var shutdownCastFlag = false;
var chargeStatus = 0;
var chargingState = 0;
var chargeMode = 0;
var chargeAutoVoltageReducer = false;
var chargeOvervoltageProtection = false;
var fullChargeOnPowerLostMode = 0;
var fullChargeOnPowerLostState = false;
var targetChargeMode = 0;
var targetFCOPLMode = 0;
var targetOvervoltageProtection = false;
var targetAutomaticVoltageReducer = false;
function repeatChar(char, times) {
return char.repeat(times);
}
// Disable all elements in shutdown-form
function disableShutdownForm() {
const form = document.getElementById('shutdown-form');
const elements = form.querySelectorAll('input, button, select, textarea');
elements.forEach(element => {
element.disabled = true;
});
//const btn = document.getElementById('shutdownBtn');
//btn.disabled = true;
}
// Enable all elements in shutdown-form
function enableShutdownForm() {
const form = document.getElementById('shutdown-form');
const elements = form.querySelectorAll('input, button, select, textarea');
elements.forEach(element => {
element.disabled = false;
});
//const btn = document.getElementById('shutdownBtn');
//btn.disabled = false;
}
dropdownShutLevelSelect.addEventListener('change', function() {
let targetShutdownLvMode = parseInt(this.value);
let s = 'shutdown suggest cast: ' + targetShutdownLvMode;
console.log(s);
if (targetShutdownLvMode!==shutdownSuggestionLevel){
this.disabled = true;
SendState('shutlv', targetShutdownLvMode);
}
});
dropdownAutoOnSelect.addEventListener('change', function() {
let targetAutoMode = parseInt(this.value);
let s = 'auto on mode: ' + targetAutoMode;
console.log(s);
if (targetAutoMode!==autoOnMode){
this.disabled = true;
SendState('auto', targetAutoMode);
}
});
toggleSwitch.addEventListener('change', function() {
let targetState = toggleSwitch.checked;
let s = 'Toggle switched: ' + (targetState?'ON':'OFF');
console.log(s);
if (targetState!==stateValue){
//Disabled Button
toggleSwitch.disabled = true;
SendState('on', targetState?'1':'0');
}
});
function shutdownTriggered(){
//shutdownForm.style.display = 'none';
let t = parseInt(shutTimerInput.value);
console.log('Send timed shutdown: ' + t);
if (stateValue){
//url: htttp://<IP>/state?shutdown=<seconds>
SendState('shutdown', t);
}
}
function ConnectionRefresh(){
lastConnectTick = performance.now();
connectStateLabel.textContent = ' 🟢';
connectStateLabel.title = 'Connected';
}
dropdownChargeModeSelect.addEventListener('change', function() {
targetChargeMode = parseInt(this.value);
let s = 'charge mode: ' + targetChargeMode;
console.log(s);
if (targetChargeMode!==chargeMode){
this.disabled = true;
SendState('cm', targetChargeMode);
}
});
dropdownFullChargeOnPowerLostModeSelect.addEventListener('change', function() {
targetFCOPLMode = parseInt(this.value);
let s = 'FCOPL mode: ' + targetFCOPLMode;
console.log(s);
if (targetFCOPLMode!==fullChargeOnPowerLostMode){
this.disabled = true;
SendState('coil', targetFCOPLMode);
}
});
function resetFCoPLTriggered(){
btnResetFCOPL.disabled = true;
if (fullChargeOnPowerLostState){
SendState('rofc', 1);
}
}
toggleAutomaticVoltageReducer.addEventListener('change', function() {
targetAutomaticVoltageReducer = toggleAutomaticVoltageReducer.checked;
let s = 'AVR switched: ' + (targetAutomaticVoltageReducer?'ON':'OFF');
console.log(s);
if (targetAutomaticVoltageReducer!==chargeAutoVoltageReducer){
//Disabled Button
toggleAutomaticVoltageReducer.disabled = true;
SendState('car', targetAutomaticVoltageReducer?'1':'0');
}
});
toggleOvervoltageProtection.addEventListener('change', function() {
targetOvervoltageProtection = toggleOvervoltageProtection.checked;
let s = 'OVP switched: ' + (targetOvervoltageProtection?'ON':'OFF');
console.log(s);
if (targetOvervoltageProtection!==chargeOvervoltageProtection){
//Disabled Button
toggleOvervoltageProtection.disabled = true;
SendState('covp', targetOvervoltageProtection?'1':'0');
}
});
function SendState(cmd, value) {
// Clear timeout
clearTimeout(checkRefresh);
if (cmd !== "") {
onChange = true;
const url = '/state?' + cmd + '=' + value;
console.log("Fetch URL:", url);
fetch(url, {
method: "GET",
headers: { "Content-Type": "application/x-www-form-urlencoded" }
})
.then(response => {
if (!response.ok) throw new Error("Failed to update.");
return response.text();
})
.then(data => {
console.log(`${cmd} updated:`, data);
onChange = false;
if (parseInt(data.success)>0) ConnectionRefresh();
})
.catch(error => {
console.error("Error:", error);
onChange = false;
});
} else {
if (!onChange) {
const url = '/state/';
fetch(url)
.then(response => {
if (!response.ok) throw new Error("Failed to fetch data.");
return response.json(); // Already a JSON object, no need for JSON.parse()
})
.then(data => {
//console.log("Fetched Data:", data);
if (parseInt(data.success)>0) ConnectionRefresh();
if (onChange) return;
//sync display
if (data.success=="2"){
autoOnMode = parseInt(data.auto);
let targetAutoMode = parseInt(dropdownAutoOnSelect.value);
if (targetAutoMode!=autoOnMode){
dropdownAutoOnSelect.value = autoOnMode.toString();
}
if (dropdownAutoOnSelect.disabled){
dropdownAutoOnSelect.disabled = false;
}
//toggleAutoBtn.textContent = autoOn?'ON':'OFF';
stateTarget = parseInt(data.on) > 0;
stateValue = parseInt(data.state) > 0;
if (stateTarget==stateValue){
toggleSwitch.checked = stateTarget;
toggleSwitch.disabled = false;
}
//switchBtn.textContent = stateOn?'ON':'OFF';
lineFrom = parseInt(data.line);
isChargingOrBlink = parseInt(data.battBlink) > 0;
chargeLevel = parseInt(data.chargeLv); //battery level at charges
isBattCritical = parseInt(data.battCritical) > 0;
//Version 1.1
battPercentage = parseInt(data.battP);
chargeStatus = parseInt(data.chgError);
chargingState = parseInt(data.charging);
chargeMode = parseInt(data.chgMode);
chargeAutoVoltageReducer = parseInt(data.chgReducer) > 0;
chargeOvervoltageProtection = parseInt(data.chgOVP) > 0;
fullChargeOnPowerLostMode = parseInt(data.chgFullTrig);
fullChargeOnPowerLostState = parseInt(data.chgOFC) > 0;
//let s = lineFrom==0?'OFF':(lineFrom==1 || isChargingOrBlink)?'⚡️ AC':'🔋 Battery';
let s = lineFrom==0?'OFF':(lineFrom==1 || isChargingOrBlink)?'⚡️ Line':'🔋 Battery';
if (chargingState>0){
if (chargeStatus==0){ //normal state
if (lineFrom==1 && chargingState==2) s += " (Charging)";
if (lineFrom==1 && chargingState==1) s += " (Reduced Charging)";
}else{
//charging while error (retry test)
if (lineFrom==1 && chargingState>0) s += " (Charging)";
}
}
if (lineFrom==2 && isBattCritical) s += " (Critical)";
labelPower.textContent = s;
labelInput.textContent = data.v + " V";
battLevel = parseInt(data.batt);
battMaxLevel = parseInt(data.battMax);
//display V1.1
//charge mode:
if (targetChargeMode!=chargeMode){
dropdownChargeModeSelect.value = chargeMode.toString();
}
if (dropdownChargeModeSelect.disabled){
dropdownChargeModeSelect.disabled = false;
}
//fcopl:
if (targetFCOPLMode!=fullChargeOnPowerLostMode){
dropdownFullChargeOnPowerLostModeSelect.value = fullChargeOnPowerLostMode.toString();
}
if (dropdownFullChargeOnPowerLostModeSelect.disabled){
dropdownFullChargeOnPowerLostModeSelect.disabled = false;
}
//cvopl reset button:
if (fullChargeOnPowerLostState){
if (btnResetFCOPL.disabled || btnResetFCOPL.style.display !== "inline-block"){
btnResetFCOPL.disabled = false;
btnResetFCOPL.style.display = "inline-block";
}
}else{
if (!btnResetFCOPL.disabled || btnResetFCOPL.style.visibility !== "none"){
btnResetFCOPL.disabled = true;
btnResetFCOPL.style.display = "none";
}
}
//AVR:
//if (toggleAutomaticVoltageReducer.checked!=chargeAutoVoltageReducer){
if (toggleAutomaticVoltageReducer.disabled){
toggleAutomaticVoltageReducer.checked = chargeAutoVoltageReducer;
toggleAutomaticVoltageReducer.disabled = false;
}
//}
//OVP:
if (toggleOvervoltageProtection.disabled){
toggleOvervoltageProtection.checked = chargeOvervoltageProtection;
toggleOvervoltageProtection.disabled = false;
}
if (stateTarget==stateValue){
if (stateValue){
enableShutdownForm();
}else{
disableShutdownForm();
}
}else{
disableShutdownForm();
}
timedShutdown = parseInt(data.timedShutdown) > 0;
if (timedShutdown){
timedShutRemaining = parseInt(data.shutdownRemain);
if (timedShutRemaining==0){
shutTimer.textContent = 'TURNING OFF...';
}else{
shutTimer.textContent = timedShutRemaining.toString();
}
shutdownForm.style.display = 'none';
}else{
timedShutRemaining = 0;
shutTimer.textContent = '';
shutdownForm.style.display = 'block';
}
if (!everReceiveStat){
shutTimerInput.value = parseInt(data.shutLastTimer);
}
everReceiveStat = true;
//shutdown suggestion level
let shutLv = parseInt(data.shutdownSuggestMode);
if (shutdownSuggestionLevel!=shutLv){
shutdownSuggestionLevel = shutLv;
dropdownShutLevelSelect.value = shutLv.toString();
}
if (dropdownShutLevelSelect.disabled){
dropdownShutLevelSelect.disabled = false;
}
shutdownCastFlag = parseInt(data.shutdownSuggest) > 0;
shutFlagLabel.textContent = shutdownCastFlag?'✔️':' ';
if (chargeStatus==0){
labelStatus.textContent = "Normal";
}else if (chargeStatus==1){
labelStatus.textContent = "⚠️ Battery Not Detected" + (chargingState==0?"":" (Retry charging...)");
}else if (chargeStatus==2){
labelStatus.textContent = "⛔ Battery Shorted" + (chargingState==0?"":" (Retry charging..)");
}else if (chargeStatus==4){
labelStatus.textContent = "⚠️ Charge Overvoltage" + (chargingState==0?"":" (Retry charging...)");
}
}
})
.catch(error => console.error("Error:", error));
}
}
}
//charging independent animation
function BattDisplayRoutine(){
//connection state update when timeout
if (performance.now() - lastConnectTick >= 3000){
connectStateLabel.textContent = ' ⚫';
connectStateLabel.title = 'Unavailable';
}
if (!isChargingOrBlink){
if (isBattCritical){
chargeTickTock = !chargeTickTock;
let bar = chargeTickTock?0:1;
if (chargeTickTock){
labelBatt.textContent = repeatChar('🟧', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
}else{
labelBatt.textContent = repeatChar('🟧', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
}
}else{
chargeTickTock = false;
if (battMaxLevel==0){
labelBatt.textContent = '⬛⬛⬛⬛';//'Unknown';
}else{
labelBatt.textContent = repeatChar('🟩', battLevel) + repeatChar('⬛', battMaxLevel-battLevel) + " " + battPercentage + "%";
}
}
}else{
chargeTickTock = !chargeTickTock;
let bar = chargeTickTock?Math.min(chargeLevel+1, battMaxLevel):Math.min(chargeLevel, battMaxLevel);
if (chargeTickTock){
labelBatt.textContent = repeatChar('🟩', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
}else{
labelBatt.textContent = repeatChar('🟩', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
}
}
}
function syncState(){
//checkOnlineState();
if (onChange){
clearTimeout(checkRefresh);
return;
}
checkRefresh = setTimeout(function() {
SendState("", 0);
}, 1000);
}
disableShutdownForm();
// Set an interval to sync display
setInterval(syncState, 500);
SendState("", 0);
syncState();
setInterval(BattDisplayRoutine, 1000);
</script>
</body>
</html>

95
dev15.ino Normal file
View File

@ -0,0 +1,95 @@
// the setup function runs once when you press reset or power the board
#include "upsIO.h"
#include "upsSvr.h"
bool printDebug = true;
bool inOk = false;
int16_t adc = 0;
float voltage = 0;
uint32_t infoTick = 0;
void setup() {
// initialize digital pin LED_BUILTIN as an output.
Serial.begin(115200);
Serial.println();
//ESP.wdtDisable();
//initUpsIo(true);
initUpsIo();
//initUpsIo(false, true);
upsServerInit();
//CharliplexDemoRoutine();
}
// the loop function runs over and over again forever
void loop() {
if (printDebug && (millis()-infoTick>=500)){
infoTick = millis();
adc = analogRead(A0);//ioAdc;//analogRead(A0);//ioAdc;//
//voltage = (16.0*((float)adc / 1024)) * 0.983; //use 1.5 MOhm series resistor
voltage = (16.0*((float)adc / 1024)) * 1.0; //use 1.5 MOhm series resistor
Serial.println("VBatt: " + (String(voltage, 2)) + " V");
//adc = ioAdcFiltered;
voltage = batteryVoltageUnfiltered;//(16.0*((float)adc / 1024)) * 1.0; //use 1.5 MOhm series resistor
Serial.println("VBatt (Sub-Min): " + (String(voltage, 2)) + " V");
//adc = adcValueMin;
voltage = batteryVoltage;//(16.0*((float)adc / 1024)) * 1.0; //use 1.5 MOhm series resistor
Serial.print("VBatt (Min): " + (String(voltage, 2)) + " V");
Serial.print(" (" + (String(batteryPercentage)) + "%)");
Serial.println(", LV: " + (String(batteryLevel)));
Serial.println("Charge Error: " + (String(chargeError)));
Serial.println("Charge Switch: " + (String(isChargeActive)));
Serial.println("Input Under Voltage: " + (String(inputUnderVoltage)));
Serial.println("Input Voltage Exists: " + (String(inputVoltageKeepup)));
//inOk = digitalRead(pinInputSense);
//Serial.println("Vin: " + (String)(inOk?"True":"False"));
//buttonState = !digitalRead(pinInputButton); //inverted
Serial.println("Button: " + (String)(buttonState?"True":"False"));
Serial.println("Button (short): " + (String)(buttonShortPress?"True":"False"));
Serial.println("Button (long): " + (String)(buttonLongPress?"True":"False"));
Serial.println("Power OUT: " + (String)(powerOutputActive?"True":"False"));
//digitalWrite(pinLed, HIGH); // turn the LED on (HIGH is the voltage level)
//digitalWrite(pinPowerOut, HIGH);
Serial.println("KEEP POWER: " + (String)(keepPowerState?"True":"False"));
Serial.println("Display: " + (String)(displayActive?"True":"False"));
Serial.println("RSSI: " + (String)(rssi));
Serial.println("Charge Allowed: " + (String)(chargeAllowed?"True":"False"));
//for persistent shutdown demo: ==============================================================
// if (persistentShutdown){
// Serial.println("Shutdown in: " + (String)(getPersistentShutdownTick()/1000000));
// }else{
// if (restartAutoPowerOn()) Serial.println("Automatic ON restarted");
// setPersistentShutdown(10000000UL);
// }
//============================================================================================
}
upsServerRoutine();
upsRoutineInterrupt();
//delay(2000); // wait for a second
//digitalWrite(pinLed, LOW); // turn the LED off by making the voltage LOW
//digitalWrite(pinPowerOut, LOW);
//CharlieplexDemoRoutine();
//UpsIoRoutine();
}

117
upsCfg.h Normal file
View File

@ -0,0 +1,117 @@
#include <FS.h>
#include <ArduinoJson.h>
//shutdown suggestion level
#define SHUTDOWN_SUGGESTION_DISABLED 0
#define SHUTDOWN_SUGGESTION_ON_BATT 1
#define SHUTDOWN_SUGGESTION_ON_BATT_75 2
#define SHUTDOWN_SUGGESTION_ON_BATT_50 3
#define SHUTDOWN_SUGGESTION_ON_BATT_25 4
#define SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL 5
static volatile uint8_t cfgShutdownSuggestion = SHUTDOWN_SUGGESTION_ON_BATT_25;
#define AUTO_POWER_OFF 0
#define AUTO_POWER_ON 1
#define AUTO_POWER_PERSIST 2
static volatile uint8_t cfgAutoOn = AUTO_POWER_ON;//AUTO_POWER_PERSIST;//AUTO_POWER_ON;
#define SHUTDOWN_DURATION_DEFAULT 120
static volatile uint16_t cfgShutdownInSeconds = SHUTDOWN_DURATION_DEFAULT;//default time in seconds for shutdown
//full charge trigger:
#define INPUT_LOST_TRIGGER_CHARGE_NONE 0
#define INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_ALWAYS 1 //trigger full charge if below 100 percent
#define INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_95 2 //trigger full charge if below 95 percent
#define INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_90 3 //trigger full charge if below 90 percent
#define INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_85 4 //trigger full charge if below 85 percent
#define INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80 5 //trigger full charge if below 80 percent
static volatile uint8_t cfgInputLostTriggerChargeMode = INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_90;
#define CHARGE_MODE_ALWAYS 0
#define CHARGE_MODE_RANGE_90_100 1
#define CHARGE_MODE_RANGE_85_100 2
#define CHARGE_MODE_RANGE_80_100 3
#define CHARGE_MODE_RANGE_90_95 4
#define CHARGE_MODE_RANGE_85_95 5
#define CHARGE_MODE_RANGE_80_95 6
#define CHARGE_MODE_RANGE_85_90 7
#define CHARGE_MODE_RANGE_80_90 8
#define CHARGE_MODE_RANGE_80_85 9
static volatile uint8_t cfgChargeMode = CHARGE_MODE_RANGE_85_90;
static volatile uint8_t cfgChargeOvervoltageProtection = 1;
static volatile uint8_t cfgChargeAutomaticReducer = 1;
bool spiffsOk = false;
bool initConfigs(){
//setup spiffs
spiffsOk = SPIFFS.begin();
if (!spiffsOk){
Serial.println("SPIFFS ERROR!");
}else{
//LoadCfg();
}
return spiffsOk;
}
void loadCfg(){
if (!spiffsOk) return;
// Open file for reading
File file = SPIFFS.open("/cfg.json", "r");
if (!file) {
Serial.println("Failed to open config");
return;
}
// Parse file contents as JSON
DynamicJsonDocument jsonDoc(255);
DeserializationError error = deserializeJson(jsonDoc, file);
file.close();
if (error) {
Serial.println("Failed to parse file settings");
return;
}
uint8_t tmpAutoOnMode = jsonDoc["auto"].as<int>();
cfgAutoOn = tmpAutoOnMode;
if (cfgAutoOn>AUTO_POWER_PERSIST) cfgAutoOn = AUTO_POWER_PERSIST;
uint8_t v = jsonDoc["shut"].as<int>();
if (v>SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL) v = SHUTDOWN_SUGGESTION_ON_BATT_25;
cfgShutdownSuggestion = v;
v = jsonDoc["chgMode"].as<int>();
if (v>CHARGE_MODE_RANGE_80_85) v = CHARGE_MODE_RANGE_90_100;
cfgChargeMode = v;
v = jsonDoc["chgILTC"].as<int>();
if (v>INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80) v = INPUT_LOST_TRIGGER_CHARGE_NONE;
cfgInputLostTriggerChargeMode = v;
v = jsonDoc["chgOVP"].as<int>();
if (v>1) v = 1;
cfgChargeOvervoltageProtection = v;
v = jsonDoc["chgAR"].as<int>();
if (v>1) v = 1;
cfgChargeAutomaticReducer = v;
}
void saveCfg(){
if (!spiffsOk) return;
DynamicJsonDocument jsonDoc(255);
jsonDoc["auto"] = cfgAutoOn;
jsonDoc["shut"] = cfgShutdownSuggestion;
jsonDoc["chgMode"] = cfgChargeMode;
jsonDoc["chgILTC"] = cfgInputLostTriggerChargeMode;
jsonDoc["chgOVP"] = cfgChargeOvervoltageProtection;
jsonDoc["chgAR"] = cfgChargeAutomaticReducer;
File file = SPIFFS.open("/cfg.json", "w+");
if (file) {
uint32_t sz = serializeJson(jsonDoc, file);
file.close();
Serial.println("Configuration updated!");
}
}

1338
upsIO.h Normal file

File diff suppressed because it is too large Load Diff

226
upsOled.h Normal file
View File

@ -0,0 +1,226 @@
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "bitmap.h"
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32 // 0.91 inch OLED is usually 128x32
// Custom Pins
#define OLED_SCL 12 // MISO = D6 = OLED_SCL
#define OLED_SDA 13 // MOSI = D7 = OLED_SDA
#define UNUSED_PIN 14 // SCK = D5 (reserved when need reset pin)
#define OLED_RESET -1 // Or 0 if display has a reset pin
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ OLED_SCL, /* data=*/ OLED_SDA); // pin remapping with ESP8266 HW I2C
uint8_t displayContrast = 96;
uint8_t battLevelNum = 4;
uint8_t battBlockWidth = 15; uint8_t battBlockHeight = 14;
uint8_t battBlockGap = 3;
//for demo
uint8_t dispDemoIdx = 0;
void displaySplash(){
u8g2.setContrast(255);
//u8g2.drawXBMP(0, 0, 128, 32, splash_logo_bitmap);
u8g2.drawXBMP(0, 0, 128, 32, splash_logo_bitmap_inverted);
u8g2.sendBuffer();
}
void drawBatteryBlocks(int8_t x, int8_t y, int8_t nFilled){
int8_t xp = x+((battBlockWidth+battBlockGap)*battLevelNum);
xp-=battBlockGap;
int8_t yp = y;
u8g2.setDrawColor(1);
for (int8_t i=0; i<battLevelNum; i++){
xp-=battBlockWidth;
if (i<nFilled){
u8g2.drawRBox(xp, yp, battBlockWidth, battBlockHeight, 2); //Draws a filled box with rounded
}else{
u8g2.drawRFrame(xp, yp, battBlockWidth, battBlockHeight, 2); //Draws an empty frame with rounded corners.
}
xp-=battBlockGap;
}
}
void drawBatteryCase(int8_t x, int8_t y, uint8_t style){
//u8g2.drawLine(x, y, x+(battBlockWidth+battBlockGap)*battLevelNum), y);
int8_t sideGap = 4;
int8_t xpe = (x+((battBlockWidth+battBlockGap)*battLevelNum)) - battBlockGap;
int8_t w = ((battBlockWidth+battBlockGap)*battLevelNum) - battBlockGap;
u8g2.setDrawColor(1);
//u8g2.drawLine(x-sideGap, y-sideGap, xpe+sideGap, y-sideGap);
u8g2.drawRFrame(x-sideGap, y-sideGap, w+sideGap+sideGap, battBlockHeight+sideGap+sideGap, 3);
//kepala batre
int8_t headW = 5;
int8_t headH = battBlockHeight-2;
int8_t yc = y+(battBlockHeight/2);
//u8g2.drawRFrame(x-(sideGap+headW), y+sideGap, headW, battBlockHeight, 3);
if (style==0){
u8g2.drawRFrame(x-(sideGap+headW), yc-(headH/2), headW+1, headH, 1);
}else if (style==1){
u8g2.drawRBox(x-(sideGap+headW), yc-(headH/2), headW+1, headH, 1);
}else if (style==2 || style==3){
u8g2.drawRFrame(x-(sideGap+headW), yc-(headH/2), headW+1, headH, 1);
u8g2.setDrawColor(0);
int8_t xp = (x-(sideGap+headW)) + headW;
int8_t yp = yc-(headH/2);
if (style==2) u8g2.drawLine(xp, yp+1, xp, yp+(headH-2)); //rigid
if (style==3) u8g2.drawLine(xp, yp+0, xp, yp+(headH-1)); //curved or sleek
}
}
void drawBatteryPercentage(int8_t x, int8_t y, int8_t textWidth, int8_t percentage){
//draw text right aligned
String s = String(percentage) + "%";
char char_array[6];
s.toCharArray(char_array, s.length()+1);
u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
int8_t w = u8g2.getStrWidth(char_array); //note: current string width calculation is still not 100% perfect as the initial offset is part of the returned width.
int8_t xp = x + (textWidth-w);
u8g2.setDrawColor(1);
u8g2.drawStr(xp,y,char_array);
}
void drawActiveTransmission(int8_t x, int8_t y, bool inArrow, bool outArrow){
u8g2.setDrawColor(1);
if (inArrow) u8g2.drawXBMP(x, y, 2, 4, active_data_2x4_in_bitmap);
if (outArrow) u8g2.drawXBMP(x+3, y, 2, 4, active_data_2x4_out_bitmap);
}
void drawWifi(int8_t x, int8_t y, uint8_t level){
//power 0 = not drawn
u8g2.setDrawColor(1);
if (level==1) u8g2.drawXBMP(x, y, 16, 16, wifi2_lv1_bitmap);
if (level==2) u8g2.drawXBMP(x, y, 16, 16, wifi2_lv2_bitmap);
if (level>2) u8g2.drawXBMP(x, y, 16, 16, wifi2_lv3_bitmap);
}
void drawWifiAndTransmission(int8_t x, int8_t y, uint8_t level, bool inArrow, bool outArrow){
//power 0 = not drawn
drawWifi(x, y+1, level);
drawActiveTransmission(x+11, y+12, inArrow, outArrow);
}
void drawPower(int8_t x, int8_t y, bool timed){
u8g2.setDrawColor(1);
if (timed){
u8g2.drawXBMP(x, y, 16, 16, timer_16x16_bitmap);
}else{
u8g2.drawXBMP(x, y, 16, 16, power_bitmap);
}
}
void drawBolt(int8_t x, int8_t y){
u8g2.setDrawColor(1);
u8g2.drawXBMP(x, y, 16, 16, bolt_16x16_bitmap);
}
void drawCharging(int8_t x, int8_t y, bool reduced){
u8g2.setDrawColor(1);
if (reduced){
u8g2.drawXBMP(x, y, 7, 8, charging_7x8_bitmap);
}else{
u8g2.drawXBMP(x, y, 7, 8, charge_fast_7x8_bitmap);
}
}
void drawWarning(int8_t x, int8_t y, uint8_t mode){
u8g2.setDrawColor(1);
if (mode==0){
u8g2.drawXBMP(x, y, 16, 16, warn_16x16_bitmap);
}else if (mode==1){
u8g2.drawXBMP(x, y, 16, 16, error_16x16_bitmap);
}else if (mode==2){
u8g2.drawXBMP(x, y, 16, 16, fatal_16x16_bitmap);
}
}
void initDisplay(bool showSplash){
pinMode(UNUSED_PIN, INPUT);
u8g2.begin();
u8g2.setContrast(displayContrast);
if (showSplash){
displaySplash();
}else{
u8g2.setContrast(displayContrast);
u8g2.clearBuffer();
u8g2.sendBuffer();
}
}
void clearDisplay(){
u8g2.clearBuffer();
}
void sendDisplay(){
u8g2.sendBuffer();
}
void drawString(int8_t x, int8_t y, String s){
u8g2.setFont(u8g2_font_ncenB08_tr);
char char_array[32];
s.toCharArray(char_array, s.length()+1);
char_array[s.length()] = 0;
u8g2.setDrawColor(1);
u8g2.drawStr(x, y, char_array);
}
uint8_t stringWidth(String s){
char char_array[32];
s.toCharArray(char_array, s.length()+1);
char_array[s.length()] = 0;
u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
return u8g2.getStrWidth(char_array);
}
uint8_t stringCenterX(String s){
char char_array[32];
s.toCharArray(char_array, s.length()+1);
u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
uint8_t w = u8g2.getStrWidth(char_array);
return (SCREEN_WIDTH-w)/2;
}
uint8_t stringRightSideX(String s){
char char_array[32];
s.toCharArray(char_array, s.length()+1);
u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
uint8_t w = u8g2.getStrWidth(char_array);
return (SCREEN_WIDTH-w);
}
void displayDemoRoutine1(){
u8g2.setContrast(displayContrast);
u8g2.clearBuffer();
//drawBatteryBlock(5,5, true);
drawBatteryBlocks(55, 4, dispDemoIdx);
drawBatteryCase(55, 4, 2);
//u8g2.setDrawColor(1);
//u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
//u8g2.drawStr(100,32,"100%");
drawBatteryPercentage(100,32,28, 25*dispDemoIdx);
if (dispDemoIdx<=3) drawPower(0,0, dispDemoIdx>=2);
if (dispDemoIdx==1 || dispDemoIdx==3) drawBolt(20,0);
//drawWifi(0, 16, dispDemoIdx);
//drawActiveTransmission(11,28, (dispDemoIdx==1 || dispDemoIdx==3)?true:false, (dispDemoIdx==2 || dispDemoIdx==3)?true:false);
drawWifiAndTransmission(0, 16, dispDemoIdx, (dispDemoIdx==1 || dispDemoIdx==3)?true:false, (dispDemoIdx==2 || dispDemoIdx==3)?true:false);
drawCharging(88, 24, dispDemoIdx==4);
drawWarning(20,16, dispDemoIdx);
u8g2.setFont(u8g2_font_ncenB08_tr);
if (dispDemoIdx==0) u8g2.drawStr(39, 32, "OVER.");
if (dispDemoIdx==1) u8g2.drawStr(39, 32, "N0 BAT.");
if (dispDemoIdx==2) u8g2.drawStr(39, 32, "SHORT");
dispDemoIdx++;
if (dispDemoIdx > 4) dispDemoIdx = 0;
u8g2.sendBuffer();
}

431
upsSvr.h Normal file
View File

@ -0,0 +1,431 @@
//todo:
// - more accurate TX/RX indication (for security concern), e.g. favicon is served static, thus not display TX/RX arrow.
// - Login and apikey access for next version
#include "wifi.h"
#include "upsCfg.h"
#ifndef UPSIO_H
#include "upsIo.h"
#endif
//#include <AsyncTCP.h> //for esp32
#include <ESPAsyncTCP.h> //for esp8266
#include <ESPAsyncWebServer.h>
int8_t wifiStatePrev = 99;
bool configChanged = false;
AsyncWebServer server(80);
void applyConfigs(){
autoPowerOn = (cfgAutoOn >= AUTO_POWER_ON); //auto power on
//cfgChargeMode: charge mode:
switch (cfgChargeMode){
case CHARGE_MODE_ALWAYS:{
chargeAlwaysActive = true;
}
break;
case CHARGE_MODE_RANGE_90_100:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 100;
chargePercentageForResume = 90;
}
break;
case CHARGE_MODE_RANGE_85_100:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 100;
chargePercentageForResume = 85;
}
break;
case CHARGE_MODE_RANGE_80_100:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 100;
chargePercentageForResume = 80;
}
break;
case CHARGE_MODE_RANGE_90_95:{
chargeAlwaysActive = false;
chargeToFull = false;
chargePercentageForStop = 95;
chargePercentageForResume = 90;
}
break;
case CHARGE_MODE_RANGE_85_95:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 95;
chargePercentageForResume = 85;
}
break;
case CHARGE_MODE_RANGE_80_95:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 95;
chargePercentageForResume = 80;
}
break;
case CHARGE_MODE_RANGE_85_90:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 90;
chargePercentageForResume = 85;
}
break;
case CHARGE_MODE_RANGE_80_90:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 90;
chargePercentageForResume = 80;
}
break;
case CHARGE_MODE_RANGE_80_85:{
chargeAlwaysActive = false;
chargeToFull = true;
chargePercentageForStop = 85;
chargePercentageForResume = 80;
}
break;
}
//cfgInputLostTriggerChargeMode:
switch (cfgInputLostTriggerChargeMode){
case INPUT_LOST_TRIGGER_CHARGE_NONE:{
chargeAfterInputLost = false;
}
break;
case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_ALWAYS:{
chargeAfterInputLost = true;
fullChargeAfterInputLost = true;
}
break;
case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_95:{
chargeAfterInputLost = true;
fullChargeAfterInputLost = false;
triggerFullChargeAfterInputLostAtPercentage = 95;
}
break;
case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_90:{
chargeAfterInputLost = true;
fullChargeAfterInputLost = false;
triggerFullChargeAfterInputLostAtPercentage = 90;
}
break;
case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_85:{
chargeAfterInputLost = true;
fullChargeAfterInputLost = false;
triggerFullChargeAfterInputLostAtPercentage = 85;
}
break;
case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80:{
chargeAfterInputLost = true;
fullChargeAfterInputLost = false;
triggerFullChargeAfterInputLostAtPercentage = 80;
}
break;
}
//cfgChargeOvervoltageProtection:
chargeOvervoltageProtection = (cfgChargeOvervoltageProtection > 0);
//cfgChargeAutomaticReducer:
chargeReduceAtBatteryFull = (cfgChargeAutomaticReducer > 0);
}
bool getShutdownSuggestionFlag(){
if (cfgShutdownSuggestion==SHUTDOWN_SUGGESTION_DISABLED) return false;
if (!startupReady) return false;
switch(cfgShutdownSuggestion){
case SHUTDOWN_SUGGESTION_ON_BATT:{
return powerOutputActive && !inputVoltageKeepup;
}
break;
case SHUTDOWN_SUGGESTION_ON_BATT_75:{
return powerOutputActive && !inputVoltageKeepup && (batteryLevel<=3);
}
break;
case SHUTDOWN_SUGGESTION_ON_BATT_50:{
return powerOutputActive && !inputVoltageKeepup && (batteryLevel<=2);
}
break;
case SHUTDOWN_SUGGESTION_ON_BATT_25:{
return powerOutputActive && !inputVoltageKeepup && (batteryLevel<=1);
}
break;
case SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL:{
return powerOutputActive && !inputVoltageKeepup && (batteryLevel<=1) && isUpsBatteryCritical();
}
break;
default:{
return false;
}
}
return false;
}
void handleGetState(AsyncWebServerRequest *request) {
setNetworkIndicatorInTransmission(true, false);
bool isRefresh = true;
String v = "";
uint8_t i = 0;
if (request->hasParam("on")) {
isRefresh = false;
v = request->getParam("on")->value();
bool tgtState = v != "0";
Serial.print("Switch request: ");
Serial.println(tgtState);
if (tgtState != powerOutputActive) {
setOutputPowerState(tgtState);
Serial.println("Switching...");
}
}
if (request->hasParam("auto")) {
isRefresh = false;
v = request->getParam("auto")->value();
uint8_t tmpAutoOnMode = v.toInt();
if ((tmpAutoOnMode != cfgAutoOn) && (tmpAutoOnMode <= AUTO_POWER_PERSIST) ) {
cfgAutoOn = tmpAutoOnMode;
configChanged = true;
}
}
if (request->hasParam("shutdown")) {
isRefresh = false;
v = request->getParam("shutdown")->value();
uint16_t tSec = v.toInt();
if (tSec > 999) tSec = 999;
uint32_t nanoT = ((uint32_t)tSec) * 1000000UL;
if (nanoT < 999999) nanoT = 999999;
bool ok = setPersistentShutdown(nanoT);
if ((cfgShutdownInSeconds != tSec && ok)){
cfgShutdownInSeconds = tSec;
configChanged = true;
}
Serial.print("Timed Shutdown request: "); Serial.println(tSec);
//Serial.println(ok);
}
if (request->hasParam("shutlv")) {
isRefresh = false;
v = request->getParam("shutlv")->value();
int16_t lv = v.toInt();
if (lv >= SHUTDOWN_SUGGESTION_DISABLED && lv <= SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL) {
if (lv != cfgShutdownSuggestion) {
cfgShutdownSuggestion = lv;
configChanged = true;
Serial.print("Shutdown level changed: ");
Serial.println(lv);
}
} else {
Serial.print("Invalid shutdown level: ");
Serial.println(v);
}
}
//battery management params:
//cfgChargeMode (charge mode):
if (request->hasParam("cm")) {
isRefresh = false;
v = request->getParam("cm")->value();
i = v.toInt();
if (i<=CHARGE_MODE_RANGE_80_85){
cfgChargeMode = i;
configChanged = true;
Serial.print("CM: "); Serial.println(i);
}
}
//cfgInputLostTriggerChargeMode (charge on input lost):
if (request->hasParam("coil")) {
isRefresh = false;
v = request->getParam("coil")->value();
i = v.toInt();
if (i<=INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80){
cfgInputLostTriggerChargeMode = i;
configChanged = true;
Serial.print("COIL: "); Serial.println(i);
}
}
//cfgChargeOvervoltageProtection (charge overvoltage protection):
if (request->hasParam("covp")) {
isRefresh = false;
v = request->getParam("covp")->value();
i = v.toInt();
if (i>1) i = 1;
if (i!=cfgChargeOvervoltageProtection){
cfgChargeOvervoltageProtection = i;
configChanged = true;
Serial.print("COVP: "); Serial.println(i);
}
}
//reset "On-Going Full Charge" triggered after input lost (reset ongoing full charge):
if (request->hasParam("rofc")) {
isRefresh = false;
v = request->getParam("rofc")->value();
i = v.toInt();
if (i>0 && needFullChargeOnce) {
resetOnGoingFullChargeAfterLineLost();
Serial.print("ROFC: "); Serial.println(i);
}
}
//cfgChargeAutomaticReducer (charge automatic reducer)
if (request->hasParam("car")) {
isRefresh = false;
v = request->getParam("car")->value();
i = v.toInt();
if (i>1) i = 1;
if (i!=cfgChargeAutomaticReducer){
cfgChargeAutomaticReducer = i;
configChanged = true;
Serial.print("CAR: "); Serial.println(i);
}
}
DynamicJsonDocument jsonDoc(1024);
String jsonResponse;
//String stateOn = isUpsOn()?"1":"0";
//String autoOn = String(cfgAutoOn);
//String voltageValue = String(batteryVoltage, 2);
//2 battery, 1 Input line or AC, 0 OFF
//String lineActive = isUpsPoweredFromBattery()?"2":isInputLinePowered()?"1":isUpsOn()?"2":"0";
uint8_t lineActiveState = isUpsPoweredFromBattery()?2:isInputLinePowered()?1:isUpsOn()?2:0;
//String batt = String(batteryLevel);
//String battMax = "4";
// String battBlink = IsUpsChargingOrBlink()?"1":"0";
// String chargeLv = IsUpsChargingOrBlink()?(String(UpsBattMinimalLevelUnderChargingOrBlink())):(String(UpsBatteryLevel()));
// String battCritical = IsUpsBatteryCritical()?"1":"0";
// String isTimedShut = timedShutdownFlag?"1":"0";
// String onShutRemaining = String(GetShutdownCountdown());
// String shutdownSuggest = GetShutdownSuggestionFlag()?"1":"0";
// String shutdownSuggestMode = String(cfgShutdownSuggestion);
// String shutdownLastPeriod = String(timeShutdownLastPeriod);
//json vars:
jsonDoc["success"] = isRefresh ? 2 : 1;
jsonDoc["on"] = powerOutputActive?1:0;;
jsonDoc["state"] = isUpsOn()?1:0;
jsonDoc["v"] = String(batteryVoltage, 2);
jsonDoc["batt"] = batteryLevel;
jsonDoc["battP"] = batteryPercentage;
jsonDoc["line"] = lineActiveState;
jsonDoc["auto"] = cfgAutoOn;
jsonDoc["battBlink"] = isUpsBatteryIndicatorChargingOrBlink()?1:0;
jsonDoc["battCritical"] = isUpsBatteryCritical()?1:0;
jsonDoc["chargeLv"] = getRawBatteryLevelFromPercentage(batteryPercentage); //real level
jsonDoc["batt"] = batteryLevel; //staged level
jsonDoc["battMax"] = 4;
jsonDoc["timedShutdown"] = (persistentShutdown && powerOutputActive)?1:0;
jsonDoc["shutdownRemain"] = getPersistentShutdownTick()/1000000;
jsonDoc["shutdownSuggestMode"] = cfgShutdownSuggestion;
jsonDoc["shutdownSuggest"] = getShutdownSuggestionFlag()?1:0; //internal flag raised if cfgShutdownSuggestion fullfill the condition
jsonDoc["shutLastTimer"] = cfgShutdownInSeconds;
//charge configuration:
jsonDoc["chgError"] = chargeError; //error state
jsonDoc["charging"] = getChargingState(); //on charging state
jsonDoc["chgMode"] = cfgChargeMode;
jsonDoc["chgOVP"] = cfgChargeOvervoltageProtection; //over voltage protection
jsonDoc["chgReducer"] = cfgChargeAutomaticReducer;
jsonDoc["chgFullTrig"] = cfgInputLostTriggerChargeMode;
jsonDoc["chgOFC"] = needFullChargeOnce?1:0; //when input lost and "Ongoing Full Charge" flag triggered
serializeJson(jsonDoc, jsonResponse);
request->send(200, "application/json", jsonResponse);
setNetworkIndicatorInTransmission(true, true);
}
void onRootRequest(AsyncWebServerRequest *request) {
setNetworkIndicatorInTransmission(true, false);
Serial.println("onRootRequest");
bool isOK = false;
if (spiffsOk) {
if (SPIFFS.exists("/index.html")) {
isOK = true;
request->send(SPIFFS, "/index.html", "text/html");
setNetworkIndicatorInTransmission(true, true);
}
}
if (!isOK) {
request->send(404, "text/text", "404: Not Found");
setNetworkIndicatorInTransmission(true, true);
}
}
void upsServerInit() {
bool b = initConfigs();
if (b){
loadCfg();
applyConfigs();
}
//transfer configuration to ups library:
if (cfgAutoOn >= AUTO_POWER_ON) {
autoPowerOn = true;
restartAutoPowerOn();
} else {
autoPowerOn = false;
}
//for favicon
server.serveStatic("/favicon.ico", SPIFFS, "/favicon.ico");
server.serveStatic("*/favicon.ico", SPIFFS, "/favicon.ico");
//for RESTapi
server.on("/state", HTTP_GET, handleGetState);
//root
server.on("/", onRootRequest);
server.on("/*", onRootRequest);
//Redirect all not-found requests to the root
server.onNotFound(onRootRequest);
server.begin();
Serial.println("Server is running!");
}
void upsServerRoutine() {
WiFiManagerRoutine();
//updating wifi state:
if (wifiCurrentState != wifiStatePrev) {
wifiStatePrev = wifiCurrentState;
setNetworkState(wifiStatePrev);
}
//update signal:
indicatorNetworkSignalLevel = wifiSignalLevel;
//manage auto on mode:
if (cfgAutoOn == AUTO_POWER_PERSIST) {
if (inputVoltageKeepup && inputVoltageOK) {
if (restartAutoPowerOn()) Serial.println("Automatic ON restarted");
}
}
//if button long pressed -------------------------------------------
if (buttonLongPressIgnore && buttonLongPress){
buttonLongPressIgnore = true;
Serial.println("Long Pressed Button Action is Reserved!");
//(not used)
}
//------------------------------------------------------------------
//saving configuration:
if (configChanged) {
configChanged = false;
saveCfg();
applyConfigs();
}
}

106
wifi.h Normal file
View File

@ -0,0 +1,106 @@
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
bool useStaticIP = true;
#ifndef STASSID
#define STASSID "EVXIO-HUB"
#define STAPSK "Hub-1122"
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
String staticIP = "192.168.0.100";
String staticGateway = "192.168.0.1";
String staticSubnet = "255.255.255.0";
String staticDNS = "192.168.0.1";
String mdnsPrefix = "SMART-UPS"; //for fast access like http://smart-ups.local
static volatile bool wifiStarted = false;
static volatile uint32_t wifiNextCheckTick = 0;
static volatile bool wifiConnected = false;
static volatile bool wifiConnecting = false;
static volatile uint8_t wifiCurrentState = 0;
static volatile bool setWifiConfig = false;
static volatile uint8_t wifiSignalLevel = 0; //0=none, 1=low, 2=medium, 2=high
long rssi = -999;
void WiFiManagerRoutine() {
int8_t wifiState = WiFi.status();
bool isWifiConnected = wifiState == WL_CONNECTED;
if (isWifiConnected) wifiConnecting = false;
if (wifiConnected != isWifiConnected){
setWifiConfig = true; //reset config on reconnect
wifiConnected = isWifiConnected;
if (wifiConnected) wifiCurrentState = 2; //connected
}
//wifi signal update
rssi = WiFi.RSSI();
if (rssi <= -80){
wifiSignalLevel = 1;
}else if (rssi > -80 && rssi <= -70){
wifiSignalLevel = 2;
}else if (rssi > -70 && rssi <= -60){
wifiSignalLevel = 3;
}else{
wifiSignalLevel = 3;
}
if (!isWifiConnected){
if (wifiState==WL_NO_SSID_AVAIL || wifiState==WL_CONNECT_FAILED || wifiState==WL_CONNECTION_LOST) {
wifiConnecting = false;
wifiCurrentState=0;
wifiSignalLevel = 0;
}
if (!wifiStarted || (millis() - wifiNextCheckTick) >= 30000UL){ //check each 30seconds
wifiStarted = true;
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.setOutputPower(20.5);//set to max dbm for reliability (for highest RF power output, supply current ~ 80mA)
//WiFi.setOutputPower(0);//(for lowest RF power output, supply current ~ 70mA)
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
wifiNextCheckTick = millis(); //next 30 seconds check again
wifiConnecting = true;
wifiCurrentState = 1; //connecting
}
}else{
if (!wifiStarted){
wifiStarted = true;
setWifiConfig = true;
}
if (setWifiConfig){
//set connection on first time connected
setWifiConfig = false;
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
if (useStaticIP){
IPAddress ip, gateway, subnet, dns;
bool b = ip.fromString(staticIP);
b = gateway.fromString(staticGateway);
b = subnet.fromString(staticSubnet);
b = dns.fromString(staticDNS);
WiFi.config(ip, gateway, subnet, dns);
}
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if (MDNS.begin(mdnsPrefix)) {
Serial.print("MDNS responder started as ");
Serial.print(mdnsPrefix);
Serial.println(".local");
}
}
MDNS.update();
}
}