arduino-sketches

Subversion Repositories:
Compare Path: Rev
With Path: Rev
?path1? @ 17  →  ?path2? @ 18
/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;
}
}
}