arduino-sketches – Rev 18

Subversion Repositories:
Rev:
/*************************************************************************/
/*    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;
    }
  }
}

Generated by GNU Enscript 1.6.5.90.