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 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/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/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..861d657d 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 @@ -183,6 +184,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; @@ -230,6 +234,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]; @@ -245,6 +251,9 @@ 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 + char homeassist_apikey[24+1]; }; 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/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 d94ef05c..868e531a 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); @@ -42,7 +43,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 +57,19 @@ 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; + +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/HAL_ESP32.cpp b/ESPController/src/HAL_ESP32.cpp index 1601c1c7..2e6d99e5 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,19 +280,34 @@ 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(); - - // 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}; + + 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(); + } + + + // 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 7100e4b0..b7959d40 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 @@ -82,7 +83,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 std::string wificonfigfilename("/diybms/wifi.json"); @@ -319,7 +320,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 +1481,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 +1570,29 @@ 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 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 @@ -1577,79 +1601,94 @@ 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"); + 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_wifi_connect(); + 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; + wifi_count_sta_connected++; + + 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) { - wifi_isconnected = false; - if (s_retry_num < 200) + ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); + wifi_ap_connect_retry_num++; + wifi_count_sta_disconnected++; + + if (wifi_isconnected) { - esp_wifi_connect(); - s_retry_num++; - ESP_LOGI(TAG, "Retry %i, connect to Wifi AP", s_retry_num); + ShutdownAllNetworkServices(); + wifi_isconnected = false; + } + + if (wifi_ap_connect_retry_num < 25) + { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); + ESP_LOGI(TAG, "WIFI connect quick retry %i", wifi_ap_connect_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 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"); + wifi_count_sta_lost_ip++; - // Shut down all TCP/IP reliant services - if (server_running) - { - stop_webserver(_myserver); - server_running = false; - _myserver = nullptr; - } - stopMqtt(); - stopMDNS(); - - esp_wifi_disconnect(); - + ShutdownAllNetworkServices(); wake_up_tft(true); - - // Try and reconnect - esp_wifi_connect(); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - wifi_isconnected = true; + wifi_count_sta_got_ip++; + auto event = (ip_event_got_ip_t *)event_data; - // ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); - s_retry_num = 0; - // xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + + 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); 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); - 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); } } @@ -1782,7 +1821,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)); @@ -1841,15 +1880,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) @@ -2626,11 +2656,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); @@ -2642,7 +2672,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; @@ -2682,6 +2712,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 (;;) @@ -2728,8 +2767,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. @@ -2781,18 +2823,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 @@ -3613,7 +3660,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}, @@ -3627,7 +3674,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}, @@ -3635,6 +3682,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() @@ -3748,10 +3796,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)) { ESP_LOGE(TAG, "LittleFS mount failed, did you upload file system image?"); @@ -3789,6 +3833,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 @@ -3832,6 +3885,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 @@ -3856,7 +3913,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); @@ -3873,10 +3930,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 @@ -4040,7 +4097,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() { @@ -4054,6 +4111,9 @@ void logActualTime() void loop() { + delay(100); + + unsigned long currentMillis = millis(); if (card_action == CardAction::Mount) { @@ -4070,26 +4130,26 @@ 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 - - // 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) { - esp_wifi_connect(); + // Attempt to connect to MQTT if enabled and not already connected + connectToMqtt(); + } + else + { + ESP_LOGI(TAG, "Trying to connect WIFI"); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } - wifitimer = currentMillis; - - // Attempt to connect to MQTT if enabled and not already connected - connectToMqtt(); } + // Wait another 30 seconds + wifitimer = currentMillis + 30000; } // Call update to receive, decode and process incoming packets diff --git a/ESPController/src/mqtt.cpp b/ESPController/src/mqtt.cpp index a8bd8268..b555372b 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; } @@ -42,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 && mqttClient_connected) + 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) @@ -76,77 +91,91 @@ 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, + .buffer_size = 512, + // 30 seconds + .reconnect_timeout_ms = 30000, + .out_buffer_size = 2048, + // 4 seconds + .network_timeout_ms = 4000}; 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 +183,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) @@ -214,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("}"); @@ -239,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(","); @@ -259,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(","); @@ -315,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/pylonforce_canbus.cpp b/ESPController/src/pylonforce_canbus.cpp new file mode 100644 index 00000000..105ba001 --- /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-pyforce"; + +#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..43dfc08a 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"; @@ -41,6 +42,8 @@ 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 canbus_equipment_addr_JSONKEY[] = "canbusequip"; static const char nominalbatcap_JSONKEY[] = "nominalbatcap"; static const char chargevolt_JSONKEY[] = "chargevolt"; static const char chargecurrent_JSONKEY[] = "chargecurrent"; @@ -86,6 +89,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 @@ -117,6 +121,8 @@ 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 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"; @@ -140,6 +146,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"; @@ -174,6 +181,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 +344,6 @@ void SaveConfiguration(diybms_eeprom_settings *settings) } else { - // Save settings MACRO_NVSWRITE(totalNumberOfBanks) MACRO_NVSWRITE(totalNumberOfSeriesModules) @@ -367,7 +374,9 @@ 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(canbusbaud); + MACRO_NVSWRITE_UINT8(canbus_equipment_addr); MACRO_NVSWRITE(currentMonitoring_shuntmv); MACRO_NVSWRITE(currentMonitoring_shuntmaxcur); @@ -409,6 +418,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); @@ -430,6 +440,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); } @@ -508,6 +520,8 @@ void LoadConfiguration(diybms_eeprom_settings *settings) MACRO_NVSREAD_UINT8(rs485stopbits); MACRO_NVSREAD_UINT8(canbusprotocol); MACRO_NVSREAD_UINT8(canbusinverter); + MACRO_NVSREAD(canbusbaud); + MACRO_NVSREAD_UINT8(canbus_equipment_addr) MACRO_NVSREAD(nominalbatcap); MACRO_NVSREAD(chargevolt); MACRO_NVSREAD(chargecurrent); @@ -533,6 +547,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); @@ -554,6 +569,8 @@ void LoadConfiguration(diybms_eeprom_settings *settings) MACRO_NVSREAD(floatvoltagetimer); MACRO_NVSREAD_UINT8(stateofchargeresumevalue); + MACRO_NVSREADSTRING(homeassist_apikey); + nvs_close(nvs_handle); } @@ -582,9 +599,13 @@ 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; + + _myset->canbus_equipment_addr = 0; + _myset->canbusbaud=500; _myset->nominalbatcap = 280; // Scale 1 _myset->chargevolt = 565; // Scale 0.1 _myset->chargecurrent = 650; // Scale 0.1 @@ -994,8 +1015,11 @@ 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_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; @@ -1049,6 +1073,8 @@ 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; @@ -1075,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++) { @@ -1145,6 +1176,8 @@ 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]; @@ -1168,10 +1201,18 @@ 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"]; 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/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 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/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..4be8d34a 100644 --- a/ESPController/src/webserver_helper_funcs.cpp +++ b/ESPController/src/webserver_helper_funcs.cpp @@ -12,6 +12,19 @@ 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 % + + 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.at(random(0, alphabet.length())); + } +} + void setCookieValue() { // We generate a unique number which is used in all following JSON requests @@ -21,18 +34,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_post.cpp b/ESPController/src/webserver_json_post.cpp index 53701d66..045c5fe1 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); @@ -478,6 +481,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 +491,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); @@ -636,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]; @@ -1171,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", @@ -1182,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, @@ -1195,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 5129f6bd..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" @@ -172,7 +173,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; } } @@ -600,6 +601,8 @@ 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; @@ -791,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); @@ -803,11 +834,22 @@ 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; 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; @@ -1248,6 +1290,72 @@ 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, "home assistant api request"); + + 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, 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, nullptr); + } + + int bufferused = 0; + + // 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)) diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm index a562f55b..9bbb122b 100644 --- a/ESPController/web_src/default.htm +++ b/ESPController/web_src/default.htm @@ -343,7 +343,7 @@

