diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8d07fe39..1d91653e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,34 +10,23 @@ jobs: job_build_modulecode: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Cache pip - uses: actions/cache@v3 + - name: Cache pio + uses: actions/cache@v4 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: ${{ runner.os }}-pip- - - - name: Cache PlatformIO - uses: actions/cache@v3 - with: - path: ~/.platformio - key: ${{ runner.os }}-platformio-2022-${{ hashFiles('**/lockfiles') }} + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio - name: Set up Python - uses: actions/setup-python@v4 - - - uses: actions/cache@v3 + uses: actions/setup-python@v5 with: - path: ~/.local/share/virtualenvs - key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }} - restore-keys: ${{ runner.os }}-pipenv- + python-version: '3.11' - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install --upgrade platformio + run: pip install --upgrade platformio - name: Create folders run: | @@ -65,13 +54,6 @@ jobs: run: | cp ./STM32All-In-One/.pio/build/V*/*.bin ~/OUTPUT/Modules/BIN/ - - name: Get latest esptool - run: | - git clone https://github.com/espressif/esptool.git - cd esptool - pip install --user -e . - cd ~ - python -m esptool version - name: Build code for ESP32 controller run: pio run --project-dir=/home/runner/work/diyBMSv4ESP32/diyBMSv4ESP32/ESPController --environment esp32-devkitc --project-conf=/home/runner/work/diyBMSv4ESP32/diyBMSv4ESP32/ESPController/platformio.ini @@ -92,8 +74,12 @@ jobs: run: | cp ./ESPController/.pio/build/esp32-devkitc/diybms_controller_filesystemimage_espressif32_esp32-devkitc.bin ~/OUTPUT/Controller/ - - name: Build single ESP32 image + - name: Get latest esptool / Build single ESP32 image run: | + git clone https://github.com/espressif/esptool.git + cd esptool + pip install --user -e . + cd ~ python -m esptool --chip esp32 merge_bin -o ~/OUTPUT/Controller/esp32-controller-firmware-complete.bin --flash_mode=keep --flash_size 4MB 0x1000 ~/OUTPUT/Controller/bootloader.bin 0x8000 ~/OUTPUT/Controller/partitions.bin 0xe000 ~/OUTPUT/Controller/boot_app0.bin 0x10000 ~/OUTPUT/Controller/diybms_controller_firmware_espressif32_esp32-devkitc.bin 0x1C0000 ~/OUTPUT/Controller/diybms_controller_firmware_espressif32_esp32-devkitc.bin 0x370000 ~/OUTPUT/Controller/diybms_controller_filesystemimage_espressif32_esp32-devkitc.bin - name: Board test code for ESP32 controller @@ -104,7 +90,7 @@ jobs: cp ./ESP32BoardTest/.pio/build/esp32-devkitc/diybms_boardtest_espressif32_esp32-devkitc.bin ~/OUTPUT/ControllerBoardTest/ - name: Publish Artifacts 1 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: DIYBMS-Compiled path: ~/OUTPUT @@ -115,7 +101,7 @@ jobs: needs: [job_build_modulecode] steps: - name: Download artifact DIYBMS-Compiled - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: DIYBMS-Compiled @@ -142,7 +128,7 @@ jobs: run: mv release.zip release_${{ env.dt }}.zip - name: Publish Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: DIYBMS-Release-Artifact-${{ env.dt }} path: | diff --git a/ESP32BoardTest/platformio.ini b/ESP32BoardTest/platformio.ini index d2959cc0..b92813be 100644 --- a/ESP32BoardTest/platformio.ini +++ b/ESP32BoardTest/platformio.ini @@ -36,25 +36,22 @@ build_flags = [env] framework = arduino ; 4MB FLASH DEVKITC -platform = espressif32 -;platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream +platform = espressif32@~6.4.0 board = esp32dev monitor_speed = 115200 -monitor_port=COM4 +monitor_port=COM3 monitor_filters = log2file, esp32_exception_decoder board_build.flash_mode = dout board_build.filesystem = littlefs extra_scripts = pre:buildscript_versioning.py - - upload_speed=921600 -upload_port=COM4 +upload_port=COM3 platform_packages = - framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.6 + framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.14 board_build.partitions = diybms_partitions.csv diff --git a/ESP32BoardTest/src/main.cpp b/ESP32BoardTest/src/main.cpp index 5391b167..cf932fa3 100644 --- a/ESP32BoardTest/src/main.cpp +++ b/ESP32BoardTest/src/main.cpp @@ -87,6 +87,9 @@ static constexpr const char *const TAG = "diybms"; #define RS485_TX GPIO_NUM_22 #define RS485_ENABLE GPIO_NUM_25 +#define INA229_CHIPSELECT GPIO_NUM_33 +#define INA229_INTERRUPT_PIN GPIO_NUM_35 + enum RGBLED : uint8_t { OFF = 0, @@ -605,7 +608,7 @@ void testSerial() delay(10); ESP_LOGI(TAG, "Test serial TX1/RX1"); - for (size_t i = 0; i < 50; i++) + for (size_t i = 0; i < 20; i++) { SERIAL_DATA.println("test!"); @@ -623,6 +626,65 @@ void testSerial() } } + enum INA_REGISTER : uint8_t + { + CONFIG = 0, + ADC_CONFIG = 1, + // Shunt Calibration + SHUNT_CAL = 2, + // Shunt Temperature Coefficient + SHUNT_TEMPCO = 3, + // Shunt Voltage Measurement 24bit + VSHUNT = 4, + // Bus Voltage Measurement 24bit + VBUS = 5, + DIETEMP = 6, + // Current Result 24bit + CURRENT = 7, + // Power Result 24bit + POWER = 8, + // Energy Result 40bit + ENERGY = 9, + // Charge Result 40bit + CHARGE = 0x0A, + // Alert triggers + DIAG_ALRT = 0x0b, + // Shunt Overvoltage Threshold + // overcurrent protection + SOVL = 0x0c, + // Shunt Undervoltage Threshold + // undercurrent protection + SUVL = 0x0d, + // Bus Overvoltage Threshold + BOVL = 0x0e, + // Bus Undervoltage Threshold + BUVL = 0x0f, + // Temperature Over-Limit Threshold + TEMP_LIMIT = 0x10, + // Power Over-Limit Threshold + PWR_LIMIT = 0x11, + // Manufacturer ID + MANUFACTURER_ID = 0x3E, + // Device ID + DEVICE_ID = 0x3F + + }; + +uint16_t read16bits(INA_REGISTER r) +{ + SPISettings _spisettings = SPISettings(10000000, MSBFIRST, SPI_MODE1); + vspi.beginTransaction(_spisettings); + digitalWrite(INA229_CHIPSELECT, LOW); + // The transfers are always a step behind, so the transfer reads the previous value/command + vspi.write((uint8_t)((r << 2U) | B00000001)); + uint16_t value = vspi.transfer16(0); + digitalWrite(INA229_CHIPSELECT, HIGH); + vspi.endTransaction(); + + ESP_LOGD(TAG, "Read register 0x%02x = 0x%04x", r, value); + return value; +} + void setup() { // We are not testing ESP32, so switch off WIFI + BT @@ -638,6 +700,18 @@ void setup() ConfigureI2C(); ConfigureVSPI(); + + uint16_t value = read16bits(INA_REGISTER::DEVICE_ID); + if ((value >> 4) == 0x229) + { + ESP_LOGI(TAG, "FOUND CURRENT SHUNT CHIP INA%02x, Revision=%u", value >> 4, value & B1111); + } + else + { + // Stop here - no chip found + ESP_LOGW(TAG, "** CURRENT SHUNT CHIP INA229 CHIP ABSENT **"); + } + mountSDCard(); // Create a test file @@ -681,7 +755,7 @@ void setup() Led(RGBLED::OFF); // Test SERIAL - SERIAL_DATA.begin(2400, SERIAL_8N1, 2, 32); // Serial for comms to modules + SERIAL_DATA.begin(5000, SERIAL_8N1, 2, 32); // Serial for comms to modules testSerial(); init_tft_display(); diff --git a/ESPController/data/cfg_lifepo4_16cells.json b/ESPController/data/cfg_lifepo4_16cells.json index a0e3c1cf..affe8902 100644 --- a/ESPController/data/cfg_lifepo4_16cells.json +++ b/ESPController/data/cfg_lifepo4_16cells.json @@ -1 +1 @@ -{"diybms_settings":{"totalNumberOfBanks":1,"totalNumberOfSeriesModules":16,"baudRate":10000,"interpacketgap":3000,"graph_voltagehigh":3650,"graph_voltagelow":3000,"BypassOverTempShutdown":65,"BypassThresholdmV":3450,"timeZone":0,"minutesTimeZone":0,"daylight":false,"ntpServer":"time.google.com","loggingEnabled":false,"loggingFrequencySeconds":15,"currentMonitoringEnabled":true,"currentMonitoringModBusAddress":90,"rs485baudrate":19200,"rs485databits":3,"rs485parity":0,"rs485stopbits":1,"language":"en","mqtt":{"enabled":true,"uri":"mqtt://192.168.0.26:1883","topic":"emon/diybms","username":"emonpi","password":"emonpimqtt2016"},"influxdb":{"enabled":false,"apitoken":"","bucket":"bucketname","org":"organisation","url":"http://192.168.0.49:8086/api/v2/write","logfreq":15},"outputs":{"default":[153,153,153,153],"type":[0,0,0,0]},"rules":{"EmergencyStop":{"value":0,"hysteresis":0,"state":[0,0,0,0]},"BMSError":{"value":0,"hysteresis":0,"state":[0,0,0,0]},"CurrentMonitorOverCurrentAmps":{"value":100,"hysteresis":100,"state":[0,0,0,0]},"ModuleOverVoltage":{"value":3650,"hysteresis":3500,"state":[255,0,0,0]},"ModuleUnderVoltage":{"value":2900,"hysteresis":3050,"state":[255,0,0,0]},"ModuleOverTemperatureInternal":{"value":75,"hysteresis":75,"state":[0,0,0,0]},"ModuleUnderTemperatureInternal":{"value":5,"hysteresis":5,"state":[0,0,0,0]},"ModuleOverTemperatureExternal":{"value":50,"hysteresis":50,"state":[0,0,0,0]},"ModuleUnderTemperatureExternal":{"value":2,"hysteresis":2,"state":[0,0,0,0]},"CurrentMonitorOverVoltage":{"value":57600,"hysteresis":57000,"state":[0,0,0,0]},"CurrentMonitorUnderVoltage":{"value":48000,"hysteresis":49000,"state":[0,0,0,0]},"BankOverVoltage":{"value":57600,"hysteresis":57000,"state":[0,0,0,0]},"BankUnderVoltage":{"value":48000,"hysteresis":49000,"state":[0,0,0,0]},"Timer2":{"value":1020,"hysteresis":1020,"state":[0,0,0,0]},"Timer1":{"value":480,"hysteresis":480,"state":[0,0,0,0]}},"canbusprotocol":2,"nominalbatcap":280,"chargevolt":568,"chargecurrent":650,"dischargecurrent":650,"dischargevolt":488,"chargetemplow":0,"chargetemphigh":50,"dischargetemplow":-30,"dischargetemphigh":55,"stopchargebalance":false,"socoverride":false,"socforcelow":false,"dynamiccharge":true,"preventdischarge":false,"preventcharging":false,"cellminmv":3050,"cellmaxmv":3460,"kneemv":3320,"cellmaxspikemv":3550,"sensitivity":27,"cur_val1":36,"cur_val2":7,"tilevisibility":[49152,0,62209,0,0]}} \ No newline at end of file +{"diybms_settings":{"totalNumberOfBanks":1,"totalNumberOfSeriesModules":16,"baudRate":10000,"interpacketgap":3000,"graph_voltagehigh":3650,"graph_voltagelow":3000,"BypassOverTempShutdown":65,"BypassThresholdmV":3450,"timeZone":0,"minutesTimeZone":0,"daylight":false,"ntpServer":"time.google.com","loggingEnabled":false,"loggingFrequencySeconds":15,"currentMonitoringEnabled":true,"currentMonitoringModBusAddress":90,"rs485baudrate":19200,"rs485databits":3,"rs485parity":0,"rs485stopbits":1,"language":"en","mqtt":{"enabled":true,"uri":"mqtt://192.168.0.26:1883","topic":"emon/diybms","username":"emonpi","password":"emonpimqtt2016"},"influxdb":{"enabled":false,"apitoken":"","bucket":"bucketname","org":"organisation","url":"http://192.168.0.49:8086/api/v2/write","logfreq":15},"outputs":{"default":[153,153,153,153],"type":[0,0,0,0]},"rules":{"EmergencyStop":{"value":0,"hysteresis":0,"state":[0,0,0,0]},"BMSError":{"value":0,"hysteresis":0,"state":[0,0,0,0]},"CurrentMonitorOverCurrentAmps":{"value":100,"hysteresis":100,"state":[0,0,0,0]},"ModuleOverVoltage":{"value":3650,"hysteresis":3500,"state":[255,0,0,0]},"ModuleUnderVoltage":{"value":2900,"hysteresis":3050,"state":[255,0,0,0]},"ModuleOverTemperatureInternal":{"value":75,"hysteresis":75,"state":[0,0,0,0]},"ModuleUnderTemperatureInternal":{"value":5,"hysteresis":5,"state":[0,0,0,0]},"ModuleOverTemperatureExternal":{"value":50,"hysteresis":50,"state":[0,0,0,0]},"ModuleUnderTemperatureExternal":{"value":2,"hysteresis":2,"state":[0,0,0,0]},"CurrentMonitorOverVoltage":{"value":57600,"hysteresis":57000,"state":[0,0,0,0]},"CurrentMonitorUnderVoltage":{"value":48000,"hysteresis":49000,"state":[0,0,0,0]},"BankOverVoltage":{"value":57600,"hysteresis":57000,"state":[0,0,0,0]},"BankUnderVoltage":{"value":48000,"hysteresis":49000,"state":[0,0,0,0]},"Timer2":{"value":1020,"hysteresis":1020,"state":[0,0,0,0]},"Timer1":{"value":480,"hysteresis":480,"state":[0,0,0,0]}},"protocol":2,"nominalbatcap":280,"chargevolt":568,"chargecurrent":650,"dischargecurrent":650,"dischargevolt":488,"chargetemplow":0,"chargetemphigh":50,"dischargetemplow":-30,"dischargetemphigh":55,"stopchargebalance":false,"socoverride":false,"socforcelow":false,"dynamiccharge":true,"preventdischarge":false,"preventcharging":false,"cellminmv":3050,"cellmaxmv":3460,"kneemv":3320,"cellmaxspikemv":3550,"sensitivity":27,"cur_val1":36,"cur_val2":7,"tilevisibility":[49152,0,62209,0,0]}} \ No newline at end of file diff --git a/ESPController/include/CurrentMonitorINA229.h b/ESPController/include/CurrentMonitorINA229.h index 0e98b5aa..47edf6f6 100644 --- a/ESPController/include/CurrentMonitorINA229.h +++ b/ESPController/include/CurrentMonitorINA229.h @@ -217,9 +217,12 @@ class CurrentMonitorINA229 uint16_t shunttempcoefficient, bool TemperatureCompEnabled); - void GuessSOC(); + void DefaultSOC(); void TakeReadings(); + uint32_t raw_milliamphour_out() const{ return milliamphour_out; } + uint32_t raw_milliamphour_in() const{ return milliamphour_in; } + uint32_t calc_milliamphour_out() const{ return milliamphour_out - milliamphour_out_offset; } uint32_t calc_milliamphour_in() const{ return milliamphour_in - milliamphour_in_offset; } uint32_t calc_daily_milliamphour_out() const{ return daily_milliamphour_out; } @@ -264,12 +267,15 @@ class CurrentMonitorINA229 return registers.R_DIAG_ALRT & ALL_ALERT_BITS; } void SetSOC(uint16_t value); + void SetSOCByMilliAmpCounter(uint32_t in,uint32_t out); void ResetDailyAmpHourCounters() { daily_milliamphour_out=0; daily_milliamphour_in=0; } + uint16_t raw_stateofcharge() const { return SOC; } + private: uint16_t SOC = 0; float voltage = 0; diff --git a/ESPController/include/HAL_ESP32.h b/ESPController/include/HAL_ESP32.h index fc9e4170..e6a20ac1 100644 --- a/ESPController/include/HAL_ESP32.h +++ b/ESPController/include/HAL_ESP32.h @@ -107,135 +107,24 @@ class HAL_ESP32 void CANBUSEnable(bool value); - bool IsVSPIMutexAvailable() - { - if (xVSPIMutex == NULL) - return false; + bool IsVSPIMutexAvailable(); + bool GetVSPIMutex(); + bool ReleaseVSPIMutex(); - return (uxSemaphoreGetCount(xVSPIMutex) == 1); - } + bool GetDisplayMutex(); + bool ReleaseDisplayMutex(); - bool GetDisplayMutex() - { - if (xDisplayMutex == NULL) - return false; - - // Wait 100ms max - if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(100)) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to get Display mutex"); - return false; - } - return true; - } - bool ReleaseDisplayMutex() - { - if (xDisplayMutex == NULL) - return false; - - return (xSemaphoreGive(xDisplayMutex) == pdTRUE); - } + bool Geti2cMutex(); + bool Releasei2cMutex(); - bool GetVSPIMutex() - { - if (xVSPIMutex == NULL) - return false; - - // Wait 100ms max - if (xSemaphoreTake(xVSPIMutex, pdMS_TO_TICKS(100)) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to get VSPI mutex"); - return false; - } - return true; - } - bool ReleaseVSPIMutex() - { - if (xVSPIMutex == NULL) - return false; - - if (xSemaphoreGive(xVSPIMutex) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to release VSPI mutex"); - return false; - } - return true; - } - - bool Geti2cMutex() - { - if (xi2cMutex == NULL) - return false; - - // Wait 100ms max - if (xSemaphoreTake(xi2cMutex, pdMS_TO_TICKS(100)) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to get I2C mutex"); - return false; - } - return true; - } - bool Releasei2cMutex() - { - if (xi2cMutex == NULL) - return false; - - if (xSemaphoreGive(xi2cMutex) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to release I2C mutex"); - return false; - } - return true; - } - - bool GetRS485Mutex() - { - if (RS485Mutex == NULL) - return false; - - // Wait 100ms max - if (xSemaphoreTake(RS485Mutex, pdMS_TO_TICKS(100)) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to get RS485 mutex"); - return false; - } - return true; - } - bool ReleaseRS485Mutex() - { - if (RS485Mutex == NULL) - return false; - - if (xSemaphoreGive(RS485Mutex) == pdFALSE) - { - ESP_LOGE(TAG, "Unable to release RS485 mutex"); - return false; - } - return true; - } + bool GetRS485Mutex(); + bool ReleaseRS485Mutex(); // Infinite loop flashing the LED RED/WHITE - void Halt(RGBLED colour) - { - ESP_LOGE(TAG, "SYSTEM HALTED"); - - while (true) - { - Led(RGBLED::Red); - delay(700); - Led(colour); - delay(300); - } - } + void Halt(RGBLED colour); - uint8_t LastTCA6408Value() - { - return TCA6408_Input; - } - uint8_t LastTCA9534APWRValue() - { - return TCA9534APWR_Input; - } + uint8_t LastTCA6408Value(); + uint8_t LastTCA9534APWRValue(); bool MountSDCard(); void UnmountSDCard(); TouchScreenValues TouchScreenUpdate(); diff --git a/ESPController/include/Rules.h b/ESPController/include/Rules.h index 89800d89..6b595770 100644 --- a/ESPController/include/Rules.h +++ b/ESPController/include/Rules.h @@ -150,14 +150,7 @@ class Rules /// @brief Set a rule status /// @param r Rule to change /// @param value True = rule is active - void setRuleStatus(Rule r, bool value) - { - if (ruleOutcome(r) != value) - { - rule_outcome.at(r) = value; - ESP_LOGI(TAG, "Rule %s state=%u", RuleTextDescription.at(r).c_str(), (uint8_t)value); - } - } + void setRuleStatus(Rule r, bool value); // True if at least 1 module has an external temp sensor fitted bool moduleHasExternalTempSensor; @@ -180,13 +173,7 @@ class Rules { return chargemode; } - void setChargingMode(ChargingMode newMode) - { - if (chargemode == newMode) - return; - ESP_LOGI(TAG, "Charging mode changed %u", newMode); - chargemode = newMode; - } + void setChargingMode(ChargingMode newMode); // Number of modules which have not yet reported back to the controller uint8_t invalidModuleCount; diff --git a/ESPController/include/defines.h b/ESPController/include/defines.h index 861d657d..2488da3d 100644 --- a/ESPController/include/defines.h +++ b/ESPController/include/defines.h @@ -20,7 +20,7 @@ #define SERIAL_RS485 Serial1 // Total number of cells a single controler can handle (memory limitation) -#define maximum_controller_cell_modules 128 +#define maximum_controller_cell_modules 192 typedef union { @@ -96,12 +96,13 @@ enum CanBusInverter : uint8_t }; -enum CanBusProtocolEmulation : uint8_t +enum ProtocolEmulation : uint8_t { - CANBUS_DISABLED = 0x00, + EMULATION_DISABLED = 0x00, CANBUS_VICTRON = 0x01, CANBUS_PYLONTECH = 0x02, - CANBUS_PYLONFORCEH2 = 0x03 + CANBUS_PYLONFORCEH2 = 0x03, + RS485_PYLONTECH = 0x04 }; enum CurrentMonitorDevice : uint8_t @@ -161,6 +162,7 @@ struct diybms_eeprom_settings uint16_t currentMonitoring_shuntmv; uint16_t currentMonitoring_shuntmaxcur; + /// @brief Amp-hours battery capacity uint16_t currentMonitoring_batterycapacity; uint16_t currentMonitoring_fullchargevolt; uint16_t currentMonitoring_tailcurrent; @@ -182,7 +184,7 @@ struct diybms_eeprom_settings char language[2 + 1]; - CanBusProtocolEmulation canbusprotocol; + ProtocolEmulation protocol; CanBusInverter canbusinverter; //CANBUS baud rate, 250=250k, 500=500k uint16_t canbusbaud; @@ -254,6 +256,20 @@ struct diybms_eeprom_settings uint8_t canbus_equipment_addr; // battery index on the same canbus for PYLONFORCE, 0 - 15, default 0 char homeassist_apikey[24+1]; + + /// @brief State of health variables - total lifetime mAh output (discharge) + // Might need to watch overflow on the uint32 (max value 4,294,967,295mAh) = approx 15339 cycles of 280Ah battery + uint32_t soh_total_milliamphour_out; + /// @brief State of health variables - total lifetime mAh input (charge) + uint32_t soh_total_milliamphour_in; + /// @brief State of health variables - total expected lifetime cycles of battery (6000) + uint16_t soh_lifetime_battery_cycles; + /// @brief State of health variables - expected remaining capacity (%) at end of life/max cycles + uint8_t soh_eol_capacity; + /// @brief State of health variables - estimated number of cycles + uint16_t soh_estimated_battery_cycles; + /// @brief Calculated percentage calculation of health + float soh_percent; }; typedef union diff --git a/ESPController/include/pylon_rs485.h b/ESPController/include/pylon_rs485.h new file mode 100644 index 00000000..45cabaf9 --- /dev/null +++ b/ESPController/include/pylon_rs485.h @@ -0,0 +1,54 @@ +#ifndef DIYBMS_PYLON_RS485_H_ +#define DIYBMS_PYLON_RS485_H_ + +#include "defines.h" +#include "Rules.h" +#include "HAL_ESP32.h" + + +class PylonRS485 { + public: + /** + * @brief Constructor of the PylonRS485 class + */ + PylonRS485(uart_port_t portNum, diybms_eeprom_settings& settings, Rules& rules, currentmonitoring_struct& currentMonitor, + ControllerState& controllerState, HAL_ESP32& hal); + + /** + * @brief Call this to periodically check queries from inverter and to form a reply + */ + void handle_rx(); + + private: + typedef struct { + uint8_t soh; + uint16_t ver; + uint16_t addr; + uint16_t cid1; + uint16_t cid2; + char length[4]; + } __attribute__((packed)) THeader; + + uart_port_t uart_num; + diybms_eeprom_settings& settings; + Rules& rules; + currentmonitoring_struct& current_monitor; + ControllerState& controller_state; + HAL_ESP32& hal; + + uint16_t pack_voltage; + uint16_t charge_voltage; + uint16_t discharge_voltage; + uint16_t charge_current_limit; + uint16_t discharge_current_limit; + bool stop_charging; + bool stop_discharging; + uint8_t flags; + char tmp_buf[150]; + + uint32_t hex2int(char *hex, char len); + void insertLength(char *buf, int payload_len); + int appendChecksum(char *buf, int buf_size, int payload_len); +}; + +#endif \ No newline at end of file diff --git a/ESPController/include/settings.h b/ESPController/include/settings.h index f75c9649..89179c07 100644 --- a/ESPController/include/settings.h +++ b/ESPController/include/settings.h @@ -14,7 +14,7 @@ bool getSetting(nvs_handle_t handle, const char *key, float *out_value); bool getSetting(nvs_handle_t handle, const char *key, uint8_t *out_value); bool getSetting(nvs_handle_t handle, const char *key, int8_t *out_value); bool getSetting(nvs_handle_t handle, const char *key, uint16_t *out_value); -// bool getSetting(nvs_handle_t handle, const char *key, uint32_t *out_value); +bool getSetting(nvs_handle_t handle, const char *key, uint32_t *out_value); bool getSetting(nvs_handle_t handle, const char *key, int32_t *out_value); bool getSetting(nvs_handle_t handle, const char *key, int16_t *out_value); bool getSetting(nvs_handle_t handle, const char *key, bool *out_value); @@ -22,7 +22,7 @@ bool getSetting(nvs_handle_t handle, const char *key, bool *out_value); bool getSettingBlob(nvs_handle_t handle, const char *key, void *out_value, size_t size); void InitializeNVS(); -void SaveConfiguration(diybms_eeprom_settings *settings); +void SaveConfiguration(const diybms_eeprom_settings *settings); void LoadConfiguration(diybms_eeprom_settings *settings); void ValidateConfiguration(diybms_eeprom_settings *settings); void DefaultConfiguration(diybms_eeprom_settings *settings); @@ -30,15 +30,20 @@ void DefaultConfiguration(diybms_eeprom_settings *settings); void SaveWIFI(const wifi_eeprom_settings *wifi); bool LoadWIFI(wifi_eeprom_settings *wifi); -void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settings *settings); -void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings); +void GenerateSettingsJSONDocument(JsonDocument &doc, diybms_eeprom_settings *settings); +void JSONToSettings(JsonDocument &doc, diybms_eeprom_settings *settings); void writeSetting(nvs_handle_t handle, const char *key, bool value); void writeSetting(nvs_handle_t handle, const char *key, uint8_t value); void writeSetting(nvs_handle_t handle, const char *key, uint16_t value); void writeSetting(nvs_handle_t handle, const char *key, int16_t value); +void writeSetting(nvs_handle_t handle, const char *key, int32_t value); +void writeSetting(nvs_handle_t handle, const char *key, uint32_t value); void writeSetting(nvs_handle_t handle, const char *key, int8_t value); void writeSetting(nvs_handle_t handle, const char *key, const char *value); void writeSettingBlob(nvs_handle_t handle, const char *key, const void *value, size_t length); +bool GetStateOfCharge(uint32_t *in,uint32_t *out); +void SaveStateOfCharge(uint32_t,uint32_t); + #endif \ No newline at end of file diff --git a/ESPController/include/webserver.h b/ESPController/include/webserver.h index 3c4830d7..494a45f7 100644 --- a/ESPController/include/webserver.h +++ b/ESPController/include/webserver.h @@ -27,8 +27,8 @@ int printBoolean(char *buffer, size_t bufferLen, const char *fieldName, boolean void generateUUID(); void StartServer(); -void resetModuleMinMaxVoltage(uint8_t module); -void clearModuleValues(uint8_t module); +void resetModuleMinMaxVoltage(uint8_t m); +void clearModuleValues(uint8_t m); httpd_handle_t start_webserver(void); void stop_webserver(httpd_handle_t server); @@ -47,6 +47,6 @@ extern RelayState previousRelayState[RELAY_TOTAL]; extern currentmonitoring_struct currentMonitor; extern void suspendTasksDuringFirmwareUpdate(); extern void resumeTasksAfterFirmwareUpdateFailure(); -extern void SaveConfiguration(diybms_eeprom_settings *settings); +extern void SaveConfiguration(const diybms_eeprom_settings *settings); extern esp_err_t content_handler_coredumpdownloadfile(httpd_req_t *req); #endif \ No newline at end of file diff --git a/ESPController/include/webserver_helper_funcs.h b/ESPController/include/webserver_helper_funcs.h index 610c6a81..1428c19d 100644 --- a/ESPController/include/webserver_helper_funcs.h +++ b/ESPController/include/webserver_helper_funcs.h @@ -30,6 +30,8 @@ bool validateXSSWithPOST(httpd_req_t *req, const char *postbuffer, bool urlEncod void setCookieValue(); void setCookie(httpd_req_t *req); +void randomCharacters(char* value, int length); + // These are borrowed from the new ESP IDF framework, will need to be removed if framework is upgraded esp_err_t httpd_req_get_cookie_val(httpd_req_t *req, const char *cookie_name, char *val, size_t *val_size); esp_err_t httpd_cookie_key_value(const char *cookie_str, const char *key, char *val, size_t *val_size); diff --git a/ESPController/include/webserver_json_post.h b/ESPController/include/webserver_json_post.h index 2d10e5d9..641837f4 100644 --- a/ESPController/include/webserver_json_post.h +++ b/ESPController/include/webserver_json_post.h @@ -16,6 +16,7 @@ extern HAL_ESP32 hal; extern fs::SDFS SD; extern TaskHandle_t avrprog_task_handle; +extern TaskHandle_t rs485_rx_task_handle; extern uint32_t canbus_messages_received; extern uint32_t canbus_messages_sent; extern uint32_t canbus_messages_failed_sent; @@ -39,6 +40,7 @@ extern void configureSNTP(long gmtOffset_sec, int daylightOffset_sec, const char extern void DefaultConfiguration(diybms_eeprom_settings *_myset); extern bool SaveWIFIJson(const wifi_eeprom_settings* setting); extern void randomCharacters(char *value, int length); +extern void CalculateStateOfHealth(diybms_eeprom_settings *settings); esp_err_t post_savebankconfig_json_handler(httpd_req_t *req, bool urlEncoded); esp_err_t post_saventp_json_handler(httpd_req_t *req, bool urlEncoded); diff --git a/ESPController/platformio.ini b/ESPController/platformio.ini index e07c64dc..25873c96 100644 --- a/ESPController/platformio.ini +++ b/ESPController/platformio.ini @@ -35,11 +35,12 @@ build_flags = [env] framework = arduino ; 4MB FLASH DEVKITC -platform = espressif32@~6.4.0 +; recommended to pin to a version, see https://github.com/platformio/platform-espressif32/releases +platform = espressif32@^6.8.1 board = esp32dev monitor_speed = 115200 -monitor_port=COM3 +monitor_port=COM4 monitor_filters = esp32_exception_decoder board_build.flash_mode = dout board_build.filesystem = littlefs @@ -52,16 +53,16 @@ extra_scripts = post:extract_bootloader.py upload_speed=921600 -upload_port=COM3 +upload_port=COM4 board_build.arduino.upstream_packages = no platform_packages = - framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.14 + framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.17 board_build.partitions = diybms_partitions.csv lib_deps = - bblanchon/ArduinoJson@^6.21.3 - bodmer/TFT_eSPI@^2.5.31 + bblanchon/ArduinoJson@^7.1.0 + bodmer/TFT_eSPI@^2.5.43 https://github.com/stuartpittaway/SerialEncoder.git [env:esp32-devkitc] diff --git a/ESPController/prebuild_compress.py b/ESPController/prebuild_compress.py index e63fa8e0..b24703c4 100644 --- a/ESPController/prebuild_compress.py +++ b/ESPController/prebuild_compress.py @@ -13,9 +13,9 @@ def prepare_www_files(): # WARNING - this script will DELETE your 'data' dir and recreate an empty one to copy/gzip files from 'data_src' - # so make sure to edit your files in 'data_src' folder as changes madt to files in 'data' woll be LOST + # so make sure to edit your files in 'data_src' folder as changes madt to files in 'data' will be LOST # - # If 'data_src' dir doesn't exist, and 'data' dir is found, the script will autimatically + # If 'data_src' dir doesn't exist, and 'data' dir is found, the script will automatically # rename 'data' to 'data_src # add filetypes (extensions only) to be gzipped before uploading. Everything else will be copied directly @@ -65,7 +65,7 @@ def prepare_www_files(): for file in files_to_gzip: print(' GZipping file: ' + file + ' to data dir') - with open(file, 'rb') as f_in, gzip.open(os.path.join(data_dir, os.path.basename(file) + '.gz'), 'wb') as f_out: + with open(file, 'rb') as f_in, gzip.GzipFile(filename=os.path.join(data_dir, os.path.basename(file) + '.gz'), mode='w', compresslevel=9) as f_out: shutil.copyfileobj(f_in, f_out) print('[/COPY/GZIP DATA FILES]') diff --git a/ESPController/src/CurrentMonitorINA229.cpp b/ESPController/src/CurrentMonitorINA229.cpp index b4c271f3..30424a80 100644 --- a/ESPController/src/CurrentMonitorINA229.cpp +++ b/ESPController/src/CurrentMonitorINA229.cpp @@ -71,20 +71,44 @@ void CurrentMonitorINA229::CalculateLSB() // registers.R_SHUNT_CAL = ((uint32_t)registers.R_SHUNT_CAL * 985) / 1000; } + +void CurrentMonitorINA229::SetSOCByMilliAmpCounter(uint32_t in,uint32_t out) { + // Assume battery is fully charged + milliamphour_in = in; + // And we have consumed this much... + milliamphour_out = out; + + // Zero out readings using the offsets + milliamphour_out_offset = milliamphour_out; + milliamphour_in_offset = milliamphour_in; + + ESP_LOGI(TAG, "SetSOCByMilliAmpCounter mA in=%u, mA out=%u",milliamphour_in,milliamphour_out); +} + // Sets SOC by setting "fake" in/out amphour counts // value=8212 = 82.12% void CurrentMonitorINA229::SetSOC(uint16_t value) { // Assume battery is fully charged - milliamphour_in = 1000 * (uint32_t)registers.batterycapacity_amphour; + //milliamphour_in = 1000 * (uint32_t)registers.batterycapacity_amphour; + // And we have consumed this much... + //milliamphour_out = (uint32_t)((1.0F - ((float)value / 10000.0F)) * (float)milliamphour_in); + + // Updated SoC logic by delboy711 https://github.com/stuartpittaway/diyBMSv4ESP32/issues/232 + // Assume battery is fully charged + milliamphour_in = lround(100000.0*(float)registers.batterycapacity_amphour/(registers.charge_efficiency_factor)); // And we have consumed this much... - milliamphour_out = (uint32_t)((1.0F - ((float)value / 10000.0F)) * milliamphour_in); + milliamphour_out = (uint32_t)((1.0 - ((float)value / 10000.0)) * (1000.0*(float)registers.batterycapacity_amphour)); // Zero out readings using the offsets milliamphour_out_offset = milliamphour_out; milliamphour_in_offset = milliamphour_in; + + ESP_LOGI(TAG, "SetSOC mA in=%u, mA out=%u",milliamphour_in,milliamphour_out); } + + uint8_t CurrentMonitorINA229::readRegisterValue(INA_REGISTER r) const { // These are not really registers, but shape the SPI frame to indicate read/write @@ -391,47 +415,11 @@ void CurrentMonitorINA229::CalculateAmpHourCounts() } } -// Guess the SoC % based on battery voltage - not accurate, just a guess! -void CurrentMonitorINA229::GuessSOC() +// Set SoC to default values +void CurrentMonitorINA229::DefaultSOC() { // Default SOC% at 60% - uint16_t soc = 6000; - - // We apply a "guestimate" to SoC based on voltage - not really accurate, but somewhere to start - // only applicable to 24V/48V (16S) LIFEPO4 setups. These voltages should be the unloaded (no current flowing) voltage. - // Assumption that its LIFEPO4 cells we are using... - float v = BusVoltage(); - - if (v > 20 && v < 30) - { - // Scale up 24V battery to use the 48V scale - v = v * 2; - } - - if (v > 40 && v < 60) - { - // 16S LIFEPO4... - if (v >= 40.0) - soc = 500; - if (v >= 48.0) - soc = 900; - if (v >= 50.0) - soc = 1400; - if (v >= 51.2) - soc = 1700; - if (v >= 51.6) - soc = 2000; - if (v >= 52.0) - soc = 3000; - if (v >= 52.4) - soc = 4000; - if (v >= 52.8) - soc = 7000; - if (v >= 53.2) - soc = 9000; - } - - SetSOC(soc); + SetSOC(6000); // Reset the daily counters daily_milliamphour_in = 0; diff --git a/ESPController/src/HAL_ESP32.cpp b/ESPController/src/HAL_ESP32.cpp index 2e6d99e5..e771675c 100644 --- a/ESPController/src/HAL_ESP32.cpp +++ b/ESPController/src/HAL_ESP32.cpp @@ -50,6 +50,136 @@ void HAL_ESP32::UnmountSDCard() } } +bool HAL_ESP32::IsVSPIMutexAvailable() +{ + if (xVSPIMutex == NULL) + return false; + + return (uxSemaphoreGetCount(xVSPIMutex) == 1); +} + +bool HAL_ESP32::GetDisplayMutex() +{ + if (xDisplayMutex == NULL) + return false; + + // Wait 100ms max + if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(100)) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to get Display mutex"); + return false; + } + return true; +} +bool HAL_ESP32::ReleaseDisplayMutex() +{ + if (xDisplayMutex == NULL) + return false; + + return (xSemaphoreGive(xDisplayMutex) == pdTRUE); +} + +bool HAL_ESP32::GetVSPIMutex() +{ + if (xVSPIMutex == NULL) + return false; + + // Wait 100ms max + if (xSemaphoreTake(xVSPIMutex, pdMS_TO_TICKS(100)) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to get VSPI mutex"); + return false; + } + return true; +} +bool HAL_ESP32::ReleaseVSPIMutex() +{ + if (xVSPIMutex == NULL) + return false; + + if (xSemaphoreGive(xVSPIMutex) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to release VSPI mutex"); + return false; + } + return true; +} + +bool HAL_ESP32::Geti2cMutex() +{ + if (xi2cMutex == NULL) + return false; + + // Wait 100ms max + if (xSemaphoreTake(xi2cMutex, pdMS_TO_TICKS(100)) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to get I2C mutex"); + return false; + } + return true; +} +bool HAL_ESP32::Releasei2cMutex() +{ + if (xi2cMutex == NULL) + return false; + + if (xSemaphoreGive(xi2cMutex) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to release I2C mutex"); + return false; + } + return true; +} + +bool HAL_ESP32::GetRS485Mutex() +{ + if (RS485Mutex == NULL) + return false; + + // Wait 100ms max + if (xSemaphoreTake(RS485Mutex, pdMS_TO_TICKS(100)) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to get RS485 mutex"); + return false; + } + return true; +} +bool HAL_ESP32::ReleaseRS485Mutex() +{ + if (RS485Mutex == NULL) + return false; + + if (xSemaphoreGive(RS485Mutex) == pdFALSE) + { + ESP_LOGE(TAG, "Unable to release RS485 mutex"); + return false; + } + return true; +} + +// Infinite loop flashing the LED RED/WHITE +void HAL_ESP32::Halt(RGBLED colour) +{ + ESP_LOGE(TAG, "SYSTEM HALTED"); + + while (true) + { + Led(RGBLED::Red); + delay(700); + Led(colour); + delay(300); + } +} + +uint8_t HAL_ESP32::LastTCA6408Value() +{ + return TCA6408_Input; +} +uint8_t HAL_ESP32::LastTCA9534APWRValue() +{ + return TCA9534APWR_Input; +} + uint8_t HAL_ESP32::readByte(i2c_port_t i2c_num, uint8_t dev, uint8_t reg) { // We use the native i2c commands for ESP32 as the Arduino library diff --git a/ESPController/src/Rules.cpp b/ESPController/src/Rules.cpp index d4bfeb93..f4871c46 100644 --- a/ESPController/src/Rules.cpp +++ b/ESPController/src/Rules.cpp @@ -415,7 +415,7 @@ void Rules::RunRules( bool Rules::SharedChargingDischargingRules(const diybms_eeprom_settings *mysettings) { - if (mysettings->canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + if (mysettings->protocol == ProtocolEmulation::EMULATION_DISABLED) return false; if (invalidModuleCount > 0) @@ -524,7 +524,7 @@ void Rules::CalculateDynamicChargeCurrent(const diybms_eeprom_settings *mysettin // Remember dynamicChargeCurrent scale is 0.1 dynamicChargeCurrent = mysettings->chargecurrent; - if (!mysettings->dynamiccharge || mysettings->canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + if (!mysettings->dynamiccharge || mysettings->protocol == ProtocolEmulation::EMULATION_DISABLED) { // Its switched off, use default return; @@ -576,7 +576,7 @@ void Rules::CalculateDynamicChargeCurrent(const diybms_eeprom_settings *mysettin // Output is cached in variable dynamicChargeVoltage as its used in multiple places void Rules::CalculateDynamicChargeVoltage(const diybms_eeprom_settings *mysettings, const CellModuleInfo *cellarray) { - if (!mysettings->dynamiccharge || mysettings->canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + if (!mysettings->dynamiccharge || mysettings->protocol == ProtocolEmulation::EMULATION_DISABLED) { // Dynamic charge switched off, use default or float voltage dynamicChargeVoltage = (chargemode == ChargingMode::floating) ? mysettings->floatvoltage : mysettings->chargevolt; @@ -667,7 +667,7 @@ void Rules::CalculateDynamicChargeVoltage(const diybms_eeprom_settings *mysettin /// @return SoC is rounded down to nearest integer and limits output range between 0 and 100. uint16_t Rules::StateOfChargeWithRulesApplied(const diybms_eeprom_settings *mysettings, float realSOC) const { - uint16_t value = floor(realSOC); + auto value = (uint16_t)floor(realSOC); // Deliberately force SoC to be reported as 2%, to trick external CANBUS devices into trickle charging (if they support it) if (mysettings->socforcelow) @@ -707,7 +707,7 @@ uint16_t Rules::StateOfChargeWithRulesApplied(const diybms_eeprom_settings *myse void Rules::CalculateChargingMode(const diybms_eeprom_settings *mysettings, const currentmonitoring_struct *currentMonitor) { // If we are not using CANBUS - ignore the charge mode, it doesn't mean anything - if (mysettings->canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + if (mysettings->protocol == ProtocolEmulation::EMULATION_DISABLED) { return; } @@ -725,7 +725,7 @@ void Rules::CalculateChargingMode(const diybms_eeprom_settings *mysettings, cons // or battery is below resume level so normal charging operation in progress // No difference in STANDARD or DYNAMIC modes - purely visual on screen/cosmetic - if (mysettings->dynamiccharge == true && mysettings->canbusprotocol != CanBusProtocolEmulation::CANBUS_DISABLED) + if (mysettings->dynamiccharge == true && mysettings->protocol != ProtocolEmulation::EMULATION_DISABLED) { setChargingMode(ChargingMode::dynamic); } @@ -767,4 +767,21 @@ void Rules::CalculateChargingMode(const diybms_eeprom_settings *mysettings, cons return; } } +} + +void Rules::setRuleStatus(Rule r, bool value) +{ + if (ruleOutcome(r) != value) + { + rule_outcome.at(r) = value; + ESP_LOGI(TAG, "Rule %s state=%u", RuleTextDescription.at(r).c_str(), (uint8_t)value); + } +} + +void Rules::setChargingMode(ChargingMode newMode) +{ + if (chargemode == newMode) + return; + ESP_LOGI(TAG, "Charging mode changed %u", newMode); + chargemode = newMode; } \ No newline at end of file diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index b7959d40..3e4d8d97 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -75,6 +75,7 @@ extern "C" #include "victron_canbus.h" #include "pylon_canbus.h" #include "pylonforce_canbus.h" +#include "pylon_rs485.h" #include "string_utils.h" #include @@ -191,6 +192,9 @@ uint16_t sequence = 0; ControllerState _controller_state = ControllerState::Unknown; +// Create PylonTech RS485 protocol emulation instance, passing references to necessary variables +PylonRS485 pylon_rs485(rs485_uart_num, mysettings, rules, currentMonitor, _controller_state, hal); + uint32_t time100 = 0; uint32_t time20 = 0; uint32_t time10 = 0; @@ -1280,7 +1284,7 @@ void ProcessRules() rules.SetWarning(InternalWarningCode::NoExternalTempSensor); } - if (mysettings.canbusprotocol != CanBusProtocolEmulation::CANBUS_DISABLED) + if (mysettings.protocol != ProtocolEmulation::EMULATION_DISABLED) { if (!rules.IsChargeAllowed(&mysettings)) { @@ -1550,6 +1554,7 @@ void configureSNTP(long gmtOffset_sec, int daylightOffset_sec, const char *serve static void stopMDNS() { + ESP_LOGI(TAG, "stop mdns"); mdns_free(); } @@ -1572,26 +1577,32 @@ static void startMDNS() void ShutdownAllNetworkServices() { + ESP_LOGI(TAG, "ShutdownAllNetworkServices"); // Shut down all TCP/IP reliant services if (server_running) { - stop_webserver(_myserver); + if (_myserver != nullptr) + { + stop_webserver(_myserver); + _myserver = nullptr; + } + server_running = false; - _myserver = nullptr; } + stopMqtt(); stopMDNS(); } /// @brief Count of events of RSSI low -uint16_t wifi_count_rssi_low=0; -uint16_t wifi_count_sta_start=0; +uint16_t wifi_count_rssi_low = 0; +uint16_t wifi_count_sta_start = 0; /// @brief Count of events for WIFI connect -uint16_t wifi_count_sta_connected=0; +uint16_t wifi_count_sta_connected = 0; /// @brief Count of events for WIFI disconnect -uint16_t wifi_count_sta_disconnected=0; -uint16_t wifi_count_sta_lost_ip=0; -uint16_t wifi_count_sta_got_ip=0; +uint16_t wifi_count_sta_disconnected = 0; +uint16_t wifi_count_sta_lost_ip = 0; +uint16_t wifi_count_sta_got_ip = 0; /// @brief WIFI Event Handler /// @param @@ -1719,9 +1730,9 @@ bool SaveWIFIJson(const wifi_eeprom_settings *setting) return false; } - StaticJsonDocument<512> doc; + JsonDocument doc; - JsonObject wifi = doc.createNestedObject("wifi"); + JsonObject wifi = doc["wifi"].to(); wifi["ssid"] = setting->wifi_ssid; wifi["password"] = setting->wifi_passphrase; @@ -2728,7 +2739,7 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c // Delay 1 second vTaskDelay(pdMS_TO_TICKS(1000)); - if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_PYLONTECH) + if (mysettings.protocol == ProtocolEmulation::CANBUS_PYLONTECH) { // Pylon Tech Battery Emulation // https://github.com/PaulSturbo/DIY-BMS-CAN/blob/main/SEPLOS%20BMS%20CAN%20Protocoll%20V1.0.pdf @@ -2767,11 +2778,11 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c // Delay a little whilst sending packets to give ESP32 some breathing room and not flood the CANBUS // vTaskDelay(pdMS_TO_TICKS(100)); } - else if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_PYLONFORCEH2 ) + else if (mysettings.protocol == ProtocolEmulation::CANBUS_PYLONFORCEH2) { pylonforce_handle_tx(); } - else if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_VICTRON) + else if (mysettings.protocol == ProtocolEmulation::CANBUS_VICTRON) { // minimum CAN-IDs required for the core functionality are 0x351, 0x355, 0x356 and 0x35A. @@ -2809,7 +2820,7 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c { for (;;) { - while (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + while (mysettings.protocol == ProtocolEmulation::EMULATION_DISABLED || mysettings.protocol == ProtocolEmulation::RS485_PYLONTECH) { // Canbus is disbled, sleep until this changes.... vTaskDelay(pdMS_TO_TICKS(2000)); @@ -2821,12 +2832,12 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c if (res == ESP_OK) { canbus_messages_received++; - ESP_LOGD(TAG, "CANBUS received message ID: %0x, DLC: %d, flags: %0x", - message.identifier, message.data_length_code, message.flags); - if (!(message.flags & TWAI_MSG_FLAG_RTR)) // we do not answer to Remote-Transmission-Requests + // ESP_LOGD(TAG, "CANBUS received message ID: %0x, DLC: %d, flags: %0x",message.identifier, message.data_length_code, message.flags); + + if (!(message.flags & TWAI_MSG_FLAG_RTR)) // we do not answer to Remote-Transmission-Requests { -// ESP_LOG_BUFFER_HEXDUMP(TAG, message.data, message.data_length_code, ESP_LOG_DEBUG); - if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_PYLONFORCEH2 ) + // ESP_LOG_BUFFER_HEXDUMP(TAG, message.data, message.data_length_code, ESP_LOG_DEBUG); + if (mysettings.protocol == ProtocolEmulation::CANBUS_PYLONFORCEH2) { pylonforce_handle_rx(&message); } @@ -2920,147 +2931,156 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c { for (;;) { - // Wait until this task is triggered (sending queue task triggers it) - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - - // Delay 50ms for the data to arrive - vTaskDelay(pdMS_TO_TICKS(50)); - - uint16_t len = 0; - - if (hal.GetRS485Mutex()) + // If Pylon Tech RS485 protocol emulation is enabled, MODBUS current shunt can't be used + if (mysettings.protocol == ProtocolEmulation::RS485_PYLONTECH) { - // Wait 200ms before timeout - len = (uint16_t)uart_read_bytes(rs485_uart_num, frame, sizeof(frame), pdMS_TO_TICKS(200)); - hal.ReleaseRS485Mutex(); + // For this protocol emulation, RS485 on diyBMS acts as a slave, waiting for queries from the inverter + pylon_rs485.handle_rx(); } - - // Min packet length of 5 bytes - if (len > 5) + else { - uint8_t id = frame[0]; + // Original RS485 RX handler, RS485 port on diyBMS acts as a master + // Wait until this task is triggered (sending queue task triggers it, or save of savechargeconfig form) + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - auto crc = (uint16_t)((frame[len - 2] << 8) | frame[len - 1]); // combine the crc Low & High bytes + // Delay 50ms for the data to arrive + vTaskDelay(pdMS_TO_TICKS(50)); - auto temp = calculateCRC(frame, (uint8_t)(len - 2)); - // Swap bytes to match MODBUS ordering - auto calculatedCRC = (uint16_t)(temp << 8) | (uint16_t)(temp >> 8); + uint16_t len = 0; - // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + if (hal.GetRS485Mutex()) + { + // Wait 200ms before timeout + len = (uint16_t)uart_read_bytes(rs485_uart_num, frame, sizeof(frame), pdMS_TO_TICKS(200)); + hal.ReleaseRS485Mutex(); + } - if (calculatedCRC == crc) + // Min packet length of 5 bytes + if (len > 5) { - // if the calculated crc matches the recieved crc continue to process data... - uint8_t RS485Error = frame[1] & B10000000; - if (RS485Error == 0) - { - uint8_t cmd = frame[1] & B01111111; - uint8_t length = frame[2]; + uint8_t id = frame[0]; + + auto crc = (uint16_t)((frame[len - 2] << 8) | frame[len - 1]); // combine the crc Low & High bytes - ESP_LOGD(TAG, "Recv %i bytes, id=%u, cmd=%u", len, id, cmd); - // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + auto temp = calculateCRC(frame, (uint8_t)(len - 2)); + // Swap bytes to match MODBUS ordering + auto calculatedCRC = (uint16_t)(temp << 8) | (uint16_t)(temp >> 8); - if (mysettings.currentMonitoringDevice == CurrentMonitorDevice::PZEM_017) + // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + + if (calculatedCRC == crc) + { + // if the calculated crc matches the recieved crc continue to process data... + uint8_t RS485Error = frame[1] & B10000000; + if (RS485Error == 0) { - if (cmd == 6 && id == 248) - { - ESP_LOGI(TAG, "Reply to broadcast/change address"); - } - if (cmd == 6 && id == mysettings.currentMonitoringModBusAddress) - { - ESP_LOGI(TAG, "Reply to set param"); - } - else if (cmd == 3 && id == mysettings.currentMonitoringModBusAddress) - { - // 75mV shunt (hard coded for PZEM) - currentMonitor.modbus.shuntmillivolt = 75; + uint8_t cmd = frame[1] & B01111111; + uint8_t length = frame[2]; - // Shunt type 0x0000 - 0x0003 (100A/50A/200A/300A) - switch (((uint32_t)frame[9] << 8 | (uint32_t)frame[10])) + ESP_LOGD(TAG, "Recv %i bytes, id=%u, cmd=%u", len, id, cmd); + // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + + if (mysettings.currentMonitoringDevice == CurrentMonitorDevice::PZEM_017) + { + if (cmd == 6 && id == 248) { - case 0: - currentMonitor.modbus.shuntmaxcurrent = 100; - break; - case 1: - currentMonitor.modbus.shuntmaxcurrent = 50; - break; - case 2: - currentMonitor.modbus.shuntmaxcurrent = 200; - break; - case 3: - currentMonitor.modbus.shuntmaxcurrent = 300; - break; - default: - currentMonitor.modbus.shuntmaxcurrent = 0; + ESP_LOGI(TAG, "Reply to broadcast/change address"); + } + if (cmd == 6 && id == mysettings.currentMonitoringModBusAddress) + { + ESP_LOGI(TAG, "Reply to set param"); + } + else if (cmd == 3 && id == mysettings.currentMonitoringModBusAddress) + { + // 75mV shunt (hard coded for PZEM) + currentMonitor.modbus.shuntmillivolt = 75; + + // Shunt type 0x0000 - 0x0003 (100A/50A/200A/300A) + switch (((uint32_t)frame[9] << 8 | (uint32_t)frame[10])) + { + case 0: + currentMonitor.modbus.shuntmaxcurrent = 100; + break; + case 1: + currentMonitor.modbus.shuntmaxcurrent = 50; + break; + case 2: + currentMonitor.modbus.shuntmaxcurrent = 200; + break; + case 3: + currentMonitor.modbus.shuntmaxcurrent = 300; + break; + default: + currentMonitor.modbus.shuntmaxcurrent = 0; + } + } + else if (cmd == 4 && id == mysettings.currentMonitoringModBusAddress && len == 21) + { + // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + + // memset(¤tMonitor.modbus, 0, sizeof(currentmonitor_raw_modbus)); + currentMonitor.validReadings = true; + currentMonitor.timestamp = esp_timer_get_time(); + // voltage in 0.01V + currentMonitor.modbus.voltage = (float)((uint32_t)frame[3] << 8 | (uint32_t)frame[4]) / (float)100.0; + // current in 0.01A + currentMonitor.modbus.current = (float)((uint32_t)frame[5] << 8 | (uint32_t)frame[6]) / (float)100.0; + // power in 0.1W + currentMonitor.modbus.power = ((uint32_t)frame[7] << 8 | (uint32_t)frame[8] | (uint32_t)frame[9] << 24 | (uint32_t)frame[10] << 16) / 10.0F; + } + else + { + // Dump out unhandled reply + ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); } } - else if (cmd == 4 && id == mysettings.currentMonitoringModBusAddress && len == 21) - { - // ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); - - // memset(¤tMonitor.modbus, 0, sizeof(currentmonitor_raw_modbus)); - currentMonitor.validReadings = true; - currentMonitor.timestamp = esp_timer_get_time(); - // voltage in 0.01V - currentMonitor.modbus.voltage = (float)((uint32_t)frame[3] << 8 | (uint32_t)frame[4]) / (float)100.0; - // current in 0.01A - currentMonitor.modbus.current = (float)((uint32_t)frame[5] << 8 | (uint32_t)frame[6]) / (float)100.0; - // power in 0.1W - currentMonitor.modbus.power = ((uint32_t)frame[7] << 8 | (uint32_t)frame[8] | (uint32_t)frame[9] << 24 | (uint32_t)frame[10] << 16) / 10.0F; - } - else - { - // Dump out unhandled reply - ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); - } - } - // ESP_LOGD(TAG, "CRC pass Id=%u F=%u L=%u", id, cmd, length); - if (mysettings.currentMonitoringDevice == CurrentMonitorDevice::DIYBMS_CURRENT_MON_MODBUS) - { - if (id == mysettings.currentMonitoringModBusAddress && cmd == 3) + // ESP_LOGD(TAG, "CRC pass Id=%u F=%u L=%u", id, cmd, length); + if (mysettings.currentMonitoringDevice == CurrentMonitorDevice::DIYBMS_CURRENT_MON_MODBUS) { - ProcessDIYBMSCurrentMonitorRegisterReply(length); + if (id == mysettings.currentMonitoringModBusAddress && cmd == 3) + { + ProcessDIYBMSCurrentMonitorRegisterReply(length); - if (_tft_screen_available) + if (_tft_screen_available) + { + // Refresh the TFT display + xTaskNotify(updatetftdisplay_task_handle, 0x00, eNotifyAction::eNoAction); + } + } + else if (id == mysettings.currentMonitoringModBusAddress && cmd == 16) { - // Refresh the TFT display - xTaskNotify(updatetftdisplay_task_handle, 0x00, eNotifyAction::eNoAction); + ESP_LOGI(TAG, "Write multiple regs, success"); + } + else + { + // Dump out unhandled reply + ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); } - } - else if (id == mysettings.currentMonitoringModBusAddress && cmd == 16) - { - ESP_LOGI(TAG, "Write multiple regs, success"); - } - else - { - // Dump out unhandled reply - ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); } } + else + { + ESP_LOGE(TAG, "RS485 error"); + ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + } } else { - ESP_LOGE(TAG, "RS485 error"); - ESP_LOG_BUFFER_HEXDUMP(TAG, frame, len, esp_log_level_t::ESP_LOG_DEBUG); + ESP_LOGE(TAG, "CRC error"); } } else { - ESP_LOGE(TAG, "CRC error"); + // We didn't receive anything on RS485, record error and mark current monitor as invalid + ESP_LOGE(TAG, "Short packet %i bytes", len); + + // Indicate that the current monitor values are now invalid/unknown + currentMonitor.validReadings = false; } - } - else - { - // We didn't receive anything on RS485, record error and mark current monitor as invalid - ESP_LOGE(TAG, "Short packet %i bytes", len); - // Indicate that the current monitor values are now invalid/unknown - currentMonitor.validReadings = false; + // Notify sending queue, to continue + xTaskNotify(service_rs485_transmit_q_task_handle, 0x00, eNotifyAction::eNoAction); } - - // Notify sending queue, to continue - xTaskNotify(service_rs485_transmit_q_task_handle, 0x00, eNotifyAction::eNoAction); - } // infinite loop } @@ -3198,6 +3218,21 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c } } +void CalculateStateOfHealth(diybms_eeprom_settings *settings) +{ + float batcap_mah = 1000.0F * settings->nominalbatcap; + float in = (float)settings->soh_total_milliamphour_in / batcap_mah; + float out = (float)settings->soh_total_milliamphour_out / batcap_mah; + // Take worst case number of cycles + float cycles = max(in, out); + + settings->soh_estimated_battery_cycles = (uint16_t)round(cycles); + + settings->soh_percent = 100.0F - ((100.0F - settings->soh_eol_capacity) * (cycles / (float)settings->soh_lifetime_battery_cycles)); + + ESP_LOGI(TAG, "State of health calc %f %, estimated cycles=%f", settings->soh_percent, cycles); +} + // Do activities which are not critical to the system like background loading of config, or updating timing results etc. [[noreturn]] void lazy_tasks(void *) { @@ -3233,6 +3268,14 @@ void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, c // Has day rolled over? if (year_day != timeinfo.tm_yday) { + + mysettings.soh_total_milliamphour_out += currentMonitor.modbus.daily_milliamphour_out; + mysettings.soh_total_milliamphour_in += currentMonitor.modbus.daily_milliamphour_in; + + SaveConfiguration(&mysettings); + + CalculateStateOfHealth(&mysettings); + // Reset the current monitor at midnight (ish) CurrentMonitorResetDailyAmpHourCounters(); @@ -3553,7 +3596,7 @@ bool AreWifiConfigurationsTheSame(const wifi_eeprom_settings *a, const wifi_eepr /// @return TRUE if the WIFI config on the card is different, false if identical bool LoadWiFiConfigFromSDCard(const bool existingConfigValid) { - StaticJsonDocument<2048> json; + JsonDocument json; DeserializationError error; if (!_sd_card_installed) @@ -3660,7 +3703,7 @@ struct log_level_t }; // Default log levels to use for various components. -const std::array log_levels = +const std::array log_levels = { log_level_t{.tag = "*", .level = ESP_LOG_DEBUG}, {.tag = "wifi", .level = ESP_LOG_WARN}, @@ -3673,7 +3716,7 @@ const std::array log_levels = {.tag = "diybms-tx", .level = ESP_LOG_INFO}, {.tag = "diybms-rules", .level = ESP_LOG_INFO}, {.tag = "diybms-softap", .level = ESP_LOG_INFO}, - {.tag = "diybms-tft", .level = ESP_LOG_INFO}, + {.tag = "diybms-tft", .level = ESP_LOG_WARN}, {.tag = "diybms-victron", .level = ESP_LOG_INFO}, {.tag = "diybms-webfuncs", .level = ESP_LOG_INFO}, {.tag = "diybms-webpost", .level = ESP_LOG_INFO}, @@ -3681,6 +3724,7 @@ const std::array log_levels = {.tag = "diybms-web", .level = ESP_LOG_INFO}, {.tag = "diybms-set", .level = ESP_LOG_INFO}, {.tag = "diybms-mqtt", .level = ESP_LOG_INFO}, + {.tag = "diybms-ctrl", .level = ESP_LOG_INFO}, {.tag = "diybms-pylon", .level = ESP_LOG_INFO}, {.tag = "diybms-pyforce", .level = ESP_LOG_INFO}, {.tag = "curmon", .level = ESP_LOG_INFO}}; @@ -3764,8 +3808,8 @@ void setup() ESP_LOGI(TAG, R"( - _ __ - _| o |_) |\/| (_ + _ __ + _| o |_) |\/| (_ (_| | \/ |_) | | __) / @@ -3837,7 +3881,7 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", { // Generate new key memset(&mysettings.homeassist_apikey, 0, sizeof(mysettings.homeassist_apikey)); - randomCharacters(mysettings.homeassist_apikey, sizeof(mysettings.homeassist_apikey) - 1); + randomCharacters(mysettings.homeassist_apikey, sizeof(mysettings.homeassist_apikey) - 1); saveConfiguration(); } ESP_LOGI(TAG, "homeassist_apikey=%s", mysettings.homeassist_apikey); @@ -3872,9 +3916,18 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", mysettings.currentMonitoring_shunttempcoefficient, mysettings.currentMonitoring_tempcompenabled); - currentmon_internal.GuessSOC(); + currentmon_internal.DefaultSOC(); + + uint32_t in; + uint32_t out; + if (GetStateOfCharge(&in, &out)) + { + currentmon_internal.SetSOCByMilliAmpCounter(in, out); + } currentmon_internal.TakeReadings(); + + CalculateStateOfHealth(&mysettings); } else { @@ -3979,7 +4032,7 @@ void ESPCoreDumpToJSON(JsonObject &doc) { if (esp_core_dump_image_check() == ESP_OK) { - JsonObject core = doc.createNestedObject("coredump"); + JsonObject core = doc["coredump"].to(); // A valid core dump is in FLASH storage esp_core_dump_summary_t *summary = (esp_core_dump_summary_t *)malloc(sizeof(esp_core_dump_summary_t)); @@ -4001,7 +4054,7 @@ void ESPCoreDumpToJSON(JsonObject &doc) core["bt_corrupted"] = summary->exc_bt_info.corrupted; core["bt_depth"] = summary->exc_bt_info.depth; - auto backtrace = core.createNestedArray("backtrace"); + auto backtrace = core["backtrace"].to(); for (auto value : summary->exc_bt_info.bt) { ultoa(value, outputString, 16); @@ -4015,13 +4068,15 @@ void ESPCoreDumpToJSON(JsonObject &doc) ultoa(summary->ex_info.exc_vaddr, outputString, 16); core["exc_vaddr"] = outputString; - auto exc_a = core.createNestedArray("exc_a"); + auto exc_a = core["exc_a"].to(); + ; for (auto value : summary->ex_info.exc_a) { ultoa(value, outputString, 16); exc_a.add(outputString); } - auto epcx = core.createNestedArray("epcx"); + auto epcx = core["epcx"].to(); + ; for (auto value : summary->ex_info.epcx) { ultoa(value, outputString, 16); @@ -4040,12 +4095,12 @@ void ESPCoreDumpToJSON(JsonObject &doc) /// @return esp_err_t diagnosticJSON(httpd_req_t *req, char buffer[], int bufferLenMax) { - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject root = doc.to(); - JsonObject diag = root.createNestedObject("diagnostic"); + JsonObject diag = root["diagnostic"].to(); diag["numtasks"] = uxTaskGetNumberOfTasks(); - auto tasks = diag.createNestedArray("tasks"); + auto tasks = diag["tasks"].to(); // Array of pointers to the task handles we are going to examine const std::array task_handle_ptrs = @@ -4061,7 +4116,7 @@ esp_err_t diagnosticJSON(httpd_req_t *req, char buffer[], int bufferLenMax) { if (*h != nullptr) { - JsonObject nested = tasks.createNestedObject(); + JsonObject nested = tasks.add(); nested["name"] = pcTaskGetName(*h); nested["hwm"] = uxTaskGetStackHighWaterMark(*h); } @@ -4077,7 +4132,7 @@ esp_err_t diagnosticJSON(httpd_req_t *req, char buffer[], int bufferLenMax) { if (h != nullptr) { - JsonObject nested = tasks.createNestedObject(); + JsonObject nested = tasks.add(); nested["name"] = pcTaskGetName(h); nested["hwm"] = uxTaskGetStackHighWaterMark(h); } @@ -4097,7 +4152,7 @@ esp_err_t diagnosticJSON(httpd_req_t *req, char buffer[], int bufferLenMax) unsigned long wifitimer = 0; unsigned long heaptimer = 0; -// unsigned long taskinfotimer = 0; +uint8_t flash_write_soc_timer = 0; void logActualTime() { @@ -4111,9 +4166,10 @@ void logActualTime() void loop() { - delay(100); + //vTaskDelete(NULL); + delay(250); - unsigned long currentMillis = millis(); + auto currentMillis = millis(); if (card_action == CardAction::Mount) { @@ -4142,8 +4198,9 @@ void loop() // Attempt to connect to MQTT if enabled and not already connected connectToMqtt(); } - else + else if (wifi_ap_connect_retry_num >= 25) { + // Try to reconnect WIFI every 30 seconds, if we have exhausted the first 25 "quick" attempts ESP_LOGI(TAG, "Trying to connect WIFI"); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } @@ -4157,6 +4214,8 @@ void loop() if (currentMillis > heaptimer) { + // This gets called once per minute... + logActualTime(); /* total_free_bytes; Total free bytes in the heap. Equivalent to multi_free_heap_size(). @@ -4180,7 +4239,16 @@ void loop() heap.free_blocks, heap.total_blocks); - // Report again in 30 seconds - heaptimer = currentMillis + 30000; + // Report again in 60 seconds + heaptimer = currentMillis + 60000; + + // Once every 10 minutes, store the state of charge into flash, just in case the controller is rebooted and we can restore this value + // on power up. 10 minutes was chosen so we don't rapidly wear out the internal flash memory with writes. + flash_write_soc_timer++; + if (flash_write_soc_timer > 10 && currentmon_internal.calc_state_of_charge() > 1.0F && mysettings.currentMonitoringEnabled && mysettings.currentMonitoringDevice == CurrentMonitorDevice::DIYBMS_CURRENT_MON_INTERNAL) + { + flash_write_soc_timer = 0; + SaveStateOfCharge(currentmon_internal.raw_milliamphour_in(), currentmon_internal.raw_milliamphour_out()); + } } } diff --git a/ESPController/src/pylon_canbus.cpp b/ESPController/src/pylon_canbus.cpp index 7d8ffbfe..d9e51ca6 100644 --- a/ESPController/src/pylon_canbus.cpp +++ b/ESPController/src/pylon_canbus.cpp @@ -94,8 +94,7 @@ void pylon_message_355() data.stateofchargevalue = rules.StateOfChargeWithRulesApplied(&mysettings, currentMonitor.stateofcharge); // 2 SOH value un16 1 % - // TODO: Need to determine this based on age of battery/cycles etc. - data.stateofhealthvalue = 100; + data.stateofhealthvalue = (uint16_t)(trunc(mysettings.soh_percent)); send_canbus_message(0x355, (uint8_t *)&data, sizeof(data355)); } diff --git a/ESPController/src/pylon_rs485.cpp b/ESPController/src/pylon_rs485.cpp new file mode 100644 index 00000000..fb6fbaac --- /dev/null +++ b/ESPController/src/pylon_rs485.cpp @@ -0,0 +1,283 @@ +/*------------------------------------------------------------------------ + * + * Project: PylonTech protocol emulation library for diyBMS + * Using RS485 @ 9600 baud + * + * Author: Michal Ruzek (ruza87) + * + * ----------------------------------------------------------------------- + */ + + +#include "pylon_rs485.h" + +static constexpr const char * const TAG = "diybms-pylon485"; + +#define PYL_VERSION 0x3832 // '28' in ASCII +#define PYL_ADDR 0x3230 // '02' in ASCII +#define PYL_CID1 0x3634 // '46' in ASCII + +#define CMD_VERSION 0x4634 // '4F' in ASCII +#define CMD_ANALOG_VAL 0x3234 // '42' in ASCII +#define CMD_CHARGE_MGMT 0x3239 // '92' in ASCII + + +PylonRS485::PylonRS485(uart_port_t portNum, diybms_eeprom_settings& settings, Rules& rules, currentmonitoring_struct& currentMonitor, + ControllerState& controllerState, HAL_ESP32& hal) + : uart_num(portNum), + settings(settings), + rules(rules), + current_monitor(currentMonitor), + controller_state(controllerState), + hal(hal) { + +} + + +uint32_t PylonRS485::hex2int(char *hex, char len) { + uint32_t val = 0; + char count = 0; + while (count < len) { + // get current character then increment + uint8_t byte = *hex++; + count++; + // transform hex character to the 4bit equivalent number, using the ascii table indexes + if (byte >= '0' && byte <= '9') byte = byte - '0'; + else if (byte >= 'a' && byte <='f') byte = byte - 'a' + 10; + else if (byte >= 'A' && byte <='F') byte = byte - 'A' + 10; + // shift 4 to make space for new digit, and add the 4 bits of the new digit + val = (val << 4) | (byte & 0xF); + } + return val; +} + + +void PylonRS485::insertLength(char *buf, int payload_len) { + //do not include header into payload len + payload_len -= sizeof(THeader); + uint8_t sum = payload_len & 0x0F; //first 4 digits + sum += (payload_len >> 4) & 0x0F; //second 4 digits + sum += (payload_len >> 8) & 0x0F; //third 4 digits + sum = sum % 16; // modulo 16 + sum = ~sum; // invert bits + sum = (sum + 1) & 0x0F; // add one. + + //print into temporary buffer (trailing \0 present) + char tmp[6]; + snprintf(tmp, sizeof(tmp), "%01X%03X", sum, payload_len); + + //insert into payload on proper position + THeader* hdr_ptr = (THeader*)buf; + for (char i=0; i<4; i++) { + hdr_ptr->length[i] = tmp[i]; + } +} + + +int PylonRS485::appendChecksum(char *buf, int buf_size, int payload_len) { + uint16_t sum = 0; + //do not include SOF into chksum (start at 1) + for (int i=1; i sizeof(hdr)) { + // at least size of header, read it out + rx_read = uart_read_bytes(uart_num, (uint8_t*)&hdr, sizeof(hdr), pdMS_TO_TICKS(500)); + if (rx_read != sizeof(hdr)) { + //wrong size, bail out + uart_flush_input(uart_num); + hal.ReleaseRS485Mutex(); + return; + } + + // parse header, check values + if (hdr.soh != '~' || hdr.cid1 != PYL_CID1) { + // invalid message format, drop + uart_flush_input(uart_num); + hal.ReleaseRS485Mutex(); + return; + } + + // read the rest of the message (optional INFO + 4B of ascii chksum + terminator) + uint32_t body_len = (hex2int(hdr.length, 4)) & 0x0FFF; // mask to remove lchksum + body_len += 5; //add length of message checksum (4 chars in ASCII) and terminator '\r' + if (body_len > sizeof(tmp_buf) || body_len + rx_read > rx_avail) { + // requested more data than available, fail + uart_flush_input(uart_num); + hal.ReleaseRS485Mutex(); + return; + } + if (uart_read_bytes(uart_num, tmp_buf, body_len, pdMS_TO_TICKS(500)) != body_len) { + //wrong size, bail out + uart_flush_input(uart_num); + hal.ReleaseRS485Mutex(); + return; + } + + // TODO: check payload checksum in tmp_buf?? + + // skip if not for us + if (hdr.addr != PYL_ADDR) { + hal.ReleaseRS485Mutex(); + return; + } + + // header seems good, parse command + switch (hdr.cid2) { + case CMD_VERSION: + //do not check version for this command + response_len = snprintf(tmp_buf, sizeof(tmp_buf),"~" // SOH + "28" // VER + "02" // ADDR + "4600" // CID1 + RET of zero + "0000" // Zero LENGTH + ); + break; + + case CMD_ANALOG_VAL: + //skip if version doesn't match + if (hdr.ver != PYL_VERSION) break; + + // If current shunt is installed, use the voltage from that as it should be more accurate + if (settings.currentMonitoringEnabled && current_monitor.validReadings) { + pack_voltage = current_monitor.modbus.voltage * 1000.0; + } + else { + // Use highest bank voltage calculated by controller and modules + pack_voltage = rules.highestBankVoltage; + } + + response_len = snprintf(tmp_buf, sizeof(tmp_buf),"~" // SOH + "28" // VER + "02" // ADDR + "4600" // CID1 + RET of zero + "XXXX" // Placeholder for length (computed later) + "11" // Info flags + "02" // Command == ADDR + "10" // Cell count = 16 (10h) + "0D200D200D200D200D200D200D200D200D200D200D200D200D200D200D200D20" // 16x cell voltage 3360mv as hexa ascii (taken from doc example) + "010BC30BC30BC30BCD0BCD" // 5x temperature sensor with value in kelvins (taken from doc example) + "0000" // current = 0 (resolution = tenths of mA, realValue = thisValue * 100) + "%04X" // pack voltage (mV) + "FFFF02" // remain capacity, user def = 02 + "FFFF" // total capacity + "0002" // num of cycles + , pack_voltage); + //add length + insertLength(tmp_buf, response_len); + break; + + case CMD_CHARGE_MGMT: + //skip if version doesn't match + if (hdr.ver != PYL_VERSION) break; + + // Defaults (do nothing) + charge_current_limit = 1; + discharge_current_limit = 1; + charge_voltage = rules.DynamicChargeVoltage() * 100; //convert to mV scale + discharge_voltage = settings.dischargevolt * 100; //convert to mV scale + if (rules.IsChargeAllowed(&settings)) { + if (rules.numberOfBalancingModules > 0 && settings.stopchargebalance == true) { + // Balancing is active, so stop charging + charge_current_limit = 1; + } + else { + // Default - normal behaviour + charge_current_limit = rules.DynamicChargeCurrent(); + } + } + if (rules.IsDischargeAllowed(&settings)) { + discharge_current_limit = settings.dischargecurrent; + } + + // My inverter doesn't read "Alarm message 0x44h" -> use status field of this message to stop chrg/dischrg + // when alarm occurs. + stop_charging = !rules.IsChargeAllowed(&settings); + stop_discharging = !rules.IsDischargeAllowed(&settings); + + // The IsChargeAllowed / IsDischargeAllowed methods already cover: + // - Internal errors, Emergency stop + // - Low & High module temperature alarms (set in Rules, stops both chrg & dischrg) + // - Bank overvoltage & undervoltage alarm (set in Rules, stops both chrg & dischrg) + // - External temperature alarm (set on Charging page, stops both chrg & dischrg) + // - Low bank voltage (set on Charging page, stops discharging) + // - High bank voltage (set on Charging page, stops charging) + // - Cell undervoltage (set on Charging page, stops discharging) + // - Cell overvoltage (set on Charging page, stops charging) + + // Battery high voltage alarm from Current monitor -> stop charging + stop_charging |= rules.ruleOutcome(Rule::CurrentMonitorOverVoltage); + + // Battery low voltage alarm from Current monitor -> stop discharging + stop_discharging |= rules.ruleOutcome(Rule::CurrentMonitorUnderVoltage); + + // Place bolean flags into byte status field + flags = 0; + if (!stop_charging) { + flags |= 0x80; + } + if (!stop_discharging) { + flags |= 0x40; + } + + response_len = snprintf(tmp_buf, sizeof(tmp_buf),"~" // SOH + "28" // VER + "02" // ADDR + "4600" // CID1 + RET of zero + "XXXX" // Placeholder for length (computed later) + "02" // Command == ADDR + "%04X" // Charge limit + "%04X" // Discharge limit + "%04X" // Charge current + "%04X" // Discharge current + "%02X" // Status flags + , charge_voltage, discharge_voltage, charge_current_limit, discharge_current_limit, flags); + //add length + insertLength(tmp_buf, response_len); + break; + + default: + //ignore unsupported commands + ESP_LOGD(TAG, "Unsupported command 0x%04X", hdr.cid2); + break; + } + + if (response_len > 0) { + //append checksum and trailer to the response (leading SOH is skipped during computation) + response_len = appendChecksum(tmp_buf, sizeof(tmp_buf), response_len); + + //send through UART + uart_write_bytes(uart_num, tmp_buf, response_len); + } + + hal.ReleaseRS485Mutex(); + } + else { + // Got nothing or buffer too small, suspend the task + hal.ReleaseRS485Mutex(); + vTaskDelay(pdMS_TO_TICKS(200)); + } + + } +} + diff --git a/ESPController/src/pylonforce_canbus.cpp b/ESPController/src/pylonforce_canbus.cpp index 105ba001..b17e20a7 100644 --- a/ESPController/src/pylonforce_canbus.cpp +++ b/ESPController/src/pylonforce_canbus.cpp @@ -132,17 +132,18 @@ void pylonforce_message_4210() data.temperature = 121+1000; // 12.1 °C } - // TODO: Need to determine this based on age of battery/cycles etc. - data.stateofhealthvalue = 100; // Only send CANBUS message if we have a current monitor enabled & valid if (mysettings.currentMonitoringEnabled && currentMonitor.validReadings && (mysettings.currentMonitoringDevice == CurrentMonitorDevice::DIYBMS_CURRENT_MON_MODBUS || mysettings.currentMonitoringDevice == CurrentMonitorDevice::DIYBMS_CURRENT_MON_INTERNAL)) { - data.stateofchargevalue = rules.StateOfChargeWithRulesApplied(&mysettings, currentMonitor.stateofcharge); + data.stateofchargevalue = (uint8_t)rules.StateOfChargeWithRulesApplied(&mysettings, currentMonitor.stateofcharge); + // Based on age of battery/cycles + data.stateofhealthvalue = (uint8_t)(trunc(mysettings.soh_percent)); } else { data.stateofchargevalue = 50; + data.stateofhealthvalue = 100; } send_ext_canbus_message(0x4210+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4210)); diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 993a08c4..73434d5e 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -40,7 +40,7 @@ static const char influxdb_databasebucket_JSONKEY[] = "bucket"; static const char influxdb_orgid_JSONKEY[] = "org"; static const char influxdb_serverurl_JSONKEY[] = "url"; static const char influxdb_loggingFreqSeconds_JSONKEY[] = "logfreq"; -static const char canbusprotocol_JSONKEY[] = "canbusprotocol"; +static const char protocol_JSONKEY[] = "protocol"; static const char canbusinverter_JSONKEY[] = "canbusinverter"; static const char canbusbaud_JSONKEY[] = "canbusbaud"; static const char canbus_equipment_addr_JSONKEY[] = "canbusequip"; @@ -91,6 +91,15 @@ static const char floatvoltagetimer_JSONKEY[] = "floatvoltagetimer"; static const char stateofchargeresumevalue_JSONKEY[] = "stateofchargeresumevalue"; static const char homeassist_apikey_JSONKEY[] = "homeassistapikey"; +static const char soh_total_milliamphour_out_JSONKEY[] = "soh_mah_out"; +static const char soh_total_milliamphour_in_JSONKEY[] = "soh_mah_in"; + +static const char soh_lifetime_battery_cycles_JSONKEY[] = "soh_batcycle"; + +static const char soh_eol_capacity_JSONKEY[] = "soh_eol_capacity"; + + + /* NVS KEYS THESE STRINGS ARE USED TO HOLD THE PARAMETER IN NVS FLASH, MAXIMUM LENGTH OF 16 CHARACTERS */ @@ -119,10 +128,10 @@ static const char rs485baudrate_NVSKEY[] = "485baudrate"; static const char rs485databits_NVSKEY[] = "485databits"; static const char rs485parity_NVSKEY[] = "485parity"; static const char rs485stopbits_NVSKEY[] = "485stopbits"; -static const char canbusprotocol_NVSKEY[] = "canbusprotocol"; +static const char protocol_NVSKEY[] = "protocol"; static const char canbusinverter_NVSKEY[] = "canbusinverter"; static const char canbusbaud_NVSKEY[] = "canbusbaud"; -static const char canbus_equipment_addr_NVSKEY[]="canbusequip"; +static const char canbus_equipment_addr_NVSKEY[] = "canbusequip"; static const char nominalbatcap_NVSKEY[] = "nominalbatcap"; static const char chargevolt_NVSKEY[] = "cha_volt"; static const char chargecurrent_NVSKEY[] = "cha_current"; @@ -183,6 +192,14 @@ static const char floatvoltagetimer_NVSKEY[] = "floatVtimer"; static const char stateofchargeresumevalue_NVSKEY[] = "socresume"; static const char homeassist_apikey_NVSKEY[] = "haapikey"; +static const char soh_total_milliamphour_out_NVSKEY[] = "soh_mah_out"; +static const char soh_total_milliamphour_in_NVSKEY[] = "soh_mah_in"; +static const char soh_lifetime_battery_cycles_NVSKEY[] = "soh_batcycle"; +static const char soh_eol_capacity_NVSKEY[] = "soh_eol_cap"; + +static const char soc_milliamphour_out_NVSKEY[] = "soc_mah_out"; +static const char soc_milliamphour_in_NVSKEY[] = "soc_mah_in"; + #define MACRO_NVSWRITE(VARNAME) writeSetting(nvs_handle, VARNAME##_NVSKEY, settings->VARNAME); #define MACRO_NVSWRITE_UINT8(VARNAME) writeSetting(nvs_handle, VARNAME##_NVSKEY, (uint8_t)settings->VARNAME); #define MACRO_NVSWRITESTRING(VARNAME) writeSetting(nvs_handle, VARNAME##_NVSKEY, &settings->VARNAME[0]); @@ -201,7 +218,6 @@ bool ValidateGetSetting(esp_err_t err, const char *key) case ESP_OK: ESP_LOGD(TAG, "Read key (%s)", key); return true; - break; case ESP_ERR_NVS_NOT_FOUND: ESP_LOGW(TAG, "Key not initialized (%s)", key); break; @@ -310,6 +326,11 @@ void writeSetting(nvs_handle_t handle, const char *key, int16_t value) ESP_LOGD(TAG, "Writing (%s)=%i", key, value); ESP_ERROR_CHECK(nvs_set_i16(handle, key, value)); } +void writeSetting(nvs_handle_t handle, const char *key, uint32_t value) +{ + ESP_LOGD(TAG, "Writing (%s)=%u", key, value); + ESP_ERROR_CHECK(nvs_set_u32(handle, key, value)); +} void writeSetting(nvs_handle_t handle, const char *key, int32_t value) { ESP_LOGD(TAG, "Writing (%s)=%i", key, value); @@ -331,7 +352,65 @@ void writeSettingBlob(nvs_handle_t handle, const char *key, const void *value, s ESP_ERROR_CHECK(nvs_set_blob(handle, key, value, length)); } -void SaveConfiguration(diybms_eeprom_settings *settings) +/// @brief Reads state of charge (milliamp hour counts) from flash +/// @param in pointer to milliamp in count +/// @param out pointer to milliamp out count +/// @return true if values are valid +bool GetStateOfCharge(uint32_t *in, uint32_t *out) +{ + const char *partname = "diybms-ctrl"; + ESP_LOGI(TAG, "Read state of charge from flash"); + + nvs_handle_t nvs_handle; + auto err = nvs_open(partname, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Error (%s) opening NVS handle", esp_err_to_name(err)); + } + else + { + // Open + auto ret1 = getSetting(nvs_handle, soc_milliamphour_in_NVSKEY, in); + auto ret2 = getSetting(nvs_handle, soc_milliamphour_out_NVSKEY, out); + + nvs_close(nvs_handle); + + if (ret1 && ret2) + { + return true; + } + + ESP_LOGI(TAG, "SoC value doesn't exist in flash"); + } + return false; +} + +/// @brief Stores the milliamp current values to allow restore of state of charge on power up +/// @param in milliamp hours in +/// @param out milliamp hours out +void SaveStateOfCharge(uint32_t in, uint32_t out) +{ + const char *partname = "diybms-ctrl"; + ESP_LOGI(TAG, "Write SoC to flash in=%u out=%u", in, out); + + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(partname, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Error %s opening NVS handle!", esp_err_to_name(err)); + } + else + { + // Save settings + writeSetting(nvs_handle, soc_milliamphour_in_NVSKEY, in); + writeSetting(nvs_handle, soc_milliamphour_out_NVSKEY, out); + + ESP_ERROR_CHECK(nvs_commit(nvs_handle)); + nvs_close(nvs_handle); + } +} + +void SaveConfiguration(const diybms_eeprom_settings *settings) { const char *partname = "diybms-ctrl"; ESP_LOGI(TAG, "Write config"); @@ -350,11 +429,11 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE(baudRate) MACRO_NVSWRITE(interpacketgap) - MACRO_NVSWRITEBLOB(rulevalue); - MACRO_NVSWRITEBLOB(rulehysteresis); - MACRO_NVSWRITEBLOB(rulerelaystate); - MACRO_NVSWRITEBLOB(rulerelaydefault); - MACRO_NVSWRITEBLOB(relaytype); + MACRO_NVSWRITEBLOB(rulevalue) + MACRO_NVSWRITEBLOB(rulehysteresis) + MACRO_NVSWRITEBLOB(rulerelaystate) + MACRO_NVSWRITEBLOB(rulerelaydefault) + MACRO_NVSWRITEBLOB(relaytype) MACRO_NVSWRITE(graph_voltagehigh) MACRO_NVSWRITE(graph_voltagelow) @@ -368,33 +447,35 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE(currentMonitoringEnabled) MACRO_NVSWRITE(currentMonitoringModBusAddress) - MACRO_NVSWRITE_UINT8(currentMonitoringDevice); + MACRO_NVSWRITE_UINT8(currentMonitoringDevice) MACRO_NVSWRITE(rs485baudrate) - MACRO_NVSWRITE_UINT8(rs485databits); - MACRO_NVSWRITE_UINT8(rs485parity); - MACRO_NVSWRITE_UINT8(rs485stopbits); - MACRO_NVSWRITE_UINT8(canbusprotocol); - MACRO_NVSWRITE_UINT8(canbusinverter); - MACRO_NVSWRITE(canbusbaud); - MACRO_NVSWRITE_UINT8(canbus_equipment_addr); - - MACRO_NVSWRITE(currentMonitoring_shuntmv); - MACRO_NVSWRITE(currentMonitoring_shuntmaxcur); - MACRO_NVSWRITE(currentMonitoring_batterycapacity); - MACRO_NVSWRITE(currentMonitoring_fullchargevolt); - MACRO_NVSWRITE(currentMonitoring_tailcurrent); - MACRO_NVSWRITE(currentMonitoring_chargeefficiency); - MACRO_NVSWRITE(currentMonitoring_shuntcal); - MACRO_NVSWRITE(currentMonitoring_temperaturelimit); - MACRO_NVSWRITE(currentMonitoring_overvoltagelimit); - MACRO_NVSWRITE(currentMonitoring_undervoltagelimit); - MACRO_NVSWRITE(currentMonitoring_overcurrentlimit); - MACRO_NVSWRITE(currentMonitoring_undercurrentlimit); - MACRO_NVSWRITE(currentMonitoring_overpowerlimit); - MACRO_NVSWRITE(currentMonitoring_shunttempcoefficient); - MACRO_NVSWRITE(currentMonitoring_tempcompenabled); - - MACRO_NVSWRITE(nominalbatcap); + + MACRO_NVSWRITE_UINT8(rs485databits) + MACRO_NVSWRITE_UINT8(rs485parity) + MACRO_NVSWRITE_UINT8(rs485stopbits) + MACRO_NVSWRITE_UINT8(protocol) + MACRO_NVSWRITE_UINT8(canbusinverter) + MACRO_NVSWRITE(canbusbaud) + MACRO_NVSWRITE_UINT8(canbus_equipment_addr) + + MACRO_NVSWRITE(currentMonitoring_shuntmv) + MACRO_NVSWRITE(currentMonitoring_shuntmaxcur) + MACRO_NVSWRITE(currentMonitoring_batterycapacity) + MACRO_NVSWRITE(currentMonitoring_fullchargevolt) + MACRO_NVSWRITE(currentMonitoring_tailcurrent) + MACRO_NVSWRITE(currentMonitoring_chargeefficiency) + MACRO_NVSWRITE(currentMonitoring_shuntcal) + MACRO_NVSWRITE(currentMonitoring_temperaturelimit) + MACRO_NVSWRITE(currentMonitoring_overvoltagelimit) + MACRO_NVSWRITE(currentMonitoring_undervoltagelimit) + MACRO_NVSWRITE(currentMonitoring_overcurrentlimit) + MACRO_NVSWRITE(currentMonitoring_undercurrentlimit) + MACRO_NVSWRITE(currentMonitoring_overpowerlimit) + MACRO_NVSWRITE(currentMonitoring_shunttempcoefficient) + MACRO_NVSWRITE(currentMonitoring_tempcompenabled) + + MACRO_NVSWRITE(nominalbatcap) + MACRO_NVSWRITE(chargevolt) MACRO_NVSWRITE(chargecurrent) MACRO_NVSWRITE(dischargecurrent) @@ -402,45 +483,50 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE(cellminmv) MACRO_NVSWRITE(cellmaxmv) MACRO_NVSWRITE(kneemv) - MACRO_NVSWRITE(sensitivity); - MACRO_NVSWRITE(current_value1); - MACRO_NVSWRITE(current_value2); - MACRO_NVSWRITE(cellmaxspikemv); - MACRO_NVSWRITE(chargetemplow); - MACRO_NVSWRITE(chargetemphigh); - MACRO_NVSWRITE(dischargetemplow); - MACRO_NVSWRITE(dischargetemphigh); - MACRO_NVSWRITE(stopchargebalance); - MACRO_NVSWRITE(socoverride); - MACRO_NVSWRITE(socforcelow); - - MACRO_NVSWRITE(dynamiccharge); - MACRO_NVSWRITE(preventcharging); - MACRO_NVSWRITE(preventdischarge); - MACRO_NVSWRITE(mqtt_enabled); - MACRO_NVSWRITE(mqtt_basic_cell_reporting); - MACRO_NVSWRITE(influxdb_enabled); - MACRO_NVSWRITE(influxdb_loggingFreqSeconds); - - MACRO_NVSWRITEBLOB(tileconfig); - - MACRO_NVSWRITESTRING(ntpServer); - MACRO_NVSWRITESTRING(language); - MACRO_NVSWRITESTRING(mqtt_uri); - MACRO_NVSWRITESTRING(mqtt_topic); - MACRO_NVSWRITESTRING(mqtt_username); - MACRO_NVSWRITESTRING(mqtt_password); - MACRO_NVSWRITESTRING(influxdb_serverurl); - MACRO_NVSWRITESTRING(influxdb_databasebucket); - MACRO_NVSWRITESTRING(influxdb_apitoken); - MACRO_NVSWRITESTRING(influxdb_orgid); - - MACRO_NVSWRITE(absorptiontimer); - MACRO_NVSWRITE(floatvoltage); - MACRO_NVSWRITE(floatvoltagetimer); - MACRO_NVSWRITE(stateofchargeresumevalue); - - MACRO_NVSWRITESTRING(homeassist_apikey); + MACRO_NVSWRITE(sensitivity) + MACRO_NVSWRITE(current_value1) + MACRO_NVSWRITE(current_value2) + MACRO_NVSWRITE(cellmaxspikemv) + MACRO_NVSWRITE(chargetemplow) + MACRO_NVSWRITE(chargetemphigh) + MACRO_NVSWRITE(dischargetemplow) + MACRO_NVSWRITE(dischargetemphigh) + MACRO_NVSWRITE(stopchargebalance) + MACRO_NVSWRITE(socoverride) + MACRO_NVSWRITE(socforcelow) + + MACRO_NVSWRITE(dynamiccharge) + MACRO_NVSWRITE(preventcharging) + MACRO_NVSWRITE(preventdischarge) + MACRO_NVSWRITE(mqtt_enabled) + MACRO_NVSWRITE(mqtt_basic_cell_reporting) + MACRO_NVSWRITE(influxdb_enabled) + MACRO_NVSWRITE(influxdb_loggingFreqSeconds) + + MACRO_NVSWRITEBLOB(tileconfig) + + MACRO_NVSWRITESTRING(ntpServer) + MACRO_NVSWRITESTRING(language) + MACRO_NVSWRITESTRING(mqtt_uri) + MACRO_NVSWRITESTRING(mqtt_topic) + MACRO_NVSWRITESTRING(mqtt_username) + MACRO_NVSWRITESTRING(mqtt_password) + MACRO_NVSWRITESTRING(influxdb_serverurl) + MACRO_NVSWRITESTRING(influxdb_databasebucket) + MACRO_NVSWRITESTRING(influxdb_apitoken) + MACRO_NVSWRITESTRING(influxdb_orgid) + + MACRO_NVSWRITE(absorptiontimer) + MACRO_NVSWRITE(floatvoltage) + MACRO_NVSWRITE(floatvoltagetimer) + MACRO_NVSWRITE(stateofchargeresumevalue) + + MACRO_NVSWRITESTRING(homeassist_apikey) + + MACRO_NVSWRITE(soh_total_milliamphour_out) + MACRO_NVSWRITE(soh_total_milliamphour_in) + MACRO_NVSWRITE(soh_lifetime_battery_cycles) + MACRO_NVSWRITE_UINT8(soh_eol_capacity) ESP_ERROR_CHECK(nvs_commit(nvs_handle)); nvs_close(nvs_handle); @@ -473,104 +559,110 @@ void LoadConfiguration(diybms_eeprom_settings *settings) else { // Open - MACRO_NVSREAD(totalNumberOfBanks); - MACRO_NVSREAD(totalNumberOfSeriesModules); - MACRO_NVSREAD(baudRate); - MACRO_NVSREAD(interpacketgap); - - MACRO_NVSREADBLOB(rulevalue); - MACRO_NVSREADBLOB(rulehysteresis); - MACRO_NVSREADBLOB(rulerelaystate); - MACRO_NVSREADBLOB(rulerelaydefault); - MACRO_NVSREADBLOB(relaytype); - - MACRO_NVSREAD(graph_voltagehigh); - MACRO_NVSREAD(graph_voltagelow); - MACRO_NVSREAD(BypassOverTempShutdown); - MACRO_NVSREAD(BypassThresholdmV); - MACRO_NVSREAD(timeZone); - MACRO_NVSREAD(minutesTimeZone); - MACRO_NVSREAD(daylight); - MACRO_NVSREAD(loggingEnabled); - MACRO_NVSREAD(loggingFrequencySeconds); - - MACRO_NVSREAD(currentMonitoringEnabled); - MACRO_NVSREAD(currentMonitoringModBusAddress); - MACRO_NVSREAD_UINT8(currentMonitoringDevice); - - MACRO_NVSREAD(currentMonitoring_shuntmv); - MACRO_NVSREAD(currentMonitoring_shuntmaxcur); - MACRO_NVSREAD(currentMonitoring_batterycapacity); - MACRO_NVSREAD(currentMonitoring_fullchargevolt); - MACRO_NVSREAD(currentMonitoring_tailcurrent); - MACRO_NVSREAD(currentMonitoring_chargeefficiency); - MACRO_NVSREAD(currentMonitoring_shuntcal); - MACRO_NVSREAD(currentMonitoring_temperaturelimit); - MACRO_NVSREAD(currentMonitoring_overvoltagelimit); - MACRO_NVSREAD(currentMonitoring_undervoltagelimit); - MACRO_NVSREAD(currentMonitoring_overcurrentlimit); - MACRO_NVSREAD(currentMonitoring_undercurrentlimit); - MACRO_NVSREAD(currentMonitoring_overpowerlimit); - MACRO_NVSREAD(currentMonitoring_shunttempcoefficient); - MACRO_NVSREAD(currentMonitoring_tempcompenabled); - - MACRO_NVSREAD(rs485baudrate); - MACRO_NVSREAD_UINT8(rs485databits); - MACRO_NVSREAD_UINT8(rs485parity); - MACRO_NVSREAD_UINT8(rs485stopbits); - MACRO_NVSREAD_UINT8(canbusprotocol); - MACRO_NVSREAD_UINT8(canbusinverter); - MACRO_NVSREAD(canbusbaud); - MACRO_NVSREAD_UINT8(canbus_equipment_addr) - MACRO_NVSREAD(nominalbatcap); - MACRO_NVSREAD(chargevolt); - MACRO_NVSREAD(chargecurrent); - MACRO_NVSREAD(dischargecurrent); - MACRO_NVSREAD(dischargevolt); - MACRO_NVSREAD(cellminmv); - MACRO_NVSREAD(cellmaxmv); - MACRO_NVSREAD(kneemv); - MACRO_NVSREAD(sensitivity); - MACRO_NVSREAD(current_value1); - MACRO_NVSREAD(current_value2); - MACRO_NVSREAD(cellmaxspikemv); - MACRO_NVSREAD(chargetemplow); - MACRO_NVSREAD(chargetemphigh); - MACRO_NVSREAD(dischargetemplow); - MACRO_NVSREAD(dischargetemphigh); - MACRO_NVSREAD(stopchargebalance); - MACRO_NVSREAD(socoverride); - MACRO_NVSREAD(socforcelow); - - MACRO_NVSREAD(dynamiccharge); - MACRO_NVSREAD(preventcharging); - MACRO_NVSREAD(preventdischarge); - - MACRO_NVSREAD(mqtt_enabled); - MACRO_NVSREAD(mqtt_basic_cell_reporting); - MACRO_NVSREAD(influxdb_enabled); - MACRO_NVSREAD(influxdb_loggingFreqSeconds); - - MACRO_NVSREADBLOB(tileconfig); - - MACRO_NVSREADSTRING(ntpServer); - MACRO_NVSREADSTRING(language); - MACRO_NVSREADSTRING(mqtt_uri); - MACRO_NVSREADSTRING(mqtt_topic); - MACRO_NVSREADSTRING(mqtt_username); - MACRO_NVSREADSTRING(mqtt_password); - MACRO_NVSREADSTRING(influxdb_serverurl); - MACRO_NVSREADSTRING(influxdb_databasebucket); - MACRO_NVSREADSTRING(influxdb_apitoken); - MACRO_NVSREADSTRING(influxdb_orgid); - - MACRO_NVSREAD(absorptiontimer); - MACRO_NVSREAD(floatvoltage); - MACRO_NVSREAD(floatvoltagetimer); - MACRO_NVSREAD_UINT8(stateofchargeresumevalue); - - MACRO_NVSREADSTRING(homeassist_apikey); + MACRO_NVSREAD(totalNumberOfBanks) + MACRO_NVSREAD(totalNumberOfSeriesModules) + MACRO_NVSREAD(baudRate) + MACRO_NVSREAD(interpacketgap) + + MACRO_NVSREADBLOB(rulevalue) + MACRO_NVSREADBLOB(rulehysteresis) + MACRO_NVSREADBLOB(rulerelaystate) + MACRO_NVSREADBLOB(rulerelaydefault) + MACRO_NVSREADBLOB(relaytype) + + MACRO_NVSREAD(graph_voltagehigh) + MACRO_NVSREAD(graph_voltagelow) + MACRO_NVSREAD(BypassOverTempShutdown) + MACRO_NVSREAD(BypassThresholdmV) + MACRO_NVSREAD(timeZone) + MACRO_NVSREAD(minutesTimeZone) + MACRO_NVSREAD(daylight) + MACRO_NVSREAD(loggingEnabled) + MACRO_NVSREAD(loggingFrequencySeconds) + + MACRO_NVSREAD(currentMonitoringEnabled) + MACRO_NVSREAD(currentMonitoringModBusAddress) + MACRO_NVSREAD_UINT8(currentMonitoringDevice) + + MACRO_NVSREAD(currentMonitoring_shuntmv) + MACRO_NVSREAD(currentMonitoring_shuntmaxcur) + MACRO_NVSREAD(currentMonitoring_batterycapacity) + MACRO_NVSREAD(currentMonitoring_fullchargevolt) + MACRO_NVSREAD(currentMonitoring_tailcurrent) + MACRO_NVSREAD(currentMonitoring_chargeefficiency) + MACRO_NVSREAD(currentMonitoring_shuntcal) + MACRO_NVSREAD(currentMonitoring_temperaturelimit) + MACRO_NVSREAD(currentMonitoring_overvoltagelimit) + MACRO_NVSREAD(currentMonitoring_undervoltagelimit) + MACRO_NVSREAD(currentMonitoring_overcurrentlimit) + MACRO_NVSREAD(currentMonitoring_undercurrentlimit) + MACRO_NVSREAD(currentMonitoring_overpowerlimit) + MACRO_NVSREAD(currentMonitoring_shunttempcoefficient) + MACRO_NVSREAD(currentMonitoring_tempcompenabled) + + MACRO_NVSREAD(rs485baudrate) + MACRO_NVSREAD_UINT8(rs485databits) + MACRO_NVSREAD_UINT8(rs485parity) + MACRO_NVSREAD_UINT8(rs485stopbits) + MACRO_NVSREAD_UINT8(protocol) + MACRO_NVSREAD_UINT8(canbusinverter) + MACRO_NVSREAD(canbusbaud) + + MACRO_NVSREAD_UINT8(canbus_equipment_addr) + MACRO_NVSREAD(nominalbatcap) + MACRO_NVSREAD(chargevolt) + MACRO_NVSREAD(chargecurrent) + MACRO_NVSREAD(dischargecurrent) + MACRO_NVSREAD(dischargevolt) + MACRO_NVSREAD(cellminmv) + MACRO_NVSREAD(cellmaxmv) + MACRO_NVSREAD(kneemv) + MACRO_NVSREAD(sensitivity) + MACRO_NVSREAD(current_value1) + MACRO_NVSREAD(current_value2) + MACRO_NVSREAD(cellmaxspikemv) + MACRO_NVSREAD(chargetemplow) + MACRO_NVSREAD(chargetemphigh) + MACRO_NVSREAD(dischargetemplow) + MACRO_NVSREAD(dischargetemphigh) + MACRO_NVSREAD(stopchargebalance) + MACRO_NVSREAD(socoverride) + MACRO_NVSREAD(socforcelow) + + MACRO_NVSREAD(dynamiccharge) + MACRO_NVSREAD(preventcharging) + MACRO_NVSREAD(preventdischarge) + + MACRO_NVSREAD(mqtt_enabled) + MACRO_NVSREAD(mqtt_basic_cell_reporting) + MACRO_NVSREAD(influxdb_enabled) + MACRO_NVSREAD(influxdb_loggingFreqSeconds) + + MACRO_NVSREADBLOB(tileconfig) + + MACRO_NVSREADSTRING(ntpServer) + MACRO_NVSREADSTRING(language) + MACRO_NVSREADSTRING(mqtt_uri) + MACRO_NVSREADSTRING(mqtt_topic) + MACRO_NVSREADSTRING(mqtt_username) + MACRO_NVSREADSTRING(mqtt_password) + MACRO_NVSREADSTRING(influxdb_serverurl) + MACRO_NVSREADSTRING(influxdb_databasebucket) + MACRO_NVSREADSTRING(influxdb_apitoken) + MACRO_NVSREADSTRING(influxdb_orgid) + + MACRO_NVSREAD(absorptiontimer) + MACRO_NVSREAD(floatvoltage) + MACRO_NVSREAD(floatvoltagetimer) + MACRO_NVSREAD_UINT8(stateofchargeresumevalue) + + MACRO_NVSREADSTRING(homeassist_apikey) + + MACRO_NVSREAD(soh_total_milliamphour_out) + MACRO_NVSREAD(soh_total_milliamphour_in) + MACRO_NVSREAD(soh_lifetime_battery_cycles) + MACRO_NVSREAD_UINT8(soh_eol_capacity) nvs_close(nvs_handle); } @@ -601,11 +693,11 @@ void DefaultConfiguration(diybms_eeprom_settings *_myset) _myset->mqtt_enabled = false; _myset->mqtt_basic_cell_reporting = false; - _myset->canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; + _myset->protocol = ProtocolEmulation::EMULATION_DISABLED; _myset->canbusinverter = CanBusInverter::INVERTER_GENERIC; _myset->canbus_equipment_addr = 0; - _myset->canbusbaud=500; + _myset->canbusbaud = 500; _myset->nominalbatcap = 280; // Scale 1 _myset->chargevolt = 565; // Scale 0.1 _myset->chargecurrent = 650; // Scale 0.1 @@ -753,6 +845,13 @@ void DefaultConfiguration(diybms_eeprom_settings *_myset) _myset->floatvoltagetimer = 6 * 60; // Once battery SoC drops below this value, resume normal charging operation _myset->stateofchargeresumevalue = 96; + + // State of health + _myset->soh_total_milliamphour_out = 0; + _myset->soh_total_milliamphour_in = 0; + _myset->soh_lifetime_battery_cycles = 6000; + _myset->soh_eol_capacity = 80; + _myset->soh_percent = 100.0F; } /// @brief Save WIFI settings into FLASH NVS @@ -846,12 +945,12 @@ void ValidateConfiguration(diybms_eeprom_settings *settings) settings->baudRate = defaults.baudRate; } - if (settings->graph_voltagehigh > 5000 || settings->graph_voltagehigh < 2000 || settings->graph_voltagehigh < 0) + if (settings->graph_voltagehigh > 5000 || settings->graph_voltagehigh < 2000) { settings->graph_voltagehigh = defaults.graph_voltagehigh; } - if (settings->graph_voltagelow > settings->graph_voltagehigh || settings->graph_voltagelow < 0) + if (settings->graph_voltagelow > settings->graph_voltagehigh) { settings->graph_voltagelow = 0; } @@ -970,9 +1069,9 @@ void ValidateConfiguration(diybms_eeprom_settings *settings) } // Builds up a JSON document which mirrors the parameters inside "diybms_eeprom_settings" -void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settings *settings) +void GenerateSettingsJSONDocument(JsonDocument &doc, diybms_eeprom_settings *settings) { - JsonObject root = doc->createNestedObject("diybms_settings"); + JsonObject root = doc["diybms_settings"].to(); root[totalNumberOfBanks_JSONKEY] = settings->totalNumberOfBanks; root[totalNumberOfSeriesModules_JSONKEY] = settings->totalNumberOfSeriesModules; @@ -1015,9 +1114,9 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[rs485stopbits_JSONKEY] = settings->rs485stopbits; root[language_JSONKEY] = settings->language; - root[homeassist_apikey_JSONKEY]=settings->homeassist_apikey; + root[homeassist_apikey_JSONKEY] = settings->homeassist_apikey; - JsonObject mqtt = root.createNestedObject("mqtt"); + JsonObject mqtt = root["mqtt"].to(); mqtt[mqtt_enabled_JSONKEY] = settings->mqtt_enabled; mqtt[mqtt_basic_cell_reporting_JSONKEY] = settings->mqtt_basic_cell_reporting; mqtt[mqtt_uri_JSONKEY] = settings->mqtt_uri; @@ -1025,7 +1124,7 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin mqtt[mqtt_username_JSONKEY] = settings->mqtt_username; mqtt[mqtt_password_JSONKEY] = settings->mqtt_password; - JsonObject influxdb = root.createNestedObject("influxdb"); + JsonObject influxdb = root["influxdb"].to(); influxdb[influxdb_enabled_JSONKEY] = settings->influxdb_enabled; influxdb[influxdb_apitoken_JSONKEY] = settings->influxdb_apitoken; influxdb[influxdb_databasebucket_JSONKEY] = settings->influxdb_databasebucket; @@ -1033,16 +1132,18 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin influxdb[influxdb_serverurl_JSONKEY] = settings->influxdb_serverurl; influxdb[influxdb_loggingFreqSeconds_JSONKEY] = settings->influxdb_loggingFreqSeconds; - JsonObject outputs = root.createNestedObject("outputs"); - JsonArray d = outputs.createNestedArray("default"); - JsonArray t = outputs.createNestedArray("type"); + JsonObject outputs = root["outputs"].to(); + JsonArray d = outputs["default"].to(); + ; + JsonArray t = outputs["type"].to(); + ; for (uint8_t i = 0; i < RELAY_TOTAL; i++) { d.add(settings->rulerelaydefault[i]); t.add(settings->relaytype[i]); } - JsonObject rules = root.createNestedObject("rules"); + JsonObject rules = root["rules"].to(); for (uint8_t rr = 0; rr < RELAY_RULES; rr++) { // This is a default "catch all" @@ -1059,22 +1160,23 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin ESP_LOGE(TAG, "Loop outside bounds of MAXIMUM_RuleNumber"); } - JsonObject state = rules.createNestedObject(elementName); + JsonObject state = rules[elementName].to(); state["value"] = settings->rulevalue[rr]; state["hysteresis"] = settings->rulehysteresis[rr]; - JsonArray relaystate = state.createNestedArray("state"); + JsonArray relaystate = state["state"].to(); + ; for (uint8_t rt = 0; rt < RELAY_TOTAL; rt++) { relaystate.add(settings->rulerelaystate[rr][rt]); } } // end for - root[canbusprotocol_JSONKEY] = (uint8_t)settings->canbusprotocol; + root[protocol_JSONKEY] = (uint8_t)settings->protocol; root[canbusinverter_JSONKEY] = (uint8_t)settings->canbusinverter; root[canbusbaud_JSONKEY] = settings->canbusbaud; - root[canbus_equipment_addr_JSONKEY]=settings->canbus_equipment_addr; + root[canbus_equipment_addr_JSONKEY] = settings->canbus_equipment_addr; root[nominalbatcap_JSONKEY] = settings->nominalbatcap; root[chargevolt_JSONKEY] = settings->chargevolt; @@ -1106,16 +1208,22 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[floatvoltagetimer_JSONKEY] = settings->floatvoltagetimer; root[stateofchargeresumevalue_JSONKEY] = settings->stateofchargeresumevalue; - JsonArray tv = root.createNestedArray("tilevisibility"); + JsonArray tv = root["tilevisibility"].to(); for (uint8_t i = 0; i < sizeof(settings->tileconfig) / sizeof(uint16_t); i++) { tv.add(settings->tileconfig[i]); } // wifi["password"] = DIYBMSSoftAP::Config().wifi_passphrase; + + root[soh_total_milliamphour_out_JSONKEY] = settings->soh_total_milliamphour_out; + root[soh_total_milliamphour_in_JSONKEY] = settings->soh_total_milliamphour_in; + root[soh_lifetime_battery_cycles_JSONKEY] = settings->soh_lifetime_battery_cycles; + root[soh_eol_capacity_JSONKEY] = settings->soh_eol_capacity; + } -void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) +void JSONToSettings(JsonDocument &doc, diybms_eeprom_settings *settings) { // Use defaults to populate the settings, just in case we are missing values from the JSON DefaultConfiguration(settings); @@ -1174,10 +1282,10 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) strncpy(settings->language, root[language_JSONKEY].as().c_str(), sizeof(settings->language)); - settings->canbusprotocol = (CanBusProtocolEmulation)root[canbusprotocol_JSONKEY]; + settings->protocol = (ProtocolEmulation)root[protocol_JSONKEY]; settings->canbusinverter = (CanBusInverter)root[canbusinverter_JSONKEY]; settings->canbusbaud = root[canbusbaud_JSONKEY]; - settings->canbus_equipment_addr=root[canbus_equipment_addr_JSONKEY]; + settings->canbus_equipment_addr = root[canbus_equipment_addr_JSONKEY]; settings->nominalbatcap = root[nominalbatcap_JSONKEY]; settings->chargevolt = root[chargevolt_JSONKEY]; settings->chargecurrent = root[chargecurrent_JSONKEY]; @@ -1201,10 +1309,16 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) settings->current_value1 = root[current_value1_JSONKEY]; settings->current_value2 = root[current_value2_JSONKEY]; - settings->absorptiontimer=root[absorptiontimer_JSONKEY]; - settings->floatvoltage=root[floatvoltage_JSONKEY]; - settings->floatvoltagetimer=root[floatvoltagetimer_JSONKEY]; - settings->stateofchargeresumevalue=root[stateofchargeresumevalue_JSONKEY]; + settings->absorptiontimer = root[absorptiontimer_JSONKEY]; + settings->floatvoltage = root[floatvoltage_JSONKEY]; + settings->floatvoltagetimer = root[floatvoltagetimer_JSONKEY]; + settings->stateofchargeresumevalue = root[stateofchargeresumevalue_JSONKEY]; + + settings->soh_total_milliamphour_out = root[soh_total_milliamphour_out_JSONKEY]; + settings->soh_total_milliamphour_in = root[soh_total_milliamphour_in_JSONKEY]; + settings->soh_lifetime_battery_cycles = root[soh_lifetime_battery_cycles_JSONKEY]; + settings->soh_eol_capacity=root[soh_eol_capacity_JSONKEY]; + strncpy(settings->homeassist_apikey, root[homeassist_apikey_JSONKEY].as().c_str(), sizeof(settings->homeassist_apikey)); @@ -1212,7 +1326,7 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) if (!mqtt.isNull()) { settings->mqtt_enabled = mqtt[mqtt_enabled_JSONKEY]; - settings->mqtt_basic_cell_reporting=mqtt[mqtt_basic_cell_reporting_JSONKEY]; + settings->mqtt_basic_cell_reporting = mqtt[mqtt_basic_cell_reporting_JSONKEY]; strncpy(settings->mqtt_uri, mqtt[mqtt_uri_JSONKEY].as().c_str(), sizeof(settings->mqtt_uri)); strncpy(settings->mqtt_topic, mqtt[mqtt_topic_JSONKEY].as().c_str(), sizeof(settings->mqtt_topic)); strncpy(settings->mqtt_username, mqtt[mqtt_username_JSONKEY].as().c_str(), sizeof(settings->mqtt_username)); @@ -1302,4 +1416,4 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) // Need to check for over flow of tileconfig array settings->tileconfig[i] = v.as(); } -} \ No newline at end of file +} diff --git a/ESPController/src/victron_canbus.cpp b/ESPController/src/victron_canbus.cpp index efa26fd6..47f67e1a 100644 --- a/ESPController/src/victron_canbus.cpp +++ b/ESPController/src/victron_canbus.cpp @@ -183,7 +183,7 @@ void victron_message_355() struct data355 { uint16_t stateofchargevalue; - // uint16_t stateofhealthvalue; + uint16_t stateofhealthvalue; // uint16_t highresolutionsoc; }; @@ -193,7 +193,7 @@ void victron_message_355() // 0 SOC value un16 1 % data.stateofchargevalue = rules.StateOfChargeWithRulesApplied(&mysettings, currentMonitor.stateofcharge); // 2 SOH value un16 1 % - // data.stateofhealthvalue = 100; + data.stateofhealthvalue = (uint16_t)(trunc(mysettings.soh_percent)); send_canbus_message(0x355, (uint8_t *)&data, sizeof(data355)); } diff --git a/ESPController/src/webserver.cpp b/ESPController/src/webserver.cpp index d31ec5d0..b398a892 100644 --- a/ESPController/src/webserver.cpp +++ b/ESPController/src/webserver.cpp @@ -668,9 +668,10 @@ httpd_handle_t start_webserver(void) /* Function for stopping the webserver */ void stop_webserver(httpd_handle_t server) { - if (server) + if (server!=nullptr) { /* Stop the httpd server */ + ESP_LOGI(TAG, "httpd_stop"); httpd_stop(server); } } diff --git a/ESPController/src/webserver_json_post.cpp b/ESPController/src/webserver_json_post.cpp index 045c5fe1..98819705 100644 --- a/ESPController/src/webserver_json_post.cpp +++ b/ESPController/src/webserver_json_post.cpp @@ -188,8 +188,8 @@ esp_err_t post_saveinfluxdbsetting_json_handler(httpd_req_t *req, bool urlEncode // Saves all the BMS controller settings to a JSON file in FLASH esp_err_t post_saveconfigurationtoflash_json_handler(httpd_req_t *req, bool urlEncoded) { - DynamicJsonDocument doc(5000); - GenerateSettingsJSONDocument(&doc, &mysettings); + JsonDocument doc; + GenerateSettingsJSONDocument(doc, &mysettings); struct tm timeinfo; @@ -472,14 +472,14 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) // If a user updates the charge config, reset the charging mode as well rules.setChargingMode(ChargingMode::standard); - if (GetKeyValue(httpbuf, "canbusprotocol", &temp, urlEncoded)) + if (GetKeyValue(httpbuf, "protocol", &temp, urlEncoded)) { - mysettings.canbusprotocol = (CanBusProtocolEmulation)temp; + mysettings.protocol = (ProtocolEmulation)temp; } else { // Field not found/invalid, so disable - mysettings.canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; + mysettings.protocol = ProtocolEmulation::EMULATION_DISABLED; mysettings.canbusinverter = CanBusInverter::INVERTER_GENERIC; mysettings.canbusbaud = 500; } @@ -563,7 +563,7 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) GetKeyValue(httpbuf, "floattimer", &mysettings.floatvoltagetimer, urlEncoded); GetKeyValue(httpbuf, "socresume", &mysettings.stateofchargeresumevalue, urlEncoded); - if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_DISABLED) + if (mysettings.protocol == ProtocolEmulation::EMULATION_DISABLED) { // Reset CAN counters if its disabled. canbus_messages_received = 0; @@ -573,13 +573,33 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) } // Default GENERIC inverter for VICTRON integration - if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_VICTRON) + if (mysettings.protocol == ProtocolEmulation::CANBUS_VICTRON) { mysettings.canbusinverter = CanBusInverter::INVERTER_GENERIC; } + GetKeyValue(httpbuf, "expected_cycles", &mysettings.soh_lifetime_battery_cycles, urlEncoded); + GetKeyValue(httpbuf, "eol_capacity", &mysettings.soh_eol_capacity, urlEncoded); + + if (GetKeyValue(httpbuf, "total_ah_discharge", &mysettings.soh_total_milliamphour_out, urlEncoded)) + { + mysettings.soh_total_milliamphour_out = mysettings.soh_total_milliamphour_out * 1000; + } + if (GetKeyValue(httpbuf, "total_ah_charge", &mysettings.soh_total_milliamphour_in, urlEncoded)) + { + mysettings.soh_total_milliamphour_in = mysettings.soh_total_milliamphour_in * 1000; + } + + CalculateStateOfHealth(&mysettings); + saveConfiguration(); + // Notify the RS458 RX task that might be in bocking state (after enabling the RS485_PYLONTECH emulation) + if (rs485_rx_task_handle != NULL) + { + xTaskNotify(rs485_rx_task_handle, 0x00, eNotifyAction::eNoAction); + } + return SendSuccess(req); } @@ -643,9 +663,9 @@ esp_err_t post_savecmrelay_json_handler(httpd_req_t *req, bool urlEncoded) } /// @brief Generates new home assistant API key and stored into flash -/// @param req -/// @param urlEncoded -/// @return +/// @param req +/// @param urlEncoded +/// @return esp_err_t post_homeassistant_apikey_json_handler(httpd_req_t *req, bool urlEncoded) { char buffer[32]; @@ -856,7 +876,7 @@ esp_err_t post_avrprog_json_handler(httpd_req_t *req, bool urlEncoded) return SendFailure(req); } - DynamicJsonDocument doc(512); + JsonDocument doc; int bufferused = 0; @@ -875,7 +895,7 @@ esp_err_t post_avrprog_json_handler(httpd_req_t *req, bool urlEncoded) if (LittleFS.exists(manifestfilename)) { - DynamicJsonDocument jsonmanifest(3000); + JsonDocument jsonmanifest; File file = LittleFS.open(manifestfilename); DeserializationError error = deserializeJson(jsonmanifest, file); if (error != DeserializationError::Ok) @@ -1063,6 +1083,8 @@ esp_err_t post_saverules_json_handler(httpd_req_t *req, bool urlEncoded) esp_err_t post_restoreconfig_json_handler(httpd_req_t *req, bool urlEncoded) { + JsonDocument doc; + bool success = false; if (_avrsettings.programmingModeEnabled) @@ -1081,9 +1103,7 @@ esp_err_t post_restoreconfig_json_handler(httpd_req_t *req, bool urlEncoded) } uint16_t flashram = 0; - if (GetKeyValue(httpbuf, "flashram", &flashram, urlEncoded)) - { - } + GetKeyValue(httpbuf, "flashram", &flashram, urlEncoded); if (flashram == 0) { @@ -1098,9 +1118,6 @@ esp_err_t post_restoreconfig_json_handler(httpd_req_t *req, bool urlEncoded) { ESP_LOGI(TAG, "Restore SD config from %s", filename); - // Needs to be large enough to de-serialize the JSON file - DynamicJsonDocument doc(5000); - File file = SD.open(filename, "r"); // Deserialize the JSON document @@ -1143,9 +1160,6 @@ esp_err_t post_restoreconfig_json_handler(httpd_req_t *req, bool urlEncoded) { ESP_LOGI(TAG, "Restore LittleFS config from %s", filename); - // Needs to be large enough to de-serialize the JSON file - DynamicJsonDocument doc(5500); - File file = LittleFS.open(filename, "r"); // Deserialize the JSON document @@ -1253,4 +1267,4 @@ esp_err_t save_data_handler(httpd_req_t *req) ESP_LOGE(TAG, "No API post match: %s", name.c_str()); return httpd_resp_send_500(req); -} \ No newline at end of file +} diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index e922680d..ce82aaed 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -25,7 +25,7 @@ esp_err_t content_handler_avrstorage(httpd_req_t *req) if (LittleFS.exists(manifest)) { // Use Dynamic to avoid head issues - DynamicJsonDocument doc(3000); + JsonDocument doc; File file = LittleFS.open(manifest); DeserializationError error = deserializeJson(doc, file); if (error) @@ -524,9 +524,9 @@ esp_err_t content_handler_modules(httpd_req_t *req) prg.sendGetSettingsRequest(c); } - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject root = doc.to(); - JsonObject settings = root.createNestedObject("settings"); + JsonObject settings = root["settings"].to(); uint8_t b = c / mysettings.totalNumberOfSeriesModules; uint8_t m = c - (b * mysettings.totalNumberOfSeriesModules); @@ -563,7 +563,7 @@ esp_err_t content_handler_modules(httpd_req_t *req) esp_err_t content_handler_avrstatus(httpd_req_t *req) { int bufferused = 0; - StaticJsonDocument<200> doc; + JsonDocument doc; doc["inprogress"] = _avrsettings.inProgress ? 1 : 0; doc["result"] = _avrsettings.progresult; doc["duration"] = _avrsettings.duration; @@ -577,10 +577,10 @@ esp_err_t content_handler_avrstatus(httpd_req_t *req) esp_err_t content_handler_tileconfig(httpd_req_t *req) { - StaticJsonDocument<200> doc; + JsonDocument doc; JsonObject root = doc.to(); - JsonObject settings = root.createNestedObject("tileconfig"); - JsonArray v = settings.createNestedArray("values"); + JsonObject settings = root["tileconfig"].to(); + JsonArray v = settings["values"].to();; for (auto n : mysettings.tileconfig) { @@ -595,15 +595,23 @@ esp_err_t content_handler_chargeconfig(httpd_req_t *req) { int bufferused = 0; - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject root = doc.to(); - JsonObject settings = root.createNestedObject("chargeconfig"); + JsonObject settings = root["chargeconfig"].to(); - settings["canbusprotocol"] = mysettings.canbusprotocol; + settings["protocol"] = mysettings.protocol; settings["canbusinverter"] = mysettings.canbusinverter; settings["canbusbaud"] = mysettings.canbusbaud; settings["equip_addr"] = mysettings.canbus_equipment_addr; settings["nominalbatcap"] = mysettings.nominalbatcap; + + settings["expectedlifetime_cycles"] = mysettings.soh_lifetime_battery_cycles; + settings["eol_capacity"] = mysettings.soh_eol_capacity; + settings["total_mah_charge"] = mysettings.soh_total_milliamphour_in; + settings["total_mah_discharge"] = mysettings.soh_total_milliamphour_out; + settings["estimatebatterycycle"] = mysettings.soh_estimated_battery_cycles; + settings["stateofhealth"] = mysettings.soh_percent; + settings["chargevolt"] = mysettings.chargevolt; settings["chargecurrent"] = mysettings.chargecurrent; settings["dischargecurrent"] = mysettings.dischargecurrent; @@ -640,7 +648,7 @@ esp_err_t content_handler_rules(httpd_req_t *req) { int bufferused = 0; - DynamicJsonDocument doc(3000); + JsonDocument doc; JsonObject root = doc.to(); struct tm timeinfo; @@ -655,7 +663,7 @@ esp_err_t content_handler_rules(httpd_req_t *req) root["ControlState"] = _controller_state; - JsonArray defaultArray = root.createNestedArray("relaydefault"); + JsonArray defaultArray = root["relaydefault"].to(); for (auto v : mysettings.rulerelaydefault) { switch (v) @@ -673,7 +681,7 @@ esp_err_t content_handler_rules(httpd_req_t *req) } } - JsonArray typeArray = root.createNestedArray("relaytype"); + JsonArray typeArray = root["relaytype"].to(); for (auto v : mysettings.relaytype) { switch (v) @@ -690,15 +698,15 @@ esp_err_t content_handler_rules(httpd_req_t *req) } } - JsonArray bankArray = root.createNestedArray("rules"); + JsonArray bankArray = root["rules"].to(); for (uint8_t r = 0; r < RELAY_RULES; r++) { - JsonObject rule = bankArray.createNestedObject(); + JsonObject rule = bankArray.add(); rule["value"] = mysettings.rulevalue[r]; rule["hysteresis"] = mysettings.rulehysteresis[r]; rule["triggered"] = rules.ruleOutcome((Rule)r); - JsonArray data = rule.createNestedArray("relays"); + JsonArray data = rule["relays"].to(); for (auto v : mysettings.rulerelaystate[r]) { @@ -727,10 +735,10 @@ esp_err_t content_handler_settings(httpd_req_t *req) { int bufferused = 0; - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject root = doc.to(); - JsonObject settings = root.createNestedObject("settings"); + JsonObject settings = root["settings"].to(); settings["totalnumberofbanks"] = mysettings.totalNumberOfBanks; settings["totalseriesmodules"] = mysettings.totalNumberOfSeriesModules; @@ -794,7 +802,7 @@ esp_err_t content_handler_settings(httpd_req_t *req) settings["man_dns2"] = ip4_to_string(_wificonfig.wifi_dns2); } - JsonObject wifi = root.createNestedObject("wifi"); + JsonObject wifi = root["wifi"].to(); if (wifi_isconnected) { @@ -831,13 +839,13 @@ esp_err_t content_handler_integration(httpd_req_t *req) { int bufferused = 0; - DynamicJsonDocument doc(1024); + JsonDocument doc; JsonObject root = doc.to(); - JsonObject ha = root.createNestedObject("ha"); + JsonObject ha = root["ha"].to(); ha["api"] = mysettings.homeassist_apikey; - JsonObject mqtt = root.createNestedObject("mqtt"); + JsonObject mqtt = root["mqtt"].to(); mqtt["enabled"] = mysettings.mqtt_enabled; mqtt["basiccellreporting"] = mysettings.mqtt_basic_cell_reporting; mqtt["topic"] = mysettings.mqtt_topic; @@ -853,7 +861,7 @@ esp_err_t content_handler_integration(httpd_req_t *req) // We don't output the password in the json file as this could breach security // mqtt["password"] =mysettings.mqtt_password; - JsonObject influxdb = root.createNestedObject("influxdb"); + JsonObject influxdb = root["influxdb"].to(); influxdb["enabled"] = mysettings.influxdb_enabled; influxdb["url"] = mysettings.influxdb_serverurl; influxdb["bucket"] = mysettings.influxdb_databasebucket; @@ -982,7 +990,7 @@ esp_err_t content_handler_monitor2(httpd_req_t *req) (unsigned int)rules.getChargingMode(), rules.getChargingTimerSecondsRemaining()); - if (mysettings.canbusprotocol != CanBusProtocolEmulation::CANBUS_DISABLED && mysettings.dynamiccharge) + if (mysettings.protocol != ProtocolEmulation::EMULATION_DISABLED && mysettings.dynamiccharge) { bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, R"("dyncv":%u,"dyncc":%u,)", @@ -1339,7 +1347,7 @@ esp_err_t ha_handler(httpd_req_t *req) currentMonitor.stateofcharge); } - if (mysettings.canbusprotocol != CanBusProtocolEmulation::CANBUS_DISABLED && mysettings.dynamiccharge) + if (mysettings.protocol != ProtocolEmulation::EMULATION_DISABLED && mysettings.dynamiccharge) { bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, R"(,"dyncv":%u,"dyncc":%u)", diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index 9bbb122b..eed3ef60 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -231,7 +231,7 @@ -
+
Graph: 2D @@ -654,7 +654,7 @@

