From f881bc409316fe729c897b6c2199e4c206121782 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:59:21 +0100 Subject: [PATCH 01/27] Basic home assistant API integration --- ESPController/include/defines.h | 2 + .../include/webserver_json_requests.h | 1 + ESPController/src/main.cpp | 13 +++- ESPController/src/settings.cpp | 11 ++- ESPController/src/webserver.cpp | 9 ++- ESPController/src/webserver_helper_funcs.cpp | 28 ++++---- ESPController/src/webserver_json_requests.cpp | 69 ++++++++++++++++++- 7 files changed, 115 insertions(+), 18 deletions(-) diff --git a/ESPController/include/defines.h b/ESPController/include/defines.h index 2077a6f1..a12436f0 100644 --- a/ESPController/include/defines.h +++ b/ESPController/include/defines.h @@ -245,6 +245,8 @@ struct diybms_eeprom_settings // Holds a bit pattern indicating which "tiles" are visible on the web gui uint16_t tileconfig[5]; + + char homeassist_apikey[24+1]; }; typedef union diff --git a/ESPController/include/webserver_json_requests.h b/ESPController/include/webserver_json_requests.h index c209d101..5b3747e3 100644 --- a/ESPController/include/webserver_json_requests.h +++ b/ESPController/include/webserver_json_requests.h @@ -24,6 +24,7 @@ esp_err_t api_handler(httpd_req_t *req); esp_err_t content_handler_downloadfile(httpd_req_t *req); +esp_err_t ha_handler(httpd_req_t *req); esp_err_t SendFileInChunks(httpd_req_t *req, FS &filesystem, const char *filename); int fileSystemListDirectory(char *buffer, size_t bufferLen, fs::FS &fs, const char *dirname, uint8_t levels); diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index cab2f00d..e982ef9e 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -82,7 +82,7 @@ extern "C" #include "history.h" CurrentMonitorINA229 currentmon_internal = CurrentMonitorINA229(); - +extern void randomCharacters(char *value, int length); const uart_port_t rs485_uart_num = UART_NUM_1; const char *wificonfigfilename = "/diybms/wifi.json"; @@ -3730,6 +3730,15 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", LoadConfiguration(&mysettings); ValidateConfiguration(&mysettings); + if (strlen(mysettings.homeassist_apikey) == 0) + { + // Generate new key + memset(&mysettings.homeassist_apikey, 0, sizeof(mysettings.homeassist_apikey)); + randomCharacters(mysettings.homeassist_apikey, sizeof(mysettings.homeassist_apikey) - 1); + saveConfiguration(); + } + ESP_LOGI(TAG, "homeassist_apikey=%s", mysettings.homeassist_apikey); + if (!EepromConfigValid) { // We don't have a valid WIFI configuration, so force terminal based setup @@ -3857,7 +3866,7 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", } /// @brief Convert an ESP32 core dump stored in FLASH to a JSON object fragment -/// @param doc +/// @param doc void ESPCoreDumpToJSON(JsonObject &doc) { if (esp_core_dump_image_check() == ESP_OK) diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 482e7f5f..a5d4829f 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -86,6 +86,7 @@ static const char absorptiontimer_JSONKEY[] = "absorptiontimer"; static const char floatvoltage_JSONKEY[] = "floatvoltage"; static const char floatvoltagetimer_JSONKEY[] = "floatvoltagetimer"; static const char stateofchargeresumevalue_JSONKEY[] = "stateofchargeresumevalue"; +static const char homeassist_apikey_JSONKEY[] = "homeassistapikey"; /* NVS KEYS THESE STRINGS ARE USED TO HOLD THE PARAMETER IN NVS FLASH, MAXIMUM LENGTH OF 16 CHARACTERS @@ -174,6 +175,7 @@ static const char absorptiontimer_NVSKEY[] = "absorptimer"; static const char floatvoltage_NVSKEY[] = "floatV"; static const char floatvoltagetimer_NVSKEY[] = "floatVtimer"; static const char stateofchargeresumevalue_NVSKEY[] = "socresume"; +static const char homeassist_apikey_NVSKEY[] = "haapikey"; #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); @@ -336,7 +338,6 @@ void SaveConfiguration(diybms_eeprom_settings *settings) } else { - // Save settings MACRO_NVSWRITE(totalNumberOfBanks) MACRO_NVSWRITE(totalNumberOfSeriesModules) @@ -430,6 +431,8 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE(floatvoltagetimer); MACRO_NVSWRITE(stateofchargeresumevalue); + MACRO_NVSWRITESTRING(homeassist_apikey); + ESP_ERROR_CHECK(nvs_commit(nvs_handle)); nvs_close(nvs_handle); } @@ -554,6 +557,8 @@ void LoadConfiguration(diybms_eeprom_settings *settings) MACRO_NVSREAD(floatvoltagetimer); MACRO_NVSREAD_UINT8(stateofchargeresumevalue); + MACRO_NVSREADSTRING(homeassist_apikey); + nvs_close(nvs_handle); } @@ -994,6 +999,8 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[rs485stopbits_JSONKEY] = settings->rs485stopbits; root[language_JSONKEY] = settings->language; + root[homeassist_apikey_JSONKEY]=settings->homeassist_apikey; + JsonObject mqtt = root.createNestedObject("mqtt"); mqtt[mqtt_enabled_JSONKEY] = settings->mqtt_enabled; mqtt[mqtt_uri_JSONKEY] = settings->mqtt_uri; @@ -1168,6 +1175,8 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) settings->current_value1 = root[current_value1_JSONKEY]; settings->current_value2 = root[current_value2_JSONKEY]; + strncpy(settings->homeassist_apikey, root[homeassist_apikey_JSONKEY].as().c_str(), sizeof(settings->homeassist_apikey)); + JsonObject mqtt = root["mqtt"]; if (!mqtt.isNull()) { diff --git a/ESPController/src/webserver.cpp b/ESPController/src/webserver.cpp index 743acf47..d31ec5d0 100644 --- a/ESPController/src/webserver.cpp +++ b/ESPController/src/webserver.cpp @@ -536,6 +536,8 @@ static const httpd_uri_t uri_static_content_get = {.uri = "*", .method = HTTP_GE static const httpd_uri_t uri_ota_post = {.uri = "/ota", .method = HTTP_POST, .handler = ota_post_handler, .user_ctx = NULL}; static const httpd_uri_t uri_uploadfile_post = {.uri = "/uploadfile", .method = HTTP_POST, .handler = uploadfile_post_handler, .user_ctx = NULL}; +static const httpd_uri_t uri_homeassist_get = {.uri = "/ha", .method = HTTP_GET, .handler = ha_handler, .user_ctx = NULL}; + void resetModuleMinMaxVoltage(uint8_t m) { cmi[m].voltagemVMin = 9999; @@ -618,10 +620,10 @@ httpd_handle_t start_webserver(void) /* Generate default configuration */ httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.max_uri_handlers = 10; + config.max_uri_handlers = 11; config.max_open_sockets = 8; config.max_resp_headers = 16; - config.stack_size = 6300; + config.stack_size = 6250; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true; @@ -647,6 +649,9 @@ httpd_handle_t start_webserver(void) ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_ota_post)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_uploadfile_post)); + ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_homeassist_get)); + + #ifdef USE_WEBSOCKET_DEBUG_LOG // Websocket ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_ws_get)); diff --git a/ESPController/src/webserver_helper_funcs.cpp b/ESPController/src/webserver_helper_funcs.cpp index 8e8b6185..35c76963 100644 --- a/ESPController/src/webserver_helper_funcs.cpp +++ b/ESPController/src/webserver_helper_funcs.cpp @@ -12,6 +12,20 @@ void setCookie(httpd_req_t *req) httpd_resp_set_hdr(req, "Set-Cookie", cookie); } +void randomCharacters(char* value, int length) { + // Pick random characters from this string (we could just use ASCII offset instead of this) + // but this also avoids javascript escape characters like backslash and cookie escape chars like ; and % + char alphabet[] = "!$*#@ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"; + + // Leave NULL terminator on char array + for (uint8_t x = 0; x < length; x++) + { + // Random number between 0 and array length (minus null char) + value[x] = alphabet[random(0, sizeof(alphabet) - 2)]; + } + +} + void setCookieValue() { // We generate a unique number which is used in all following JSON requests @@ -21,18 +35,8 @@ void setCookieValue() // ESP32 has inbuilt random number generator // https://techtutorialsx.com/2017/12/22/esp32-arduino-random-number-generation/ - // Pick random characters from this string (we could just use ASCII offset instead of this) - // but this also avoids javascript escape characters like backslash and cookie escape chars like ; and % - char alphabet[] = "!$*#@ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"; - - memset(CookieValue, 0, sizeof(CookieValue)); - - // Leave NULL terminator on char array - for (uint8_t x = 0; x < sizeof(CookieValue) - 1; x++) - { - // Random number between 0 and array length (minus null char) - CookieValue[x] = alphabet[random(0, sizeof(alphabet) - 2)]; - } + memset(&CookieValue, 0, sizeof(CookieValue)); + randomCharacters(CookieValue,sizeof(CookieValue)-1); // Generate the full cookie string, as a HTTPONLY cookie, valid for this session only snprintf(cookie, sizeof(cookie), "DIYBMS=%s; path=/; HttpOnly; SameSite=Strict", CookieValue); diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index 333c32cc..213e87dd 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -338,7 +338,7 @@ esp_err_t content_handler_coredumpdownloadfile(httpd_req_t *req) ESP_ERROR_CHECK_WITHOUT_ABORT(httpd_resp_send_chunk(req, httpbuf, 256)); } - //After download, erase the core dump from flash + // After download, erase the core dump from flash ESP_ERROR_CHECK_WITHOUT_ABORT(esp_core_dump_image_erase()); // Indicate last chunk (zero byte length) @@ -1249,6 +1249,73 @@ esp_err_t content_handler_monitor2(httpd_req_t *req) return httpd_resp_send_chunk(req, httpbuf, 0); } +/// @brief +/// @param req Incoming HTTPD request handle +/// @return Error/success status +esp_err_t ha_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + setNoStoreCacheControl(req); + + ESP_LOGI(TAG, "ha_handler"); + + char buffer[128]; + esp_err_t result = httpd_req_get_hdr_value_str(req, "ApiKey", buffer, sizeof(buffer)); + + if (result != ESP_OK) + { + ESP_LOGE(TAG, "Missing header ApiKey"); + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); + } + + if (strncmp(mysettings.homeassist_apikey, buffer, strlen(mysettings.homeassist_apikey)) != 0) + { + ESP_LOGE(TAG, "Unauthorized ApiKey=%s", buffer); + return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, NULL); + } + + int bufferused = 0; + const char *nullstring = "null"; + + // Output the first batch of settings/parameters/values + bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, + R"({"activerules":%u,"chgmode":%u,"lowbankv":%u,"highbankv":%u,"lowcellv":%u,"highcellv":%u,"highextt":%i,"highintt":%i)", + rules.active_rule_count, + (unsigned int)rules.getChargingMode(), + rules.lowestBankVoltage, + rules.highestBankVoltage, + rules.lowestCellVoltage, + rules.highestCellVoltage, + rules.highestExternalTemp, + rules.highestInternalTemp); + + if (mysettings.currentMonitoringEnabled && currentMonitor.validReadings) + { + bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, + R"(,"c":%.4f,"v":%.4f,"pwr":%.2f,"soc":%.2f)", + currentMonitor.modbus.current, + currentMonitor.modbus.voltage, + currentMonitor.modbus.power, + currentMonitor.stateofcharge); + } + + if (mysettings.canbusprotocol != CanBusProtocolEmulation::CANBUS_DISABLED && mysettings.dynamiccharge) + { + bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, + R"(,"dyncv":%u,"dyncc":%u)", + rules.DynamicChargeVoltage(), + rules.DynamicChargeCurrent()); + } + + bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, + R"(,"chgallow":%u,"dischgallow":%u)", + rules.IsChargeAllowed(&mysettings) ? 1 : 0, + rules.IsDischargeAllowed(&mysettings) ? 1 : 0); + + bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, "}"); + return httpd_resp_send(req, httpbuf, bufferused); +} + esp_err_t api_handler(httpd_req_t *req) { if (!validateXSS(req)) From 14863e042b1528e7152ad94e80ba4375461e8ebb Mon Sep 17 00:00:00 2001 From: Patrick Prasse Date: Fri, 22 Sep 2023 11:22:26 +0200 Subject: [PATCH 02/27] PYLONTECH ForceH* canbus support --- ESPController/include/Rules.h | 7 +- ESPController/include/defines.h | 5 +- ESPController/include/pylonforce_canbus.h | 26 + ESPController/src/HAL_ESP32.cpp | 19 +- ESPController/src/Rules.cpp | 4 + ESPController/src/main.cpp | 52 +- ESPController/src/pylonforce_canbus.cpp | 665 ++++++++++++++++++++++ ESPController/src/settings.cpp | 1 + ESPController/src/victron_canbus.cpp | 1 + ESPController/web_src/default.htm | 1 + 10 files changed, 754 insertions(+), 27 deletions(-) create mode 100644 ESPController/include/pylonforce_canbus.h create mode 100644 ESPController/src/pylonforce_canbus.cpp diff --git a/ESPController/include/Rules.h b/ESPController/include/Rules.h index f083f082..89800d89 100644 --- a/ESPController/include/Rules.h +++ b/ESPController/include/Rules.h @@ -107,10 +107,13 @@ class Rules // Number of modules who have reported zero volts (bad!) uint8_t zeroVoltageModuleCount; - // Highest pack voltage (millivolts) + // Highest pack voltage (millivolts) & address uint32_t highestBankVoltage; - // Lowest pack voltage (millivolts) + uint8_t address_highestBankVoltage; + + // Lowest pack voltage (millivolts) & address uint32_t lowestBankVoltage; + uint8_t address_lowestBankVoltage; // Highest cell voltage in the whole system (millivolts) uint16_t highestCellVoltage; diff --git a/ESPController/include/defines.h b/ESPController/include/defines.h index 2077a6f1..9aded64f 100644 --- a/ESPController/include/defines.h +++ b/ESPController/include/defines.h @@ -100,7 +100,8 @@ enum CanBusProtocolEmulation : uint8_t { CANBUS_DISABLED = 0x00, CANBUS_VICTRON = 0x01, - CANBUS_PYLONTECH = 0x02 + CANBUS_PYLONTECH = 0x02, + CANBUS_PYLONFORCEH2 = 0x03 }; enum CurrentMonitorDevice : uint8_t @@ -245,6 +246,8 @@ struct diybms_eeprom_settings // Holds a bit pattern indicating which "tiles" are visible on the web gui uint16_t tileconfig[5]; + + uint8_t canbus_equipment_addr; // battery index on the same canbus for PYLONFORCE, 0 - 15, default 0 }; typedef union diff --git a/ESPController/include/pylonforce_canbus.h b/ESPController/include/pylonforce_canbus.h new file mode 100644 index 00000000..c43f90aa --- /dev/null +++ b/ESPController/include/pylonforce_canbus.h @@ -0,0 +1,26 @@ +#ifndef DIYBMS_PYLONFORCE_CANBUS_H_ +#define DIYBMS_PYLONFORCE_CANBUS_H_ + +#include "defines.h" +#include "Rules.h" +#include + + +void pylonforce_handle_rx(twai_message_t *); +void pylonforce_handle_tx(); + + +extern uint8_t TotalNumberOfCells(); +extern Rules rules; +extern currentmonitoring_struct currentMonitor; +extern diybms_eeprom_settings mysettings; +extern std::string hostname; +extern ControllerState _controller_state; +extern uint32_t canbus_messages_failed_sent; +extern uint32_t canbus_messages_sent; +extern uint32_t canbus_messages_received; +extern bool wifi_isconnected; + +extern void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, const uint8_t length); + +#endif \ No newline at end of file diff --git a/ESPController/src/HAL_ESP32.cpp b/ESPController/src/HAL_ESP32.cpp index 1601c1c7..03a9d975 100644 --- a/ESPController/src/HAL_ESP32.cpp +++ b/ESPController/src/HAL_ESP32.cpp @@ -286,13 +286,18 @@ void HAL_ESP32::ConfigureCAN() twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(gpio_num_t::GPIO_NUM_16, gpio_num_t::GPIO_NUM_17, TWAI_MODE_NORMAL); twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); - // Filter out all messages except 0x305 and 0x307 - // https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/can.html - // 01100000101 00000 00000000 00000000 = 0x60A00000 (0x305) - // 01100000111 00000 00000000 00000000 = 0x60E00000 (0x307) - // 00000000010 11111 11111111 11111111 = 0x005FFFFF - // ^ THIS BIT IS IGNORED USING THE MASK SO 0x305 and 0x307 are permitted - twai_filter_config_t f_config = {.acceptance_code = 0x60A00000, .acceptance_mask = 0x005FFFFF, .single_filter = true}; + + // We need the CAN ids: + // * Pylontech LV battery: 0x305, 0x307 + // * Pylontech Force HV battery: 0x4200, 0x8200, 0x8210 + // from these ids we _can not_ derive a filter that makes sense, i.e. + // twai_filter_config_t f_config = { + // .acceptance_code = 0x305<<21 & 0x307<<21 & 0x4200<<3 & 0x8200<<3 & 0x8210<<3, // 0 + // .acceptance_mask = 0x305<<21 | 0x307<<21 | 0x4200<<3 | 0x8200<<3 | 0x8210<<3, // 0x60E61080 + // .single_filter = true + // }; + // ----> acceptance_code == 0, so we can only set ALL + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); // Install CAN driver if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) diff --git a/ESPController/src/Rules.cpp b/ESPController/src/Rules.cpp index 2bf1358b..d4bfeb93 100644 --- a/ESPController/src/Rules.cpp +++ b/ESPController/src/Rules.cpp @@ -55,7 +55,9 @@ void Rules::ClearValues() HighestCellVoltageInBank.fill(0); highestBankVoltage = 0; + address_highestBankVoltage = maximum_number_of_banks+1; lowestBankVoltage = 0xFFFFFFFF; + address_lowestBankVoltage = maximum_number_of_banks+1; highestCellVoltage = 0; lowestCellVoltage = 0xFFFF; highestExternalTemp = -127; @@ -160,10 +162,12 @@ void Rules::ProcessBank(uint8_t bank) if (bankvoltage.at(bank) > highestBankVoltage) { highestBankVoltage = bankvoltage.at(bank); + address_highestBankVoltage = bank; } if (bankvoltage.at(bank) < lowestBankVoltage) { lowestBankVoltage = bankvoltage.at(bank); + address_lowestBankVoltage = bank; } if (VoltageRangeInBank(bank) > highestBankRange) diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index e6aef8a6..c42452ea 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -74,6 +74,7 @@ extern "C" #include "mqtt.h" #include "victron_canbus.h" #include "pylon_canbus.h" +#include "pylonforce_canbus.h" #include "string_utils.h" #include @@ -2640,11 +2641,11 @@ static const char *ESP32_TWAI_STATUS_STRINGS[] = { "RECOVERY UNDERWAY" // CAN_STATE_RECOVERING }; -void send_canbus_message(uint32_t identifier, const uint8_t *buffer, const uint8_t length) +void _send_canbus_message(const uint32_t identifier, const uint8_t *buffer, const uint8_t length, const uint32_t flags) { twai_message_t message; message.identifier = identifier; - message.flags = TWAI_MSG_FLAG_NONE; + message.flags = flags; message.data_length_code = length; memcpy(&message.data, buffer, length); @@ -2696,6 +2697,15 @@ void send_canbus_message(uint32_t identifier, const uint8_t *buffer, const uint8 } } +void send_canbus_message(const uint32_t identifier, const uint8_t *buffer, const uint8_t length) +{ + _send_canbus_message(identifier, buffer, length, TWAI_MSG_FLAG_NONE); +} +void send_ext_canbus_message(const uint32_t identifier, const uint8_t *buffer, const uint8_t length) +{ + _send_canbus_message(identifier, buffer, length, TWAI_MSG_FLAG_EXTD); +} + [[noreturn]] void canbus_tx(void *) { for (;;) @@ -2742,8 +2752,11 @@ void send_canbus_message(uint32_t identifier, const uint8_t *buffer, const uint8 // Delay a little whilst sending packets to give ESP32 some breathing room and not flood the CANBUS // vTaskDelay(pdMS_TO_TICKS(100)); } - - if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_VICTRON) + else if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_PYLONFORCEH2 ) + { + pylonforce_handle_tx(); + } + else if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_VICTRON) { // minimum CAN-IDs required for the core functionality are 0x351, 0x355, 0x356 and 0x35A. @@ -2795,18 +2808,23 @@ void send_canbus_message(uint32_t identifier, const uint8_t *buffer, const uint8 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)) + 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); - }*/ - - // Remote inverter should send a 305 message every few seconds - // for now, keep track of last message. - // TODO: in future, add timeout/error condition to shut down - if (message.identifier == 0x305) - { - canbus_last_305_message_time = esp_timer_get_time(); +// ESP_LOG_BUFFER_HEXDUMP(TAG, message.data, message.data_length_code, ESP_LOG_DEBUG); + if (mysettings.canbusprotocol == CanBusProtocolEmulation::CANBUS_PYLONFORCEH2 ) + { + pylonforce_handle_rx(&message); + } + else + { + // Remote inverter should send a 305 message every few seconds + // for now, keep track of last message. + // TODO: in future, add timeout/error condition to shut down + if (message.identifier == 0x305) + { + canbus_last_305_message_time = esp_timer_get_time(); + } + } } } else @@ -3886,10 +3904,10 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", xTaskCreate(rs485_tx, "485_TX", 2940, nullptr, 1, &rs485_tx_task_handle); xTaskCreate(rs485_rx, "485_RX", 2940, nullptr, 1, &rs485_rx_task_handle); xTaskCreate(service_rs485_transmit_q, "485_Q", 2950, nullptr, 1, &service_rs485_transmit_q_task_handle); - xTaskCreate(canbus_tx, "CAN_Tx", 2950, nullptr, 1, &canbus_tx_task_handle); + xTaskCreate(canbus_tx, "CAN_Tx", 4096, nullptr, 1, &canbus_tx_task_handle); xTaskCreate(canbus_rx, "CAN_Rx", 2950, nullptr, 1, &canbus_rx_task_handle); xTaskCreate(transmit_task, "Tx", 1950, nullptr, configMAX_PRIORITIES - 3, &transmit_task_handle); - xTaskCreate(replyqueue_task, "rxq", 2350, nullptr, configMAX_PRIORITIES - 2, &replyqueue_task_handle); + xTaskCreate(replyqueue_task, "rxq", 4096, nullptr, configMAX_PRIORITIES - 2, &replyqueue_task_handle); xTaskCreate(lazy_tasks, "lazyt", 2500, nullptr, 0, &lazy_task_handle); // Set relay defaults diff --git a/ESPController/src/pylonforce_canbus.cpp b/ESPController/src/pylonforce_canbus.cpp new file mode 100644 index 00000000..c54052bc --- /dev/null +++ b/ESPController/src/pylonforce_canbus.cpp @@ -0,0 +1,665 @@ +/* + ____ ____ _ _ ____ __ __ ___ +( _ \(_ _)( \/ )( _ \( \/ )/ __) + )(_) )_)(_ \ / ) _ < ) ( \__ \ +(____/(____) (__) (____/(_/\/\_)(___/ + + (c) 2023 Patrick Prasse + +This code communicates emulates a PYLON FORCE BATTERY using CANBUS @ 500kbps and 29 bit addresses. + +MOSTLY: https://onlineshop.gcsolar.co.za/wp-content/uploads/2021/07/CAN-Bus-Protocol-Sermatec-high-voltage-V1.1810kW.pdf +and https://www.eevblog.com/forum/programming/pylontech-sc0500-protocol-hacking/msg3742672/#msg3742672 +and https://u.pcloud.link/publink/show?code=XZCKP5VZGOrK3QVaYLuy4XWqcwWvsJUUpO4y (README Growatt-Battery-BMS.pdf) +and Deye 2_CAN-Bus-Protocol-high-voltag-V1.17.pdf +*/ + +#define USE_ESP_IDF_LOG 1 +static constexpr const char *const TAG = "diybms-pylonforce"; + +#include "pylonforce_canbus.h" +#include "mqtt.h" + +extern bool mqttClient_connected; +extern esp_mqtt_client_handle_t mqtt_client; + +// we want packed structures so the compiler adds no padding +#pragma pack(push, 1) + +// 0x7310+Addr - Versions +void pylonforce_message_7310() +{ + struct data7310 + { + uint8_t hardware_version; + uint8_t reserve1; + uint8_t hardware_version_major; + uint8_t hardware_version_minor; + uint8_t software_version_major; + uint8_t software_version_minor; + uint8_t software_version_development_major; + uint8_t software_version_development_minor; + }; + + data7310 data; + memset(&data, 0, sizeof(data7310)); + // I don't know if we could actually send our git version bytes... + data.hardware_version = 0x01; + data.hardware_version_major = 0x02; + data.hardware_version_minor = 0x01; + data.software_version_major = 0x01; + data.software_version_minor = 0x02; + + send_ext_canbus_message(0x7310+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data7310)); +} + +// 0x7320+Addr - Module / cell quantities +void pylonforce_message_7320() +{ + struct data7320 + { + uint16_t battery_series_cells; // number of battery cells in series (over all modules/boxes) + uint8_t battery_module_in_series_qty; // number of battery modules (i.e. boxes of cell_qty_in_module) in series + uint8_t cell_qty_in_module; // number of series cells per module (boxes) + uint16_t voltage_level; // resolution 1V, offset 0V + uint16_t ah_number; // resolution 1Ah, offset 0V + }; + + data7320 data; + memset(&data, 0, sizeof(data7320)); + + data.battery_series_cells = mysettings.totalNumberOfSeriesModules; + data.battery_module_in_series_qty = mysettings.totalNumberOfBanks; + data.cell_qty_in_module = mysettings.totalNumberOfSeriesModules / data.battery_module_in_series_qty; + data.voltage_level = (uint16_t)((uint32_t)mysettings.cellmaxmv * (uint32_t)mysettings.totalNumberOfSeriesModules / (uint32_t)1000); + data.ah_number = mysettings.nominalbatcap; + + send_ext_canbus_message(0x7320+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data7320)); +} + + +// 0x7330+Addr / 0x7340+Addr - Transmit the DIYBMS hostname via two CAN Messages +// Sermatec PDF +// same as 0x42e0+Addr / 0x42f0+Addr in Deye +void pylonforce_message_7330_7340() +{ + char buffer[16+1]; + memset( buffer, 0, sizeof(buffer) ); + strncpy(buffer,hostname.c_str(),sizeof(buffer)); + send_ext_canbus_message(0x7330+mysettings.canbus_equipment_addr, (uint8_t *)&hostname, 8); + vTaskDelay(pdMS_TO_TICKS(60)); + send_ext_canbus_message(0x7340+mysettings.canbus_equipment_addr, (uint8_t *)&hostname[8], 8); +} + + +// 0x4210+Addr - Total Voltage / total current / SOC / SOH +void pylonforce_message_4210() +{ + struct data4210 + { + uint16_t voltage; // Battery Pile Total Voltage, 100mV (0.1V) resolution + uint16_t current; // Battery Pile Current, 100mA (0.1A) resolution with offset 3000A + uint16_t temperature; // second level BMS temperature, 0.1°C resolution, offset 100°C + uint8_t stateofchargevalue; // SOC, Resolution 1%, offset 0 + uint8_t stateofhealthvalue; // SOH, Resolution 1%, offset 0 + }; + + data4210 data; + memset(&data, 0, sizeof(data4210)); + + // If current shunt is installed, use the voltage from that as it should be more accurate + if (mysettings.currentMonitoringEnabled && currentMonitor.validReadings) + { + data.voltage = currentMonitor.modbus.voltage * 10; + data.current = (currentMonitor.modbus.current-3000) * 10; + } + else + { + // Use highest bank voltage calculated by controller and modules + data.voltage = rules.highestBankVoltage / 100; + data.current = 0; + } + + + // Temperature 0.1 C using external temperature sensor + if (rules.moduleHasExternalTempSensor) + { + data.temperature = (uint16_t)max(0, (int16_t)(rules.highestExternalTemp + 100) * (int16_t)10); + } + else + { + // No external temp sensors + 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); + } + else + { + data.stateofchargevalue = 50; + } + + send_ext_canbus_message(0x4210+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4210)); +} + + +// 0x4220+Addr - Battery cutoff voltages + current limits +void pylonforce_message_4220() +{ + struct data4220 + { + uint16_t battery_charge_voltage; // Charge cutoff voltage, resolution 0.1V, offset 0 + uint16_t battery_discharge_voltage; // Discharge cutoff voltage, resolution 0.1V, offset 0 + + // TODO: these two might be swapped, as stated in README Growatt-Battery-BMS.pdf + uint16_t battery_charge_current_limit; // Max charge current, resolution 0.1A, offset 3000A (therefore logically >=30000) + uint16_t battery_discharge_current_limit; // Max discharge current (negative), resolution 0.1A, offset 3000A (therefore logically <=30000, as discharge current is negative Amps) + }; + + data4220 data; + memset(&data, 0, sizeof(data4220)); + + // Defaults (do nothing) + data.battery_charge_voltage = 0; + data.battery_charge_current_limit = 30000; // effective zero after applied offsets + data.battery_discharge_current_limit = 30000; // effective zero after applied offsets + data.battery_discharge_voltage = mysettings.dischargevolt; + + if (rules.IsChargeAllowed(&mysettings)) + { + data.battery_charge_voltage = rules.DynamicChargeVoltage(); + data.battery_charge_current_limit = 30000 + (uint16_t)max((int16_t)0,rules.DynamicChargeCurrent()); + } + else + { + ESP_LOGV(TAG, "Charging not allowed in message 4220"); + } + + if (rules.IsDischargeAllowed(&mysettings)) + { + data.battery_discharge_current_limit = 30000 - (uint16_t)max((uint16_t)0,mysettings.dischargecurrent); + } + else + { + ESP_LOGV(TAG, "Discharging not allowed in message 4220"); + } + + send_ext_canbus_message(0x4220+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4220)); +} + + +// 0x4230+Addr - Highest / lowest cell voltages +void pylonforce_message_4230() +{ + struct data4230 + { + uint16_t max_single_battery_cell_voltage; // Voltage of the highest cell, resolution 0.001V + uint16_t min_single_battery_cell_voltage; // Voltage of the lowest cell, resolution 0.001V + uint16_t max_battery_cell_number; // Number of the highest voltage cell, 0 - X + uint16_t min_battery_cell_number; // Number of the lowest voltage cell, 0 - X + }; + + data4230 data; + memset(&data, 0, sizeof(data4230)); + + data.max_single_battery_cell_voltage = rules.highestCellVoltage; + data.min_single_battery_cell_voltage = rules.lowestCellVoltage; + data.max_battery_cell_number = rules.address_HighestCellVoltage; + data.min_battery_cell_number = rules.address_LowestCellVoltage; + + send_ext_canbus_message(0x4230+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4230)); +} + +// 0x4240+Addr - Highest / lowest cell temperatures +void pylonforce_message_4240() +{ + struct data4240 + { + uint16_t max_single_battery_cell_temperature; // temperature of the highest cell, resolution 0.1°C, offset 100°C + uint16_t min_single_battery_cell_temperature; // temperature of the lowest cell, resolution 0.1°C, offset 100°C + uint16_t max_battery_cell_number; // Number of the highest temperature cell, 0 - X + uint16_t min_battery_cell_number; // Number of the lowest temperature cell, 0 - X + }; + + data4240 data; + memset(&data, 0, sizeof(data4240)); + + if (rules.moduleHasExternalTempSensor) + { + data.max_single_battery_cell_temperature = (rules.highestExternalTemp+100)*10; + data.min_single_battery_cell_temperature = (rules.lowestExternalTemp+100)*10; + data.max_battery_cell_number = rules.address_highestExternalTemp; + data.min_battery_cell_number = rules.address_lowestExternalTemp; + } + else + { + data.max_single_battery_cell_temperature = 110; // 10°C + data.min_single_battery_cell_temperature = 110; // 0°C + data.max_battery_cell_number = 1; + data.min_battery_cell_number = 2; + } + + send_ext_canbus_message(0x4240+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4240)); +} + + +#define BASIC_STATUS_SLEEP 0 +#define BASIC_STATUS_CHARGE 1 +#define BASIC_STATUS_DISCHARGE 2 +#define BASIC_STATUS_IDLE 3 + +// 0x4250+Addr - Status +void pylonforce_message_4250() +{ + struct data4250 + { + // use uint8_t for bitfields as otherwise there may be issues with endian order (see C99 6.7.2.1-11) + + uint8_t basic_status_status : 3; // 0: sleep, 1: charge, 2: discharge, 3: idle, 4-7: reserved + uint8_t basic_status_force_charge_request : 1; + uint8_t basic_status_balance_charge_request : 1; + uint8_t basic_status_reserve5 : 1; + uint8_t basic_status_reserve6 : 1; + uint8_t basic_status_reserve7 : 1; + + uint16_t cycle_period; + + uint8_t error_volt_sensor: 1; // voltage sensor error + uint8_t error_tmpr: 1; // temperature sensor error + uint8_t error_in_comm: 1; // internal communication error + uint8_t error_dcov: 1; // input over voltage error + uint8_t error_rv: 1; // input reversal error + uint8_t error_relay: 1; // relay check error + uint8_t error_damage: 1; // Deepl translation from chinese: "Battery damage malfunction (caused by battery overdischarge, etc.)" + uint8_t error_other: 1; // Other error (deepl translation from chinese: "Other malfunctions (see malfunction extensions for details)") + + // datasheet says "告警 Alarm", translation from chinese: warning + // if we set a bit here they are shown in Goodwe app PV Master under "BMS Status" + // alarm_cht is shown as "Charge over-temp. 2" which I suppose is a alarm/error state + uint8_t alarm_blv: 1; // single cell low voltage alarm + uint8_t alarm_bhv: 1; // single cell high voltage alarm + uint8_t alarm_plv: 1; // charge system low voltage alarm + uint8_t alarm_phv: 1; // charge system high voltage alarm + uint8_t alarm_clt: 1; // charge cell low temperature alarm + uint8_t alarm_cht: 1; // charge cell high temperature alarm + uint8_t alarm_dlt: 1; // discharge cell low temperature alarm + uint8_t alarm_dht: 1; // discharge cell high temperature alarm + uint8_t alarm_coca: 1; // charge over current alarm + uint8_t alarm_doca: 1; // discharge over current alarm + uint8_t alarm_mlv: 1; // module low voltage alarm + uint8_t alarm_mhv: 1; // module high voltage alarm + uint8_t alarm_reserve12: 1; + uint8_t alarm_reserve13: 1; + uint8_t alarm_reserve14: 1; + uint8_t alarm_reserve15: 1; + + // datasheet says "保护 Protection", translation from chinese: safeguard + // if we set a bit here they are shown in Goodwe app PV Master under "Battery warning" + // protect_cht is shown as "Charge over-temp. 1" which I suppose is a pre-warning state + uint8_t protect_blv: 1; // single cell low voltage protect + uint8_t protect_bhv: 1; // single cell high voltage protect + uint8_t protect_plv: 1; // charge system low voltage protect + uint8_t protect_phv: 1; // charge system high voltage protect + uint8_t protect_clt: 1; // charge cell low temperature protect + uint8_t protect_cht: 1; // charge cell high temperature protect + uint8_t protect_dlt: 1; // discharge cell low temperature protect + uint8_t protect_dht: 1; // discharge cell high temperature protect + uint8_t protect_coca: 1; // charge over current protect + uint8_t protect_doca: 1; // discharge over current protect + uint8_t protect_mlv: 1; // module low voltage protect + uint8_t protect_mhv: 1; // module high voltage protect + uint8_t protect_reserve12: 1; + uint8_t protect_reserve13: 1; + uint8_t protect_reserve14: 1; + uint8_t protect_reserve15: 1; + }; + + data4250 data; + memset(&data, 0, sizeof(data4250)); + + if (_controller_state == ControllerState::Running) + { + if (mysettings.currentMonitoringEnabled && currentMonitor.validReadings) + { + data.basic_status_status = currentMonitor.modbus.current > 0 ? + BASIC_STATUS_CHARGE : + currentMonitor.modbus.current < 0 ? BASIC_STATUS_DISCHARGE : BASIC_STATUS_IDLE; + } + else + { + // we don't know because we have no current monitor + data.basic_status_status = BASIC_STATUS_IDLE; + } + + data.alarm_mhv = ((rules.ruleOutcome(Rule::BankOverVoltage) || rules.ruleOutcome(Rule::CurrentMonitorOverVoltage)) ? 1 : 0); + data.alarm_mlv = ((rules.ruleOutcome(Rule::BankUnderVoltage) || rules.ruleOutcome(Rule::CurrentMonitorUnderVoltage)) ? 1 : 0); + + // TODO: maybe calculate from dynamic charge current? + data.alarm_coca = rules.ruleOutcome(Rule::CurrentMonitorOverCurrentAmps) ? 1 : 0; + data.alarm_doca = rules.ruleOutcome(Rule::CurrentMonitorOverCurrentAmps) ? 1 : 0; + + data.alarm_bhv = ((rules.ruleOutcome(Rule::ModuleOverVoltage)) ? 1 : 0); + data.alarm_blv = ((rules.ruleOutcome(Rule::ModuleUnderVoltage)) ? 1 : 0); + + if (rules.moduleHasExternalTempSensor) + { + data.alarm_cht = (rules.ruleOutcome(Rule::ModuleOverTemperatureExternal) ? 1 : 0); + data.alarm_clt = (rules.ruleOutcome(Rule::ModuleUnderTemperatureExternal) ? 1 : 0); + } + + // charge system high voltage alarm + if (rules.highestBankVoltage / 100 > mysettings.chargevolt) + { + data.alarm_phv = 1; + } + + // charge system low voltage alarm + if (rules.lowestBankVoltage / 100 < mysettings.dischargevolt) + { + data.alarm_plv = 1; + } + + // charge cell high temperature alarm + // discharge cell high temperature alarm + if (rules.moduleHasExternalTempSensor && rules.highestExternalTemp > mysettings.chargetemphigh) + { + data.alarm_cht = 1; + data.alarm_dht = 1; + } + + // charge cell low temperature alarm + // discharge cell low temperature alarm + if (rules.moduleHasExternalTempSensor && rules.lowestExternalTemp < mysettings.chargetemplow) + { + data.alarm_clt = 1; + data.alarm_dlt = 1; + } + } + else + { + data.basic_status_status = BASIC_STATUS_SLEEP; + } + +// data.error_in_comm = ((rules.ruleOutcome(Rule::BMSError) | rules.ruleOutcome(Rule::EmergencyStop)) ? 1 : 0); + data.error_other = ((_controller_state != ControllerState::Running || + rules.ruleOutcome(Rule::BMSError) || + rules.ruleOutcome(Rule::EmergencyStop)) ? 1 : 0); +// data.error_other = 1; +// data.protect_plv = 1; +// data.alarm_cht = 1; +// data.protect_cht = 1; + + send_ext_canbus_message(0x4250+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4250)); +} + + +// 0x4260+Addr - Highest / lowest module (diyBMS "bank") voltages +void pylonforce_message_4260() +{ + struct data4260 + { + uint16_t max_single_battery_module_voltage; // Voltage of the highest module, resolution 0.001V + uint16_t min_single_battery_module_voltage; // Voltage of the lowest module, resolution 0.001V + uint16_t max_battery_module_number; // Number of the highest voltage module, 0 - X + uint16_t min_battery_module_number; // Number of the lowest voltage module, 0 - X + }; + + data4260 data; + memset(&data, 0, sizeof(data4260)); + + data.max_single_battery_module_voltage = rules.highestBankVoltage; + data.min_single_battery_module_voltage = rules.lowestBankVoltage; + data.max_battery_module_number = rules.address_highestBankVoltage; + data.min_battery_module_number = rules.address_lowestBankVoltage; + + send_ext_canbus_message(0x4260+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4260)); +} + + +// 0x4270+Addr - Highest / lowest module (diyBMS "bank") temperatures +void pylonforce_message_4270() +{ + struct data4270 + { + uint16_t max_single_battery_module_temperature; // temperature of the highest module, resolution 0.1°C, offset 100°C + uint16_t min_single_battery_module_temperature; // temperature of the lowest module, resolution 0.1°C, offset 100°C + uint16_t max_battery_module_number; // Number of the highest temperature module, 0 - X + uint16_t min_battery_module_number; // Number of the lowest temperature module, 0 - X + }; + + data4270 data; + memset(&data, 0, sizeof(data4270)); + + if (rules.moduleHasExternalTempSensor) + { + data.max_single_battery_module_temperature = (rules.highestExternalTemp+100)*10; + data.min_single_battery_module_temperature = (rules.lowestExternalTemp+100)*10; + data.max_battery_module_number = 0; // TODO + data.min_battery_module_number = 0; // TODO + } + else + { + data.max_single_battery_module_temperature = 1000+121; // 12.1°C + data.min_single_battery_module_temperature = 1000+121; // 12.1°C + data.max_battery_module_number = 0; + data.min_battery_module_number = 0; + } + + send_ext_canbus_message(0x4270+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4270)); +} + + +// 0x4280+Addr - Status +void pylonforce_message_4280() +{ + struct data4280 + { + uint8_t charge_forbidden_mark; + uint8_t discharge_forbidden_mark; + + uint8_t reserve2; + uint8_t reserve3; + uint8_t reserve4; + uint8_t reserve5; + uint8_t reserve6; + uint8_t reserve7; + }; + + data4280 data; + memset(&data, 0, sizeof(data4280)); + + if (_controller_state != ControllerState::Running || !rules.IsChargeAllowed(&mysettings)) + { + data.charge_forbidden_mark = 0xAA; + } + + if (_controller_state != ControllerState::Running || !rules.IsDischargeAllowed(&mysettings)) + { + data.discharge_forbidden_mark = 0xAA; + } + + send_ext_canbus_message(0x4280+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4280)); +} + + +// 0x4290+Addr - Guessed: Startup faults +void pylonforce_message_4290() +{ + struct data4290 + { + uint8_t fault_expansion_reserve7 : 1; + uint8_t fault_expansion_reserve6 : 1; + uint8_t fault_expansion_reserve5 : 1; + uint8_t fault_expansion_abnormal_safety_functions : 1; + uint8_t fault_expansion_abnormal_power_on_self_test : 1; + uint8_t fault_expansion_abnormal_internal_bus : 1; + uint8_t fault_expansion_abnormal_bmic : 1; + uint8_t fault_expansion_abnormal_shutdown_circuit : 1; + + uint8_t reserve1; + uint8_t reserve2; + uint8_t reserve3; + uint8_t reserve4; + uint8_t reserve5; + uint8_t reserve6; + uint8_t reserve7; + }; + + data4290 data; + memset(&data, 0, sizeof(data4290)); + + send_ext_canbus_message(0x4290+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data4290)); +} + +// 0x42e0+Addr / 0x42f0+Addr - Transmit the DIYBMS hostname via two CAN Messages +// Deye 2 CAN Bus Protocol V1.17 +// seems to be also in SolArk +// the same as 0x7330+Addr / 0x7340+Addr in Sermatec HV +void pylonforce_message_42e0_42f0() +{ + char buffer[16+1]; + memset( buffer, 0, sizeof(buffer) ); + strncpy(buffer,hostname.c_str(),sizeof(buffer)); + send_ext_canbus_message(0x42e0+mysettings.canbus_equipment_addr, (uint8_t *)&buffer, 8); + vTaskDelay(pdMS_TO_TICKS(60)); + send_ext_canbus_message(0x42f0+mysettings.canbus_equipment_addr, (uint8_t *)&buffer[8], 8); +} + + + + +// have we seen the ensemble_information message from inverter? +bool seen_ensemble_information = false; + +// have we seen the identify message from inverter? +bool seen_identify_message = false; + +// is this the first call of handle_tx? +bool first_handle_tx = true; + +void pylonforce_handle_tx() +{ + ESP_LOGV(TAG, "pylonforce_handle_tx\n"); + + if( seen_identify_message || first_handle_tx ) + { + ESP_LOGV(TAG, "seen_identify_message\n"); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_7310(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_7320(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_7330_7340(); + + seen_identify_message = false; // message answered + } + // no else here + if( seen_ensemble_information || first_handle_tx ) + { + ESP_LOGV(TAG, "seen_ensemble_information\n"); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4210(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4220(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4230(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4240(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4250(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4260(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4270(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4280(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_4290(); + vTaskDelay(pdMS_TO_TICKS(60)); + pylonforce_message_42e0_42f0(); + + seen_ensemble_information = false; // message answered + } + + first_handle_tx = false; +} + +void pylonforce_handle_rx(twai_message_t *message) +{ + if( !(message->flags & TWAI_MSG_FLAG_EXTD) ) // no 29bit addresses, can not be for us + return; + if( message->identifier == 0x4200 ) + { + if( message->data[0] == 0x02 ) // question from inverter: system equipment information + { + seen_identify_message = true; + } + else if( message->data[0] == 0x00 ) // question from inverter: ensemble information + { + seen_ensemble_information = true; + } + } + else if( message->identifier == (0x8200+mysettings.canbus_equipment_addr) ) + { + // Sleep / Awake Command + // diyBMS is always awake + // no reply + + // byte0 == 0x55: Control device enter sleep status; + // byte0 == 0xAA: Control device quit sleep status; + // Others: Null + } + else if( message->identifier == (0x8210+mysettings.canbus_equipment_addr) ) + { + // Charge/Discharge Command + // diyBMS relay control is scope of rules, not CAN + // no reply + + /* From documentation: +*Note: +1. Charge Command: When the battery is in under-voltage protection, the relay is open. When +EMS or PCS is going to charge the battery, send this command, then the battery will close +the main relay. If the battery is in sleep status, wake up first then use this command. +2. Discharge Command: When the battery is in over-voltage protection, the relay is open. +When EMS or PCS is going to discharge the battery, send this command, then the battery +will close the main relay. If the battery is in sleep status, wake up first then use this +command. + */ + + // byte0 == 0xAA: effect; Others: Null (* Note 1) + // byte1 == 0xAA: effect; Others: Null (* Note 2) + } + else if( message->identifier == (0x8240+mysettings.canbus_equipment_addr) ) + { + // Temporary masking "external communication error" command + + /* From documentation: +Note: +After receive this command, BMS will estimate the condition and give reply. +If meet the condition, in 5 minutes, BMS will ignore the “external communication fail” alarm, which +means relay will keep ON while no communication between BMS and EMS/PCS. +In this 5 minutes, if there is a protection alarm, BMS will cut off the relay as normal + */ + + + uint8_t data[8]; + memset(&data, 0, sizeof(data)); + + // TODO: reply: OK, will act this command immediately + //data[0] = 0xAA; + + // reply: won`t act this command + data[0] = 0x00; + + send_ext_canbus_message(0x8250+mysettings.canbus_equipment_addr, (uint8_t *)&data, sizeof(data)); + } +} + +#pragma pack(pop) + diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 482e7f5f..930e36ff 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -585,6 +585,7 @@ void DefaultConfiguration(diybms_eeprom_settings *_myset) _myset->canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; _myset->canbusinverter = CanBusInverter::INVERTER_GENERIC; + _myset->canbus_equipment_addr = 0; _myset->nominalbatcap = 280; // Scale 1 _myset->chargevolt = 565; // Scale 0.1 _myset->chargecurrent = 650; // Scale 0.1 diff --git a/ESPController/src/victron_canbus.cpp b/ESPController/src/victron_canbus.cpp index e5a5c5d1..efa26fd6 100644 --- a/ESPController/src/victron_canbus.cpp +++ b/ESPController/src/victron_canbus.cpp @@ -20,6 +20,7 @@ static constexpr const char *const TAG = "diybms-victron"; void victron_message_370_371() { char buffer[16+1]; + memset( buffer, 0, sizeof(buffer) ); strncpy(buffer,hostname.c_str(),sizeof(buffer)); send_canbus_message(0x370, (const uint8_t *)&buffer[0], 8); diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index a562f55b..6dacef0b 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -959,6 +959,7 @@

