/*************************************************************************/ /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ /*************************************************************************/ // Project URL: // // http://grimore.org/iot/reading_victron_energy_serial_data // // // // The following Arduino sketch implements a reader for all devices // // created by Victron Energy by following the VE.Direct Protocol // // description. The sketch captures VE.Direct frames, computes the // // checksum of each frame and if the frame is valid then a JSON payload // // is generated and published to an MQTT broker. // // // // Connecting to a Victron Energy devices involves creating a cable that // // is specific to the device to be interfaced with. However, the // // interface will always be an RS232 serial port interface such that the // // serial ports on an ESP board can be used. // // // // For example, a Victron Energy Power Inverter has a port on the upper // // side that consists in three pins, typically, from left-to right in // // order carrying the meaning: GND, RX, TX, 5V with the RX and TX having // // to be inverted (crossed-over) in order to interface with the device. // // // // For simplicity's sake, the sketch just uses the default serial port // // which is the same one that is used typically to connect to the ESP // // board using an USB cable. By consequence, serial port functions such // // as Serial.println() should be avoided because they will end up // // sending data to the device instead of through the USB cable. // /////////////////////////////////////////////////////////////////////////// // Removing comment for debugging over the first serial port. //#define DEBUG 1 // The AP to connect to via Wifi. #define STA_SSID "" // The AP Wifi password. #define STA_PSK "" // The MQTT broker to connect to. #define MQTT_HOST "" // The MQTT broker username. #define MQTT_USERNAME "" // The MQTT broker password. #define MQTT_PASSWORD "" // The MQTT broker port. #define MQTT_PORT 1883 // The default MQTT client ID is "esp-CHIPID" where CHIPID is the ESP8266 // or ESP32 chip identifier. #define MQTT_CLIENT_ID() String("esp-" + String(GET_CHIP_ID(), HEX)) // The authentication password to use for OTA updates. #define OTA_PASSWORD "" // The OTA port on which updates take place. #define OTA_PORT 8266 // The default topic that the sketch subscribes to is "esp/CHIPID" where // CHIPID is the ESP8266 or ESP32 chip identifier. #define MQTT_TOPIC() String("esp/" + String(GET_CHIP_ID(), HEX)) // Platform specific defines. #if defined(ARDUINO_ARCH_ESP8266) #define GET_CHIP_ID() (ESP.getChipId()) #elif defined(ARDUINO_ARCH_ESP32) #define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32)) #endif // Miscellaneous defines. //#define CHIP_ID_HEX (String(GET_CHIP_ID()).c_str()) #define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX)) // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf // matches a message from a VE Direct frame #define VE_DIRECT_MESSAGE_REGEX "\r\n([a-zA-Z0-9_]+)\t([^\r\n]+)" // Platform specific libraries. #if defined(ARDUINO_ARCH_ESP8266) #include #include #elif defined(ARDUINO_ARCH_ESP32) #include #include #endif // General libraries. #include #include #include #include #include #if defined(ARDUINO_ARCH_ESP32) #include #include #endif WiFiClient espClient; PubSubClient mqttClient(espClient); StaticJsonDocument<512> veFrame; String veFrameBuffer = ""; bool mqttConnect() { #ifdef DEBUG Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST)); #endif mqttClient.setServer(MQTT_HOST, MQTT_PORT); StaticJsonDocument<256> msg; if (mqttClient.connect(MQTT_CLIENT_ID().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) { #ifdef DEBUG Serial.println("Established connection with MQTT broker using client ID: " + MQTT_CLIENT_ID()); #endif msg["action"] = "connected"; char output[512]; serializeJson(msg, output, 512); mqttClient.publish(MQTT_TOPIC().c_str(), output); #ifdef DEBUG Serial.println("Attempting to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif if (!mqttClient.subscribe(MQTT_TOPIC().c_str())) { #ifdef DEBUG Serial.println("Failed to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif return false; } #ifdef DEBUG Serial.println("Subscribed to MQTT topic: " + MQTT_TOPIC()); #endif msg.clear(); msg["action"] = "subscribed"; serializeJson(msg, output, 512); mqttClient.publish(MQTT_TOPIC().c_str(), output); return true; } #ifdef DEBUG Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state())); #endif return false; } bool programLoop() { // Process OTA loop first since emergency OTA updates might be needed. ArduinoOTA.handle(); // Process MQTT client loop. if (!mqttClient.connected()) { // If the connection to the MQTT broker has failed then sleep before carrying on. if (!mqttConnect()) { return false; } } return mqttClient.loop(); } //https://www.victronenergy.com/live/vedirect_protocol:faq#q8how_do_i_calculate_the_text_checksum // computes the checksum of a VEDirect frame // return true iff. the checksum of the frame is valid. bool isVEDirectChecksumValid(String frame) { int checksum = 0; for (int i = 0; i < frame.length(); ++i) { checksum = (checksum + frame[i]) & 255; } return checksum == 0; } void frameRegexMatchCallback(const char *match, const unsigned int length, const MatchState &ms) { // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf: // k -> 9 bytes // v -> 33 bytes char k[9]; ms.GetCapture(k, 0); char v[33]; ms.GetCapture(v, 1); // The checksum is irrelevant since the frame has already deemed to be valid. if (String(k) == "Checksum") { return; } veFrame[k] = v; } void setup() { // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf // baud: 19200 // data bits: 8 // parity: none // stop bits: 1 // flow control: none Serial.begin(19200); #ifdef DEBUG Serial.println("Booted, setting up Wifi in 10s..."); #endif delay(10000); WiFi.mode(WIFI_STA); #if defined(ARDUINO_ARCH_ESP8266) WiFi.hostname(HOSTNAME().c_str()); #elif defined(ARDUINO_ARCH_ESP32) WiFi.setHostname(HOSTNAME().c_str()); #endif WiFi.begin(STA_SSID, STA_PSK); while (WiFi.waitForConnectResult() != WL_CONNECTED) { #ifdef DEBUG Serial.println("Failed to connect to Wifi, rebooting in 5s..."); #endif delay(5000); ESP.restart(); } #ifdef DEBUG Serial.print("Connected to Wifi: "); Serial.println(WiFi.localIP()); Serial.println("Setting up OTA in 10s..."); #endif delay(10000); // Port defaults to 8266 ArduinoOTA.setPort(OTA_PORT); // Hostname defaults to esp-[ChipID] ArduinoOTA.setHostname(HOSTNAME().c_str()); // Set the OTA password ArduinoOTA.setPassword(OTA_PASSWORD); ArduinoOTA.onStart([]() { switch (ArduinoOTA.getCommand()) { case U_FLASH: // Sketch #ifdef DEBUG Serial.println("OTA start updating sketch."); #endif break; #if defined(ARDUINO_ARCH_ESP8266) case U_FS: #elif defined(ARDUINO_ARCH_ESP32) case U_SPIFFS: #endif #ifdef DEBUG Serial.println("OTA start updating filesystem."); #endif SPIFFS.end(); break; default: #ifdef DEBUG Serial.println("Unknown OTA update type."); #endif break; } }); ArduinoOTA.onEnd([]() { #ifdef DEBUG Serial.println("OTA update complete."); #endif SPIFFS.begin(); #if defined(ARDUINO_ARCH_ESP8266) // For what it's worth, check the filesystem on ESP8266. SPIFFS.check(); #endif ESP.restart(); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { #ifdef DEBUG Serial.printf("OTA update progress: %u%%\r", (progress / (total / 100))); #endif }); ArduinoOTA.onError([](ota_error_t error) { #ifdef DEBUG Serial.printf("OTA update error [%u]: ", error); #endif switch (error) { case OTA_AUTH_ERROR: #ifdef DEBUG Serial.println("OTA authentication failed"); #endif break; case OTA_BEGIN_ERROR: #ifdef DEBUG Serial.println("OTA begin failed"); #endif break; case OTA_CONNECT_ERROR: #ifdef DEBUG Serial.println("OTA connect failed"); #endif break; case OTA_RECEIVE_ERROR: #ifdef DEBUG Serial.println("OTA receive failed"); #endif break; case OTA_END_ERROR: #ifdef DEBUG Serial.println("OTA end failed"); #endif break; default: #ifdef DEBUG Serial.println("Unknown OTA failure"); #endif break; } ESP.restart(); }); ArduinoOTA.begin(); // Set up MQTT client. mqttClient.setServer(MQTT_HOST, MQTT_PORT); // Touchdown. #ifdef DEBUG Serial.println("Setup complete."); #endif } void loop() { // Check the Wifi connection status. int wifiStatus = WiFi.status(); switch (wifiStatus) { case WL_CONNECTED: if (!programLoop()) { delay(1000); break; } delay(1); break; case WL_NO_SHIELD: #ifdef DEBUG Serial.println("No Wifi shield present."); #endif goto DEFAULT_CASE; break; case WL_NO_SSID_AVAIL: #ifdef DEBUG Serial.println("Configured SSID not found."); #endif goto DEFAULT_CASE; break; // Temporary statuses indicating transitional states. case WL_IDLE_STATUS: case WL_SCAN_COMPLETED: delay(1000); break; // Fatal Wifi statuses trigger a delayed ESP restart. case WL_CONNECT_FAILED: case WL_CONNECTION_LOST: case WL_DISCONNECTED: default: #ifdef DEBUG Serial.println("Wifi connection failed with status: " + String(wifiStatus)); #endif DEFAULT_CASE: delay(10000); ESP.restart(); break; } } void serialEvent() { while (Serial.available()) { // get the new byte: char c = (char)Serial.read(); veFrameBuffer += c; MatchState checksumMatchState; checksumMatchState.Target((char *)veFrameBuffer.c_str()); char result = checksumMatchState.Match("Checksum\t."); // The checksum field that marks the end of the frame has been found. if (result == REGEXP_MATCHED) { // Compute the checksum and see whether the frame is valid. if (!isVEDirectChecksumValid(veFrameBuffer)) { // If the checksum fails to compute then the frame is invalid so discard it. veFrameBuffer = ""; return; } // The frame is valid so match the individual messages. MatchState messageMatchState((char *)veFrameBuffer.c_str()); messageMatchState.GlobalMatch(VE_DIRECT_MESSAGE_REGEX, frameRegexMatchCallback); // Publish the frame. char output[512]; serializeJson(veFrame, output, 512); veFrame.clear(); mqttClient.publish(MQTT_TOPIC().c_str(), output); // Reset the buffer. veFrameBuffer = ""; return; } } }