diff --git a/README.md b/README.md index 29e9828..40e027d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,32 @@ -# VictronVEDirectArduino +# Victron VE.Direct Arduino library Light-weight Arduino library to read basic data using the VE.Direct protocol from Victron Energy Built as part of a larger project, now making it available separately in case others find it useful. Setup: - - An Arduino(ish) board - - A Victron Energy device that sends serial data using the text version of the VE.Direct protocol + - An Arduino(ish) board, works on Particle devices like Particle Photon and Particle Argon + - A Victron Energy device that sends serial data using the text version of the VE.Direct protocol (little different values for different devices) - A 5v to 3.3v serial converter (BMV is 3.3v - don't plug it directly into an Arduino!) + - Good to know is hat BMW-712 takes 3.3v in the VE.Direct port but the SmartSolar MPPT 75/15 need 5v to communicate on the VE.Direct port - Plugged into the Arduino on a serial port (eg Serial1, Serial2 etc) - See also: https://www.victronenergy.com/live/vedirect_protocol:faq - - Developed and tested with a BMV-700 battery monitor - Distributed under an MIT license - see LICENCE.txt - + - Developed and tested with: MPPT 75/15, MPPT 100/30, BMW-712 + Provides: - - Access to basic energy readings - Volts, Power, Current, State of Charge (SOC) + - Access to the full protocol of VE.Direct variables like Power, Voltages, Panel Voltage and Panel Power depending on device. - A diagnostic "full dump" of everything coming from the device -### Usage: +Code examples exists in the examples directory and the library you find in the src directory. + +### Example of basic usage: #include "VEDirect.h" - VEDirect my_bmv(Serial3); + VEDirect my_bmv(Serial1); if my_bmv.begin() { - my_int32 = my_bmv.read(VE_SOC); + int32_t my_int32 = my_bmv.read(VE_SOC); } - // VE_SOC, VE_VOLTAGE, VE_CURRENT, VE_POWER + // Data you can get: VE_FW, VE_VOLTAGE, VE_CURRENT, VE_VOLTAGE_PV, VE_POWER_PV, VE_STATE, VE_MPPT, VE_ERROR, VE_LOAD, VE_YIELD_TOTAL, VE_YIELD_TODAY, VE_POWER_MAX_TODAY, VE_YIELD_YESTERDAY, VE_POWER_MAX_YESTERDAY, VE_DAY_SEQUENCE_NUMBER, VE_LAST_LABEL, VE_SOC, VE_POWER, VE_ALARM diff --git a/VEDirect.cpp b/VEDirect.cpp deleted file mode 100644 index 662f2ef..0000000 --- a/VEDirect.cpp +++ /dev/null @@ -1,247 +0,0 @@ -/****************************************************************** - VEDirect Arduino - - Copyright 2018, 2019, Brendan McLearie - Distributed under MIT license - see LICENSE.txt - - See README.md - - File: VEDirect.h - - Class / enums / API -******************************************************************/ - -#include "VEDirect.h" - -VEDirect::VEDirect(HardwareSerial& port): - VESerial(port) - // Initialise the serial port that the - // VE.Direct device is connected to and - // store it for later use. -{ -} - -VEDirect::~VEDirect() { - // virtual destructor -} - -uint8_t VEDirect::begin() { - // Check connection the serial port - VESerial.begin(19200); - if (VESerial) { - delay(500); - if(VESerial.available()) { - VESerial.flush(); - VESerial.end(); - return 1; - } - } - return 0; -} - -int32_t VEDirect::read(uint8_t target) { - // Read VE.Direct text blocks from the serial port - // Search for the label specified by enum target_label - // Extract and return the corresponding value. - - int32_t ret = 0; // The value to be returned - char VE_line[VED_LINE_SIZE]; // Line buffer - char* label; - char* value; - uint8_t buf_idx = 0; - - const char delim[2] = "\t"; // Delim between label and value - // Simple state machine as to navigate the - // flow of text data that that is sent every second - uint8_t block_count = 0; - uint8_t cr = 0; - uint8_t checksum = 0; - - uint8_t b; // byte read from the stream - - VESerial.begin(19200); - - if (VESerial) { - // BMV continuously transmits 2x text data blocks to deliver all data. - // Read 3x times, discarding the first (likely) partial block, - // to get the two complete data blocks. - while (block_count < 3) { - if (VESerial.available()) { - // Get the next byte from the serial stream - b = VESerial.read(); - switch (b) { - case '\n': // start of newline - reset read buffer - cr = 0; - VE_line[0] = '\0'; - buf_idx = 0; - break; - case '\r': // eol - terminate the buffer - cr = 1; - VE_line[buf_idx] = '\0'; - buf_idx++; - break; - default: - // Is the checksum expected? - if (checksum) { - // TODO: Capture it and use it later - // Currently: just ignore it and the preceding \t - // Assume eol, reset and increment the block_count - if (b != '\t') { - // then it a checksum byte - cr = 0; - VE_line[0] = '\0'; - buf_idx = 0; - block_count++; - checksum = 0; - } // no else - was the \t before, ignore - } else { - // Normal char of interest - // Clear cr flag which may have been set - cr = 0; - // Add the char to the buffer - VE_line[buf_idx] = b; - buf_idx++; - // Check for the Checksum label - // Turn on flag to trigger checksum - // \t and byte capture on next loops - if (strncmp(VE_line, "Checksum", 8) == 0) { - VE_line[8] = '\0'; - checksum = 1; - } - } - } - } - // Evaluate the flags and buffer contents - if (cr && buf_idx) { - // whole line in buffer - label = strtok(VE_line, delim); - value = strtok(0, delim); - // Look for the label passed requested on he call - switch (target) { - case VE_SOC: - if (strcmp(label, "SOC") == 0) { - sscanf(value, "%ld", &ret); - return ret; - } - break; - case VE_VOLTAGE: - if (strcmp(label, "V") == 0) { - sscanf(value, "%ld", &ret); - return ret; - } - break; - case VE_POWER: - if (strcmp(label, "P") == 0) { - sscanf(value, "%ld", &ret); - return ret; - } - break; - case VE_CURRENT: - if (strcmp(label, "I") == 0) { - sscanf(value, "%ld", &ret); - return ret; - } - break; - default: - break; - } - } - } - // Tidy up - VESerial.flush(); - VESerial.end(); - } - return ret; -} - -void VEDirect::copy_raw_to_serial0() { - // Read VE.Direct text blocks from the serial port - // Buffer them and then print them to Serial - // ******* - // NOTE: Do not use this function for anything serious - // To allow lower Serial0 speed (eg 9600) it buffers - // the input from the VE device (at 19200) and then prints it - // It is memory / malloc ugly and without fail safes - // Only useful for low level port dumping - - typedef struct BUFFER { - char* line; - struct BUFFER* next; - } bufline; - - bufline* head = (bufline *)malloc(sizeof(bufline)); - head->next = NULL; - bufline* walker = head; - - char VE_line[VED_LINE_SIZE]; // Line buffer - uint8_t buf_idx = 0; - - // Simple state machine as to navigate the - // flow of text data that that is sent every second - uint8_t block_count = 0; - uint8_t newline = 0; - - uint8_t b; // byte read from the stream - - VESerial.begin(19200); - - if (VESerial) { - // BMV continuously transmits 2x text data blocks to deliver all data. - // Read 3x times, discarding the first (likely) partial block, - // to get the two complete data blocks. - while (block_count < 15) { - if (VESerial.available()) { - // Get the next byte from the serial stream - b = VESerial.read(); - switch (b) { - case '\r': - break; - case '\n': // terminate the buffer - newline = 1; - VE_line[buf_idx] = '\0'; - break; - default: - // Normal char of interest - newline = 0; - // Add the char to the buffer - VE_line[buf_idx] = b; - buf_idx++; - - if (strncmp(VE_line, "Checksum", 8) == 0) { - VE_line[8] = '\0'; - block_count++; - } - - } - } - // Evaluate the flags and buffer contents - if (newline && buf_idx) { - // whole line in buffer - walker->line = (char *)malloc(strlen(VE_line) +10); - strcpy(walker->line, VE_line); - walker->next =(bufline *)malloc(sizeof(bufline)); - walker = walker->next; - walker->next = NULL; - - newline = 0; - VE_line[0] = '\0'; - buf_idx = 0; - - } - } - // print it all - Serial.println("Out of collect loop"); - walker = head; - while (walker->next != NULL) { - Serial.println(walker->line); - free(walker->line); - walker = walker->next; - free(head); - head = walker; - } - free(head); - - // Tidy up - VESerial.flush(); - VESerial.end(); - } -} diff --git a/VEDirect.h b/VEDirect.h deleted file mode 100644 index 6a6e455..0000000 --- a/VEDirect.h +++ /dev/null @@ -1,41 +0,0 @@ -/****************************************************************** - VEDirect Arduino - - Copyright 2018, 2019, Brendan McLearie - Distributed under MIT license - see LICENSE.txt - - See README.md - - File: VEDirect.h - - Class / enums / API -******************************************************************/ - -#ifndef VEDIRECT_H_ -#define VEDIRECT_H_ - -#include - -#define VED_LINE_SIZE 20 // Seems to be plenty. VE.Direc protocol could change. - -enum VE_DIRECT_DATA { - VE_SOC = 0, - VE_VOLTAGE, - VE_POWER, - VE_CURRENT -}; - -class VEDirect { -private: - HardwareSerial& VESerial; -public: - VEDirect(HardwareSerial& port); - virtual ~VEDirect(); - uint8_t begin(); - int32_t read(uint8_t target); - void copy_raw_to_serial0(); -}; - - - - -#endif /* VEDIRECT_H_ */ diff --git a/example.ino b/example.ino deleted file mode 100644 index eb69441..0000000 --- a/example.ino +++ /dev/null @@ -1,56 +0,0 @@ -/****************************************************************** - VEDirect Arduino - - Copyright 2018, 2019, Brendan McLearie - Distributed under MIT license - see LICENSE.txt - - See README.md - - File: example.ino / example.cpp - - Provides example use of the VEDirect library -******************************************************************/ - -#include "Arduino.h" -#include "VEDirect.h" - -// 32 bit ints to collect the data from the device -int32_t VE_soc, VE_power, VE_voltage, VE_current; - -// VEDirect instantiated with relevant serial object -VEDirect myve(Serial3); - -void setup() { - Serial.begin(9600); // Adjust as needed - Serial.println("Reading values from Victron Energy device using VE.Direct text mode"); -} - -void loop() { - // Read the data - if(myve.begin()) { // test connection - VE_soc = myve.read(VE_SOC); - VE_power = myve.read(VE_POWER); - VE_voltage = myve.read(VE_VOLTAGE); - VE_current = myve.read(VE_CURRENT); - } else { - Serial.println("Could not open serial port to VE device"); - while (1); - } - - // Print it each of the values - Serial.println("Values Received"); - Serial.print("State of Charge (SOC): "); - Serial.println(VE_soc, DEC); - Serial.print("Power: "); - Serial.println(VE_power, DEC); - Serial.print("Voltage "); - Serial.println(VE_voltage, DEC); - Serial.print("Current "); - Serial.println(VE_current, DEC); - Serial.println(); - - // Copy the raw data stream (excluding the checkdum line and byte) to Serial0 - Serial.println("All data from device (excluding checksum line)"); - myve.copy_raw_to_serial0(); - delay(10000); - while(1); -} diff --git a/examples/Particle/victron-bmw.ino b/examples/Particle/victron-bmw.ino new file mode 100644 index 0000000..e69de29 diff --git a/examples/Particle/victron-mppt.ino b/examples/Particle/victron-mppt.ino new file mode 100644 index 0000000..5432a34 --- /dev/null +++ b/examples/Particle/victron-mppt.ino @@ -0,0 +1,50 @@ +/****************************************************************** + VE.Direct Arduino Example for Particle devices + + Copyright 2020, Rickard Nordström Pettersson + Distributed under MIT license - see LICENSE.txt + + See README.md + + File: victron-mppt.ino + - Provides example use of the VEDirect library on a Particle Photon +******************************************************************/ + +#include "VEDirect.h" + +#define PUBLISH_EVENT_NAME "Victron-VEDirect" + +char tmp[200]; + +// 32 bit ints to collect the data from the device +int32_t VE_fw, VE_voltage, VE_current, VE_voltage_pv, VE_power_pv, VE_state, VE_mppt, + VE_error, VE_yield_total, VE_yield_today, VE_power_max_today, VE_yield_yesterday, + VE_power_max_yesterday, VE_day_sequence_number; + +uint8_t VE_load; + +VEDirect myve(Serial1); + +void setup() { + Serial.begin(9600); // Adjust as needed +} + +void loop() { + // Read the data + if(myve.begin()) { + VE_voltage = myve.read(VE_VOLTAGE); + VE_current = myve.read(VE_CURRENT); + VE_voltage_pv = myve.read(VE_VOLTAGE_PV); + VE_power_pv = myve.read(VE_POWER_PV); + VE_state = myve.read(VE_STATE); + VE_error = myve.read(VE_ERROR); + + sprintf(tmp ,"{ \"Voltage\": \"%d\", \"Current\": \"%d\", \"PowerPV\": \"%d\", \"VoltagePV\": \"%d\", \"ErrorCode\": \"%d\", \"StateOfOperation\": \"%d\" }\r\n", VE_voltage, VE_current, VE_power_pv, VE_voltage_pv, VE_error, VE_state); + + Particle.publish(PUBLISH_EVENT_NAME, tmp, 60, PRIVATE); + } else { + // Serial.println("Could not open serial port to VE device"); + } + + delay(60000); +} diff --git a/examples/ReadVEDirect/ReadVEDirect.ino b/examples/ReadVEDirect/ReadVEDirect.ino new file mode 100644 index 0000000..9ccaa5b --- /dev/null +++ b/examples/ReadVEDirect/ReadVEDirect.ino @@ -0,0 +1,124 @@ +/****************************************************************** + VEDirect Arduino + + Copyright 2018, 2019, Brendan McLearie + Distributed under MIT license - see LICENSE.txt + + See README.md + + File: ReadVEDirect.ino / ReadVEDirect.cpp + - Provides example use of the VEDirect library +******************************************************************/ + +#include "Arduino.h" +#include "VEDirect.h" + +// 32 bit ints to collect the data from the device +int32_t VE_fw, VE_voltage, VE_current, VE_voltage_pv, VE_power_pv, VE_state, VE_mppt, + VE_error, VE_yield_total, VE_yield_today, VE_power_max_today, VE_yield_yesterday, + VE_power_max_yesterday, VE_day_sequence_number; +// Boolean to collect an ON/OFF value +uint8_t VE_load; + +String CS0 = "Off"; +String CS2 = "Fault"; +String CS3 = "Bulk"; +String CS4 = "Absorption"; +String CS5 = "Float"; +String ERR0 = "No error"; +String ERR2 = "Battery voltage too high"; +String ERR17 = "Charger voltage too high"; +String ERR18 = "Charger over current"; +String ERR20 = "Bulk time limit exceeded"; +String ERR21 = "Current sensor issue"; +String ERR26 = "Terminals overheated"; +String ERR33 = "Input Voltage too high (solar panel)"; +String ERR34 = "Input current too high (solar panel)"; +String ERR38 = "Input shutdown (due to excessive battery voltage)"; +String ERR116 = "Factory calibration lost"; +String ERR117 = "invalied/incompatible firmware"; +String ERR119 = "User settings invalid"; +// VEDirect instantiated with relevant serial object +VEDirect myve(Serial2); + +void setup() { + Serial.begin(9600); // Adjust as needed +} + +void loop() { + Serial.println("Reading values from Victron Energy device using VE.Direct text mode"); + Serial.println(); + + // Read the data + if(myve.begin()) { // test connection + VE_fw = myve.read(VE_FW); + VE_voltage = myve.read(VE_VOLTAGE); + VE_current = myve.read(VE_CURRENT); + VE_voltage_pv = myve.read(VE_VOLTAGE_PV); + VE_power_pv = myve.read(VE_POWER_PV); + VE_state = myve.read(VE_STATE); + VE_mppt = myve.read(VE_MPPT); + VE_error = myve.read(VE_ERROR); + VE_load = myve.read(VE_LOAD); + VE_yield_total = myve.read(VE_YIELD_TOTAL); + VE_yield_today = myve.read(VE_YIELD_TODAY); + VE_power_max_today = myve.read(VE_POWER_MAX_TODAY); + VE_yield_yesterday = myve.read(VE_YIELD_YESTERDAY); + VE_power_max_yesterday = myve.read(VE_POWER_MAX_YESTERDAY); + VE_day_sequence_number = myve.read(VE_DAY_SEQUENCE_NUMBER); + + } else { + Serial.println("Could not open serial port to VE device"); + //while (1); + } + + // Print each of the values + Serial.print("Voltage "); + Serial.println(VE_voltage, DEC); + Serial.print("Current "); + Serial.println(VE_current, DEC); + Serial.print("Power PV "); + Serial.println(VE_power_pv, DEC); + Serial.print("Voltage PV "); + Serial.println(VE_voltage_pv, DEC); + Serial.print("Yield Total kWh "); + Serial.println(VE_yield_total, DEC); + Serial.print("Yield Today kWh "); + Serial.println(VE_yield_today, DEC); + Serial.print("Yield Yesterday kWh "); + Serial.println(VE_yield_yesterday, DEC); + Serial.print("Max Power Today "); + Serial.println(VE_power_max_today, DEC); + Serial.print("Max Power Yesterday "); + Serial.println(VE_power_max_yesterday, DEC); + Serial.print("MPPT Code "); + Serial.println(VE_mppt, DEC); + Serial.print("MPPT Firmware "); + Serial.println(VE_fw, DEC); + Serial.print("Day Sequence Number "); + Serial.println(VE_day_sequence_number, DEC); + Serial.print("Error Code "); + Serial.println(VE_error, DEC); + Serial.print("Error Code "); + if (VE_error == 0){Serial.println(ERR0);} + if (VE_error == 2){Serial.println(ERR2);} + if (VE_error == 17){Serial.println(ERR17);} + if (VE_error == 18){Serial.println(ERR18);} + if (VE_error == 20){Serial.println(ERR20);} + Serial.print("State of operation "); + Serial.println(VE_state, DEC); + Serial.print("State of operation "); + if (VE_state == 0){Serial.println(CS0);} + if (VE_state == 2){Serial.println(CS2);} + if (VE_state == 3){Serial.println(CS3);} + if (VE_state == 4){Serial.println(CS4);} + if (VE_state == 5){Serial.println(CS5);} + Serial.println(); + + // Copy the raw data stream (minus the \r) to Serial0 + // Call read() with a token that won't match any VE.Direct labels + Serial.println("All data from device:"); + myve.read(VE_DUMP); + Serial.println(); + delay(10000); +} diff --git a/src/VEDirect.cpp b/src/VEDirect.cpp new file mode 100644 index 0000000..68c720f --- /dev/null +++ b/src/VEDirect.cpp @@ -0,0 +1,116 @@ +/****************************************************************** + VEDirect Arduino + + Copyright 2018, 2019, Brendan McLearie + Distributed under MIT license - see LICENSE.txt + + See README.md + + File: VEDirect.cpp + - Implementation + Updates: + - 2019-07-14 See VEDirect.h +******************************************************************/ + +#include "VEDirect.h" + +VEDirect::VEDirect(HardwareSerial& port): + VESerial(port) + // Initialise the serial port that the + // VE.Direct device is connected to and + // store it for later use. +{ +} + +VEDirect::~VEDirect() { + // virtual destructor +} + +uint8_t VEDirect::begin() { + // Check connection the serial port + VESerial.begin(19200); + if (VESerial) { + delay(500); + if(VESerial.available()) { + VESerial.flush(); + VESerial.end(); + return 1; + } + } + return 0; +} + +int32_t VEDirect::read(uint8_t target) { + // Read VE.Direct lines from the serial port + // Search for the label specified by enum target + // Extract and return the corresponding value + // If value is "ON" return 1. If "OFF" return 0; + + uint16_t loops = VED_MAX_READ_LOOPS; + uint8_t lines = VED_MAX_READ_LINES; + int32_t ret = 0; // The value to be returned + char line[VED_LINE_SIZE] = "\0"; // Line buffer + uint8_t idx = 0; // Line buffer index + char* label; + char* value_str; + int8_t b; // byte read from the stream + + VESerial.begin(VED_BAUD_RATE); + + while (lines > 0) { + if (VESerial.available()) { + while (loops > 0) { + b = VESerial.read(); + if ((b == -1) || (b == '\r')) { // Ignore '\r' and empty reads + loops--; + } else { + if (b == '\n') { // EOL + break; + } else { + if (idx < VED_LINE_SIZE) { + line[idx++] = b; // Add it to the buffer + } else { + return 0; // Buffer overrun + } + } + } + } + line[idx] = '\0'; // Terminate the string + + // Line in buffer + if (target == VE_DUMP) { + // Diagnostic routine - just print to Serial0; + Serial.println(line); + // Continue on rather than break to reset for next line + } + + label = strtok(line, "\t"); + if (strcmp_P(label, ved_labels[target]) == 0) { + value_str = strtok(0, "\t"); + if (value_str[0] == 'O') { //ON OFF type + if (value_str[1] == 'N') { + ret = 1; // ON + break; + } else { + ret = 0; // OFF + break; + } + } else { + sscanf(value_str, "%ld", &ret); + break; + } + } else { // Line not of interest + lines--; + loops = VED_MAX_READ_LOOPS; + line[0] = '\0'; + idx = 0; + } + } + } + return ret; +} + +void VEDirect::copy_raw_to_serial0() { + read(VE_DUMP); +} + diff --git a/src/VEDirect.h b/src/VEDirect.h new file mode 100644 index 0000000..9ebc07e --- /dev/null +++ b/src/VEDirect.h @@ -0,0 +1,92 @@ +/****************************************************************** + VEDirect Arduino + + Copyright 2018, 2019, 2020 Brendan McLearie + Distributed under MIT license - see LICENSE.txt + + See README.md + + File: VEDirect.h + - Class / enums / API + + Updates: + - 2019-07-14: + - Rewrite of read - cleaner. + - Target labels extendible with enum and PROGMEM strings + - Retired copy_raw_to_serial0() code - use VE_DUMP on read + - Added some tunable parameters see #defines + - 2020-08-24 - Contribution of Rickard Nordström Pettersson + - Added VE_SOC, VE_POWER, VE_ALARM for BMW devices +******************************************************************/ + +#ifndef VEDIRECT_H_ +#define VEDIRECT_H_ + +#include + +// Tunable parameters - defaults tested on mega2560 R3 +#define VED_LINE_SIZE 40 // Seems to be plenty. VE.Direct protocol could change +#define VED_MAX_LEBEL_SIZE 20 // Max length of all labels of interest + '\0'. See ved_labels[] +#define VED_MAX_READ_LOOPS 60000 // How many read loops to be considered a read time-out +#define VED_MAX_READ_LINES 50 // How many lines to read looking for a value + // before giving up. Also determines lines for diag dump +#define VED_BAUD_RATE 19200 + +// Extend this and ved_labels[] for needed inclusions +enum VE_DIRECT_DATA { + VE_DUMP = 0, + VE_FW, + VE_VOLTAGE, + VE_CURRENT, + VE_VOLTAGE_PV, + VE_POWER_PV, + VE_STATE, + VE_MPPT, + VE_ERROR, + VE_LOAD, + VE_YIELD_TOTAL, + VE_YIELD_TODAY, + VE_POWER_MAX_TODAY, + VE_YIELD_YESTERDAY, + VE_POWER_MAX_YESTERDAY, + VE_DAY_SEQUENCE_NUMBER, + VE_SOC, + VE_POWER, + VE_ALARM, + VE_LAST_LABEL, +}; + +const char ved_labels[VE_LAST_LABEL][VED_MAX_LEBEL_SIZE] PROGMEM = { + "Dump", // a string that won't match any label + "FW", + "V", + "I", + "VPV", + "PPV", + "CS", + "MPPT", + "ERR", + "LOAD", + "H19", + "H20", + "H21", + "H22", + "H23", + "HSDS", + "SOC", + "P", + "Alarm" +}; + +class VEDirect { +public: + VEDirect(HardwareSerial& port); + virtual ~VEDirect(); + uint8_t begin(); + int32_t read(uint8_t target); + void copy_raw_to_serial0(); // kept for backwards compatibility +private: + HardwareSerial& VESerial; +}; + +#endif /* VEDIRECT_H_ */