Charge/Discharge configuration

+ From e0ad6cf8aa2d15dad1c3e2440b41d6728c88dd4b Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:11:57 +0000 Subject: [PATCH 03/27] MQTT changes for reliability --- .../include/webserver_json_requests.h | 9 +- ESPController/src/main.cpp | 41 ++++----- ESPController/src/mqtt.cpp | 84 +++++++++++-------- ESPController/src/webserver_json_requests.cpp | 8 ++ ESPController/web_src/default.htm | 24 +++++- ESPController/web_src/pagecode.js | 8 +- 6 files changed, 118 insertions(+), 56 deletions(-) diff --git a/ESPController/include/webserver_json_requests.h b/ESPController/include/webserver_json_requests.h index d94ef05c..761f7bd3 100644 --- a/ESPController/include/webserver_json_requests.h +++ b/ESPController/include/webserver_json_requests.h @@ -42,7 +42,7 @@ extern uint32_t canbus_messages_received_error; extern Rules rules; extern ControllerState _controller_state; -extern void formatCurrentDateTime(char* buf, size_t buf_size); +extern void formatCurrentDateTime(char *buf, size_t buf_size); extern void setNoStoreCacheControl(httpd_req_t *req); extern char CookieValue[20 + 1]; extern std::string hostname; @@ -56,4 +56,11 @@ extern CurrentMonitorINA229 currentmon_internal; extern History history; extern wifi_eeprom_settings _wificonfig; extern esp_err_t diagnosticJSON(httpd_req_t *req, char buffer[], int bufferLenMax); + +extern bool mqttClient_connected; +extern uint16_t mqtt_error_connection_count; +extern uint16_t mqtt_error_transport_count; +extern uint16_t mqtt_connection_count; +extern uint16_t mqtt_disconnection_count; + #endif diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 7100e4b0..aa8ea37f 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -1577,28 +1577,31 @@ static void startMDNS() static void event_handler(void *, esp_event_base_t event_base, int32_t event_id, void *event_data) { + ESP_LOGD(TAG, "WIFI: event=%i, id=%i", event_base, event_id); + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_BSS_RSSI_LOW) { ESP_LOGW(TAG, "WiFi signal strength low"); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + ESP_LOGI(TAG, "WIFI_EVENT_STA_START"); wifi_isconnected = false; - esp_wifi_connect(); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); wifi_isconnected = false; if (s_retry_num < 200) { - esp_wifi_connect(); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); s_retry_num++; - ESP_LOGI(TAG, "Retry %i, connect to Wifi AP", s_retry_num); + ESP_LOGI(TAG, "Retry %i, connect to WIFI AP", s_retry_num); } else { - // xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); - ESP_LOGE(TAG, "Connect to the Wifi AP failed"); + ESP_LOGE(TAG, "Connect to the WIFI AP failed"); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) @@ -1616,9 +1619,7 @@ static void event_handler(void *, esp_event_base_t event_base, } stopMqtt(); stopMDNS(); - - esp_wifi_disconnect(); - + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); wake_up_tft(true); // Try and reconnect @@ -1628,21 +1629,21 @@ static void event_handler(void *, esp_event_base_t event_base, { wifi_isconnected = true; auto event = (ip_event_got_ip_t *)event_data; - // ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; - // xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); // Start up all the services after TCP/IP is established configureSNTP(mysettings.timeZone * 3600 + mysettings.minutesTimeZone * 60, mysettings.daylight ? 3600 : 0, mysettings.ntpServer); if (!server_running) { + // Start web server StartServer(); server_running = true; } - connectToMqtt(); - + // This only exists in the loop() + // connectToMqtt(); startMDNS(); ip_string = ip4_to_string(event->ip_info.ip.addr); @@ -4077,18 +4078,18 @@ void loop() // on first pass wifitimer is zero if (currentMillis - wifitimer > 30000) { - // Attempt to connect to WiFi every 30 seconds, this caters for when WiFi drops - // such as AP reboot - - // wifi_init_sta(); - if (!wifi_isconnected) + // Attempt to connect to WiFi every 30 seconds, this caters for when WiFi drops such as AP reboot + if (wifi_isconnected) { + // Attempt to connect to MQTT if enabled and not already connected + connectToMqtt(); + } + else + { + ESP_LOGI(TAG, "Trying to connect WIFI"); esp_wifi_connect(); } wifitimer = currentMillis; - - // Attempt to connect to MQTT if enabled and not already connected - connectToMqtt(); } } diff --git a/ESPController/src/mqtt.cpp b/ESPController/src/mqtt.cpp index a8bd8268..52aafa4b 100644 --- a/ESPController/src/mqtt.cpp +++ b/ESPController/src/mqtt.cpp @@ -16,6 +16,10 @@ static constexpr const char *const TAG = "diybms-mqtt"; bool mqttClient_connected = false; esp_mqtt_client_handle_t mqtt_client = nullptr; +uint16_t mqtt_error_connection_count = 0; +uint16_t mqtt_error_transport_count = 0; +uint16_t mqtt_connection_count = 0; +uint16_t mqtt_disconnection_count = 0; bool checkMQTTReady() { @@ -23,14 +27,20 @@ bool checkMQTTReady() { return false; } + + if (mqtt_client == nullptr) + { + ESP_LOGW(TAG, "MQTT enabled, but not yet init"); + return false; + } if (!wifi_isconnected) { - ESP_LOGE(TAG, "MQTT enabled. WIFI not connected"); + ESP_LOGW(TAG, "MQTT enabled, WIFI not connected"); return false; } if (mqttClient_connected == false) { - ESP_LOGE(TAG, "MQTT enabled. But not connected"); + ESP_LOGW(TAG, "MQTT enabled, but not connected"); return false; } @@ -47,7 +57,7 @@ static inline void publish_message(std::string &topic, std::string &payload, boo static constexpr int MQTT_QUALITY_OF_SERVICE = 1; static constexpr int MQTT_RETAIN_MESSAGE = 0; - if (mqtt_client && mqttClient_connected) + if (mqtt_client != nullptr && mqttClient_connected) { int id = esp_mqtt_client_publish( mqtt_client, topic.c_str(), payload.c_str(), payload.length(), @@ -76,77 +86,89 @@ static void mqtt_connected_handler(void *, esp_event_base_t, int32_t, void *) { ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); mqttClient_connected = true; + mqtt_connection_count++; } static void mqtt_disconnected_handler(void *, esp_event_base_t, int32_t, void *) { ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); mqttClient_connected = false; + mqtt_disconnection_count++; } static void mqtt_error_handler(void *, esp_event_base_t, int32_t, void *event_data) { - // ESP_LOGD(TAG, "Event base=%s, event_id=%d", base, event_id); auto event = (esp_mqtt_event_handle_t)event_data; - // auto client = event->client; - // int msg_id; - ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); + // ESP_LOGE(TAG, "MQTT_EVENT_ERROR type=%i",event->error_handle->error_type); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) + { + mqtt_error_connection_count++; + // esp_mqtt_connect_return_code_t reason for failure + ESP_LOGE(TAG, "MQTT_ERROR_TYPE_CONNECTION_REFUSED code=%i", event->error_handle->connect_return_code); + } + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + mqtt_error_transport_count++; // log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err); // log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err); // log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno); - ESP_LOGE(TAG, "Last err no string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); + ESP_LOGE(TAG, "ERROR_TYPE_TCP (%s)", strerror(event->error_handle->esp_transport_sock_errno)); } } void stopMqtt() { - if (mqtt_client != nullptr && mqttClient_connected) + if (mqtt_client != nullptr) { - // ESP_LOGI(TAG, "Stopping MQTT client"); + ESP_LOGI(TAG, "Stopping MQTT client"); mqttClient_connected = false; - ESP_LOGI(TAG, "esp_mqtt_client_disconnect"); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_disconnect(mqtt_client)); - /* - Comment out to see if this helps with https://github.com/stuartpittaway/diyBMSv4ESP32/issues/225 ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(mqtt_client)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_destroy(mqtt_client)); mqtt_client = nullptr; - */ + + //Reset stats + mqtt_error_connection_count = 0; + mqtt_error_transport_count = 0; + mqtt_connection_count = 0; + mqtt_disconnection_count = 0; } } // Connects to MQTT if required void connectToMqtt() { - if (mysettings.mqtt_enabled && mqttClient_connected) - { - // Already connected and enabled - return; - } + ESP_LOGI(TAG, "MQTT counters: Err_Con=%u,Err_Trans=%u,Conn=%u,Disc=%u", mqtt_error_connection_count, + mqtt_error_transport_count, mqtt_connection_count, mqtt_disconnection_count); - if (mysettings.mqtt_enabled) + if (mysettings.mqtt_enabled && mqtt_client == nullptr) { - // stopMqtt(); - - ESP_LOGI(TAG, "Connect MQTT"); + ESP_LOGI(TAG, "esp_mqtt_client_init"); // Need to preset variables in esp_mqtt_client_config_t otherwise LoadProhibited errors esp_mqtt_client_config_t mqtt_cfg{ - .event_handle = nullptr, .host = "", .uri = mysettings.mqtt_uri, .disable_auto_reconnect = false}; - - mqtt_cfg.username = mysettings.mqtt_username; - mqtt_cfg.password = mysettings.mqtt_password; + .event_handle = nullptr, + .host = "", + .uri = mysettings.mqtt_uri, + .username = mysettings.mqtt_username, + .password = mysettings.mqtt_password, + //Reconnect if there server has a problem (or wrong IP/password etc.) + .disable_auto_reconnect = false, + //30 seconds + .reconnect_timeout_ms = 30000, + //3 seconds + .network_timeout_ms = 3000}; mqtt_client = esp_mqtt_client_init(&mqtt_cfg); + if (mqtt_client != nullptr) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_CONNECTED, mqtt_connected_handler, nullptr)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_DISCONNECTED, mqtt_disconnected_handler, nullptr)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_ERROR, mqtt_error_handler, nullptr)); + ESP_LOGI(TAG, "esp_mqtt_client_start"); if (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_start(mqtt_client)) != ESP_OK) { ESP_LOGE(TAG, "esp_mqtt_client_start failed"); @@ -154,13 +176,9 @@ void connectToMqtt() } else { - ESP_LOGE(TAG, "mqtt_client returned NULL"); + ESP_LOGE(TAG, "esp_mqtt_client_init returned NULL"); } } - /*else - { - stopMqtt(); - }*/ } void GeneralStatusPayload(const PacketRequestGenerator *prg, const PacketReceiveProcessor *receiveProc, uint16_t requestq_count, const Rules *rules) diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index 5129f6bd..6670b9c7 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -808,6 +808,14 @@ esp_err_t content_handler_integration(httpd_req_t *req) mqtt["topic"] = mysettings.mqtt_topic; mqtt["uri"] = mysettings.mqtt_uri; mqtt["username"] = mysettings.mqtt_username; + + mqtt["connected"]= mqttClient_connected; + mqtt["err_conn_count"]=mqtt_error_connection_count; + mqtt["err_trans_count"]=mqtt_error_transport_count; + mqtt["conn_count"]=mqtt_connection_count; + mqtt["disc_count"]=mqtt_disconnection_count; + + // We don't output the password in the json file as this could breach security // mqtt["password"] =mysettings.mqtt_password; diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index a562f55b..3c3f2c85 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -343,7 +343,7 @@