History

Date/time Voltage Current - SoC% + SoC%% mA In mA Out Highest Range @@ -989,11 +989,11 @@

Charge/Discharge configuration

- This feature allows diyBMS to provide battery and BMS data to CANBUS devices using various 3rd party + This feature allows diyBMS to provide battery and BMS data to CANBUS or RS485 devices using various 3rd party protocols.

- These settings require an external inverter/charger to be able to integrate using CANBUS to control + These settings require an external inverter/charger to be able to integrate using CANBUS (RS458) to control charge/discharge parameters. Pylontech normally operates at 500k baud. Victron can use other speeds depending on the device.

@@ -1005,12 +1005,13 @@

Charge/Discharge configuration

- - - + +
@@ -1034,6 +1035,32 @@

Charge/Discharge configuration

+

State of Health

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +

Charging

@@ -2039,4 +2066,4 @@

Logging

- \ No newline at end of file + diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index 09be33bf..79dc038b 100644 --- a/ESPController/web_src/pagecode.js +++ b/ESPController/web_src/pagecode.js @@ -70,7 +70,7 @@ function upload_file() { xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { - var status = xhr.status; + let status = xhr.status; if (status >= 200 && status < 400) { //Refresh the storage page $("#storage").trigger("click"); @@ -100,7 +100,7 @@ function upload_firmware() { }); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { - var status = xhr.status; + let status = xhr.status; if (status >= 200 && status < 400) { $("#status_div").text("Upload accepted. BMS will reboot."); } else { @@ -120,16 +120,16 @@ function CalculateChargeCurrent(value1, value2, highestCellVoltage, maximumcharg return maximumchargecurrent; } - var knee_voltage = 0 / 100.0; - var at_knee = Math.pow(value1, knee_voltage * Math.pow(knee_voltage, value2)); + let knee_voltage = 0 / 100.0; + let at_knee = Math.pow(value1, knee_voltage * Math.pow(knee_voltage, value2)); - var target_cell_voltage = (cellmaxmv - kneemv) / 100.0; - var at_target_cell_voltage = Math.pow(value1, target_cell_voltage * Math.pow(target_cell_voltage, value2)); + let target_cell_voltage = (cellmaxmv - kneemv) / 100.0; + let at_target_cell_voltage = Math.pow(value1, target_cell_voltage * Math.pow(target_cell_voltage, value2)); - var actual_cell_voltage = (highestCellVoltage - kneemv) / 100.0; - var at_actual_cell_voltage = Math.pow(value1, actual_cell_voltage * Math.pow(actual_cell_voltage, value2)); + let actual_cell_voltage = (highestCellVoltage - kneemv) / 100.0; + let at_actual_cell_voltage = Math.pow(value1, actual_cell_voltage * Math.pow(actual_cell_voltage, value2)); - var percent = 1 - (at_actual_cell_voltage / at_knee) / at_target_cell_voltage; + let percent = 1 - (at_actual_cell_voltage / at_knee) / at_target_cell_voltage; if (percent < 0.01) { percent = 0.01; @@ -139,8 +139,6 @@ function CalculateChargeCurrent(value1, value2, highestCellVoltage, maximumcharg } function DrawChargingGraph() { - - var xaxisvalues = []; var yaxisvalues = []; @@ -158,18 +156,10 @@ function DrawChargingGraph() { yaxisvalues.push(CalculateChargeCurrent(value1, value2, voltage, chargecurrent, kneemv, cellmaxmv)); } - /* - if (window.g3 != null) { - window.g3.dispose(); - window.g3 = null; - } - */ if (window.g3 == null) { window.g3 = echarts.init(document.getElementById('graph3')) - var option; - - option = { + let option = { tooltip: { trigger: 'axis', @@ -304,7 +294,7 @@ function refreshCurrentMonitorValues() { $("#cmmodel").val(data.model.toString(16)); $("#cmfirmwarev").val(data.firmwarev.toString(16)); - var d = new Date(data.firmwaredate * 1000); + let d = new Date(data.firmwaredate * 1000); $("#cmfirmwaredate").val(d.toString()); @@ -357,13 +347,13 @@ function refreshCurrentMonitorValues() { // Show and hide tiles based on bit pattern in tileconfig array function refreshVisibleTiles() { for (i = 0; i < TILE_IDS.length; i++) { - var tc = TILE_IDS[i]; - var value = tileconfig[i]; - for (var a = tc.length - 1; a >= 0; a--) { - var visible = (value & 1) == 1 ? true : false; + let tc = TILE_IDS[i]; + let value = tileconfig[i]; + for (let a = tc.length - 1; a >= 0; a--) { + let visible = (value & 1) == 1 ? true : false; value = value >>> 1; if (tc[a] != null && tc[a] != undefined && tc[a] != "") { - var obj = $("#" + tc[a]); + let obj = $("#" + tc[a]); if (visible) { //Only show if we have not force hidden it if (obj.hasClass(".hide") == false) { @@ -383,16 +373,16 @@ function refreshVisibleTiles() { function postTileVisibiltity() { $(".stat.vistile.hide").removeClass("vistile"); - var newconfig = []; - for (var index = 0; index < tileconfig.length; index++) { + let newconfig = []; + for (let index = 0; index < tileconfig.length; index++) { newconfig.push(0); } - for (var i = 0; i < TILE_IDS.length; i++) { - var tc = TILE_IDS[i]; - var value = 0; - var v = 0x8000; - for (var a = 0; a < tc.length; a++) { + for (let i = 0; i < TILE_IDS.length; i++) { + let tc = TILE_IDS[i]; + let value = 0; + let v = 0x8000; + for (let a = 0; a < tc.length; a++) { if (tc[a] != null && tc[a] != undefined && tc[a] != "") { if ($("#" + tc[a]).hasClass("vistile")) { value = value | v; @@ -404,8 +394,8 @@ function postTileVisibiltity() { newconfig[i] = value; } - var diff = false; - for (var index = 0; index < tileconfig.length; index++) { + let diff = false; + for (let index = 0; index < tileconfig.length; index++) { if (tileconfig[index] != newconfig[index]) { tileconfig[index] = newconfig[index]; diff = true; @@ -550,7 +540,7 @@ function configureModule(button, cellid, attempts) { $('#m').val(data.settings.id); if (data.settings.Cached == true) { - var currentReading = parseFloat($("#modulesRows > tr.selected > td:nth-child(3)").text()); + const currentReading = parseFloat($("#modulesRows > tr.selected > td:nth-child(3)").text()); $("#ActualVoltage").val(currentReading.toFixed(3)); $("#settingConfig h2").html("Settings for module bank:" + data.settings.bank + " module:" + data.settings.module); @@ -642,630 +632,638 @@ function secondsToHms(seconds) { return ""; } - var d = Math.floor(seconds / (3600 * 24)); - var h = Math.floor(seconds % (3600 * 24) / 3600); - var m = Math.floor(seconds % 3600 / 60); - var s = Math.floor(seconds % 60); + let d = Math.floor(seconds / (3600 * 24)); + let h = Math.floor(seconds % (3600 * 24) / 3600); + let m = Math.floor(seconds % 3600 / 60); + let s = Math.floor(seconds % 60); - var dDisplay = d > 0 ? d + "d" : ""; - var hDisplay = h > 0 ? h + "h" : ""; - var mDisplay = m > 0 ? m + "m" : ""; - var sDisplay = h > 0 ? "" : (s > 0 ? s + "s" : ""); + let dDisplay = d > 0 ? d + "d" : ""; + let hDisplay = h > 0 ? h + "h" : ""; + let mDisplay = m > 0 ? m + "m" : ""; + let sDisplay = h > 0 ? "" : (s > 0 ? s + "s" : ""); return dDisplay + hDisplay + mDisplay + sDisplay; } -function queryBMS() { - $.getJSON("/api/monitor2", function (jsondata) { - var labels = []; - var cells = []; - var bank = []; - var voltages = []; - var voltagesmin = []; - var voltagesmax = []; - var tempint = []; - var tempext = []; - var pwm = []; - - var minVoltage = DEFAULT_GRAPH_MIN_VOLTAGE / 1000.0; - var maxVoltage = DEFAULT_GRAPH_MAX_VOLTAGE / 1000.0; - - var minExtTemp = 999; - var maxExtTemp = -999; - - var bankNumber = 0; - var cellsInBank = 0; - - // Need one color for each bank, could make it colourful I suppose :-) - const colours = [ - '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', - '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', - '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', - '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', - ] - - const red = '#B44247' - - const highestCell = '#8c265d' - const lowestCell = '#b6a016' - - var markLineData = []; - - markLineData.push({ name: 'avg', type: 'average', lineStyle: { color: '#ddd', width: 2, type: 'dotted', opacity: 0.3 }, label: { distance: [10, 0], position: 'start', color: "#eeeeee", textBorderColor: '#313131', textBorderWidth: 2 } }); - //markLineData.push({ name: 'min', type: 'min', lineStyle: { color: '#ddd', width: 2, type: 'dotted', opacity: 0.3 }, label: { distance: [10, 0], position: 'start', color: "#eeeeee", textBorderColor: '#313131', textBorderWidth: 2 } }); - //markLineData.push({ name: 'max', type: 'max', lineStyle: { color: '#ddd', width: 2, type: 'dotted', opacity: 0.3 }, label: { distance: [10, 0], position: 'start', color: "#eeeeee", textBorderColor: '#313131', textBorderWidth: 2 } }); - - var xAxis = 0; - for (let index = 0; index < jsondata.banks; index++) { - markLineData.push({ name: "Bank " + index, xAxis: xAxis, lineStyle: { color: colours[index], width: 4, type: 'dashed', opacity: 0.5 }, label: { show: true, distance: [0, 0], formatter: '{b}', color: '#eeeeee', textBorderColor: colours[index], textBorderWidth: 2 } }); - xAxis += jsondata.seriesmodules; - } +function updateChart(jsondata) { + + let labels = []; + let cells = []; + let bank = []; + let voltages = []; + let voltagesmin = []; + let voltagesmax = []; + let tempint = []; + let tempext = []; + let pwm = []; - if (jsondata.voltages) { - //Clone array of voltages - tempArray = []; - for (i = 0; i < jsondata.voltages.length; i++) { - tempArray[i] = jsondata.voltages[i]; - } + let minVoltage = DEFAULT_GRAPH_MIN_VOLTAGE / 1000.0; + let maxVoltage = DEFAULT_GRAPH_MAX_VOLTAGE / 1000.0; - //Split voltages into banks - sorted_voltages = []; - for (i = 0; i < jsondata.banks; i++) { - unsorted = tempArray.splice(0, jsondata.seriesmodules); - sorted_voltages.push(unsorted.sort()); - } + let minExtTemp = 999; + let maxExtTemp = -999; - for (let i = 0; i < jsondata.voltages.length; i++) { - labels.push(bankNumber + "/" + i); + var bankNumber = 0; + let cellsInBank = 0; - // Make different banks different colours (stripes) - var stdcolor = colours[bankNumber]; + // Need one color for each bank, could make it colourful I suppose :-) + const colours = [ + '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', + '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', + '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', + '#55a1ea', '#33628f', '#498FD0', '#6D8EA0', + ] - var color = stdcolor; + const red = '#B44247' - //Highlight lowest cell voltage in this bank - if (jsondata.voltages[i] === sorted_voltages[bankNumber][0]) { - color = lowestCell; - } - //Highlight highest cell voltage in this bank - if (jsondata.voltages[i] === sorted_voltages[bankNumber][jsondata.seriesmodules - 1]) { - color = highestCell; - } - // Red - if (jsondata.bypass[i] === 1) { - color = red; - } + const highestCell = '#8c265d' + const lowestCell = '#b6a016' - var v = (parseFloat(jsondata.voltages[i]) / 1000.0); - voltages.push({ value: v, itemStyle: { color: color } }); + let markLineData = []; - //Auto scale graph is outside of normal bounds - if (v > maxVoltage) { maxVoltage = v; } - if (v < minVoltage) { minVoltage = v; } + markLineData.push({ name: 'avg', type: 'average', lineStyle: { color: '#eee', width: 3, type: 'dotted', opacity: 0.4 }, label: { distance: [10, 0], position: 'start', color: "#eeeeee", textBorderColor: '#313131', textBorderWidth: 2 } }); - if (jsondata.minvoltages) { - voltagesmin.push((parseFloat(jsondata.minvoltages[i]) / 1000.0)); - } - if (jsondata.maxvoltages) { - voltagesmax.push((parseFloat(jsondata.maxvoltages[i]) / 1000.0)); - } + let xAxis = 0; + for (let index = 0; index < jsondata.banks; index++) { + markLineData.push({ name: "Bank " + index, xAxis: xAxis, lineStyle: { color: colours[index], width: 4, type: 'dashed', opacity: 0.5 }, label: { show: true, distance: [0, 0], formatter: '{b}', color: '#eeeeee', textBorderColor: colours[index], textBorderWidth: 2 } }); + xAxis += jsondata.seriesmodules; + } - bank.push(bankNumber); - cells.push(i); + if (jsondata.voltages) { + //Clone array of voltages + let tempArray = []; + for (i = 0; i < jsondata.voltages.length; i++) { + tempArray[i] = jsondata.voltages[i]; + } + //Split voltages into banks + let sorted_voltages = []; + for (i = 0; i < jsondata.banks; i++) { + let unsorted = tempArray.splice(0, jsondata.seriesmodules); + sorted_voltages.push(unsorted.sort()); + } - cellsInBank++; - if (cellsInBank == jsondata.seriesmodules) { - cellsInBank = 0; - bankNumber++; - } + for (let i = 0; i < jsondata.voltages.length; i++) { + labels.push(bankNumber + "/" + i); - color = jsondata.bypasshot[i] == 1 ? red : stdcolor; - tempint.push({ value: jsondata.inttemp[i], itemStyle: { color: color } }); - var exttemp = (jsondata.exttemp[i] == -40 ? 0 : jsondata.exttemp[i]); - tempext.push({ value: exttemp, itemStyle: { color: stdcolor } }); + // Make different banks different colours (stripes) + let stdcolor = colours[bankNumber]; - if (jsondata.exttemp[i] != null) { - if (exttemp > maxExtTemp) { - maxExtTemp = exttemp; - } - if (exttemp < minExtTemp) { - minExtTemp = exttemp; - } - } + let color = stdcolor; + //Highlight lowest cell voltage in this bank + if (jsondata.voltages[i] === sorted_voltages[bankNumber][0]) { + color = lowestCell; + } + //Highlight highest cell voltage in this bank + if (jsondata.voltages[i] === sorted_voltages[bankNumber][jsondata.seriesmodules - 1]) { + color = highestCell; + } + // Red + if (jsondata.bypass[i] === 1) { + color = red; + } + + const v = (parseFloat(jsondata.voltages[i]) / 1000.0); + voltages.push({ value: v, itemStyle: { color: color } }); + + //Auto scale graph is outside of normal bounds + if (v > maxVoltage) { maxVoltage = v; } + if (v < minVoltage) { minVoltage = v; } - pwm.push({ value: jsondata.bypasspwm[i] == 0 ? null : Math.trunc(jsondata.bypasspwm[i] / 255 * 100) }); + if (jsondata.minvoltages) { + voltagesmin.push((parseFloat(jsondata.minvoltages[i]) / 1000.0)); + } + if (jsondata.maxvoltages) { + voltagesmax.push((parseFloat(jsondata.maxvoltages[i]) / 1000.0)); } - } - //Scale down for low voltages - if (minVoltage < 0) { minVoltage = 0; } - - if (jsondata) { - $("#badcrc .v").html(jsondata.badcrc); - $("#ignored .v").html(jsondata.ignored); - $("#sent .v").html(jsondata.sent); - $("#received .v").html(jsondata.received); - $("#roundtrip .v").html(jsondata.roundtrip); - $("#oos .v").html(jsondata.oos); - $("#canfail .v").html(jsondata.can_fail); - $("#canrecerr .v").html(jsondata.can_r_err); - $("#cansent .v").html(jsondata.can_sent); - $("#canrecd .v").html(jsondata.can_rec); - $("#qlen .v").html(jsondata.qlen); - $("#uptime .v").html(secondsToHms(jsondata.uptime)); - if (minExtTemp == 999 || maxExtTemp == -999) { - $("#celltemp .v").html(""); - } else { - $("#celltemp .v").html(minExtTemp + "/" + maxExtTemp + "°C"); + bank.push(bankNumber); + cells.push(i); + + + cellsInBank++; + if (cellsInBank == jsondata.seriesmodules) { + cellsInBank = 0; + bankNumber++; } - if (jsondata.activerules == 0) { - $("#activerules").hide(); - } else { - $("#activerules").html(jsondata.activerules); - $("#activerules").show(400); + color = jsondata.bypasshot[i] == 1 ? red : stdcolor; + tempint.push({ value: jsondata.inttemp[i], itemStyle: { color: color } }); + let exttemp = (jsondata.exttemp[i] == -40 ? 0 : jsondata.exttemp[i]); + tempext.push({ value: exttemp, itemStyle: { color: stdcolor } }); + + if (jsondata.exttemp[i] != null) { + if (exttemp > maxExtTemp) { + maxExtTemp = exttemp; + } + if (exttemp < minExtTemp) { + minExtTemp = exttemp; + } } - if (jsondata.dyncv) { - $("#dyncvolt .v").html(parseFloat(jsondata.dyncv / 10).toFixed(2) + "V"); - } else { $("#dyncvolt .v").html(""); } - if (jsondata.dyncc) { - $("#dynccurr .v").html(parseFloat(jsondata.dyncc / 10).toFixed(2) + "A"); - } else { $("#dynccurr .v").html(""); } + pwm.push({ value: jsondata.bypasspwm[i] == 0 ? null : Math.trunc(jsondata.bypasspwm[i] / 255 * 100) }); + } + } + + //Scale down for low voltages + if (minVoltage < 0) { minVoltage = 0; } + + if (jsondata) { + $("#badcrc .v").html(jsondata.badcrc); + $("#ignored .v").html(jsondata.ignored); + $("#sent .v").html(jsondata.sent); + $("#received .v").html(jsondata.received); + $("#roundtrip .v").html(jsondata.roundtrip); + $("#oos .v").html(jsondata.oos); + $("#canfail .v").html(jsondata.can_fail); + $("#canrecerr .v").html(jsondata.can_r_err); + $("#cansent .v").html(jsondata.can_sent); + $("#canrecd .v").html(jsondata.can_rec); + $("#qlen .v").html(jsondata.qlen); + $("#uptime .v").html(secondsToHms(jsondata.uptime)); + if (minExtTemp == 999 || maxExtTemp == -999) { + $("#celltemp .v").html(""); + } else { + $("#celltemp .v").html(minExtTemp + "/" + maxExtTemp + "°C"); + } + + if (jsondata.activerules == 0) { + $("#activerules").hide(); + } else { + $("#activerules").html(jsondata.activerules); + $("#activerules").show(400); + } + if (jsondata.dyncv) { + $("#dyncvolt .v").html(parseFloat(jsondata.dyncv / 10).toFixed(2) + "V"); + } else { $("#dyncvolt .v").html(""); } + if (jsondata.dyncc) { + $("#dynccurr .v").html(parseFloat(jsondata.dyncc / 10).toFixed(2) + "A"); + } else { $("#dynccurr .v").html(""); } - switch (jsondata.cmode) { - case 0: $("#chgmode .v").html("Standard"); break; - case 1: $("#chgmode .v").html("Absorb " + secondsToHms(jsondata.ctime)); break; - case 2: $("#chgmode .v").html("Float " + secondsToHms(jsondata.ctime)); break; - case 3: $("#chgmode .v").html("Dynamic"); break; - case 4: $("#chgmode .v").html("Stopped"); break; - default: $("#chgmode .v").html("Unknown"); - } + + + switch (jsondata.cmode) { + case 0: $("#chgmode .v").html("Standard"); break; + case 1: $("#chgmode .v").html("Absorb " + secondsToHms(jsondata.ctime)); break; + case 2: $("#chgmode .v").html("Float " + secondsToHms(jsondata.ctime)); break; + case 3: $("#chgmode .v").html("Dynamic"); break; + case 4: $("#chgmode .v").html("Stopped"); break; + default: $("#chgmode .v").html("Unknown"); } + } - if (jsondata.bankv) { - for (var bankNumber = 0; bankNumber < jsondata.bankv.length; bankNumber++) { - $("#voltage" + bankNumber + " .v").html((parseFloat(jsondata.bankv[bankNumber]) / 1000.0).toFixed(2) + "V"); - $("#range" + bankNumber + " .v").html(jsondata.voltrange[bankNumber] + "mV"); - $("#voltage" + bankNumber).removeClass("hide"); - $("#range" + bankNumber).removeClass("hide"); - } + if (jsondata.bankv) { + for (let bankNumber = 0; bankNumber < jsondata.bankv.length; bankNumber++) { + $("#voltage" + bankNumber + " .v").html((parseFloat(jsondata.bankv[bankNumber]) / 1000.0).toFixed(2) + "V"); + $("#range" + bankNumber + " .v").html(jsondata.voltrange[bankNumber] + "mV"); + $("#voltage" + bankNumber).removeClass("hide"); + $("#range" + bankNumber).removeClass("hide"); + } - for (var bankNumber = jsondata.bankv.length; bankNumber < MAXIMUM_NUMBER_OF_BANKS; bankNumber++) { - $("#voltage" + bankNumber).hide().addClass("hide"); - $("#range" + bankNumber).hide().addClass("hide"); - } + for (let bankNumber = jsondata.bankv.length; bankNumber < MAXIMUM_NUMBER_OF_BANKS; bankNumber++) { + $("#voltage" + bankNumber).hide().addClass("hide"); + $("#range" + bankNumber).hide().addClass("hide"); } + } - if (jsondata.current) { - if (jsondata.current[0] == null) { - $("#current .v").html(""); - $("#shuntv .v").html(""); - $("#soc .v").html(""); - $("#power .v").html(""); - $("#amphout .v").html(""); - $("#amphin .v").html(""); - $("#damphout .v").html(""); - $("#damphin .v").html(""); - $("#time100 .v").html(""); - $("#time10 .v").html(""); - $("#time20 .v").html(""); - } else { - var data = jsondata.current[0]; - $("#current .v").html(parseFloat(data.c).toFixed(2) + "A"); - $("#shuntv .v").html(parseFloat(data.v).toFixed(2) + "V"); - $("#soc .v").html(parseFloat(data.soc).toFixed(2) + "%"); - $("#power .v").html(parseFloat(data.p) + "W"); - $("#amphout .v").html((parseFloat(data.mahout) / 1000).toFixed(3)); - $("#amphin .v").html((parseFloat(data.mahin) / 1000).toFixed(3)); - $("#damphout .v").html((parseFloat(data.dmahout) / 1000).toFixed(3)); - $("#damphin .v").html((parseFloat(data.dmahin) / 1000).toFixed(3)); - - if (data.time100 > 0) { - $("#time100 .v").html(secondsToHms(data.time100)); - } else { $("#time100 .v").html("∞"); } - if (data.time20 > 0) { - $("#time20 .v").html(secondsToHms(data.time20)); - } else { $("#time20 .v").html("∞"); } - if (data.time10 > 0) { - $("#time10 .v").html(secondsToHms(data.time10)); - } else { $("#time10 .v").html("∞"); } - } + if (jsondata.current) { + if (jsondata.current[0] == null) { + $("#current .v").html(""); + $("#shuntv .v").html(""); + $("#soc .v").html(""); + $("#power .v").html(""); + $("#amphout .v").html(""); + $("#amphin .v").html(""); + $("#damphout .v").html(""); + $("#damphin .v").html(""); + $("#time100 .v").html(""); + $("#time10 .v").html(""); + $("#time20 .v").html(""); + } else { + var data = jsondata.current[0]; + $("#current .v").html(parseFloat(data.c).toFixed(2) + "A"); + $("#shuntv .v").html(parseFloat(data.v).toFixed(2) + "V"); + $("#soc .v").html(parseFloat(data.soc).toFixed(2) + "%"); + $("#power .v").html(parseFloat(data.p) + "W"); + $("#amphout .v").html((parseFloat(data.mahout) / 1000).toFixed(3)); + $("#amphin .v").html((parseFloat(data.mahin) / 1000).toFixed(3)); + $("#damphout .v").html((parseFloat(data.dmahout) / 1000).toFixed(3)); + $("#damphin .v").html((parseFloat(data.dmahin) / 1000).toFixed(3)); + + if (data.time100 > 0) { + $("#time100 .v").html(secondsToHms(data.time100)); + } else { $("#time100 .v").html("∞"); } + if (data.time20 > 0) { + $("#time20 .v").html(secondsToHms(data.time20)); + } else { $("#time20 .v").html("∞"); } + if (data.time10 > 0) { + $("#time10 .v").html(secondsToHms(data.time10)); + } else { $("#time10 .v").html("∞"); } } + } - //Loop size needs increasing when more warnings are added - if (jsondata.warnings) { - for (let warning = 1; warning <= 9; warning++) { - if (jsondata.warnings.includes(warning)) { - //Once a warning has triggered, hide it from showing in the future - if ($("#warning" + warning).data("notify") == undefined) { - $("#warning" + warning).data("notify", 1); - $.notify($("#warning" + warning).text(), { autoHideDelay: 15000, globalPosition: 'top left', className: 'warn' }); - } + //Loop size needs increasing when more warnings are added + if (jsondata.warnings) { + for (let warning = 1; warning <= 9; warning++) { + if (jsondata.warnings.includes(warning)) { + //Once a warning has triggered, hide it from showing in the future + if ($("#warning" + warning).data("notify") == undefined) { + $("#warning" + warning).data("notify", 1); + $.notify($("#warning" + warning).text(), { autoHideDelay: 15000, globalPosition: 'top left', className: 'warn' }); } } + } - //Allow charge/discharge warnings to reappear - if (jsondata.warnings.includes(7) == false) { - $("#warning7").removeData("notify"); - } - if (jsondata.warnings.includes(8) == false) { - $("#warning8").removeData("notify"); - } + //Allow charge/discharge warnings to reappear + if (jsondata.warnings.includes(7) == false) { + $("#warning7").removeData("notify"); + } + if (jsondata.warnings.includes(8) == false) { + $("#warning8").removeData("notify"); } + } - //Needs increasing when more errors are added - if (jsondata.errors) { - for (let error = 1; error <= 7; error++) { - if (jsondata.errors.includes(error)) { - $("#error" + error).show(); + //Needs increasing when more errors are added + if (jsondata.errors) { + for (let error = 1; error <= 7; error++) { + if (jsondata.errors.includes(error)) { + $("#error" + error).show(); - if (error == INTERNALERRORCODE.ModuleCountMismatch) { - $("#missingmodule1").html(jsondata.modulesfnd); - $("#missingmodule2").html(jsondata.banks * jsondata.seriesmodules); - } - } else { - $("#error" + error).hide(); + if (error == INTERNALERRORCODE.ModuleCountMismatch) { + $("#missingmodule1").html(jsondata.modulesfnd); + $("#missingmodule2").html(jsondata.banks * jsondata.seriesmodules); } + } else { + $("#error" + error).hide(); } } + } - $("#info").show(); - $("#iperror").hide(); + $("#info").show(); + $("#iperror").hide(); - if ($('#modulesPage').is(':visible')) { - //The modules page is visible - var tbody = $("#modulesRows"); + if ($('#modulesPage').is(':visible')) { + //The modules page is visible + var tbody = $("#modulesRows"); - if ($('#modulesRows tr').length != cells.length) { - $("#settingConfig").hide(); + if ($('#modulesRows tr').length != cells.length) { + $("#settingConfig").hide(); - //Add rows if they dont exist (or incorrect amount) - $(tbody).find("tr").remove(); + //Add rows if they dont exist (or incorrect amount) + $(tbody).find("tr").remove(); - $.each(cells, function (index, value) { - $(tbody).append("" - + bank[index] - + "" + value + "" - + "" - + "" - + "") - }); + $.each(cells, function (index, value) { + $(tbody).append("" + + bank[index] + + "" + value + "" + + "" + + "" + + "") + }); + } + + var rows = $(tbody).find("tr"); + + $.each(cells, function (index, value) { + var columns = $(rows[index]).find("td"); + $(columns[2]).html(voltages[index].value.toFixed(3)); + if (voltagesmin.length > 0) { + $(columns[3]).html(voltagesmin[index].toFixed(3)); + } else { + $(columns[3]).html("n/a"); } + if (voltagesmax.length > 0) { + $(columns[4]).html(voltagesmax[index].toFixed(3)); + } else { + $(columns[4]).html("n/a"); + } + $(columns[5]).html(tempint[index].value); + $(columns[6]).html(tempext[index].value); + $(columns[7]).html(pwm[index].value); + }); - var rows = $(tbody).find("tr"); + //As the module page is open, we refresh the last 3 columns using seperate JSON web service to keep the monitor2 + //packets as small as possible + $.getJSON("/api/monitor3", function (jsondata) { + var tbody = $("#modulesRows"); + var rows = $(tbody).find("tr"); $.each(cells, function (index, value) { var columns = $(rows[index]).find("td"); - $(columns[2]).html(voltages[index].value.toFixed(3)); - if (voltagesmin.length > 0) { - $(columns[3]).html(voltagesmin[index].toFixed(3)); - } else { - $(columns[3]).html("n/a"); - } - if (voltagesmax.length > 0) { - $(columns[4]).html(voltagesmax[index].toFixed(3)); - } else { - $(columns[4]).html("n/a"); - } - $(columns[5]).html(tempint[index].value); - $(columns[6]).html(tempext[index].value); - $(columns[7]).html(pwm[index].value); - }); - - //As the module page is open, we refresh the last 3 columns using seperate JSON web service to keep the monitor2 - //packets as small as possible - - $.getJSON("/api/monitor3", function (jsondata) { - var tbody = $("#modulesRows"); - var rows = $(tbody).find("tr"); - $.each(cells, function (index, value) { - var columns = $(rows[index]).find("td"); - $(columns[8]).html(jsondata.badpacket[index]); - $(columns[9]).html(jsondata.pktrecvd[index]); - $(columns[10]).html(jsondata.balcurrent[index]); - }); + $(columns[8]).html(jsondata.badpacket[index]); + $(columns[9]).html(jsondata.pktrecvd[index]); + $(columns[10]).html(jsondata.balcurrent[index]); }); - } + }); + } - if ($('#homePage').is(':visible')) { - if (window.g1 == null && $('#graph1').css('display') != 'none') { - // based on prepared DOM, initialize echarts instance - window.g1 = echarts.init(document.getElementById('graph1')) + if ($('#homePage').is(':visible')) { + if (window.g1 == null && $('#graph1').css('display') != 'none') { + // based on prepared DOM, initialize echarts instance + window.g1 = echarts.init(document.getElementById('graph1')) - // specify chart configuration item and data - var option = { - tooltip: { - show: true, axisPointer: { - type: 'cross', label: { - backgroundColor: '#6a7985' - } + // specify chart configuration item and data + let option = { + tooltip: { + show: true, axisPointer: { + type: 'cross', label: { + backgroundColor: '#6a7985' + } + } + }, + legend: { + show: false + }, + xAxis: [{ + gridIndex: 0, type: 'category', axisLine: { + lineStyle: { + color: '#c1bdbd' + } + } + }, { + gridIndex: 1, type: 'category', axisLine: { + lineStyle: { color: '#c1bdbd' } + } + }], + yAxis: [{ + id: 0, gridIndex: 0, name: 'Volts', type: 'value', min: 2.5, max: 4.5, interval: 0.25, position: 'left', + axisLine: { + lineStyle: { + color: '#c1bdbd' } }, - legend: { - show: false + axisLabel: { + formatter: function (value, index) { + return value.toFixed(2); + } + } + }, + { + id: 1, + gridIndex: 0, name: 'Bypass', type: 'value', min: 0, + max: 100, interval: 10, position: 'right', + axisLabel: { formatter: '{value}%' }, + splitLine: { show: false }, + axisLine: { lineStyle: { type: 'dotted', color: '#c1bdbd' } }, + axisTick: { show: false } + }, + { + id: 2, + gridIndex: 1, + name: 'Temperature', + type: 'value', + interval: 10, + position: 'left', + axisLine: { + lineStyle: { color: '#c1bdbd' } }, - xAxis: [{ - gridIndex: 0, type: 'category', axisLine: { - lineStyle: { - color: '#c1bdbd' + axisLabel: { formatter: '{value}°C' } + }], + series: [ + { + xAxisIndex: 0, + name: 'Voltage', + yAxisIndex: 0, + type: 'bar', + data: [], + markLine: { + silent: true, symbol: 'none', data: markLineData + }, + itemStyle: { color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] }, + label: { + normal: { + show: true, position: 'insideBottom', distance: 10, align: 'left', verticalAlign: 'middle', rotate: 90, formatter: '{c}V', fontSize: 24, color: '#eeeeee', fontFamily: 'Share Tech Mono' } } }, { - gridIndex: 1, type: 'category', axisLine: { - lineStyle: { color: '#c1bdbd' } - } - }], - yAxis: [{ - id: 0, gridIndex: 0, name: 'Volts', type: 'value', min: 2.5, max: 4.5, interval: 0.25, position: 'left', - axisLine: { - lineStyle: { - color: '#c1bdbd' + xAxisIndex: 0, + name: 'Min V', + yAxisIndex: 0, + type: 'line', + data: [], + label: { + normal: { + show: true, position: 'bottom', distance: 5, formatter: '{c}V', fontSize: 14, color: '#eeeeee', fontFamily: 'Share Tech Mono' } }, - axisLabel: { - formatter: function (value, index) { - return value.toFixed(2); + symbolSize: 16, + symbol: ['circle'], + itemStyle: { + normal: { + color: "#c1bdbd", lineStyle: { color: 'transparent' } } } - }, - { - id: 1, - gridIndex: 0, name: 'Bypass', type: 'value', min: 0, - max: 100, interval: 10, position: 'right', - axisLabel: { formatter: '{value}%' }, - splitLine: { show: false }, - axisLine: { lineStyle: { type: 'dotted', color: '#c1bdbd' } }, - axisTick: { show: false } - }, - { - id: 2, - gridIndex: 1, - name: 'Temperature', - type: 'value', - interval: 10, - position: 'left', - axisLine: { - lineStyle: { color: '#c1bdbd' } - }, - axisLabel: { formatter: '{value}°C' } - }], - series: [ - { - xAxisIndex: 0, - name: 'Voltage', - yAxisIndex: 0, - type: 'bar', - data: [], - markLine: { - silent: true, symbol: 'none', data: markLineData - }, - itemStyle: { color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] }, - label: { - normal: { - show: true, position: 'insideBottom', distance: 10, align: 'left', verticalAlign: 'middle', rotate: 90, formatter: '{c}V', fontSize: 24, color: '#eeeeee', fontFamily: 'Share Tech Mono' - } - } - }, { - xAxisIndex: 0, - name: 'Min V', - yAxisIndex: 0, - type: 'line', - data: [], - label: { - normal: { - show: true, position: 'bottom', distance: 5, formatter: '{c}V', fontSize: 14, color: '#eeeeee', fontFamily: 'Share Tech Mono' - } - }, - symbolSize: 16, - symbol: ['circle'], - itemStyle: { - normal: { - color: "#c1bdbd", lineStyle: { color: 'transparent' } - } + } + , { + xAxisIndex: 0, + name: 'Max V', + yAxisIndex: 0, + type: 'line', + data: [], + label: { + normal: { + show: true, position: 'top', distance: 5, formatter: '{c}V', fontSize: 14, color: '#c1bdbd', fontFamily: 'Share Tech Mono' } - } - , { - xAxisIndex: 0, - name: 'Max V', - yAxisIndex: 0, - type: 'line', - data: [], - label: { - normal: { - show: true, position: 'top', distance: 5, formatter: '{c}V', fontSize: 14, color: '#c1bdbd', fontFamily: 'Share Tech Mono' - } - }, - symbolSize: 16, - symbol: ['arrow'], - itemStyle: { - normal: { - color: "#c1bdbd", lineStyle: { color: 'transparent' } - } + }, + symbolSize: 16, + symbol: ['arrow'], + itemStyle: { + normal: { + color: "#c1bdbd", lineStyle: { color: 'transparent' } } } + } - , { - xAxisIndex: 0, - name: 'Bypass', - yAxisIndex: 1, - type: 'line', - data: [], - label: { - normal: { - show: true, position: 'right', distance: 5, formatter: '{c}%', fontSize: 14, color: '#f0e400', fontFamily: 'Share Tech Mono' - } - }, - symbolSize: 16, - symbol: ['square'], - itemStyle: { normal: { color: "#f0e400", lineStyle: { color: 'transparent' } } } - } + , { + xAxisIndex: 0, + name: 'Bypass', + yAxisIndex: 1, + type: 'line', + data: [], + label: { + normal: { + show: true, position: 'right', distance: 5, formatter: '{c}%', fontSize: 14, color: '#f0e400', fontFamily: 'Share Tech Mono' + } + }, + symbolSize: 16, + symbol: ['square'], + itemStyle: { normal: { color: "#f0e400", lineStyle: { color: 'transparent' } } } + } - , { - xAxisIndex: 1, - yAxisIndex: 2, - name: 'BypassTemperature', - type: 'bar', - data: [], - itemStyle: { - color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] - }, - label: { - normal: { - show: true, position: 'insideBottom', distance: 8, - align: 'left', verticalAlign: 'middle', - rotate: 90, formatter: '{c}°C', fontSize: 20, color: '#eeeeee', fontFamily: 'Share Tech Mono' - } + , { + xAxisIndex: 1, + yAxisIndex: 2, + name: 'BypassTemperature', + type: 'bar', + data: [], + itemStyle: { + color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] + }, + label: { + normal: { + show: true, position: 'insideBottom', distance: 8, + align: 'left', verticalAlign: 'middle', + rotate: 90, formatter: '{c}°C', fontSize: 20, color: '#eeeeee', fontFamily: 'Share Tech Mono' } } + } - , { - xAxisIndex: 1, - yAxisIndex: 2, - name: 'CellTemperature', - type: 'bar', - data: [], - itemStyle: { - color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] - }, - label: { - normal: { - show: true, position: 'insideBottom', distance: 8, - align: 'left', verticalAlign: 'middle', rotate: 90, - formatter: '{c}°C', fontSize: 20, color: '#eeeeee', fontFamily: 'Share Tech Mono' - } + , { + xAxisIndex: 1, + yAxisIndex: 2, + name: 'CellTemperature', + type: 'bar', + data: [], + itemStyle: { + color: '#55a1ea', barBorderRadius: [8, 8, 0, 0] + }, + label: { + normal: { + show: true, position: 'insideBottom', distance: 8, + align: 'left', verticalAlign: 'middle', rotate: 90, + formatter: '{c}°C', fontSize: 20, color: '#eeeeee', fontFamily: 'Share Tech Mono' } - } - ], - grid: [ - { - containLabel: false, left: '4%', right: '4%', bottom: '30%' - }, { - containLabel: false, left: '4%', right: '4%', top: '76%' - }] - }; + } + ], + grid: [ + { + containLabel: false, left: '4%', right: '4%', bottom: '30%' + + }, { + containLabel: false, left: '4%', right: '4%', top: '76%' + }] + }; - // use configuration item and data specified to show chart - g1.setOption(option); + if (jsondata.voltages.length > 24) { + // When lots of cell data is on screen, hide the labels to improve visability + for (const element of option.series) { + element.label.normal.show = false; + } } - if (window.g2 == null && $('#graph2').css('display') != 'none' && window.Graph3DAvailable === true) { - window.g2 = echarts.init(document.getElementById('graph2')); - - var Option3dBar = { - tooltip: {}, - visualMap: { max: 4, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'] } }, - xAxis3D: { type: 'category', data: [], name: 'Cell', nameTextStyle: { color: '#ffffff' } }, - yAxis3D: { type: 'category', data: [], name: 'Bank', nameTextStyle: { color: '#ffffff' } }, - zAxis3D: { type: 'value', name: 'Voltage', nameTextStyle: { color: '#ffffff' } }, - grid3D: { - boxWidth: 200, - boxDepth: 80, - viewControl: { - // projection: 'orthographic' - }, - light: { - main: { - intensity: 1.2, - shadow: true - }, - ambient: { - intensity: 0.3 - } - } + // use configuration item and data specified to show chart + g1.setOption(option); + + } + + if (window.g2 == null && $('#graph2').css('display') != 'none' && window.Graph3DAvailable === true) { + window.g2 = echarts.init(document.getElementById('graph2')); + + var Option3dBar = { + tooltip: {}, + visualMap: { max: 4, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'] } }, + xAxis3D: { type: 'category', data: [], name: 'Cell', nameTextStyle: { color: '#ffffff' } }, + yAxis3D: { type: 'category', data: [], name: 'Bank', nameTextStyle: { color: '#ffffff' } }, + zAxis3D: { type: 'value', name: 'Voltage', nameTextStyle: { color: '#ffffff' } }, + grid3D: { + boxWidth: 200, + boxDepth: 80, + viewControl: { + // projection: 'orthographic' }, - series: [{ - type: 'bar3D', - data: [], - shading: 'lambert', - label: { textStyle: { fontSize: 16, borderWidth: 1, color: '#ffffff' } }, - - emphasis: { - label: { - textStyle: { - fontSize: 16, - color: '#aaa' - } - }, - itemStyle: { color: '#fff' } + light: { + main: { + intensity: 1.2, + shadow: true + }, + ambient: { + intensity: 0.3 } - }] - }; + } + }, + series: [{ + type: 'bar3D', + data: [], + shading: 'lambert', + label: { textStyle: { fontSize: 16, borderWidth: 1, color: '#ffffff' } }, + + emphasis: { + label: { + textStyle: { + fontSize: 16, + color: '#aaa' + } + }, + itemStyle: { color: '#fff' } + } + }] + }; - g2.setOption(Option3dBar); - } + g2.setOption(Option3dBar); + } - if (window.g1 != null && $('#graph1').css('display') != 'none') { - g1.setOption({ - markLine: { data: markLineData }, - xAxis: { data: labels }, - yAxis: [{ gridIndex: 0, min: minVoltage, max: maxVoltage }] - , series: [{ name: 'Voltage', data: voltages } - , { name: 'Min V', data: voltagesmin } - , { name: 'Max V', data: voltagesmax } - , { name: 'Bypass', data: pwm } - , { name: 'BypassTemperature', data: tempint } - , { name: 'CellTemperature', data: tempext }] - }); - } + if (window.g1 != null && $('#graph1').css('display') != 'none') { + g1.setOption({ + markLine: { data: markLineData }, + xAxis: { data: labels }, + yAxis: [{ gridIndex: 0, min: minVoltage, max: maxVoltage }] + , series: [{ name: 'Voltage', data: voltages } + , { name: 'Min V', data: voltagesmin } + , { name: 'Max V', data: voltagesmax } + , { name: 'Bypass', data: pwm } + , { name: 'BypassTemperature', data: tempint } + , { name: 'CellTemperature', data: tempext }] + }); + } - if (window.g2 != null && $('#graph2').css('display') != 'none') { - //Format the data to show as 3D Bar chart - var cells3d = []; - var banks3d = []; + if (window.g2 != null && $('#graph2').css('display') != 'none') { + //Format the data to show as 3D Bar chart + var cells3d = []; + var banks3d = []; - for (var seriesmodules = 0; seriesmodules < jsondata.seriesmodules; seriesmodules++) { - cells3d.push({ value: 'Cell ' + seriesmodules, textStyle: { color: '#ffffff' } }); - } + for (let seriesmodules = 0; seriesmodules < jsondata.seriesmodules; seriesmodules++) { + cells3d.push({ value: 'Cell ' + seriesmodules, textStyle: { color: '#ffffff' } }); + } - var data3d = []; - var cell = 0; - for (var bankNumber = 0; bankNumber < jsondata.banks; bankNumber++) { - banks3d.push({ value: 'Bank ' + bankNumber, textStyle: { color: '#ffffff' } }); - //Build up 3d array for cell data - for (var seriesmodules = 0; seriesmodules < jsondata.seriesmodules; seriesmodules++) { - data3d.push({ value: [seriesmodules, bankNumber, voltages[cell].value], itemStyle: voltages[cell].itemStyle }); - cell++; - } + var data3d = []; + var cell = 0; + for (let bankNumber = 0; bankNumber < jsondata.banks; bankNumber++) { + banks3d.push({ value: 'Bank ' + bankNumber, textStyle: { color: '#ffffff' } }); + //Build up 3d array for cell data + for (let seriesmodules = 0; seriesmodules < jsondata.seriesmodules; seriesmodules++) { + data3d.push({ value: [seriesmodules, bankNumber, voltages[cell].value], itemStyle: voltages[cell].itemStyle }); + cell++; } + } - g2.setOption({ - xAxis3D: { data: cells3d }, - yAxis3D: { data: banks3d }, - zAxis3D: { min: minVoltage, max: maxVoltage }, - series: [{ data: data3d }] + g2.setOption({ + xAxis3D: { data: cells3d }, + yAxis3D: { data: banks3d }, + zAxis3D: { min: minVoltage, max: maxVoltage }, + series: [{ data: data3d }] - , grid3D: { - boxWidth: 20 * jsondata.seriesmodules > 200 ? 200 : 20 * jsondata.seriesmodules, - boxDepth: 20 * jsondata.banks > 100 ? 100 : 20 * jsondata.banks - } + , grid3D: { + boxWidth: 20 * jsondata.seriesmodules > 200 ? 200 : 20 * jsondata.seriesmodules, + boxDepth: 20 * jsondata.banks > 100 ? 100 : 20 * jsondata.banks } - - ); } - }//end homepage visible + ); + } - $("#homePage").css({ opacity: 1.0 }); - $("#loading").hide(); - //Call again in a few seconds - setTimeout(queryBMS, 3500); + }//end homepage visible - loadVisibleTileData(); + $("#homePage").css({ opacity: 1.0 }); + $("#loading").hide(); + //Call again in a few seconds + setTimeout(queryBMS, 3500); - }).fail(function (jqXHR, textStatus, errorThrown) { + loadVisibleTileData(); +} + +function queryBMS() { + $.getJSON("/api/monitor2", updateChart).fail(function (jqXHR, textStatus, errorThrown) { if (jqXHR.status == 400 && jqXHR.responseJSON.error === "Invalid cookie") { if ($("#warningXSS").data("notify") == undefined) { @@ -1320,7 +1318,7 @@ $(function () { ); $(".rule").on("change", function () { - var origv = $(this).attr("data-origv") + let origv = $(this).attr("data-origv") if (origv !== this.value) { $(this).addClass("modified"); } else { @@ -1329,10 +1327,10 @@ $(function () { }); $("#labelMaxModules").text(MAXIMUM_NUMBER_OF_SERIES_MODULES); - for (var n = 1; n <= MAXIMUM_NUMBER_OF_SERIES_MODULES; n++) { + for (let n = 1; n <= MAXIMUM_NUMBER_OF_SERIES_MODULES; n++) { $("#totalSeriesModules").append('') } - for (var n = MAXIMUM_NUMBER_OF_BANKS - 1; n >= 0; n--) { + for (let n = MAXIMUM_NUMBER_OF_BANKS - 1; n >= 0; n--) { $("#totalBanks").prepend('') $("#info").prepend('
Range ' + n + ':
'); $("#info").prepend('
Voltage ' + n + ':
'); @@ -1356,10 +1354,10 @@ $(function () { }); $('#CalculateCalibration').click(function () { - var currentReading = parseFloat($("#modulesRows > tr.selected > td:nth-child(3)").text()); - var currentCalib = parseFloat($("#Calib").val()); - var actualV = parseFloat($("#ActualVoltage").val()); - var result = (currentCalib / currentReading) * actualV; + const currentReading = parseFloat($("#modulesRows > tr.selected > td:nth-child(3)").text()); + const currentCalib = parseFloat($("#Calib").val()); + const actualV = parseFloat($("#ActualVoltage").val()); + let result = (currentCalib / currentReading) * actualV; $("#Calib").val(result.toFixed(4)); return true; }); @@ -1613,17 +1611,17 @@ $(function () { $("#networkForm").show(); - + $("#rssi_now").val(data.wifi.rssi); $("#bssid").val(data.wifi.bssid); $("#ssid").val(data.wifi.ssid); - + $("#rssi_low").val(data.wifi.rssi_low); $("#sta_start").val(data.wifi.sta_start); $("#sta_connected").val(data.wifi.sta_connected); $("#sta_disconnected").val(data.wifi.sta_disconnected); $("#sta_lost_ip").val(data.wifi.sta_lost_ip); - $("#sta_got_ip").val(data.wifi.sta_got_ip); + $("#sta_got_ip").val(data.wifi.sta_got_ip); }).fail(function () { $.notify("Request failed", { autoHide: true, globalPosition: 'top right', className: 'error' }); } ); @@ -1766,8 +1764,8 @@ $(function () { $("#influxOrgId").val(data.influxdb.orgid); $("#influxFreq").val(data.influxdb.frequency); - $("#haUrl").val(window.location.origin+"/ha"); - $("#haAPI").val(data.ha.api); + $("#haUrl").val(window.location.origin + "/ha"); + $("#haAPI").val(data.ha.api); $("#haForm").show(); $("#mqttForm").show(); @@ -1998,11 +1996,18 @@ $(function () { $.getJSON("/api/chargeconfig", function (data) { - $("#canbusprotocol").val(data.chargeconfig.canbusprotocol); + $("#protocol").val(data.chargeconfig.protocol); $("#canbusinverter").val(data.chargeconfig.canbusinverter); $("#canbusbaud").val(data.chargeconfig.canbusbaud); $("#nominalbatcap").val(data.chargeconfig.nominalbatcap); + $("#expected_cycles").val(data.chargeconfig.expectedlifetime_cycles); + $("#eol_capacity").val(data.chargeconfig.eol_capacity); + $("#total_ah_charge").val(Math.trunc(data.chargeconfig.total_mah_charge / 1000)); + $("#total_ah_discharge").val(Math.trunc(data.chargeconfig.total_mah_discharge / 1000)); + $("#estimate_bat_cycle").val(data.chargeconfig.estimatebatterycycle); + $("#stateofhealth").val((data.chargeconfig.stateofhealth).toFixed(2)); + $("#chargevolt").val((data.chargeconfig.chargevolt / 10.0).toFixed(1)); $("#chargecurrent").val((data.chargeconfig.chargecurrent / 10.0).toFixed(1)); $("#dischargecurrent").val((data.chargeconfig.dischargecurrent / 10.0).toFixed(1)); @@ -2129,7 +2134,7 @@ $(function () { }, }); }); - + $("#haForm").unbind('submit').submit(function (e) { e.preventDefault(); @@ -2146,7 +2151,7 @@ $(function () { }, }); }); - + $("#rulesForm").unbind('submit').submit(function (e) { e.preventDefault(); @@ -2336,13 +2341,11 @@ $(function () { DrawChargingGraph(); }); - //On page ready queryBMS(); - //Automatically open correct sub-page based on hash - var hash = $(location).attr('hash'); + let hash = $(location).attr('hash'); switch (hash) { case "#tiles": case "#modules": diff --git a/STM32All-In-One/include/EmbeddedFiles_Defines.h b/STM32All-In-One/include/EmbeddedFiles_Defines.h index ffcc88a2..6e9e29b1 100644 --- a/STM32All-In-One/include/EmbeddedFiles_Defines.h +++ b/STM32All-In-One/include/EmbeddedFiles_Defines.h @@ -5,12 +5,12 @@ #ifndef EmbeddedFiles_Defines_H #define EmbeddedFiles_Defines_H -static const char GIT_VERSION[] = "256670999f7ae2237b8ed7a913f5b9631bb5f405"; +static const char GIT_VERSION[] = "6e62d616aa642884cb6458c0baf8057ccf2c11d6"; -static const uint16_t GIT_VERSION_B1 = 0x1bb5; +static const uint16_t GIT_VERSION_B1 = 0xcf2c; -static const uint16_t GIT_VERSION_B2 = 0xf405; +static const uint16_t GIT_VERSION_B2 = 0x11d6; -static const char COMPILE_DATE_TIME[] = "2023-11-13T11:48:01.770Z"; +static const char COMPILE_DATE_TIME[] = "2024-08-02T12:18:35.609Z"; #endif \ No newline at end of file diff --git a/STM32All-In-One/platformio.ini b/STM32All-In-One/platformio.ini index a369aec8..3a3efa28 100644 --- a/STM32All-In-One/platformio.ini +++ b/STM32All-In-One/platformio.ini @@ -12,7 +12,7 @@ default_envs = V490_AUTOBAUD_VREF4096, V490_AUTOBAUD_VREF4500 [env] -platform = platformio/ststm32@^17.0.0 +platform = platformio/ststm32@^17.5.0 board = genericSTM32F030K6T6 board_build.core = stm32 framework = arduino @@ -29,7 +29,7 @@ lib_deps = monitor_port=COM3 monitor_speed=115200 -upload_port = COM4 +upload_port = COM5 debug_tool = stlink upload_protocol = stlink ;upload_protocol = serial