diff --git a/docs/source/Plugin/P078.rst b/docs/source/Plugin/P078.rst index fe33621ea..804a60291 100644 --- a/docs/source/Plugin/P078.rst +++ b/docs/source/Plugin/P078.rst @@ -28,18 +28,235 @@ Supported hardware |P078_usedby| -Sensor -^^^^^^ +Introduction +------------ + +Eastron is the manufacturer of many energy meters. +These can be had in various form factors, from DIN rail modules to panel meters. +There are many variations in number of phases, direct metering or using current transformer clamps (``CT`` in the name). + +Apart from all these differences in features, there are also variations in interfacing to read the meter. +The ESPEasy plugin for these meters only can interact with them using Modbus RTU. + +This Modbus RTU protocol uses the half-duplex RS485 serial interface. + + +RS485 Modbus Interface +---------------------- + +RS485 allows to 'daisy chain' various meters on to the same bus over long distances. (upto 1200 m). + +Interfacing RS485 devices with an ESP module requires some chip to convert typical serial signals to the differential signal required for RS485 devices. +Commonly used chips are the MAX485 or ADM483, but there are lots of other variants which can also be used. + +Typically such a chip has a data input (``DI`` or ``D``) and data output (``RO`` or ``R``). +To toggle between sending and listening, the ``DE`` pin must be pulled 'high' when sending data. + +The ``/RE`` pin is logically inverted and thus can be connected to the ``DE`` pin. +This allows for a single GPIO pin to be used to toggle between sending and receiving data. + +.. note:: + Most RS485 transceiver chips, like the MAX485, allow for a supply voltage (for the chip) higher than the 3.3V used for the ESP. Some even will not work reliable at 3.3 Volt. + Keep in mind that on most RS485 transceiver boards, all microcontroller connected pins have a pull-up resistor to the supplied VCC. + Meaning when you connect this chip to 5V, the signal connected to the ESP pins should be converted down to the lower 3.3V levels the ESP board uses. + + +Manual switching of transmitter/receiver without collision detection +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The most commonly used schematic for connecting a RS485 transceiver chip to a microcontroller, like the ESP boards: + + +.. code-block:: + + VCC ---------------+ + | + +-------x-------+ + RXD <------| RO | + | B|-----------<> B + TXD ------>| DI ADM483 | + ESP | | RS485 bus side + RTS --+--->| DE | + | | A|-----------<> A + +----| /RE | + +-------x-------+ + | + GND + + +Collision detection circuit +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By connecting the ``/RE`` pin to GND, the transceiver chip has "receiving" enabled even when sending. +This allows to detect "collision", when other devices are sending data when the ESP attempts to send data. + +If there is only a single 'master' on the RS485 bus, there is no need for using this kind of setup. + +.. note:: Only ESP32 (and variants) can handle collision detection on RS485. + +.. code-block:: + + VCC ---------------+ + | + +-------x-------+ + RXD <------| R | + | B|----------<> B + TXD ------>| D ADM483 | + ESP32 | | RS485 bus side + RTS ------>| DE | + | A|----------<> A + +----| /RE | + | +-------x-------+ + | | + GND GND + +Wiring RS485 bus +^^^^^^^^^^^^^^^^ + +The RS485 bus transports a differential signal to all connected modules. + +There is some debate about whether the GND should also be connected, since the ``A`` and ``B`` line carry all what is needed. +When there is no common GND among modules and the ESP board, the voltage on both A and B might be too high compared to the module's GND, which may cause damage to the module. +Most cheap MAX485 boards have a 20k pull-up resistor on the ``A`` line and a 20k pull-down resistor on the ``B`` line, to prevent high voltage differences on the MAX485 chip. +But this does not protect the modules when no GND line is present. +So when possible, also wire the GND between modules and the GND of the RS485 transceiver chip. + +Eastron modules do not appear to have a direct connection between GND and mains voltage. But this is not guaranteed for all RS485 modules being sold. +So the best approach is to use proper isolation when using mains connected devices. +For example, the ``ADM2483`` is a galvanic isolated RS485 transceiver chip. + +It is best to twist the A/B lines when using longer wires to reduce effects of noise. + +All modules on the same bus must be daisy chained from one to the other. +A star topology is not allowed. + +.. image:: RS485_wiring.png + :width: 918px + :height: 327px + :scale: 50 % + :alt: RS485 Wiring + :align: center + + +Most MAX485 modules already have a 120 Ohm resistor over the A/B pins. +It is adviced to add another 120 Ohm resistor over the A/B connector of the last module on the bus to act as a 'terminator', to reduce signal reflections. +For short links, a second 120 Ohm resistor is almost never needed, especially given the rather low baud rate used by these modules. + + +Configuration +------------- See: :ref:`SerialHelper_page` +.. note:: All Eastron modules on the same ESPEasy node must use the same serial settings, like baud rate and serial port config. -**TODO**: Complete this documentation... +Device Settings +^^^^^^^^^^^^^^^ -.. Commands available -.. ^^^^^^^^^^^^^^^^^^ +The Model Type selector only limits which values can be selected in the Output Configuration section. +This way, only supported register reads will be offered to select. -.. .. include:: P078_commands.repl +It also is used to simplify the generated strings and suggested task value names. +For example on 1 phase modules, there is no need to specify the phase. + +All Eastron module versions use the same internal register numbering schema. +For example, register ``0x0000`` is always about measuring the voltage between L1 and neutral. +But register ``0x0002`` (voltage between L2 and neutral) is only supported on 3-phase modules. + +So selecting the wrong Model Type may still work fine, only it is possible not all supported options can be selected or unsupported options may be selected. + +Output Configuration +^^^^^^^^^^^^^^^^^^^^ + +ESPEasy only allows upto 4 task values per task. +However the modules made by Eastron allow for much more values to be collected. +For example, the SDM630 has almost 100 values that can be read. + +To allow collecting more values from a module, multiple tasks may be added which all have the same serial port settings and modbus address configured. + +All task running the Eastron plugin will add their configured registers to be read, including the modbus address, are put in a queue in ESPEasy. +This queue is being read continuously and the read values will be immediately set on the output task values of the matching Eastron task. + +ESPEasy tries to read the next register 10x per second. + +The set baud rate determines the number of registers that can be read per second: + +* @2400 baud: 3 - 5 registers per second. +* @9600 baud: 10 registers per second. + +When a task is being read (e.g. per set "Interval" or when calling ``TaskRun`` from the rules), the last read values will be sent to any connected controller and events will be generated with these values. +Thus on a setup which in total reads more than 10 registers (either on a single or multiple modules) can not provide new values on each task run when set to an interval of 1 second. + +With "Stats" enabled, the recorded min/max peak values will be updated on each read of that register. +So when the "Interval" is set to a long interval, the peaks may be taken from the frequent reads inbetween. + + +Units of Measure +^^^^^^^^^^^^^^^^ + +Depending on the model, these Eastron modules can provide various units of measure: + +* Line voltage and THD% (total harmonic distortion) of all phases +* Line Frequency +* Currents, Current demands and current THD% of all phases +* Power, maximum power demand and power factor +* Active energy imported and exported +* Reactive energy imported and exported + +The options with "Demand" in their name, are measurements within a time window. +The default "Demand Time Interval" is a preset period of 60 minutes. + +ESPEasy currently does not support setting the time on these modules, or triggering a new start of a demand period. +Thus the start of such interval may not be on the start of each hour. + + +Set Baudrate/ID +^^^^^^^^^^^^^^^ + +The default baud rate of 2400 of most of these sensors may be too low for using Software Serial. +On ESP8266, the module may rather frequently crash due to watch dog resets when running Software Serial at such low baud rates. + +It is adviced to set the module to 9600 baud. + +Using a higher baud rate will also take less resources of the ESPEasy node. +For example, writing the 8 byte command to the module takes roughly 35 msec @2400 baud and only 13 msec @9600 baud. + +When running at 9600 baud, ESPEasy can typically read upto 10 registers per second for all connected Eastron modules. + +On some modules, like the SDM120, the module settings can only be set via the Modbus interface. + +When an Eastron task is enabled in ESPEasy, it will continously read the set registers on all connected Eastron modules. +This will affect the commands used to change settings and thus this continous reading must be paused. + +The commands in ESPEasy to change settings have a last optional parameter ``node_id``, which is the current set address of the module. +If none is given, the factory default address ``1`` is assumed. +Valid node IDs are ``1 ... 247``. + +Steps to change settings: + +* Pause continously reading registers on the module by calling ``Eastron,Pause`` in ESPEasy. +* Enable ``-set-`` mode on the module by pressing the button on the module for 3 seconds. +* To change the baud rate, call ``Eastron,SetBaud,[,]`` in ESPEasy, where the new baud rate parameter can be given either as a value ``0`` ... ``5``, or the exact (supported) baud rate. (see description below). +* To change the node ID (address), call ``Eastron,SetID,[,]``. +* Press the button on the module again for 3 seconds to save the new values. +* Reading the set registers can be resumed with the command ``Eastron,Resume`` + +Since changing the node ID is effective immediately, it makes sense to change the baud rate first and then the ID if both need to be changed. + +.. note:: The new baud rate will only be used after a power cycle of the module. + + + + +Sensor +------ + + + +Commands available +------------------ + +.. include:: P078_commands.repl .. Events .. ~~~~~~ @@ -52,8 +269,10 @@ Change log .. versionchanged:: 2.0 ... - |added| - Major overhaul for 2.0 release. + |added| 2023-08-06: + * Add support for many more modules + * Add commands to set ID and baud rate + * Massively reduce use of resources when reading sensor .. versionadded:: 1.0 ... diff --git a/docs/source/Plugin/P078_commands.repl b/docs/source/Plugin/P078_commands.repl new file mode 100644 index 000000000..cc0d2355f --- /dev/null +++ b/docs/source/Plugin/P078_commands.repl @@ -0,0 +1,56 @@ +.. csv-table:: + :header: "Command Syntax", "Extra information" + :widths: 20, 30 + + " + + | ``Eastron,Pause`` + | ``Eastron,Resume`` + + "," + | ``Pause`` and ``Resume`` refer to the continuous reading of set registers of all connected Eastron modules. + | When changing a setting on a specific module, this background register reading must be temporarily halted by calling ``Eastron,Pause`` first. + | All commands are case insensitive. + + " + " + + | ``Eastron,SetBaud,[,]`` + | ``Eastron,SetID,[,]`` + + "," + | The optional ```` parameter indicates the current Modbus address. When not given, the default of address ``1`` is assumed. + | Valid node IDs are ``1 ... 247``. + | Call ``Eastron,Pause`` before changing settings on the module. + | On some modules, like the SDM120, the user must press the button for 3 seconds to enter ``-set-`` mode. + | After enabling the ``-set-`` mode, these commands can be given and then the user must press the button for 3 seconds to store the changed settings. + | + | Changing ID is active immediately, thus the new ID must be used for commands given after changing the ID. + | Change of baud rate is only active after the module is power cycled. + | + | Allowed baud rates: + | SDM120 / SDM230: + | ``0`` = 2400 baud (default) + | ``1`` = 4800 baud + | ``2`` = 9600 baud + | ``5`` = 1200 baud + | + | SDM320 / SDM530Y: + | ``0`` = 2400 baud + | ``1`` = 4800 baud + | ``2`` = 9600 baud (default) + | ``5`` = 1200 baud + | + | SDM630 / SDM72 / SDM72V2: + | ``0`` = 2400 baud + | ``1`` = 4800 baud + | ``2`` = 9600 baud (default) + | ``3`` = 19200 baud + | ``4`` = 38400 baud + | + | The baud rate parameter can be given either as a value ``0`` ... ``5``, or the exact (supported) baud rate. + | + | All commands are case insensitive. + + " + diff --git a/docs/source/Plugin/RS485_wiring.png b/docs/source/Plugin/RS485_wiring.png new file mode 100644 index 000000000..bfcd04f5f Binary files /dev/null and b/docs/source/Plugin/RS485_wiring.png differ diff --git a/docs/source/Plugin/_plugin_substitutions_p07x.repl b/docs/source/Plugin/_plugin_substitutions_p07x.repl index 789a75257..8e9d5c6b1 100644 --- a/docs/source/Plugin/_plugin_substitutions_p07x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p07x.repl @@ -99,22 +99,22 @@ .. _P077_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P077_CSE7766.ino .. |P077_usedby| replace:: `Sonoff S31, Sonoff Pow R2, Sonoff POW R3xx(D), Sonoff Dual R3` .. |P077_shortinfo| replace:: This plugin is specifically made for Sonoff devices Sonoff S31, Sonoff Pow R2 and Sonoff POW/Dual R3 models. -.. |P077_maintainer| replace:: `.` +.. |P077_maintainer| replace:: TD-er .. |P077_compileinfo| replace:: `.` .. |P077_usedlibraries| replace:: `ESPEasySerial` -.. |P078_name| replace:: :cyan:`Eastron SDM120C/220T/230/630` +.. |P078_name| replace:: :cyan:`Eastron SDMxxx Modbus` .. |P078_type| replace:: :cyan:`Energy (AC)` -.. |P078_typename| replace:: :cyan:`Energy (AC) - Eastron SDM120C/220T/230/630` +.. |P078_typename| replace:: :cyan:`Energy (AC) - Eastron SDMxxx Modbus` .. |P078_porttype| replace:: `Serial` .. |P078_status| replace:: :yellow:`ENERGY` .. |P078_github| replace:: P078_Eastron.ino .. _P078_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P078_Eastron.ino -.. |P078_usedby| replace:: `.` +.. |P078_usedby| replace:: `Modbus variants of Eastron SDM120M, SDM120CT, SDM220M, SDM230M, SDM72M, SDM72CTM, SDM630, SDM630MCT, SDM630-EV, SDM54-M, SDM320Y, DDM18SD, SMART X-96, MCS-U22` .. |P078_shortinfo| replace:: `.` -.. |P078_maintainer| replace:: `.` +.. |P078_maintainer| replace:: TD-er .. |P078_compileinfo| replace:: `.` -.. |P078_usedlibraries| replace:: `.` +.. |P078_usedlibraries| replace:: https://github.com/reaper7/SDM_Energy_Meter .. |P079_name| replace:: :cyan:`Wemos / Lolin Motorshield` .. |P079_type| replace:: :cyan:`Motor` diff --git a/lib/ESPEasySerial/ESPEasySerial.cpp b/lib/ESPEasySerial/ESPEasySerial.cpp index dc1f9f288..d8f2f21c9 100644 --- a/lib/ESPEasySerial/ESPEasySerial.cpp +++ b/lib/ESPEasySerial/ESPEasySerial.cpp @@ -258,6 +258,17 @@ String ESPeasySerial::getLogString() const { return getSerialConfig().getLogString(); } +bool ESPeasySerial::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) { + if (_serialPort != nullptr) { + #ifdef ESP32 + return _serialPort->setRS485Mode(rtsPin, enableCollisionDetection); + #else + return _serialPort->setRS485Mode(rtsPin, false); + #endif + } + return false; +} + bool ESPeasySerial::isValid() const { // FIXME TD-er: Must call isValid() on the individual _serialPort types return _serialPort != nullptr; diff --git a/lib/ESPEasySerial/ESPeasySerial.h b/lib/ESPEasySerial/ESPeasySerial.h index 2b9f89728..7f9e5ea19 100644 --- a/lib/ESPEasySerial/ESPeasySerial.h +++ b/lib/ESPEasySerial/ESPeasySerial.h @@ -163,6 +163,11 @@ public: return String(); } + // Try enabling RTS pin and set to UART_RS485_HALF_DUPLEX + // RTS pin can then be connected to ~RE/DE pin of MAX485 + // @retval True when supported and successful. + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); + private: bool isValid() const; diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.cpp b/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.cpp index 103326019..e22b15318 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.cpp +++ b/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.cpp @@ -3,6 +3,9 @@ #include "ESPEasySerialType.h" +#ifdef ESP32 +#include +#endif Port_ESPEasySerial_HardwareSerial_t::Port_ESPEasySerial_HardwareSerial_t() {} @@ -268,3 +271,19 @@ size_t Port_ESPEasySerial_HardwareSerial_t::setTxBufferSize(size_t new_size) } return 0; } + + +bool Port_ESPEasySerial_HardwareSerial_t::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) +{ + #ifdef ESP32 + if (_serial != nullptr) { + if (rtsPin >= 0) { + return _serial->setPins(-1, -1, -1, rtsPin) && + _serial->setHwFlowCtrlMode(UART_HW_FLOWCTRL_RTS) && + _serial->setMode(enableCollisionDetection ? UART_MODE_RS485_COLLISION_DETECT : UART_MODE_RS485_HALF_DUPLEX); + } + _serial->setMode(UART_MODE_UART); + } + #endif + return false; +} \ No newline at end of file diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.h b/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.h index 3fc38731c..bdd5cde66 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_HardwareSerial.h @@ -47,6 +47,11 @@ public: size_t setRxBufferSize(size_t new_size); size_t setTxBufferSize(size_t new_size); + // Try enabling RTS pin and set to UART_RS485_HALF_DUPLEX + // RTS pin can then be connected to ~RE/DE pin of MAX485 + // @retval True when supported and successful. + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); + private: HardwareSerial *_serial = nullptr; diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.cpp b/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.cpp index 27cf16a04..38acf8370 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.cpp +++ b/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.cpp @@ -135,4 +135,10 @@ size_t Port_ESPEasySerial_I2C_SC16IS752_t::setTxBufferSize(size_t new_size) return 0; } +bool Port_ESPEasySerial_I2C_SC16IS752_t::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) +{ + // TODO TD-er: Check if we can enable RTS on this chip + return false; +} + #endif // ifndef DISABLE_SC16IS752_Serial diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.h b/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.h index 27bdd4cf0..55776495a 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_I2C_SC16IS752.h @@ -44,6 +44,7 @@ public: size_t setRxBufferSize(size_t new_size); size_t setTxBufferSize(size_t new_size); + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); private: diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.cpp b/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.cpp index e33466bbb..61cbf3df2 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.cpp +++ b/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.cpp @@ -169,4 +169,11 @@ size_t Port_ESPEasySerial_SW_Serial_t::setTxBufferSize(size_t new_size) return 0; } +bool Port_ESPEasySerial_SW_Serial_t::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) +{ + // TODO TD-er: Check if we can include toggling this pin in the SW serial lib + return false; +} + + #endif // if USES_SW_SERIAL diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.h b/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.h index 306bd5f95..5f183f20f 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_SW_Serial.h @@ -46,6 +46,7 @@ public: size_t setRxBufferSize(size_t new_size); size_t setTxBufferSize(size_t new_size); + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); private: #if USES_LATEST_SOFTWARE_SERIAL_LIBRARY diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.cpp b/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.cpp index 658addbbe..2bb6b4ab4 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.cpp +++ b/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.cpp @@ -261,6 +261,12 @@ size_t Port_ESPEasySerial_USBCDC_t::setTxBufferSize(size_t new_size) return 0; } +bool Port_ESPEasySerial_USBCDC_t::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) +{ + return false; +} + + int Port_ESPEasySerial_USBCDC_t::getBaudRate() const { if (_serial != nullptr) { diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.h b/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.h index 35b16e033..1a6ae9b4b 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_USBCDC.h @@ -41,6 +41,8 @@ public: size_t setRxBufferSize(size_t new_size); size_t setTxBufferSize(size_t new_size); + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); + virtual int getBaudRate() const override; private: diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.cpp b/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.cpp index 31bea1630..2fded8da4 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.cpp +++ b/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.cpp @@ -195,4 +195,10 @@ size_t Port_ESPEasySerial_USB_HWCDC_t::setTxBufferSize(size_t new_size) return 0; } +bool Port_ESPEasySerial_USB_HWCDC_t::setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) +{ + return false; +} + + #endif // if USES_HWCDC diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.h b/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.h index 1f3e3d208..b9801fe8c 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_USB_HWCDC.h @@ -40,6 +40,8 @@ public: size_t setRxBufferSize(size_t new_size); size_t setTxBufferSize(size_t new_size); + bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection = false); + private: # if ARDUINO_USB_CDC_ON_BOOT // Serial used for USB CDC diff --git a/lib/ESPEasySerial/Port_ESPEasySerial_base.h b/lib/ESPEasySerial/Port_ESPEasySerial_base.h index 032934223..e317b6764 100644 --- a/lib/ESPEasySerial/Port_ESPEasySerial_base.h +++ b/lib/ESPEasySerial/Port_ESPEasySerial_base.h @@ -72,10 +72,13 @@ public: virtual operator bool() const = 0; - virtual void setDebugOutput(bool) = 0; + virtual void setDebugOutput(bool) = 0; + + virtual size_t setRxBufferSize(size_t new_size) = 0; + virtual size_t setTxBufferSize(size_t new_size) = 0; + virtual bool setRS485Mode(int8_t rtsPin, bool enableCollisionDetection) = 0; + - virtual size_t setRxBufferSize(size_t new_size) = 0; - virtual size_t setTxBufferSize(size_t new_size) = 0; const ESPEasySerialConfig& getSerialConfig() const { return _config; diff --git a/lib/SDM_Energy_Meter/README.md b/lib/SDM_Energy_Meter/README.md index 7728266ec..3f85fcc14 100644 --- a/lib/SDM_Energy_Meter/README.md +++ b/lib/SDM_Energy_Meter/README.md @@ -1,6 +1,4 @@ -## WARNING! library initialization changed! ## -old library version is available at [old_template branch](https://github.com/reaper7/SDM_Energy_Meter/tree/old_template)
-## Library for reading SDM120 SDM220 SDM230 SDM630 Modbus Energy meters. ## +## Library for reading SDM72 SDM120 SDM220 SDM230 SDM630 DDM18SD Modbus Energy meters. ## ### SECTIONS: ### #### 1. [INTRODUCTION](#introduction) #### @@ -8,15 +6,20 @@ #### 3. [CONFIGURING](#configuring) #### #### 4. [INITIALIZING](#initializing) #### #### 5. [READING](#reading) #### -#### 6. [DEBUGING](#debuging) #### +#### 6. [PROBLEMS](#problems) #### #### 7. [CREDITS](#credits) #### --- ### Introduction: ### This library allows you reading SDM module(s) using: -- [x] Hardware Serial (recommended option, smallest number of reads errors) or -- [x] Software Serial ([library for ESP8266](https://github.com/plerup/espsoftwareserial)) +- [x] Hardware Serial (recommended option, smallest number of reads errors, especially for esp8266) or +- [x] Software Serial, attached as core libraries for ESP8266 and AVR or as external lib for ESP32
+ (the new version of esp Software Serial library
+ has a different initialization compared to avr!
+ This version of SDM library (>=2.2.2) works only with esp Software Serial 8.0.1 or higher!!!
+ If you have an older esp Software Serial version
+ then use other SDM library, details below the Credits section
)
you also need rs232<->rs485 converter: - [x] with automatic flow direction control (look at images below) or @@ -24,13 +27,14 @@ you also need rs232<->rs485 converter: (in this case MAX485 DE and RE pins must be connected together to one of uC pin
and this pin must be passed when initializing the library
) -_Tested on Wemos D1 Mini with Arduino IDE 1.8.3-1.9.0b & ESP8266 core 2.3.0-2.4.1_ +_Tested on Wemos D1 Mini with Arduino IDE 1.8.3-1.8.10 & ESP8266 core 2.3.0-2.5.2_ --- ### Screenshots: ### -

