mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 01:39:30 +02:00
Merge branch 'dalathegreat:main' into main
This commit is contained in:
commit
24c1ce73ae
10 changed files with 61 additions and 73 deletions
50
README.md
50
README.md
|
@ -33,52 +33,18 @@ At the same time, EV manufacturers have been putting high capacity battery packs
|
||||||
|
|
||||||
For examples showing wiring, see each battery type's own Wiki page. For instance the [Nissan LEAF page](https://github.com/dalathegreat/Battery-Emulator/wiki/Battery:-Nissan-LEAF---e%E2%80%90NV200)
|
For examples showing wiring, see each battery type's own Wiki page. For instance the [Nissan LEAF page](https://github.com/dalathegreat/Battery-Emulator/wiki/Battery:-Nissan-LEAF---e%E2%80%90NV200)
|
||||||
|
|
||||||
## How to compile the software 💻
|
## How to install the software 💻
|
||||||
|
|
||||||
Start by watching this [quickstart guide](https://www.youtube.com/watch?v=hcl2GdHc0Y0)
|
Start by watching this [quickstart guide](https://www.youtube.com/watch?v=hcl2GdHc0Y0)
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=hcl2GdHc0Y0)
|
[](https://www.youtube.com/watch?v=hcl2GdHc0Y0)
|
||||||
|
|
||||||
1. Download the Arduino IDE: https://www.arduino.cc/en/software
|
1. Open the [webinstaller page](https://dalathegreat.github.io/BE-Web-Installer/)
|
||||||
2. Open the Arduino IDE.
|
2. Follow the instructions on that page to install the software
|
||||||
3. Click `File` menu -> `Preferences` -> `Additional Development` -> `Additional Board Manager URLs` -> Enter the URL in the input box: `https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json` and click OK.
|
3. After successful installation, connect to the wireless network (Battery-Emulator , password: 123456789)
|
||||||
4. Click `Tools` menu -> `Board: "...."` -> `Boards Manager...`, install the `esp32` package by `Espressif Systems` (not `Arduino ESP32 Boards`), then press `Close`.
|
4. Go to setup page and configure component selection
|
||||||
|
5. (OPTIONAL, connect the board to your home Wifi)
|
||||||
**NOTE: The ESP32 version depends on which release of Battery-Emulator you are running! See the [Release Notes](https://github.com/dalathegreat/Battery-Emulator/releases) for more info on which version to use with the current version (Suggested ESP32 version X.Y.Z)**
|
6. Connect your battery and inverter to the board and you are done! 🔋⚡
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
5. The Arduino board should be set to `ESP32 Dev Module` and `Partition Scheme` to `Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)` (under `Tools` -> `Board` -> `ESP32 Arduino`) with the following settings:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
6. Select which battery type you will use, along with other optional settings. This is done in the `USER_SETTINGS.h` file.
|
|
||||||
7. Copy the `USER_SECRETS.TEMPLATE.h` file to `USER_SECRETS.h` and update connectivity settings inside this file.
|
|
||||||
8. Press `Verify` and `Upload` to send the sketch to the board.
|
|
||||||
NOTE: In some cases, the LilyGo must be powered through the main power connector instead of USB-C
|
|
||||||
when performing the initial firmware upload.
|
|
||||||
NOTE: On Mac, the following USB driver may need to be installed: https://github.com/WCHSoftGroup/ch34xser_macos
|
|
||||||
|
|
||||||
NOTE: If you see garbled messages on the serial console, change the serial console to match the baud rate to the code, currently 115200.
|
|
||||||
|
|
||||||
NOTE: If you see the error `Sketch too big` then check you set the Partition Scheme above correctly.
|
|
||||||
|
|
||||||
This video explains all the above mentioned steps:
|
|
||||||
<https://youtu.be/_mH2AjnAjDk>
|
|
||||||
|
|
||||||
|
|
||||||
### Linux Development Environment Setup
|
|
||||||
In addition to the steps above, ESP32 requires a dependency for a Python module, pyserial install using the cli.\
|
|
||||||
```python3 -m pip install pyserial```
|
|
||||||
|
|
||||||
If you're using Ubuntu , use apt to manage the dependencies of arduino:\
|
|
||||||
pyserial install: ```sudo apt install python3-serial```
|
|
||||||
|
|
||||||
Arduino AppImage must be set as executable after downloading to run correctly\
|
|
||||||
example: ```chmod 775 arduino-ide_2.3.3_Linux_64bit.AppImage```
|
|
||||||
|
|
||||||
Also you might need to install FUSE to run appimages
|
|
||||||
```sudo apt install libfuse2```
|
|
||||||
|
|
||||||
## Dependencies 📖
|
## Dependencies 📖
|
||||||
This code uses the following excellent libraries:
|
This code uses the following excellent libraries:
|
||||||
|
@ -88,7 +54,7 @@ This code uses the following excellent libraries:
|
||||||
- [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License
|
- [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License
|
||||||
- [ESP32Async/AsyncTCP](https://github.com/ESP32Async/AsyncTCP) LGPL-3.0 license
|
- [ESP32Async/AsyncTCP](https://github.com/ESP32Async/AsyncTCP) LGPL-3.0 license
|
||||||
- [ESP32Async/ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) LGPL-3.0 license
|
- [ESP32Async/ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) LGPL-3.0 license
|
||||||
- [miwagner/ESP32-Arduino-CAN](https://github.com/miwagner/ESP32-Arduino-CAN/) MIT-License
|
- [pierremolinaro/acan-esp32](https://github.com/pierremolinaro/acan-esp32) MIT-License
|
||||||
- [pierremolinaro/acan2515](https://github.com/pierremolinaro/acan2515) MIT-License
|
- [pierremolinaro/acan2515](https://github.com/pierremolinaro/acan2515) MIT-License
|
||||||
- [pierremolinaro/acan2517FD](https://github.com/pierremolinaro/acan2517FD) MIT-License
|
- [pierremolinaro/acan2517FD](https://github.com/pierremolinaro/acan2517FD) MIT-License
|
||||||
|
|
||||||
|
|
|
@ -296,6 +296,8 @@ bool user_selected_tesla_GTW_rightHandDrive = true;
|
||||||
uint16_t user_selected_tesla_GTW_mapRegion = 2;
|
uint16_t user_selected_tesla_GTW_mapRegion = 2;
|
||||||
uint16_t user_selected_tesla_GTW_chassisType = 2;
|
uint16_t user_selected_tesla_GTW_chassisType = 2;
|
||||||
uint16_t user_selected_tesla_GTW_packEnergy = 1;
|
uint16_t user_selected_tesla_GTW_packEnergy = 1;
|
||||||
|
/* User-selected EGMP+others settings */
|
||||||
|
bool user_selected_use_estimated_SOC = false;
|
||||||
|
|
||||||
// Use 0V for user selected cell/pack voltage defaults (On boot will be replaced with saved values from NVM)
|
// Use 0V for user selected cell/pack voltage defaults (On boot will be replaced with saved values from NVM)
|
||||||
uint16_t user_selected_max_pack_voltage_dV = 0;
|
uint16_t user_selected_max_pack_voltage_dV = 0;
|
||||||
|
|
|
@ -62,7 +62,7 @@ extern uint16_t user_selected_max_pack_voltage_dV;
|
||||||
extern uint16_t user_selected_min_pack_voltage_dV;
|
extern uint16_t user_selected_min_pack_voltage_dV;
|
||||||
extern uint16_t user_selected_max_cell_voltage_mV;
|
extern uint16_t user_selected_max_cell_voltage_mV;
|
||||||
extern uint16_t user_selected_min_cell_voltage_mV;
|
extern uint16_t user_selected_min_cell_voltage_mV;
|
||||||
|
extern bool user_selected_use_estimated_SOC;
|
||||||
extern bool user_selected_LEAF_interlock_mandatory;
|
extern bool user_selected_LEAF_interlock_mandatory;
|
||||||
extern bool user_selected_tesla_digital_HVIL;
|
extern bool user_selected_tesla_digital_HVIL;
|
||||||
extern uint16_t user_selected_tesla_GTW_country;
|
extern uint16_t user_selected_tesla_GTW_country;
|
||||||
|
|
|
@ -114,16 +114,17 @@ uint8_t KiaEGmpBattery::calculateCRC(CAN_frame rx_frame, uint8_t length, uint8_t
|
||||||
|
|
||||||
void KiaEGmpBattery::update_values() {
|
void KiaEGmpBattery::update_values() {
|
||||||
|
|
||||||
#ifdef ESTIMATE_SOC_FROM_CELLVOLTAGE
|
if (user_selected_use_estimated_SOC) {
|
||||||
// Use the simplified pack-based SOC estimation with proper compensation
|
// Use the simplified pack-based SOC estimation with proper compensation
|
||||||
datalayer.battery.status.real_soc = estimateSOC(batteryVoltage, datalayer.battery.info.number_of_cells, batteryAmps);
|
datalayer.battery.status.real_soc =
|
||||||
|
estimateSOC(batteryVoltage, datalayer.battery.info.number_of_cells, batteryAmps);
|
||||||
|
|
||||||
// For comparison or fallback, we can still calculate from min/max cell voltages
|
// For comparison or fallback, we can still calculate from min/max cell voltages
|
||||||
SOC_estimated_lowest = estimateSOCFromCell(CellVoltMin_mV);
|
SOC_estimated_lowest = estimateSOCFromCell(CellVoltMin_mV);
|
||||||
SOC_estimated_highest = estimateSOCFromCell(CellVoltMax_mV);
|
SOC_estimated_highest = estimateSOCFromCell(CellVoltMax_mV);
|
||||||
#else
|
} else {
|
||||||
datalayer.battery.status.real_soc = (SOC_Display * 10); //increase SOC range from 0-100.0 -> 100.00
|
datalayer.battery.status.real_soc = (SOC_Display * 10); //increase SOC range from 0-100.0 -> 100.00
|
||||||
#endif
|
}
|
||||||
|
|
||||||
datalayer.battery.status.soh_pptt = (batterySOH * 10); //Increase decimals from 100.0% -> 100.00%
|
datalayer.battery.status.soh_pptt = (batterySOH * 10); //Increase decimals from 100.0% -> 100.00%
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
#include "CanBattery.h"
|
#include "CanBattery.h"
|
||||||
#include "KIA-E-GMP-HTML.h"
|
#include "KIA-E-GMP-HTML.h"
|
||||||
|
|
||||||
#define ESTIMATE_SOC_FROM_CELLVOLTAGE
|
extern bool user_selected_use_estimated_SOC;
|
||||||
|
|
||||||
class KiaEGmpBattery : public CanBattery {
|
class KiaEGmpBattery : public CanBattery {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -976,22 +976,22 @@ void TeslaBattery::
|
||||||
if ((datalayer.system.status.inverter_allows_contactor_closing == true) &&
|
if ((datalayer.system.status.inverter_allows_contactor_closing == true) &&
|
||||||
(datalayer.battery.status.bms_status != FAULT) && (!datalayer.system.settings.equipment_stop_active)) {
|
(datalayer.battery.status.bms_status != FAULT) && (!datalayer.system.settings.equipment_stop_active)) {
|
||||||
// Carry on: 0x221 DRIVE state & reset power down timer
|
// Carry on: 0x221 DRIVE state & reset power down timer
|
||||||
vehicleState = 1;
|
vehicleState = CAR_DRIVE;
|
||||||
powerDownTimer = 180; //0x221 50ms cyclic, 20 calls/second
|
powerDownSeconds = 9;
|
||||||
} else {
|
} else {
|
||||||
// Faulted state, or inverter blocks contactor closing
|
// Faulted state, or inverter blocks contactor closing
|
||||||
// Shut down: 0x221 ACCESSORY state for 3 seconds, followed by GOING_DOWN, then OFF
|
// Shut down: 0x221 ACCESSORY state for 3 seconds, followed by GOING_DOWN, then OFF
|
||||||
if (powerDownTimer <= 180 && powerDownTimer > 120) {
|
if (powerDownSeconds <= 9 && powerDownSeconds > 6) {
|
||||||
vehicleState = 2; //ACCESSORY
|
vehicleState = ACCESSORY;
|
||||||
powerDownTimer--;
|
powerDownSeconds--;
|
||||||
}
|
}
|
||||||
if (powerDownTimer <= 120 && powerDownTimer > 60) {
|
if (powerDownSeconds <= 6 && powerDownSeconds > 3) {
|
||||||
vehicleState = 3; //GOING_DOWN
|
vehicleState = GOING_DOWN;
|
||||||
powerDownTimer--;
|
powerDownSeconds--;
|
||||||
}
|
}
|
||||||
if (powerDownTimer <= 60 && powerDownTimer > 0) {
|
if (powerDownSeconds <= 3 && powerDownSeconds > 0) {
|
||||||
vehicleState = 0; //OFF
|
vehicleState = CAR_OFF;
|
||||||
powerDownTimer--;
|
powerDownSeconds--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2059,7 +2059,7 @@ void TeslaBattery::transmit_can(unsigned long currentMillis) {
|
||||||
previousMillis50 = currentMillis;
|
previousMillis50 = currentMillis;
|
||||||
|
|
||||||
//0x221 VCFRONT_LVPowerState
|
//0x221 VCFRONT_LVPowerState
|
||||||
if (vehicleState == 1) { // Drive
|
if (vehicleState == CAR_DRIVE) {
|
||||||
switch (muxNumber_TESLA_221) {
|
switch (muxNumber_TESLA_221) {
|
||||||
case 0:
|
case 0:
|
||||||
generateMuxFrameCounterChecksum(TESLA_221_DRIVE_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
generateMuxFrameCounterChecksum(TESLA_221_DRIVE_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
||||||
|
@ -2077,7 +2077,7 @@ void TeslaBattery::transmit_can(unsigned long currentMillis) {
|
||||||
//Generate next new frame
|
//Generate next new frame
|
||||||
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
||||||
}
|
}
|
||||||
if (vehicleState == 2) { // Accessory
|
if (vehicleState == ACCESSORY) {
|
||||||
switch (muxNumber_TESLA_221) {
|
switch (muxNumber_TESLA_221) {
|
||||||
case 0:
|
case 0:
|
||||||
generateMuxFrameCounterChecksum(TESLA_221_ACCESSORY_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
generateMuxFrameCounterChecksum(TESLA_221_ACCESSORY_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
||||||
|
@ -2095,7 +2095,7 @@ void TeslaBattery::transmit_can(unsigned long currentMillis) {
|
||||||
//Generate next new frame
|
//Generate next new frame
|
||||||
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
||||||
}
|
}
|
||||||
if (vehicleState == 3) { // Going down
|
if (vehicleState == GOING_DOWN) {
|
||||||
switch (muxNumber_TESLA_221) {
|
switch (muxNumber_TESLA_221) {
|
||||||
case 0:
|
case 0:
|
||||||
generateMuxFrameCounterChecksum(TESLA_221_GOING_DOWN_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
generateMuxFrameCounterChecksum(TESLA_221_GOING_DOWN_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
||||||
|
@ -2113,7 +2113,7 @@ void TeslaBattery::transmit_can(unsigned long currentMillis) {
|
||||||
//Generate next new frame
|
//Generate next new frame
|
||||||
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
frameCounter_TESLA_221 = (frameCounter_TESLA_221 + 1) % 16;
|
||||||
}
|
}
|
||||||
if (vehicleState == 0) { // Off
|
if (vehicleState == CAR_OFF) {
|
||||||
switch (muxNumber_TESLA_221) {
|
switch (muxNumber_TESLA_221) {
|
||||||
case 0:
|
case 0:
|
||||||
generateMuxFrameCounterChecksum(TESLA_221_OFF_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
generateMuxFrameCounterChecksum(TESLA_221_OFF_Mux0, frameCounter_TESLA_221, 52, 4, 56, 8);
|
||||||
|
|
|
@ -78,7 +78,11 @@ class TeslaBattery : public CanBattery {
|
||||||
uint8_t muxNumber_TESLA_221 = 0;
|
uint8_t muxNumber_TESLA_221 = 0;
|
||||||
uint8_t frameCounter_TESLA_221 = 15; // Start at 15 for Mux 0
|
uint8_t frameCounter_TESLA_221 = 15; // Start at 15 for Mux 0
|
||||||
uint8_t vehicleState = 1; // "OFF": 0, "DRIVE": 1, "ACCESSORY": 2, "GOING_DOWN": 3
|
uint8_t vehicleState = 1; // "OFF": 0, "DRIVE": 1, "ACCESSORY": 2, "GOING_DOWN": 3
|
||||||
uint16_t powerDownTimer = 180; // Car power down (i.e. contactor open) tracking timer, 3 seconds per sendingState
|
static const uint8_t CAR_OFF = 0;
|
||||||
|
static const uint8_t CAR_DRIVE = 1;
|
||||||
|
static const uint8_t ACCESSORY = 2;
|
||||||
|
static const uint8_t GOING_DOWN = 3;
|
||||||
|
uint8_t powerDownSeconds = 9; // Car power down (i.e. contactor open) tracking timer, 3 seconds per sendingState
|
||||||
//0x2E1 VCFRONT_status, 6 mux tracker
|
//0x2E1 VCFRONT_status, 6 mux tracker
|
||||||
uint8_t muxNumber_TESLA_2E1 = 0;
|
uint8_t muxNumber_TESLA_2E1 = 0;
|
||||||
//0x334 UI
|
//0x334 UI
|
||||||
|
|
|
@ -104,6 +104,7 @@ void init_stored_settings() {
|
||||||
user_selected_can_addon_crystal_frequency_mhz = settings.getUInt("CANFREQ", 8);
|
user_selected_can_addon_crystal_frequency_mhz = settings.getUInt("CANFREQ", 8);
|
||||||
user_selected_canfd_addon_crystal_frequency_mhz = settings.getUInt("CANFDFREQ", 40);
|
user_selected_canfd_addon_crystal_frequency_mhz = settings.getUInt("CANFDFREQ", 40);
|
||||||
user_selected_LEAF_interlock_mandatory = settings.getBool("INTERLOCKREQ", false);
|
user_selected_LEAF_interlock_mandatory = settings.getBool("INTERLOCKREQ", false);
|
||||||
|
user_selected_use_estimated_SOC = settings.getBool("SOCESTIMATED", false);
|
||||||
user_selected_tesla_digital_HVIL = settings.getBool("DIGITALHVIL", false);
|
user_selected_tesla_digital_HVIL = settings.getBool("DIGITALHVIL", false);
|
||||||
user_selected_tesla_GTW_country = settings.getUInt("GTWCOUNTRY", 0);
|
user_selected_tesla_GTW_country = settings.getUInt("GTWCOUNTRY", 0);
|
||||||
user_selected_tesla_GTW_rightHandDrive = settings.getBool("GTWRHD", false);
|
user_selected_tesla_GTW_rightHandDrive = settings.getBool("GTWRHD", false);
|
||||||
|
|
|
@ -251,6 +251,10 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti
|
||||||
return settings.getBool("DBLBTR") ? "checked" : "";
|
return settings.getBool("DBLBTR") ? "checked" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (var == "SOCESTIMATED") {
|
||||||
|
return settings.getBool("SOCESTIMATED") ? "checked" : "";
|
||||||
|
}
|
||||||
|
|
||||||
if (var == "CNTCTRL") {
|
if (var == "CNTCTRL") {
|
||||||
return settings.getBool("CNTCTRL") ? "checked" : "";
|
return settings.getBool("CNTCTRL") ? "checked" : "";
|
||||||
}
|
}
|
||||||
|
@ -912,6 +916,11 @@ const char* getCANInterfaceName(CAN_Interface interface) {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form .if-socestimated { display: none; } /* Integrations where you can turn on SOC estimation */
|
||||||
|
form[data-battery="16"] .if-socestimated {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
form .if-dblbtr { display: none; }
|
form .if-dblbtr { display: none; }
|
||||||
form[data-dblbtr="true"] .if-dblbtr {
|
form[data-dblbtr="true"] .if-dblbtr {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
@ -1022,6 +1031,11 @@ const char* getCANInterfaceName(CAN_Interface interface) {
|
||||||
<input name='DCHGPOWER' pattern="^[0-9]+$" type='text' value='%DCHGPOWER%' />
|
<input name='DCHGPOWER' pattern="^[0-9]+$" type='text' value='%DCHGPOWER%' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="if-socestimated">
|
||||||
|
<label>Use estimated SOC: </label>
|
||||||
|
<input type='checkbox' name='SOCESTIMATED' value='on' %SOCESTIMATED% />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="if-battery">
|
<div class="if-battery">
|
||||||
<label for='BATTCOMM'>Battery interface: </label><select name='BATTCOMM' id='BATTCOMM'>
|
<label for='BATTCOMM'>Battery interface: </label><select name='BATTCOMM' id='BATTCOMM'>
|
||||||
%BATTCOMM%
|
%BATTCOMM%
|
||||||
|
|
|
@ -397,10 +397,10 @@ void init_webserver() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const char* boolSettingNames[] = {
|
const char* boolSettingNames[] = {
|
||||||
"DBLBTR", "CNTCTRL", "CNTCTRLDBL", "PWMCNTCTRL", "PERBMSRESET", "SDLOGENABLED", "STATICIP",
|
"DBLBTR", "CNTCTRL", "CNTCTRLDBL", "PWMCNTCTRL", "PERBMSRESET", "SDLOGENABLED", "STATICIP",
|
||||||
"REMBMSRESET", "EXTPRECHARGE", "USBENABLED", "CANLOGUSB", "WEBENABLED", "CANFDASCAN", "CANLOGSD",
|
"REMBMSRESET", "EXTPRECHARGE", "USBENABLED", "CANLOGUSB", "WEBENABLED", "CANFDASCAN", "CANLOGSD",
|
||||||
"WIFIAPENABLED", "MQTTENABLED", "NOINVDISC", "HADISC", "MQTTTOPICS", "MQTTCELLV", "INVICNT",
|
"WIFIAPENABLED", "MQTTENABLED", "NOINVDISC", "HADISC", "MQTTTOPICS", "MQTTCELLV", "INVICNT",
|
||||||
"GTWRHD", "DIGITALHVIL", "PERFPROFILE", "INTERLOCKREQ",
|
"GTWRHD", "DIGITALHVIL", "PERFPROFILE", "INTERLOCKREQ", "SOCESTIMATED",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles the form POST from UI to save settings of the common image
|
// Handles the form POST from UI to save settings of the common image
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue