diff --git a/docs/source/Interfaces/Interfaces.rst b/docs/source/Interfaces/Interfaces.rst index 87e9084f16..c3a77c6f00 100644 --- a/docs/source/Interfaces/Interfaces.rst +++ b/docs/source/Interfaces/Interfaces.rst @@ -205,5 +205,20 @@ NB: When selecting the *User-defined* option, **all 3 GPIO pins should be set**, Modbus ------ -TODO Modbus configuration and documentation to be added. +When using devices that use the Modbus RTU protocol the Modbus interface can be configured here. The Modbus RTU devices are connected via a RS485 serial interface. Multiple devices can be connected to the same serial interface, and are distinguished by their Modbus address. A serial to RS485 converter is required to connect Modbus RTU devices to the ESP board. +Note that many plugins implement the Modbuis protocol for specific devices without the use of this generic Modbus interface. Only use the Modbus interface as configured on this page when using one or more plugins that depend on this generic Modbus RTU facility. +The Modbus interface uses a serial port on the ESP board to communicate with the Modbus RTU devices. The RS485 link is half-duplex, meaning that the ESP board can either send or receive data at a time. Most RS485 converters have a DE (Driver Enable) pin that can be controlled by the ESP board to switch between sending and receiving data. +The DE pin can be configured here. If the converter has an automatic DE function, this can be set to "-None-". Some serial ports on the ESP board have a collision detect feature, which can be enabled here. When supported and enabled, the ESP board will detect if another device is sending data at the same time, and will wait sending data. This can help to avoid collisions on the RS485 link. In case all Modbus devices are well-behaving and plugins are well-configured the collision detect function is not required for Modbus RTU operation. + +.. image:: Modbus_Interface.png + +Main configuration item is the selection of the serial interface to use for Modbus. The available options depend on the ESP board used. For details about the available serial interfaces, see serial helper page. The option "Not set" means that Modbus link is not used. When a serial interface is selected, it will be initialized and the Modbus RTU devices can be connected to it. + +Selection of the RX and TX pins is required. + +If the RS485 converter has a DE (Driver Enable) pin to be controlled by the ESP board it shall be configured here. If the converter has an automatic DE function, this can be set to "-None-". + +The Baud rate for the Modbus RTU communication shall be set. The default value is 9600 baud, but it can be set to other values as required by the connected devices. + +Collision Detection is a feature that can be used when the serial port hardware supports it, otherwise it is ignored. \ No newline at end of file diff --git a/docs/source/Interfaces/Modbus_Interface.png b/docs/source/Interfaces/Modbus_Interface.png new file mode 100644 index 0000000000..5987bb147d Binary files /dev/null and b/docs/source/Interfaces/Modbus_Interface.png differ diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst new file mode 100644 index 0000000000..8952920b63 --- /dev/null +++ b/docs/source/Plugin/P183.rst @@ -0,0 +1,123 @@ +.. include:: ../Plugin/_plugin_substitutions_p18x.repl +.. _P183_page: + +|P183_typename| +================================================== + +|P183_shortinfo| + +Plugin details +-------------- + +Type: |P183_type| + +Port Type: |P183_porttype| + +Name: |P183_name| + +Status: |P183_status| + +GitHub: |P183_github|_ + +Maintainer: |P183_maintainer| + +Used libraries: |P183_usedlibraries| + +Introduction +------------ + +Modbus is a serial communication protocol commonly used for connecting industrial electronic devices. +It is a master/slave (or client/server) protocol, which means that one device (the master) initiates communication and the other devices (the slaves) respond. +Modbus RTU (Remote Terminal Unit) is a variant of the Modbus protocol that uses binary representation of data and is typically used over serial communication lines such as RS-485 or RS-232. +This plugin supports Modbus RTU communication over a serial interface. To support RS-485 communication, an external RS-485 to TTL converter is required. + +Modbus RTU uses a register-based addressing scheme, where each device has a unique address and data is stored in registers. +Registers can be of different types, such as holding registers, input registers, coils, and discrete inputs. +This plugin supports reading holding registers (function code 03) and writing to holding registers (function code 06). + +The plugin can be configured to read up to 4 holding registers from a Modbus slave device and store the values in user variables. +It can also write values to a specified holding register on the Modbus slave device. + +The plugin does not support any hardware specific features. Instead it is a generic plugin that can be used with any Modbus RTU compatible device. You have to lookup the correct register addresses and data formats in the documentation of the Modbus device you want to communicate with. + +Supported hardware +------------------ + +|P183_usedby| + +Configuration +------------- + +* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +Sensor +^^^^^^ + +.. image:: P183_device_settings.png + +The available Modbus protocol settings here depend on the build used. + +* **Modbus Link**: The Modbs link teh device is connected to. The link must be configured in the Interafces page. + +* **Modbus Device Address**: The Modbus slave device ID to communicate with (1..247). + +Output Configuration +^^^^^^^^^^^^^^^^^^^^ + +.. image:: P183_output_config.png + +* **Number of registers to read**: The number of holding registers the plugin will read (1..4). + +* **Holding register for valueX**: The Modbus holding register address to read for valueX (X=1..4). + +The holding register address is a 16-bit value (0..65535). This is the register address as specified with the Modbus device and is used directly in the Modbus read holding registers (function code 03). The plugin will read the number of values specified, starting with value1 + +Commands available +^^^^^^^^^^^^^^^^^^ + +.. include:: P183_commands.repl + +.. Events +.. ~~~~~~ + +.. .. include:: P183_events.repl + +Get Config Values +^^^^^^^^^^^^^^^^^ + +Get Config Values retrieves values or settings from the sensor or plugin, and can be used in Rules, Display plugins, Formula's etc. The square brackets **are** part of the variable. Replace ```` by the **Name** of the task. + +.. include:: P183_config_values.repl + +Plugin state +------------ + +This plugin uses a new Modbus facility that allows multiple Modbus devices to share the same Modbus link. +The current implementataion of the Modbus facility requires that the plugin specifies the serial link parameters. +When a Modbus link is shared between multiple plugins, the last plugin that connects to the Modbus link will determine the serial link parameters. +This may lead to unexpected behavior if multiple plugins are using different serial link parameters on the same serial port. For a deterministic behavior, make sure that all plugins using the same serial port use the same serial link parameters. + +In the future the Modbus facility may be extended to configure the Modbus links as separate entities. As a result the plugins would only need to specify which Modbus link to use, instead of the serial link parameters. +This change will be backward incompatible with the current implementation. Therefore the plugin status is set to 'Experimental'. + +Change log +---------- + +.. versionchanged:: 2.0 + ... + + |added| + Major overhaul for 2.0 release. + +.. versionadded:: 1.0 + ... + + |added| + Initial release version. + + + + + diff --git a/docs/source/Plugin/P183_commands.repl b/docs/source/Plugin/P183_commands.repl new file mode 100644 index 0000000000..b750376cc6 --- /dev/null +++ b/docs/source/Plugin/P183_commands.repl @@ -0,0 +1,28 @@ +.. csv-table:: + :header: "Command", "Extra information" + :widths: 20, 30 + + " + ``modbus,write,
,`` + + "," + Write value ```` into the holding register ``
``. + " + " + ``modbus,read,
`` + + "," + Read the value from the holding register ``
``. Note that the value is read from the Modbus device but it will not be returned as part of the command. The value is printed in the logging. + " + " + ``modbus,scan`` + + "," + Scans the modbus address range for responding units. It will do a read function for holding register 1. Note this will take some time. Ouput is in the logging. + " + " + ``modbus,dump,,`` + + "," + Dumps the holding registers in the address range ```` until ````. + " \ No newline at end of file diff --git a/docs/source/Plugin/P183_config_values.repl b/docs/source/Plugin/P183_config_values.repl new file mode 100644 index 0000000000..65f63d65ea --- /dev/null +++ b/docs/source/Plugin/P183_config_values.repl @@ -0,0 +1,10 @@ +.. csv-table:: + :header: "Config value", "Information" + :widths: 20, 30 + + " + | ``[#register,]`` + "," + | Returns the value of holding register ```` of the device. The value is directly read from the Modbus device. The holding register number ```` can be any valid register in the device. + " + diff --git a/docs/source/Plugin/P183_device_settings.png b/docs/source/Plugin/P183_device_settings.png new file mode 100644 index 0000000000..1e1fb8f788 Binary files /dev/null and b/docs/source/Plugin/P183_device_settings.png differ diff --git a/docs/source/Plugin/P183_output_config.png b/docs/source/Plugin/P183_output_config.png new file mode 100644 index 0000000000..6f4802c231 Binary files /dev/null and b/docs/source/Plugin/P183_output_config.png differ diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index beee4d1d38..a5440da620 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -509,7 +509,7 @@ There are different released versions of ESP Easy: ":ref:`P177_page`","|P177_status|","|P177_status_lb|","P177" ":ref:`P178_page`","|P178_status|","|P178_status_lb|","P178" ":ref:`P180_page`","|P180_status|","|P180_status_lb|","P180" - + ":ref:`P183_page`","[P183_status]","[P183_status_lb]","P183" .. include:: .. include:: _plugin_sets_overview.repl diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index 0c4fe03df9..6d9c200790 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`, :ref:`P176_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`, :ref:`P183_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`, :ref:`P175_page` diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index 7294330f03..d82dcbc998 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -10,1862 +10,3 @@ Build set: :green:`NORMAL` :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" :widths: 10, 3, 3, 3 - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION A` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P067_page`", "✓", "✓", "P067" - ":ref:`P068_page`", "✓", "✓", "P068" - ":ref:`P070_page`", "✓", "✓", "P070" - ":ref:`P071_page`", "✓", "✓", "P071" - ":ref:`P072_page`", "✓", "✓", "P072" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P074_page`", "✓", "✓", "P074" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P080_page`", "✓", "✓", "P080" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P083_page`", "✓", "✓", "P083" - ":ref:`P084_page`", "✓", "✓", "P084" - ":ref:`P086_page`", "✓", "✓", "P086" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P090_page`", "✓", "✓", "P090" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P097_page`", "✓", "✓", "P097" - ":ref:`P098_page`", "✓", "✓", "P098" - ":ref:`P105_page`", "✓", "✓", "P105" - ":ref:`P134_page`", "✓", "✓", "P134" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION B` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P069_page`", "✓", "✓", "P069" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P100_page`", "✓", "✓", "P100" - ":ref:`P101_page`", "✓", "✓", "P101" - ":ref:`P106_page`", "✓", "✓", "P106" - ":ref:`P107_page`", "✓", "✓", "P107" - ":ref:`P108_page`", "✓", "✓", "P108" - ":ref:`P110_page`", "✓", "✓", "P110" - ":ref:`P113_page`", "✓", "✓", "P113" - ":ref:`P115_page`", "✓", "✓", "P115" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION C` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P085_page`", "✓", "✓", "P085" - ":ref:`P087_page`", "✓", "✓", "P087" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P091_page`", "✓", "✓", "P091" - ":ref:`P092_page`", "✓", "✓", "P092" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P111_page`", "✓", "✓", "P111" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P143_page`", "✓", "✓", "P143" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION D` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P093_page`", "✓", "✓", "P093" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P098_page`", "✓", "✓", "P098" - ":ref:`P114_page`", "✓", "✓", "P114" - ":ref:`P117_page`", "✓", "✓", "P117" - ":ref:`P124_page`", "✓", "✓", "P124" - ":ref:`P127_page`", "✓", "✓", "P127" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION E` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P119_page`", "✓", "✓", "P119" - ":ref:`P120_page`", "✓", "✓", "P120" - ":ref:`P121_page`", "✓", "✓", "P121" - ":ref:`P125_page`", "✓", "✓", "P125" - ":ref:`P126_page`", "✓", "✓", "P126" - ":ref:`P129_page`", "✓", "✓", "P129" - ":ref:`P133_page`", "✓", "✓", "P133" - ":ref:`P135_page`", "✓", "✓", "P135" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P144_page`", "✓", "✓", "P144" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION F` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P112_page`", "✓", "✓", "P112" - ":ref:`P118_page`", "✓", "✓", "P118" - ":ref:`P122_page`", "✓", "✓", "P122" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P145_page`", "✓", "✓", "P145" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P147_page`", "✓", "✓", "P147" - ":ref:`P150_page`", "✓", "✓", "P150" - ":ref:`P151_page`", "✓", "✓", "P151" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P153_page`", "✓", "✓", "P153" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION G` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P105_page`", "✓", "", "P105" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P142_page`", "✓", "✓", "P142" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P154_page`", "✓", "✓", "P154" - ":ref:`P159_page`", "✓", "✓", "P159" - ":ref:`P162_page`", "✓", "✓", "P162" - ":ref:`P163_page`", "✓", "", "P163" - ":ref:`P164_page`", "✓", "✓", "P164" - ":ref:`P166_page`", "✓", "✓", "P166" - ":ref:`P168_page`", "✓", "✓", "P168" - ":ref:`P170_page`", "✓", "✓", "P170" - ":ref:`P172_page`", "✓", "✓", "P172" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`COLLECTION H` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P045_page`", "✓", "✓", "P045" - ":ref:`P046_page`", "✓", "✓", "P046" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P048_page`", "✓", "✓", "P048" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P050_page`", "✓", "✓", "P050" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P054_page`", "✓", "✓", "P054" - ":ref:`P055_page`", "✓", "✓", "P055" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P057_page`", "✓", "✓", "P057" - ":ref:`P058_page`", "✓", "✓", "P058" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P060_page`", "✓", "✓", "P060" - ":ref:`P061_page`", "✓", "✓", "P061" - ":ref:`P062_page`", "✓", "✓", "P062" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P064_page`", "✓", "✓", "P064" - ":ref:`P065_page`", "✓", "✓", "P065" - ":ref:`P066_page`", "✓", "✓", "P066" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P082_page`", "✓", "✓", "P082" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P173_page`", "✓", "", "P173" - ":ref:`P177_page`", "✓", "", "P177" - ":ref:`P178_page`", "✓", "", "P178" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`C012_page`", "✓", "✓", "C012" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`C014_page`", "✓", "✓", "C014" - ":ref:`C017_page`", "✓", "✓", "C017" - ":ref:`C018_page`", "✓", "✓", "C018" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`CLIMATE A` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P068_page`", "✓", "✓", "P068" - ":ref:`P069_page`", "✓", "✓", "P069" - ":ref:`P072_page`", "✓", "✓", "P072" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P083_page`", "✓", "✓", "P083" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P090_page`", "✓", "✓", "P090" - ":ref:`P103_page`", "✓", "✓", "P103" - ":ref:`P105_page`", "✓", "✓", "P105" - ":ref:`P106_page`", "✓", "✓", "P106" - ":ref:`P117_page`", "✓", "✓", "P117" - ":ref:`P118_page`", "✓", "✓", "P118" - ":ref:`P127_page`", "✓", "✓", "P127" - ":ref:`P133_page`", "✓", "✓", "P133" - ":ref:`P135_page`", "✓", "✓", "P135" - ":ref:`P142_page`", "✓", "✓", "P142" - ":ref:`P144_page`", "✓", "✓", "P144" - ":ref:`P147_page`", "✓", "✓", "P147" - ":ref:`P150_page`", "✓", "✓", "P150" - ":ref:`P151_page`", "✓", "✓", "P151" - ":ref:`P153_page`", "✓", "✓", "P153" - ":ref:`P154_page`", "✓", "✓", "P154" - ":ref:`P164_page`", "✓", "✓", "P164" - ":ref:`P167_page`", "✓", "✓", "P167" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - -Build set: :yellow:`CLIMATE B` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P047_page`", "✓", "✓", "P047" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P051_page`", "✓", "✓", "P051" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P068_page`", "✓", "✓", "P068" - ":ref:`P069_page`", "✓", "✓", "P069" - ":ref:`P072_page`", "✓", "✓", "P072" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P081_page`", "✓", "✓", "P081" - ":ref:`P083_page`", "✓", "✓", "P083" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P090_page`", "✓", "✓", "P090" - ":ref:`P103_page`", "✓", "✓", "P103" - ":ref:`P105_page`", "✓", "✓", "P105" - ":ref:`P106_page`", "✓", "✓", "P106" - ":ref:`P117_page`", "✓", "✓", "P117" - ":ref:`P118_page`", "✓", "✓", "P118" - ":ref:`P127_page`", "✓", "✓", "P127" - ":ref:`P133_page`", "✓", "✓", "P133" - ":ref:`P135_page`", "✓", "✓", "P135" - ":ref:`P142_page`", "✓", "✓", "P142" - ":ref:`P144_page`", "✓", "✓", "P144" - ":ref:`P147_page`", "✓", "✓", "P147" - ":ref:`P150_page`", "✓", "✓", "P150" - ":ref:`P151_page`", "✓", "✓", "P151" - ":ref:`P153_page`", "✓", "✓", "P153" - ":ref:`P154_page`", "✓", "✓", "P154" - ":ref:`P164_page`", "✓", "✓", "P164" - ":ref:`P168_page`", "✓", "✓", "P168" - ":ref:`P169_page`", "✓", "", "P169" - ":ref:`P172_page`", "✓", "✓", "P172" - ":ref:`P173_page`", "✓", "✓", "P173" - ":ref:`P175_page`", "✓", "", "P175" - ":ref:`P177_page`", "✓", "", "P177" - ":ref:`P178_page`", "✓", "", "P178" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C011_page`", "✓", "✓", "C011" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - -Build set: :yellow:`DISPLAY A` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P075_page`", "✓", "✓", "P075" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P104_page`", "✓", "✓", "P104" - ":ref:`P109_page`", "✓", "", "P109" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P143_page`", "✓", "✓", "P143" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`DISPLAY B` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P095_page`", "✓", "✓", "P095" - ":ref:`P099_page`", "✓", "✓", "P099" - ":ref:`P109_page`", "✓", "", "P109" - ":ref:`P116_page`", "✓", "✓", "P116" - ":ref:`P123_page`", "✓", "", "P123" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P141_page`", "✓", "✓", "P141" - ":ref:`P143_page`", "✓", "✓", "P143" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P148_page`", "✓", "", "P148" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`ENERGY` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P076_page`", "✓", "✓", "P076" - ":ref:`P077_page`", "✓", "✓", "P077" - ":ref:`P078_page`", "✓", "✓", "P078" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P085_page`", "✓", "✓", "P085" - ":ref:`P087_page`", "✓", "", "P087" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P093_page`", "✓", "✓", "P093" - ":ref:`P102_page`", "✓", "✓", "P102" - ":ref:`P108_page`", "✓", "✓", "P108" - ":ref:`P115_page`", "✓", "✓", "P115" - ":ref:`P132_page`", "✓", "✓", "P132" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P148_page`", "✓", "", "P148" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P176_page`", "✓", "", "P176" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`IR` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P016_page`", "✓", "✓", "P016" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P035_page`", "✓", "✓", "P035" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`IRext` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P088_page`", "✓", "✓", "P088" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`NEOPIXEL` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "✓", "P000" - ":ref:`P001_page`", "✓", "✓", "P001" - ":ref:`P002_page`", "✓", "✓", "P002" - ":ref:`P003_page`", "✓", "✓", "P003" - ":ref:`P004_page`", "✓", "✓", "P004" - ":ref:`P005_page`", "✓", "✓", "P005" - ":ref:`P006_page`", "✓", "✓", "P006" - ":ref:`P007_page`", "✓", "✓", "P007" - ":ref:`P008_page`", "✓", "✓", "P008" - ":ref:`P009_page`", "✓", "✓", "P009" - ":ref:`P010_page`", "✓", "✓", "P010" - ":ref:`P011_page`", "✓", "✓", "P011" - ":ref:`P012_page`", "✓", "✓", "P012" - ":ref:`P013_page`", "✓", "✓", "P013" - ":ref:`P014_page`", "✓", "✓", "P014" - ":ref:`P015_page`", "✓", "✓", "P015" - ":ref:`P017_page`", "✓", "✓", "P017" - ":ref:`P018_page`", "✓", "✓", "P018" - ":ref:`P019_page`", "✓", "✓", "P019" - ":ref:`P020_page`", "✓", "✓", "P020" - ":ref:`P021_page`", "✓", "✓", "P021" - ":ref:`P022_page`", "✓", "✓", "P022" - ":ref:`P023_page`", "✓", "✓", "P023" - ":ref:`P024_page`", "✓", "✓", "P024" - ":ref:`P025_page`", "✓", "✓", "P025" - ":ref:`P026_page`", "✓", "✓", "P026" - ":ref:`P027_page`", "✓", "✓", "P027" - ":ref:`P028_page`", "✓", "✓", "P028" - ":ref:`P029_page`", "✓", "✓", "P029" - ":ref:`P031_page`", "✓", "✓", "P031" - ":ref:`P032_page`", "✓", "✓", "P032" - ":ref:`P033_page`", "✓", "✓", "P033" - ":ref:`P034_page`", "✓", "✓", "P034" - ":ref:`P036_page`", "✓", "✓", "P036" - ":ref:`P037_page`", "✓", "✓", "P037" - ":ref:`P038_page`", "✓", "✓", "P038" - ":ref:`P039_page`", "✓", "✓", "P039" - ":ref:`P040_page`", "✓", "✓", "P040" - ":ref:`P041_page`", "✓", "✓", "P041" - ":ref:`P042_page`", "✓", "✓", "P042" - ":ref:`P043_page`", "✓", "✓", "P043" - ":ref:`P044_page`", "✓", "✓", "P044" - ":ref:`P049_page`", "✓", "✓", "P049" - ":ref:`P052_page`", "✓", "✓", "P052" - ":ref:`P053_page`", "✓", "✓", "P053" - ":ref:`P056_page`", "✓", "✓", "P056" - ":ref:`P059_page`", "✓", "✓", "P059" - ":ref:`P063_page`", "✓", "✓", "P063" - ":ref:`P070_page`", "✓", "✓", "P070" - ":ref:`P073_page`", "✓", "✓", "P073" - ":ref:`P079_page`", "✓", "✓", "P079" - ":ref:`P089_page`", "✓", "✓", "P089" - ":ref:`P105_page`", "✓", "", "P105" - ":ref:`P128_page`", "✓", "✓", "P128" - ":ref:`P131_page`", "✓", "✓", "P131" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P146_page`", "✓", "✓", "P146" - ":ref:`P152_page`", "✓", "✓", "P152" - ":ref:`P165_page`", "✓", "✓", "P165" - ":ref:`P180_page`", "✓", "✓", "P180" - ":ref:`C001_page`", "✓", "✓", "C001" - ":ref:`C002_page`", "✓", "✓", "C002" - ":ref:`C003_page`", "✓", "✓", "C003" - ":ref:`C004_page`", "✓", "✓", "C004" - ":ref:`C005_page`", "✓", "✓", "C005" - ":ref:`C006_page`", "✓", "✓", "C006" - ":ref:`C007_page`", "✓", "✓", "C007" - ":ref:`C008_page`", "✓", "✓", "C008" - ":ref:`C009_page`", "✓", "✓", "C009" - ":ref:`C010_page`", "✓", "✓", "C010" - ":ref:`C013_page`", "✓", "✓", "C013" - ":ref:`N001_page`", "✓", "✓", "N001" - ":ref:`N002_page`", "✓", "✓", "N002" - ":ref:`NW001_page`", "✓", "✓", "NW001" - ":ref:`NW002_page`", "✓", "✓", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - -Build set: :yellow:`MAX` ---------------------------------------------- - -.. collapse:: Details... - - .. csv-table:: - :header: "Plugin name", "ESP32", "ESP8266", "Plugin number" - :widths: 10, 3, 3, 3 - - ":ref:`P000_page`", "✓", "", "P000" - ":ref:`P001_page`", "✓", "", "P001" - ":ref:`P002_page`", "✓", "", "P002" - ":ref:`P003_page`", "✓", "", "P003" - ":ref:`P004_page`", "✓", "", "P004" - ":ref:`P005_page`", "✓", "", "P005" - ":ref:`P006_page`", "✓", "", "P006" - ":ref:`P007_page`", "✓", "", "P007" - ":ref:`P008_page`", "✓", "", "P008" - ":ref:`P009_page`", "✓", "", "P009" - ":ref:`P010_page`", "✓", "", "P010" - ":ref:`P011_page`", "✓", "", "P011" - ":ref:`P012_page`", "✓", "", "P012" - ":ref:`P013_page`", "✓", "", "P013" - ":ref:`P014_page`", "✓", "", "P014" - ":ref:`P015_page`", "✓", "", "P015" - ":ref:`P016_page`", "✓", "", "P016" - ":ref:`P017_page`", "✓", "", "P017" - ":ref:`P018_page`", "✓", "", "P018" - ":ref:`P019_page`", "✓", "", "P019" - ":ref:`P020_page`", "✓", "", "P020" - ":ref:`P021_page`", "✓", "", "P021" - ":ref:`P022_page`", "✓", "", "P022" - ":ref:`P023_page`", "✓", "", "P023" - ":ref:`P024_page`", "✓", "", "P024" - ":ref:`P025_page`", "✓", "", "P025" - ":ref:`P026_page`", "✓", "", "P026" - ":ref:`P027_page`", "✓", "", "P027" - ":ref:`P028_page`", "✓", "", "P028" - ":ref:`P029_page`", "✓", "", "P029" - ":ref:`P030_page`", "✓", "", "P030" - ":ref:`P031_page`", "✓", "", "P031" - ":ref:`P032_page`", "✓", "", "P032" - ":ref:`P033_page`", "✓", "", "P033" - ":ref:`P034_page`", "✓", "", "P034" - ":ref:`P035_page`", "✓", "", "P035" - ":ref:`P036_page`", "✓", "", "P036" - ":ref:`P037_page`", "✓", "", "P037" - ":ref:`P038_page`", "✓", "", "P038" - ":ref:`P039_page`", "✓", "", "P039" - ":ref:`P040_page`", "✓", "", "P040" - ":ref:`P041_page`", "✓", "", "P041" - ":ref:`P042_page`", "✓", "", "P042" - ":ref:`P043_page`", "✓", "", "P043" - ":ref:`P044_page`", "✓", "", "P044" - ":ref:`P045_page`", "✓", "", "P045" - ":ref:`P046_page`", "✓", "", "P046" - ":ref:`P047_page`", "✓", "", "P047" - ":ref:`P048_page`", "✓", "", "P048" - ":ref:`P049_page`", "✓", "", "P049" - ":ref:`P050_page`", "✓", "", "P050" - ":ref:`P051_page`", "✓", "", "P051" - ":ref:`P052_page`", "✓", "", "P052" - ":ref:`P053_page`", "✓", "", "P053" - ":ref:`P054_page`", "✓", "", "P054" - ":ref:`P055_page`", "✓", "", "P055" - ":ref:`P056_page`", "✓", "", "P056" - ":ref:`P057_page`", "✓", "", "P057" - ":ref:`P058_page`", "✓", "", "P058" - ":ref:`P059_page`", "✓", "", "P059" - ":ref:`P060_page`", "✓", "", "P060" - ":ref:`P061_page`", "✓", "", "P061" - ":ref:`P062_page`", "✓", "", "P062" - ":ref:`P063_page`", "✓", "", "P063" - ":ref:`P064_page`", "✓", "", "P064" - ":ref:`P065_page`", "✓", "", "P065" - ":ref:`P066_page`", "✓", "", "P066" - ":ref:`P067_page`", "✓", "", "P067" - ":ref:`P068_page`", "✓", "", "P068" - ":ref:`P069_page`", "✓", "", "P069" - ":ref:`P070_page`", "✓", "", "P070" - ":ref:`P071_page`", "✓", "", "P071" - ":ref:`P072_page`", "✓", "", "P072" - ":ref:`P073_page`", "✓", "", "P073" - ":ref:`P074_page`", "✓", "", "P074" - ":ref:`P075_page`", "✓", "", "P075" - ":ref:`P076_page`", "✓", "", "P076" - ":ref:`P077_page`", "✓", "", "P077" - ":ref:`P078_page`", "✓", "", "P078" - ":ref:`P079_page`", "✓", "", "P079" - ":ref:`P080_page`", "✓", "", "P080" - ":ref:`P081_page`", "✓", "", "P081" - ":ref:`P082_page`", "✓", "", "P082" - ":ref:`P083_page`", "✓", "", "P083" - ":ref:`P084_page`", "✓", "", "P084" - ":ref:`P085_page`", "✓", "", "P085" - ":ref:`P086_page`", "✓", "", "P086" - ":ref:`P087_page`", "✓", "", "P087" - ":ref:`P088_page`", "✓", "", "P088" - ":ref:`P089_page`", "✓", "", "P089" - ":ref:`P090_page`", "✓", "", "P090" - ":ref:`P091_page`", "✓", "", "P091" - ":ref:`P092_page`", "✓", "", "P092" - ":ref:`P093_page`", "✓", "", "P093" - ":ref:`P094_page`", "✓", "✓", "P094" - ":ref:`P095_page`", "✓", "", "P095" - ":ref:`P097_page`", "✓", "", "P097" - ":ref:`P098_page`", "✓", "", "P098" - ":ref:`P099_page`", "✓", "", "P099" - ":ref:`P100_page`", "✓", "", "P100" - ":ref:`P101_page`", "✓", "", "P101" - ":ref:`P102_page`", "✓", "", "P102" - ":ref:`P103_page`", "✓", "", "P103" - ":ref:`P104_page`", "✓", "", "P104" - ":ref:`P105_page`", "✓", "", "P105" - ":ref:`P106_page`", "✓", "", "P106" - ":ref:`P107_page`", "✓", "", "P107" - ":ref:`P108_page`", "✓", "", "P108" - ":ref:`P109_page`", "✓", "", "P109" - ":ref:`P110_page`", "✓", "", "P110" - ":ref:`P111_page`", "✓", "", "P111" - ":ref:`P112_page`", "✓", "", "P112" - ":ref:`P113_page`", "✓", "", "P113" - ":ref:`P114_page`", "✓", "", "P114" - ":ref:`P115_page`", "✓", "", "P115" - ":ref:`P116_page`", "✓", "", "P116" - ":ref:`P117_page`", "✓", "", "P117" - ":ref:`P118_page`", "✓", "", "P118" - ":ref:`P119_page`", "✓", "", "P119" - ":ref:`P120_page`", "✓", "", "P120" - ":ref:`P121_page`", "✓", "", "P121" - ":ref:`P122_page`", "✓", "", "P122" - ":ref:`P123_page`", "✓", "", "P123" - ":ref:`P124_page`", "✓", "", "P124" - ":ref:`P125_page`", "✓", "", "P125" - ":ref:`P126_page`", "✓", "", "P126" - ":ref:`P127_page`", "✓", "", "P127" - ":ref:`P128_page`", "✓", "", "P128" - ":ref:`P129_page`", "✓", "", "P129" - ":ref:`P131_page`", "✓", "", "P131" - ":ref:`P132_page`", "✓", "", "P132" - ":ref:`P133_page`", "✓", "", "P133" - ":ref:`P134_page`", "✓", "", "P134" - ":ref:`P135_page`", "✓", "", "P135" - ":ref:`P137_page`", "✓", "", "P137" - ":ref:`P138_page`", "✓", "", "P138" - ":ref:`P139_page`", "✓", "", "P139" - ":ref:`P140_page`", "✓", "", "P140" - ":ref:`P141_page`", "✓", "", "P141" - ":ref:`P142_page`", "✓", "", "P142" - ":ref:`P143_page`", "✓", "", "P143" - ":ref:`P144_page`", "✓", "", "P144" - ":ref:`P145_page`", "✓", "", "P145" - ":ref:`P146_page`", "✓", "", "P146" - ":ref:`P147_page`", "✓", "", "P147" - ":ref:`P148_page`", "✓", "", "P148" - ":ref:`P150_page`", "✓", "", "P150" - ":ref:`P151_page`", "✓", "", "P151" - ":ref:`P152_page`", "✓", "", "P152" - ":ref:`P153_page`", "✓", "", "P153" - ":ref:`P154_page`", "✓", "", "P154" - ":ref:`P159_page`", "✓", "", "P159" - ":ref:`P162_page`", "✓", "", "P162" - ":ref:`P163_page`", "✓", "", "P163" - ":ref:`P164_page`", "✓", "", "P164" - ":ref:`P165_page`", "✓", "", "P165" - ":ref:`P166_page`", "✓", "", "P166" - ":ref:`P167_page`", "✓", "", "P167" - ":ref:`P168_page`", "✓", "", "P168" - ":ref:`P169_page`", "✓", "", "P169" - ":ref:`P170_page`", "✓", "", "P170" - ":ref:`P172_page`", "✓", "", "P172" - ":ref:`P173_page`", "✓", "", "P173" - ":ref:`P175_page`", "✓", "", "P175" - ":ref:`P176_page`", "✓", "", "P176" - ":ref:`P177_page`", "✓", "", "P177" - ":ref:`P178_page`", "✓", "", "P178" - ":ref:`P180_page`", "✓", "", "P180" - ":ref:`C001_page`", "✓", "", "C001" - ":ref:`C002_page`", "✓", "", "C002" - ":ref:`C003_page`", "✓", "", "C003" - ":ref:`C004_page`", "✓", "", "C004" - ":ref:`C005_page`", "✓", "", "C005" - ":ref:`C006_page`", "✓", "", "C006" - ":ref:`C007_page`", "✓", "", "C007" - ":ref:`C008_page`", "✓", "", "C008" - ":ref:`C009_page`", "✓", "", "C009" - ":ref:`C010_page`", "✓", "", "C010" - ":ref:`C011_page`", "✓", "", "C011" - ":ref:`C012_page`", "✓", "", "C012" - ":ref:`C013_page`", "✓", "", "C013" - ":ref:`C014_page`", "✓", "", "C014" - ":ref:`C016_page`", "✓", "", "C016" - ":ref:`C017_page`", "✓", "", "C017" - ":ref:`C018_page`", "✓", "", "C018" - ":ref:`N001_page`", "✓", "", "N001" - ":ref:`N002_page`", "✓", "", "N002" - ":ref:`NW001_page`", "✓", "", "NW001" - ":ref:`NW002_page`", "✓", "", "NW002" - ":ref:`NW003_page`", "✓", "", "NW003" - ":ref:`NW004_page`", "✓", "", "NW004" - ":ref:`NW005_page`", "✓", "", "NW005" - diff --git a/docs/source/Plugin/_plugin_substitutions_p18x.repl b/docs/source/Plugin/_plugin_substitutions_p18x.repl index 654288b7c3..440ca17c0d 100644 --- a/docs/source/Plugin/_plugin_substitutions_p18x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p18x.repl @@ -12,3 +12,15 @@ .. |P180_compileinfo| replace:: `.` .. |P180_usedlibraries| replace:: `.` +.. |P183_name| replace:: :cyan:`Modbus RTU` +.. |P183_type| replace:: :cyan:`Communication` +.. |P183_typename| replace:: :cyan:`Communication - Modbus RTU` +.. |P183_porttype| replace:: `Serial` +.. |P183_status| replace:: :yellow:`TESTING` :yellow:`TESTING` +.. |P183_github| replace:: P183_modbus.ino +.. _P183_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P183_modbus.ino +.. |P183_usedby| replace:: `Modbus RTU over RS485 devices` +.. |P183_shortinfo| replace:: `Generic Modbus RTU sensor` +.. |P183_maintainer| replace:: `flashmark` +.. |P183_compileinfo| replace:: `.` +.. |P183_usedlibraries| replace:: `ESPEasySerial` diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml new file mode 100644 index 0000000000..50365290f7 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.puml @@ -0,0 +1,47 @@ +@startuml + +class plugin { + +} + +class plugin_struct { + +} + +package "modbusFacility" { + class Modbus_mgr <> { + } + + class Modbus_link { + } + + class Modbus_device { + } + + class Queue { + } + + struct Transaction { + } +} + +class Serial_port { +} + +class interface_gui { +} + +class kvs { +} + +plugin *-- plugin_struct +plugin_struct *-right- Modbus_device +Modbus_link "1" -right-> "1" Serial_port +Modbus_mgr "1" *--- "*" Modbus_link +Modbus_mgr "1" <-- "*" Modbus_device +Modbus_link *-- "1" Queue +Queue *-left- "*" Transaction +Modbus_device "1" --> "*" Transaction +interface_gui -left-> Modbus_mgr +Modbus_mgr --> kvs +@enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_class.svg b/misc/modbusFacility/Modbus_class.svg new file mode 100644 index 0000000000..9da7462b91 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.svg @@ -0,0 +1 @@ +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_portinterface_guikvs111*1*1*1* \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_link_state.puml b/misc/modbusFacility/Modbus_link_state.puml new file mode 100644 index 0000000000..8373f9d9fd --- /dev/null +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -0,0 +1,14 @@ +@startuml +[*] --> NOT_QUEUED : newTransaction() +state active { +NOT_QUEUED --> QUEUED : queueTransaction() +QUEUED --> MESSAGE_SENT : link inactive +MESSAGE_SENT --> RESPONSE_RECEIVED : received valid response +MESSAGE_SENT --> ERROR_OCCURRED : response timeout or invalid response +MESSAGE_SENT -right-> RECOVERING : response timeout or invalid response after previous timeout +RECOVERING --> ERROR_OCCURRED : timeout +} +active -down-> READY_FOR_DESTROY : freeTransaction() +READY_FOR_DESTROY --> [*] : processing + +@enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_link_state.svg b/misc/modbusFacility/Modbus_link_state.svg new file mode 100644 index 0000000000..b6ff91aed4 --- /dev/null +++ b/misc/modbusFacility/Modbus_link_state.svg @@ -0,0 +1 @@ +activeQUEUEDMESSAGE_SENTRESPONSE_RECEIVEDERROR_OCCURREDNOT_QUEUEDREADY_FOR_DESTROYnewTransaction()queueTransaction()link inactivereceived valid responseresponse timeout or invalid responsefreeTransaction()processing \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_notes.txt b/misc/modbusFacility/Modbus_notes.txt new file mode 100644 index 0000000000..13db191fb1 --- /dev/null +++ b/misc/modbusFacility/Modbus_notes.txt @@ -0,0 +1,55 @@ +The MODBUS_FAC facilities intend to provide a framework that allows multiple Modbus RTU devices to connect to a single physical Modbus link. It supports multiple physical Modbus links in parallel on the ame bus. +This is realized by introducing 3 classes: +- Modbus_device: Represents a single device. The Modbus_device is responsible for coding/decoding the Modus protocol. +- Modbus_link: Represents a single Modbus connection. Each Modbus_link object is connected to a separate serial link. +- Modbus_mgr: Singleton manager that manages all links and keeps track of which devices are connected to which link. +See class diagram Modbus_class for relations. + +The main design requirement is to allow multiple plugins to use the same Modbus link. This is achieved by separating the link control from the device control. +Modbus connunication consists of a request-reply message exchange over a serial link. After a request is sent the bus is occupied until the reply is received or a timeout occurred. +A matching request-reply message pair is called a transaction in MODBUS_FAC. + +The main design constraint for the MODBUS_FAC is to reduce the time wasted in waiting for a response message. For this a transaction queue is used. +The queue is owned by the Modbus_link. A Modbus_device puts a request message on the queue and does not wait for the reply. +Once the matching reply is received by the Modbus_link the Modbus_device is notified by a callback function. + +The Modbus_device uses transactions to communicate to the associated Modbus hardware module. Messages are encoded in Modbus RTU protocol format by the Modbus_device. +Once the request message is created it is transferred to the Modbus_link where it is added the the transaction queue. +When the transaction is done, either a response message is received or a timeout has occurred, the Modbus_device callback function is called and the response message or timeout is decoded and evaluated. + +The Modbus_link uses a queue to handle transactions sequentially. Each transaction has a state to track if it is queued, being processed, or completed. At maximum one single transaction per link is being processed at a time. +The modbus_link uses an ESPEasy serial link for the actual transmitting and receiving of the messages. + +A singleton Modbus_mgr owns the Modbus_links. The Modbus_mgt is responsible for creating and configuring the Modbus_links. Configuration is done through the interfaces page, tab Modbus. +The Modbus_device is configured with a link ID. The Modbus_device uses the link ID to register itself at the Modbus_mgr. The Modbus_mgr will return a pointer to the associated Modbus_link. Once connected the Modbus_device will access teh Modbus_link directly. + +Drawback of the queue mechanism is the need for the Modbus_device (and the associated plugin) to wait for the response message without blocking the CPU. For this the Modbus_device needs to track "state". +A plugin can queue only a single transaction or it must be able to track for which transaction in the queue a response is received. +Sevaral mechanisms are used to track transactions at link level: +1) The Modbus_link assigns a unique ID to each transaction. This is a 16 bit sequence number that will wrap around. The Modbus_device can use the ID to match a respose to the right pending transaction. +2) The Modbus_device can use the field _userID to store a 16 bit identifier. This identifier is not changed or interpreted by the Modbus_link. It can be used to recognize the transaction based on a Modbus_device determined algorithm +3) The Modbus_device can use the fields _userData and _userState to add two pointers that will not be changed or interpreted by the Modbus_link. These can be used to associate transaction related data with the transaction. + +The Modbus_device provides Modbus access functions (commands) to the plugin. Access like reading or writing registers is translated into a Modbus send message and queued on the Modbus_link. Once the message is queued the Modbus_devcie will return. +The Modbus_device may have multiple pending transactions. The Modbus_device uses one of the mentioned tracking mechanisms to recognize to which Modbus command the response message belongs. The response will be decoded. +In case the plugin needs to be informed about the results, e.g. a read command, the Modbus_device will send a PLUGIN_TASKTIMER_IN event to the plugin. This allows the plugin to process the results. See Modbus_seq2 sequence diagram. + +The PLUGIN_TASKTIMER_IN event works well for plugin functions that allow independent processing once the data is available. Some plugin functions like PLUGIN_GET_CONFIG_VALUE require a direct response. +If this response depends on Modbus tarnsactions the plugin shall either regularly poll the data and cache it or take the penalty and use a blocking funtion to retrieve this data. + +The Modbus_device provides the following functions to a plugin: + +1) bool readHoldingRegister(uint16_t address, uint16_t *valueptr, ModbusResultState *stateptr); +This function reads a single holding register from the Modbus hardware. The valueptr returns the value from the register once the transaction is finished. The stateptr shows the actual state of the transaction. The plugin shall poll the state until the tranaction is finished. +The Modbus_device will not use a PLUGIN_TASKTIMER_IN event to indicate the function has finished + +2) bool readHoldingRegister(uint16_t address, uint16_t uid); +This function reads a single holding register from the Modbus hardware. The uid shall be used by the plugin to recognize which read action the result belongs to. This uid will also be used in the PLUGIN_TASKTIMER_IN event to indicate the command. +At the end of the transaction the Modbus_device sends a PLUGIN_TASKTIMER_IN event with the following data: + idx: The uid given with the command + par1: Success of the transaction 0=not succseful 1=succesful, result available + par2: The value of the holding register cast to a int + +3) bool writeSingleRegister(uint16_t address, uint16_t value, ModbusResultState *stateptr); +This function writes a single holding register to the Modbus hardware. The value will be written to the specified register address. The stateptr shows the actual state of the transaction. The plugin may poll the state until the tranaction is finished. +The Modbus_device will not use a PLUGIN_TASKTIMER_IN event to indicate the function has finished \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.puml b/misc/modbusFacility/Modbus_seq2.puml new file mode 100644 index 0000000000..2ec0f09708 --- /dev/null +++ b/misc/modbusFacility/Modbus_seq2.puml @@ -0,0 +1,56 @@ +@startuml +actor System as system +participant Plugin as plugin +participant Plugin_struct as plugin_struct +participant Modbus_device as modbus_device +participant Modbus_mgr as modbus_mgr <> +participant Modbus_link as modbus_link +queue Queue as queue + +system -> modbus_mgr ++ : init +create modbus_link +modbus_mgr -> modbus_link : new() +system <-- modbus_mgr -- +system -> plugin ++ : init +create plugin_struct +plugin -> plugin_struct ++ : new() +create modbus_device +plugin_struct -> modbus_device ++ : new() +modbus_device -> modbus_mgr ++ : connect() +modbus_device <-- modbus_mgr -- : link +plugin_struct <-- modbus_device -- +plugin <-- plugin_struct -- +system <-- plugin -- + +== read == + +system -> plugin ++ : read +plugin -> plugin_struct ++ : read() +plugin_struct -> modbus_device ++ : readHoldingRegister() +modbus_device -> modbus_link ++: queueTransaction() +modbus_link -> queue : add +modbus_device <-- modbus_link -- +plugin_struct <-- modbus_device -- +plugin_struct -> plugin_struct : retrieve latest value +plugin <-- plugin_struct -- +system <-- plugin -- + +system -> modbus_mgr ++ : ten_per_second +modbus_mgr -> modbus_link ++: processCommand() +alt response available + modbus_link -> modbus_device ++ : linkCallback() + modbus_device -> plugin ++ : PLUGIN_TASKTIMER_IN + plugin -> plugin_struct ++ : plugin_task_timer() + plugin <-- plugin_struct -- + modbus_device <-- plugin -- + modbus_link <-- modbus_device -- +end +alt no transaction + modbus_link -> queue : get next transaction + alt transaction available + modbus_link -> modbus_link : send request + end +end +modbus_mgr <-- modbus_link -- +system <-- modbus_mgr -- +@enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.svg b/misc/modbusFacility/Modbus_seq2.svg new file mode 100644 index 0000000000..6adf1e64b6 --- /dev/null +++ b/misc/modbusFacility/Modbus_seq2.svg @@ -0,0 +1 @@ +SystemSystemPluginPluginPlugin_structModbus_device«singleton»Modbus_mgr«singleton»Modbus_mgrModbus_linkQueueQueueinitnew()Modbus_linkinitnew()Plugin_structnew()Modbus_deviceconnect()linkreadreadread()readHoldingRegister()queueTransaction()addretrieve latest valueten_per_secondprocessCommand()alt[response available]linkCallback()PLUGIN_TASKTIMER_INplugin_task_timer()alt[no transaction]get next transactionalt[transaction available]send request \ No newline at end of file diff --git a/out/misc/modbusFacility/Modbus_class/Modbus_class.svg b/out/misc/modbusFacility/Modbus_class/Modbus_class.svg new file mode 100644 index 0000000000..9da7462b91 --- /dev/null +++ b/out/misc/modbusFacility/Modbus_class/Modbus_class.svg @@ -0,0 +1 @@ +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_portinterface_guikvs111*1*1*1* \ No newline at end of file diff --git a/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg b/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg new file mode 100644 index 0000000000..b6ff91aed4 --- /dev/null +++ b/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg @@ -0,0 +1 @@ +activeQUEUEDMESSAGE_SENTRESPONSE_RECEIVEDERROR_OCCURREDNOT_QUEUEDREADY_FOR_DESTROYnewTransaction()queueTransaction()link inactivereceived valid responseresponse timeout or invalid responsefreeTransaction()processing \ No newline at end of file diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino new file mode 100644 index 0000000000..73d8e06e88 --- /dev/null +++ b/src/_P183_modbus.ino @@ -0,0 +1,351 @@ +#include "_Plugin_Helper.h" +#ifdef USES_P183 + +// ####################################################################################################### +// ############## Plugin 183: Modbus RTU generic sensor interface ############### +// ####################################################################################################### + +/* + Plugin written by: Flashmark + + This plugin reads values from a generic Modbus RTU device. It sees the device as a series of registers. + Up to 4 registers can be monitored and presented as standard output values of the plugin. + The plugin also provides means to write register using the PLUGIN_WRITE commands. + For debugging the Modbus and accessing other registers additional commands are available. + This plugin uses a generic MODBUS_FAC facility to share a single Modbus link with multiple device instances. + */ + +/** + * Changelog: + * 2026-04-29 flashmark: Refactor for new modbus facility using separated Modbus link object. + * 2026-04-13 flashmark: Separate Modbus link definition from plugin. + * 2025-10-12 flashmark: Restructuring and adding a MODBUS_FAC facility + * 2025-08-24 flashmark: Initial version + */ + + //// # define P183_DEBUG + +# define PLUGIN_183 +# define PLUGIN_ID_183 183 +# define PLUGIN_NAME_183 "Communication - Modbus RTU" +# define P183_NR_OUTPUT_VALUES 4 +# define PLUGIN_VALUENAME1_183 "Value1" +# define PLUGIN_VALUENAME2_183 "Value2" +# define PLUGIN_VALUENAME3_183 "Value3" +# define PLUGIN_VALUENAME4_183 "Value4" + +# include +# include "src/PluginStructs/P183_data_struct.h" +# include "src/Helpers/Modbus_device.h" +# include "src/Helpers/Modbus_mgr.h" + +boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + auto& dev = Device[++deviceCount]; + dev.Number = PLUGIN_ID_183; + dev.Type = DEVICE_TYPE_CUSTOM0; // Custom device type, connects to Modbus + dev.VType = Sensor_VType::SENSOR_TYPE_QUAD; + dev.FormulaOption = true; + dev.ValueCount = P183_NR_OUTPUT_VALUES; + dev.OutputDataType = Output_Data_type_t::Simple; + dev.SendDataOption = true; + dev.TimerOption = true; + dev.PluginStats = true; + dev.TaskLogsOwnPeaks = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_183); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_183)); + break; + } + + case PLUGIN_GET_DEVICEVALUECOUNT: + { + event->Par1 = getValueCountFromSensorType(static_cast(P183_NR_OUTPUTS)); + success = true; + break; + } + + case PLUGIN_GET_DEVICEVTYPE: + { + event->sensorType = static_cast(P183_NR_OUTPUTS); + event->idx = P183_NR_OUTPUTS_INDEX; + success = true; + break; + } + + case PLUGIN_SET_DEFAULTS: + { + P183_DEV_ID = P183_DEV_ID_DFLT; + P183_LINK_ID = P183_LINK_ID_DFLT; + P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs + P183_CACHE_SIZE = 0; // Default to no cache + success = true; + break; + } + + case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: + { + // if ((P183_NR_OUTPUTS < 1) || (P183_NR_OUTPUTS > P183_NR_OUTPUT_VALUES)) { + // P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs + // } +// addFormNumericBox(F("Number of values to read"), P183_NR_OUTPUTS_LABEL, P183_NR_OUTPUTS); + + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) + { + addFormNumericBox(concat(F("Holding Register for value"), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), + P183_ADDRESS(outputIndex)); + } + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); + addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, P183_MAX_MODBUS_NODES); + addFormNumericBox(F("Cache size"), P183_CACHE_SIZE_LABEL, P183_CACHE_SIZE, 0, P183_CACHE_SIZE_MAX); + addFormNumericBox(F("Cache start address"), P183_CACHE_START_LABEL, P183_CACHE_START, 0, P183_CACHE_START_MAX); + + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); + P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); + P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); + P183_CACHE_START = getFormItemInt(P183_CACHE_START_LABEL); + P183_CACHE_SIZE = getFormItemInt(P183_CACHE_SIZE_LABEL); + + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) + { + P183_ADDRESS(outputIndex) = getFormItemInt(P183_ADDRESS_LABEL(outputIndex)); + } + + success = true; + break; + } + + case PLUGIN_INIT: + { + initPluginTaskData(event->TaskIndex, new (std::nothrow) P183_data_struct(event)); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data != nullptr) { + success = P183_data->plugin_init(P183_DEV_ID, P183_LINK_ID); + } + else { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); + # endif // LIMIT_BUILD_SIZE + success = false; + } + break; + } + + case PLUGIN_EXIT: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P183_data) { + P183_data->plugin_exit(); + } + success = true; + break; + } + + case PLUGIN_READ: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read invalid data struct")); + # endif // LIMIT_BUILD_SIZE + return false; + } + success = P183_data->plugin_read(event); // Delegate to data_struct + break; + } + + case PLUGIN_WRITE: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus write invalid data struct")); + # endif // LIMIT_BUILD_SIZE + return false; + } + + const String cmd = parseString(string, 1); + + if (equals(cmd, F("modbus"))) { + const String subcmd = parseString(string, 2); + + if (equals(subcmd, F("write"))) { + // Write a value to a Modbus register + int address = event->Par2; + uint16_t value = event->Par3; + P183_data->writeRegister(address, value); + # ifdef P183_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus write value %u to address 0x%04x"), value, address)); + } + # endif // P183_DEBUG + success = true; + } + else if (equals(subcmd, F("read"))) { + // Read a value from a Modbus register + uint16_t address = event->Par2; + uint16_t value = 0; + + if ((address >= P183_CACHE_START) && (address < P183_CACHE_START + P183_CACHE_SIZE)) { + value = P183_data->readRegisterCache(address); + } + else { + # if P183_ALLOW_MODBUS_WAIT + value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + # else + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus read command with invalid address"), event->TaskIndex+1)); + value = 0; + # endif // if P183_ALLOW_MODBUS_WAIT + + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P183[%d]: Modbus read value %u from address 0x%04x"), event->TaskIndex+1, value, address)); + } + success = true; + } + else if (equals(subcmd, F("dump"))) { + uint16_t start_address = event->Par2; + uint16_t end_address = event->Par3; + + if (end_address < start_address) { + end_address = start_address; + } + + if (end_address - start_address > 100) { + end_address = start_address + 100; // Limit to 100 addresses + } + P183_data->scan_device(P183_DEV_ID, start_address, end_address); + success = true; + } + else if (equals(subcmd, F("scan"))) { + // Scan for Modbus devices + P183_data->scan_modbus(); + success = true; + } + else if (equals(subcmd, F("debug"))) { + // Dump Modbus admin info + ModbusMGR_singleton.dumpAdminInfo(); + success = true; + } + # ifndef LIMIT_BUILD_SIZE + else { + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Unknown command"), event->TaskIndex+1)); + } + # endif // LIMIT_BUILD_SIZE + } + + break; + } + + // Event from Modbus device for a response received on a queued request + case PLUGIN_TASKTIMER_IN: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus task timer invalid data struct"), event->TaskIndex+1)); + # endif // LIMIT_BUILD_SIZE + return false; + } + success = P183_data->plugin_task_timer(event); // Delegate to data_struct + break; + } + + case PLUGIN_GET_CONFIG_VALUE: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config invalid data struct"), event->TaskIndex+1)); + # endif // LIMIT_BUILD_SIZE + return false; + } + + const String cmd = parseString(string, 1); + + if (equals(cmd, F("register"))) { + uint16_t address = parseString(string, 2).toInt(); + uint16_t value = 0; + + if ((address >= P183_CACHE_START) && (address < P183_CACHE_START + P183_CACHE_SIZE)) { + value = P183_data->readRegisterCache(address); + } + else { + # if P183_ALLOW_MODBUS_WAIT + value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + # else + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config command invalid address"), event->TaskIndex+1)); + value = 0; + # endif // if P183_ALLOW_MODBUS_WAIT + } + string = String(value); + success = true; + } + break; + } + + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += strformat(F("Modbus %d
Addr: %d"), P183_LINK_ID, P183_DEV_ID); + success = true; + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus once per second invalid data struct"), event->TaskIndex+1)); + # endif // LIMIT_BUILD_SIZE + return false; + } + + success = P183_data->plugin_once_per_second(event); + break; + } + + } + + return success; +} + +#endif // USES_P183 diff --git a/src/src/CustomBuild/StorageLayout.h b/src/src/CustomBuild/StorageLayout.h index b9958d6d9a..8d7dc87618 100644 --- a/src/src/CustomBuild/StorageLayout.h +++ b/src/src/CustomBuild/StorageLayout.h @@ -63,7 +63,7 @@ #else # define DAT_BASIC_SETTINGS_SIZE 3072 // Current Settings Struct size is ~1.3k, leave some room to extend #endif -#endif +#endif #ifdef ESP32 # define DAT_BASIC_SETTINGS_SIZE 6144 // Current Settings Struct size is ~3k, leave some room to extend #endif @@ -132,9 +132,11 @@ #ifndef DAT_NETWORK_INTERFACE_SIZE # define DAT_NETWORK_INTERFACE_SIZE 1024 -#endif - +#endif +#ifndef DAT_MODBUS_INTERFACE_SIZE +# define DAT_MODBUS_INTERFACE_SIZE 256 // Reserved size for Modbus link settings +#endif /* @@ -169,11 +171,11 @@ # define DAT_OFFSET_CONTROLLER (DAT_OFFSET_TASKS + (DAT_TASKS_DISTANCE * TASKS_MAX)) // each controller = 1k, 3 max, DAT_OFFSET_CDN is at position of any 4th controller. # endif // ifndef DAT_OFFSET_CONTROLLER # ifndef DAT_OFFSET_CUSTOM_CONTROLLER - # define DAT_OFFSET_CUSTOM_CONTROLLER (DAT_OFFSET_CONTROLLER + (DAT_CONTROLLER_SIZE * CONTROLLER_MAX)) // each custom controller config = 1k, 3 max + # define DAT_OFFSET_CUSTOM_CONTROLLER (DAT_OFFSET_CONTROLLER + (DAT_CONTROLLER_SIZE * CONTROLLER_MAX)) // each custom controllerconfig = 1k, 3 max # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN # define DAT_OFFSET_CDN (DAT_OFFSET_CUSTOM_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # endif # ifndef CONFIG_FILE_SIZE # define CONFIG_FILE_SIZE 65536 @@ -191,8 +193,8 @@ # define DAT_OFFSET_CUSTOM_CONTROLLER 32768 // each custom controller config = 1k, 4 max. # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN - # define DAT_OFFSET_CDN (DAT_OFFSET_TASKS - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # define DAT_OFFSET_CDN (DAT_OFFSET_TASKS - DAT_CDN_SIZE) // single CDN settings block of 1k + # endif # ifdef LIMIT_BUILD_SIZE // Limit the config size for 1M builds, since their file system is also quite small @@ -218,21 +220,21 @@ # define DAT_OFFSET_CUSTOM_CONTROLLER 12288 // each custom controller config = 1k, 4 max. # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN - # define DAT_OFFSET_CDN (DAT_OFFSET_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # define DAT_OFFSET_CDN (DAT_OFFSET_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k + # endif # ifndef CONFIG_FILE_SIZE # define CONFIG_FILE_SIZE (DAT_OFFSET_TASKS + ((DAT_TASKS_SIZE + DAT_TASKS_CUSTOM_SIZE) * TASKS_MAX)) # endif // ifndef CONFIG_FILE_SIZE - // On ESP8266 we will not store Network Interface settings - // The only 2 interfaces supported on ESP8266 are: - // - WiFi STA - // - WiFi AP - // Both do not need extra settings for basic functionality - // All other network interface plugins will be ESP32-only. +// On ESP8266 we will not store Network Interface settings +// The only 2 interfaces supported on ESP8266 are: +// - WiFi STA +// - WiFi AP +// Both do not need extra settings for basic functionality +// All other network interface plugins will be ESP32-only. # ifndef DAT_NETWORK_INTERFACES_OFFSET # define DAT_NETWORK_INTERFACES_OFFSET 16384 - # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER + # endif # ifndef DAT_OFFSET_DEV_CREDENTIALS # define DAT_OFFSET_DEV_CREDENTIALS 0 # endif @@ -241,5 +243,18 @@ # endif #endif // if defined(ESP32) + #ifndef DAT_MODBUS_INTERFACE_OFFSET + # if FEATURE_NON_STANDARD_24_TASKS && defined(ESP8266) + # if defined(FEATURE_MODBUS_FAC) && FEATURE_MODBUS_FAC + # error "Not yet defined where to store modbus data, see https: // github.com/letscontrolit/ESPEasy/pull/5390#pullrequestreview-4124760094" + // " + # endif + # else // ifdef FEATURE_NON_STANDARD_24_TASKS + # if defined(FEATURE_MODBUS_FAC) && FEATURE_MODBUS_FAC + # define DAT_MODBUS_INTERFACE_OFFSET DAT_BASIC_SETTINGS_SIZE // Stored in the 1k right after the basic settings, each Modbus link + // config = 256 bytes, 4 max + # endif + # endif // ifdef FEATURE_NON_STANDARD_24_TASKS + #endif // ifndef DAT_MODBUS_INTERFACE_OFFSET #endif // CUSTOMBUILD_STORAGE_LAYOUT_H diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 0ddd3935bd..a06ec0ce95 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1723,6 +1723,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Remove plugins from 'collection' builds which rely on the neopixel library // to make sure those builds will fit again. #ifdef USES_P038 @@ -1963,6 +1966,10 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_ENERGY_COLLECTION @@ -2075,6 +2082,10 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_DISPLAY_A_COLLECTION // Collection of display plugins, set B (AdaGFX_Helper). @@ -2082,6 +2093,9 @@ To create/register a plugin, you have to : #ifndef PLUGIN_DESCR #define PLUGIN_DESCR "Display B" #endif + #ifndef BUILD_NO_DEBUG + #define BUILD_NO_DEBUG + #endif #if !defined(LIMIT_BUILD_SIZE) && (defined(ESP8266) || !(ESP_IDF_VERSION_MAJOR > 3)) #ifndef PLUGIN_BUILD_MAX_ESP32 #define LIMIT_BUILD_SIZE // Reduce buildsize (on ESP8266 / pre-IDF4.x) to fit in all Display plugins @@ -2186,6 +2200,11 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) +// #define USES_P183 // Communication - Modbus RTU + // TODO TD-er: Disabled due to build size + #endif + #endif // ifdef PLUGIN_DISPLAY_B_COLLECTION // Collection of climate A plugins. @@ -2355,6 +2374,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers @@ -2454,6 +2476,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers @@ -2535,9 +2560,14 @@ To create/register a plugin, you have to : #define USES_P165 // Display - NeoPixel (7-Segment) #endif - #ifndef USES_P180 + #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) +// #define USES_P183 // Communication - Modbus RTU +// Disabled for now due to build size + #endif + #endif // ifdef PLUGIN_NEOPIXEL_COLLECTION @@ -3007,6 +3037,9 @@ To create/register a plugin, you have to : #ifndef USES_P180 #define USES_P180 // Generic - I2C Generic #endif + #ifndef USES_P183 + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers #ifndef USES_C015 @@ -3124,6 +3157,14 @@ To create/register a plugin, you have to : #define FEATURE_MODBUS 1 #endif +#if defined(USES_P183) + //P183 uses the Modbus facilities + #ifdef FEATURE_MODBUS_FAC + #undef FEATURE_MODBUS_FAC + #endif + #define FEATURE_MODBUS_FAC 1 +#endif + #if defined(USES_C001) || defined (USES_C002) || defined(USES_P029) #ifndef FEATURE_DOMOTICZ #define FEATURE_DOMOTICZ 1 @@ -3769,12 +3810,8 @@ To create/register a plugin, you have to : #define FEATURE_MODBUS 0 #endif -#ifndef FEATURE_MODBUS_INTERFACES_TAB // TODO Temporary, until P183 finished -#ifdef USES_P183 -#define FEATURE_MODBUS_INTERFACES_TAB 1 -#else -#define FEATURE_MODBUS_INTERFACES_TAB 0 -#endif +#ifndef FEATURE_MODBUS_FAC +#define FEATURE_MODBUS_FAC 0 #endif #ifndef FEATURE_CAN @@ -4489,7 +4526,7 @@ To create/register a plugin, you have to : # endif #endif -#if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE || FEATURE_STORE_NETWORK_INTERFACE_SETTINGS +#if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE || FEATURE_STORE_NETWORK_INTERFACE_SETTINGS || FEATURE_MODBUS_FAC # ifdef FEATURE_ESPEASY_KEY_VALUE_STORE # undef FEATURE_ESPEASY_KEY_VALUE_STORE # endif diff --git a/src/src/DataStructs/TimingStats.cpp b/src/src/DataStructs/TimingStats.cpp index 22e3232786..aab757e591 100644 --- a/src/src/DataStructs/TimingStats.cpp +++ b/src/src/DataStructs/TimingStats.cpp @@ -278,6 +278,9 @@ const __FlashStringHelper* getMiscStatsName_F(TimingStatsElements stat) { #if FEATURE_MQTT case TimingStatsElements::PERIODICAL_MQTT: return F("Periodical MQTT"); #endif +#if FEATURE_MODBUS_FAC + case TimingStatsElements::MODBUS_PROCESS_LINKS: return F("Modbus process links"); +#endif // if FEATURE_MODBUS_FAC case TimingStatsElements::PARSE_TEMPLATE_PADDED: return F("parseTemplate_padded()"); case TimingStatsElements::PARSE_SYSVAR: return F("parseSystemVariables()"); case TimingStatsElements::PARSE_SYSVAR_NOCHANGE: return F("parseSystemVariables() No change"); diff --git a/src/src/DataStructs/TimingStats.h b/src/src/DataStructs/TimingStats.h index 354140a7a5..bc6757ab61 100644 --- a/src/src/DataStructs/TimingStats.h +++ b/src/src/DataStructs/TimingStats.h @@ -120,7 +120,10 @@ enum class TimingStatsElements { C013_RECEIVE_SENSOR_DATA, WEBSERVER_HANDLE_CLIENT, UPDATE_RTTTL, - +#if FEATURE_MODBUS_FAC + MODBUS_PROCESS_LINKS, +#endif // if FEATURE_MODBUS_FAC + // Web serving HANDLE_SERVING_WEBPAGE, HANDLE_SERVING_WEBPAGE_JSON, diff --git a/src/src/DataTypes/SettingsType.cpp b/src/src/DataTypes/SettingsType.cpp index f0d3afb93f..e5730bd3d5 100644 --- a/src/src/DataTypes/SettingsType.cpp +++ b/src/src/DataTypes/SettingsType.cpp @@ -33,7 +33,9 @@ const __FlashStringHelper * SettingsType::getSettingsTypeString(Enum settingsTyp #if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE case Enum::DeviceSpecificCredentials_type: return F("DeviceSpecificCredentials"); #endif - +#if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: return F("ModbusInterface"); +#endif case Enum::SettingsType_MAX: break; } @@ -161,7 +163,18 @@ bool SettingsType::getSettingsParameters(Enum settingsType, int index, int& max_ } break; #endif +#if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: + { + max_index = 4; // up to 4 Modbus interfaces TODO: use poroper define for this. + offset = (DAT_MODBUS_INTERFACE_OFFSET) + (index * (DAT_MODBUS_INTERFACE_SIZE)); + max_size = (DAT_MODBUS_INTERFACE_SIZE); + // struct_size may differ. + struct_size = 0; + } + break; +#endif //FEATURE_MODBUS_FAC case Enum::SettingsType_MAX: { @@ -243,6 +256,10 @@ unsigned int SettingsType::getSVGcolor(Enum settingsType) { case Enum::CdnSettings_Type: return 0xff6600; #endif +# if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: + return 0x0066FF; +# endif case Enum::SettingsType_MAX: break; } @@ -265,7 +282,10 @@ SettingsType::SettingsFileEnum SettingsType::getSettingsFile(Enum settingsType) #if FEATURE_STORE_NETWORK_INTERFACE_SETTINGS case Enum::NetworkInterfaceSettings_Type: #endif +#if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: return SettingsFileEnum::FILE_CONFIG_type; +#endif //FEATURE_MODBUS_FAC case Enum::NotificationSettings_Type: return SettingsFileEnum::FILE_NOTIFICATION_type; case Enum::SecuritySettings_Type: diff --git a/src/src/DataTypes/SettingsType.h b/src/src/DataTypes/SettingsType.h index ea92f610c7..fb1a20a9c5 100644 --- a/src/src/DataTypes/SettingsType.h +++ b/src/src/DataTypes/SettingsType.h @@ -25,7 +25,9 @@ class SettingsType { #if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE DeviceSpecificCredentials_type, #endif - +#if FEATURE_MODBUS_FAC + ModbusInterfaceSettings_Type, +#endif //FEATURE_MODBUS_FAC SettingsType_MAX }; diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp new file mode 100644 index 0000000000..93913d09d3 --- /dev/null +++ b/src/src/Helpers/Modbus_device.cpp @@ -0,0 +1,476 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS device class +// This class implements a single Modbus device connected over a serial link. +// It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. +// It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. +// The Modbus device class will interpret the Modbus messages for the connected hardware and queue it at the link class. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include "../Helpers/Modbus_device.h" +# include "../Helpers/Modbus_mgr.h" + +////# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + +// Modbus function codes, see Modbus specification +const uint8_t MODBUS_READ_COILS = 0x01; +const uint8_t MODBUS_READ_DISCRETE_INPUTS = 0x02; +const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; +const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; +const uint8_t MODBUS_WRITE_SINGLE_COIL = 0x05; +const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; +const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; +const uint8_t MODBUS_READ_WRITE_MULTIPLE_REGISTERS = 0x17; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus device class +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusDEVICE_struct::~ModbusDEVICE_struct() { + + ModbusMGR_singleton.disconnect(_linkId, this); + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reset the Modbus device class to initial state +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::reset() { + + ModbusMGR_singleton.disconnect(_linkId, this); + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initializae the Modbus device and connect it to the given Modbus link +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t taskIndex) +{ + bool success = ModbusMGR_singleton.connect(linkId, this); + + _modbus_address = slaveAddress; + _taskIndex = taskIndex; + _linkId = linkId; + + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus Device: Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d, taskIndex=%d"), + slaveAddress, this, _deviceID, linkId, taskIndex)); + } + # endif // MODBUS_DEBUG + return success; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Checker for device class initialization status +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::isInitialized() const { + return _linkId >= 0; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Set the Modbus timeout value for this device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::setModbusTimeout(uint16_t timeout) { _timeout = timeout; } + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve the Modbus timeout value for this device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusDEVICE_struct::getModbusTimeout() const +{ + return _timeout; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register. The result will be available later through the provided pointers. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, + ModbusResultState*statePtr) +{ + if (!isInitialized()) { + return false; + } + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure + } + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = 0; // Not used for this type of request + transaction->_userData = valuePtr; + transaction->_userState = statePtr; + createReadFrame(*transaction, _modbus_address, address); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register. The result will be available later through a task event. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) +{ + return readModuleHoldingRegister(_modbus_address, address, uid); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modbus holding register from another module on the bus. The result will be available later. +// The function returns true if the request was queued. +// Note: This function accesses registers from other devices on the same Modbus bus. +// This should be used with care to prevent conflicts. This is beyond the intended scope of the Modbus device class. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t uid) +{ + if (!isInitialized()) { + return false; + } + + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure + } + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; + transaction->_userState = nullptr; + createReadFrame(*transaction, busAddress, registerAddress); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading multiple Modubus holding registers. The result will be available later through a task event. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, uint16_t uid) +{ + if (!isInitialized()) { + return false; + } + + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure + } + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; + transaction->_userState = nullptr; + createReadFrame(*transaction, _modbus_address, address, size); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Construct a Modbus read holding registers message +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::createReadFrame(Modbus_Transaction& request, + uint8_t busAddress, + uint16_t registerAddress, + uint16_t registerCount) +{ + request._messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + request._sendframe[0] = busAddress; + request._sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; + request._sendframe[2] = highByte(registerAddress); + request._sendframe[3] = lowByte(registerAddress); + request._sendframe[4] = highByte(registerCount); // Number of registers to read + request._sendframe[5] = lowByte(registerCount); + uint16_t crc = CalculateCRC(request._sendframe, 6); + request._sendframe[6] = lowByte(crc); // CRC low byte + request._sendframe[7] = highByte(crc); // CRC high byte + request._sendframe_length = 8; // Size with CRC + request._rcvframe_length = 5 + (2 * registerCount); // Expected # bytes in response +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start writing a Modbus single register. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, + uint16_t value, + ModbusResultState *statePtr) +{ + if (!isInitialized()) { + return false; + } + + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure + } + transaction->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; + transaction->_userId = 0; // Not used for this type of request + transaction->_userData = nullptr; + transaction->_userState = statePtr; + + + transaction->_sendframe[0] = _modbus_address; + transaction->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; + transaction->_sendframe[2] = highByte(address); + transaction->_sendframe[3] = lowByte(address); + transaction->_sendframe[4] = highByte(value); + transaction->_sendframe[5] = lowByte(value); + uint16_t crc = CalculateCRC(transaction->_sendframe, 6); + transaction->_sendframe[6] = lowByte(crc); // CRC low byte + transaction->_sendframe[7] = highByte(crc); // CRC high byte + transaction->_sendframe_length = 8; // Size with CRC + transaction->_rcvframe_length = 8; // Expect 8 bytes in response + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link + *statePtr = ModbusResultState::Busy; + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Process any pending commands for this device. +// Should only be used when polling for an immediate response, not for regular asynchronous requests. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::processCommand(void) +{ + if (isInitialized()) { + ModbusMGR_singleton.processLinks(); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback function called by the Modbus link when a response is received for a queued request. +// Note that the response might be an invalid response or a timeout +// The queueID identifies the request. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) +{ + ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise + + if (req == nullptr) { + addLogMove(LOG_LEVEL_INFO, F("Modbus Device: ERROR, Null pointer passed in callback")); + return false; + } + + # ifdef MODBUS_DEBUG + String log = strformat(F("Modbus Device: Device callback, device= %d, link= %d, Request= %d, Message= %d"), + _deviceID, + _linkId, + req->_id, + static_cast(req->_messageType) + ); + # endif // MODBUS_DEBUG + + if (req->_state == ModbusQueueState::ERROR_OCCURRED) { + sendEvent(*req, ModbusResultMessageType::Error, 0, 0, 0); + # ifdef MODBUS_DEBUG + log += F(" Link error occurred"); + # endif // MODBUS_DEBUG + } + else { + switch (req->_messageType) + { + case ModbusTransactionType::READ_HOLDING_REGISTERS: + { + int registerCount = req->_sendframe[4] << 8 | req->_sendframe[5]; // Number of registers requested in the original request + + // Validate the response: Check slave address, function code and byte count + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && + (registerCount == req->_rcvframe[2] >> 1)) + { + uint16_t crc = CalculateCRC(req->_rcvframe, req->_rcvframe_length - 2); + + if ((req->_rcvframe[req->_rcvframe_length - 2] == lowByte(crc)) && (req->_rcvframe[req->_rcvframe_length - 1] == highByte(crc))) { + + // Check which return mode is used by the client and return the value accordingly + if (req->_userData != nullptr) { + // Return value through user data pointer specified by the client when queuing the request + // Note: this is only the first register value if multiple registers were read. Returning multiple register values through + // user data pointer is not supported. + *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; + } + else if (registerCount == 1) + { + // Return value through an event with one parameter. + sendEvent(*req, ModbusResultMessageType::SingleValue, (req->_rcvframe[3] << 8) | req->_rcvframe[4]); + } + else + { + // Return value through an event with a ModbusRegisterSet_struct. + + for (int i = 0; i < registerCount && i < 8; i++) { + _registerSet.data[i] = (req->_rcvframe[3 + (i * 2)] << 8) | req->_rcvframe[4 + (i * 2)]; + } + _registerSet.size = registerCount; + sendEvent(*req, ModbusResultMessageType::MultiValue, &_registerSet); + } + resultState = ModbusResultState::Success; + } + else { + # ifdef MODBUS_DEBUG + log += F(" CRC check failed"); + # endif // ifdef MODBUS_DEBUG + } + + } + else { + # ifdef MODBUS_DEBUG + log += F(" Invalid response format"); + # endif // ifdef MODBUS_DEBUG + } + break; + } + + case ModbusTransactionType::WRITE_SINGLE_REGISTER: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_WRITE_SINGLE_REGISTER) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); + + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + resultState = ModbusResultState::Success; + } + } + break; + } + + case ModbusTransactionType::NONE: + { + // Error condition, this transaction type should not be queued + # ifdef MODBUS_DEBUG + log += F(" Invalid transaction type"); + # endif // MODBUS_DEBUG + break; + } + + default: + { + // Error condition, missed a transaction type + # ifdef MODBUS_DEBUG + log += F(" Unknown transaction type"); + # endif // MODBUS_DEBUG + break; + } + } + } + + if (req->_userState != nullptr) { + *(static_cast(req->_userState)) = resultState; + } + # ifdef MODBUS_DEBUG + log += F(", Result = "); + log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); + addLogMove(LOG_LEVEL_INFO, log); + # endif // MODBUS_DEBUG + return resultState == ModbusResultState::Success; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Send a PLUGIN_TASKTIMER_IN event to the task associated with this device. +// This is used by the Modbus link to notify the device of responses received for queued requests. +// This version of the function is used to pass data through parameters in the event. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + int par2, + int par3, + int par4, + int par5, + int par6, + int par7, + int par8) +{ + struct EventStruct TempEvent; + + TempEvent.Par1 = static_cast(messageType); + TempEvent.Par2 = par2; + TempEvent.Par3 = par3; + TempEvent.Par4 = par4; + TempEvent.Par5 = par5; + TempEvent.Par6 = par6; + TempEvent.Par7 = par7; + TempEvent.Par8 = par8; + TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device + TempEvent.idx = req._userId; // Identifier as specified by the client in the request + TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM; + String dummy; + + PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Send a PLUGIN_TASKTIMER_IN event to the task associated with this device. +// This is used by the Modbus link to notify the device of responses received for queued requests. +// This version of the function is used to pass multiple register values in a ModbusRegisterSet_struct. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet) +{ + struct EventStruct TempEvent; + + TempEvent.Par1 = static_cast(messageType); + TempEvent.Data = reinterpret_cast(registerSet); + TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device + TempEvent.idx = req._userId; // Identifier as specified by the client in the request + TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM; + String dummy; + + // Note: The ModbusRegisterSet_struct is passed as a pointer in the Data field of the event to avoid a deep copy. + // This assumes PluginCall will secure that the pointer will remain valid when the event is processed. + // And the data is consumed when the function returns (No multi-threading) + PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Compute the Modbus RTU CRC +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusDEVICE_struct::CalculateCRC(uint8_t *buf, int len) { + uint16_t crc = 0xFFFF; + + for (int pos = 0; pos < len; pos++) { + crc ^= (uint16_t)buf[pos]; // XOR uint8_t into least sig. uint8_t of crc + + for (int i = 8; i != 0; i--) { // Loop over each bit + if ((crc & 0x0001) != 0) { // If the LSB is set + crc >>= 1; // Shift right and XOR 0xA001 + crc ^= 0xA001; + } else { // Else LSB is not set + crc >>= 1; // Just shift right + } + } + } + return crc; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the buffer contents to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus Device Dumping buffer: "); + + for (size_t i = 0; i < length; ++i) { + log += String(buffer[i], HEX); + + if (i < length - 1) { + log += F(", "); + } + } + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // MODBUS_DEBUG +} + +#endif // if FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h new file mode 100644 index 0000000000..5f5d974696 --- /dev/null +++ b/src/src/Helpers/Modbus_device.h @@ -0,0 +1,137 @@ +#ifndef HELPERS_MODBUS_DEVICE_H +#define HELPERS_MODBUS_DEVICE_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include +# include "Modbus_link.h" + +# ifndef MODBUS_BROADCAST_ADDRESS +# define MODBUS_BROADCAST_ADDRESS 0xFE // Address used for boardcast messages +# endif // ifndef MODBUS_BROADCAST_ADDRESS + +# define MODBUS_MAX_REGISTERS_PER_TRANSACTION 125 // Modbus protocol allows up to 125 registers in a single read/write transaction + +// States for the Modbus queue elements +enum class ModbusResultState { + Busy = 0, // Transaction is not completed + Success = 1, // Transaction successfully completed + Error = 2, // Transaction completed with an error + +}; + +// Type of callback message sent to the device when a response is received for a queued request +enum class ModbusResultMessageType { + Error = 0, // Request completed with an error, no data available + SingleValue = 1, // Transaction returned a value as a parameter in the callback + MultiValue = 2, // Transaction returned multiple values in a structure + +}; + +struct ModbusRegisterSet_struct +{ + uint16_t data[MODBUS_MAX_REGISTERS_PER_TRANSACTION]; // Modbus allows up to 125 registers in a single read/write transaction + uint16_t size; // Number of valid registers in the data array + +}; + +// ModbusDEVICE structure representing a MODBUS Device +// This is a single device that may share it's Modbus link with multiple other devices. +// It uses the ModbusLINKManager to find the ModbusLINK object that handles the data transport. +// It is the ModbusDEVICE that builds the Modbus request frames and parses the responses. +struct ModbusDEVICE_struct { + +public: + + ModbusDEVICE_struct() = default; + + ~ModbusDEVICE_struct(); + + void reset(); + + bool init(uint8_t slaveAddress, + int linkId, + taskIndex_t taskIndex); + + bool isInitialized() const; + + void setModbusTimeout(uint16_t timeout); + + uint16_t getModbusTimeout() const; + + bool linkCallback(Modbus_Transaction *transaction); + + // Start reading a Modubus holding register. The result will be available later. + // The function returns true if the request was queued. + // The state variable will signal the processing state of the request. + bool readHoldingRegister(uint16_t address, + uint16_t *valueptr, + ModbusResultState*stateptr); + + // Start reading a Modbus holding register with reslt returned through event PLUGIN_TASKTIMER_IN + // The function returns true if the request was queued. + // Use uid to identify the request. This value will be passed back in the PLUGIN_TASKTIMER_IN event + bool readHoldingRegister(uint16_t address, + uint16_t uid); + + bool readHoldingRegisters(uint16_t address, + uint16_t size, + uint16_t uid); + + // Start writing a single Modbus register. + // The function returns true if the request was queued. + bool writeSingleRegister(uint16_t address, + uint16_t value, + ModbusResultState *stateptr); + + // Start reading a Modbus holding register from another module. The result will be available later. + // The function returns true if the request was queued. + // Note: This function accesses registers from other devices on the same Modbus bus. This is beyond the intended scope of the Modbus + // device class. + bool readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t uid); + + void processCommand(void); + +private: + + uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + uint8_t _linkId = 0xFF; // Link index used to identify the Modbus link + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device + ModbusRegisterSet_struct _registerSet = {}; // Structure to hold multiple register values for multi-value responses + + void sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + int par2 = 0, + int par3 = 0, + int par4 = 0, + int par5 = 0, + int par6 = 0, + int par7 = 0, + int par8 = 0); + + void sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet); + + void createReadFrame(Modbus_Transaction& request, + uint8_t busAddress, + uint16_t registerAddress, + uint16_t registerCount = 1); + + static uint16_t CalculateCRC(uint8_t *buf, + int len); + + static void dump_buffer(const uint8_t *buffer, + size_t length); + +}; + +#endif // FEAURE_MODBUS_FAC +#endif // HELPERS_MODBUS_DEVICE_H diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp new file mode 100644 index 0000000000..06ce3ac360 --- /dev/null +++ b/src/src/Helpers/Modbus_link.cpp @@ -0,0 +1,491 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS link class +// This class implements a Modbus link over a serial connection. It is part of the Modbus facilities supporting multiple Modbus +// devices on multiple serial Modbus links. +// It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. +// It exepcts a Modbus device instance to construct and interpret the Modbus messages for the specific device. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include "../Helpers/Modbus_device.h" +# include "../Helpers/Modbus_link.h" + +////# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus link class +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusLINK_struct::~ModbusLINK_struct() { + reset(); + + if (_easySerial != nullptr) { + delete _easySerial; + _easySerial = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reset the ModbusLINK class to initial state +// This aborts all pending transactions and frees the associated resources. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::reset() { + for (auto it = _transactionQueue.begin(); it != _transactionQueue.end(); it++) { + (*it)._state = ModbusQueueState::ERROR_OCCURRED; + + if ((*it)._device != nullptr) { + (*it)._device->linkCallback(&(*it)); // Notify the device that the request finished with an error + } + + ////delete (*it); // destroy the queue element + it = _transactionQueue.erase(it); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialize the link with the given serial port and parameters, including a dere pin for RS485 +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::init(const ESPEasySerialPort port, + const int8_t serial_rx, + const int8_t serial_tx, + uint32_t baudrate, + int8_t dere_pin, + bool collision_detect) { + int available = 0; + + _initialized = false; // In case of reinitialization the flag might already been set + + // (re)create the serial port object + // If the serial port object already exists, delete it first. + if (_easySerial != nullptr) { + delete _easySerial; + _easySerial = nullptr; + } + + if (port == ESPEasySerialPort::not_set) { + return true; // No port configured, report this as a successful initialization of a not connected link + // Note that the link object exists, but is flagged as not initialized and has no serial port object + } + else { + _easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); + } + + if (_easySerial == nullptr) { + return false; + } + + // Set RS485 mode if requested using selected pin for RTS + const bool rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); + _easySerial->begin(baudrate); + _easySerial->flush(); + available = _easySerial->available(); + + if (available > 0) { + // Clear any pending input + for (int i = available; i > 0; --i) { + _easySerial->read(); + } + } + _dere_pin = dere_pin; + _collision_detect = collision_detect; + + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = + strformat(F( + "Modbus Link: Init link=%s, RX pin %d, TX pin %d, RS485 pin %d, baudrate %d, collision detection %s, RS485 mode %s"), + ESPEasySerialPort_toString(port), + serial_rx, + serial_tx, + dere_pin, + baudrate, + collision_detect ? F("enabled") : F("disabled"), + rs485Mode ? F("enabled") : F("disabled") + ); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // MODBUS_DEBUG + + _initialized = true; + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Free all queued transactions for the given device +// This shall be called when a device is removed to free the pending requests for the device preventing callback issues. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::freeTransactions(const ModbusDEVICE_struct *device) +{ + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to free transactions on uninitialized link")); + return; + } + + for ( auto it = _transactionQueue.begin(); it != _transactionQueue.end(); ++it ) { + if ((*it)._device == device) { + (*it)._state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed + } + } + processQueue(); // Trigger processing of the command queue to free the marked transactions +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Create a new transaction. The transaction is added to the queue +// The transaction is not queued for processing yet. +// After filling the details of the transaction, the client shall call queueTransaction() to queue it for processing. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +Modbus_transaction_ptr ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device) +{ + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to create transaction for uninitialized link %d"), this->_linkId)); + return nullptr; + } + else { + auto& tr = _transactionQueue.emplace_back(device); // Put a new transaction at the end of the queue + tr._timeout = _modbus_timeout; // Default timeout value + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus Link: new transaction link=%d, transaction ID %u, device %p"), this->_linkId, tr._id, device)); + # endif // MODBUS_DEBUG + return &tr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Queue a Modbus transaction. The request is appended to the queue and assigned a unique identifier. +// The client can use this identifier to retrieve the response later. +// This function will transfer ownership of the transaction to the Modbus link, which is responsible for freeing +// the associated resources when the transaction is completed. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { + bool success = false; + + if (transaction == nullptr) { + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to queue null transaction for link %d"), this->_linkId)); + } else if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to queue transaction on uninitialized link %d"), this->_linkId)); + transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + } + else if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: receive buffer too large for link %d"), this->_linkId)); + transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + } + else { + transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued + success = true; + } + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && (transaction != nullptr)) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link: link=%d, Queueing transaction ID %u, state %s"), this->_linkId, transaction->_id, + toString(transaction->_state))); + } + # endif // MODBUS_DEBUG + + processQueue(); // Trigger processing of the command queue + return success; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Evaluate the next action to take to process the queue +// This function shall be called periodically to keep the Modbus link active +// The queue is ordered by the order of queuing, and max one transaction is processed at a time. +// _activeTransaction points to the transaction currently being processed, or nullptr if no transaction is active. +// If there is an active transaction it is handled first. +// If the link is not busy with an active transaction the queue is examined +// Elements are examined in order until either +// - an element is found that can be sent, which is then sent and set as active +// - an element is found that is marked for destruction, which is then removed from the queue +// - the queue is empty and nothing is done +// +// Note that a timeout for a transaction assumes no late response message will be received. +// If a late response is received, it most likely interferes with the next transaction queued. That transaction most likely fails. +// A rough mitigation is to wait for a full timeout if the next transaction also fails +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::processQueue() +{ + if (_processing || !isInitialized() || (_transactionQueue.empty())) { + return; // Serial port not initialized or queue is empty, nothing to process + } + _processing = true; // Set processing flag to prevent reentrancy issues + + // Check if there is an active transaction and check if it can be completed + if (_activeTransaction != nullptr) { + + switch (_activeTransaction->_state) + { + case ModbusQueueState::MESSAGE_SENT: + { + + bool responseOK = true; // Assume response message is OK unless proven otherwise + bool linkBusy = false; // Assume we are done unless proven otherwise + int available = _easySerial->available(); // Cache the number of bytes available in the serial buffer + + // Waiting for response + if (available >= (_activeTransaction->_rcvframe_length)) { + _easySerial->readBytes(_activeTransaction->_rcvframe, _activeTransaction->_rcvframe_length); + _activeTransaction->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + } + else if (timePassedSince(_activeTransaction->_startTime) > _activeTransaction->_timeout) { + // Timeout expired + _activeTransaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark transaction as error + _prev_transact_failed = true; // Mark link that the previous transaction failed + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link ERROR: link=%d, transaction ID= %u, available= %d, expected= %d"), this->_linkId, + _activeTransaction->_id, available, _activeTransaction->_rcvframe_length)); + # endif // MODBUS_DEBUG + } + else { + // Still waiting + linkBusy = true; // Only process one request at a time + } + + if (!linkBusy) { // We received a response or an error occurred, + + // process the result + if (_activeTransaction->_device != nullptr) { + responseOK = _activeTransaction->_device->linkCallback(_activeTransaction); // Notify the device that a response was + // received + } + else { + addLogMove(LOG_LEVEL_ERROR, F("Modbus link: ERROR, Null pointer for device in active transaction")); + } + + if (_prev_transact_failed && !responseOK) { + _activeTransaction->_state = ModbusQueueState::RECOVERING; // Wait till timeout to recover from the error + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link RECOVERING: link=%d, transaction ID= %u"), this->_linkId, _activeTransaction->_id)); + # endif // MODBUS_DEBUG + } + else { + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link FINISHED: link=%d, transaction ID= %u"), this->_linkId, + _activeTransaction->_id)); + # endif // MODBUS_DEBUG + } + } + } + + // Waiting for the timeout to recover from potential link errors. + case ModbusQueueState::RECOVERING: + { + if (timePassedSince(_activeTransaction->_startTime) > _activeTransaction->_timeout) { + // Timeout expired + _prev_transact_failed = false; // Clear the previous transaction failed flag + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link RECOVERY FINISHED: link=%d, transaction ID= %u"), this->_linkId, + _activeTransaction->_id)); + # endif // MODBUS_DEBUG + } + break; + } + + // Transaction should already be marked for destruction and activeTransaction should have been cleared + // The activeTransaction can be set to READY_FOR_DESTROY if the device discarded it + case ModbusQueueState::READY_FOR_DESTROY: + case ModbusQueueState::ERROR_OCCURRED: + case ModbusQueueState::RESPONSE_RECEIVED: + { + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + break; + } + + // This is an error condition. activeTransaction should not point to a transaction in this state. + // Set the activeTransaction to nullptr to allow processing of the next transaction in the queue. + case ModbusQueueState::NOT_QUEUED: + case ModbusQueueState::QUEUED: + { + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + } + } + } + + // If there is no active transaction, check the queue for the next transaction to process + if ((_activeTransaction == nullptr) || (_activeTransaction->_state == ModbusQueueState::READY_FOR_DESTROY)) { + auto it = _transactionQueue.begin(); // Iterator for the request queue + + // Main loop to process the queue. + // Note that the iterator is explicitly incremented in the loop, or implicitly when a transaction is removed from the queue. + while (it != _transactionQueue.end()) { + + switch (it->_state) + { + // Transaction is created but not yet queued. Skip it untill it is queued or discarded. + case ModbusQueueState::NOT_QUEUED: + { + it++; // Not queued yet, move to the next transaction in the queue + break; + } + + // Transaction in queue ready to be transmitted. + // If there is no active transaction yet, send the request and mark it as active. + case ModbusQueueState::QUEUED: + { + if (_activeTransaction == nullptr) { // Pick only the first rtansaction from the queue to process + + // Clear the serial input buffer before sending the request + int available = _easySerial->available(); + + if (available > 0) { + // Clear any pending input + for (int i = available; i > 0; --i) { + _easySerial->read(); + } + } + _easySerial->write(it->_sendframe, it->_sendframe_length); + it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + it->_startTime = millis(); // Record the time the transaction + _activeTransaction = &(*it); // Mark this transaction as active + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link: PICKING link=%d, transaction ID=%u, state=%s"), this->_linkId, it->_id, + toString(it->_state))); + # endif // MODBUS_DEBUG + } + it++; // Move to the next transaction in the queue + break; + } + + // Transaction is sent, waiting for response. Handled above in the active Transaction processing section. + case ModbusQueueState::MESSAGE_SENT: + // Transaction failed in suspicious conditions, Handled above in the active Transaction processing section. + case ModbusQueueState::RECOVERING: + { + it++; // Move to the next transaction in the queue + break; + } + + // All remaining states indicate that the transaction can be removed from the queue and destroyed. + case ModbusQueueState::READY_FOR_DESTROY: + case ModbusQueueState::ERROR_OCCURRED: // Not exepected + case ModbusQueueState::RESPONSE_RECEIVED: // Not expected + { + if (_activeTransaction == &(*it)) { + _activeTransaction = nullptr; // Clear the active transaction if it is being removed from the queue + } + it->_state = ModbusQueueState::READY_FOR_DESTROY; // For safety + it = _transactionQueue.erase(it); // Remove it from the list + break; + } + } // switch + } // next iterarion + } // check for inactive link conditions + _processing = false; // Clear processing flag to allow new processing cycles + return; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int16_t ModbusLINK_struct::getBaudrate() const +{ + return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int16_t ModbusLINK_struct::getSerialRX() const +{ + return _easySerial != nullptr ? _easySerial->getRxPin() : -1; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int16_t ModbusLINK_struct::getSerialTX() const +{ + return _easySerial != nullptr ? _easySerial->getTxPin() : -1; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int8_t ModbusLINK_struct::getDerePin() const +{ + return _dere_pin; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::getCollisionDetect() const +{ + return _collision_detect; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element state to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Modbus Link: State= "), toString(state))); + } + # endif // MODBUS_DEBUG +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +const __FlashStringHelper* toString(ModbusQueueState_t state) { + # ifdef MODBUS_DEBUG + + switch (state) + { + case ModbusQueueState::NOT_QUEUED: + return F("NOT_QUEUED"); + case ModbusQueueState::QUEUED: + return F("QUEUED"); + case ModbusQueueState::MESSAGE_SENT: + return F("MESSAGE_SENT"); + case ModbusQueueState::RESPONSE_RECEIVED: + return F("RESPONSE_RECEIVED"); + case ModbusQueueState::ERROR_OCCURRED: + return F("ERROR_OCCURRED"); + case ModbusQueueState::READY_FOR_DESTROY: + return F("READY_FOR_DESTROY"); + case ModbusQueueState::RECOVERING: + return F("RECOVERING"); + } + # endif // MODBUS_DEBUG + return F(""); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element contents to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void Modbus_Transaction::print() +{ + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = strformat(F("Modbus Transaction %p: [ID=%u, Device=%p, State="), this, _id, _device); + log += toString(_state); + log += F(", TX=("); + + for (int i = 0; i < _sendframe_length; i++) { + log += String(_sendframe[i], HEX); + + if (i < _sendframe_length - 1) { + log += F(","); + } + } + log += F("), RX=("); + + for (int i = 0; i < _rcvframe_length; i++) { + log += String(_rcvframe[i], HEX); + + if (i < _rcvframe_length - 1) { + log += F(","); + } + } + log += F(")] "); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // MODBUS_DEBUG +} + +#endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h new file mode 100644 index 0000000000..d05560a954 --- /dev/null +++ b/src/src/Helpers/Modbus_link.h @@ -0,0 +1,133 @@ +#ifndef HELPERS_MODBUS_LINK_H +#define HELPERS_MODBUS_LINK_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include "../../_Plugin_Helper.h" +# include + +# define MODBUS_XMIT_BUFFER 12 +# define MODBUS_RCV_BUFFER 256 + +// Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues +struct ModbusDEVICE_struct; + +// States for the Modbus queue elements +typedef enum class ModbusQueueState { + NOT_QUEUED = 0, // Initial state, element is created but not yet queued + QUEUED = 1, // Element is queued and waiting to be processed + MESSAGE_SENT = 2, // Request message has been sent, waiting for response + RESPONSE_RECEIVED = 3, // Response has been received and is being processed + ERROR_OCCURRED = 4, // An error occurred during processing (e.g., timeout, invalid response) + READY_FOR_DESTROY = 5, // Element is marked for deletion and can be freed + RECOVERING = 6 // The transaction is being recovered from a link error + +} ModbusQueueState_t; + +const __FlashStringHelper* toString(ModbusQueueState_t state); + +// Types of Modbus transactions supported by the Modbuss_device +// This enumeration is used by the Modbus device to indicate which transaction is associated with the queue element. +// See Modbus specification for details on function codes. +typedef enum class ModbusTransactionType { + NONE = 0, // Undefined/unknown transaction type + READ_HOLDING_REGISTERS = 1, // Read holding registers (function code 0x03) + WRITE_SINGLE_REGISTER = 2 // Write single register (function code 0x06) + +} ModbusTransactionType_t; + +// Modbus transaction structure +// This structure represents a single Modbus request and its associated response. +struct Modbus_Transaction { + Modbus_Transaction() = default; + Modbus_Transaction(ModbusDEVICE_struct *device) + { + _device = device; + _id = identifier++; // Assign a unique ID to the transaction + } + + void print(); // Print the transaction details for debugging + + ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message + void *_userData = nullptr; // Pointer to user (device) data + void *_userState = nullptr; // Pointer to user (device) defined state + uint16_t _userId = 0; // Client defined identifier for this transaction, + // can be used to match responses to requests + uint16_t _id = 0; // Unique ID of the request (for debugging) + struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the + // action + uint16_t _sendframe_length = 0; // Length of the request frame + uint16_t _rcvframe_length = 0; // Expected length of the response frame + enum ModbusQueueState _state = ModbusQueueState::NOT_QUEUED; // State of the request exchange + uint16_t _timeout = 0; // Specified timeout value for the request + unsigned long _startTime = 0; // Time the request was issued + uint8_t _sendframe[MODBUS_XMIT_BUFFER] = { 0 }; // Reqest frame to send + uint8_t _rcvframe[MODBUS_RCV_BUFFER] = { 0 }; // Response frame received + + static inline uint16_t identifier = 0; // Static variable to generate unique identifiers for each transaction +}; + +typedef Modbus_Transaction *Modbus_transaction_ptr; + +// Queue of Modbus request elements +typedef std::list Modbus_TransactionQueue; + + +// ModbusLINK structure representing a MODBUS LINK +// This is a single serial link that can have multiple Modbus devices conected to it. +// It is used by the ModbusLINKManager to manage multiple links. +// Each ModbusLINK can have multiple ModbusDEVICE_struct instances representing the devices on the link. +// The ModbusLINK structure maintains a queue of Modbus requests and associated responses. +struct ModbusLINK_struct { + ModbusLINK_struct() = default; + ModbusLINK_struct(int8_t linkId) : _linkId(linkId) {} + + ~ModbusLINK_struct(); + + void reset(); + + bool init(const ESPEasySerialPort port, + const int8_t serial_rx, + const int8_t serial_tx, + uint32_t baudrate, + int8_t dere_pin, + bool collision_detect = false); + + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } + + void freeTransactions(const ModbusDEVICE_struct *device); + Modbus_transaction_ptr newTransaction(ModbusDEVICE_struct *device); + bool queueTransaction(Modbus_Transaction *transaction); + void processQueue(); + + int16_t getBaudrate() const; + int16_t getSerialRX() const; + int16_t getSerialTX() const; + int8_t getDerePin() const; + bool getCollisionDetect() const; + +private: + + static void dumpState(ModbusQueueState_t state); + + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus transactions to process + Modbus_transaction_ptr _activeTransaction = nullptr; // Pointer to the currently active transaction being processed + + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + int8_t _linkId = -1; // Link ID assigned by the Modbus manager + int8_t _dere_pin = -1; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled + bool _initialized = false; + bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to + // prevent reentrancy issues + bool _prev_transact_failed = false; // Flag to indicate if the previous transaction failed, used to trigger recovery + // actions + +}; + + +#endif // FEATURE_MODBUS +#endif // HELPERS_MODBUS_LINK_H diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp new file mode 100644 index 0000000000..1b59f3ba3d --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -0,0 +1,542 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS manager class +// This class implements a singleton administration object for Modbus devices & links. +// It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. +// It associates Modbus devices with Modbus links and manages their lifecycle. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include +# include "../Helpers/Modbus_mgr.h" + +////# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + + +// Macro to check if a linkId is valid and if the link is initialized +# define MODBUS_MGR_VALID_LINK_ID(id) (validLinkId(id) && (_modbus_links[id].link != nullptr)) + +# define MODBUS_MAX_BAUDRATE_SEL 8 + +// Key indices for storing Modbus link settings in the key-value store +# define MODBUS_PORT_KEY_INDEX 1 +# define MODBUS_RX_KEY_INDEX 2 +# define MODBUS_TX_KEY_INDEX 3 +# define MODBUS_BAUDRATE_KEY_INDEX 4 +# define MODBUS_DERE_PIN_KEY_INDEX 5 +# define MODBUS_COLLISION_DETECT_KEY_INDEX 6 + + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Singleton instance of the Modbus manager +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct_t ModbusMGR_singleton = {}; + +uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting); +uint8_t modbus_baudrateToStorageValue(uint32_t baudrate); + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Constructor of the Modbus manager class, initializes the internal data structures +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct::ModbusMGR_struct() +{} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus manager class, should not be called as this is intended to be a singleton +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct::~ModbusMGR_struct() +{ + // This class is a singleton, so destructor should not be called. + // However, in case it is called, we clean up the allocated resources. + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + + if (_modbus_links[i].link != nullptr) { + delete _modbus_links[i].link; + _modbus_links[i].link = nullptr; + } + + if (_modbus_links[i].kvs != nullptr) { + delete _modbus_links[i].kvs; + _modbus_links[i].kvs = nullptr; + } + } + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialize the Modbus manager link administration. +// This will read the persisted data for each link and create the link objects for the links that are configured. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::initialize() +{ + if (!_initialized) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + + int8_t val; + _modbus_links[i].link = nullptr; // Initialize link pointer to a safe value + // Create a key-value store for the link and load the persisted settings + _modbus_links[i].kvs = new (std::nothrow) ESPEasy_key_value_store; + + if (_modbus_links[i].kvs != nullptr) { + _modbus_links[i].kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); + _modbus_links[i].kvs->getValue(MODBUS_PORT_KEY_INDEX, val); + _modbus_links[i].port = static_cast(val); + _modbus_links[i].kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i].serial_rx); + _modbus_links[i].kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i].serial_tx); + _modbus_links[i].kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i].baudrate); + _modbus_links[i].kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i].dere_pin); + _modbus_links[i].kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i].collision_detect); + } + + // Create the Modbus link object if a port is configured for the link + if (_modbus_links[i].port != ESPEasySerialPort::not_set) { + _modbus_links[i].link = new (std::nothrow) ModbusLINK_struct(i); + + if (_modbus_links[i].link != nullptr) { + _modbus_links[i].link->init(_modbus_links[i].port, + _modbus_links[i].serial_rx, + _modbus_links[i].serial_tx, + _modbus_links[i].baudrate, + _modbus_links[i].dere_pin, + _modbus_links[i].collision_detect); + } + } + + } + _initialized = true; + } + # ifdef MODBUS_DEBUG + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Connect a Modbus device to a Modbus link. A unique device ID is assigned to the device. +// Returns a pointer to the Modbus link object and the assigned device ID if connection is successful, otherwise returns false. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::connect(int linkId, const ModbusDEVICE_struct *device) +{ + bool returnValue = false; + + # ifdef MODBUS_DEBUG + String log = strformat(F("Modbus Manager: Connect device= %p to linkId=%d"), device, linkId); + # endif // ifdef MODBUS_DEBUG + + initialize(); // TODO Initialization sequence to be refactored. + + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { + # ifdef MODBUS_DEBUG + log += F(" Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else if (_modbus_links[linkId].port == ESPEasySerialPort::not_set) { + # ifdef MODBUS_DEBUG + log += F(" No link available at linkIndex= "); + log += linkId; + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else { + // No administration to be done on manager or link side + returnValue = true; + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + + return returnValue; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Disconnect the Modbus device with the given device ID. +// If no other devices are using the same link, the link is also deleted. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::disconnect(int linkId, const ModbusDEVICE_struct*device) { + + bool returnValue = false; + + # ifdef MODBUS_DEBUG + dumpAdminInfo(); + String log = strformat(F("Modbus Manager: Disconnect device= %p from linkId= %d "), device, linkId); + # endif // ifdef MODBUS_DEBUG + + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else { + _modbus_links[linkId].link->freeTransactions(device); + returnValue = true; + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return returnValue; +} + +Modbus_transaction_ptr ModbusMGR_struct::newTransaction(int linkId, ModbusDEVICE_struct *device) +{ + Modbus_transaction_ptr transaction = nullptr; + + # ifdef MODBUS_DEBUG + String log = strformat(F("Modbus Manager: New transaction for linkId= %d, device= %p"), linkId, device); + # endif // ifdef MODBUS_DEBUG + + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + # endif // ifdef MODBUS_DEBUG + } + else { + transaction = _modbus_links[linkId].link->newTransaction(device); + # ifdef MODBUS_DEBUG + log += strformat(F(" Transaction pointer= %p"), transaction); + transaction->print(); // Print the transaction details for debugging + # endif // ifdef MODBUS_DEBUG + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return transaction; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::queueTransaction(int linkId, Modbus_Transaction *transaction) +{ + bool returnValue = false; + + # ifdef MODBUS_DEBUG + String log = concat(F("Modbus Manager: Queue transaction for linkId= "), linkId); + # endif // ifdef MODBUS_DEBUG + + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { + # ifdef MODBUS_DEBUG + log += F(" Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else { + returnValue = _modbus_links[linkId].link->queueTransaction(transaction); + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return returnValue; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// This function should be called periodically to allow the Modbus manager to process the Modbus links +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::processLinks() +{ + if (isInitialized()) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if ((_modbus_links[i].link != nullptr)) { + _modbus_links[i].link->processQueue(); // Trigger processing of the command queue on the link + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Dump the Modbus manager admin information to the log for debugging purposes +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::dumpAdminInfo() +{ + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, F("Modbus Manager: Dumping admin info")); + + // Iterate over the modbus links and dump their info + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus Admin: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), + i, + ESPEasySerialPort_toString(_modbus_links[i].port), + _modbus_links[i].serial_rx, + _modbus_links[i].serial_tx, + _modbus_links[i].baudrate, + _modbus_links[i].dere_pin, + FsP(_modbus_links[i].rs485_mode ? F("Yes") : F("No")), + FsP(_modbus_links[i].collision_detect ? F("Yes") : F("No")) + )); + } + + # endif // ifdef MODBUS_DEBUG +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Put the Modbus link configuration on the web page +// This is called from the interfaces configuration page to show the Modbus link configuration. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::show_modbus_interfaces() +{ + String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]{}; // Array to hold the baudrate options for the selector + + constexpr int optionBaudCount = static_cast(NR_ELEMENTS(options_baudrate)); + + for (int i = 0; i < optionBaudCount; ++i) { + options_baudrate[i] = modbus_storageValueToBaudrate(i); // Fill the baudrate options with actual baudrate values + } + const FormSelectorOptions baudselector(optionBaudCount, + options_baudrate); + + // Iterate over the modbus links and show their configuration on the web page + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) + { + + addFormSubHeader(concat(F("Modbus "), link)); + addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); + + const String slid = concat(F("MBport"), link); + const String txid = concat(F("MBtx"), link); + const String rxid = concat(F("MBrx"), link); + + serialHelper_webformLoad(_modbus_links[link].port, + _modbus_links[link].serial_rx, + _modbus_links[link].serial_tx, + INCLUDE_HW_SERIAL + INCLUDE_SW_SERIAL + INCLUDE_I2C_SERIAL, + F("Serial Port"), + slid, + txid, + rxid, + concat(F("i2cad"), link), + concat(F("i2cch"), link)); + + addFormPinSelect(PinSelectPurpose::Serial_input, + formatGpioName_serialTX(false), + txid, + _modbus_links[link].serial_tx); + + addFormPinSelect(PinSelectPurpose::Serial_output, + formatGpioName_serialRX(false), + rxid, + _modbus_links[link].serial_rx); + + html_add_script(strformat(F("elId('%s').onchange();"), slid.c_str()), false); + + addFormPinSelect(PinSelectPurpose::Generic_output, + formatGpioName_output_optional(F("~RE/DE")), + concat(F("MBde"), link), + _modbus_links[link].dere_pin); + + baudselector.addFormSelector(F("Baud Rate"), + concat(F("MBbaud"), link), + modbus_baudrateToStorageValue(_modbus_links[link].baudrate)); + addUnit(F("baud")); + + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), concat(F("MBcoll"), link), _modbus_links[link].collision_detect); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); + # endif // ifdef ESP32 + + addFormDetailsEnd(); + + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Convert stored baudrate setting (enumeration value) to actual baudrate value +// Returns the actual baudrate value. +// The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. +// Values outside this range will be mapped to the closest valid value. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting) { + if (baudrate_setting > 7) { return 9600; } + + switch (baudrate_setting) + { + case 0: return 1200; + case 1: return 2400; + case 2: return 4800; + case 3: return 9600; + case 4: return 19200; + case 5: return 38400; + case 6: return 57600; + case 7: return 115200; + default: + return 9600; // Default to a safe value if the setting is invalid + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Convert actual baudrate value to stored baudrate setting (enumeration value) +// Returns the stored baudrate setting. +// The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. +// Values outside this range will be mapped to the closest valid value. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint8_t modbus_baudrateToStorageValue(uint32_t baudrate) { + if (baudrate <= 1200) { return 0; } + + if (baudrate <= 2400) { return 1; } + + if (baudrate <= 4800) { return 2; } + + if (baudrate <= 9600) { return 3; } + + if (baudrate <= 19200) { return 4; } + + if (baudrate <= 38400) { return 5; } + + if (baudrate <= 57600) { return 6; } + return 7; // 115200 +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// process the Modbus link configuration from the web page save action and update the Modbus manager configuration +// This is called from the interfaces configuration page to show the Modbus link configuration. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::save_modbus_interfaces(String& error) +{ + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { + int baudrate_setting; + int port_setting = static_cast(_modbus_links[link].port); + int tx_setting = _modbus_links[link].serial_tx; + int rx_setting = _modbus_links[link].serial_rx; + int dere_setting = _modbus_links[link].dere_pin; + bool collision_detect_setting = _modbus_links[link].collision_detect; + bool settingsChanged = false; + + update_whenset_FormItemInt(concat(F("MBport"), link), port_setting, &settingsChanged); + baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); + update_whenset_FormItemInt(concat(F("MBbaud"), link), baudrate_setting, &settingsChanged); + tx_setting = _modbus_links[link].serial_tx; + rx_setting = _modbus_links[link].serial_rx; + + if (port_setting == static_cast(ESPEasySerialPort::sc16is752)) { + update_whenset_FormItemInt(concat(F("i2cad"), link), rx_setting, &settingsChanged); + update_whenset_FormItemInt(concat(F("i2cch"), link), tx_setting, &settingsChanged); + } + else { + update_whenset_FormItemInt(concat(F("MBtx"), link), tx_setting, &settingsChanged); + update_whenset_FormItemInt(concat(F("MBrx"), link), rx_setting, &settingsChanged); + } + dere_setting = _modbus_links[link].dere_pin; + + if (update_whenset_FormItemInt(concat(F("MBde"), link), dere_setting, &settingsChanged)) + { + # ifdef ESP32 + + // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. + // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. + collision_detect_setting = isFormItemChecked(concat(F("MBcoll"), link)); + settingsChanged |= (collision_detect_setting != _modbus_links[link].collision_detect); + # endif // ifdef ESP32 + } + + if (settingsChanged) { + setLink(link, + static_cast(port_setting), + rx_setting, + tx_setting, + modbus_storageValueToBaudrate(baudrate_setting), + dere_setting, + collision_detect_setting); + } + } + return true; // Success +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Setup the Modbus link with the specified parameters. Settings will be persisted on disk. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::setLink(const int linkIndex, + const ESPEasySerialPort port, + const int8_t serial_rx, + const int8_t serial_tx, + uint16_t baudrate, + int8_t dere_pin, + bool collision_detect) +{ + # ifdef MODBUS_DEBUG + String log = F("Modbus_mgr: SetLink. "); + + addLogMove(LOG_LEVEL_INFO, + strformat(F("setlink: LinkID=%d, Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, CollisionDetect=%s"), + linkIndex, + ESPEasySerialPort_toString(port), + serial_rx, + serial_tx, + baudrate, + dere_pin, + FsP(collision_detect ? F("Yes") : F("No")) + )); + # endif // ifdef MODBUS_DEBUG + + if (!validLinkId(linkIndex)) { + # ifdef MODBUS_DEBUG + log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return false; // Invalid link index + } + + // Check of the link is being disabled (port set to not_set) or enabled (port set to a valid port) + if (_modbus_links[linkIndex].port == ESPEasySerialPort::not_set) { + if ((port != ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link == nullptr)) { + _modbus_links[linkIndex].link = new (std::nothrow) ModbusLINK_struct(linkIndex); + } + } + else if ((port == ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link != nullptr)) { + delete _modbus_links[linkIndex].link; + _modbus_links[linkIndex].link = nullptr; + } + + // If the link object is enabled (has a valid pointer), initialize it with the new parameters + if (_modbus_links[linkIndex].link != nullptr) { + if (!_modbus_links[linkIndex].link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + # ifdef MODBUS_DEBUG + log += strformat(F("Link initialization failed")); + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return false; // Initialization failed + } + } + + // Store the updated link parameters in the Modbus manager's internal data structure + _modbus_links[linkIndex].port = port; + _modbus_links[linkIndex].serial_rx = serial_rx; + _modbus_links[linkIndex].serial_tx = serial_tx; + _modbus_links[linkIndex].baudrate = baudrate; + _modbus_links[linkIndex].dere_pin = dere_pin; + _modbus_links[linkIndex].rs485_mode = (dere_pin != -1); + _modbus_links[linkIndex].collision_detect = collision_detect; + + // Store the link configuration parameters in the key-value store for persistence + if (_modbus_links[linkIndex].kvs) { + // Store the link configuration parameters in the key-value store for persistence + _modbus_links[linkIndex].kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_RX_KEY_INDEX, static_cast(serial_rx)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_TX_KEY_INDEX, static_cast(serial_tx)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); + _modbus_links[linkIndex].kvs->store(SettingsType::Enum::ModbusInterfaceSettings_Type, linkIndex, 0, 0); + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, F("Modbus: setlink successfull")); + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + return true; +} + +#endif // if FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h new file mode 100644 index 0000000000..6fb1a84874 --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.h @@ -0,0 +1,85 @@ +#ifndef HELPERS_MODBUS_MGR_H +#define HELPERS_MODBUS_MGR_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS_FAC + +# include +# include "../Helpers/Modbus_link.h" +# include "../Helpers/_ESPEasy_key_value_store.h" + +// Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues +struct ModbusDEVICE_struct; + +// ModbusMGR structure representing the singleton Modbus Management entity +// Thw manager has an overview of all Modbus links and the conneted devices. +// The manager allows multiple Modbus devices to connect to a single Modbus link while supporting multiple links. +// The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and +// ModbusLINK object. +typedef struct ModbusMGR_struct { + ModbusMGR_struct(); + ~ModbusMGR_struct(); + + bool initialize(); + void processLinks(); + + bool connect(int linkId, + const ModbusDEVICE_struct *deviceID); + + bool disconnect(int linkId, + const ModbusDEVICE_struct*deviceID); + + Modbus_transaction_ptr newTransaction(int linkId, + ModbusDEVICE_struct *device); + + bool queueTransaction(int linkId, + Modbus_Transaction *transaction); + + + void dumpAdminInfo(); + + void show_modbus_interfaces(); + bool save_modbus_interfaces(String& error); + + bool isInitialized() const { return _initialized; } + +private: + + static const int MAX_MODBUS_LINKS = 4; // Maximum number of Modbus links supported + + // Structure representing the information of a Modbus link as managed by the Modbus_mgr + // Includes associated ModbusLINK object, link configuration and config storage + struct ModbusLinkInfo_struct { + ESPEasySerialPort port = ESPEasySerialPort::not_set; + int8_t serial_rx = -1; + int8_t serial_tx = -1; + int16_t baudrate = 9600; + int8_t dere_pin = -1; // Pin used for RS485 DE/RE control, -1 if not used + bool rs485_mode = false; // True if RS485 mode is enabled + bool collision_detect = false; // True if collision detection is enabled + struct ModbusLINK_struct *link = nullptr; // Pointer to the Modbus link object + ESPEasy_key_value_store *kvs = nullptr; // Key-value store for storing link-specific settings and parameters + + }; + + static bool validLinkId(int linkId) { return (linkId >= 0) && (linkId < ModbusMGR_struct::MAX_MODBUS_LINKS); } + + ModbusLinkInfo_struct _modbus_links[MAX_MODBUS_LINKS]{}; // All links managed by the Modbus manager + bool _initialized = false; // Flag indicating if the manager is initialized + int _deviceCounter = 0; + + bool setLink(const int linkIndex, + const ESPEasySerialPort port, + const int8_t serial_rx, + const int8_t serial_tx, + uint16_t baudrate, + int8_t dere_pin, + bool collision_detect); + +} ModbusMGR_struct_t; + +extern ModbusMGR_struct_t ModbusMGR_singleton; // Singleton instance of the Modbus Manager + +#endif // FEAURE_MODBUS +#endif // HELPERS_MODBUS_MGR_H diff --git a/src/src/Helpers/PeriodicalActions.cpp b/src/src/Helpers/PeriodicalActions.cpp index c2c829222e..e6e95101d0 100644 --- a/src/src/Helpers/PeriodicalActions.cpp +++ b/src/src/Helpers/PeriodicalActions.cpp @@ -49,6 +49,9 @@ #include "../Helpers/MDNS_Helper.h" #endif +#if FEATURE_MODBUS_FAC +#include "../Helpers/Modbus_mgr.h" +#endif #define PLUGIN_ID_MQTT_IMPORT 37 @@ -136,6 +139,12 @@ void run10TimesPerSecond() { Blynk_Run_c015(); } #endif + #if FEATURE_MODBUS_FAC + START_TIMER; + ModbusMGR_singleton.processLinks(); + STOP_TIMER(MODBUS_PROCESS_LINKS); + #endif + if (!UseRTOSMultitasking && (ESPEasy::net::NetworkConnected() || ESPEasy::net::wifi::wifiAPmodeActivelyUsed())) { // FIXME TD-er: What about client connected via AP? diff --git a/src/src/Helpers/Scheduler_IntervalTimer.cpp b/src/src/Helpers/Scheduler_IntervalTimer.cpp index 2c62b1e348..7b6f3a7144 100644 --- a/src/src/Helpers/Scheduler_IntervalTimer.cpp +++ b/src/src/Helpers/Scheduler_IntervalTimer.cpp @@ -13,7 +13,6 @@ #include "../Helpers/Networking.h" #include "../Helpers/PeriodicalActions.h" - /*********************************************************************************************\ * Interval Timer * These timers set a new scheduled timer, based on the old value. diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp new file mode 100644 index 0000000000..59546259e5 --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -0,0 +1,304 @@ +#include "../PluginStructs/P183_data_struct.h" + +#ifdef USES_P183 + +// ####################################################################################################### +// ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### +// ####################################################################################################### +////# define P183_DEBUG +# ifndef PLUGIN_BUILD_MAX_ESP32 +# undef P183_DEBUG // Debugging switched off +# endif + +// Actions for PLUGIN_TASKTIMER_IN event to distinguish between regular read results and scan sequences +# define ACTION_DUMP_RANGE 0xFFFF +# define ACTION_SCAN_BUS 0xFFFE +# define ACTION_READ_CACHE 0xFFFD + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Constructor of the plugin data structure. Initializes the data members to default values. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +P183_data_struct::P183_data_struct(struct EventStruct *event) { + _taskIndex = event->TaskIndex; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the plugin data structure. Clean up any resources used by the plugin. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +P183_data_struct::~P183_data_struct() { + delete _modbusDevice; + _modbusDevice = nullptr; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization. Takes the Modbus device address and link ID as parameters. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) +{ + // Create a fresh Modbus_device object to handle the Modbus communication + if (_modbusDevice != nullptr) { + delete _modbusDevice; + _modbusDevice = nullptr; + } + _modbusDevice = new (std::nothrow) ModbusDEVICE_struct(); + + if (_modbusDevice == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Unable to allocate Modbus device object"), _taskIndex)); + # endif // LIMIT_BUILD_SIZE + return false; + } + + if (!_modbusDevice->init(slaveAddress, linkId, _taskIndex)) { + return false; + } + _modbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::plugin_exit() +{ + if (_modbusDevice != nullptr) { + delete _modbusDevice; + _modbusDevice = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Plugin read function. Queues a new request to read the Modbus registers. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_read(struct EventStruct *event) { + if (_modbusDevice == nullptr) { + return false; + } + + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) + { + // Queue a read request for each active output value. The result will be processed in the task timer event. + // Use the output index as the event index to identify which output value the result belongs to. + _modbusDevice->readHoldingRegister(P183_ADDRESS(outputIndex), outputIndex); + } + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Handles the PLUGIN_TASKTIMER_IN event. +// This is used to process the results of Modbus read requests and to trigger the next step in a Modbus scan sequence. +// event->idx is used to identify which transaction the result belongs to. +// event->Par1 is used to indicate whether the Modbus read was successful (true) or not (false). +// event->Par2 is used to pass the value read from Modbus when the read was successful. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_task_timer(EventStruct *event) +{ + # ifdef P183_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("P183[%d]: TaskTimer called IDX=%d, par1=%d, par2=%d, par3=%d, par4=%d"), + event->idx, event->Par1, event->Par2, event->Par3, event->Par4)); + # endif // P183_DEBUG + + if (event->idx == ACTION_DUMP_RANGE) { + if (event->Par1) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), _lastAddress, _lastAddress, event->Par2, event->Par2)); + } else { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), _lastAddress, _lastAddress)); + } + _lastAddress++; + scan_next_address(); + return true; + } + else if (event->idx == ACTION_SCAN_BUS) { + if (event->Par1) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Device found at address %u (0x%02X)"), _lastAddress, _lastAddress)); + } + _lastAddress++; + scan_next_module(); + return true; + } + else if (event->idx == ACTION_READ_CACHE) { + // This is the result of the regular cache read triggered in plugin_once_per_second. Update the user variables with the cache values. + + if (event->Data == nullptr) { + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: No data received for cache read"), _taskIndex)); + return false; + } + ModbusRegisterSet_struct *registerSet = reinterpret_cast(event->Data); + int count = registerSet->size; + + if (count > _cacheSize) { + count = _cacheSize; // Prevent overflow if the device returns more registers than the cache can hold + } + + for (int i = 0; i < count; i++) { + _RegisterCache[i] = registerSet->data[i]; // Copy the received values to the cache + } + return true; + } + else { + int outputIndex = event->idx; + + if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Invalid output index in task timer event"), _taskIndex)); + # endif // LIMIT_BUILD_SIZE + return false; + } + + if (event->Par1) { + UserVar.setFloat(event->TaskIndex, outputIndex, event->Par2); // Update the user variable with the value read from Modbus + return true; + } + return false; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggered once per second. Fetch the cache values +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_once_per_second(EventStruct *event) +{ + if (_modbusDevice == nullptr) { + return false; + } + + if (P183_CACHE_SIZE == 0) { + return true; // Cache not used, nothing to do + } + + if (P183_CACHE_SIZE > P183_CACHE_SIZE_MAX) { + return false; // Cache size exceeds maximum allowed size + } + + // Queue a read request for the cache values. The result will be processed in the task timer event. + _cacheStart = P183_CACHE_START; + _cacheSize = P183_CACHE_SIZE; + _modbusDevice->readHoldingRegisters(_cacheStart, _cacheSize, ACTION_READ_CACHE); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start iterating over a register range of a Modbus device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_device(uint8_t node_id, uint16_t start_reg, uint16_t end_reg) +{ + uint16_t value = 0; + + addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); + + if (_modbusDevice == nullptr) { + return; + } + + if (start_reg < end_reg) { + _lastAddress = start_reg; + _endAddress = end_reg; + _scanning = true; + scan_next_address(); + } + else + { + _scanning = false; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Read the next holding register from the Modbus device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_next_address() +{ + if (_scanning) { + if (_lastAddress <= _endAddress) { + _modbusDevice->readHoldingRegister(_lastAddress, ACTION_DUMP_RANGE); + } else { + _scanning = false; + addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning device")); + } + return; + } + _scanning = true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Scan Modbus addreses on the bus and log the results +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_modbus() +{ + addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); + + if (_modbusDevice == nullptr) { + return; + } + + _lastAddress = 1; + _endAddress = 247; + _scanning = true; + scan_next_module(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_next_module() +{ + if (_scanning) { + if (_lastAddress <= _endAddress) { + _modbusDevice->readModuleHoldingRegister(_lastAddress, 1, ACTION_SCAN_BUS); + } + else + { + _scanning = false; + addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning for modules")); + } + } +} + +#if P183_ALLOW_MODBUS_WAIT +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Read a Modbus register from the device. Block until the data is available +// Warning: this may take time as we wait for the Modbus message to be exchanged +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t P183_data_struct::readRegisterWait(uint16_t address) { + uint16_t value = 0; + uint32_t startTime = millis(); + ModbusResultState state = ModbusResultState::Busy; + + if (_modbusDevice == nullptr) { + return 0; + } + + _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action + + while (state == ModbusResultState::Busy) { + delay(50); + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + + if (timePassedSince(startTime) > P183_MODBUS_TIMEOUT) { + state = ModbusResultState::Error; // Timeout, exit the loop with an error state + } + } + + return value; +} +#endif // ifdef P183_ALLOW_MODBUS_WAIT + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t P183_data_struct::readRegisterCache(uint16_t address) +{ + if ((address < _cacheStart) || (address >= _cacheStart + _cacheSize)) { + return 0; + } + else { + return _RegisterCache[address - _cacheStart]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::writeRegister(uint16_t address, uint16_t value) +{ + if (_modbusDevice == nullptr) { + return; + } + + _modbusDevice->writeSingleRegister(address, value, &_lastActionState); // Queue the action (and for now forget it) +} + +#endif // ifdef USES_P183 diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h new file mode 100644 index 0000000000..7f4531267c --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.h @@ -0,0 +1,98 @@ +#ifndef PLUGINSTRUCTS_P183_DATA_STRUCT_H +#define PLUGINSTRUCTS_P183_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#ifdef USES_P183 + +///# define P183_DEBUG // Switch on additional debug logging +# ifdef BUILD_NO_DEBUG +# undef P183_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + +# ifndef P183_ALLOW_MODBUS_WAIT +# define P183_ALLOW_MODBUS_WAIT 0 // Default to disabled +# endif // ifndef P183_ALLOW_MODBUS_WAIT + +# include "../Helpers/Modbus_device.h" + +// Plugin configuration parameters +// PCONFIG(0) is the Modbus device ID. +// PCONFIG(1) is the Modbus link ID. +// PCONFIG(2) is used for flags for future use. Currently not used. +// PCONFIG(3) is the number of active output values (1-4) +// PCONFIG(4) is the Modbus register address for value 1 +// PCONFIG(5) is the Modbus register address for value 2 +// PCONFIG(6) is the Modbus register address for value 3 +// PCONFIG(7) is the Modbus register address for value 4 +// Use P183_ADDRESS(x) to access the PCONFIG value for value x +// PCONFIG_LONG(0) is the number of Modbus registers to keep in cache +// PCONFIG_LONG(1) is the starting register address for the cache +# define P183_DEV_ID PCONFIG(0) +# define P183_DEV_ID_LABEL PCONFIG_LABEL(0) +# define P183_LINK_ID PCONFIG(1) +# define P183_LINK_ID_LABEL PCONFIG_LABEL(1) +# define P183_NR_OUTPUTS_INDEX 3 +# define P183_NR_OUTPUTS PCONFIG(P183_NR_OUTPUTS_INDEX) +# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(P183_NR_OUTPUTS_INDEX) +# define P183_ADDRESS(x) PCONFIG(P183_NR_OUTPUTS_INDEX + 1 + x) +# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) + +# define P183_CACHE_SIZE PCONFIG_LONG(0) +# define P183_CACHE_SIZE_LABEL F("P183sz") +# define P183_CACHE_START PCONFIG_LONG(1) +# define P183_CACHE_START_LABEL F("P183st") + +# define P183_DEV_ID_DFLT 1 +# define P183_LINK_ID_DFLT 0 +# define P183_MODBUS_TIMEOUT 1000 // milliseconds +# define P183_MAX_MODBUS_NODES 247 +# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address +# define P183_CACHE_SIZE_MAX 100 // Maximum number of registers to keep in cache +# define P183_CACHE_START_MAX 65535 // Maximum starting address for cache + +// The default set of single-value VType options +// constexpr uint8_t P183_START_VTYPE = 0; + +struct P183_data_struct : public PluginTaskData_base { + P183_data_struct(struct EventStruct *event); + P183_data_struct() = delete; + virtual ~P183_data_struct(); + + bool plugin_init(uint8_t slaveAddress, + int linkId); + + void plugin_exit(); + bool plugin_read(struct EventStruct *event); + bool plugin_task_timer(struct EventStruct *event); + bool plugin_once_per_second(struct EventStruct *event); + void scan_device(uint8_t node_id, + uint16_t start_reg, + uint16_t end_reg); + void scan_modbus(); + # if P183_ALLOW_MODBUS_WAIT + uint16_t readRegisterWait(uint16_t address); + # endif // ifdef P183_ALLOW_MODBUS_WAIT + uint16_t readRegisterCache(uint16_t address); + + void writeRegister(uint16_t address, + uint16_t value); + +private: + + taskIndex_t _taskIndex = INVALID_TASK_INDEX; + struct ModbusDEVICE_struct *_modbusDevice = nullptr; + uint16_t _RegisterCache[P183_CACHE_SIZE_MAX] = { 0 }; + uint16_t _cacheStart = 0; + int _cacheSize = 0; + ModbusResultState _lastActionState = ModbusResultState::Busy; + uint16_t _lastAddress = 0; + uint16_t _endAddress = 0; + bool _scanning = false; + + void scan_next_address(); + void scan_next_module(); + +}; + +#endif // ifdef USES_P183 +#endif // ifndef PLUGINSTRUCTS_P183_DATA_STRUCT_H diff --git a/src/src/WebServer/ESPEasy_WebServer.cpp b/src/src/WebServer/ESPEasy_WebServer.cpp index 8f20a3d743..93be63b6e0 100644 --- a/src/src/WebServer/ESPEasy_WebServer.cpp +++ b/src/src/WebServer/ESPEasy_WebServer.cpp @@ -304,9 +304,9 @@ void WebServerInit() #if FEATURE_SPI web_server.on(F("/interfaces_spi"), handle_interfaces_spi); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC web_server.on(F("/interfaces_modbus"), handle_interfaces_modbus); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN web_server.on(F("/interfaces_can"), handle_interfaces_can); #endif // if FEATURE_CAN diff --git a/src/src/WebServer/InterfacesPage.cpp b/src/src/WebServer/InterfacesPage.cpp index abdc7c9277..850c15e23c 100644 --- a/src/src/WebServer/InterfacesPage.cpp +++ b/src/src/WebServer/InterfacesPage.cpp @@ -24,6 +24,9 @@ # if FEATURE_I2C_MULTIPLE # include "../Helpers/Hardware_device_info.h" # endif // if FEATURE_I2C_MULTIPLE +# if FEATURE_MODBUS_FAC +# include "../Helpers/Modbus_mgr.h" +# endif // ******************************************************************************** // Web Interface hardware page @@ -42,12 +45,12 @@ void handle_interfaces_spi() { } #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void handle_interfaces_modbus() { navMenuIndex = MENU_INDEX_INTERFACES_MODBUS; handle_interfaces(); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void handle_interfaces_can() { @@ -80,9 +83,9 @@ void handle_interfaces() { #if FEATURE_I2C newNavIndex = MENU_INDEX_INTERFACES_I2C; #elif FEATURE_SPI - newNavIndex = MENU_INDEX_INTERFACES_SPI; -#elif FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB - newNavIndex = MENU_INDEX_INTERFACES_MODBUS; + navMenuIndex = MENU_INDEX_INTERFACES_SPI; +#elif FEATURE_MODBUS_FAC + navMenuIndex = MENU_INDEX_INTERFACES_MODBUS; #elif FEATURE_CAN newNavIndex = MENU_INDEX_INTERFACES_CAN; #elif FEATURE_WRMBUS @@ -116,11 +119,11 @@ void handle_interfaces() { } #endif - #if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB + #if FEATURE_MODBUS_FAC if (navMenuIndex == MENU_INDEX_INTERFACES_MODBUS) { interfaces_show_MODBUS(); } - #endif // if FEATURE_MODBUS + #endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN if (navMenuIndex == MENU_INDEX_INTERFACES_CAN) { @@ -166,9 +169,9 @@ void save_interfaces() { if ((navMenuIndex == MENU_INDEX_INTERFACES_SPI) && save_SPI(error)) { updated = true; } #endif - #if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB + #if FEATURE_MODBUS_FAC if ((navMenuIndex == MENU_INDEX_INTERFACES_MODBUS) && save_MODBUS(error)) { updated = true; } - #endif // if FEATURE_MODBUS + #endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN if ((navMenuIndex == MENU_INDEX_INTERFACES_CAN) && save_CAN(error)) { updated = true; } @@ -332,11 +335,11 @@ bool save_SPI(String& error) { #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC bool save_MODBUS(String& error) { - return false; // TODO + return ModbusMGR_singleton.save_modbus_interfaces(error); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN bool save_CAN(String& error) { @@ -538,11 +541,11 @@ void interfaces_show_SPI() { } #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void interfaces_show_MODBUS() { - addRowLabel(F("TODO")); // TODO + ModbusMGR_singleton.show_modbus_interfaces(); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void interfaces_show_CAN() { diff --git a/src/src/WebServer/InterfacesPage.h b/src/src/WebServer/InterfacesPage.h index a96601c7ad..b3d6f110a0 100644 --- a/src/src/WebServer/InterfacesPage.h +++ b/src/src/WebServer/InterfacesPage.h @@ -21,11 +21,11 @@ bool save_SPI(String& error); void interfaces_show_SPI(); #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void handle_interfaces_modbus(); bool save_MODBUS(String& error); void interfaces_show_MODBUS(); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void handle_interfaces_can(); diff --git a/src/src/WebServer/WebTemplateParser.cpp b/src/src/WebServer/WebTemplateParser.cpp index 2a138a8dde..c8b6fc68cc 100644 --- a/src/src/WebServer/WebTemplateParser.cpp +++ b/src/src/WebServer/WebTemplateParser.cpp @@ -120,9 +120,9 @@ const __FlashStringHelper* getGpMenuIcon(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: return ICON("SPI"); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: return ICON("Modbus"); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: return ICON("CAN bus"); #endif // if FEATURE_CAN @@ -154,13 +154,13 @@ const __FlashStringHelper* getGpMenuLabel(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: #endif // if FEATURE_CAN -#if FEATURE_I2C || FEATURE_SPI || (FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB) || FEATURE_CAN +#if FEATURE_I2C || FEATURE_SPI || FEATURE_MODBUS_FAC || FEATURE_CAN break; // No label, only an 'icon', for the second-level menu #endif // if FEATURE_I2C || FEATURE_SPI || FEATURE_MODBUS || FEATURE_CAN #if FEATURE_WRMBUS @@ -191,9 +191,9 @@ const __FlashStringHelper* getGpMenuURL(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: return F("/interfaces_spi"); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: return F("/interfaces_modbus"); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: return F("/interfaces_can"); #endif // if FEATURE_CAN @@ -221,7 +221,7 @@ bool GpMenuVisible(uint8_t index) { case MENU_INDEX_INTERFACES: return MENU_INDEX_INTERFACES_VISIBLE; case MENU_INDEX_INTERFACES_I2C: return (1 == FEATURE_I2C); case MENU_INDEX_INTERFACES_SPI: return (1 == FEATURE_SPI); - case MENU_INDEX_INTERFACES_MODBUS: return (1 == FEATURE_MODBUS && 1 == FEATURE_MODBUS_INTERFACES_TAB); + case MENU_INDEX_INTERFACES_MODBUS: return (1 == FEATURE_MODBUS_FAC); #if defined(FEATURE_CAN) case MENU_INDEX_INTERFACES_CAN: return (1 == FEATURE_CAN); #endif // if defined(FEATURE_CAN)