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