Diagnostics

-


+      

       

Running tasks: @@ -517,6 +517,28 @@

MQTT

maxlength="32" /> + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index 3c606fde..5d19bc5c 100644 --- a/ESPController/web_src/pagecode.js +++ b/ESPController/web_src/pagecode.js @@ -1740,6 +1740,12 @@ $(function () { $("#mqttUsername").val(data.mqtt.username); $("#mqttPassword").val(""); + $("#mqttConnected").val(data.mqtt.connected); + $("#mqttErrConnCount").val(data.mqtt.err_conn_count); + $("#mqttErrTransCount").val(data.mqtt.err_trans_count); + $("#mqttConnCount").val(data.mqtt.conn_count); + $("#mqttDiscCount").val(data.mqtt.disc_count); + $("#influxEnabled").prop("checked", data.influxdb.enabled); $("#influxUrl").val(data.influxdb.url); $("#influxDatabase").val(data.influxdb.bucket); @@ -1976,7 +1982,7 @@ $(function () { function (data) { $("#canbusprotocol").val(data.chargeconfig.canbusprotocol); - $("#canbusinverter").val(data.chargeconfig.canbusinverter); + $("#canbusinverter").val(data.chargeconfig.canbusinverter); $("#nominalbatcap").val(data.chargeconfig.nominalbatcap); $("#chargevolt").val((data.chargeconfig.chargevolt / 10.0).toFixed(1)); From fd1b9cee33dcd72dc8f81b5a394a46e8374dd878 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:42:37 +0000 Subject: [PATCH 04/27] Update main.cpp --- ESPController/src/main.cpp | 103 +++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index aa8ea37f..680246aa 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -319,7 +319,7 @@ void wake_up_tft(bool force) if (tftwake_timer != nullptr) { force_tft_wake = force; - if (xTimerStart(tftwake_timer, pdMS_TO_TICKS(10)) != pdPASS) + if (xTimerStart(tftwake_timer, pdMS_TO_TICKS(50)) != pdPASS) { ESP_LOGE(TAG, "TFT wake timer error"); } @@ -1480,7 +1480,7 @@ void pulse_relay_off(const TimerHandle_t) } } -static int s_retry_num = 0; +static int wifi_ap_connect_retry_num = 0; void formatCurrentDateTime(char *buf, size_t buf_size) { @@ -1569,6 +1569,19 @@ static void startMDNS() } } +void ShutdownAllNetworkServices() +{ + // Shut down all TCP/IP reliant services + if (server_running) + { + stop_webserver(_myserver); + server_running = false; + _myserver = nullptr; + } + stopMqtt(); + stopMDNS(); +} + /// @brief WIFI Event Handler /// @param /// @param event_base @@ -1577,7 +1590,7 @@ static void startMDNS() static void event_handler(void *, esp_event_base_t event_base, int32_t event_id, void *event_data) { - ESP_LOGD(TAG, "WIFI: event=%i, id=%i", event_base, event_id); + // ESP_LOGD(TAG, "WIFI: event=%i, id=%i", event_base, event_id); if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_BSS_RSSI_LOW) { @@ -1589,48 +1602,53 @@ static void event_handler(void *, esp_event_base_t event_base, wifi_isconnected = false; ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) + { + // We have joined the access point - now waiting for IP address IP_EVENT_STA_GOT_IP + wifi_ap_connect_retry_num = 0; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); - wifi_isconnected = false; - if (s_retry_num < 200) + wifi_ap_connect_retry_num++; + + if (wifi_isconnected) + { + ShutdownAllNetworkServices(); + wifi_isconnected = false; + } + + if (wifi_ap_connect_retry_num < 25) { - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); - s_retry_num++; - ESP_LOGI(TAG, "Retry %i, connect to WIFI AP", s_retry_num); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + ESP_LOGI(TAG, "WIFI connect quick retry %i", wifi_ap_connect_retry_num); } else { - ESP_LOGE(TAG, "Connect to the WIFI AP failed"); + ESP_LOGE(TAG, "Connect to WIFI AP failed, tried %i times", wifi_ap_connect_retry_num); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { wifi_isconnected = false; - ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); - // Shut down all TCP/IP reliant services - if (server_running) - { - stop_webserver(_myserver); - server_running = false; - _myserver = nullptr; - } - stopMqtt(); - stopMDNS(); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); + ShutdownAllNetworkServices(); + // ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); wake_up_tft(true); // Try and reconnect - esp_wifi_connect(); + // esp_wifi_connect(); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - wifi_isconnected = true; auto event = (ip_event_got_ip_t *)event_data; - ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); - s_retry_num = 0; + + if (event->ip_changed) + { + ESP_LOGI(TAG, "IP ADDRESS HAS CHANGED"); + ShutdownAllNetworkServices(); + } // Start up all the services after TCP/IP is established configureSNTP(mysettings.timeZone * 3600 + mysettings.minutesTimeZone * 60, mysettings.daylight ? 3600 : 0, mysettings.ntpServer); @@ -1648,9 +1666,10 @@ static void event_handler(void *, esp_event_base_t event_base, ip_string = ip4_to_string(event->ip_info.ip.addr); - wake_up_tft(true); - ESP_LOGI(TAG, "You can access DIYBMS interface at http://%s.local or http://%s", hostname.c_str(), ip_string.c_str()); + + wifi_isconnected = true; + wake_up_tft(true); } } @@ -1783,7 +1802,7 @@ void wifi_init_sta(void) cfg.dynamic_tx_buf_num = 32; cfg.tx_buf_type = 1; cfg.cache_tx_buf_num = 1; - cfg.static_rx_buf_num = 4; + cfg.static_rx_buf_num = 6; cfg.dynamic_rx_buf_num = 32; ESP_ERROR_CHECK(esp_wifi_init(&cfg)); @@ -1842,15 +1861,6 @@ uint16_t calculateCRC(const uint8_t *f, uint8_t bufferSize) } return temp; - /* - // Reverse byte order. - uint16_t temp2 = temp >> 8; - temp = (temp << 8) | temp2; - temp &= 0xFFFF; - // the returned value is already swapped - // crcLo byte is first & crcHi byte is last - return temp; - */ } uint8_t SetMobusRegistersFromFloat(uint8_t *cmd, uint8_t ptr, float value) @@ -3857,7 +3867,7 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", pulse_relay_off_timer = xTimerCreate("PULSE", pdMS_TO_TICKS(250), pdFALSE, (void *)2, &pulse_relay_off); assert(pulse_relay_off_timer); - tftwake_timer = xTimerCreate("TFTWAKE", pdMS_TO_TICKS(2), pdFALSE, (void *)3, &tftwakeup); + tftwake_timer = xTimerCreate("TFTWAKE", pdMS_TO_TICKS(50), pdFALSE, (void *)3, &tftwakeup); assert(tftwake_timer); xTaskCreate(voltageandstatussnapshot_task, "snap", 1950, nullptr, 1, &voltageandstatussnapshot_task_handle); @@ -4041,7 +4051,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; +// unsigned long taskinfotimer = 0; void logActualTime() { @@ -4055,6 +4065,9 @@ void logActualTime() void loop() { + delay(100); + + unsigned long currentMillis = millis(); if (card_action == CardAction::Mount) { @@ -4071,12 +4084,11 @@ void loop() mountSDCard(); } - unsigned long currentMillis = millis(); - - if (_controller_state != ControllerState::NoWifiConfiguration) + // on first pass wifitimer is zero + if (_controller_state != ControllerState::NoWifiConfiguration && currentMillis > wifitimer) { - // on first pass wifitimer is zero - if (currentMillis - wifitimer > 30000) + // Avoid triggering on the very first loop (causes ESP_ERR_WIFI_CONN warning) + if (wifitimer > 0) { // Attempt to connect to WiFi every 30 seconds, this caters for when WiFi drops such as AP reboot if (wifi_isconnected) @@ -4087,10 +4099,11 @@ void loop() else { ESP_LOGI(TAG, "Trying to connect WIFI"); - esp_wifi_connect(); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } - wifitimer = currentMillis; } + // Wait another 30 seconds + wifitimer = currentMillis + 30000; } // Call update to receive, decode and process incoming packets From e084000960c295ec8a1dabe1185aea6ac7f7d2b2 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:42:39 +0000 Subject: [PATCH 05/27] Update tft.cpp --- ESPController/src/tft.cpp | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/ESPController/src/tft.cpp b/ESPController/src/tft.cpp index 8eba9326..73ad131d 100644 --- a/ESPController/src/tft.cpp +++ b/ESPController/src/tft.cpp @@ -461,7 +461,7 @@ void init_tft_display() } // This task switches on/off the TFT screen, and triggers a redraw of its contents -void tftwakeup(TimerHandle_t xTimer) +void tftwakeup(TimerHandle_t) { // Use parameter to force a refresh (used when realtime events occur like wifi disconnect) if (_tft_screen_available) @@ -475,19 +475,16 @@ void tftwakeup(TimerHandle_t xTimer) // Screen is already awake, so can we process a touch command? // ESP_LOGD(TAG, "touched=%u, pressure=%u, X=%u, Y=%u", _lastTouch.touched, _lastTouch.pressure, _lastTouch.X, _lastTouch.Y); - if (_lastTouch.touched) + // X range is 0-4096 + if (_lastTouch.touched && _lastTouch.X < 1000) { - // X range is 0-4096 - if (_lastTouch.X < 1000) - { - ESP_LOGD(TAG, "Touched LEFT"); - PageBackward(); - } - else if (_lastTouch.X > 3000) - { - ESP_LOGD(TAG, "Touched RIGHT"); - PageForward(); - } + ESP_LOGD(TAG, "Touched LEFT"); + PageBackward(); + } + else if (_lastTouch.touched && _lastTouch.X > 3000) + { + ESP_LOGD(TAG, "Touched RIGHT"); + PageForward(); } } @@ -499,14 +496,6 @@ void tftwakeup(TimerHandle_t xTimer) // Always start on the same screen/settings ResetScreenSequence(); - if (hal.GetDisplayMutex()) - { - // Fill screen with a grey colour, to let user know - // we have responded to touch (may may be a short delay until the display task runs) - tft.fillScreen(TFT_LIGHTGREY); - hal.ReleaseDisplayMutex(); - } - hal.TFTScreenBacklight(true); } @@ -612,8 +601,8 @@ void PrepareTFT_SocBarGraph() tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK); - // The bar graph - int16_t SoC =(int16_t)currentMonitor.stateofcharge; + // The bar graph + int16_t SoC = (int16_t)currentMonitor.stateofcharge; if (SoC > 100) { @@ -624,8 +613,8 @@ void PrepareTFT_SocBarGraph() if (SoC != 100) { - //Clear between SoC and 100% - tft.fillRect((xhalfway - 100) + (2 * SoC), yhalfway - 22, 200-(2 * SoC), 44, TFT_BLACK); + // Clear between SoC and 100% + tft.fillRect((xhalfway - 100) + (2 * SoC), yhalfway - 22, 200 - (2 * SoC), 44, TFT_BLACK); } // Stripe lines From 3fe82d8ac8e60e065c47918ef26573e694933a83 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:30:05 +0000 Subject: [PATCH 06/27] Update platformio.ini --- STM32All-In-One/platformio.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/STM32All-In-One/platformio.ini b/STM32All-In-One/platformio.ini index 3867256e..a4be8771 100644 --- a/STM32All-In-One/platformio.ini +++ b/STM32All-In-One/platformio.ini @@ -12,9 +12,7 @@ default_envs = V490_10K, V490_5K, V490_5K_VREF4500, V490_10K_VREF4500 [env] -;Version 16 results in a compile size larger than 32k -platform = ststm32@17.0.0 -;platform = https://github.com/platformio/platform-ststm32.git#v17.0.0 +platform = platformio/ststm32@^17.0.0 board = genericSTM32F030K6T6 board_build.core = stm32 framework = arduino From 4f25e3c2b514cc8372583973958b6b4699e15193 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:30:23 +0000 Subject: [PATCH 07/27] Reduce delays during balance function --- STM32All-In-One/src/main.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index fae8015e..55bc737f 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -341,7 +341,7 @@ void ADCSampleCellVoltages(uint8_t cellCount, std::array &rawADC) rawADC.at(cellid) = takeRawMCP33151ADCReading(); } } - +/* /// @brief Measure internal parasitic sampling voltages to offset from future voltage measurements /// @param cellCount number of cells to sample /// @param cd Cell data array @@ -362,7 +362,7 @@ void ParasiticCapacitanceChargeInjectionErrorCalibration(uint8_t cellCount, Cell //DecimateRawADCParasiteVoltage(rawADC, cd, cellCount); digitalWrite(SAMPLE_AFE, HIGH); // Sample mode -} +}*/ /// @brief Calibrate internal op-amp buffer void BufferAmplifierOffsetCalibration() @@ -553,7 +553,7 @@ void setup() ErrorFlashes(3); } - ParasiticCapacitanceChargeInjectionErrorCalibration(number_of_active_cells, celldata); + //ParasiticCapacitanceChargeInjectionErrorCalibration(number_of_active_cells, celldata); BufferAmplifierOffsetCalibration(); configureModules(); @@ -924,8 +924,8 @@ void loop() { // Switch off any bypass balancing before taking voltage readings MAX14921Command(0, ANALOG_BUFFERED_T1); - // Allow voltages to bounce back - delay(20); + // Allow cell voltages to bounce back + delay(10); } digitalWrite(SAMPLE_AFE, HIGH); // Sample cell voltages (all 16 cells at same time) @@ -998,7 +998,7 @@ void loop() } // Every so often, we should call this to calibrate the op-amp as it changes in ambient temperature (takes 8ms to complete) - if (PP.getPacketReceivedCounter() % 4096 == 0) + if (PP.getPacketReceivedCounter() % 8192 == 0) { BufferAmplifierOffsetCalibration(); } From 73df16d408462e88646703a881f6ecd11237d105 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:32:36 +0000 Subject: [PATCH 08/27] Remove commented out code --- STM32All-In-One/include/cell.h | 18 ++---------------- STM32All-In-One/platformio.ini | 2 +- STM32All-In-One/src/main.cpp | 33 --------------------------------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/STM32All-In-One/include/cell.h b/STM32All-In-One/include/cell.h index 25c69701..80c7787a 100644 --- a/STM32All-In-One/include/cell.h +++ b/STM32All-In-One/include/cell.h @@ -15,7 +15,7 @@ class Cell /// @brief returns cell voltage with parasitic voltage removed uint16_t getCellVoltage() const { - return cellVoltage;// - getParasiteVoltage(); + return cellVoltage; } uint16_t CombineTemperatures() const @@ -36,20 +36,7 @@ class Cell { cellVoltage = v; } - /* - /// @brief Sets Parasite voltage (raw value) - /// @param v raw ADC value, which is internally divided by 128 - void setParasiteVoltage(uint16_t v) - { - parasiteVoltage = v / 128; - } - /// @brief Gets Parasite voltage - /// @return value in millivolts - uint16_t getParasiteVoltage() const - { - return parasiteVoltage; - } - */ + /// @brief Set external temperature measurement /// @param t Temperature in degrees C void setExternalTemperature(int16_t t) @@ -208,7 +195,6 @@ class Cell private: bool ChangesAllowed{true}; uint16_t cellVoltage{0}; - //uint16_t parasiteVoltage{0}; int16_t externalTemperature{-999}; int16_t internalTemperature{-999}; bool CellIsInBypass{false}; diff --git a/STM32All-In-One/platformio.ini b/STM32All-In-One/platformio.ini index a4be8771..0bf514b5 100644 --- a/STM32All-In-One/platformio.ini +++ b/STM32All-In-One/platformio.ini @@ -52,7 +52,7 @@ build_flags= -DINT_BCOEFFICIENT=3950 -DEXT_BCOEFFICIENT=3950 -DLOAD_RESISTANCE=18.0 - -DDIYBMSBAUD=5000 + -DDIYBMSBAUD=5000 -DDIYBMSREFMILLIVOLT=4096 -DADC_SAMPLINGTIME=ADC_SAMPLETIME_239CYCLES_5 -DSERIAL_RX_BUFFER_SIZE=64 diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 55bc737f..11c72f98 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -78,7 +78,6 @@ const auto FAN = PB3; [[noreturn]] void ErrorFlashes(int number); uint32_t takeRawMCP33151ADCReading(); uint32_t MAX14921Command(uint8_t b1, uint8_t b2, uint8_t b3); -//void DecimateRawADCParasiteVoltage(std::array &rawADC, CellData &cd, uint8_t numberCells); void DecimateRawADCCellVoltage(std::array &rawADC, CellData &cd, uint8_t numberCells); void TakeExternalTempMeasurements(CellData &cd); uint16_t DecimateValue(uint64_t val); @@ -341,28 +340,6 @@ void ADCSampleCellVoltages(uint8_t cellCount, std::array &rawADC) rawADC.at(cellid) = takeRawMCP33151ADCReading(); } } -/* -/// @brief Measure internal parasitic sampling voltages to offset from future voltage measurements -/// @param cellCount number of cells to sample -/// @param cd Cell data array -void ParasiticCapacitanceChargeInjectionErrorCalibration(uint8_t cellCount, CellData &cd) -{ - // MAX14921 - digitalWrite(SAMPLE_AFE, HIGH); // Sample mode - // Parasitic Capacitance Charge Injection Error Calibration - MAX14921Command(0, 0, 0); - delay(250); - digitalWrite(SAMPLE_AFE, LOW); // Hold mode - - std::array rawADC; - rawADC.fill(0); - - ADCSampleCellVoltages(cellCount, rawADC); - - //DecimateRawADCParasiteVoltage(rawADC, cd, cellCount); - - digitalWrite(SAMPLE_AFE, HIGH); // Sample mode -}*/ /// @brief Calibrate internal op-amp buffer void BufferAmplifierOffsetCalibration() @@ -612,16 +589,6 @@ uint16_t DecimateValue(uint64_t val) return (uint16_t)val; } -// Take the raw oversampled readings and decimate -/* -void DecimateRawADCParasiteVoltage(std::array &rawADC, CellData &cells, uint8_t numberCells) -{ - for (int cellid = 0; cellid < numberCells; cellid++) - { - cells.at(cellid).setParasiteVoltage(DecimateValue(rawADC[cellid])); - } -} -*/ // Take the raw oversampled readings and decimate void DecimateRawADCCellVoltage(std::array &rawADC, CellData &cells, uint8_t numberCells) { From 6753d13309a13468db4d820717b067206914beeb Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:59:18 +0000 Subject: [PATCH 09/27] Changes for CANBUS baud rate speed selection #251 --- ESPController/include/HAL_ESP32.h | 2 +- ESPController/include/defines.h | 3 +++ ESPController/src/HAL_ESP32.cpp | 18 ++++++++++++++---- ESPController/src/main.cpp | 7 ++++--- ESPController/src/settings.cpp | 9 ++++++++- ESPController/src/webserver_json_post.cpp | 3 +++ ESPController/src/webserver_json_requests.cpp | 1 + ESPController/web_src/default.htm | 15 +++++++++------ ESPController/web_src/pagecode.js | 1 + 9 files changed, 44 insertions(+), 15 deletions(-) diff --git a/ESPController/include/HAL_ESP32.h b/ESPController/include/HAL_ESP32.h index 89c33daa..fc9e4170 100644 --- a/ESPController/include/HAL_ESP32.h +++ b/ESPController/include/HAL_ESP32.h @@ -101,11 +101,11 @@ class HAL_ESP32 SPIClass *VSPI_Ptr(); void Led(uint8_t bits); + void ConfigureCAN(uint16_t canbusbaudrate) const; void ConfigurePins(); void TFTScreenBacklight(bool Status); void CANBUSEnable(bool value); - void ConfigureCAN(); bool IsVSPIMutexAvailable() { diff --git a/ESPController/include/defines.h b/ESPController/include/defines.h index 2077a6f1..36f8a755 100644 --- a/ESPController/include/defines.h +++ b/ESPController/include/defines.h @@ -183,6 +183,9 @@ struct diybms_eeprom_settings CanBusProtocolEmulation canbusprotocol; CanBusInverter canbusinverter; + //CANBUS baud rate, 250=250k, 500=500k + uint16_t canbusbaud; + //Nominal battery capacity (amp hours) uint16_t nominalbatcap; // Maximum charge voltage - scale 0.1 uint16_t chargevolt; diff --git a/ESPController/src/HAL_ESP32.cpp b/ESPController/src/HAL_ESP32.cpp index 1601c1c7..888010bd 100644 --- a/ESPController/src/HAL_ESP32.cpp +++ b/ESPController/src/HAL_ESP32.cpp @@ -27,7 +27,7 @@ bool HAL_ESP32::MountSDCard() } else { - ESP_LOGI(TAG, "SD card mounted, type %i",(int)cardType); + ESP_LOGI(TAG, "SD card mounted, type %i", (int)cardType); result = true; } } @@ -36,7 +36,7 @@ bool HAL_ESP32::MountSDCard() ESP_LOGE(TAG, "Card mount failed"); } ReleaseVSPIMutex(); - } + } return result; } @@ -280,11 +280,21 @@ void HAL_ESP32::Led(uint8_t bits) WriteTCA9534APWROutputState(); } -void HAL_ESP32::ConfigureCAN() +void HAL_ESP32::ConfigureCAN(uint16_t canbusbaudrate) const { // Initialize configuration structures using macro initializers twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(gpio_num_t::GPIO_NUM_16, gpio_num_t::GPIO_NUM_17, TWAI_MODE_NORMAL); - twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); + + twai_timing_config_t t_config; + if (canbusbaudrate == 250) + { + t_config = TWAI_TIMING_CONFIG_250KBITS(); + } + else + { + //Default 500K rate + t_config = TWAI_TIMING_CONFIG_500KBITS(); + } // Filter out all messages except 0x305 and 0x307 // https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/can.html diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 680246aa..2f9a649e 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -3759,9 +3759,6 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", InitializeNVS(); - // Switch CAN chip TJA1051T/3 ON - hal.CANBUSEnable(true); - hal.ConfigureCAN(); if (!LittleFS.begin(false)) { @@ -3843,6 +3840,10 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", rules.setChargingMode(ChargingMode::standard); + // Switch CAN chip TJA1051T/3 ON + hal.CANBUSEnable(true); + hal.ConfigureCAN(mysettings.canbusbaud); + // Serial pins IO2/IO32 SERIAL_DATA.begin(mysettings.baudRate, SERIAL_8N1, 2, 32); // Serial for comms to modules diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 482e7f5f..7c3753f1 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -41,6 +41,7 @@ static const char influxdb_serverurl_JSONKEY[] = "url"; static const char influxdb_loggingFreqSeconds_JSONKEY[] = "logfreq"; static const char canbusprotocol_JSONKEY[] = "canbusprotocol"; static const char canbusinverter_JSONKEY[] = "canbusinverter"; +static const char canbusbaud_JSONKEY[] = "canbusbaud"; static const char nominalbatcap_JSONKEY[] = "nominalbatcap"; static const char chargevolt_JSONKEY[] = "chargevolt"; static const char chargecurrent_JSONKEY[] = "chargecurrent"; @@ -117,6 +118,7 @@ static const char rs485parity_NVSKEY[] = "485parity"; static const char rs485stopbits_NVSKEY[] = "485stopbits"; static const char canbusprotocol_NVSKEY[] = "canbusprotocol"; static const char canbusinverter_NVSKEY[] = "canbusinverter"; +static const char canbusbaud_NVSKEY[] = "canbusbaud"; static const char nominalbatcap_NVSKEY[] = "nominalbatcap"; static const char chargevolt_NVSKEY[] = "cha_volt"; static const char chargecurrent_NVSKEY[] = "cha_current"; @@ -367,7 +369,8 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE_UINT8(rs485parity); MACRO_NVSWRITE_UINT8(rs485stopbits); MACRO_NVSWRITE_UINT8(canbusprotocol); - MACRO_NVSWRITE_UINT8(canbusinverter); + MACRO_NVSWRITE(canbusinverter); + MACRO_NVSWRITE_UINT8(canbusbaud); MACRO_NVSWRITE(currentMonitoring_shuntmv); MACRO_NVSWRITE(currentMonitoring_shuntmaxcur); @@ -508,6 +511,7 @@ void LoadConfiguration(diybms_eeprom_settings *settings) MACRO_NVSREAD_UINT8(rs485stopbits); MACRO_NVSREAD_UINT8(canbusprotocol); MACRO_NVSREAD_UINT8(canbusinverter); + MACRO_NVSREAD(canbusbaud); MACRO_NVSREAD(nominalbatcap); MACRO_NVSREAD(chargevolt); MACRO_NVSREAD(chargecurrent); @@ -585,6 +589,7 @@ void DefaultConfiguration(diybms_eeprom_settings *_myset) _myset->canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; _myset->canbusinverter = CanBusInverter::INVERTER_GENERIC; + _myset->canbusbaud=500; _myset->nominalbatcap = 280; // Scale 1 _myset->chargevolt = 565; // Scale 0.1 _myset->chargecurrent = 650; // Scale 0.1 @@ -1049,6 +1054,7 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[canbusprotocol_JSONKEY] = (uint8_t)settings->canbusprotocol; root[canbusinverter_JSONKEY] = (uint8_t)settings->canbusinverter; + root[canbusbaud_JSONKEY] = settings->canbusbaud; root[nominalbatcap_JSONKEY] = settings->nominalbatcap; root[chargevolt_JSONKEY] = settings->chargevolt; @@ -1145,6 +1151,7 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) settings->canbusprotocol = (CanBusProtocolEmulation)root[canbusprotocol_JSONKEY]; settings->canbusinverter = (CanBusInverter)root[canbusinverter_JSONKEY]; + settings->canbusbaud = root[canbusbaud_JSONKEY]; settings->nominalbatcap = root[nominalbatcap_JSONKEY]; settings->chargevolt = root[chargevolt_JSONKEY]; settings->chargecurrent = root[chargecurrent_JSONKEY]; diff --git a/ESPController/src/webserver_json_post.cpp b/ESPController/src/webserver_json_post.cpp index 53701d66..6747a351 100644 --- a/ESPController/src/webserver_json_post.cpp +++ b/ESPController/src/webserver_json_post.cpp @@ -478,6 +478,7 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) // Field not found/invalid, so disable mysettings.canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; mysettings.canbusinverter = CanBusInverter::INVERTER_GENERIC; + mysettings.canbusbaud = 500; } // Default value @@ -487,6 +488,8 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) mysettings.canbusinverter = (CanBusInverter)temp; } + GetKeyValue(httpbuf, "canbusbaud", &mysettings.canbusbaud, urlEncoded); + GetKeyValue(httpbuf, "nominalbatcap", &mysettings.nominalbatcap, urlEncoded); GetKeyValue(httpbuf, "cellminmv", &mysettings.cellminmv, urlEncoded); GetKeyValue(httpbuf, "cellmaxmv", &mysettings.cellmaxmv, urlEncoded); diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index 6670b9c7..2f50fafb 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -600,6 +600,7 @@ esp_err_t content_handler_chargeconfig(httpd_req_t *req) settings["canbusprotocol"] = mysettings.canbusprotocol; settings["canbusinverter"] = mysettings.canbusinverter; + settings["canbusbaud"] = mysettings.canbusbaud; settings["nominalbatcap"] = mysettings.nominalbatcap; settings["chargevolt"] = mysettings.chargevolt; settings["chargecurrent"] = mysettings.chargecurrent; diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index 3c3f2c85..7904951f 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -965,14 +965,13 @@

Charge/Discharge configuration

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

Temperature control utilises the external temperature sensors on the diyBMS modules. This is very useful for LIFEPO4 cells which cannot be charged when below 0°C.

Current shunt/monitor is required for reliable integration with external inverter/chargers.

-
@@ -992,6 +991,14 @@

Charge/Discharge configuration

+
+ + +
+
@@ -1116,10 +1123,6 @@

Charge/Discharge configuration

Warning Incorrect use of these settings could destroy your battery and cause harm!

-

- Warning - CANBUS BMS integration is currently at an EXPERIMENTAL stage, please report issues. -

Remember to install terminator resistors at both ends of CAN connection. On the controller, jumper JP1 can be soldered closed for this purpose. diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index 5d19bc5c..1f8a9deb 100644 --- a/ESPController/web_src/pagecode.js +++ b/ESPController/web_src/pagecode.js @@ -1983,6 +1983,7 @@ $(function () { $("#canbusprotocol").val(data.chargeconfig.canbusprotocol); $("#canbusinverter").val(data.chargeconfig.canbusinverter); + $("#canbusbaud").val(data.chargeconfig.canbusbaud); $("#nominalbatcap").val(data.chargeconfig.nominalbatcap); $("#chargevolt").val((data.chargeconfig.chargevolt / 10.0).toFixed(1)); From 178a6118ed2ee8b7bb2439fae70b9c551c3d5ac5 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:01:58 +0000 Subject: [PATCH 10/27] Update settings.cpp --- ESPController/src/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 7c3753f1..103e53f7 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -370,7 +370,7 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE_UINT8(rs485stopbits); MACRO_NVSWRITE_UINT8(canbusprotocol); MACRO_NVSWRITE(canbusinverter); - MACRO_NVSWRITE_UINT8(canbusbaud); + MACRO_NVSWRITE(canbusbaud); MACRO_NVSWRITE(currentMonitoring_shuntmv); MACRO_NVSWRITE(currentMonitoring_shuntmaxcur); From e6b128e23e77bb55fc5eea8b56bf6da3368cd1f6 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:02:12 +0000 Subject: [PATCH 11/27] Output RSSI to console log on WIFI connection --- ESPController/src/main.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 2f9a649e..52ac97d8 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -1606,6 +1606,11 @@ static void event_handler(void *, esp_event_base_t event_base, { // We have joined the access point - now waiting for IP address IP_EVENT_STA_GOT_IP wifi_ap_connect_retry_num = 0; + + wifi_ap_record_t ap; + esp_wifi_sta_get_ap_info(&ap); + + ESP_LOGI(TAG, "WIFI_EVENT_STA_CONNECTED channel=%u, rssi=%i", ap.primary, ap.rssi); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -1620,7 +1625,7 @@ static void event_handler(void *, esp_event_base_t event_base, if (wifi_ap_connect_retry_num < 25) { - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); ESP_LOGI(TAG, "WIFI connect quick retry %i", wifi_ap_connect_retry_num); } else @@ -1634,11 +1639,7 @@ static void event_handler(void *, esp_event_base_t event_base, ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); ShutdownAllNetworkServices(); - // ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); wake_up_tft(true); - - // Try and reconnect - // esp_wifi_connect(); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { @@ -3759,7 +3760,6 @@ ESP32 Chip model = %u, Rev %u, Cores=%u, Features=%u)", InitializeNVS(); - if (!LittleFS.begin(false)) { ESP_LOGE(TAG, "LittleFS mount failed, did you upload file system image?"); From 3306f522294bbc25693bc4fde5873544855db57a Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:55:42 +0000 Subject: [PATCH 12/27] Replaced Steinhart calculation with LUT fixes #252 --- STM32All-In-One/include/packet_processor.h | 1 - STM32All-In-One/lib/Steinhart/Steinhart.cpp | 27 ---------- STM32All-In-One/lib/Steinhart/Steinhart.h | 11 ----- STM32All-In-One/src/main.cpp | 55 +++++++++++++++++++-- 4 files changed, 51 insertions(+), 43 deletions(-) delete mode 100644 STM32All-In-One/lib/Steinhart/Steinhart.cpp delete mode 100644 STM32All-In-One/lib/Steinhart/Steinhart.h diff --git a/STM32All-In-One/include/packet_processor.h b/STM32All-In-One/include/packet_processor.h index 50d0b317..af29d1cf 100644 --- a/STM32All-In-One/include/packet_processor.h +++ b/STM32All-In-One/include/packet_processor.h @@ -7,7 +7,6 @@ #error You need to specify the DIYBMSMODULEVERSION define #endif -#include "Steinhart.h" #include "defines.h" #include "crc16.h" #include "cell.h" diff --git a/STM32All-In-One/lib/Steinhart/Steinhart.cpp b/STM32All-In-One/lib/Steinhart/Steinhart.cpp deleted file mode 100644 index 654c0b18..00000000 --- a/STM32All-In-One/lib/Steinhart/Steinhart.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "Steinhart.h" - - -int16_t Steinhart::ThermistorToCelcius(uint16_t BCOEFFICIENT, uint16_t RawADC, float ADCScaleMax) { - -//We can calculate the Steinhart-Hart Thermistor Equation based on the B Coefficient of the thermistor -// at 25 degrees C rating -#define NOMINAL_TEMPERATURE 25 - -//If we get zero its likely the ADC is connected to ground - if (RawADC>0){ - //https://arduinodiy.wordpress.com/2015/11/10/measuring-temperature-with-ntc-the-steinhart-hart-formula/ - - float steinhart = (ADCScaleMax/(float)RawADC - 1.0F); - - steinhart = log(steinhart); // ln(R/Ro) - steinhart /= BCOEFFICIENT; // 1/B * ln(R/Ro) - steinhart += 1.0F / (NOMINAL_TEMPERATURE + 273.15F); // + (1/To) - steinhart = 1.0F / steinhart; // Invert - steinhart -= 273.15F; // convert to oC - - return (int16_t)steinhart; - } - - return (int16_t)-999; -} - diff --git a/STM32All-In-One/lib/Steinhart/Steinhart.h b/STM32All-In-One/lib/Steinhart/Steinhart.h deleted file mode 100644 index 34302c94..00000000 --- a/STM32All-In-One/lib/Steinhart/Steinhart.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef Steinhart_H // include guard -#define Steinhart_H - -#include - -class Steinhart { - public: - static int16_t ThermistorToCelcius(uint16_t BCOEFFICIENT, uint16_t RawADC, float ADCScaleMax); - -}; -#endif diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 11c72f98..8a8be09f 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -530,7 +530,6 @@ void setup() ErrorFlashes(3); } - //ParasiticCapacitanceChargeInjectionErrorCalibration(number_of_active_cells, celldata); BufferAmplifierOffsetCalibration(); configureModules(); @@ -626,11 +625,39 @@ uint32_t MAX14921Command(uint8_t b1, uint8_t b2, uint8_t b3) return reply; } -/// @brief Read external temperature sensors, connected to STM32 +// NTC THERMISTOR CMFB103F3950FANT +// ADC MAPPING TO TEMPERATURE (DEGREES C) +constexpr std::array thermistorTable = + { + -40, -40, -40, -40, -40, -40, -40, -40, -40, -39, -37, -36, -34, -33, -31, -30, -29, + -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -19, -18, -17, -16, -16, -15, -14, + -14, -13, -13, -12, -11, -11, -10, -10, -9, -9, -8, -8, -7, -6, -6, -5, -5, -4, -4, + -4, -3, -3, -2, -2, -1, -1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 7, 7, + 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 14, 14, 14, 15, 15, 15, + 16, 16, 16, 17, 17, 17, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, + 24, 24, 24, 24, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 30, 30, 30, 31, 31, + 31, 32, 32, 33, 33, 33, 34, 34, 35, 35, 35, 36, 36, 37, 37, 37, 38, 38, 39, 39, 39, 40, + 40, 41, 41, 42, 42, 42, 43, 43, 44, 44, 45, 45, 46, 46, 47, 47, 48, 48, 49, 49, 50, 50, + 51, 51, 52, 52, 53, 54, 54, 55, 55, 56, 57, 57, 58, 59, 59, 60, 61, 61, 62, 63, 63, 64, + 65, 66, 67, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 85, 86, 88, + 89, 91, 93, 95, 97, 99, 101, 104, 107, 110, 113, 117, 120, 120, 120, 120, 120, 120, 120, + 120, 120, 120}; + +/// @brief Read external temperature sensors, connected to STM32 (12 bit ADC) /// @return Temperature in celcius. A value of -999 means not-connected (shorted to ground) int16_t ReadThermistor(uint32_t pin) { - return Steinhart::ThermistorToCelcius(EXT_BCOEFFICIENT, (uint16_t)analogRead(pin), 4095); + auto value = (uint16_t)analogRead(pin); + + // Ignore extreme ranges - assume not connected + if (value < 15 || value > 4000) + { + return (int16_t)-999; + } + + // Scale 12 bit to 8 bit (TODO: we can probably just change the resolution of analogRead instead) + auto byte_value = (uint8_t)map(value, 0, 4096, 0, 255); + return thermistorTable.at(byte_value); } /// @brief Read external temperature sensors (connected to board J11/J12/J13 sockets) @@ -656,10 +683,20 @@ void DisableThermistorPower() /// @return Celcius temperature reading int16_t ReadTH() { + //14 bit reply... auto value = DecimateValue(takeRawMCP33151ADCReading()); // THx is connected to 3.3V max via 10K resistors - scale 3.3V to 4.096V reference // 3.600 is used as temperature appears to be over read by 2 celcius - return Steinhart::ThermistorToCelcius(INT_BCOEFFICIENT, value, (3.600F / ((float)DIYBMSREFMILLIVOLT / 1000.0F)) * 4095.0F); + + // Ignore extreme ranges - assume not connected + if (value == 0) + { + return (int16_t)-999; + } + + // Scale to 8 bit + auto byte_value = (uint8_t)map(value, 0,long((3600.0F / ((float)DIYBMSREFMILLIVOLT)) * 4095.0F), 0, 255); + return thermistorTable.at(byte_value); } /// @brief internal temperature sensors (on board) @@ -689,6 +726,16 @@ void TakeOnboardInternalTempMeasurements(CellData &cd) // Cell 0 internal temperature is the on-board (PCB) sensor (marked TH6) cd.at(0).setInternalTemperature(t3); + + if (!PP.BalanceBoardInstalled) + { + // Populate all cells with an internal temperature to prevent controller error messages + for (size_t i = 0; i < 16; i += 2) + { + cd.at(i).setInternalTemperature(t3); + cd.at(i + 1).setInternalTemperature(t3); + } + } } [[noreturn]] void ErrorFlashes(int number) From 89c424cf8f17478f6b3d3bc95507a5e90c2c52f7 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:55:59 +0000 Subject: [PATCH 13/27] Reduce logging level on VICTRON messages --- ESPController/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 52ac97d8..19ac9bc5 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -2654,7 +2654,7 @@ void send_canbus_message(uint32_t identifier, const uint8_t *buffer, const uint8 if (result == ESP_OK) { // Everything normal/good - ESP_LOGD(TAG, "Sent CAN message 0x%x", identifier); + // ESP_LOGD(TAG, "Sent CAN message 0x%x", identifier); // ESP_LOG_BUFFER_HEX_LEVEL(TAG, &message, sizeof(twai_message_t), esp_log_level_t::ESP_LOG_DEBUG); canbus_messages_sent++; return; @@ -3639,7 +3639,7 @@ const std::array log_levels = {.tag = "diybms-rules", .level = ESP_LOG_INFO}, {.tag = "diybms-softap", .level = ESP_LOG_INFO}, {.tag = "diybms-tft", .level = ESP_LOG_INFO}, - {.tag = "diybms-victron", .level = ESP_LOG_DEBUG}, + {.tag = "diybms-victron", .level = ESP_LOG_INFO}, {.tag = "diybms-webfuncs", .level = ESP_LOG_INFO}, {.tag = "diybms-webpost", .level = ESP_LOG_INFO}, {.tag = "diybms-webreq", .level = ESP_LOG_INFO}, From 19634f20c6a75ca09494c4092c0db5edde6e65f7 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:20:09 +0000 Subject: [PATCH 14/27] Implement auto baud rate detection V490 #253 On power up V490 module will scan through the allowed baud rates, starting with the fastest and drop down until it detects a valid communication packet from the controller. Once this is received, the baud rate is locked until next power cycle. --- STM32All-In-One/platformio.ini | 30 ++------------------ STM32All-In-One/src/main.cpp | 52 +++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/STM32All-In-One/platformio.ini b/STM32All-In-One/platformio.ini index 0bf514b5..a369aec8 100644 --- a/STM32All-In-One/platformio.ini +++ b/STM32All-In-One/platformio.ini @@ -9,7 +9,7 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = V490_10K, V490_5K, V490_5K_VREF4500, V490_10K_VREF4500 +default_envs = V490_AUTOBAUD_VREF4096, V490_AUTOBAUD_VREF4500 [env] platform = platformio/ststm32@^17.0.0 @@ -34,49 +34,25 @@ debug_tool = stlink upload_protocol = stlink ;upload_protocol = serial -[env:V490_10K] +[env:V490_AUTOBAUD_VREF4096] build_flags= -DDIYBMSMODULEVERSION=490 -DINT_BCOEFFICIENT=3950 -DEXT_BCOEFFICIENT=3950 -DLOAD_RESISTANCE=18.0 - -DDIYBMSBAUD=10000 -DDIYBMSREFMILLIVOLT=4096 -DADC_SAMPLINGTIME=ADC_SAMPLETIME_239CYCLES_5 -DSERIAL_RX_BUFFER_SIZE=64 -DSERIAL_TX_BUFFER_SIZE=64 -[env:V490_5K] -build_flags= - -DDIYBMSMODULEVERSION=490 - -DINT_BCOEFFICIENT=3950 - -DEXT_BCOEFFICIENT=3950 - -DLOAD_RESISTANCE=18.0 - -DDIYBMSBAUD=5000 - -DDIYBMSREFMILLIVOLT=4096 - -DADC_SAMPLINGTIME=ADC_SAMPLETIME_239CYCLES_5 - -DSERIAL_RX_BUFFER_SIZE=64 - -DSERIAL_TX_BUFFER_SIZE=64 -[env:V490_5K_VREF4500] +[env:V490_AUTOBAUD_VREF4500] build_flags=-DDIYBMSMODULEVERSION=490 -DINT_BCOEFFICIENT=3950 -DEXT_BCOEFFICIENT=3950 -DLOAD_RESISTANCE=18.0 - -DDIYBMSBAUD=5000 -DDIYBMSREFMILLIVOLT=4500 -DADC_SAMPLINGTIME=ADC_SAMPLETIME_239CYCLES_5 -DSERIAL_RX_BUFFER_SIZE=64 -DSERIAL_TX_BUFFER_SIZE=64 -[env:V490_10K_VREF4500] -build_flags= - -DDIYBMSMODULEVERSION=490 - -DINT_BCOEFFICIENT=3950 - -DEXT_BCOEFFICIENT=3950 - -DLOAD_RESISTANCE=18.0 - -DDIYBMSBAUD=10000 - -DDIYBMSREFMILLIVOLT=4500 - -DADC_SAMPLINGTIME=ADC_SAMPLETIME_239CYCLES_5 - -DSERIAL_RX_BUFFER_SIZE=64 - -DSERIAL_TX_BUFFER_SIZE=64 diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 8a8be09f..0ee7bb1f 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -51,10 +51,6 @@ extern "C" #include #include "packet_processor.h" -#if !defined(DIYBMSBAUD) -#error Expected DIYBMSBAUD define -#endif - #if !defined(HAL_UART_MODULE_ENABLED) #error Expected HAL_UART_MODULE_ENABLED #endif @@ -110,6 +106,16 @@ PacketProcessor PP; /// @brief Bitmap of which cells are balancing (maps into MAX chip register) uint16_t cell_balancing = 0; +/// @brief Is serial baud rate scanning active? +bool serialBaudScanning = true; +/// @brief Index to current SerialBaudRates array +int8_t serialBaudIndex = 0; +/// @brief Wait X iterations of loop() before swapping baud rates +int8_t serialBaudCountDown = 15; + +// Baud rates that we can use +constexpr std::array SerialBaudRates = {10000, 9600, 5000, 2400}; + const uint32_t LEVEL_SHIFTING_DELAY_MAX = 50; // μs const uint32_t T_SETTLING_TIME_MAX = 10; // μs @@ -506,7 +512,8 @@ void setup() // Set up data handler Serial1.setTx(PA_9); Serial1.setRx(PA_10); - Serial1.begin(DIYBMSBAUD, SERIAL_8N1); + // Start using the first serial baud rate (10k) + Serial1.begin(SerialBaudRates.at(serialBaudIndex), SERIAL_8N1); myPacketSerial.begin(&Serial1, &onPacketReceived, sizeof(PacketStruct), SerialPacketReceiveBuffer, sizeof(SerialPacketReceiveBuffer)); SPI.begin(); @@ -683,7 +690,7 @@ void DisableThermistorPower() /// @return Celcius temperature reading int16_t ReadTH() { - //14 bit reply... + // 14 bit reply... auto value = DecimateValue(takeRawMCP33151ADCReading()); // THx is connected to 3.3V max via 10K resistors - scale 3.3V to 4.096V reference // 3.600 is used as temperature appears to be over read by 2 celcius @@ -695,7 +702,7 @@ int16_t ReadTH() } // Scale to 8 bit - auto byte_value = (uint8_t)map(value, 0,long((3600.0F / ((float)DIYBMSREFMILLIVOLT)) * 4095.0F), 0, 255); + auto byte_value = (uint8_t)map(value, 0, long((3600.0F / ((float)DIYBMSREFMILLIVOLT)) * 4095.0F), 0, 255); return thermistorTable.at(byte_value); } @@ -1001,6 +1008,11 @@ void loop() waitbeforebalance--; } + if (serialBaudScanning) + { + NotificationLedOn(); + } + // Service the serial port/queue for (size_t i = 0; i < 300; i++) { @@ -1011,6 +1023,32 @@ void loop() delay(1); } + if (serialBaudScanning) + { + NotificationLedOff(); + serialBaudCountDown--; + + if (PP.getPacketReceivedCounter() > 0) + { + // We have found our baud rate - and processed at least 1 packet successfully, so stop scanning + serialBaudScanning = false; + } + else if (serialBaudCountDown <= 0) + { + // If we got this far, we have not yet found/received a valid packet of serial data, so try another baud rate + serialBaudCountDown = 15; + serialBaudIndex++; + + if (serialBaudIndex >= SerialBaudRates.size()) + { + serialBaudIndex = 0; + } + + Serial1.end(); + Serial1.begin(SerialBaudRates.at(serialBaudIndex), SERIAL_8N1); + } + } + // Every so often, we should call this to calibrate the op-amp as it changes in ambient temperature (takes 8ms to complete) if (PP.getPacketReceivedCounter() % 8192 == 0) { From bd7afba5e8f9d073bec7ae99c2348efeb3bda56d Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:45:40 +0000 Subject: [PATCH 15/27] Use ENQUEUE and QOS=0 for MQTT messages --- ESPController/src/mqtt.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ESPController/src/mqtt.cpp b/ESPController/src/mqtt.cpp index 52aafa4b..14696b6d 100644 --- a/ESPController/src/mqtt.cpp +++ b/ESPController/src/mqtt.cpp @@ -52,19 +52,24 @@ bool checkMQTTReady() /// @param topic Topic to publish the message to. /// @param payload Message payload to be published. /// @param clear_payload When true @param payload will be cleared upon sending. -static inline void publish_message(std::string &topic, std::string &payload, bool clear_payload = true) +static inline void publish_message(std::string const &topic, std::string &payload, bool clear_payload = true) { - static constexpr int MQTT_QUALITY_OF_SERVICE = 1; + static constexpr int MQTT_QUALITY_OF_SERVICE = 0; static constexpr int MQTT_RETAIN_MESSAGE = 0; if (mqtt_client != nullptr && mqttClient_connected) { - int id = esp_mqtt_client_publish( - mqtt_client, topic.c_str(), payload.c_str(), payload.length(), - MQTT_QUALITY_OF_SERVICE, MQTT_RETAIN_MESSAGE); + int id = esp_mqtt_client_enqueue(mqtt_client, topic.c_str(), + payload.c_str(), payload.length(), + MQTT_QUALITY_OF_SERVICE, MQTT_RETAIN_MESSAGE, true); + + if (id < 0) + { + ESP_LOGE(TAG, "Topic:%s, failed publish", topic.c_str()); + } ESP_LOGD(TAG, "Topic:%s, ID:%d, Length:%i", topic.c_str(), id, payload.length()); - ESP_LOGV(TAG, "Payload:%s", payload.c_str()); + // ESP_LOGV(TAG, "Payload:%s", payload.c_str()); } if (clear_payload) @@ -129,7 +134,7 @@ void stopMqtt() ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_destroy(mqtt_client)); mqtt_client = nullptr; - //Reset stats + // Reset stats mqtt_error_connection_count = 0; mqtt_error_transport_count = 0; mqtt_connection_count = 0; @@ -154,11 +159,11 @@ void connectToMqtt() .uri = mysettings.mqtt_uri, .username = mysettings.mqtt_username, .password = mysettings.mqtt_password, - //Reconnect if there server has a problem (or wrong IP/password etc.) + // Reconnect if there server has a problem (or wrong IP/password etc.) .disable_auto_reconnect = false, - //30 seconds + // 30 seconds .reconnect_timeout_ms = 30000, - //3 seconds + // 3 seconds .network_timeout_ms = 3000}; mqtt_client = esp_mqtt_client_init(&mqtt_cfg); From 58ff87158a2e2daff8696b2121b66bd50e489936 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:50:04 +0000 Subject: [PATCH 16/27] Add "basic reporting" cell functionality for MQTT messages --- ESPController/include/defines.h | 2 + ESPController/src/mqtt.cpp | 52 +++++++++++-------- ESPController/src/settings.cpp | 7 +++ ESPController/src/webserver_json_post.cpp | 3 ++ ESPController/src/webserver_json_requests.cpp | 14 ++--- ESPController/web_src/default.htm | 11 ++-- ESPController/web_src/pagecode.js | 1 + 7 files changed, 57 insertions(+), 33 deletions(-) diff --git a/ESPController/include/defines.h b/ESPController/include/defines.h index 36f8a755..7d188d1b 100644 --- a/ESPController/include/defines.h +++ b/ESPController/include/defines.h @@ -233,6 +233,8 @@ struct diybms_eeprom_settings // NOTE this array is subject to buffer overflow vulnerabilities! bool mqtt_enabled; + // Only report basic cell data (voltage and temperture) over MQTT + bool mqtt_basic_cell_reporting; char mqtt_uri[128 + 1]; char mqtt_topic[32 + 1]; char mqtt_username[32 + 1]; diff --git a/ESPController/src/mqtt.cpp b/ESPController/src/mqtt.cpp index 14696b6d..b555372b 100644 --- a/ESPController/src/mqtt.cpp +++ b/ESPController/src/mqtt.cpp @@ -161,10 +161,12 @@ void connectToMqtt() .password = mysettings.mqtt_password, // Reconnect if there server has a problem (or wrong IP/password etc.) .disable_auto_reconnect = false, + .buffer_size = 512, // 30 seconds .reconnect_timeout_ms = 30000, - // 3 seconds - .network_timeout_ms = 3000}; + .out_buffer_size = 2048, + // 4 seconds + .network_timeout_ms = 4000}; mqtt_client = esp_mqtt_client_init(&mqtt_cfg); @@ -237,14 +239,15 @@ void GeneralStatusPayload(const PacketRequestGenerator *prg, const PacketReceive void BankLevelInformation(const Rules *rules) { + std::string bank_status; + bank_status.reserve(64); // Output bank level information (just voltage for now) for (int8_t bank = 0; bank < mysettings.totalNumberOfBanks; bank++) { - ESP_LOGI(TAG, "Bank(%d) status payload", bank); - std::string bank_status; - bank_status.reserve(128); + ESP_LOGI(TAG, "Bank %d status payload", bank); + bank_status.clear(); bank_status.append("{\"voltage\":") - .append(float_to_string(rules->bankvoltage.at(bank) / 1000.0f)) + .append(float_to_string((float)(rules->bankvoltage.at(bank)) / 1000.0f)) .append(",\"range\":") .append(std::to_string(rules->VoltageRangeInBank(bank))) .append("}"); @@ -262,7 +265,10 @@ void RuleStatus(const Rules *rules) rule_status.append("{"); for (uint8_t i = 0; i < RELAY_RULES; i++) { - rule_status.append("\"").append(std::to_string(i)).append("\":").append(std::to_string(rules->ruleOutcome((Rule)i) ? 1 : 0)); + rule_status.append("\"") + .append(std::to_string(i)) + .append("\":") + .append(std::to_string(rules->ruleOutcome((Rule)i) ? 1 : 0)); if (i < (RELAY_RULES - 1)) { rule_status.append(","); @@ -282,7 +288,11 @@ void OutputStatus(const RelayState *previousRelayState) relay_status.append("{"); for (uint8_t i = 0; i < RELAY_TOTAL; i++) { - relay_status.append("\"").append(std::to_string(i)).append("\":").append(std::to_string((previousRelayState[i] == RelayState::RELAY_ON) ? 1 : 0)); + relay_status.append("\"") + .append(std::to_string(i)) + .append("\":") + .append(std::to_string((previousRelayState[i] == RelayState::RELAY_ON) ? 1 : 0)); + if (i < (RELAY_TOTAL - 1)) { relay_status.append(","); @@ -338,31 +348,29 @@ void MQTTCellData() ESP_LOGI(TAG, "MQTT Payload for cell data"); + std::string status; + status.reserve(128); + while (i < TotalNumberOfCells() && counter < MAX_MODULES_PER_ITERATION) { // Only send valid module data if (cmi[i].valid) { - std::string status; - std::string topic = mysettings.mqtt_topic; - status.reserve(128); - uint8_t bank = i / mysettings.totalNumberOfSeriesModules; uint8_t m = i - (bank * mysettings.totalNumberOfSeriesModules); - status.append("{\"voltage\":").append(float_to_string(cmi[i].voltagemV / 1000.0f)); - status.append(",\"vMax\":").append(float_to_string(cmi[i].voltagemVMax / 1000.0f)); - status.append(",\"vMin\":").append(float_to_string(cmi[i].voltagemVMin / 1000.0f)); - status.append(",\"inttemp\":").append(std::to_string(cmi[i].internalTemp)); - status.append(",\"exttemp\":").append(std::to_string(cmi[i].externalTemp)); - status.append(",\"bypass\":").append(std::to_string(cmi[i].inBypass ? 1 : 0)); - status.append(",\"PWM\":").append(std::to_string((int)((float)cmi[i].PWMValue / (float)255.0 * 100))); - status.append(",\"bypassT\":").append(std::to_string(cmi[i].bypassOverTemp ? 1 : 0)); - status.append(",\"bpc\":").append(std::to_string(cmi[i].badPacketCount)); - status.append(",\"mAh\":").append(std::to_string(cmi[i].BalanceCurrentCount)); + status.clear(); + status.append("{\"voltage\":").append(float_to_string(cmi[i].voltagemV / 1000.0f)).append(",\"exttemp\":").append(std::to_string(cmi[i].externalTemp)); + + if (mysettings.mqtt_basic_cell_reporting == false) + { + status.append(",\"vMax\":").append(float_to_string(cmi[i].voltagemVMax / 1000.0f)).append(",\"vMin\":").append(float_to_string(cmi[i].voltagemVMin / 1000.0f)).append(",\"inttemp\":").append(std::to_string(cmi[i].internalTemp)).append(",\"bypass\":").append(std::to_string(cmi[i].inBypass ? 1 : 0)).append(",\"PWM\":").append(std::to_string((int)((float)cmi[i].PWMValue / (float)255.0 * 100))).append(",\"bypassT\":").append(std::to_string(cmi[i].bypassOverTemp ? 1 : 0)).append(",\"bpc\":").append(std::to_string(cmi[i].badPacketCount)).append(",\"mAh\":").append(std::to_string(cmi[i].BalanceCurrentCount)); + } + status.append("}"); + std::string topic = mysettings.mqtt_topic; topic.append("/").append(std::to_string(bank)).append("/").append(std::to_string(m)); publish_message(topic, status); } diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 103e53f7..6b802adc 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -29,6 +29,7 @@ static const char rs485parity_JSONKEY[] = "rs485parity"; static const char rs485stopbits_JSONKEY[] = "rs485stopbits"; static const char language_JSONKEY[] = "language"; static const char mqtt_enabled_JSONKEY[] = "enabled"; +static const char mqtt_basic_cell_reporting_JSONKEY[] = "basiccellrpt"; static const char mqtt_uri_JSONKEY[] = "uri"; static const char mqtt_topic_JSONKEY[] = "topic"; static const char mqtt_username_JSONKEY[] = "username"; @@ -142,6 +143,7 @@ static const char dynamiccharge_NVSKEY[] = "dynamiccharge"; static const char preventcharging_NVSKEY[] = "preventchar"; static const char preventdischarge_NVSKEY[] = "preventdis"; static const char mqtt_enabled_NVSKEY[] = "mqttenable"; +static const char mqtt_basic_cell_reporting_NVSKEY[] = "basiccellrpt"; static const char influxdb_enabled_NVSKEY[] = "infenabled"; static const char influxdb_loggingFreqSeconds_NVSKEY[] = "inflogFreq"; static const char tileconfig_NVSKEY[] = "tileconfig"; @@ -412,6 +414,7 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE(preventcharging); MACRO_NVSWRITE(preventdischarge); MACRO_NVSWRITE(mqtt_enabled); + MACRO_NVSWRITE(mqtt_basic_cell_reporting); MACRO_NVSWRITE(influxdb_enabled); MACRO_NVSWRITE(influxdb_loggingFreqSeconds); @@ -537,6 +540,7 @@ void LoadConfiguration(diybms_eeprom_settings *settings) MACRO_NVSREAD(preventdischarge); MACRO_NVSREAD(mqtt_enabled); + MACRO_NVSREAD(mqtt_basic_cell_reporting); MACRO_NVSREAD(influxdb_enabled); MACRO_NVSREAD(influxdb_loggingFreqSeconds); @@ -586,6 +590,7 @@ void DefaultConfiguration(diybms_eeprom_settings *_myset) // EEPROM settings are invalid so default configuration _myset->mqtt_enabled = false; + _myset->mqtt_basic_cell_reporting = false; _myset->canbusprotocol = CanBusProtocolEmulation::CANBUS_DISABLED; _myset->canbusinverter = CanBusInverter::INVERTER_GENERIC; @@ -1001,6 +1006,7 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin JsonObject mqtt = root.createNestedObject("mqtt"); 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; mqtt[mqtt_topic_JSONKEY] = settings->mqtt_topic; mqtt[mqtt_username_JSONKEY] = settings->mqtt_username; @@ -1179,6 +1185,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]; 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)); diff --git a/ESPController/src/webserver_json_post.cpp b/ESPController/src/webserver_json_post.cpp index 6747a351..2b89e22f 100644 --- a/ESPController/src/webserver_json_post.cpp +++ b/ESPController/src/webserver_json_post.cpp @@ -79,6 +79,7 @@ esp_err_t post_savemqtt_json_handler(httpd_req_t *req, bool urlEncoded) { // Default to off mysettings.mqtt_enabled = false; + mysettings.mqtt_basic_cell_reporting=false; // Username and password are optional and may not be HTTP posted from web browser memset(mysettings.mqtt_username, 0, sizeof(mysettings.mqtt_username)); @@ -86,6 +87,8 @@ esp_err_t post_savemqtt_json_handler(httpd_req_t *req, bool urlEncoded) GetKeyValue(httpbuf, "mqttEnabled", &mysettings.mqtt_enabled, urlEncoded); + GetKeyValue(httpbuf, "mqttBasicReporting", &mysettings.mqtt_basic_cell_reporting, urlEncoded); + GetTextFromKeyValue(httpbuf, "mqttTopic", mysettings.mqtt_topic, sizeof(mysettings.mqtt_topic), urlEncoded); GetTextFromKeyValue(httpbuf, "mqttUri", mysettings.mqtt_uri, sizeof(mysettings.mqtt_uri), urlEncoded); diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index 2f50fafb..afa1013d 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -172,7 +172,7 @@ int fileSystemListDirectory(httpd_req_t *r, char *buffer, size_t bufferLen, fs:: { // Flush http buffer every X files to prevent overflows httpd_resp_send_chunk(r, buffer, bufferused); - bufferused=0; + bufferused = 0; } } @@ -806,16 +806,16 @@ esp_err_t content_handler_integration(httpd_req_t *req) JsonObject mqtt = root.createNestedObject("mqtt"); mqtt["enabled"] = mysettings.mqtt_enabled; + mqtt["basiccellreporting"] = mysettings.mqtt_basic_cell_reporting; mqtt["topic"] = mysettings.mqtt_topic; mqtt["uri"] = mysettings.mqtt_uri; mqtt["username"] = mysettings.mqtt_username; - - mqtt["connected"]= mqttClient_connected; - mqtt["err_conn_count"]=mqtt_error_connection_count; - mqtt["err_trans_count"]=mqtt_error_transport_count; - mqtt["conn_count"]=mqtt_connection_count; - mqtt["disc_count"]=mqtt_disconnection_count; + mqtt["connected"] = mqttClient_connected; + mqtt["err_conn_count"] = mqtt_error_connection_count; + mqtt["err_trans_count"] = mqtt_error_transport_count; + mqtt["conn_count"] = mqtt_connection_count; + mqtt["disc_count"] = mqtt_disconnection_count; // We don't output the password in the json file as this could breach security // mqtt["password"] =mysettings.mqtt_password; diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index 7904951f..574e5a3d 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -482,19 +482,22 @@

Global Settings

Integration

-

- For security, you will need to re-enter the password for the service(s) you want to enable or modify, before you - save. -

+

MQTT

+

For security, you will need to re-enter the password for the MQTT service if you want to enable or modify, before you save.

URI should be similar to mqtt://192.168.0.26:1833

+

Basic cell data option reduces the amount of MQTT data being sent over the network.

+
+ + +
diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index 1f8a9deb..bbc702a9 100644 --- a/ESPController/web_src/pagecode.js +++ b/ESPController/web_src/pagecode.js @@ -1735,6 +1735,7 @@ $(function () { function (data) { $("#mqttEnabled").prop("checked", data.mqtt.enabled); + $("#mqttBasicReporting").prop("checked", data.mqtt.basiccellreporting); $("#mqttTopic").val(data.mqtt.topic); $("#mqttUri").val(data.mqtt.uri); $("#mqttUsername").val(data.mqtt.username); From 989d47d5b440e22411f2d671da4da5732e6ea9a2 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:40:38 +0000 Subject: [PATCH 17/27] Update cell.h --- STM32All-In-One/include/cell.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/STM32All-In-One/include/cell.h b/STM32All-In-One/include/cell.h index 80c7787a..11c29678 100644 --- a/STM32All-In-One/include/cell.h +++ b/STM32All-In-One/include/cell.h @@ -140,10 +140,10 @@ class Cell } if (v > 20) { - BypassTemperatureSetPoint = v; + BypassTemperatureSetPoint = (uint8_t)v; // Set back hysteresis is set point minus 5 degrees C - BypassTemperatureHysteresis = v - 5; + BypassTemperatureHysteresis = (uint8_t)v - 5; } // Revalidate fan temperature after bypass temperature change From f51d33c00850b9a2437fc75aee614b46e89cbcc1 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:40:58 +0000 Subject: [PATCH 18/27] Improve communication speed by removing delays --- STM32All-In-One/src/main.cpp | 147 +++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 43 deletions(-) diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 0ee7bb1f..2f1941fe 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -529,7 +529,18 @@ void setup() takeRawMCP33151ADCReading(); } - number_of_active_cells = queryAFE(); + // Sometimes on power up, the chip returns 1 cell more than actually connected, so take a few + // samples and return the lowest cell count + number_of_active_cells = 255; + for (size_t i = 0; i < 4; i++) + { + auto x = queryAFE(); + + if (x < number_of_active_cells) + { + number_of_active_cells = x; + } + } // We need at least 4 cells with correct voltages for this to work if (number_of_active_cells < 4) @@ -787,7 +798,7 @@ uint16_t CalculateCellBalanceRequirement(CellData &cd, uint8_t num_cells) // Our cell voltage is OVER the voltage setpoint limit, start draining cell using bypass resistor if (!cell.IsBypassActive()) { - // We have just entered the bypass code + // We have just entered the bypass cell.StartBypass(); } @@ -850,12 +861,13 @@ void CalculateCellVoltageMinMaxAvg(CellData &cd, range = highestmV - lowestmV; } +int16_t lastRunAwayCellIndex = -1; + /// @brief Determine what (if any) cells need balancing, also controls relay output /// @param highestTemp Highest temperature of on-board sensors (balance temperature) /// @return bit pattern for which MOSFETs need enabling uint16_t DoCellBalancing(const int16_t highestTemp) { - uint16_t lowestmV; uint16_t highestmV; uint8_t highest_index; @@ -922,17 +934,76 @@ uint16_t DoCellBalancing(const int16_t highestTemp) // Should any cells require balancing - check if they have gone over the threshold cb = CalculateCellBalanceRequirement(celldata, number_of_active_cells); - // Now determine if we should balance the highest "run away" cell + // Now determine if we should balance the highest "run away" cell - only if "normal" balancing is not active // If the highest cell is above average cell voltage by X millivolts, then begin balancing until it no longer is. // Also ensure the cell voltage is above a minimum - LIFEPO4 cells need to be over 3400mV for this function to be useful. - if (highestmV > PP.getRunAwayCellMinimumVoltage() && highest_average_diff > PP.getRunAwayCellDifferential()) + if (cb == 0 && highestmV > PP.getRunAwayCellMinimumVoltage() && highest_average_diff > PP.getRunAwayCellDifferential()) { cb = cb | (uint16_t)(1U << (15 - highest_index)); + + if (celldata.at(highest_index).IsBypassActive() == false) + { + celldata.at(highest_index).StartBypass(); + lastRunAwayCellIndex = highest_index; + } + } + else + { + // Stop run away cell balancing, if it was active. + if (lastRunAwayCellIndex >= 0) + { + if (celldata.at(lastRunAwayCellIndex).IsBypassActive()) + { + celldata.at(lastRunAwayCellIndex).StopBypass(); + } + lastRunAwayCellIndex = -1; + } } return cb; } +// Checks the serial port for about 60ms and processes any requests +// auto checks for serial baud rate scanning +void ServiceSerialPort() +{ + // Service the serial port/queue + for (size_t i = 0; i < 60; i++) + { + // Call update to receive, decode and process incoming packets. + myPacketSerial.checkInputStream(); + + // Allow data to be received in buffer (delay must be AFTER) checkInputStream + delay(1); + } + + if (serialBaudScanning) + { + NotificationLedOff(); + serialBaudCountDown--; + + if (PP.getPacketReceivedCounter() > 0) + { + // We have found our baud rate - and processed at least 1 packet successfully, so stop scanning + serialBaudScanning = false; + } + else if (serialBaudCountDown <= 0) + { + // If we got this far, we have not yet found/received a valid packet of serial data, so try another baud rate + serialBaudCountDown = 15; + serialBaudIndex++; + + if (serialBaudIndex >= SerialBaudRates.size()) + { + serialBaudIndex = 0; + } + + Serial1.end(); + Serial1.begin(SerialBaudRates.at(serialBaudIndex), SERIAL_8N1); + } + } +} + void loop() { @@ -949,8 +1020,22 @@ void loop() delay(10); } - digitalWrite(SAMPLE_AFE, HIGH); // Sample cell voltages (all 16 cells at same time) - delay(60); // Wait 60ms for capacitors to fill and equalize against cell voltages (1uF caps) + digitalWrite(SAMPLE_AFE, HIGH); // Sample cell voltages (all 16 cells at same time) + + if (waitbeforebalance == 0) + { + // This also takes about 60ms, but allows request packets to be processed quicker than waiting + ServiceSerialPort(); + } + else + { + // Delay until we have been through this loop a few times (received a good packet) + // so we don't return zero voltage readings to controller + delay(60); // Wait 60ms for capacitors to fill and equalize against cell voltages (1uF caps) + } + + // delay(60); // Wait 60ms for capacitors to fill and equalize against cell voltages (1uF caps) + digitalWrite(SAMPLE_AFE, LOW); // Disable sampling - HOLD mode delayMicroseconds(LEVEL_SHIFTING_DELAY_MAX); // Wait to settle @@ -1008,50 +1093,26 @@ void loop() waitbeforebalance--; } - if (serialBaudScanning) - { - NotificationLedOn(); - } + // This needs to be below all other cell checking code + ServiceSerialPort(); - // Service the serial port/queue - for (size_t i = 0; i < 300; i++) + // Every so often, we should call this to calibrate the op-amp as it changes in ambient temperature (takes 8ms to complete) + if (PP.getPacketReceivedCounter() % 8192 == 0) { - // Call update to receive, decode and process incoming packets. - myPacketSerial.checkInputStream(); - - // Allow data to be received in buffer (delay must be AFTER) checkInputStream - delay(1); + BufferAmplifierOffsetCalibration(); } if (serialBaudScanning) { - NotificationLedOff(); - serialBaudCountDown--; - - if (PP.getPacketReceivedCounter() > 0) - { - // We have found our baud rate - and processed at least 1 packet successfully, so stop scanning - serialBaudScanning = false; - } - else if (serialBaudCountDown <= 0) - { - // If we got this far, we have not yet found/received a valid packet of serial data, so try another baud rate - serialBaudCountDown = 15; - serialBaudIndex++; - - if (serialBaudIndex >= SerialBaudRates.size()) - { - serialBaudIndex = 0; - } - - Serial1.end(); - Serial1.begin(SerialBaudRates.at(serialBaudIndex), SERIAL_8N1); - } + NotificationLedOn(); } - // Every so often, we should call this to calibrate the op-amp as it changes in ambient temperature (takes 8ms to complete) - if (PP.getPacketReceivedCounter() % 8192 == 0) + // Sleep for 800ms + // TODO:ideally, this would be a true CPU sleep with RTC + UART wake up + uint16_t countdown = 400; + while (Serial1.available() == 0 && countdown > 0) { - BufferAmplifierOffsetCalibration(); + delay(2); + countdown--; } } From 5a723899facca9abf778cbc221bccae5f53bc75d Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:41:05 +0000 Subject: [PATCH 19/27] Run away cell balance logic changes --- STM32All-In-One/include/cell.h | 27 +++++-------- STM32All-In-One/src/main.cpp | 74 +++++++++------------------------- 2 files changed, 30 insertions(+), 71 deletions(-) diff --git a/STM32All-In-One/include/cell.h b/STM32All-In-One/include/cell.h index 11c29678..fe79079b 100644 --- a/STM32All-In-One/include/cell.h +++ b/STM32All-In-One/include/cell.h @@ -91,25 +91,20 @@ class Cell void StartBypass() { - // Record when the bypass started - bypassStartTime = millis(); - bypassStartVoltage = getCellVoltage(); - CellIsInBypass = true; + if (!IsBypassActive()) + { + // Record when the bypass started + CellIsInBypass = true; + } } void StopBypass() { - //"guess" how much energy we have burnt during the balance - // this IGNORES any over temperature situation we may had had! - - // bypassStartVoltage is in millivolts, so we get milli-amp current output - // For example 4000mV / 18R = 222.22mA - float CurrentmA = ((float)bypassStartVoltage / (float)LOAD_RESISTANCE); - - float seconds = (millis() - bypassStartTime) / 1000; - - float milliAmpHours = (CurrentmA * seconds) * (1.0 / 3600.0); + if (!IsBypassActive()) + return; - MilliAmpHourBalanceCounter += milliAmpHours; + // We don't have an accurate way to calculate energy burnt, so just increment + // the counter to show we have actually balanced something on this cell + MilliAmpHourBalanceCounter += 1; CellIsInBypass = false; } @@ -198,8 +193,6 @@ class Cell int16_t externalTemperature{-999}; int16_t internalTemperature{-999}; bool CellIsInBypass{false}; - uint32_t bypassStartTime{0}; - uint16_t bypassStartVoltage{0}; float MilliAmpHourBalanceCounter{0}; diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 2f1941fe..339cca29 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -31,14 +31,11 @@ Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales (CC BY-NC-SA 2.0 UK #include -extern "C" -{ - void SystemClock_Config(void); -} - extern "C" { #include "stm32_flash.h" + void SystemClock_Config(void); +#include } #include "SPI.h" @@ -775,10 +772,11 @@ void TakeOnboardInternalTempMeasurements(CellData &cd) } /// @brief Calculate bit pattern for cell passive balancing (MOSFET switches) -/// @param cd -/// @param num_cells +/// @param cd Cell data array +/// @param num_cells total number of cells +/// @param runawaycell_index index of cell identified as run away (or -1 if none) /// @return bit pattern -uint16_t CalculateCellBalanceRequirement(CellData &cd, uint8_t num_cells) +uint16_t CalculateCellBalanceRequirement(CellData &cd, uint8_t num_cells, int runawaycell_index) { // if balance daughter board is not installed, always return zero - no balance if (PP.BalanceBoardInstalled == false) @@ -793,25 +791,19 @@ uint16_t CalculateCellBalanceRequirement(CellData &cd, uint8_t num_cells) // Get reference to cell object Cell &cell = cd.at(i); - if (cell.BypassCheck() == true) + // Check if bypass is needed, or if runawaycell is identified + if (cell.BypassCheck() == true || runawaycell_index == i) { // Our cell voltage is OVER the voltage setpoint limit, start draining cell using bypass resistor - if (!cell.IsBypassActive()) - { - // We have just entered the bypass - cell.StartBypass(); - } + cell.StartBypass(); // Enable balancing bit pattern - this enables the MOSFET and balance resistor reply = reply | (uint16_t)(1U << (15 - i)); } else { - if (cell.IsBypassActive()) - { - // We've just ended bypass.... - cell.StopBypass(); - } + // We've just ended bypass.... + cell.StopBypass(); } } return reply; @@ -861,8 +853,6 @@ void CalculateCellVoltageMinMaxAvg(CellData &cd, range = highestmV - lowestmV; } -int16_t lastRunAwayCellIndex = -1; - /// @brief Determine what (if any) cells need balancing, also controls relay output /// @param highestTemp Highest temperature of on-board sensors (balance temperature) /// @return bit pattern for which MOSFETs need enabling @@ -922,45 +912,19 @@ uint16_t DoCellBalancing(const int16_t highestTemp) } } - // Start with everything switched off - uint16_t cb = 0; - // Calculate the average of all the cells auto averagemv = (uint16_t)(total / number_of_active_cells); // Calculate the differential between highest cell voltage and the average uint16_t highest_average_diff = highestmV - averagemv; - // Should any cells require balancing - check if they have gone over the threshold - cb = CalculateCellBalanceRequirement(celldata, number_of_active_cells); - - // Now determine if we should balance the highest "run away" cell - only if "normal" balancing is not active + // Now determine if we should balance the highest "run away" cell. // If the highest cell is above average cell voltage by X millivolts, then begin balancing until it no longer is. - // Also ensure the cell voltage is above a minimum - LIFEPO4 cells need to be over 3400mV for this function to be useful. - if (cb == 0 && highestmV > PP.getRunAwayCellMinimumVoltage() && highest_average_diff > PP.getRunAwayCellDifferential()) - { - cb = cb | (uint16_t)(1U << (15 - highest_index)); + // Ensure the cell voltage is above a minimum - LIFEPO4 cells need to be over 3400mV for this function to be useful. + auto runawaycellindex = (highestmV > PP.getRunAwayCellMinimumVoltage() && highest_average_diff > PP.getRunAwayCellDifferential()) ? highest_index : -1; - if (celldata.at(highest_index).IsBypassActive() == false) - { - celldata.at(highest_index).StartBypass(); - lastRunAwayCellIndex = highest_index; - } - } - else - { - // Stop run away cell balancing, if it was active. - if (lastRunAwayCellIndex >= 0) - { - if (celldata.at(lastRunAwayCellIndex).IsBypassActive()) - { - celldata.at(lastRunAwayCellIndex).StopBypass(); - } - lastRunAwayCellIndex = -1; - } - } - - return cb; + // Should any cells require balancing - check if they have gone over the threshold + return CalculateCellBalanceRequirement(celldata, number_of_active_cells, runawaycellindex); } // Checks the serial port for about 60ms and processes any requests @@ -1004,6 +968,7 @@ void ServiceSerialPort() } } + void loop() { @@ -1024,7 +989,7 @@ void loop() if (waitbeforebalance == 0) { - // This also takes about 60ms, but allows request packets to be processed quicker than waiting + // This also takes about 60ms, but allows request packets to be processed quicker than waiting in a blocking delay call ServiceSerialPort(); } else @@ -1108,11 +1073,12 @@ void loop() } // Sleep for 800ms - // TODO:ideally, this would be a true CPU sleep with RTC + UART wake up + // TODO:ideally, this would be a true CPU sleep with RTC + UART wake up, but running out of FLASH code space uint16_t countdown = 400; while (Serial1.available() == 0 && countdown > 0) { delay(2); countdown--; } + } From 256670999f7ae2237b8ed7a913f5b9631bb5f405 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:46:37 +0000 Subject: [PATCH 20/27] Update main.cpp --- STM32All-In-One/src/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/STM32All-In-One/src/main.cpp b/STM32All-In-One/src/main.cpp index 339cca29..c0de631a 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -35,7 +35,6 @@ extern "C" { #include "stm32_flash.h" void SystemClock_Config(void); -#include } #include "SPI.h" From ed1cd25299922bf399eee67176b7955c7e65b7e2 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:36:03 +0000 Subject: [PATCH 21/27] Update default.htm --- ESPController/web_src/default.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index 574e5a3d..7ca8ab03 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -372,7 +372,7 @@

Modules

Bypass PWM % Bad packet count Packets received - Balance energy used (mAh) + Balance statistics From 420c67055a5bb749d16114c7f3dac3742b82c143 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:36:13 +0000 Subject: [PATCH 22/27] Update EmbeddedFiles_Defines.h --- STM32All-In-One/include/EmbeddedFiles_Defines.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/STM32All-In-One/include/EmbeddedFiles_Defines.h b/STM32All-In-One/include/EmbeddedFiles_Defines.h index fdf4a828..ffcc88a2 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[] = "a4f3024afd95548a3a8628db190a98564f7cd2d5"; +static const char GIT_VERSION[] = "256670999f7ae2237b8ed7a913f5b9631bb5f405"; -static const uint16_t GIT_VERSION_B1 = 0x4f7c; +static const uint16_t GIT_VERSION_B1 = 0x1bb5; -static const uint16_t GIT_VERSION_B2 = 0xd2d5; +static const uint16_t GIT_VERSION_B2 = 0xf405; -static const char COMPILE_DATE_TIME[] = "2023-07-04T15:47:51.443Z"; +static const char COMPILE_DATE_TIME[] = "2023-11-13T11:48:01.770Z"; #endif \ No newline at end of file From 2c0e78ac43a63accfe8fbcc70ff4d89ad4710453 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:34:45 +0000 Subject: [PATCH 23/27] PylonForce canbus_equipment_addr to settings file --- ESPController/src/main.cpp | 3 ++- ESPController/src/pylonforce_canbus.cpp | 2 +- ESPController/src/settings.cpp | 6 ++++++ ESPController/src/webserver_json_requests.cpp | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 77d53bb7..af0dab9e 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -3643,7 +3643,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}, @@ -3665,6 +3665,7 @@ const std::array log_levels = {.tag = "diybms-set", .level = ESP_LOG_INFO}, {.tag = "diybms-mqtt", .level = ESP_LOG_INFO}, {.tag = "diybms-pylon", .level = ESP_LOG_INFO}, + {.tag = "diybms-pyforce", .level = ESP_LOG_INFO}, {.tag = "curmon", .level = ESP_LOG_INFO}}; void consoleConfigurationCheck() diff --git a/ESPController/src/pylonforce_canbus.cpp b/ESPController/src/pylonforce_canbus.cpp index c54052bc..105ba001 100644 --- a/ESPController/src/pylonforce_canbus.cpp +++ b/ESPController/src/pylonforce_canbus.cpp @@ -15,7 +15,7 @@ and Deye 2_CAN-Bus-Protocol-high-voltag-V1.17.pdf */ #define USE_ESP_IDF_LOG 1 -static constexpr const char *const TAG = "diybms-pylonforce"; +static constexpr const char *const TAG = "diybms-pyforce"; #include "pylonforce_canbus.h" #include "mqtt.h" diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index a60d2ef5..70e505c8 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -43,6 +43,7 @@ static const char influxdb_loggingFreqSeconds_JSONKEY[] = "logfreq"; static const char canbusprotocol_JSONKEY[] = "canbusprotocol"; static const char canbusinverter_JSONKEY[] = "canbusinverter"; static const char canbusbaud_JSONKEY[] = "canbusbaud"; +static const char canbus_equipment_addr_JSONKEY[] = "canbusequip"; static const char nominalbatcap_JSONKEY[] = "nominalbatcap"; static const char chargevolt_JSONKEY[] = "chargevolt"; static const char chargecurrent_JSONKEY[] = "chargecurrent"; @@ -120,6 +121,7 @@ static const char rs485stopbits_NVSKEY[] = "485stopbits"; static const char canbusprotocol_NVSKEY[] = "canbusprotocol"; static const char canbusinverter_NVSKEY[] = "canbusinverter"; static const char canbusbaud_NVSKEY[] = "canbusbaud"; +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"; @@ -373,6 +375,7 @@ void SaveConfiguration(diybms_eeprom_settings *settings) MACRO_NVSWRITE_UINT8(canbusprotocol); MACRO_NVSWRITE(canbusinverter); MACRO_NVSWRITE(canbusbaud); + MACRO_NVSWRITE_UINT8(canbus_equipment_addr); MACRO_NVSWRITE(currentMonitoring_shuntmv); MACRO_NVSWRITE(currentMonitoring_shuntmaxcur); @@ -515,6 +518,7 @@ void LoadConfiguration(diybms_eeprom_settings *settings) 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); @@ -1063,6 +1067,7 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[canbusprotocol_JSONKEY] = (uint8_t)settings->canbusprotocol; root[canbusinverter_JSONKEY] = (uint8_t)settings->canbusinverter; root[canbusbaud_JSONKEY] = settings->canbusbaud; + root[canbus_equipment_addr_JSONKEY]=settings->canbus_equipment_addr; root[nominalbatcap_JSONKEY] = settings->nominalbatcap; root[chargevolt_JSONKEY] = settings->chargevolt; @@ -1160,6 +1165,7 @@ void JSONToSettings(DynamicJsonDocument &doc, diybms_eeprom_settings *settings) settings->canbusprotocol = (CanBusProtocolEmulation)root[canbusprotocol_JSONKEY]; settings->canbusinverter = (CanBusInverter)root[canbusinverter_JSONKEY]; settings->canbusbaud = root[canbusbaud_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]; diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index afa1013d..b182e4d9 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -601,6 +601,7 @@ esp_err_t content_handler_chargeconfig(httpd_req_t *req) settings["canbusprotocol"] = mysettings.canbusprotocol; settings["canbusinverter"] = mysettings.canbusinverter; settings["canbusbaud"] = mysettings.canbusbaud; + settings["equip_addr"]=mysettings.canbus_equipment_addr; settings["nominalbatcap"] = mysettings.nominalbatcap; settings["chargevolt"] = mysettings.chargevolt; settings["chargecurrent"] = mysettings.chargecurrent; From e54ad795869fc7c4d9f50a74ba8b4cfb24a9804c Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:49:47 +0000 Subject: [PATCH 24/27] Update default.htm --- ESPController/web_src/default.htm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index 453ce032..f97d61da 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -1131,6 +1131,10 @@

Charge/Discharge configuration

Remember to install terminator resistors at both ends of CAN connection. On the controller, jumper JP1 can be soldered closed for this purpose.

+

+ Warning + PylonTech ForceH2 canbus emulation is currently experimental. +

From 61ff6fcf0384783186278f8958c7cf8db433833c Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:34:57 +0000 Subject: [PATCH 25/27] Home assistant and WIFI statistics --- ESPController/TODOLIST.md | 110 ------------------ ESPController/include/webserver_json_post.h | 2 + .../include/webserver_json_requests.h | 8 ++ ESPController/src/main.cpp | 17 +++ ESPController/src/settings.cpp | 10 ++ ESPController/src/webserver_helper_funcs.cpp | 7 +- ESPController/src/webserver_json_post.cpp | 41 ++++++- ESPController/src/webserver_json_requests.cpp | 43 ++++++- ESPController/web_src/default.htm | 88 +++++++++++++- ESPController/web_src/pagecode.js | 34 ++++++ 10 files changed, 231 insertions(+), 129 deletions(-) delete mode 100644 ESPController/TODOLIST.md diff --git a/ESPController/TODOLIST.md b/ESPController/TODOLIST.md deleted file mode 100644 index 37e6475b..00000000 --- a/ESPController/TODOLIST.md +++ /dev/null @@ -1,110 +0,0 @@ -# diyBMS v4 -## New Controller To-Do-List - -## Things To Check - -(in no particular order) - -### ESP32 BOOT button for WIFI RESET - -GPIO0 pin connected to interrupt, hold BOOT button on ESP32 for more than 4 seconds to factory reset the WIFI settings stored in EEPROM. -Once reset, the LED lights CYAN, at this point reset the controller to enter either the terminal based WIFI configuration (by pressing -SPACE bar) or WIFI access point configuration (default). Connect to WIFI SSID "DIY_BMS_CONTROLLER" and IP address 192.168.4.1 to ensure -set up pages. - -### USB Debugging/Console -USB Serial is connected to second UART on ESP32 allowing full console access and debug serial port via USB cable. -Also used for terminal based WIFI configuration. - -### TCA9534A -Controls RGB LED, TFT display LED and AVR ISP reset line - -### TCA6408AQPWRQ1 -Controls relay's and external IO A/B/C/D/E - -### Relay 1 -Driven from pin 9/P4 of TCA6408AQPWRQ1, confirmed working. Relay SRD-05VDC-SL-C, rated 10A @ 250VAC/ 28VDC - -### Relay 2 -Driven from pin 10/P5 of TCA6408AQPWRQ1, relay SRD-05VDC-SL-C, rated 10A @ 250VAC/ 28VDC -confirmed working - -### Relay 3 (SSR) -Driven from pin 11/P6 of TCA6408AQPWRQ1, uses Panasonic AQY212GSZ. Rated 60V @ 1A -confirmed working - -### Relay 4 (SSR) -Driven from pin 12/P7 of TCA6408AQPWRQ1, uses Panasonic AQY212GSZ. Rated 60V @ 1A -confirmed working - -### TX1/RX1 -Uses GPIO2 for RX and 32 for TX. Works as per ESP8266 modules using hardware based UART, and EL3H7(B)(TA)-G isolator. - -### I/O ports -Driven from pin 4/5/6/7 (P0/1/2/3) of TCA6408AQPWRQ1. Maps to header socket marked A/B/C/D. - -### External 5v power supply input -Confirmed working, 3.3v regulator working, reverse polarity protection working -Over voltage zener diode (ZMM5V6) not working as expected, incorrectly positioned in circuit diagram (fixed, but not tested in new revision) - -### RGB LED -Confirmed working, driven from TCA9534A pins 4/5/6 (P0/P1/P2) BLUE, RED, GREEN. - -### TFT Screen -Confirmed LED backlight working, driven from TCA9534A pin 7 (P3). - -Display uses ILI9341 driver and is 240x320 pixels, with touch and SD Card interface (on seperate pins). - -https://uk.banggood.com/2_8-Inch-ILI9341-240x320-SPI-TFT-LCD-Display-Touch-Panel-SPI-Serial-Port-Module-p-1206782.html - -Around £9 UK GBP. Has two header pins, one for the touch and display, the other for the SD Card. - -Looking top down onto the TFT screen (screen header pins on left marked J2) pins are - -* VCC -* GND -* CS -* RESET -* DC -* MOSI -* SCK -* LED backlight -* MISO -* T_CLK (touch) -* T_CS (touch) -* T_DIN (touch) -* T_DO (touch) -* T_IRQ (touch) - -### TFT Touch -Uses GPIO4 for chip select and VSPI interface for communication with XPT2046 driver -http://grobotronics.com/images/datasheets/xpt2046-datasheet.pdf - -### SD CARD -Uses GPIO5 for chip select -Micro SD card onto controller PCB see B.O.M for part numbers - -### CANBUS - -Using TJA1051T/3 CAN Bus Transceiver - -120ohm terminator resistor included on controller board (jumper to enable) - -TX=GPIO16, RX=GPIO17 and RS=connected to P4 of TCA9534A (normally low, full speed CAN) - -Confirmed working - -### RS485 -Using SN65HVD75DR, 3.3-V Supply RS-485 With IEC ESD protection. - -Driven using Hardware Serial port Serial1, TX=GPIO22, RX=GPIO21, ENABLE=GPIO25 - -Confirmed working - -### ATTINY ISP Programming -Connected to VSPI interface and uses P4 output on TCA9534A to drive reset line. - -VSPI should be disabled/not used whilst IVR programmer in use - -### TX2/RX2 -Removed in newer revision of board diff --git a/ESPController/include/webserver_json_post.h b/ESPController/include/webserver_json_post.h index 893ab672..2d10e5d9 100644 --- a/ESPController/include/webserver_json_post.h +++ b/ESPController/include/webserver_json_post.h @@ -38,6 +38,7 @@ extern void setCacheControl(httpd_req_t *req); extern void configureSNTP(long gmtOffset_sec, int daylightOffset_sec, const char *server1); extern void DefaultConfiguration(diybms_eeprom_settings *_myset); extern bool SaveWIFIJson(const wifi_eeprom_settings* setting); +extern void randomCharacters(char *value, int length); 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); @@ -67,6 +68,7 @@ esp_err_t post_avrprog_json_handler(httpd_req_t *req, bool urlEncoded); esp_err_t post_savecurrentmon_json_handler(httpd_req_t *req, bool urlEncoded); 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); +esp_err_t post_homeassistant_apikey_json_handler(httpd_req_t *req, bool urlEncoded); esp_err_t save_data_handler(httpd_req_t *req); #endif diff --git a/ESPController/include/webserver_json_requests.h b/ESPController/include/webserver_json_requests.h index e9cc3ea7..868e531a 100644 --- a/ESPController/include/webserver_json_requests.h +++ b/ESPController/include/webserver_json_requests.h @@ -64,4 +64,12 @@ extern uint16_t mqtt_error_transport_count; extern uint16_t mqtt_connection_count; extern uint16_t mqtt_disconnection_count; +extern uint16_t wifi_count_rssi_low; +extern uint16_t wifi_count_sta_start; +extern uint16_t wifi_count_sta_connected; +extern uint16_t wifi_count_sta_disconnected; +extern uint16_t wifi_count_sta_lost_ip; +extern uint16_t wifi_count_sta_got_ip; + +extern bool wifi_isconnected; #endif diff --git a/ESPController/src/main.cpp b/ESPController/src/main.cpp index 62c903dc..b7959d40 100644 --- a/ESPController/src/main.cpp +++ b/ESPController/src/main.cpp @@ -1583,6 +1583,16 @@ void ShutdownAllNetworkServices() stopMDNS(); } +/// @brief Count of events of RSSI low +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; +/// @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; + /// @brief WIFI Event Handler /// @param /// @param event_base @@ -1596,9 +1606,11 @@ static void event_handler(void *, esp_event_base_t event_base, if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_BSS_RSSI_LOW) { ESP_LOGW(TAG, "WiFi signal strength low"); + wifi_count_rssi_low++; } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + wifi_count_sta_start++; ESP_LOGI(TAG, "WIFI_EVENT_STA_START"); wifi_isconnected = false; ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); @@ -1607,6 +1619,7 @@ static void event_handler(void *, esp_event_base_t event_base, { // We have joined the access point - now waiting for IP address IP_EVENT_STA_GOT_IP wifi_ap_connect_retry_num = 0; + wifi_count_sta_connected++; wifi_ap_record_t ap; esp_wifi_sta_get_ap_info(&ap); @@ -1617,6 +1630,7 @@ static void event_handler(void *, esp_event_base_t event_base, { ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); wifi_ap_connect_retry_num++; + wifi_count_sta_disconnected++; if (wifi_isconnected) { @@ -1638,12 +1652,15 @@ static void event_handler(void *, esp_event_base_t event_base, { wifi_isconnected = false; ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); + wifi_count_sta_lost_ip++; ShutdownAllNetworkServices(); wake_up_tft(true); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + wifi_count_sta_got_ip++; + auto event = (ip_event_got_ip_t *)event_data; if (event->ip_changed) diff --git a/ESPController/src/settings.cpp b/ESPController/src/settings.cpp index 4168ce36..43dfc08a 100644 --- a/ESPController/src/settings.cpp +++ b/ESPController/src/settings.cpp @@ -1101,6 +1101,11 @@ void GenerateSettingsJSONDocument(DynamicJsonDocument *doc, diybms_eeprom_settin root[current_value1_JSONKEY] = settings->current_value1; root[current_value2_JSONKEY] = settings->current_value2; + root[absorptiontimer_JSONKEY] = settings->absorptiontimer; + root[floatvoltage_JSONKEY] = settings->floatvoltage; + root[floatvoltagetimer_JSONKEY] = settings->floatvoltagetimer; + root[stateofchargeresumevalue_JSONKEY] = settings->stateofchargeresumevalue; + JsonArray tv = root.createNestedArray("tilevisibility"); for (uint8_t i = 0; i < sizeof(settings->tileconfig) / sizeof(uint16_t); i++) { @@ -1196,6 +1201,11 @@ 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]; + strncpy(settings->homeassist_apikey, root[homeassist_apikey_JSONKEY].as().c_str(), sizeof(settings->homeassist_apikey)); JsonObject mqtt = root["mqtt"]; diff --git a/ESPController/src/webserver_helper_funcs.cpp b/ESPController/src/webserver_helper_funcs.cpp index 35c76963..4be8d34a 100644 --- a/ESPController/src/webserver_helper_funcs.cpp +++ b/ESPController/src/webserver_helper_funcs.cpp @@ -15,15 +15,14 @@ void setCookie(httpd_req_t *req) void randomCharacters(char* value, int length) { // Pick random characters from this string (we could just use ASCII offset instead of this) // but this also avoids javascript escape characters like backslash and cookie escape chars like ; and % - char alphabet[] = "!$*#@ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"; + auto alphabet=std::string("!$*#@ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"); // Leave NULL terminator on char array for (uint8_t x = 0; x < length; x++) - { + { // Random number between 0 and array length (minus null char) - value[x] = alphabet[random(0, sizeof(alphabet) - 2)]; + value[x] = alphabet.at(random(0, alphabet.length())); } - } void setCookieValue() diff --git a/ESPController/src/webserver_json_post.cpp b/ESPController/src/webserver_json_post.cpp index 2b89e22f..045c5fe1 100644 --- a/ESPController/src/webserver_json_post.cpp +++ b/ESPController/src/webserver_json_post.cpp @@ -79,7 +79,7 @@ esp_err_t post_savemqtt_json_handler(httpd_req_t *req, bool urlEncoded) { // Default to off mysettings.mqtt_enabled = false; - mysettings.mqtt_basic_cell_reporting=false; + mysettings.mqtt_basic_cell_reporting = false; // Username and password are optional and may not be HTTP posted from web browser memset(mysettings.mqtt_username, 0, sizeof(mysettings.mqtt_username)); @@ -492,7 +492,7 @@ esp_err_t post_savechargeconfig_json_handler(httpd_req_t *req, bool urlEncoded) } GetKeyValue(httpbuf, "canbusbaud", &mysettings.canbusbaud, urlEncoded); - + GetKeyValue(httpbuf, "nominalbatcap", &mysettings.nominalbatcap, urlEncoded); GetKeyValue(httpbuf, "cellminmv", &mysettings.cellminmv, urlEncoded); GetKeyValue(httpbuf, "cellmaxmv", &mysettings.cellmaxmv, urlEncoded); @@ -642,6 +642,35 @@ esp_err_t post_savecmrelay_json_handler(httpd_req_t *req, bool urlEncoded) return SendSuccess(req); } +/// @brief Generates new home assistant API key and stored into flash +/// @param req +/// @param urlEncoded +/// @return +esp_err_t post_homeassistant_apikey_json_handler(httpd_req_t *req, bool urlEncoded) +{ + char buffer[32]; + + // Compare existing key to stored value, if they match allow generation of new key + if (GetTextFromKeyValue(httpbuf, "haAPI", buffer, sizeof(buffer), urlEncoded)) + { + if (strncmp(mysettings.homeassist_apikey, buffer, strlen(mysettings.homeassist_apikey)) != 0) + { + ESP_LOGE(TAG, "Incorrect ApiKey in form variable %s", buffer); + return SendFailure(req); + } + + memset(&mysettings.homeassist_apikey, 0, sizeof(mysettings.homeassist_apikey)); + randomCharacters(mysettings.homeassist_apikey, sizeof(mysettings.homeassist_apikey) - 1); + saveConfiguration(); + + ESP_LOGI(TAG, "new ha apikey=%s", mysettings.homeassist_apikey); + + return SendSuccess(req); + } + + return SendFailure(req); +} + esp_err_t post_savenetconfig_json_handler(httpd_req_t *req, bool urlEncoded) { char buffer[32]; @@ -1177,7 +1206,7 @@ esp_err_t save_data_handler(httpd_req_t *req) return ESP_FAIL; } - std::array uri_array = { + std::array uri_array = { "savebankconfig", "saventp", "saveglobalsetting", "savemqtt", "saveinfluxdb", "saveconfigtofile", "wificonfigtofile", @@ -1188,9 +1217,9 @@ esp_err_t save_data_handler(httpd_req_t *req) "savecurrentmon", "savecmbasic", "savecmadvanced", "savecmrelay", "restoreconfig", "savechargeconfig", "visibletiles", "dailyahreset", "setsoc", - "savenetconfig"}; + "savenetconfig", "newhaapikey"}; - std::array, 29> func_ptr = { + std::array, 30> func_ptr = { post_savebankconfig_json_handler, post_saventp_json_handler, post_saveglobalsetting_json_handler, post_savemqtt_json_handler, post_saveinfluxdbsetting_json_handler, post_saveconfigurationtoflash_json_handler, post_savewificonfigtosdcard_json_handler, @@ -1201,7 +1230,7 @@ esp_err_t save_data_handler(httpd_req_t *req) post_savecurrentmon_json_handler, post_savecmbasic_json_handler, post_savecmadvanced_json_handler, post_savecmrelay_json_handler, post_restoreconfig_json_handler, post_savechargeconfig_json_handler, post_visibletiles_json_handler, post_resetdailyahcount_json_handler, post_setsoc_json_handler, - post_savenetconfig_json_handler}; + post_savenetconfig_json_handler, post_homeassistant_apikey_json_handler}; auto name = std::string(req->uri); diff --git a/ESPController/src/webserver_json_requests.cpp b/ESPController/src/webserver_json_requests.cpp index 7b1d4eca..e922680d 100644 --- a/ESPController/src/webserver_json_requests.cpp +++ b/ESPController/src/webserver_json_requests.cpp @@ -5,6 +5,7 @@ static constexpr const char *const TAG = "diybms-webreq"; #include "webserver_helper_funcs.h" #include "webserver_json_requests.h" #include +#include extern "C" { #include "esp_core_dump.h" @@ -601,7 +602,7 @@ esp_err_t content_handler_chargeconfig(httpd_req_t *req) settings["canbusprotocol"] = mysettings.canbusprotocol; settings["canbusinverter"] = mysettings.canbusinverter; settings["canbusbaud"] = mysettings.canbusbaud; - settings["equip_addr"]=mysettings.canbus_equipment_addr; + settings["equip_addr"] = mysettings.canbus_equipment_addr; settings["nominalbatcap"] = mysettings.nominalbatcap; settings["chargevolt"] = mysettings.chargevolt; settings["chargecurrent"] = mysettings.chargecurrent; @@ -793,6 +794,34 @@ esp_err_t content_handler_settings(httpd_req_t *req) settings["man_dns2"] = ip4_to_string(_wificonfig.wifi_dns2); } + JsonObject wifi = root.createNestedObject("wifi"); + + if (wifi_isconnected) + { + wifi_ap_record_t ap; + esp_wifi_sta_get_ap_info(&ap); + wifi["rssi"] = ap.rssi; + wifi["ssid"] = ap.ssid; + + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", + ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]); + wifi["bssid"] = macStr; + } + else + { + wifi["rssi"] = 0; + wifi["ssid"] = ""; + wifi["bssid"] = ""; + } + + wifi["rssi_low"] = wifi_count_rssi_low; + wifi["sta_start"] = wifi_count_sta_start; + wifi["sta_connected"] = wifi_count_sta_connected; + wifi["sta_disconnected"] = wifi_count_sta_disconnected; + wifi["sta_lost_ip"] = wifi_count_sta_lost_ip; + wifi["sta_got_ip"] = wifi_count_sta_got_ip; + bufferused += serializeJson(doc, httpbuf, BUFSIZE); return httpd_resp_send(req, httpbuf, bufferused); @@ -805,6 +834,9 @@ esp_err_t content_handler_integration(httpd_req_t *req) DynamicJsonDocument doc(1024); JsonObject root = doc.to(); + JsonObject ha = root.createNestedObject("ha"); + ha["api"] = mysettings.homeassist_apikey; + JsonObject mqtt = root.createNestedObject("mqtt"); mqtt["enabled"] = mysettings.mqtt_enabled; mqtt["basiccellreporting"] = mysettings.mqtt_basic_cell_reporting; @@ -1266,7 +1298,7 @@ esp_err_t ha_handler(httpd_req_t *req) httpd_resp_set_type(req, "application/json"); setNoStoreCacheControl(req); - ESP_LOGI(TAG, "ha_handler"); + ESP_LOGI(TAG, "home assistant api request"); char buffer[128]; esp_err_t result = httpd_req_get_hdr_value_str(req, "ApiKey", buffer, sizeof(buffer)); @@ -1274,17 +1306,16 @@ esp_err_t ha_handler(httpd_req_t *req) if (result != ESP_OK) { ESP_LOGE(TAG, "Missing header ApiKey"); - return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr); } - + if (strncmp(mysettings.homeassist_apikey, buffer, strlen(mysettings.homeassist_apikey)) != 0) { ESP_LOGE(TAG, "Unauthorized ApiKey=%s", buffer); - return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, NULL); + return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, nullptr); } int bufferused = 0; - const char *nullstring = "null"; // Output the first batch of settings/parameters/values bufferused += snprintf(&httpbuf[bufferused], BUFSIZE - bufferused, diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index f97d61da..b49ddc5e 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -482,10 +482,11 @@

Global Settings

Integration

- +

MQTT

-

For security, you will need to re-enter the password for the MQTT service if you want to enable or modify, before you save.

+

For security, you will need to re-enter the password for the MQTT service if you want to enable or + modify, before you save.

URI should be similar to mqtt://192.168.0.26:1833

Basic cell data option reduces the amount of MQTT data being sent over the network.

@@ -616,6 +617,31 @@

API Version 1.X

+ + +
+

Home Assistant Web API

+

+ Home Assistant integration can be configured using + its RESTful API. Example + configuration is available in DIYBMS + GitHub repository. +

+ +
+
+
+ + +
+
+ + +
+ +
+
+
@@ -968,7 +994,8 @@

Charge/Discharge configuration

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

Temperature control utilises the external temperature sensors on the diyBMS modules. This is very useful for @@ -1569,6 +1596,8 @@

Modules & Banks

+ +

Network - Running Settings

Wi-Fi "Station" network interface details:

@@ -1635,6 +1664,59 @@

Network - New Settings

+ +
+

WIFI Statistics

+

+ To help diagnose WIFI network issues, the values below are counters of WIFI events processed by the ESP32 +

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

Network Time Protocol

diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index bbc702a9..09be33bf 100644 --- a/ESPController/web_src/pagecode.js +++ b/ESPController/web_src/pagecode.js @@ -1613,6 +1613,18 @@ $(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); + }).fail(function () { $.notify("Request failed", { autoHide: true, globalPosition: 'top right', className: 'error' }); } ); @@ -1754,6 +1766,10 @@ $(function () { $("#influxOrgId").val(data.influxdb.orgid); $("#influxFreq").val(data.influxdb.frequency); + $("#haUrl").val(window.location.origin+"/ha"); + $("#haAPI").val(data.ha.api); + + $("#haForm").show(); $("#mqttForm").show(); $("#influxForm").show(); }).fail(function () { $.notify("Request failed", { autoHide: true, globalPosition: 'top right', className: 'error' }); } @@ -2113,6 +2129,24 @@ $(function () { }, }); }); + + $("#haForm").unbind('submit').submit(function (e) { + e.preventDefault(); + + $.ajax({ + type: $(this).attr('method'), + url: $(this).attr('action'), + data: $("#haAPI").serialize(), + success: function (data) { + showSuccess(); + $("#integration").click(); + }, + error: function (data) { + showFailure(); + }, + }); + }); + $("#rulesForm").unbind('submit').submit(function (e) { e.preventDefault(); From 31433c987d3044e48e48353f7cff9f826b5e1742 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:38:30 +0000 Subject: [PATCH 26/27] Update default.htm --- ESPController/web_src/default.htm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index b49ddc5e..9bbb122b 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -1560,7 +1560,7 @@

Controller Firmware Upgrade

Controller Settings

-
+

Modules & Banks

DIYBMS supports up to @@ -1598,7 +1598,7 @@

Modules & Banks

-
+

Network - Running Settings

Wi-Fi "Station" network interface details:

@@ -1664,8 +1664,8 @@

Network - New Settings

- -
+ +

WIFI Statistics

To help diagnose WIFI network issues, the values below are counters of WIFI events processed by the ESP32 @@ -1715,9 +1715,9 @@

WIFI Statistics

- -
+ +

Network Time Protocol

Time is set via NTP, if your controller is not connected to the Internet. Time based rules will be incorrect. @@ -1751,7 +1751,7 @@

Network Time Protocol

-
+

Display Settings

From 5e00989680e539508156879b6b368aaf8f23683b Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Sat, 25 Nov 2023 11:59:38 +0000 Subject: [PATCH 27/27] Create Home_Assistant_RESTful_API.md --- ESPController/Home_Assistant_RESTful_API.md | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 ESPController/Home_Assistant_RESTful_API.md diff --git a/ESPController/Home_Assistant_RESTful_API.md b/ESPController/Home_Assistant_RESTful_API.md new file mode 100644 index 00000000..da66b43f --- /dev/null +++ b/ESPController/Home_Assistant_RESTful_API.md @@ -0,0 +1,117 @@ +# diyBMS v4 +## Home Assistant RESTful API integration + + + +### Secrets Configuration YAML file for Home Assistant +``` +# Use this file to store secrets like usernames and passwords. +# Learn more at https://www.home-assistant.io/docs/configuration/secrets/ + +diybms_api_token: XXXXXXXXXXXXXXXXXXXXXXXX +``` + +### Example Configuration YAML file for Home Assistant +``` +# Example configuration.yaml entry for Home Assistant integration with DIYBMS + +rest: + - resource: http://192.168.0.70/ha + scan_interval: 10 + timeout: 5 + method: "GET" + headers: + Content-Type: application/json + ApiKey: !secret diybms_api_token + sensor: + - unique_id: "diybms.activerules" + value_template: "{{ value_json.activerules }}" + name: "Active rules" + state_class: "measurement" + + - unique_id: "diybms.chargemode" + value_template: "{{ value_json.chgmode }}" + name: "Charge mode" + state_class: "measurement" + + - unique_id: "diybms.lowest_bank_voltage" + value_template: "{{ value_json.lowbankv }}" + name: "Lowest bank voltage" + unit_of_measurement: "mV" + device_class: "voltage" + + - unique_id: "diybms.highest_bank_voltage" + value_template: "{{ value_json.highbankv }}" + name: "Highest bank voltage" + unit_of_measurement: "mV" + device_class: "voltage" + + - unique_id: "diybms.lowest_cell_voltage" + value_template: "{{ value_json.lowcellv }}" + name: "Lowest cell voltage" + unit_of_measurement: "mV" + device_class: "voltage" + + - unique_id: "diybms.highest_cell_voltage" + value_template: "{{ value_json.highcellv }}" + name: "Highest cell voltage" + unit_of_measurement: "mV" + device_class: "voltage" + + - unique_id: "diybms.highest_external_temp" + value_template: "{{ value_json.highextt }}" + name: "Highest cell temperature" + unit_of_measurement: "°C" + device_class: "temperature" + + - unique_id: "diybms.highest_internal_temp" + value_template: "{{ value_json.highintt }}" + name: "Highest passive balance temperature" + unit_of_measurement: "°C" + device_class: "temperature" + + - unique_id: "diybms.current" + value_template: "{% if 'c' in value_json %}{{ value_json.c }}{% else %}0{% endif %}" + name: "DC Current" + unit_of_measurement: "A" + device_class: "current" + icon: "mdi:current-dc" + + - unique_id: "diybms.voltage" + value_template: "{% if 'v' in value_json %}{{ value_json.v }}{% else %}0{% endif %}" + name: "DC voltage" + unit_of_measurement: "V" + device_class: "voltage" + + - unique_id: "diybms.power" + value_template: "{% if 'pwr' in value_json %}{{ value_json.pwr }}{% else %}0{% endif %}" + name: "Battery power" + unit_of_measurement: "W" + device_class: "power" + + - unique_id: "diybms.stateofcharge" + value_template: "{% if 'soc' in value_json %}{{ value_json.soc }}{% else %}0{% endif %}" + name: "State of charge" + unit_of_measurement: "%" + device_class: "battery" + + - unique_id: "diybms.dynamic_charge_voltage" + value_template: "{% if 'dyncv' in value_json %}{{ value_json.dyncv }}{% else %}0{% endif %}" + name: "Dynamic charge voltage" + unit_of_measurement: "V" + device_class: "voltage" + + - unique_id: "diybms.dynamic_charge_current" + value_template: "{% if 'dyncc' in value_json %}{{ value_json.dyncc }}{% else %}0{% endif %}" + name: "Dynamic charge current" + unit_of_measurement: "A" + device_class: "current" + + binary_sensor: + - unique_id: "diybms.charge_allowed" + value_template: "{{ value_json.chgallow }}" + name: "Battery charging allowed" + - unique_id: "diybms.discharge_allowed" + value_template: "{{ value_json.dischgallow }}" + name: "Battery discharging allowed" +``` \ No newline at end of file