Diagnostics

-


+      

       

Running tasks: @@ -372,7 +372,7 @@

Modules

Bypass PWM % Bad packet count Packets received - Balance energy used (mAh) + Balance statistics @@ -482,19 +482,23 @@

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.

+
+ + +
@@ -517,6 +521,28 @@

MQTT

maxlength="32" />
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -591,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. +

+ +
+
+
+ + +
+
+ + +
+ +
+
+
@@ -943,14 +994,14 @@

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.

-
@@ -959,6 +1010,7 @@

Charge/Discharge configuration

+
@@ -970,6 +1022,14 @@

Charge/Discharge configuration

+
+ + +
+
@@ -1094,14 +1154,14 @@

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.

+

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

@@ -1500,7 +1560,7 @@

Controller Firmware Upgrade

Controller Settings

-
+

Modules & Banks

DIYBMS supports up to @@ -1536,7 +1596,9 @@

Modules & Banks

-
+ + +

Network - Running Settings

Wi-Fi "Station" network interface details:

@@ -1602,7 +1664,60 @@

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

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

Network Time Protocol

-
+

Display Settings

diff --git a/ESPController/web_src/pagecode.js b/ESPController/web_src/pagecode.js index 3c606fde..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' }); } ); @@ -1735,11 +1747,18 @@ $(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); $("#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); @@ -1747,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' }); } @@ -1976,7 +1999,8 @@ $(function () { function (data) { $("#canbusprotocol").val(data.chargeconfig.canbusprotocol); - $("#canbusinverter").val(data.chargeconfig.canbusinverter); + $("#canbusinverter").val(data.chargeconfig.canbusinverter); + $("#canbusbaud").val(data.chargeconfig.canbusbaud); $("#nominalbatcap").val(data.chargeconfig.nominalbatcap); $("#chargevolt").val((data.chargeconfig.chargevolt / 10.0).toFixed(1)); @@ -2105,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(); 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 diff --git a/STM32All-In-One/include/cell.h b/STM32All-In-One/include/cell.h index 25c69701..fe79079b 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) @@ -104,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; } @@ -153,10 +135,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 @@ -208,12 +190,9 @@ 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}; - uint32_t bypassStartTime{0}; - uint16_t bypassStartVoltage{0}; float MilliAmpHourBalanceCounter{0}; 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/platformio.ini b/STM32All-In-One/platformio.ini index 3867256e..a369aec8 100644 --- a/STM32All-In-One/platformio.ini +++ b/STM32All-In-One/platformio.ini @@ -9,12 +9,10 @@ ; 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] -;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 @@ -36,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 fae8015e..c0de631a 100644 --- a/STM32All-In-One/src/main.cpp +++ b/STM32All-In-One/src/main.cpp @@ -31,14 +31,10 @@ 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 "SPI.h" @@ -51,10 +47,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 @@ -78,7 +70,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); @@ -111,6 +102,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 @@ -342,28 +343,6 @@ void ADCSampleCellVoltages(uint8_t cellCount, std::array &rawADC) } } -/// @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() { @@ -529,7 +508,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(); @@ -545,7 +525,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) @@ -553,7 +544,6 @@ void setup() ErrorFlashes(3); } - ParasiticCapacitanceChargeInjectionErrorCalibration(number_of_active_cells, celldata); BufferAmplifierOffsetCalibration(); configureModules(); @@ -612,16 +602,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) { @@ -659,11 +639,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) @@ -689,10 +697,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) @@ -722,6 +740,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) @@ -743,10 +771,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) @@ -761,25 +790,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 code - 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; @@ -834,7 +857,6 @@ void CalculateCellVoltageMinMaxAvg(CellData &cd, /// @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; @@ -889,29 +911,63 @@ 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; + // 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. + // 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; + // Should any cells require balancing - check if they have gone over the threshold - cb = CalculateCellBalanceRequirement(celldata, number_of_active_cells); + return CalculateCellBalanceRequirement(celldata, number_of_active_cells, runawaycellindex); +} - // 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 (highestmV > PP.getRunAwayCellMinimumVoltage() && highest_average_diff > PP.getRunAwayCellDifferential()) +// 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++) { - cb = cb | (uint16_t)(1U << (15 - highest_index)); + // 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); } - return cb; + 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() { @@ -924,12 +980,26 @@ 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) - 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 in a blocking delay call + 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 @@ -987,19 +1057,27 @@ void loop() waitbeforebalance--; } - // Service the serial port/queue - for (size_t i = 0; i < 300; i++) + // This needs to be below all other cell checking code + ServiceSerialPort(); + + // 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(); + BufferAmplifierOffsetCalibration(); + } - // Allow data to be received in buffer (delay must be AFTER) checkInputStream - delay(1); + if (serialBaudScanning) + { + 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() % 4096 == 0) + // Sleep for 800ms + // 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) { - BufferAmplifierOffsetCalibration(); + delay(2); + countdown--; } + }