+
+

live page example (extended) screenshot

@@ -39,14 +43,26 @@ _Tested on Wemos D1 Mini with Arduino IDE 1.8.3-1.9.0b & ESP8266 core 2.3.0-2.4. ### Configuring: ### Default configuration is specified in the [SDM.h](https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h#L18) file, and parameters are set to:
-Software Serial, baud 4800, uart config SERIAL_8N1, without DE/RE pin.
+Software Serial mode, baud 4800, uart config SERIAL_8N1, without DE/RE pin,
+uart pins for esp32 hwserial and esp32/esp8266/avr swserial as NOT_A_PIN (-1).

+For esp32 hwserial this means using the default pins for the selected uart port,
+specified in the core library (HardwareSerial.cpp).
+For swserial option (esp32/esp8266/avr) is necessary
+to specify the pin numbers, as described below.
User can set the parameters in two ways: - by editing the [SDM_Config_User.h](https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM_Config_User.h) file - by passing values during initialization (section below) +[SDM_Config_User.h](https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM_Config_User.h) file includes also two parameters that can be adjusted depending on your needs: +- WAITING_TURNAROUND_DELAY (default set to 200ms) defines the time (after sending the query) for the response from the slave device. + If the slave device does not send the required number of bytes (FRAMESIZE) within this time, an SDM_ERR_TIMEOUT error will be returned. +- RESPONSE_TIMEOUT (default set to 500ms) defines the time (after sending the request and receiving the reply) to a possible response + from other slave devices on the bus, during this time it will not be possible to execute another query. + It is a protection time for devices that are not able to quickly respond to inquiries. + NOTE for Hardware Serial mode: to force the Hardware Serial mode,
-user must edit the corresponding entry in [SDM_Config_User.h](https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM_Config_User.h#L13) file.
+user must edit the corresponding entry in [SDM_Config_User.h](https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM_Config_User.h#L17) file.
adding #define USE_HARDWARESERIAL to the main ino file is not enough.
--- @@ -58,20 +74,31 @@ initialization is limited to passing serial port reference (software or hardware and looks as follows: ```cpp //lib init when Software Serial is used: -#include #include +#include -SoftwareSerial swSerSDM(13, 15); - -// _software serial reference +// for ESP8266 and ESP32 +SoftwareSerial swSerSDM; +// _______________________________software serial reference // | SDM sdm(swSerSDM); +// for AVR +SoftwareSerial swSerSDM(SDM_RX_PIN, SDM_TX_PIN); +// | |_tx pin definition(from SDM_Config_User.h) +// |_____________rx pin definition(from SDM_Config_User.h) +// +// _______________________________software serial reference +// | +SDM sdm(swSerSDM); +``` + +```cpp //lib init when Hardware Serial is used: #include -// _hardware serial reference +// _________________________________hardware serial reference // | SDM sdm(Serial); ``` @@ -79,28 +106,63 @@ If the user wants to temporarily change the configuration during the initializat then can pass additional parameters as below: ```cpp //lib init when Software Serial is used: -#include #include +#include -SoftwareSerial swSerSDM(13, 15); +// for ESP8266 and ESP32 +SoftwareSerial swSerSDM; +// ________________________________________software serial reference +// | __________________________________baudrate(optional, default from SDM_Config_User.h) +// | | _______________________dere pin for max485(optional, default from SDM_Config_User.h) +// | | | _________software uart config(optional, default from SDM_Config_User.h) +// | | | | _____rx pin number(optional, default from SDM_Config_User.h) +// | | | | | _tx pin number(optional, default from SDM_Config_User.h) +// | | | | | | +SDM sdm(swSerSDM, 9600, NOT_A_PIN, SWSERIAL_8N1, 13, 15); -// __________________software serial reference -// | ____________baudrate(optional, default from SDM_Config_User.h) -// | | _dere pin for max485(optional, default from SDM_Config_User.h) + +// for AVR +SoftwareSerial swSerSDM(10, 11); +// ________________________________________software serial reference +// | __________________________________baudrate(optional, default from SDM_Config_User.h) +// | | _______________________dere pin for max485(optional, default from SDM_Config_User.h) // | | | SDM sdm(swSerSDM, 9600, NOT_A_PIN); +``` - +```cpp //lib init when Hardware Serial is used: #include -// _____________________________________hardware serial reference -// | _______________________________baudrate(optional, default from SDM_Config_User.h) -// | | ____________________dere pin for max485(optional, default from SDM_Config_User.h) -// | | | ________hardware uart config(optional, default from SDM_Config_User.h) -// | | | | _swap hw serial pins from 3/1 to 13/15(optional, default from SDM_Config_User.h) +// for ESP8266 +// ______________________________________hardware serial reference +// | ________________________________baudrate(optional, default from SDM_Config_User.h) +// | | _____________________dere pin for max485(optional, default from SDM_Config_User.h) +// | | | _________hardware uart config(optional, default from SDM_Config_User.h) +// | | | | __swap hw serial pins from 3/1 to 13/15(optional, default from SDM_Config_User.h) // | | | | | SDM sdm(Serial, 9600, NOT_A_PIN, SERIAL_8N1, false); + + +// for ESP32 +// ______________________________________hardware serial reference +// | ________________________________baudrate(optional, default from SDM_Config_User.h) +// | | _____________________dere pin for max485(optional, default from SDM_Config_User.h) +// | | | _________hardware uart config(optional, default from SDM_Config_User.h) +// | | | | _____rx pin number(optional, default from SDM_Config_User.h) +// | | | | | _tx pin number(optional, default from SDM_Config_User.h) +// | | | | | | +SDM sdm(Serial, 9600, NOT_A_PIN, SERIAL_8N1, 13, 15); + + +// for AVR +// ______________________________________hardware serial reference +// | ________________________________baudrate(optional, default from SDM_Config_User.h) +// | | _____________________dere pin for max485(optional, default from SDM_Config_User.h) +// | | | _________hardware uart config(optional, default from SDM_Config_User.h) +// | | | | +// | | | | +SDM sdm(Serial, 9600, NOT_A_PIN, SERIAL_8N1); ``` NOTE for ESP8266: when GPIO15 is used (especially for swapped hardware serial):
some converters (like mine) have built-in pullup resistors on TX/RX lines from rs232 side,
@@ -111,22 +173,22 @@ to ensure low level on GPIO15 by built-in in most ESP8266 modules pulldown resis --- ### Reading: ### -List of available registers for SDM120/220/230/630:
-https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h#L50 +List of available registers for SDM72/120/220/230/630:
+https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h#L103 ```cpp //reading voltage from SDM with slave address 0x01 (default) -// __________register name -// | -float voltage = sdm.readVal(SDM220T_VOLTAGE); +// ____register name +// | +float voltage = sdm.readVal(SDM_PHASE_1_VOLTAGE); //reading power from 1st SDM with slave address ID = 0x01 //reading power from 2nd SDM with slave address ID = 0x02 //useful with several meters on RS485 line -// __________register name -// | ____SDM device ID -// | | -float power1 = sdm.readVal(SDM220T_POWER, 0x01); -float power2 = sdm.readVal(SDM220T_POWER, 0x02); +// _______register name +// | SDM device ID +// | | +float power1 = sdm.readVal(SDM_PHASE_1_POWER, 0x01); +float power2 = sdm.readVal(SDM_PHASE_1_POWER, 0x02); ``` NOTE: if you reading multiple SDM devices on the same RS485 line,
remember to set the same transmission parameters on each device,
@@ -134,7 +196,7 @@ only ID must be different for each SDM device.
--- -### Debuging: ### +### Problems: ### Sometimes readVal return NaN value (not a number),
this means that the requested value could not be read from the sdm module for various reasons.
@@ -159,46 +221,83 @@ The most common problems are: - compilation error for hardware serial mode
https://github.com/reaper7/SDM_Energy_Meter/issues/23
https://github.com/reaper7/SDM_Energy_Meter/issues/24
+- SDM630 Modbus V2 serial stopbit problem
+ https://github.com/reaper7/SDM_Energy_Meter/issues/49
+- Subsequent inquiries for slow slaves
+ https://github.com/reaper7/SDM_Energy_Meter/issues/50
You can get last error code using function: ```cpp //get last error code -// __________optional parameter, -// | true -> read and reset error code -// | false or no parameter -> read error code -// | but not reset stored code (for future checking) -// | will be overwriten when next error occurs +// ______optional parameter, +// | true -> read and reset error code +// | false or no parameter -> read error code +// | but not reset stored code (for future checking) +// | will be overwriten when next error occurs uint16_t lasterror = sdm.getErrCode(true); //clear error code also available with: sdm.clearErrCode(); ``` Errors list returned by getErrCode:
-https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h#L142
+https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h#L86
You can also check total number of errors using function: ```cpp //get total errors counter -// _________optional parameter, -// | true -> read and reset errors counter -// | false or no parameter -> read errors counter -// | but not reset stored counter (for future checking) +// _____optional parameter, +// | true -> read and reset errors counter +// | false or no parameter -> read errors counter +// | but not reset stored counter (for future checking) uint16_t cnterrors = sdm.getErrCount(true); //clear errors counter also available with: sdm.clearErrCount(); ``` +And finally you can read the counter of correctly made readings: +```cpp +//get total success counter +// ___optional parameter, +// | true -> read and reset success counter +// | false or no parameter -> read success counter +// | but not reset stored counter (for future checking) +uint16_t cntsuccess = sdm.getSuccCount(true); + +//clear success counter also available with: +sdm.clearSuccCount(); +``` + --- ### Credits: ### +contribution to this project:
:+1: ESP SoftwareSerial library by Peter Lerup (https://github.com/plerup/espsoftwareserial)
:+1: crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino)
-:+1: new registers for SDM120 and SDM630 by bart.e (https://github.com/beireken/SDM220t)
+:+1: new registers for SDM120 and SDM630 by bart.e (https://github.com/reaper7/SDM_Energy_Meter/pull/3)
+:+1: new registers for SDM72 by jegaha (https://github.com/reaper7/SDM_Energy_Meter/pull/34)
+:+1: new registers for SDM120CT by JeroenSt (https://github.com/reaper7/SDM_Energy_Meter/pull/41)
+:+1: new registers for DDM18SD by JeroenSt (https://github.com/reaper7/SDM_Energy_Meter/pull/44)
+:+1: additional SDM630 registers and influxdb example by AndersV209 (https://github.com/reaper7/SDM_Energy_Meter/pull/45)
+:+1: new registers for SDM72DM V2 by datjan (https://github.com/reaper7/SDM_Energy_Meter/pull/62)
+:+1: compatibility with EspSoftwareSerial >= 8.0.1 by maxpautsch (https://github.com/reaper7/SDM_Energy_Meter/pull/75)
+ +other projects based on or using this library
+:point_right: BZ40i Energy Meter by adlerweb (https://github.com/adlerweb/BZ40i_Energy_Meter)
+:point_right: DDS238 Energy Meter by E-NINA (https://github.com/E-NINA/dds238_Energy_Meter)
+:point_right: ESPEasy by TD-er (https://github.com/TD-er/ESPEasy)
+:point_right: Sonoff-Tasmota by arendst (https://github.com/arendst/Sonoff-Tasmota)
--- -**2016-2018 Reaper7** +library version for old esp software serial (6.0.0 - 7.0.1) is available at [old_esp_swserial_600_701 branch](https://github.com/reaper7/SDM_Energy_Meter/tree/old_esp_swserial_600_701)
+library version for old esp software serial (5.2.0 - 5.4.0) is available at [old_esp_swserial_520_540 branch](https://github.com/reaper7/SDM_Energy_Meter/tree/old_esp_swserial_520_540)
+library version for old esp software serial (< 5.2.0) is available at [old_esp_swserial_lib branch](https://github.com/reaper7/SDM_Energy_Meter/tree/old_esp_swserial_lib)
+old template library version is available at [old_template branch](https://github.com/reaper7/SDM_Energy_Meter/tree/old_template)
+ +--- + +**2016-2023 Reaper7** [paypal.me/reaper7md](https://www.paypal.me/reaper7md) diff --git a/lib/SDM_Energy_Meter/SDM.cpp b/lib/SDM_Energy_Meter/SDM.cpp index 25d04cd8b..f2a902d1e 100644 --- a/lib/SDM_Energy_Meter/SDM.cpp +++ b/lib/SDM_Energy_Meter/SDM.cpp @@ -1,18 +1,34 @@ -/* Library for reading SDM 120/220/230/630 Modbus Energy meters. +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2018 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.9.0-beta & 2.4.1 esp8266 core) -* crc calculation by Jaime Garc�a (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ //------------------------------------------------------------------------------ #include "SDM.h" //------------------------------------------------------------------------------ -#ifdef USE_HARDWARESERIAL +#if defined ( USE_HARDWARESERIAL ) +#if defined ( ESP8266 ) SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, bool swapuart) : sdmSer(serial) { this->_baud = baud; - this->_config = config; this->_dere_pin = dere_pin; + this->_config = config; this->_swapuart = swapuart; } +#elif defined ( ESP32 ) +SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; + this->_rx_pin = rx_pin; + this->_tx_pin = tx_pin; +} +#else +SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; +} +#endif #else SDM::SDM(ESPeasySerial& serial, long baud, int dere_pin) : sdmSer(serial) { this->_baud = baud; @@ -24,86 +40,104 @@ SDM::~SDM() { } void SDM::begin(void) { -#ifdef USE_HARDWARESERIAL - #ifdef ESP8266 - sdmSer.begin(_baud, (SerialConfig)_config); - #else - sdmSer.begin(_baud, _config); - #endif +#if defined ( USE_HARDWARESERIAL ) +#if defined ( ESP8266 ) + sdmSer.begin(_baud, (SerialConfig)_config); +#elif defined ( ESP32 ) + sdmSer.begin(_baud, _config, _rx_pin, _tx_pin); +#else + sdmSer.begin(_baud, _config); +#endif #else sdmSer.begin(_baud); #endif -#ifdef USE_HARDWARESERIAL - #ifdef ESP8266 - if (_swapuart) - sdmSer.swap(); - #endif + +#if defined ( USE_HARDWARESERIAL ) && defined ( ESP8266 ) + if (_swapuart) + sdmSer.swap(); #endif - if (_dere_pin != NOT_A_PIN) //set output pin mode for DE/RE pin when used (for control MAX485) - pinMode(_dere_pin, OUTPUT); + if (_dere_pin != NOT_A_PIN) { + pinMode(_dere_pin, OUTPUT); //set output pin mode for DE/RE pin when used (for control MAX485) + } + dereSet(LOW); //set init state to receive from SDM -> DE Disable, /RE Enable (for control MAX485) } float SDM::readVal(uint16_t reg, uint8_t node) { - uint16_t temp; - unsigned long resptime; - uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0}; - float res = NAN; - uint16_t readErr = SDM_ERR_NO_ERROR; + startReadVal(reg, node); - sdmarr[2] = highByte(reg); - sdmarr[3] = lowByte(reg); + uint16_t readErr = SDM_ERR_STILL_WAITING; - temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes - - sdmarr[6] = lowByte(temp); - sdmarr[7] = highByte(temp); - -#ifndef USE_HARDWARESERIAL - sdmSer.listen(); //enable softserial rx interrupt -#endif - - while (sdmSer.available() > 0) { //read serial if any old data is available - sdmSer.read(); + while (readErr == SDM_ERR_STILL_WAITING) { + readErr = readValReady(node); + delay(1); } - if (_dere_pin != NOT_A_PIN) //transmit to SDM -> DE Enable, /RE Disable (for control MAX485) - digitalWrite(_dere_pin, HIGH); + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } - delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524 + if (readErr == SDM_ERR_NO_ERROR) { + return decodeFloatValue(); + } - sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes + constexpr float res = NAN; + return (res); +} - sdmSer.flush(); //clear out tx buffer +void SDM::startReadVal(uint16_t reg, uint8_t node, uint8_t functionCode) { + uint8_t data[] = { + node, // Address + functionCode, // Modbus function + highByte(reg), // Start address high byte + lowByte(reg), // Start address low byte + SDM_B_05, // Number of points high byte + SDM_B_06, // Number of points low byte + 0, // Checksum low byte + 0}; // Checksum high byte - if (_dere_pin != NOT_A_PIN) //receive from SDM -> DE Disable, /RE Enable (for control MAX485) - digitalWrite(_dere_pin, LOW); + constexpr size_t messageLength = sizeof(data) / sizeof(data[0]); + modbusWrite(data, messageLength); +} - resptime = millis() + MAX_MILLIS_TO_WAIT; +uint16_t SDM::readValReady(uint8_t node, uint8_t functionCode) { + uint16_t readErr = SDM_ERR_NO_ERROR; + if (sdmSer.available() < FRAMESIZE && ((millis() - resptime) < msturnaround)) + { + return SDM_ERR_STILL_WAITING; + } while (sdmSer.available() < FRAMESIZE) { - if (resptime < millis()) { + if ((millis() - resptime) > msturnaround) { readErr = SDM_ERR_TIMEOUT; //err debug (4) + + if (sdmSer.available() == 5) { + for(int n=0; n<5; n++) { + sdmarr[n] = sdmSer.read(); + } + if (validChecksum(sdmarr, 5)) { + readErr = sdmarr[2]; + } + } break; } - yield(); + delay(1); } if (readErr == SDM_ERR_NO_ERROR) { //if no timeout... - if(sdmSer.available() >= FRAMESIZE) { + if (sdmSer.available() >= FRAMESIZE) { for(int n=0; n 0) { //read redundant serial bytes, if any - sdmSer.read(); +#if !defined ( USE_HARDWARESERIAL ) +// sdmSer.stopListening(); //disable softserial rx interrupt +#endif + return readErr; +} + +float SDM::decodeFloatValue() const { + if (validChecksum(sdmarr, FRAMESIZE)) { + float res{}; + ((uint8_t*)&res)[3]= sdmarr[3]; + ((uint8_t*)&res)[2]= sdmarr[4]; + ((uint8_t*)&res)[1]= sdmarr[5]; + ((uint8_t*)&res)[0]= sdmarr[6]; + return res; + } + constexpr float res = NAN; + return res; +} + +float SDM::readHoldingRegister(uint16_t reg, uint8_t node) { + startReadVal(reg, node, SDM_READ_HOLDING_REGISTER); + + uint16_t readErr = SDM_ERR_STILL_WAITING; + + while (readErr == SDM_ERR_STILL_WAITING) { + delay(1); + readErr = readValReady(node, SDM_READ_HOLDING_REGISTER); } -#ifndef USE_HARDWARESERIAL - sdmSer.end(); //disable softserial rx interrupt -#endif + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } + if (readErr == SDM_ERR_NO_ERROR) { + return decodeFloatValue(); + } + + constexpr float res = NAN; return (res); } +bool SDM::writeHoldingRegister(float value, uint16_t reg, uint8_t node) { + { + uint8_t data[] = { + node, // Address + SDM_WRITE_HOLDING_REGISTER, // Function + highByte(reg), // Starting Address High + lowByte(reg), // Starting Address Low + SDM_B_05, // Number of Registers High + SDM_B_06, // Number of Registers Low + 4, // Byte count + ((uint8_t*)&value)[3], + ((uint8_t*)&value)[2], + ((uint8_t*)&value)[1], + ((uint8_t*)&value)[0], + 0, 0}; + + constexpr size_t messageLength = sizeof(data) / sizeof(data[0]); + modbusWrite(data, messageLength); + } + uint16_t readErr = SDM_ERR_STILL_WAITING; + while (readErr == SDM_ERR_STILL_WAITING) { + delay(1); + readErr = readValReady(node, SDM_READ_HOLDING_REGISTER); + } + + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } + + return readErr == SDM_ERR_NO_ERROR; +} + +uint32_t SDM::getSerialNumber(uint8_t node) { + uint32_t res{}; + readHoldingRegister(SDM_HOLDING_SERIAL_NUMBER, node); +// if (getErrCode() == SDM_ERR_NO_ERROR) { + for (size_t i = 0; i < 4; ++i) { + res = (res << 8) + sdmarr[3 + i]; + } +// } + return res; +} + uint16_t SDM::getErrCode(bool _clear) { uint16_t _tmp = readingerrcode; if (_clear == true) @@ -142,15 +260,15 @@ uint16_t SDM::getErrCode(bool _clear) { return (_tmp); } -uint16_t SDM::getErrCount(bool _clear) { - uint16_t _tmp = readingerrcount; +uint32_t SDM::getErrCount(bool _clear) { + uint32_t _tmp = readingerrcount; if (_clear == true) clearErrCount(); return (_tmp); } -uint16_t SDM::getSuccCount(bool _clear) { - uint16_t _tmp = readingsuccesscount; +uint32_t SDM::getSuccCount(bool _clear) { + uint32_t _tmp = readingsuccesscount; if (_clear == true) clearSuccCount(); return (_tmp); @@ -168,11 +286,37 @@ void SDM::clearSuccCount() { readingsuccesscount = 0; } -uint16_t SDM::calculateCRC(uint8_t *array, uint8_t num) { +void SDM::setMsTurnaround(uint16_t _msturnaround) { + if (_msturnaround < SDM_MIN_DELAY) + msturnaround = SDM_MIN_DELAY; + else if (_msturnaround > SDM_MAX_DELAY) + msturnaround = SDM_MAX_DELAY; + else + msturnaround = _msturnaround; +} + +void SDM::setMsTimeout(uint16_t _mstimeout) { + if (_mstimeout < SDM_MIN_DELAY) + mstimeout = SDM_MIN_DELAY; + else if (_mstimeout > SDM_MAX_DELAY) + mstimeout = SDM_MAX_DELAY; + else + mstimeout = _mstimeout; +} + +uint16_t SDM::getMsTurnaround() { + return (msturnaround); +} + +uint16_t SDM::getMsTimeout() { + return (mstimeout); +} + +uint16_t SDM::calculateCRC(const uint8_t *array, uint8_t len) const { uint16_t _crc, _flag; _crc = 0xFFFF; - for (uint8_t i = 0; i < num; i++) { - _crc = _crc ^ array[i]; + for (uint8_t i = 0; i < len; i++) { + _crc ^= (uint16_t)array[i]; for (uint8_t j = 8; j; j--) { _flag = _crc & 0x0001; _crc >>= 1; @@ -182,3 +326,69 @@ uint16_t SDM::calculateCRC(uint8_t *array, uint8_t num) { } return _crc; } + +void SDM::flush(unsigned long _flushtime) { + unsigned long flushstart = millis(); + sdmSer.flush(); + int available = sdmSer.available(); + while (available > 0 || ((millis() - flushstart) < _flushtime)) { + while (available > 0) { + --available; + flushstart = millis(); + //read serial if any old data is available + sdmSer.read(); + } + delay(1); + available = sdmSer.available(); + } +} + +void SDM::dereSet(bool _state) { + if (_dere_pin != NOT_A_PIN) + digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) +} + +bool SDM::validChecksum(const uint8_t* data, size_t messageLength) const { + const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes + + return data[messageLength - 2] == lowByte(temp) && + data[messageLength - 1] == highByte(temp); + +} + +void SDM::modbusWrite(uint8_t* data, size_t messageLength) { + const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes + + data[messageLength - 2] = lowByte(temp); + data[messageLength - 1] = highByte(temp); + +#if !defined ( USE_HARDWARESERIAL ) + sdmSer.listen(); //enable softserial rx interrupt +#endif + + flush(); //read serial if any old data is available + + if (_dere_pin != NOT_A_PIN) { + dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485) + + delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524 + + resptime = millis(); + } + + sdmSer.write(data, messageLength); //send 8 bytes + + if (_dere_pin != NOT_A_PIN) { + // Need to wait for all bytes in TX buffer are sent. + // N.B. flush() on serial port does often only clear the send buffer, not wait till all is sent. + const unsigned long waitForBytesSent_ms = (messageLength * 10000) / sdmSer.getBaudRate() + 1; + while ((millis() - resptime) < waitForBytesSent_ms) { + delay(1); //clear out tx buffer + } + dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) + flush(); + } + + resptime = millis(); + +} diff --git a/lib/SDM_Energy_Meter/SDM.h b/lib/SDM_Energy_Meter/SDM.h index f5a58a458..4c37aa772 100644 --- a/lib/SDM_Energy_Meter/SDM.h +++ b/lib/SDM_Energy_Meter/SDM.h @@ -1,6 +1,6 @@ -/* Library for reading SDM 120/220/230/630 Modbus Energy meters. +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2018 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.9.0-beta & 2.4.1 esp8266 core) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ //------------------------------------------------------------------------------ @@ -9,185 +9,392 @@ //------------------------------------------------------------------------------ #include #include -#ifdef USE_HARDWARESERIAL +#if defined ( USE_HARDWARESERIAL ) #include #else #include -// #include #endif //------------------------------------------------------------------------------ //DEFAULT CONFIG (DO NOT CHANGE ANYTHING!!! for changes use SDM_Config_User.h): //------------------------------------------------------------------------------ -#ifndef SDM_UART_BAUD - #define SDM_UART_BAUD 4800 //default baudrate +#if !defined ( SDM_UART_BAUD ) + #define SDM_UART_BAUD 4800 // default baudrate #endif -#ifndef DERE_PIN - #define DERE_PIN NOT_A_PIN //default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin) +#if !defined ( DERE_PIN ) + #define DERE_PIN NOT_A_PIN // default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin) #endif -#ifdef USE_HARDWARESERIAL +#if defined ( USE_HARDWARESERIAL ) - #ifndef SDM_UART_CONFIG - #define SDM_UART_CONFIG SERIAL_8N1 //default hardware uart config + #if !defined ( SDM_UART_CONFIG ) + #define SDM_UART_CONFIG SERIAL_8N1 // default hardware uart config #endif - #ifndef SWAPHWSERIAL - #define SWAPHWSERIAL 0 //(only esp8266) when hwserial used, then swap uart pins from 3/1 to 13/15 (default not swap) + #if defined ( ESP8266 ) && !defined ( SWAPHWSERIAL ) + #define SWAPHWSERIAL 0 // (only esp8266) when hwserial used, then swap uart pins from 3/1 to 13/15 (default not swap) + #endif + + #if defined ( ESP32 ) + #if !defined ( SDM_RX_PIN ) + #define SDM_RX_PIN -1 // use default rx pin for selected port + #endif + #if !defined ( SDM_TX_PIN ) + #define SDM_TX_PIN -1 // use default tx pin for selected port + #endif + #endif + +#else + + #if defined ( ESP8266 ) || defined ( ESP32 ) + #if !defined ( SDM_UART_CONFIG ) + #define SDM_UART_CONFIG SERIAL_8N1 // default softwareware uart config for esp8266/esp32 + #endif + #endif + +// #if !defined ( SDM_RX_PIN ) || !defined ( SDM_TX_PIN ) +// #error "SDM_RX_PIN and SDM_TX_PIN must be defined in SDM_Config_User.h for Software Serial option)" +// #endif + + #if !defined ( SDM_RX_PIN ) + #define SDM_RX_PIN -1 + #endif + #if !defined ( SDM_TX_PIN ) + #define SDM_TX_PIN -1 #endif #endif -#ifndef MAX_MILLIS_TO_WAIT - #define MAX_MILLIS_TO_WAIT 500 //default max time to wait for response from SDM +#if !defined ( WAITING_TURNAROUND_DELAY ) + #define WAITING_TURNAROUND_DELAY 500 // time in ms to wait for process current request #endif -//------------------------------------------------------------------------------ -#define FRAMESIZE 9 //size of out/in array -#define SDM_REPLY_BYTE_COUNT 0x04 //number of bytes with data -#define SDM_B_01 0x01 //BYTE 1 -> slave address (default value 1 read from node 1) -#define SDM_B_02 0x04 //BYTE 2 -> function code (default value 0x04 read from 3X input registers) - //BYTES 3 & 4 (BELOW) -//SDM 120 registers -#define SDM120C_VOLTAGE 0x0000 //V -#define SDM120C_CURRENT 0x0006 //A -#define SDM120C_POWER 0x000C //W -#define SDM120C_ACTIVE_APPARENT_POWER 0x0012 //VA -#define SDM120C_REACTIVE_APPARENT_POWER 0x0018 //VAR -#define SDM120C_POWER_FACTOR 0x001E // -#define SDM120C_FREQUENCY 0x0046 //Hz -#define SDM120C_IMPORT_ACTIVE_ENERGY 0x0048 //Wh -#define SDM120C_EXPORT_ACTIVE_ENERGY 0x004A //Wh -#define SDM120C_TOTAL_ACTIVE_ENERGY 0x0156 //Wh -//SDM 220 registers -#define SDM220T_VOLTAGE 0x0000 //V -#define SDM220T_CURRENT 0x0006 //A -#define SDM220T_POWER 0x000C //W -#define SDM220T_ACTIVE_APPARENT_POWER 0x0012 //VA -#define SDM220T_REACTIVE_APPARENT_POWER 0x0018 //VAR -#define SDM220T_POWER_FACTOR 0x001E // -#define SDM220T_PHASE_ANGLE 0x0024 //DEGREE -#define SDM220T_FREQUENCY 0x0046 //Hz -#define SDM220T_IMPORT_ACTIVE_ENERGY 0x0048 //Wh -#define SDM220T_EXPORT_ACTIVE_ENERGY 0x004A //Wh -#define SDM220T_IMPORT_REACTIVE_ENERGY 0x004C //VARh -#define SDM220T_EXPORT_REACTIVE_ENERGY 0x004E //VARh -#define SDM220T_TOTAL_ACTIVE_ENERGY 0x0156 //Wh -#define SDM220T_TOTAL_REACTIVE_ENERGY 0x0158 //VARh -//SDM 230 registers -#define SDM230_VOLTAGE 0x0000 //V -#define SDM230_CURRENT 0x0006 //A -#define SDM230_POWER 0x000C //W -#define SDM230_ACTIVE_APPARENT_POWER 0x0012 //VA -#define SDM230_REACTIVE_APPARENT_POWER 0x0018 //VAR -#define SDM230_POWER_FACTOR 0x001E // -#define SDM230_PHASE_ANGLE 0x0024 //DEGREE -#define SDM230_FREQUENCY 0x0046 //Hz -#define SDM230_IMPORT_ACTIVE_ENERGY 0x0048 //Wh -#define SDM230_EXPORT_ACTIVE_ENERGY 0x004A //Wh -#define SDM230_IMPORT_REACTIVE_ENERGY 0x004C //VARh -#define SDM230_EXPORT_REACTIVE_ENERGY 0x004E //VARh -#define SDM230_TOTAL_SYSTEM_POWER_DEMAND 0x0054 //W -#define SDM230_MAXIMUM_SYSTEM_POWER_DEMAND 0x0056 //W -#define SDM230_CURRENT_POSITIVE_POWER_DEMAND 0x0058 //W -#define SDM230_MAXIMUM_POSITIVE_POWER_DEMAND 0x005A //W -#define SDM230_CURRENT_REVERSE_POWER_DEMAND 0x005C //W -#define SDM230_MAXIMUM_REVERSE_POWER_DEMAND 0x005E //W -#define SDM230_CURRENT_DEMAND 0x0102 //Amps -#define SDM230_MAXIMUM_CURRENT_DEMAND 0x0108 //Amps -#define SDM230_TOTAL_ACTIVE_ENERGY 0x0156 //kwh -#define SDM230_TOTAL_REACTIVE_ENERGY 0x0158 //kvarh -#define SDM230_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY 0x0180 //Wh -#define SDM230_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 //VARh -//SDM 630 registers -#define SDM630_VOLTAGE1 0x0000 //V -#define SDM630_VOLTAGE2 0x0002 //V -#define SDM630_VOLTAGE3 0x0004 //V -#define SDM630_CURRENT1 0x0006 //A -#define SDM630_CURRENT2 0x0008 //A -#define SDM630_CURRENT3 0x000A //A -#define SDM630_CURRENTSUM 0x0030 //A -#define SDM630_POWER1 0x000C //W -#define SDM630_POWER2 0x000E //W -#define SDM630_POWER3 0x0010 //W -#define SDM630_POWERTOTAL 0x0034 //W -#define SDM630_VOLT_AMPS1 0x0012 //VA -#define SDM630_VOLT_AMPS2 0x0014 //VA -#define SDM630_VOLT_AMPS3 0x0016 //VA -#define SDM630_VOLT_AMPS_TOTAL 0x0038 //VA -#define SDM630_VOLT_AMPS_REACTIVE1 0x0018 //VAr -#define SDM630_VOLT_AMPS_REACTIVE2 0x001A //VAr -#define SDM630_VOLT_AMPS_REACTIVE3 0x001C //VAr -#define SDM630_VOLT_AMPS_REACTIVE_TOTAL 0x003C //VAr -#define SDM630_POWER_FACTOR1 0x001E -#define SDM630_POWER_FACTOR2 0x0020 -#define SDM630_POWER_FACTOR3 0x0022 -#define SDM630_POWER_FACTOR_TOTAL 0x003E -#define SDM630_PHASE_ANGLE1 0x0024 //Degrees -#define SDM630_PHASE_ANGLE2 0x0026 //Degrees -#define SDM630_PHASE_ANGLE3 0x0028 //Degrees -#define SDM630_PHASE_ANGLE_TOTAL 0x0042 //Degrees -#define SDM630_VOLTAGE_AVERAGE 0x002A //V -#define SDM630_CURRENT_AVERAGE 0x002E //A -#define SDM630_FREQUENCY 0x0046 //HZ -#define SDM630_IMPORT_ACTIVE_ENERGY 0x0048 //Wh -#define SDM630_EXPORT_ACTIVE_ENERGY 0x004A //Wh -#define SDM630_IMPORT_REACTIVE_ENERGY 0x004C //VARh -#define SDM630_EXPORT_REACTIVE_ENERGY 0x004E //VARh -#define SDM630_TOTAL_SYSTEM_POWER_DEMAND 0x0054 //W -#define SDM630_MAXIMUM_TOTAL_SYSTEM_POWER 0x0056 //W -#define SDM630_PHASE_1_LN_VOLTS_THD 0x00EA //% -#define SDM630_PHASE_2_LN_VOLTS_THD 0x00EC //% -#define SDM630_PHASE_3_LN_VOLTS_THD 0x00EE //% -#define SDM630_AVERAGE_VOLTS_THD 0x00F8 //% -#define SDM630_PHASE_1_CURRENT_THD 0x00F0 //% -#define SDM630_PHASE_2_CURRENT_THD 0x00F2 //% -#define SDM630_PHASE_3_CURRENT_THD 0x00F4 //% -#define SDM630_AVERAGE_CURRENT_THD 0x00FA //% +#if !defined ( RESPONSE_TIMEOUT ) + #define RESPONSE_TIMEOUT 10 // time in ms to wait for return response from all devices before next request +#endif + +#if !defined ( SDM_MIN_DELAY ) + #define SDM_MIN_DELAY 1 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT +#endif + +#if !defined ( SDM_MAX_DELAY ) + #define SDM_MAX_DELAY 20 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT +#endif -#define SDM_B_05 0x00 //BYTE 5 -#define SDM_B_06 0x02 //BYTE 6 //------------------------------------------------------------------------------ -#define SDM_ERR_NO_ERROR 0 //no error -#define SDM_ERR_CRC_ERROR 1 //crc error -#define SDM_ERR_WRONG_BYTES 2 //bytes b0,b1 or b2 wrong -#define SDM_ERR_NOT_ENOUGHT_BYTES 3 //not enough bytes from sdm -#define SDM_ERR_TIMEOUT 4 //timeout + +#define SDM_ERR_NO_ERROR 0 // no error +#define SDM_ERR_ILLEGAL_FUNCTION 1 +#define SDM_ERR_ILLEGAL_DATA_ADDRESS 2 +#define SDM_ERR_ILLEGAL_DATA_VALUE 3 +#define SDM_ERR_SLAVE_DEVICE_FAILURE 5 + +#define SDM_ERR_CRC_ERROR 11 // crc error +#define SDM_ERR_WRONG_BYTES 12 // bytes b0,b1 or b2 wrong +#define SDM_ERR_NOT_ENOUGHT_BYTES 13 // not enough bytes from sdm +#define SDM_ERR_TIMEOUT 14 // timeout +#define SDM_ERR_EXCEPTION 15 +#define SDM_ERR_STILL_WAITING 16 + //------------------------------------------------------------------------------ + +#define SDM_READ_HOLDING_REGISTER 0x03 +#define SDM_READ_INPUT_REGISTER 0x04 +#define SDM_WRITE_HOLDING_REGISTER 0x10 + +#define FRAMESIZE 9 // size of out/in array +#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data + +#define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1) +#define SDM_B_02 SDM_READ_INPUT_REGISTER // BYTE 2 -> function code (default value 0x04 read from 3X input registers) +#define SDM_B_05 0x00 // BYTE 5 +#define SDM_B_06 0x02 // BYTE 6 + // BYTES 3 & 4 (BELOW) + +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR SDM DEVICES | +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | SDM630 | SDM230 | SDM220 | SDM120CT| SDM120 | SDM72D | SDM72 V2| +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#define SDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_VOLTAGE 0x0002 // V | 1 | | | | | | 1 | +#define SDM_PHASE_3_VOLTAGE 0x0004 // V | 1 | | | | | | 1 | +#define SDM_PHASE_1_CURRENT 0x0006 // A | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_CURRENT 0x0008 // A | 1 | | | | | | 1 | +#define SDM_PHASE_3_CURRENT 0x000A // A | 1 | | | | | | 1 | +#define SDM_PHASE_1_POWER 0x000C // W | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_POWER 0x000E // W | 1 | | | | | | 1 | +#define SDM_PHASE_3_POWER 0x0010 // W | 1 | | | | | | 1 | +#define SDM_PHASE_1_APPARENT_POWER 0x0012 // VA | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_APPARENT_POWER 0x0014 // VA | 1 | | | | | | 1 | +#define SDM_PHASE_3_APPARENT_POWER 0x0016 // VA | 1 | | | | | | 1 | +#define SDM_PHASE_1_REACTIVE_POWER 0x0018 // VAr | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_REACTIVE_POWER 0x001A // VAr | 1 | | | | | | 1 | +#define SDM_PHASE_3_REACTIVE_POWER 0x001C // VAr | 1 | | | | | | 1 | +#define SDM_PHASE_1_POWER_FACTOR 0x001E // | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_POWER_FACTOR 0x0020 // | 1 | | | | | | 1 | +#define SDM_PHASE_3_POWER_FACTOR 0x0022 // | 1 | | | | | | 1 | +#define SDM_PHASE_1_ANGLE 0x0024 // Degrees | 1 | 1 | 1 | 1 | | | | +#define SDM_PHASE_2_ANGLE 0x0026 // Degrees | 1 | | | | | | | +#define SDM_PHASE_3_ANGLE 0x0028 // Degrees | 1 | | | | | | | +#define SDM_AVERAGE_L_TO_N_VOLTS 0x002A // V | 1 | | | | | | 1 | +#define SDM_AVERAGE_LINE_CURRENT 0x002E // A | 1 | | | | | | 1 | +#define SDM_SUM_LINE_CURRENT 0x0030 // A | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_POWER 0x0034 // W | 1 | | | | | 1 | 1 | +#define SDM_TOTAL_SYSTEM_APPARENT_POWER 0x0038 // VA | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_REACTIVE_POWER 0x003C // VAr | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_POWER_FACTOR 0x003E // | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_PHASE_ANGLE 0x0042 // Degrees | 1 | | | | | | | +#define SDM_FREQUENCY 0x0046 // Hz | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_IMPORT_ACTIVE_ENERGY 0x0048 // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_EXPORT_ACTIVE_ENERGY 0x004A // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_IMPORT_REACTIVE_ENERGY 0x004C // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | | +#define SDM_EXPORT_REACTIVE_ENERGY 0x004E // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | | +#define SDM_VAH_SINCE_LAST_RESET 0x0050 // kVAh/MVAh | 1 | | | | | | | +#define SDM_AH_SINCE_LAST_RESET 0x0052 // Ah/kAh | 1 | | | | | | | +#define SDM_TOTAL_SYSTEM_POWER_DEMAND 0x0054 // W | 1 | 1 | | | | | | +#define SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND 0x0056 // W | 1 | 1 | | | | | | +#define SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND 0x0058 // W | | 1 | | | | | | +#define SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND 0x005A // W | | 1 | | | | | | +#define SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND 0x005C // W | | 1 | | | | | | +#define SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND 0x005E // W | | 1 | | | | | | +#define SDM_TOTAL_SYSTEM_VA_DEMAND 0x0064 // VA | 1 | | | | | | | +#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | | +#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | | +#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | | +#define SDM_REACTIVE_POWER_DEMAND 0x006C // VAr | 1 | | | | | | | +#define SDM_MAXIMUM_REACTIVE_POWER_DEMAND 0x006E // VAr | 1 | | | | | | | +#define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 | +#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 | +#define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 | +#define SDM_AVERAGE_LINE_TO_LINE_VOLTS 0x00CE // V | 1 | | | | | | 1 | +#define SDM_NEUTRAL_CURRENT 0x00E0 // A | 1 | | | | | | 1 | +#define SDM_PHASE_1_LN_VOLTS_THD 0x00EA // % | 1 | | | | | | | +#define SDM_PHASE_2_LN_VOLTS_THD 0x00EC // % | 1 | | | | | | | +#define SDM_PHASE_3_LN_VOLTS_THD 0x00EE // % | 1 | | | | | | | +#define SDM_PHASE_1_CURRENT_THD 0x00F0 // % | 1 | | | | | | | +#define SDM_PHASE_2_CURRENT_THD 0x00F2 // % | 1 | | | | | | | +#define SDM_PHASE_3_CURRENT_THD 0x00F4 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD 0x00F8 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_CURRENT_THD 0x00FA // % | 1 | | | | | | | +#define SDM_TOTAL_SYSTEM_POWER_FACTOR_INV 0x00FE // | 1 | | | | | | | +#define SDM_PHASE_1_CURRENT_DEMAND 0x0102 // A | 1 | 1 | | | | | | +#define SDM_PHASE_2_CURRENT_DEMAND 0x0104 // A | 1 | | | | | | | +#define SDM_PHASE_3_CURRENT_DEMAND 0x0106 // A | 1 | | | | | | | +#define SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND 0x0108 // A | 1 | 1 | | | | | | +#define SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND 0x010A // A | 1 | | | | | | | +#define SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND 0x010C // A | 1 | | | | | | | +#define SDM_LINE_1_TO_LINE_2_VOLTS_THD 0x014E // % | 1 | | | | | | | +#define SDM_LINE_2_TO_LINE_3_VOLTS_THD 0x0150 // % | 1 | | | | | | | +#define SDM_LINE_3_TO_LINE_1_VOLTS_THD 0x0152 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD 0x0154 // % | 1 | | | | | | | +#define SDM_TOTAL_ACTIVE_ENERGY 0x0156 // kWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_TOTAL_REACTIVE_ENERGY 0x0158 // kVArh | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_L1_IMPORT_ACTIVE_ENERGY 0x015A // kWh | 1 | | | | | | | +#define SDM_L2_IMPORT_ACTIVE_ENERGY 0x015C // kWh | 1 | | | | | | | +#define SDM_L3_IMPORT_ACTIVE_ENERGY 0x015E // kWh | 1 | | | | | | | +#define SDM_L1_EXPORT_ACTIVE_ENERGY 0x0160 // kWh | 1 | | | | | | | +#define SDM_L2_EXPORT_ACTIVE_ENERGY 0x0162 // kWh | 1 | | | | | | | +#define SDM_L3_EXPORT_ACTIVE_ENERGY 0x0164 // kWh | 1 | | | | | | | +#define SDM_L1_TOTAL_ACTIVE_ENERGY 0x0166 // kWh | 1 | | | | | | | +#define SDM_L2_TOTAL_ACTIVE_ENERGY 0x0168 // kWh | 1 | | | | | | | +#define SDM_L3_TOTAL_ACTIVE_ENERGY 0x016a // kWh | 1 | | | | | | | +#define SDM_L1_IMPORT_REACTIVE_ENERGY 0x016C // kVArh | 1 | | | | | | | +#define SDM_L2_IMPORT_REACTIVE_ENERGY 0x016E // kVArh | 1 | | | | | | | +#define SDM_L3_IMPORT_REACTIVE_ENERGY 0x0170 // kVArh | 1 | | | | | | | +#define SDM_L1_EXPORT_REACTIVE_ENERGY 0x0172 // kVArh | 1 | | | | | | | +#define SDM_L2_EXPORT_REACTIVE_ENERGY 0x0174 // kVArh | 1 | | | | | | | +#define SDM_L3_EXPORT_REACTIVE_ENERGY 0x0176 // kVArh | 1 | | | | | | | +#define SDM_L1_TOTAL_REACTIVE_ENERGY 0x0178 // kVArh | 1 | | | | | | | +#define SDM_L2_TOTAL_REACTIVE_ENERGY 0x017A // kVArh | 1 | | | | | | | +#define SDM_L3_TOTAL_REACTIVE_ENERGY 0x017C // kVArh | 1 | | | | | | | +#define SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY 0x0180 // kWh | | 1 | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | | +#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_IMPORT_REACTIVE_ENERGY 0x0188 // kVArh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_EXPORT_REACTIVE_ENERGY 0x018A // kVArh | | | | | | 1 | 1 | +#define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 | +#define SDM_NET_KVARH 0x018E // kVArh | | | | | | | 1 | +#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 | +#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 | +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DDM DEVICE | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DDM18SD | +//--------------------------------------------------------------------------------------------------------- +#define DDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | +#define DDM_PHASE_1_CURRENT 0x0008 // A | 1 | +#define DDM_PHASE_1_POWER 0x0012 // W | 1 | +#define DDM_PHASE_1_REACTIVE_POWER 0x001A // VAr | 1 | +#define DDM_PHASE_1_POWER_FACTOR 0x002A // | 1 | +#define DDM_FREQUENCY 0x0036 // Hz | 1 | +#define DDM_IMPORT_ACTIVE_ENERGY 0x0100 // kWh | 1 | +#define DDM_IMPORT_REACTIVE_ENERGY 0x0400 // kVArh | 1 | +//--------------------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DEVNAME DEVICE | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME | +//--------------------------------------------------------------------------------------------------------- +//#define DEVNAME_VOLTAGE 0x0000 // V | 1 | +//#define DEVNAME_CURRENT 0x0002 // A | 1 | +//#define DEVNAME_POWER 0x0004 // W | 1 | +//--------------------------------------------------------------------------------------------------------- + + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DEVICE SETTINGS | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME | +//--------------------------------------------------------------------------------------------------------- + +// Read minutes into first demand calculation. +// When the Demand Time reaches the Demand Period +// then the demand values are valid. +#define SDM_HOLDING_DEMAND_TIME 0x0000 + +// Write demand period: 0~60 minutes. +// Default 60. +// Range: 0~60, 0 means function disabled +#define SDM_HOLDING_DEMAND_PERIOD 0x0002 + +// Write relay on period in milliseconds: +// 60, 100 or 200 ms. +// default: 100 ms +#define SDM_HOLDING_RELAY_PULSE_WIDTH 0x000C + +// Parity / stop bit settings: +// 0 = One stop bit and no parity, default. +// 1 = One stop bit and even parity. +// 2 = One s top bit and odd parity. +// 3 = Two stop bits and no parity. +// Requires a restart to become effective. +#define SDM_HOLDING_NETWORK_PARITY_STOP 0x0012 + +// Ranges from 1 to 247. Default ID is 1. +#define SDM_HOLDING_METER_ID 0x0014 + +// Write the network port baud rate for MODBUS Protocol, where: +/* +SDM120 / SDM230: + 0 = 2400 baud (default) + 1 = 4800 baud + 2 = 9600 baud + 5 = 1200 baud + +SDM320 / SDM530Y: + 0 = 2400 baud + 1 = 4800 baud + 2 = 9600 baud (default) + 5 = 1200 band + +SDM630 / SDM72 / SDM72V2: + 0 = 2400 baud + 1 = 4800 baud + 2 = 9600 baud (default) + 3 = 19200 baud + 4 = 38400 baud +*/ +#define SDM_HOLDING_BAUD_RATE 0x001C + +// Write MODBUS Protocol input parameter for pulse out 1: +// 1: Import active energy +// 2: Import + export (total) active energy +// 4: Export active energy (default). +// 5: Import reactive energy +// 6: Import + export (total) reactive energy +// 8: Export reactive energy +#define SDM_HOLDING_PULSE_1_OUTPUT_MODE 0x0056 + + + +#define SDM_HOLDING_SERIAL_NUMBER 0xFC00 +#define SDM_HOLDING_SOFTWARE_VERSION 0xFC03 + + + +//----------------------------------------------------------------------------------------------------------------------------------------------------------- + class SDM { public: -#ifdef USE_HARDWARESERIAL +#if defined ( USE_HARDWARESERIAL ) // hardware serial + #if defined ( ESP8266 ) // on esp8266 SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, bool swapuart = SWAPHWSERIAL); -#else + #elif defined ( ESP32 ) // on esp32 + SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN); + #else // on avr + SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG); + #endif +#else // software serial SDM(ESPeasySerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN); #endif virtual ~SDM(); void begin(void); - float readVal(uint16_t reg, uint8_t node = SDM_B_01); //read value from register = reg and from deviceId = node - uint16_t getErrCode(bool _clear = false); //return last errorcode (optional clear this value, default false) - uint16_t getErrCount(bool _clear = false); //return total errors count (optional clear this value, default false) - uint16_t getSuccCount(bool _clear = false); //return total success count (optional clear this value, default false) - void clearErrCode(); //clear last errorcode - void clearErrCount(); //clear total errors count - void clearSuccCount(); //clear total success count + float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node + void startReadVal(uint16_t reg, uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Start sending out the request to read a register from a specific node (allows for async access) + uint16_t readValReady(uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Check to see if a reply is ready reading from a node (allow for async access) + float decodeFloatValue() const; + + float readHoldingRegister(uint16_t reg, uint8_t node = SDM_B_01); + bool writeHoldingRegister(float value, uint16_t reg, uint8_t node = SDM_B_01); + + uint32_t getSerialNumber(uint8_t node = SDM_B_01); + + + uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase) + uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase) + uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false) + void clearErrCode(); // clear last errorcode + void clearErrCount(); // clear total errors count + void clearSuccCount(); // clear total success count + void setMsTurnaround(uint16_t _msturnaround = WAITING_TURNAROUND_DELAY); // set new value for WAITING_TURNAROUND_DELAY (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY + void setMsTimeout(uint16_t _mstimeout = RESPONSE_TIMEOUT); // set new value for RESPONSE_TIMEOUT (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY + uint16_t getMsTurnaround(); // get current value of WAITING_TURNAROUND_DELAY (ms) + uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms) private: -#ifdef USE_HARDWARESERIAL + + bool validChecksum(const uint8_t* data, size_t messageLength) const; + + void modbusWrite(uint8_t* data, size_t messageLength); + +#if defined ( USE_HARDWARESERIAL ) HardwareSerial& sdmSer; #else ESPeasySerial& sdmSer; #endif -#ifdef USE_HARDWARESERIAL +#if defined ( USE_HARDWARESERIAL ) int _config = SDM_UART_CONFIG; + #if defined ( ESP8266 ) bool _swapuart = SWAPHWSERIAL; + #elif defined ( ESP32 ) + int8_t _rx_pin = -1; + int8_t _tx_pin = -1; + #endif +#else + #if defined ( ESP8266 ) || defined ( ESP32 ) + int _config = SDM_UART_CONFIG; + #endif + int8_t _rx_pin = -1; + int8_t _tx_pin = -1; #endif long _baud = SDM_UART_BAUD; int _dere_pin = DERE_PIN; - uint16_t readingerrcode = SDM_ERR_NO_ERROR; //4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error - uint16_t readingerrcount = 0; //total errors couter - uint32_t readingsuccesscount = 0; - uint16_t calculateCRC(uint8_t *array, uint8_t num); + uint16_t readingerrcode = SDM_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error + uint16_t msturnaround = WAITING_TURNAROUND_DELAY; + uint16_t mstimeout = RESPONSE_TIMEOUT; + uint32_t readingerrcount = 0; // total errors counter + uint32_t readingsuccesscount = 0; // total success counter + unsigned long resptime = 0; + uint8_t sdmarr[FRAMESIZE] = {}; + uint16_t calculateCRC(const uint8_t *array, uint8_t len) const; + void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms + void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM }; -#endif //SDM_h +#endif // SDM_h diff --git a/lib/SDM_Energy_Meter/SDM_Config_User.h b/lib/SDM_Energy_Meter/SDM_Config_User.h index 6a9091ca7..e6595f826 100644 --- a/lib/SDM_Energy_Meter/SDM_Config_User.h +++ b/lib/SDM_Energy_Meter/SDM_Config_User.h @@ -1,36 +1,93 @@ -/* Library for reading SDM 120/220/230/630 Modbus Energy meters. +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2018 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.9.0-beta & 2.4.1 esp8266 core) -* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ -//USER CONFIG: + +/* +* USER CONFIG: +*/ + //------------------------------------------------------------------------------ + /* -#undef USE_HARDWARESERIAL //undefine USE_HARDWARESERIAL -*/ -// or -/* -#define USE_HARDWARESERIAL //define USE_HARDWARESERIAL +* define or undefine USE_HARDWARESERIAL (uncomment only one or none) */ +//#undef USE_HARDWARESERIAL +//#define USE_HARDWARESERIAL + //------------------------------------------------------------------------------ + /* -#define SDM_UART_BAUD 9600 //define user baudrate +* define user baudrate */ +//#define SDM_UART_BAUD 9600 + //------------------------------------------------------------------------------ + /* -#define DERE_PIN NOT_A_PIN //define user DERE_PIN for control MAX485 DE/RE lines (connect DE & /RE together to this pin) -*/ -//------------------------------------------------------------------------------ -#ifdef USE_HARDWARESERIAL -/* - #define SDM_UART_CONFIG SERIAL_8N1 //define user SDM_UART_CONFIG -*/ -/* - #define SWAPHWSERIAL 0 //define user SWAPHWSERIAL, if true(1) then swap uart pins from 3/1 to 13/15 (only ESP8266) +* define user SDM_RX_PIN and SDM_TX_PIN for esp/avr Software Serial option +* or ESP32 with Hardware Serial if default core pins are not suitable */ +#if defined ( USE_HARDWARESERIAL ) + #if defined ( ESP32 ) + #define SDM_RX_PIN 13 + #define SDM_TX_PIN 15 + #endif +#else + #if defined ( ESP8266 ) || defined ( ESP32 ) + #define SDM_RX_PIN 13 + #define SDM_TX_PIN 15 + #else + #define SDM_RX_PIN 10 + #define SDM_TX_PIN 11 + #endif #endif + //------------------------------------------------------------------------------ + /* -#define MAX_MILLIS_TO_WAIT 500 //define user MAX_MILLIS_TO_WAIT to wait for response from SDM +* define user DERE_PIN for control MAX485 DE/RE lines (connect DE & /RE together to this pin) */ -//------------------------------------------------------------------------------ \ No newline at end of file +//#define DERE_PIN NOT_A_PIN + +//------------------------------------------------------------------------------ + +#if defined ( USE_HARDWARESERIAL ) + + /* + * define user SDM_UART_CONFIG for hardware serial + */ + //#define SDM_UART_CONFIG SERIAL_8N1 + + //---------------------------------------------------------------------------- + + /* + * define user SWAPHWSERIAL, if true(1) then swap uart pins from 3/1 to 13/15 (only ESP8266) + */ + //#define SWAPHWSERIAL 0 + +#else + + /* + * define user SDM_UART_CONFIG for software serial + */ + //#define SDM_UART_CONFIG SWSERIAL_8N1 + +#endif + +//------------------------------------------------------------------------------ + +/* +* define user WAITING_TURNAROUND_DELAY time in ms to wait for process current request +*/ +//#define WAITING_TURNAROUND_DELAY 200 + +//------------------------------------------------------------------------------ + +/* +* define user RESPONSE_TIMEOUT time in ms to wait for return response from all devices before next request +*/ +//#define RESPONSE_TIMEOUT 500 + +//------------------------------------------------------------------------------ diff --git a/lib/SDM_Energy_Meter/examples/sdm630_influxdb/sdm630_influxdb.ino b/lib/SDM_Energy_Meter/examples/sdm630_influxdb/sdm630_influxdb.ino new file mode 100644 index 000000000..9952ae4af --- /dev/null +++ b/lib/SDM_Energy_Meter/examples/sdm630_influxdb/sdm630_influxdb.ino @@ -0,0 +1,292 @@ +/* WEMOS D1 Mini + ______________________________ + | L T L T L T L T L T L T | + | | + RST| 1|TX HSer + A0| 3|RX HSer + D0|16 5|D1 + D5|14 4|D2 + D6|12 10kPUP_0|D3 +RX SSer/HSer swap D7|13 LED_10kPUP_2|D4 +TX SSer/HSer swap D8|15 |GND + 3V3|__ |5V + | | + |___________________________| +*/ + +//REMEMBER! uncomment #define USE_HARDWARESERIAL +//in SDM_Config_User.h file if you want to use hardware uart + +#include //import SDM library +#include +#include +#include +#include +#include +#include //https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino + + + +// WiFi Parameters +const char* ssid = "SSID"; +const char* password = "PASS"; +#define HOSTNAME "SDM630" // Friedly hostname + + + +// InfluxDB v2 server url, e.g. https://eu-central-1-1.aws.cloud2.influxdata.com (Use: InfluxDB UI -> Load Data -> Client Libraries) +// InfluxDB v1 server url "http://192.168.10.2:8086" +#define INFLUXDB_URL "http://INFLUXDB IP:8086" +// InfluxDB v2 server or cloud API authentication token (Use: InfluxDB UI -> Load Data -> Tokens ->