/arduinoVictronEnergyDirect/arduinoVictronEnergyDirect.ino |
@@ -0,0 +1,385 @@ |
/*************************************************************************/ |
/* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ |
/*************************************************************************/ |
// Project URL: // |
// http://grimore.org/fuss/hardware/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 <ESP8266WiFi.h> |
#include <ESP8266mDNS.h> |
#elif defined(ARDUINO_ARCH_ESP32) |
#include <WiFi.h> |
#include <ESPmDNS.h> |
#endif |
// General libraries. |
#include <WiFiUdp.h> |
#include <ArduinoOTA.h> |
#include <PubSubClient.h> |
#include <ArduinoJson.h> |
#include <Regexp.h> |
#if defined(ARDUINO_ARCH_ESP32) |
#include <FS.h> |
#include <SPIFFS.h> |
#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; |
} |
} |
} |