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.
@@ -591,6 +617,31 @@
API Version 1.X
+
+
+
@@ -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.
-