Upload file project dev15_v2
This commit is contained in:
commit
966d24cae9
|
|
@ -0,0 +1,45 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
void listFiles() {
|
||||||
|
Serial.println("\nListing files in LittleFS:");
|
||||||
|
Dir dir = LittleFS.openDir("/");
|
||||||
|
while (dir.next()) {
|
||||||
|
Serial.printf(" %s (%d bytes)\n", dir.fileName().c_str(), dir.fileSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readFile(const char *path) {
|
||||||
|
File file = LittleFS.open(path, "r");
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("Failed to open file: %s\n", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Serial.printf("Contents of %s:\n", path);
|
||||||
|
while (file.available()) {
|
||||||
|
Serial.write(file.read());
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
if (!LittleFS.begin()) {
|
||||||
|
Serial.println("LittleFS mount failed! Did you upload data folder?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("LittleFS mounted successfully.");
|
||||||
|
listFiles();
|
||||||
|
|
||||||
|
// Read demo files
|
||||||
|
readFile("/info.txt");
|
||||||
|
readFile("/config.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"wifi_ssid": "YourNetwork",
|
||||||
|
"wifi_pass": "YourPassword",
|
||||||
|
"mode": "demo"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Wemos D1 Mini LittleFS Demo
|
||||||
|
Uploaded from Arduino IDE using LittleFS Data Upload plugin.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# LittleFS Demo
|
||||||
|
This is a sample file stored in internal flash using LittleFS.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,827 @@
|
||||||
|
<!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Overlay Styles */
|
||||||
|
#login-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background-color: rgba(18, 18, 18, 0.95);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background-color: #242424;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||||||
|
text-align: center;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.login-box input {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 80%;
|
||||||
|
border: 1px solid #444;
|
||||||
|
background-color: #121212;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.login-box h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Login Overlay -->
|
||||||
|
<div id="login-overlay">
|
||||||
|
<div class="login-box" onkeydown="if(event.key==='Enter')attemptLogin()">
|
||||||
|
<h2>UPS Authentication</h2>
|
||||||
|
<input type="text" id="login-username" placeholder="Username (admin)" value="admin">
|
||||||
|
<input type="password" id="login-password" placeholder="Password">
|
||||||
|
<br>
|
||||||
|
<button onclick="attemptLogin()" style="width: 80%; padding: 10px;">Login</button>
|
||||||
|
<p id="login-error" style="color: #ff5555; display: none; margin-top: 15px; font-size: 14px;">Invalid username or password</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<button id="logoutBtn" onclick="logout()" style="position: absolute; right: 20px; top: 20px; padding: 4px 12px; font-size: 12px;">Logout</button>
|
||||||
|
</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;
|
||||||
|
|
||||||
|
var authHeader = "";
|
||||||
|
|
||||||
|
function attemptLogin() {
|
||||||
|
var usr = document.getElementById('login-username').value;
|
||||||
|
var pwd = document.getElementById('login-password').value;
|
||||||
|
authHeader = "Basic " + btoa(usr + ":" + pwd);
|
||||||
|
document.getElementById('login-error').style.display = 'none';
|
||||||
|
SendState("", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
authHeader = "";
|
||||||
|
document.getElementById('login-password').value = "";
|
||||||
|
document.getElementById('login-error').style.display = 'none';
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
fetch('/logout', { method: "GET" }).catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
"Authorization": authHeader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
if (!response.ok) throw new Error("Failed to update.");
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('login-overlay').style.display = 'none';
|
||||||
|
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, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": authHeader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch data.");
|
||||||
|
return response.json(); // Already a JSON object, no need for JSON.parse()
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('login-overlay').style.display = 'none';
|
||||||
|
//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>
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
// the setup function runs once when you press reset or power the board
|
||||||
|
|
||||||
|
#include "upsIO.h"
|
||||||
|
#include "upsSvr.h"
|
||||||
|
#include "mqtt.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();
|
||||||
|
mqttInit();
|
||||||
|
|
||||||
|
//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();
|
||||||
|
mqttRoutine();
|
||||||
|
|
||||||
|
upsRoutineInterrupt();
|
||||||
|
|
||||||
|
//delay(2000); // wait for a second
|
||||||
|
//digitalWrite(pinLed, LOW); // turn the LED off by making the voltage LOW
|
||||||
|
//digitalWrite(pinPowerOut, LOW);
|
||||||
|
|
||||||
|
|
||||||
|
//CharlieplexDemoRoutine();
|
||||||
|
//UpsIoRoutine();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
#ifndef MQTT_H
|
||||||
|
#define MQTT_H
|
||||||
|
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
const char* mqtt_server = "broker.emqx.io";
|
||||||
|
const int mqtt_port = 1883;
|
||||||
|
|
||||||
|
// Topics
|
||||||
|
const char* mqtt_topic_state = "ups/dev15/state";
|
||||||
|
const char* mqtt_topic_cmd = "ups/dev15/cmd";
|
||||||
|
|
||||||
|
WiFiClient espClient;
|
||||||
|
PubSubClient mqttClient(espClient);
|
||||||
|
|
||||||
|
unsigned long lastMqttReconnectAttempt = 0;
|
||||||
|
unsigned long lastMqttPublish = 0;
|
||||||
|
const unsigned long mqttPublishInterval = 2000; // Publish state every 2 seconds
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
void mqttPublishState();
|
||||||
|
|
||||||
|
void mqttCallback(char* topic, byte* payload, unsigned int length) {
|
||||||
|
Serial.print("Message arrived on topic: ");
|
||||||
|
Serial.print(topic);
|
||||||
|
Serial.print(". Message: ");
|
||||||
|
String messageTemp;
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
Serial.print((char)payload[i]);
|
||||||
|
messageTemp += (char)payload[i];
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
// Parse command
|
||||||
|
if (String(topic) == mqtt_topic_cmd) {
|
||||||
|
DynamicJsonDocument doc(256);
|
||||||
|
DeserializationError error = deserializeJson(doc, payload, length);
|
||||||
|
if (!error) {
|
||||||
|
String cmd = doc["cmd"].as<String>();
|
||||||
|
String v = doc["val"].as<String>();
|
||||||
|
bool updated = false;
|
||||||
|
|
||||||
|
if (cmd == "on") {
|
||||||
|
bool tgtState = v != "0";
|
||||||
|
if (tgtState != powerOutputActive) {
|
||||||
|
// This function is defined in upsSvr.h / main logic
|
||||||
|
// Note: mqtt.h is included after or before? Need to ensure it can call setOutputPowerState
|
||||||
|
// Actually, setOutputPowerState is declared in upsCfg.h maybe?
|
||||||
|
// If compilation fails, we can just set the flag. Wait, let's use the function since it's global.
|
||||||
|
setOutputPowerState(tgtState);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "auto") {
|
||||||
|
uint8_t tmpAutoOnMode = v.toInt();
|
||||||
|
if ((tmpAutoOnMode != cfgAutoOn) && (tmpAutoOnMode <= 2)) {
|
||||||
|
cfgAutoOn = tmpAutoOnMode;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "shutdown") {
|
||||||
|
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;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "shutlv") {
|
||||||
|
int16_t lv = v.toInt();
|
||||||
|
if (lv >= 0 && lv <= 5) {
|
||||||
|
if (lv != cfgShutdownSuggestion) {
|
||||||
|
cfgShutdownSuggestion = lv;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cmd == "cm") {
|
||||||
|
int16_t i = v.toInt();
|
||||||
|
if (i >= 0 && i <= 9) {
|
||||||
|
cfgChargeMode = i;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "coil") {
|
||||||
|
int16_t i = v.toInt();
|
||||||
|
if (i >= 0 && i <= 5) {
|
||||||
|
cfgInputLostTriggerChargeMode = i;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "covp") {
|
||||||
|
int16_t i = v.toInt();
|
||||||
|
if (i > 1) i = 1;
|
||||||
|
if (i != cfgChargeOvervoltageProtection) {
|
||||||
|
cfgChargeOvervoltageProtection = i;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "rofc") {
|
||||||
|
int16_t i = v.toInt();
|
||||||
|
if (i > 0 && needFullChargeOnce) {
|
||||||
|
resetOnGoingFullChargeAfterLineLost();
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
} else if (cmd == "car") {
|
||||||
|
int16_t i = v.toInt();
|
||||||
|
if (i > 1) i = 1;
|
||||||
|
if (i != cfgChargeAutomaticReducer) {
|
||||||
|
cfgChargeAutomaticReducer = i;
|
||||||
|
configChanged = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
mqttPublishState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean mqttReconnect() {
|
||||||
|
String clientId = "UPS_DEV15_" + String(random(0xffff), HEX);
|
||||||
|
Serial.print("Attempting MQTT connection as ");
|
||||||
|
Serial.println(clientId);
|
||||||
|
if (mqttClient.connect(clientId.c_str())) {
|
||||||
|
Serial.println("MQTT Connected");
|
||||||
|
// Once connected, publish an announcement...
|
||||||
|
mqttClient.publish("ups/dev15/status", "online");
|
||||||
|
// ... and resubscribe
|
||||||
|
mqttClient.subscribe(mqtt_topic_cmd);
|
||||||
|
} else {
|
||||||
|
Serial.print("failed, rc=");
|
||||||
|
Serial.print(mqttClient.state());
|
||||||
|
Serial.println(" try again in 5 seconds");
|
||||||
|
}
|
||||||
|
return mqttClient.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttInit() {
|
||||||
|
mqttClient.setServer(mqtt_server, mqtt_port);
|
||||||
|
mqttClient.setBufferSize(1024); // Increase buffer size for large JSON payloads
|
||||||
|
mqttClient.setCallback(mqttCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttPublishState() {
|
||||||
|
DynamicJsonDocument jsonDoc(1024);
|
||||||
|
String jsonResponse;
|
||||||
|
|
||||||
|
uint8_t lineActiveState = isUpsPoweredFromBattery() ? 2 : isInputLinePowered() ? 1 : isUpsOn() ? 2 : 0;
|
||||||
|
|
||||||
|
jsonDoc["success"] = 2;
|
||||||
|
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);
|
||||||
|
jsonDoc["battMax"] = 4;
|
||||||
|
jsonDoc["timedShutdown"] = (persistentShutdown && powerOutputActive) ? 1 : 0;
|
||||||
|
jsonDoc["shutdownRemain"] = getPersistentShutdownTick() / 1000000;
|
||||||
|
jsonDoc["shutdownSuggestMode"] = cfgShutdownSuggestion;
|
||||||
|
jsonDoc["shutdownSuggest"] = getShutdownSuggestionFlag() ? 1 : 0;
|
||||||
|
jsonDoc["shutLastTimer"] = cfgShutdownInSeconds;
|
||||||
|
|
||||||
|
jsonDoc["chgError"] = chargeError;
|
||||||
|
jsonDoc["charging"] = getChargingState();
|
||||||
|
jsonDoc["chgMode"] = cfgChargeMode;
|
||||||
|
jsonDoc["chgOVP"] = cfgChargeOvervoltageProtection;
|
||||||
|
jsonDoc["chgReducer"] = cfgChargeAutomaticReducer;
|
||||||
|
jsonDoc["chgFullTrig"] = cfgInputLostTriggerChargeMode;
|
||||||
|
jsonDoc["chgOFC"] = needFullChargeOnce ? 1 : 0;
|
||||||
|
|
||||||
|
serializeJson(jsonDoc, jsonResponse);
|
||||||
|
mqttClient.publish(mqtt_topic_state, jsonResponse.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttRoutine() {
|
||||||
|
if (!mqttClient.connected()) {
|
||||||
|
long now = millis();
|
||||||
|
if (now - lastMqttReconnectAttempt > 5000) {
|
||||||
|
lastMqttReconnectAttempt = now;
|
||||||
|
// Attempt to reconnect
|
||||||
|
if (mqttReconnect()) {
|
||||||
|
lastMqttReconnectAttempt = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mqttClient.loop();
|
||||||
|
|
||||||
|
// Publish state periodically
|
||||||
|
long now = millis();
|
||||||
|
if (now - lastMqttPublish > mqttPublishInterval) {
|
||||||
|
lastMqttPublish = now;
|
||||||
|
mqttPublishState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
# Ringkasan WebServer & API — Smart UPS V1.1
|
||||||
|
|
||||||
|
## Struktur Kode WebServer
|
||||||
|
|
||||||
|
### `upsSvr.h` — Backend (C++ ESP8266)
|
||||||
|
```cpp
|
||||||
|
AsyncWebServer server(80); // Server HTTP port 80
|
||||||
|
```
|
||||||
|
|
||||||
|
**Route handlers** (daftar endpoint):
|
||||||
|
```cpp
|
||||||
|
server.on("/login", HTTP_GET, handler); // Validasi login
|
||||||
|
server.on("/logout", HTTP_GET, handler); // Logout
|
||||||
|
server.on("/state", HTTP_GET, handleGetState); // REST API utama
|
||||||
|
server.on("/", onRootRequest); // Dashboard HTML
|
||||||
|
server.on("/*", onRootRequest); // Catch-all → dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auth checking** — manual Base64 decode:
|
||||||
|
```cpp
|
||||||
|
const char* http_username = "admin";
|
||||||
|
const char* http_password = "admin123";
|
||||||
|
|
||||||
|
bool checkAuth(request) {
|
||||||
|
// 1. Ambil header "Authorization"
|
||||||
|
// 2. Cek apakah diawali "Basic "
|
||||||
|
// 3. Base64 decode bagian setelah "Basic "
|
||||||
|
// 4. Split jadi "username:password"
|
||||||
|
// 5. Bandingkan dengan http_username / http_password
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Setiap request ke `/state` dicek dulu:
|
||||||
|
```cpp
|
||||||
|
void handleGetState(request) {
|
||||||
|
if (!checkAuth(request)) {
|
||||||
|
request->send(401, "application/json", "{\"success\":0}"); // JSON, bukan popup browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ... baca/tulis parameter dan kirim response JSON ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fungsi utama `handleGetState()`**:
|
||||||
|
1. Baca parameter URL (`on`, `auto`, `shutdown`, `cm`, dll)
|
||||||
|
2. Jika ada parameter → eksekusi perintah (misal: nyalakan power)
|
||||||
|
3. Jika tidak ada parameter → kirim status lengkap sebagai JSON
|
||||||
|
|
||||||
|
### `webUI.h` — Frontend (HTML+JS embedded)
|
||||||
|
HTML ini disimpan di PROGMEM (flash), fallback jika file LittleFS tidak ada.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const char index_html[] PROGMEM = R"rawliteral(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>... semua HTML, CSS, JavaScript ...</html>
|
||||||
|
)rawliteral";
|
||||||
|
```
|
||||||
|
|
||||||
|
### `data/index.html` — Frontend (HTML+JS di LittleFS)
|
||||||
|
File fisik di LittleFS, sama persis dengan `webUI.h`. Server prioritaskan ini.
|
||||||
|
|
||||||
|
### `upsCfg.h` — Konfigurasi
|
||||||
|
Simpan & baca pengaturan dari `/cfg.json` di LittleFS.
|
||||||
|
```cpp
|
||||||
|
void saveCfg(); // Simpan: auto, shut, chgMode, chgILTC, chgOVP, chgAR
|
||||||
|
void loadCfg(); // Baca kembali
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alur Request
|
||||||
|
|
||||||
|
### Request tanpa parameter → Refresh status
|
||||||
|
```
|
||||||
|
Browser → GET /state (Authorization: Basic ...)
|
||||||
|
← 200 {"success":2, "on":1, "v":"12.5", ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request dengan parameter → Eksekusi perintah
|
||||||
|
```
|
||||||
|
Browser → GET /state?on=1 (Authorization: Basic ...)
|
||||||
|
← 200 {"success":1, "on":1, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request tanpa auth → Ditolak
|
||||||
|
```
|
||||||
|
Browser → GET /state (tanpa Authorization)
|
||||||
|
← 401 {"success":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cara Menggunakan API
|
||||||
|
|
||||||
|
### 1. Baca Status UPS
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 http://192.168.1.100/state
|
||||||
|
```
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"success":2, "on":1, "state":1, "v":"13.20", "batt":4, "battP":100, "line":1, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Nyalakan Power Output
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?on=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Matikan Power Output
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?on=0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Shutdown Timer (120 detik)
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?shutdown=120"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Set Mode Auto ON
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?auto=0" # Disabled
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?auto=1" # Line Powered
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?auto=2" # Persistent on Line
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Set Charge Mode (0=Always, 1=90-100%, ..., 9=80-85%)
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?cm=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Set FCoPL (Full Charge on Power Lost)
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?coil=0" # Disabled
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?coil=1" # Always
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?coil=3" # At battery ≤90%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. AVR / OVP
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?car=1" # AVR ON
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?car=0" # AVR OFF
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?covp=1" # OVP ON
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Set Shutdown Suggestion Level
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?shutlv=0" # Disabled
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?shutlv=1" # On Battery
|
||||||
|
curl -u admin:admin123 "http://192.168.1.100/state?shutlv=3" # ≤50% Battery
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Login (untuk testing)
|
||||||
|
```bash
|
||||||
|
curl -u admin:admin123 http://192.168.1.100/login
|
||||||
|
# 200 → {"success":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. Buka Dashboard via Browser
|
||||||
|
```
|
||||||
|
http://192.168.1.100/
|
||||||
|
→ Login overlay (isi admin / admin123)
|
||||||
|
→ Dashboard muncul
|
||||||
|
→ Klik "Logout" untuk keluar
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Respon JSON — Penjelasan Field
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": 2, // 1=ada perubahan, 2=refresh biasa
|
||||||
|
"on": 1, // target power (yg diminta ON/OFF)
|
||||||
|
"state": 1, // real power (aktual ON/OFF)
|
||||||
|
"v": "13.20", // tegangan baterai (volt)
|
||||||
|
"batt": 4, // level baterai (0-4)
|
||||||
|
"battP": 100, // persentase baterai (0-100)
|
||||||
|
"line": 1, // 0=OFF, 1=Line/AC, 2=Battery
|
||||||
|
"auto": 1, // mode auto ON (0/1/2)
|
||||||
|
"battBlink": 0, // indicator charging blink
|
||||||
|
"battCritical": 0, // baterai kritis
|
||||||
|
"timedShutdown": 0, // shutdown timer aktif?
|
||||||
|
"shutdownRemain": 0, // sisa detik shutdown
|
||||||
|
"chgError": 0, // 0=normal, 1=batt not detected, 2=short, 4=overvoltage
|
||||||
|
"charging": 2, // 0=tidak charge, 1=reduced, 2=full charging
|
||||||
|
"chgMode": 1, // mode charge (0..9)
|
||||||
|
"chgOVP": 1, // overvoltage protection ON/OFF
|
||||||
|
"chgReducer": 0, // AVR ON/OFF
|
||||||
|
"chgFullTrig": 3, // FCoPL mode (0..5)
|
||||||
|
"chgOFC": 0 // ongoing full charge flag
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catatan Penting
|
||||||
|
|
||||||
|
| Hal | Keterangan |
|
||||||
|
|-----|-----------|
|
||||||
|
| **Auth** | Basic Auth, username/password hardcoded di `upsSvr.h:16-17` |
|
||||||
|
| **Login gagal** | Server return `401` → JS tampilkan overlay login (bukan popup browser) |
|
||||||
|
| **Kompresi** | Ganti username/password dengan mengedit `upsSvr.h` |
|
||||||
|
| **Rename** | `upsSvr.h` → file backend, `webUI.h` → frontend embedded, `upsCfg.h` → config |
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
#include <LittleFS.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; // flag tetap bernama spiffsOk agar kompatibel dengan kode lain
|
||||||
|
|
||||||
|
bool initConfigs(){
|
||||||
|
// Setup LittleFS
|
||||||
|
spiffsOk = LittleFS.begin();
|
||||||
|
if (!spiffsOk){
|
||||||
|
Serial.println("LittleFS ERROR!");
|
||||||
|
}else{
|
||||||
|
// DEBUG: List all files in LittleFS
|
||||||
|
Serial.println("=== LittleFS Files ===");
|
||||||
|
Dir dir = LittleFS.openDir("/");
|
||||||
|
int fileCount = 0;
|
||||||
|
while (dir.next()) {
|
||||||
|
fileCount++;
|
||||||
|
Serial.print(" FILE: ");
|
||||||
|
Serial.print(dir.fileName());
|
||||||
|
Serial.print(" [");
|
||||||
|
Serial.print(dir.fileSize());
|
||||||
|
Serial.println(" bytes]");
|
||||||
|
}
|
||||||
|
if (fileCount == 0) Serial.println(" (no files found)");
|
||||||
|
Serial.println("=== End LittleFS ===");
|
||||||
|
}
|
||||||
|
return spiffsOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadCfg(){
|
||||||
|
if (!spiffsOk) return;
|
||||||
|
// Open file for reading
|
||||||
|
File file = LittleFS.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 = LittleFS.open("/cfg.json", "w");
|
||||||
|
if (file) {
|
||||||
|
uint32_t sz = serializeJson(jsonDoc, file);
|
||||||
|
file.close();
|
||||||
|
Serial.println("Configuration updated!");
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
//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>
|
||||||
|
|
||||||
|
const char* http_username = "admin";
|
||||||
|
const char* http_password = "admin123";
|
||||||
|
|
||||||
|
String base64Decode(const String& input) {
|
||||||
|
const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
String out;
|
||||||
|
int buf = 0, bits = 0;
|
||||||
|
for (size_t i = 0; i < input.length(); i++) {
|
||||||
|
char c = input[i];
|
||||||
|
if (c == '=') break;
|
||||||
|
const char* p = strchr(b64, c);
|
||||||
|
if (!p) continue;
|
||||||
|
buf = (buf << 6) | (p - b64);
|
||||||
|
bits += 6;
|
||||||
|
if (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
out += (char)((buf >> bits) & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkAuth(AsyncWebServerRequest *request) {
|
||||||
|
if (!request->hasHeader("Authorization")) return false;
|
||||||
|
String auth = request->header("Authorization");
|
||||||
|
if (!auth.startsWith("Basic ")) return false;
|
||||||
|
String b64 = auth.substring(6);
|
||||||
|
b64.trim();
|
||||||
|
String decoded = base64Decode(b64);
|
||||||
|
int sep = decoded.indexOf(':');
|
||||||
|
if (sep < 0) return false;
|
||||||
|
String user = decoded.substring(0, sep);
|
||||||
|
String pass = decoded.substring(sep + 1);
|
||||||
|
return user.equals(http_username) && pass.equals(http_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (!checkAuth(request)) {
|
||||||
|
setNetworkIndicatorInTransmission(true, false);
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(401, "application/json", "{\"success\":0}");
|
||||||
|
response->addHeader("WWW-Authenticate", "Basic realm=\"UPS Login\"");
|
||||||
|
request->send(response);
|
||||||
|
setNetworkIndicatorInTransmission(true, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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(1536);
|
||||||
|
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
|
||||||
|
|
||||||
|
jsonDoc["vSub"] = String(batteryVoltageUnfiltered, 2);
|
||||||
|
jsonDoc["chgSwitch"] = isChargeActive ? 1 : 0;
|
||||||
|
jsonDoc["inputUV"] = inputUnderVoltage ? 1 : 0;
|
||||||
|
jsonDoc["inputVExist"] = inputVoltageKeepup ? 1 : 0;
|
||||||
|
jsonDoc["btn"] = buttonState ? 1 : 0;
|
||||||
|
jsonDoc["btnShort"] = buttonShortPress ? 1 : 0;
|
||||||
|
jsonDoc["btnLong"] = buttonLongPress ? 1 : 0;
|
||||||
|
jsonDoc["keepPower"] = keepPowerState ? 1 : 0;
|
||||||
|
jsonDoc["display"] = displayActive ? 1 : 0;
|
||||||
|
jsonDoc["rssi"] = rssi;
|
||||||
|
jsonDoc["chargeAllowed"] = chargeAllowed ? 1 : 0;
|
||||||
|
|
||||||
|
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 (LittleFS.exists("/index.html")) {
|
||||||
|
isOK = true;
|
||||||
|
request->send(LittleFS, "/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", LittleFS, "/favicon.ico");
|
||||||
|
server.serveStatic("*/favicon.ico", LittleFS, "/favicon.ico");
|
||||||
|
|
||||||
|
//login endpoint: validate credentials
|
||||||
|
server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
setNetworkIndicatorInTransmission(true, false);
|
||||||
|
if (!checkAuth(request)) {
|
||||||
|
request->send(401, "application/json", "{\"success\":0}");
|
||||||
|
} else {
|
||||||
|
request->send(200, "application/json", "{\"success\":1}");
|
||||||
|
}
|
||||||
|
setNetworkIndicatorInTransmission(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
//logout endpoint: clear basic auth by returning 401
|
||||||
|
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
setNetworkIndicatorInTransmission(true, false);
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(401, "application/json", "{\"success\":0,\"message\":\"Logged out\"}");
|
||||||
|
request->send(response);
|
||||||
|
setNetworkIndicatorInTransmission(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
//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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,832 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
const char index_html[] PROGMEM = R"rawliteral(
|
||||||
|
<!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Overlay Styles */
|
||||||
|
#login-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background-color: rgba(18, 18, 18, 0.95);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background-color: #242424;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||||||
|
text-align: center;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.login-box input {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 80%;
|
||||||
|
border: 1px solid #444;
|
||||||
|
background-color: #121212;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.login-box h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Login Overlay -->
|
||||||
|
<div id="login-overlay">
|
||||||
|
<div class="login-box" onkeydown="if(event.key==='Enter')attemptLogin()">
|
||||||
|
<h2>UPS Authentication</h2>
|
||||||
|
<input type="text" id="login-username" placeholder="Username (admin)" value="admin">
|
||||||
|
<input type="password" id="login-password" placeholder="Password">
|
||||||
|
<br>
|
||||||
|
<button onclick="attemptLogin()" style="width: 80%; padding: 10px;">Login</button>
|
||||||
|
<p id="login-error" style="color: #ff5555; display: none; margin-top: 15px; font-size: 14px;">Invalid username or password</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<button id="logoutBtn" onclick="logout()" style="position: absolute; right: 20px; top: 20px; padding: 4px 12px; font-size: 12px;">Logout</button>
|
||||||
|
</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;
|
||||||
|
|
||||||
|
var authHeader = "";
|
||||||
|
|
||||||
|
function attemptLogin() {
|
||||||
|
var usr = document.getElementById('login-username').value;
|
||||||
|
var pwd = document.getElementById('login-password').value;
|
||||||
|
authHeader = "Basic " + btoa(usr + ":" + pwd);
|
||||||
|
document.getElementById('login-error').style.display = 'none';
|
||||||
|
SendState("", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
authHeader = "";
|
||||||
|
document.getElementById('login-password').value = "";
|
||||||
|
document.getElementById('login-error').style.display = 'none';
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
fetch('/logout', { method: "GET" }).catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
"Authorization": authHeader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
if (!response.ok) throw new Error("Failed to update.");
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('login-overlay').style.display = 'none';
|
||||||
|
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, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": authHeader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
document.getElementById('login-overlay').style.display = 'flex';
|
||||||
|
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch data.");
|
||||||
|
return response.json(); // Already a JSON object, no need for JSON.parse()
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('login-overlay').style.display = 'none';
|
||||||
|
//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>
|
||||||
|
)rawliteral";
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
|
||||||
|
bool useStaticIP = false;
|
||||||
|
#ifndef STASSID
|
||||||
|
#define STASSID "DWI 2319"
|
||||||
|
#define STAPSK "*65S265u"
|
||||||
|
#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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,585 @@
|
||||||
|
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>g<EFBFBD><EFBFBD><EFBFBD><EFBFBD>+<EFBFBD>)ł<EFBFBD><EFBFBD><EFBFBD>TiI<EFBFBD>=<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.44 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -58
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.44 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.44 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -59
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.64 V
|
||||||
|
VBatt (Sub-Min): 13.50 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -62
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.64 V
|
||||||
|
VBatt (Sub-Min): 13.50 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -62
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.50 V
|
||||||
|
VBatt (Sub-Min): 13.50 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.50 V
|
||||||
|
VBatt (Min): 13.06 V (48%), LV: 2.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -60
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.52 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -63
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.59 V
|
||||||
|
VBatt (Sub-Min): 13.52 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -63
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -61
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -63
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.66 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.66 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -60
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.61 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -59
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.55 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -59
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -61
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.59 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -57
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -61
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.53 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -62
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.44 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -56
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.45 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.66 V
|
||||||
|
VBatt (Sub-Min): 13.45 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.55 V
|
||||||
|
VBatt (Sub-Min): 13.47 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.47 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -53
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.47 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.53 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.64 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.47 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.58 V
|
||||||
|
VBatt (Sub-Min): 13.47 V
|
||||||
|
VBatt (Min): 13.17 V (67%), LV: 3.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.69 V
|
||||||
|
VBatt (Sub-Min): 13.55 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -54
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.66 V
|
||||||
|
VBatt (Sub-Min): 13.52 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -58
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.72 V
|
||||||
|
VBatt (Sub-Min): 13.52 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -58
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.56 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -57
|
||||||
|
Charge Allowed: False
|
||||||
|
VBatt: 13.61 V
|
||||||
|
VBatt (Sub-Min): 13.33 V
|
||||||
|
VBatt (Min): 13.31 V (90%), LV: 4.00
|
||||||
|
Charge Error: 0
|
||||||
|
Charge Switch: 0
|
||||||
|
Input Under Voltage: 0
|
||||||
|
Input Voltage Exists: 1
|
||||||
|
Button: False
|
||||||
|
Button (short): False
|
||||||
|
Button (long): False
|
||||||
|
Power OUT: True
|
||||||
|
KEEP POWER: True
|
||||||
|
Display: True
|
||||||
|
RSSI: -55
|
||||||
|
Charge Allowed: False
|
||||||
Loading…
Reference in New Issue