diff --git a/docs/source/Plugin/P176.rst b/docs/source/Plugin/P176.rst new file mode 100644 index 0000000000..30a0620ac6 --- /dev/null +++ b/docs/source/Plugin/P176.rst @@ -0,0 +1,123 @@ +.. include:: ../Plugin/_plugin_substitutions_p17x.repl +.. _P176_page: + +|P176_typename| +================================================== + +|P176_shortinfo| + +Plugin details +-------------- + +Type: |P176_type| + +Name: |P176_name| + +Status: |P176_status| + +GitHub: |P176_github|_ + +Maintainer: |P176_maintainer| + +Used libraries: |P176_usedlibraries| + +Supported hardware +------------------ + +|P176_usedby| + +Configuration +------------- + +.. image:: P176_DeviceConfiguration.png + +* **Name** In the Name field a unique name should be entered. + +* **Enabled** When unchecked the plugin is not enabled. + +Sensor +^^^^^^ + +See: :ref:`SerialHelper_page` + +Device Settings +^^^^^^^^^^^^^^^ + +* **Baud Rate / Serial config**: See *Serial helper configuration*, above. + +* **RX Buffersize**: The buffer to be used for ESPEasySerial, default and minimum is 128 bytes. Can be expanded up to 2048 bytes if processing of the received data takes too much time and data gets discarded. + +* **Ignore data on checksum error**: The protocol includes a checksum calculation, and when this fails, the data is probably unreliable, and should better be dismissed. + +* **Events only on updated data**: With this checkbox enabled, events and outgoing data to Controllers is only sent if at least 1 new packet is received since the last Interval or TaskRun. When disabled, data is sent out every Interval, when data is available (so at least a single packet is successfully received). This option is only available if Checksum processing is included in the build. + +Led +^^^ + +* **Led pin**: The GPIO pin a Led is connected to. To enable a *data is being processed* activity led. + +* **Led inverted**: Inverts the on/off state for the Led. + +Logging +^^^^^^^ + +* **Enable logging (for debug)**: When enabled, all received data, and the current checksum counter, are logged at INFO level, to validate if, and what, data is being received. During normal operation, this should be disabled. + +* **Quiet logging (reduces logging)**: During normal operation there is some minimal logging available, informing about successfully received packets. This logging is suppressed when Quiet logging is enabled. + +Data Acquisition +^^^^^^^^^^^^^^^^ + +This group of settings, **Single event with all values** and **Send to Controller** settings are standard available configuration items. Send to Controller is only visible when one or more Controllers are configured. + +* **Interval** By default, Interval will be set to 0 sec. In this situation, no data is sent automatically, and no events are generated. The data will still be collected and can be retrieved for use on a display, or further processing from other rules. When an Interval is set the data can be optionally sent to any configured controllers and either a single ``#All`` event or an event per value will be generated. As an alternative, the ``TaskRun`` command can be used to publish the data at any moment. + +Values +^^^^^^ + +The plugin provides all values that are received from a VE.Direct device. By entering the Name of such value here, that value will be made available. Some values are text-only, and can not be used in this way, but from rules these values can still be sent to a controller or shown on a display. + +The number of decimals to use for displaying in the Devices overview and sending to Controllers, can be selected, and an optional formula can be applied to the available value, f.e. for applying some kind of correction. + +In selected builds, per Value **Stats** options are available, that when enabled, will gather the measured data and present most recent data in a graph, as described here: :ref:`Task Value Statistics: ` + +Values that are not configured in the Values section can still be retrieved from rules, or in a display configuration, by using the regular ``[#]`` notation. The ```` is the name as set in the **Name** field, above, and the ```` is the name of the data item, available from the VE.Direct device. These names are *not* case-sensitive. + +An overview of the data is shown when the task is enabled, and data is received (successfully) from a VE.Direct device. + +Example data +------------ + +.. image:: P176_ExampleData.png + +This example shows the data as can be received from a VE.Direct device. + +The *Name* column is what should be used in the Values fields, or when as a ```` from rules or in a display configuration. The exact meaning and unit of each field can be found in the VE.Direct protocol documentation, available from Victron Energy. + +The *Data* column shows the actual data as received. If the checksum validation is enabled, this column may be empty if the checksum could not be verified, like the first (possibly incomplete) packet, but as the frequency of packets is rather high, this column should not often (or long) be empty. + +The *Value* column shows a factored result based on the value, as mV is not always very useful, so that's converted to V, mA to A, Wh to kWh, etc. + +For items that use ON and OFF (or On and Off for older firmwares) as Data, these will be available as 1 and 0 respectively, so they can be used as Values. If needed, a transformation like ``[VE.Direct#Alarm#O]`` can be used to present the value as ON/OFF for 1/0. Adding the ``C`` justification, like ``[VE.Direct#Alarm#O#C]``, will be presented as On/Off. + +It shows that it has received 2 packets, but as a packet only contains a part of the available data, and a next packet another part, etc. there's no fixed relation to the amount of samples, and the number of packets received. Some devices may send all data in a single packet, other devices may need 3 or even 4 packets to send all available data. + +Also, the number of checksum errors is shown, this counter is automatically reset after receiving 50 consecutive correct packets. + +Get Config values +----------------- + +All data values received can be retrieved by using the ``[#]`` syntax. To get the success count and error count, varables ``[#successcount]`` and ``[#errorcount]`` are available, and also the ``[#updated]`` value, to see if data was updated since the last call for this variable (returns 0/1). So this should only be called once for evaluation. The ``errorcount`` value is reset after 50 succesful received packets, so after receiving 50 successful packets it will return 0. + +Change log +---------- + +.. versionchanged:: 2.0 + ... + + |added| 2024-10-27: Initial release. + + + + + diff --git a/docs/source/Plugin/P176_DeviceConfiguration.png b/docs/source/Plugin/P176_DeviceConfiguration.png new file mode 100644 index 0000000000..86eabfd5d9 Binary files /dev/null and b/docs/source/Plugin/P176_DeviceConfiguration.png differ diff --git a/docs/source/Plugin/P176_ExampleData.png b/docs/source/Plugin/P176_ExampleData.png new file mode 100644 index 0000000000..8c13995953 Binary files /dev/null and b/docs/source/Plugin/P176_ExampleData.png differ diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index 0199448d7a..ae2110e897 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -399,6 +399,7 @@ There are different released versions of ESP Easy: ":ref:`P170_page`","|P170_status|","P170" ":ref:`P172_page`","|P172_status|","P172" ":ref:`P173_page`","|P173_status|","P173" + ":ref:`P176_page`","|P176_status|","P176" .. include:: _plugin_sets_overview.repl diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index 0881529656..cb56c37b1a 100644 --- a/docs/source/Plugin/_plugin_categories.repl +++ b/docs/source/Plugin/_plugin_categories.repl @@ -1,7 +1,7 @@ .. |Plugin_Analog_input| replace:: :ref:`P002_page`, :ref:`P007_page`, :ref:`P025_page`, :ref:`P060_page`, :ref:`P097_page` .. |Plugin_Acceleration| replace:: :ref:`P120_page`, :ref:`P125_page` .. |Plugin_Color| replace:: :ref:`P112_page` -.. |Plugin_Communication| replace:: :ref:`P016_page`, :ref:`P020_page`, :ref:`P035_page`, :ref:`P044_page`, :ref:`P054_page`, :ref:`P071_page`, :ref:`P087_page`, :ref:`P089_page`, :ref:`P094_page`, :ref:`P101_page`, :ref:`P118_page` +.. |Plugin_Communication| replace:: :ref:`P016_page`, :ref:`P020_page`, :ref:`P035_page`, :ref:`P044_page`, :ref:`P054_page`, :ref:`P071_page`, :ref:`P087_page`, :ref:`P089_page`, :ref:`P094_page`, :ref:`P101_page`, :ref:`P118_page`, :ref:`P176_page` .. |Plugin_Display| replace:: :ref:`P012_page`, :ref:`P023_page`, :ref:`P036_page`, :ref:`P057_page`, :ref:`P073_page`, :ref:`P075_page`, :ref:`P095_page`, :ref:`P104_page`, :ref:`P116_page`, :ref:`P131_page`, :ref:`P148_page` .. |Plugin_Distance| replace:: :ref:`P013_page`, :ref:`P110_page`, :ref:`P113_page`, :ref:`P134_page` .. |Plugin_Dust| replace:: :ref:`P018_page`, :ref:`P053_page`, :ref:`P056_page`, :ref:`P144_page` diff --git a/docs/source/Plugin/_plugin_substitutions_p17x.repl b/docs/source/Plugin/_plugin_substitutions_p17x.repl index 70c68bb46f..399c59961b 100644 --- a/docs/source/Plugin/_plugin_substitutions_p17x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p17x.repl @@ -36,3 +36,16 @@ .. |P173_maintainer| replace:: `tonhuisman` .. |P173_compileinfo| replace:: `.` .. |P173_usedlibraries| replace:: `.` + +.. |P176_name| replace:: :cyan:`Victron VE.Direct` +.. |P176_type| replace:: :cyan:`Communication` +.. |P176_typename| replace:: :cyan:`Communication - Victron VE.Direct` +.. |P176_porttype| replace:: `.` +.. |P176_status| replace:: :yellow:`ENERGY` +.. |P176_github| replace:: P176_VE_Direct.ino +.. _P176_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P176_VE_Direct.ino +.. |P176_usedby| replace:: `Victron Energy devices supporting the VE.Direct protocol` +.. |P176_shortinfo| replace:: `Victron VE.Direct protocol reader` +.. |P176_maintainer| replace:: `tonhuisman` +.. |P176_compileinfo| replace:: `.` +.. |P176_usedlibraries| replace:: `.` diff --git a/src/_P176_VE_Direct.ino b/src/_P176_VE_Direct.ino new file mode 100644 index 0000000000..5151030ccc --- /dev/null +++ b/src/_P176_VE_Direct.ino @@ -0,0 +1,230 @@ +#include "_Plugin_Helper.h" +#ifdef USES_P176 + +// ####################################################################################################### +// ############################# Plugin 176: Communication - Victron VE.Direct ########################### +// ####################################################################################################### + +/** Changelog: + * 2024-11-24 tonhuisman: Add setting "Events only on updated data" to not generate events/Controller output if no new packets have been received + * This check is independent from the Updated GetConfig value. + * 2024-11-10 tonhuisman: Renamed GetConfig ischanged to updated. + * 2024-11-08 tonhuisman: Add successcount, errorcount and ischanged values for GetConfig. IsChanged will reset the state on each call, so + * should be called only once in a session. + * 2024-10-30 tonhuisman: Accept ON/On and OFF/Off as 1 and 0 numeric values, to be able to use them as Values in the device configuration + * 2024-10-30 tonhuisman: Simplifying the code somewhat, making it more robust, partially by TD-er + * 2024-10-29 tonhuisman: Optimize receiving and processing data, resulting in much lower load (based on suggestions by TD-er) + * Removed the RX Timeout setting, as it doesn't help here, seems to slow things down. + * 2024-10-27 tonhuisman: Update TaskValues as soon as data arrives (successfully), show successfully received packet count, + * reset checksum error count after 50 successful packets + * 2024-10-26 tonhuisman: Add setting for RX buffer size and optional Debug logging. Add Quit log to suppress most logging + * Store original received data names, to show in Device configuration, add default decimals to conversion factors + * Improved serial processing reading per line instead of per data-block, moved to 50/sec + * 2024-10-22 tonhuisman: Add checksum handling, + * Add conversion factors to get more usable values like V, A, Ah, %. Based on VE.Direct protocol manual v. 3.3 + * 2024-10-20 tonhuisman: Start plugin for Victron VE.Direct protocol reader, based on Victron documentation + **/ + +# define PLUGIN_176 +# define PLUGIN_ID_176 176 +# define PLUGIN_NAME_176 "Communication - Victron VE.Direct" +# define PLUGIN_VALUENAME1_176 "V" // battery_voltage +# define PLUGIN_VALUENAME2_176 "I" // battery_current +# define PLUGIN_VALUENAME3_176 "P" // instantaneous_power +# define PLUGIN_VALUENAME4_176 "ERR" // error_code + +# include "./src/PluginStructs/P176_data_struct.h" + +boolean Plugin_176(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_176; + Device[deviceCount].Type = DEVICE_TYPE_SERIAL; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD; + Device[deviceCount].Ports = 0; + Device[deviceCount].FormulaOption = true; + Device[deviceCount].ValueCount = 4; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].TimerOptional = true; + Device[deviceCount].PluginStats = true; + + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_176); + + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_176)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_176)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_176)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_176)); + + break; + } + + case PLUGIN_SET_DEFAULTS: + { + CONFIG_PORT = static_cast(ESPEasySerialPort::serial1); // Serial1 port + int rxPin = 0; + int txPin = 0; + const ESPEasySerialPort port = static_cast(CONFIG_PORT); + + ESPeasySerialType::getSerialTypePins(port, rxPin, txPin); + CONFIG_PIN1 = rxPin; + CONFIG_PIN2 = txPin; + P176_SERIAL_BAUDRATE = P176_DEFAULT_BAUDRATE; + P176_SERIAL_BUFFER = P176_DEFAULT_BUFFER; + # if P176_FAIL_CHECKSUM + P176_SET_FAIL_CHECKSUM(P176_DEFAULT_FAIL_CHECKSUM); + # endif // if P176_FAIL_CHECKSUM + P176_SET_LED_PIN(-1); + P176_SET_QUIET_LOG(true); + } + + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += serialHelper_getSerialTypeLabel(event); + success = true; + break; + } + + case PLUGIN_GET_DEVICEGPIONAMES: + { + serialHelper_getGpioNames(event); + break; + } + + case PLUGIN_WEBFORM_SHOW_GPIO_DESCR: + { + string = strformat(F("LED: %s"), formatGpioLabel(P176_GET_LED_PIN, false).c_str()); + + if (validGpio(P176_GET_LED_PIN) && P176_GET_LED_INVERTED) { + string += F(" (inv)"); + } + success = true; + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + addFormNumericBox(F("Baud Rate"), F("pbaud"), P176_SERIAL_BAUDRATE, 0); + const uint8_t serialConfChoice = serialHelper_convertOldSerialConfig(P176_SERIAL_CONFIG); + serialHelper_serialconfig_webformLoad(event, serialConfChoice); + + addFormNumericBox(F("RX buffersize"), F("pbuffer"), P176_SERIAL_BUFFER, 128, 2048); + addUnit(F("128..2048")); + + # if P176_FAIL_CHECKSUM + addFormCheckBox(F("Ignore data on checksum error"), F("pchksm"), P176_GET_FAIL_CHECKSUM); + # endif // if P176_FAIL_CHECKSUM + # if P176_HANDLE_CHECKSUM + addFormCheckBox(F("Events only on updated data"), F("pupd"), P176_GET_READ_UPDATED); + # endif // if P176_HANDLE_CHECKSUM + + { // Led settings + addFormSubHeader(F("Led")); + + addFormPinSelect(PinSelectPurpose::Generic_output, F("Led pin"), F("pledpin"), P176_GET_LED_PIN); + addFormCheckBox(F("Led inverted"), F("pledinv"), P176_GET_LED_INVERTED); + } + + { + P176_data_struct *P176_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if ((nullptr != P176_data) && (P176_data->getCurrentDataSize() > 0)) { + addFormSubHeader(F("Current data")); + addRowLabel(F("Recently received data")); + P176_data->showCurrentData(); + # if P176_HANDLE_CHECKSUM + addRowLabel(F("Successfully received packets")); + addHtmlInt(P176_data->getSuccessfulPackets()); + addRowLabel(F("Recent checksum errors")); + addHtmlInt(P176_data->getChecksumErrors()); + addUnit(F("reset after 50 successful packets")); + # endif // if P176_HANDLE_CHECKSUM + } + } + # if P176_DEBUG + addFormSubHeader(F("Logging")); + addFormCheckBox(F("Enable logging (for debug)"), F("pdebug"), P176_GET_DEBUG_LOG); + # endif // if P176_DEBUG + addFormCheckBox(F("Quiet logging (reduces logging)"), F("pquiet"), P176_GET_QUIET_LOG); + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + P176_SERIAL_BAUDRATE = getFormItemInt(F("pbaud")); + P176_SERIAL_CONFIG = serialHelper_serialconfig_webformSave(); + P176_SERIAL_BUFFER = getFormItemInt(F("pbuffer")); + P176_SET_LED_PIN(getFormItemInt(F("pledpin"))); + P176_SET_LED_INVERTED(isFormItemChecked(F("pledinv"))); + # if P176_FAIL_CHECKSUM + P176_SET_FAIL_CHECKSUM(isFormItemChecked(F("pchksm"))); + # endif // if P176_FAIL_CHECKSUM + # if P176_HANDLE_CHECKSUM + P176_SET_READ_UPDATED(isFormItemChecked(F("pupd"))); + # endif // if P176_HANDLE_CHECKSUM + # if P176_DEBUG + P176_SET_DEBUG_LOG(isFormItemChecked(F("pdebug"))); + # endif // if P176_DEBUG + P176_SET_QUIET_LOG(isFormItemChecked(F("pquiet"))); + + success = true; + break; + } + + case PLUGIN_INIT: + { + initPluginTaskData(event->TaskIndex, new (std::nothrow) P176_data_struct(event)); + P176_data_struct *P176_data = static_cast(getPluginTaskData(event->TaskIndex)); + + success = (nullptr != P176_data) && P176_data->init(); + + break; + } + + case PLUGIN_READ: + { + P176_data_struct *P176_data = static_cast(getPluginTaskData(event->TaskIndex)); + + success = (nullptr != P176_data) && P176_data->plugin_read(event); + + break; + } + + case PLUGIN_FIFTY_PER_SECOND: + { + P176_data_struct *P176_data = static_cast(getPluginTaskData(event->TaskIndex)); + + success = (nullptr != P176_data) && P176_data->plugin_fifty_per_second(event); + + break; + } + + case PLUGIN_GET_CONFIG_VALUE: + { + P176_data_struct *P176_data = static_cast(getPluginTaskData(event->TaskIndex)); + + success = (nullptr != P176_data) && P176_data->plugin_get_config_value(event, string); + + break; + } + } + return success; +} + +#endif // USES_P176 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 22eb50595f..f1628de17e 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1747,6 +1747,9 @@ To create/register a plugin, you have to : #if !defined(USES_P148) && defined(ESP32) #define USES_P148 // Sonoff POWR3xxD and THR3xxD display #endif + #if !defined(USES_P176) && defined(ESP32) + #define USES_P176 // Communication - Victron VE.Direct + #endif #endif // ifdef PLUGIN_ENERGY_COLLECTION @@ -2482,6 +2485,9 @@ To create/register a plugin, you have to : #ifndef USES_P173 #define USES_P173 // Environment - SHTC3 #endif + #ifndef USES_P176 + #define USES_P176 // Communication - Victron VE.Direct + #endif // Controllers #ifndef USES_C015 diff --git a/src/src/PluginStructs/P176_data_struct.cpp b/src/src/PluginStructs/P176_data_struct.cpp new file mode 100644 index 0000000000..cc74e2e06a --- /dev/null +++ b/src/src/PluginStructs/P176_data_struct.cpp @@ -0,0 +1,459 @@ +#include "../PluginStructs/P176_data_struct.h" + +#ifdef USES_P176 + +# include + +/************************************************************************** +* Constructor +**************************************************************************/ +P176_data_struct::P176_data_struct(struct EventStruct *event) { + _port = static_cast(CONFIG_PORT); + _baud = P176_SERIAL_BAUDRATE; + _config = P176_SERIAL_CONFIG; + _serialBuffer = P176_SERIAL_BUFFER; + _rxPin = CONFIG_PIN1; + _txPin = CONFIG_PIN2; + _ledPin = P176_GET_LED_PIN; + _ledInverted = P176_GET_LED_INVERTED; + # if P176_HANDLE_CHECKSUM && P176_FAIL_CHECKSUM + _failChecksum = P176_GET_FAIL_CHECKSUM; + # endif // if P176_HANDLE_CHECKSUM && P176_FAIL_CHECKSUM + # if P176_HANDLE_CHECKSUM + _readUpdated = P176_GET_READ_UPDATED; + # endif // if P176_HANDLE_CHECKSUM + # if P176_DEBUG + _debugLog = P176_GET_DEBUG_LOG; + # endif // if P176_DEBUG + _logQuiet = P176_GET_QUIET_LOG; + _dataLine.reserve(44); // Should be max. line-length sent, including CR/LF +} + +P176_data_struct::~P176_data_struct() { + delete _serial; +} + +bool P176_data_struct::init() { + if (ESPEasySerialPort::not_set != _port) { + _serial = new (std::nothrow) ESPeasySerial(_port, _rxPin, _txPin, false, _serialBuffer); + + if (nullptr != _serial) { + # if defined(ESP8266) + _serial->begin(_baud, (SerialConfig)_config); + # elif defined(ESP32) + _serial->begin(_baud, _config); + # endif // if defined(ESP8266) + addLog(LOG_LEVEL_INFO, F("Victron: Serial port started")); + + if (validGpio(_ledPin)) { + DIRECT_PINMODE_OUTPUT(_ledPin); + DIRECT_pinWrite(_ledPin, _ledInverted ? 1 : 0); // Led off + } else { + _ledPin = -1; + } + } + } + + return isInitialized(); +} + +/***************************************************** +* plugin_read +*****************************************************/ +bool P176_data_struct::plugin_read(struct EventStruct *event) { + bool success = false; + + if (isInitialized()) { + for (uint8_t i = 0; i < VARS_PER_TASK; ++i) { + String key = getTaskValueName(event->TaskIndex, i); + key.toLowerCase(); + VictronValue value; + + if (getReceivedValue(key, value) && value.isNumeric()) { + UserVar.setFloat(event->TaskIndex, i, value.getNumValue()); + success = true; + } + } + # if P176_HANDLE_CHECKSUM + + if (success && (!_readUpdated || (_readUpdated && + (_successCounter > 0) && (_successCounter != _lastReadCounter)))) { + _lastReadCounter = _successCounter; + } else { + success = false; + } + # endif // if P176_HANDLE_CHECKSUM + } + + return success; +} + +// In order of most values included +// mV, mAh +const char p176_factor_1000[] PROGMEM = + "v|v2|v3|vs|vm|vpv|i|i2|i3|il|ce|h1|h2|h3|h6|h7|h8|h15|h16" +; + +// 0.01 kWh, 0.01 V +const char p176_factor_0_01[] PROGMEM = + "h17|h18|h19|h20|h22|ac_out_v|dc_in_v|" +; + +// promille +const char p176_factor_10[] PROGMEM = + "dm|soc|" +; + +// 0.1 A +const char p176_factor_0_1[] PROGMEM = + "ac_out_i|dc_in_i|" +; + +float P176_data_struct::getKeyFactor(const String& key, + int32_t & nrDecimals) const { + nrDecimals = 0; + + if (key.isEmpty()) { return 1.0f; } + + if (GetCommandCode(key.c_str(), p176_factor_1000) > -1) { + nrDecimals = 3; + return 0.001f; + } + + if (GetCommandCode(key.c_str(), p176_factor_0_01) > -1) { + return 100.0f; + } + + if (GetCommandCode(key.c_str(), p176_factor_10) > -1) { + nrDecimals = 1; + return 0.1f; + } + + if (GetCommandCode(key.c_str(), p176_factor_0_1) > -1) { + return 10.0f; + } + + return 1.0f; +} + +/***************************************************** +* plugin_fifty_per_second +*****************************************************/ +bool P176_data_struct::plugin_fifty_per_second(struct EventStruct *event) { + bool success = false; + + if (isInitialized()) { + if (handleSerial()) { // Read and process data + plugin_read(event); // Update task values + } + success = true; + } + + return success; +} + +/***************************************************** +* plugin_get_config_value +*****************************************************/ +bool P176_data_struct::plugin_get_config_value(struct EventStruct *event, + String & string) { + bool success = false; + + if (isInitialized()) { + const String key = parseString(string, 1, '.'); // Decimal point as separator, by convention + VictronValue value; + + # if P176_HANDLE_CHECKSUM + + if (equals(key, F("successcount"))) { + string = _successCounter; + success = true; + } else + if (equals(key, F("updated"))) { + string = _successCounter != _lastSuccessCounter; + _lastSuccessCounter = _successCounter; + success = true; + } else + if (equals(key, F("errorcount"))) { + string = _checksumErrors; + success = true; + } else + # endif // if P176_HANDLE_CHECKSUM + + if (getReceivedValue(key, value)) { + string = value.getValue(); + success = true; + } + } + + return success; +} + +/***************************************************** +* getCurrentDataSize +*****************************************************/ +size_t P176_data_struct::getCurrentDataSize() const { + return _data.size(); +} + +/***************************************************** +* showCurrentData +*****************************************************/ +bool P176_data_struct::showCurrentData() const { + bool success = false; + + if (_data.size() > 0) { + html_table(EMPTY_STRING); // Sub-table + html_table_header(F("Name"), 125); + html_table_header(F("Data"), 250); + html_table_header(F("Value"), 125); + + for (auto it = _data.begin(); it != _data.end(); ++it) { + html_TR_TD(); + addHtml(it->second.getName()); + html_TD(); + addHtml(it->second.getRawValue()); + html_TD(); + + if (it->second.isNumeric()) { + addHtml(it->second.getValue()); + } + } + html_end_table(); + } + return success; +} + +// Support functions + +/***************************************************** +* getReceivedValue +*****************************************************/ +bool P176_data_struct::getReceivedValue(const String& key, + VictronValue& value) const { + bool success = false; + + if (!key.isEmpty()) { // Find is case-sensitive + # ifdef ESP8266 + const auto it = _data.find(key); + + if (it != _data.end()) { + value = it->second; + # else // ifdef ESP8266 + + if (_data.contains(key)) { + value = _data.at(key); + # endif // ifdef ESP8266 + success = true; + } + + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, strformat(F("P176 : getReceivedValue Key:%s, value:%s"), + key.c_str(), value.getRawValue().c_str())); + } + # endif // if P176_DEBUG + } + return success; +} + +/***************************************************** +* handleSerial +*****************************************************/ +bool P176_data_struct::handleSerial() { + bool enough = false; + bool result = false; // True for a successfully received packet, with a correct checksum or _failChecksum = false + int available = _serial->available(); + uint8_t ch; + + do { + if (available > 0) { + if (_ledPin != -1) { + DIRECT_pinWrite(_ledPin, _ledInverted ? 0 : 1); + } + + ch = static_cast(_serial->read()); + available--; + + # if P176_HANDLE_CHECKSUM + _checksum += ch; + # endif // if P176_HANDLE_CHECKSUM + + switch (ch) { + case '\r': // Ignore as it'll be stripped off + break; + case '\n': + // no need to append before processing, as it's stripped off + processBuffer(_dataLine); // Store data + _dataLine.clear(); + enough = true; + + # if P176_HANDLE_CHECKSUM + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, strformat(F("P176 : Checksum state: %d"), + static_cast(_checksumState))); + } + # endif // if P176_DEBUG + + if (Checksum_state_e::ValidateNext == _checksumState) { + _checksumState = Checksum_state_e::Validating; + } else + if (Checksum_state_e::Starting == _checksumState) { // Start counting after a Checksum (aka 'end of packet') was received + _checksumState = Checksum_state_e::Counting; + _checksum = 0; + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, F("P176 : Start counting for checksum")); + } + # endif // if P176_DEBUG + } + # endif // if P176_HANDLE_CHECKSUM + break; + case '\t': + # if P176_HANDLE_CHECKSUM + + if (equals(_dataLine, F("Checksum"))) { + if (Checksum_state_e::Counting == _checksumState) { + _checksumState = Checksum_state_e::ValidateNext; + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, F("P176 : Validate next checksum")); + } + # endif // if P176_DEBUG + } else { + _checksumState = Checksum_state_e::Starting; + # if P176_FAIL_CHECKSUM + + commitTempData(!_failChecksum); // Discard any data received so far, as we don't know their checksum status + # endif // if P176_FAIL_CHECKSUM + } + } + # endif // if P176_HANDLE_CHECKSUM + _dataLine += static_cast(ch); // append after + break; + default: + _dataLine += static_cast(ch); + break; + } + + # if P176_HANDLE_CHECKSUM + + if (Checksum_state_e::Validating == _checksumState) { + if (_checksum != 0) { + _checksumState = Checksum_state_e::Error; // Error if resulting checksum isn't 0 + _checksumErrors++; + _checksumDelta = 0; + + if (loglevelActiveFor(LOG_LEVEL_ERROR)) { + addLog(LOG_LEVEL_ERROR, strformat(F("Victron: Checksum error, expected 0 but got %d (success: %d errors: %d)"), + _checksum, _successCounter, _checksumErrors)); + } + } else { + _checksumState = Checksum_state_e::Counting; // New packet is expected, start counting immediately + _successCounter++; + _checksumDelta++; + result = true; + + if (_checksumDelta >= 50) { + _checksumErrors = 0; + } + + + if (loglevelActiveFor(LOG_LEVEL_INFO) && !_logQuiet) { + addLog(LOG_LEVEL_INFO, F("Victron: Checksum validated Ok")); + } + } + _checksum = 0; // Clean start + + # if P176_FAIL_CHECKSUM + + const bool checksumSuccess = (Checksum_state_e::Error != _checksumState) || !_failChecksum; + commitTempData(checksumSuccess); + + if (checksumSuccess) { + result = true; // In case of a failed checksum + } + # endif // if P176_FAIL_CHECKSUM + } + # endif // if P176_HANDLE_CHECKSUM + + if (_ledPin != -1) { + DIRECT_pinWrite(_ledPin, _ledInverted ? 1 : 0); + } + } else { + available = _serial->available(); + enough = available <= 0; + } + } while (!enough); + return result; +} + +/***************************************************** +* processBuffer +*****************************************************/ +void P176_data_struct::processBuffer(const String& message) { + if (message.isEmpty()) { return; } + const String name = parseStringKeepCase(message, 1, '\t'); + const String value = parseStringToEndKeepCase(message, 2, '\t'); + String key(name); + key.toLowerCase(); + + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, strformat(F("P176 : Processing data '%s\t%s'" + # if P176_HANDLE_CHECKSUM + ", checksum: %d" + # endif // if P176_HANDLE_CHECKSUM + ), name.c_str(), value.c_str() + # if P176_HANDLE_CHECKSUM + , _checksum + # endif // if P176_HANDLE_CHECKSUM + )); + } + # endif // if P176_DEBUG + + if (!key.isEmpty() && !value.isEmpty() && !equals(key, F("checksum"))) { + auto it = _data.find(key); + + if (it == _data.end()) { + int32_t nrDecimals{}; + float factor = getKeyFactor(key, nrDecimals); + VictronValue val(name, value, factor, nrDecimals); + _data[key] = std::move(val); + } else { + it->second.set(value); + } + } +} + +# if P176_FAIL_CHECKSUM + +/***************************************************** +* commitTempData +*****************************************************/ +bool P176_data_struct::commitTempData(bool checksumSuccess) { + size_t nrChanged = 0; + + for (auto it = _data.begin(); it != _data.end(); ++it) { + if (it->second.commitTempData(checksumSuccess)) { + ++nrChanged; + } + } + # if P176_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && _debugLog) { + addLog(LOG_LEVEL_INFO, strformat(F("P176 : Moving %d _temp items to _data"), nrChanged)); + } + # endif // if P176_DEBUG + return nrChanged != 0; +} + +# endif // if P176_FAIL_CHECKSUM + +#endif // ifdef USES_P176 diff --git a/src/src/PluginStructs/P176_data_struct.h b/src/src/PluginStructs/P176_data_struct.h new file mode 100644 index 0000000000..29b9cd6980 --- /dev/null +++ b/src/src/PluginStructs/P176_data_struct.h @@ -0,0 +1,234 @@ +#ifndef PLUGINSTRUCTS_P176_DATA_STRUCT_H +#define PLUGINSTRUCTS_P176_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#ifdef USES_P176 + +# include + +# define P176_DEBUG 1 // Enable some extra (development) logging + +# define P176_HANDLE_CHECKSUM 1 // Implement checksum? +# define P176_FAIL_CHECKSUM 1 // Fail/ignore data on checksum errors (optional)? + +# define P176_SERIAL_CONFIG PCONFIG(0) +# define P176_SERIAL_BAUDRATE PCONFIG_LONG(1) +# define P176_SERIAL_BUFFER PCONFIG(3) + +# define P176_FLAGS PCONFIG_ULONG(0) +# define P176_FLAG_LED_PIN 0 // 8 bit +# define P176_FLAG_LED_INVERTED 8 // 1 bit +# define P176_FLAG_FAIL_CHECKSUM 9 // 1 bit +# define P176_FLAG_DEBUG_LOG 10 // 1 bit +# define P176_FLAG_LOG_QUIET 11 // 1 bit +# define P176_FLAG_READ_UPDATED 12 // 1 bit +# define P176_GET_LED_PIN get8BitFromUL(P176_FLAGS, P176_FLAG_LED_PIN) +# define P176_SET_LED_PIN(N) set8BitToUL(P176_FLAGS, P176_FLAG_LED_PIN, N) +# define P176_GET_LED_INVERTED bitRead(P176_FLAGS, P176_FLAG_LED_INVERTED) +# define P176_SET_LED_INVERTED(N) bitWrite(P176_FLAGS, P176_FLAG_LED_INVERTED, N) +# if P176_FAIL_CHECKSUM +# define P176_GET_FAIL_CHECKSUM bitRead(P176_FLAGS, P176_FLAG_FAIL_CHECKSUM) +# define P176_SET_FAIL_CHECKSUM(N) bitWrite(P176_FLAGS, P176_FLAG_FAIL_CHECKSUM, N) +# endif // if P176_FAIL_CHECKSUM +# if P176_HANDLE_CHECKSUM +# define P176_GET_READ_UPDATED bitRead(P176_FLAGS, P176_FLAG_READ_UPDATED) +# define P176_SET_READ_UPDATED(N) bitWrite(P176_FLAGS, P176_FLAG_READ_UPDATED, N) +# endif // if P176_HANDLE_CHECKSUM +# if P176_DEBUG +# define P176_GET_DEBUG_LOG bitRead(P176_FLAGS, P176_FLAG_DEBUG_LOG) +# define P176_SET_DEBUG_LOG(N) bitWrite(P176_FLAGS, P176_FLAG_DEBUG_LOG, N) +# endif // if P176_DEBUG +# define P176_GET_QUIET_LOG bitRead(P176_FLAGS, P176_FLAG_LOG_QUIET) +# define P176_SET_QUIET_LOG(N) bitWrite(P176_FLAGS, P176_FLAG_LOG_QUIET, N) + +# define P176_DEFAULT_BAUDRATE 19200 +# define P176_DEFAULT_BUFFER 128 +# define P176_DEFAULT_FAIL_CHECKSUM true + +struct P176_data_struct : public PluginTaskData_base { +public: + + P176_data_struct(struct EventStruct *event); + + P176_data_struct() = delete; + virtual ~P176_data_struct(); + + bool init(); + + bool plugin_read(struct EventStruct *event); + bool plugin_fifty_per_second(struct EventStruct *event); + bool plugin_get_config_value(struct EventStruct *event, + String & string); + size_t getCurrentDataSize() const; + bool showCurrentData() const; + bool isInitialized() const { + return nullptr != _serial; + } + + # if P176_HANDLE_CHECKSUM + uint32_t getSuccessfulPackets() const { + return _successCounter; + } + + uint32_t getChecksumErrors() const { + return _checksumErrors; + } + + # endif // if P176_HANDLE_CHECKSUM + +private: + + struct VictronValue { + VictronValue() {} + + VictronValue(const String & name, + const String& value, + const float & factor, + const int32_t& nrDecimals) + :_name(name), _factor(factor), _nrDecimals(nrDecimals) + { + set(value); + } + + String getName() const { + return _name; + } + + void set(const String& value) { +# if P176_FAIL_CHECKSUM + _valueTemp = value; +# else // if P176_FAIL_CHECKSUM + update(value); +# endif // if P176_FAIL_CHECKSUM + } + +private: + + void update(const String& value) { + _changed = !_value.equals(value); + _value = value; + + if (_changed) { + int32_t iValue = 0; + _isNumeric = true; + + if (validIntFromString(_value, iValue)) { + _numValue = iValue * _factor; + } else + if (_value.equalsIgnoreCase(F("ON"))) { // Can be ON or On + _numValue = 1.0f; + } else + if (_value.equalsIgnoreCase(F("OFF"))) { // Can be OFF or Off + _numValue = 0.0f; + } else { + _isNumeric = false; + } + _changed = false; + } + } + +public: + + bool isNumeric() const { + return _isNumeric; + } + + String getValue() const { + if (!_isNumeric) { + return _value; + } + + return toString(_numValue, _nrDecimals); + } + + float getNumValue() const { + return _numValue; + } + + String getRawValue() const { + return _value; + } + +# if P176_FAIL_CHECKSUM + bool commitTempData(bool checksumSuccess) { + if (_valueTemp.isEmpty()) { + return false; + } + + if (checksumSuccess) { + update(_valueTemp); + } + _valueTemp.clear(); + return checksumSuccess; + } + +# endif // if P176_FAIL_CHECKSUM + +private: + + String _name; + String _value; +# if P176_FAIL_CHECKSUM + String _valueTemp; +# endif // if P176_FAIL_CHECKSUM + float _factor{}; + float _numValue{}; + int32_t _nrDecimals{}; + bool _changed = true; + bool _isNumeric = true; + }; + + float getKeyFactor(const String& key, + int32_t & nrDecimals) const; + bool getReceivedValue(const String& key, + VictronValue& value) const; + bool handleSerial(); + void processBuffer(const String& message); + # if P176_FAIL_CHECKSUM + bool commitTempData(bool checksumSuccess); + # endif // if P176_FAIL_CHECKSUM + + ESPeasySerial *_serial = nullptr; + + # if P176_HANDLE_CHECKSUM + uint32_t _checksumErrors = 0; + uint32_t _checksumDelta = 0; + uint32_t _successCounter = 0; + uint32_t _lastSuccessCounter = 0; + uint32_t _lastReadCounter = 0; + bool _readUpdated = false; + # endif // if P176_HANDLE_CHECKSUM + int _baud = P176_DEFAULT_BAUDRATE; + unsigned int _serialBuffer = P176_DEFAULT_BUFFER; + String _dataLine; + int8_t _ledPin = -1; + bool _ledInverted = false; + ESPEasySerialPort _port = ESPEasySerialPort::not_set; + uint8_t _config = 0; + int8_t _rxPin = -1; + int8_t _txPin = -1; + # if P176_DEBUG + bool _debugLog = false; + # endif // if P176_DEBUG + bool _logQuiet = false; + + # if P176_HANDLE_CHECKSUM + enum class Checksum_state_e: uint8_t { + Undefined = 0, + Starting, + Counting, + ValidateNext, + Validating, + Error, + }; + Checksum_state_e _checksumState = Checksum_state_e::Undefined; + uint8_t _checksum = 0; + bool _failChecksum = false; + # endif // if P176_HANDLE_CHECKSUM + + // Key is stored in lower-case as PLUGIN_GET_CONFIG_VALUE passes in the variable name in lower-case + std::map_data{}; +}; + +#endif // ifdef USES_P176 +#endif // ifndef PLUGINSTRUCTS_P176_DATA_STRUCT_H