diff --git a/docs/source/Interfaces/Interfaces.rst b/docs/source/Interfaces/Interfaces.rst index 87e9084f16..8025e9885e 100644 --- a/docs/source/Interfaces/Interfaces.rst +++ b/docs/source/Interfaces/Interfaces.rst @@ -205,5 +205,16 @@ 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. Note that a serial to RS485 converter is required to connect Modbus RTU devices to the ESP board. +.. 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. + +The Modbus RTU devices are connected to the selected serial interface via a RS485 converter. The converter has 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-". + +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 87f3366f5e..608798b22b 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..9e26150018 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.puml @@ -0,0 +1,40 @@ +@startuml + +class plugin { + +} + +class plugin_struct { + +} + +package "modbusFacility" { + class Modbus_mgr <> { + } + + class Modbus_link { + } + + class Modbus_device { + } + + class Queue { + } + + struct Transaction { + } +} + +class Serial_port { +} + +plugin *-- plugin_struct +plugin_struct *-right- Modbus_device +Modbus_device "*" -down-> "1" Modbus_link +Modbus_link "1" -right-> "1" Serial_port +Modbus_mgr "1" *-- "*" Modbus_link +Modbus_mgr "1" -- "*" Modbus_device +Modbus_link *-- "1" Queue +Queue o-left- "*" Transaction +Modbus_device "1" --> "*" Transaction +@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..79a12939c4 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.svg @@ -0,0 +1 @@ +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_port*1111*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..c46230be2f --- /dev/null +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -0,0 +1,11 @@ +@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 +} +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/src/_P183_modbus.ino b/src/_P183_modbus.ino new file mode 100644 index 0000000000..83759cae51 --- /dev/null +++ b/src/_P183_modbus.ino @@ -0,0 +1,288 @@ +#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_SET_DEFAULTS: + { + P183_DEV_ID = P183_DEV_ID_DFLT; + 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, 247); + + 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); + + 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 + int address = event->Par2; + uint16_t value = 0; + value = P183_data->readRegisterWait(address); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); + } + success = true; + } + else if (equals(subcmd, F("dump"))) { + int start_address = event->Par2; + int 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, F("P183 : Modbus Unknown command")); + } + # endif // LIMIT_BUILD_SIZE + } + + break; + } + + 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, F("P183 : Modbus task timer invalid data struct")); + # 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, F("P183 : Modbus Get config invalid data struct")); + # endif // LIMIT_BUILD_SIZE + return false; + } + + const String cmd = parseString(string, 1); + + if (equals(cmd, F("register"))) { + int address = parseString(string, 2).toInt(); + uint16_t value = 0; + value = P183_data->readRegisterWait(address); + string = String(value); + success = true; + } + break; + } + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += strformat(F("Modbus %d"), P183_LINK_ID); + success = true; + 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..c9a1a79f73 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). @@ -2186,6 +2197,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_B_COLLECTION // Collection of climate A plugins. @@ -2355,6 +2370,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 +2472,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 +2556,13 @@ 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 + #endif + #endif // ifdef PLUGIN_NEOPIXEL_COLLECTION @@ -3007,6 +3032,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 +3152,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 +3805,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 +4521,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/DataTypes/SettingsType.cpp b/src/src/DataTypes/SettingsType.cpp index fde993e3ed..01bc068d0b 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 b5f2c43cb7..380130988d 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..85831ee5a4 --- /dev/null +++ b/src/src/Helpers/Modbus_device.cpp @@ -0,0 +1,371 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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 + +const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; +const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; +const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; +const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus device class +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusDEVICE_struct::~ModbusDEVICE_struct() { + if (_modbus_link != nullptr) { + _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a + // destructed object + } + ModbusMGR_singleton.disconnect(_deviceID); + _modbus_link = nullptr; + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reset the Modbus device class to initial state +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::reset() { + if (_modbus_link != nullptr) { + _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a + // destructed object + } + ModbusMGR_singleton.disconnect(_deviceID); + _modbus_link = nullptr; + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization connected to an existing link. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t taskIndex) +{ + bool success = ModbusMGR_singleton.connect(linkId, &_modbus_link, &_deviceID); + + _modbus_address = slaveAddress; + _taskIndex = taskIndex; + # 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 (_modbus_link != nullptr) && (_modbus_link->isInitialized()); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, + ModbusResultState *statePtr) +{ + if (!isInitialized()) { + return false; + } + Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + + if (request == nullptr) { + return false; // Failed to allocate a request structure + } + request->_userData = valuePtr; + request->_userState = statePtr; + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + createReadFrame(*request, _modbus_address, address); + (void) _modbus_link->queueTransaction(request); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register. The result will be available later through an 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_RequestQueueElement *request = _modbus_link->newTransaction(this); + + if (request == nullptr) { + return false; // Failed to allocate a request structure + } + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + request->_userData = nullptr; + request->_userState = nullptr; + request->_userId = uid; + createReadFrame(*request, busAddress, registerAddress); + (void) _modbus_link->queueTransaction(request); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Construct a Modbus read holding registers message +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, + uint8_t busAddress, + uint16_t registerAddress) +{ + 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] = 0; + request._sendframe[5] = 1; // Read 1 register + 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 = 7; // Expect 8 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_RequestQueueElement *request = _modbus_link->newTransaction(this); + + request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; + request->_userState = statePtr; + + request->_sendframe[0] = _modbus_address; + request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; + request->_sendframe[2] = highByte(address); + request->_sendframe[3] = lowByte(address); + request->_sendframe[4] = highByte(value); + request->_sendframe[5] = lowByte(value); + 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 = 8; // Expect 8 bytes in response + ////dump_buffer(request->_sendframe, request->_sendframe_length); + (void) _modbus_link->queueTransaction(request); + *statePtr = ModbusResultState::Busy; + + return true; +} + +void ModbusDEVICE_struct::processCommand(void) +{ + if (!isInitialized()) { + _modbus_link->processCommand(); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) +{ + ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise + + if (req == nullptr) { + addLogMove(LOG_LEVEL_INFO, F("Modbus: ERROR, Null pointer passed in callback")); + return; + } + + # ifdef MODBUS_DEBUG + String log = strformat(F("Modbus: Device callback, device= %d, Request= %d, Message= %d"), + _deviceID, + req->_id, + static_cast(req->_messageType) + ); + # endif // MODBUS_DEBUG + + if (req->_state == ModbusQueueState::ERROR_OCCURRED) { + sendEvent(req, false, 0, 0, 0); + # ifdef MODBUS_DEBUG + log += F(" Link error occurred"); + # endif // MODBUS_DEBUG + } + else { + switch (req->_messageType) + { + case ModbusTransactionType::READ_HOLDING_REGISTERS: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); + + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + int val = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + + // Valid response + if (req->_userData != nullptr) { + *(static_cast(req->_userData)) = val; + resultState = ModbusResultState::Success; + } + else + { + sendEvent(req, true, val, 0, 0); + } + } + } + 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; + } + _modbus_link->freeTransaction(req); // Free the transaction to prevent memory leaks. + # ifdef MODBUS_DEBUG + log += F(", Result = "); + log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); + addLogMove(LOG_LEVEL_INFO, log); + # endif // MODBUS_DEBUG +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement *req, int par1, int par2, int par3, int par4) +{ + struct EventStruct TempEvent; + + TempEvent.Par1 = par1; + TempEvent.Par2 = par2; + TempEvent.Par3 = par3; + TempEvent.Par4 = par4; + 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); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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..8f7ca73fa1 --- /dev/null +++ b/src/src/Helpers/Modbus_device.h @@ -0,0 +1,106 @@ +#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 + +// 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 + +}; + +// 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 { +private: + +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; + + void linkCallback(Modbus_RequestQueueElement *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); + + // 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; + 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 + + void sendEvent(Modbus_RequestQueueElement *req, + int par1, + int par2, + int par3, + int par4); + + void createReadFrame(Modbus_RequestQueueElement& request, + uint8_t busAddress, + uint16_t registerAddress); + + 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_LINK_H diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp new file mode 100644 index 0000000000..73b80876d7 --- /dev/null +++ b/src/src/Helpers/Modbus_link.cpp @@ -0,0 +1,367 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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 = _requestQueue.begin(); it != _requestQueue.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 + } + it = _requestQueue.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, + uint16_t baudrate, + int8_t dere_pin, + bool collision_detect) { + int available = 0; + + // (re)create the serial port object + // If the serial port object already exists, delete it first. + if (_easySerial != nullptr) { + delete _easySerial; + _easySerial = nullptr; + } + _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 %s, Init serial, 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; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Provide a new transaction structure that can be used to build a Modbus request and queue it at this link +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVICE_struct *device) +{ + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to create transaction on uninitialized link")); + return nullptr; + } + + Modbus_RequestQueueElement *req = new (std::nothrow) Modbus_RequestQueueElement(); + + if (req != nullptr) { + req->_id = ++(_queueID); // Assign a unique ID to the transaction + req->_device = device; // The Modbus device making the request + req->_timeout = _modbus_timeout; // Default timeout value + req->_state = ModbusQueueState::NOT_QUEUED; // Initial state + return req; + } + else { + return nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Free a previously allocated transaction structure +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) { + if (transaction != nullptr) { + if (transaction->_state == ModbusQueueState::NOT_QUEUED) { // Not on the queue, can be directly deleted + delete transaction; + } else { + transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be freed by queue processing + } + return true; + } + else { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free null transaction")); + return false; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Free all queued transactions for the given device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) +{ + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free transactions on uninitialized link")); + return; + } + + for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { + if ((*it)->_device == device) { + (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); + return 0; + } + if (transaction == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); + return 0; + } + if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER ) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, receive buffer too large")); + return 0; + } + + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); + } + # endif // MODBUS_DEBUG + transaction->_state = ModbusQueueState::QUEUED; // Initial state + _requestQueue.push_back(transaction); // Append the request to the queue + processCommand(); // Trigger processing of the command queue + return transaction->_id; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Evaluate the next action to take to process the queue +// This function shall be called periodically to keep the Modbus link active +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::processCommand() +{ + if (!isInitialized() || (_requestQueue.empty())) { + return; // Serial port not initialized or queue is empty, nothing to process + } + + auto it = _requestQueue.begin(); // Iterator for the request queue + bool busy = false; // Only process one request at a time + + while ((it != _requestQueue.end()) && !busy && ((*it) != nullptr)) { + # ifdef MODBUS_DEBUG + dumpQueueElement(*it); + # endif // MODBUS_DEBUG + + switch ((*it)->_state) + { + case ModbusQueueState::QUEUED: + { + // Send 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 + busy = true; // Only process one request at a time + break; + } + + case ModbusQueueState::MESSAGE_SENT: + { + // Waiting for response + if (_easySerial->available() >= (*it)->_rcvframe_length) { + _easySerial->readBytes((*it)->_rcvframe, (*it)->_rcvframe_length); + (*it)->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + + if ((*it)->_device != nullptr) { + (*it)->_device->linkCallback((*it)); // Notify the device that a response was received + } + } + else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { + // Timeout expired + (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + + if ((*it)->_device != nullptr) { + (*it)->_device->linkCallback((*it)); // Notify the device that a response was received + } + else {} + it++; + } + else { + // Still waiting + busy = true; // Only process one request at a time + } + break; + } + + case ModbusQueueState::ERROR_OCCURRED: + { + it++; + break; + } + + case ModbusQueueState::READY_FOR_DESTROY: + { + delete (*it); // destroy the queue element + it = _requestQueue.erase(it); // Remove it from the list + break; + } + + default: + it++; + break; + } // switch + } // next iterarion + + return; +} + +int16_t ModbusLINK_struct::getBaudrate(void) const +{ + return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; +} + +int16_t ModbusLINK_struct::getSerialRX(void) const +{ + return _easySerial != nullptr ? _easySerial->getRxPin() : -1; +} + +int16_t ModbusLINK_struct::getSerialTX(void) const +{ + return _easySerial != nullptr ? _easySerial->getTxPin() : -1; +} + +int8_t ModbusLINK_struct::getDerePin(void) const +{ + return _dere_pin; +} + +bool ModbusLINK_struct::getCollisionDetect(void) const +{ + return _collision_detect; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element contents to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = strformat(F("Modbus: [ID=%u, Device=%p, State="), el->_id, el->_device); + log += toString(el->_state); + log += F(", TX="); + + for (int i = 0; i < el->_sendframe_length; i++) { + log += String(el->_sendframe[i], HEX); + log += F(","); + } + log += F(", RX="); + + for (int i = 0; i < el->_rcvframe_length; i++) { + log += String(el->_rcvframe[i], HEX); + log += F(","); + } + log += F("] "); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // MODBUS_DEBUG +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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) { + 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"); + } + return F(""); +} + +#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..7b57a83ace --- /dev/null +++ b/src/src/Helpers/Modbus_link.h @@ -0,0 +1,127 @@ +#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 + +} 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 request queue element structure +// This structure represents a single Modbus request and its associated response. +struct Modbus_RequestQueueElement { + Modbus_RequestQueueElement() = default; + + 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; // ID of the request + 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 + // expected + 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 + +}; + +// Queue of Modbus request elements +typedef std::list Modbus_RequestQueue; + + +// 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(); + + void reset(); + + bool init(const ESPEasySerialPort port, + const int8_t serial_rx, + const int8_t serial_tx, + uint16_t baudrate, + int8_t dere_pin, + bool collision_detect = false); + + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } + + Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); + bool freeTransaction(Modbus_RequestQueueElement *transaction); + void freeTransactions(struct ModbusDEVICE_struct *device); + uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); + void processCommand(); + + int16_t getBaudrate(void) const; + + int16_t getSerialRX(void) const; + + int16_t getSerialTX(void) const; + + int8_t getDerePin(void) const; + + bool getCollisionDetect(void) const; + + +private: + + static void dumpQueueElement(Modbus_RequestQueueElement *el); + static void dumpState(ModbusQueueState_t state); + + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process + uint16_t _queueID = 0; // ID for the last request queued + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + uint32_t _reads_pass = 0; // TODO: statistics + uint32_t _reads_crc_failed = 0; // TODO: statistics + uint32_t _reads_nodata = 0; // TODO: statistics + uint8_t _dere_pin = 0; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled + bool _initialized = false; + uint8_t _last_error = 0; + +}; + + +#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..9323e75eb7 --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -0,0 +1,599 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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 + +# 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 = {}; + +int modbus_storageValueToBaudrate(uint8_t baudrate_setting); +uint8_t modbus_baudrateToStorageValue(int baudrate); + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Constructor of the Modbus manager class, initializes the internal data structures +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct::ModbusMGR_struct() +{ + # ifdef MODBUS_DEBUG + String log = F("Modbus: Manager, Constructor "); + # endif // ifdef MODBUS_DEBUG + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + _modbus_links[i] = nullptr; + } + + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + _modbus_devices[i] = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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_DEVICES; i++) { + if (_modbus_devices[i] != nullptr) { + delete _modbus_devices[i]; + _modbus_devices[i] = nullptr; + } + } + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] != nullptr) { + 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; + } + delete _modbus_links[i]; + _modbus_links[i] = 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() +{ + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] == nullptr) { + int8_t val; + _modbus_links[i] = new (std::nothrow) ModbusLinkInfo_struct(); + _modbus_links[i]->link = nullptr; + _modbus_links[i]->kvs = new (std::nothrow) ESPEasy_key_value_store; + _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); + + if (_modbus_links[i]->port != ESPEasySerialPort::not_set) { + _modbus_links[i]->link = new (std::nothrow) ModbusLINK_struct(); + _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, ModbusLINK_struct **link, uint8_t *deviceID) +{ + # ifdef MODBUS_DEBUG + String log = F("Modbus: Manager, Connect linkId="); + log += linkId; + # endif // ifdef MODBUS_DEBUG + + initialize(); // TODO Initialization sequence to be refactored. + + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; + } + + *deviceID = -1; // Default to -1, will be set to a valid device ID if connection is successful + *link = nullptr; + + if (_modbus_links[linkId] != nullptr) { + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if (_modbus_devices[i] == nullptr) { + // Found an available device slot + _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); + _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-MAX_MODBUS_DEVICES) + _modbus_devices[i]->link = _modbus_links[linkId]; + *deviceID = _modbus_devices[i]->deviceID; + *link = _modbus_devices[i]->link->link; + # ifdef MODBUS_DEBUG + log += F(" Assigned deviceID= "); + log += *deviceID; + # endif // ifdef MODBUS_DEBUG + break; + } + } + } + else { + # ifdef MODBUS_DEBUG + log += F(" No link available at linkIndex= "); + log += linkId; + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; + } + + if (*deviceID == -1) { + // No available device slot found, connection failed + # ifdef MODBUS_DEBUG + log += F(" Failed to assign device ID, no available device slots"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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(uint8_t deviceID) { + dumpAdminInfo(); + # ifdef MODBUS_DEBUG + String log = F("Modbus: Manager, Disconnect device="); + log += deviceID; + # endif // ifdef MODBUS_DEBUG + + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if ((_modbus_devices[i] != nullptr) && (_modbus_devices[i]->deviceID == deviceID)) { + // Remove the device entry + delete _modbus_devices[i]; + _modbus_devices[i] = nullptr; + } + } + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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] != nullptr) && (_modbus_links[i]->link != nullptr)) { + _modbus_links[i]->link->processCommand(); // 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() +{ + addLogMove(LOG_LEVEL_INFO, F("Modbus: Manager, Dumping admin info")); + # ifdef MODBUS_DEBUG + + // Iterate over the modbus links and dump their info + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] != nullptr) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: 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, + _modbus_links[i]->rs485_mode ? F("Yes") : F("No"), + _modbus_links[i]->collision_detect ? F("Yes") : F("No") + )); + } + else { + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Link[%d] "), i)); + } + } + + // Iterate over the modbus devices and dump their info + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if (_modbus_devices[i] != nullptr) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: Device[%d] DeviceID=%d, LinkPort=%s"), + i, + _modbus_devices[i]->deviceID, + ESPEasySerialPort_toString(_modbus_devices[i]->link->port) + )); + } + else { + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Device[%d] "), i)); + } + } + # 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 + String options_port[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Port otions for the selector, only valid ports will be + // filled. + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the + // options_port array + + constexpr int optionBaudCount = static_cast(NR_ELEMENTS(options_baudrate)); + + for (int i = 0; i < optionBaudCount; ++i) { + options_baudrate[i] = modbus_storageValueToBaudrate(i); + } + const FormSelectorOptions baudselector(optionBaudCount, options_baudrate); + + int optionPortCount = 1; + options_port[0] = F("Not set"); + portMap[0] = 0; // Map the "Not set" option to index 0 + + for (int i = 1; i < NR_ELEMENTS(options_port); i++) { + if (validSerialPort(static_cast(i))) { + options_port[optionPortCount] = ESPEasySerialPort_toString(static_cast(i)); + portMap[i] = optionPortCount; // Store the index of the valid port in the options_port array + optionPortCount++; + } + else { + options_port[i] = F("Invalid"); + portMap[i] = 0; // Map invalid/unused ports to the "Not set" option + } + } + const FormSelectorOptions portSelector(optionPortCount, options_port); + + // Iterate over the modbus links and show their configuration on the web page + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) + { + if (_modbus_links[link] != nullptr) { + addFormSubHeader(strformat(F("Modbus %u"), link)); + addFormDetailsStart(link == 0); + + int idx = static_cast(_modbus_links[link]->port); + portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); + + String id = strformat(F("MBtx%u"), link); + addRowLabel_tr_id(formatGpioName_serialTX(false), id); + addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link]->serial_tx); + + id = strformat(F("MBrx%u"), link); + addRowLabel_tr_id(formatGpioName_serialRX(false), id); + addPinSelect(PinSelectPurpose::Serial_output, id, _modbus_links[link]->serial_rx); + + id = strformat(F("MBde%u"), link); + addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); + addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link]->dere_pin); + + baudselector.addFormSelector(F("Baud Rate"), + strformat(F("MBbaud%u"), link), + modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); + addUnit(F("baud")); + + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { + int baudrate = 9600; + + switch (baudrate_setting) + { + case 0: + baudrate = 1200; + break; + case 1: + baudrate = 2400; + break; + case 2: + baudrate = 4800; + break; + case 3: + baudrate = 9600; + break; + case 4: + baudrate = 19200; + break; + case 5: + baudrate = 38400; + break; + case 6: + baudrate = 57600; + break; + case 7: + baudrate = 115200; + break; + default: + baudrate = 9600; + break; // Default value for fallback + } + return baudrate; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Convert actual baudrate value to stored baudrate setting (enumeration value) +// Returns the stored baudrate setting. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint8_t modbus_baudrateToStorageValue(int baudrate) { + if (baudrate <= 1200) { return 0; } + else if (baudrate <= 2400) { return 1; } + else if (baudrate <= 4800) { return 2; } + else if (baudrate <= 9600) { return 3; } + else if (baudrate <= 19200) { return 4; } + else if (baudrate <= 38400) { return 5; } + else if (baudrate <= 57600) { return 6; } + else if (baudrate <= 115200) { return 7; } + else { return 3; } // Default to 9600 baud for unsupported values +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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) +{ + int portCount = 1; + + // Create mapping table from dropdown enum index to actual port identifier, index 0 is reserved for "Not set" + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; + + portMap[0] = 0; // Map the "Not set" option to index 0 + + for (int i = 1; i < NR_ELEMENTS(portMap); i++) { + if (validSerialPort(static_cast(i))) { + portMap[portCount++] = i; + } + } + + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { + int port_setting = 0; + int baudrate_setting = 0; + int tx_setting = 0; + int rx_setting = 0; + int dere_setting = 0; + bool collision_detect_setting = false; + bool settingsChanged = false; + + if (_modbus_links[link] != nullptr) { + for (int i = 0; i < NR_ELEMENTS(portMap); i++) { + if (portMap[i] == static_cast(_modbus_links[link]->port)) { + port_setting = i; + break; + } + } + + if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting)) { + settingsChanged |= (portMap[port_setting] != static_cast(_modbus_links[link]->port)); + } + baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link]->baudrate); + + if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { + settingsChanged |= (modbus_storageValueToBaudrate(baudrate_setting) != _modbus_links[link]->baudrate); + } + tx_setting = _modbus_links[link]->serial_tx; + + if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { + settingsChanged |= (tx_setting != _modbus_links[link]->serial_tx); + } + rx_setting = _modbus_links[link]->serial_rx; + + if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { + settingsChanged |= (rx_setting != _modbus_links[link]->serial_rx); + } + dere_setting = _modbus_links[link]->dere_pin; + + if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { + settingsChanged |= (dere_setting != _modbus_links[link]->dere_pin); + # 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(strformat(F("MBcoll%u"), link)); + settingsChanged |= (collision_detect_setting != _modbus_links[link]->collision_detect); + # endif // ifdef ESP32 + } + } + + if (settingsChanged) { + setLink(link, + static_cast(portMap[port_setting]), + rx_setting, + tx_setting, + modbus_storageValueToBaudrate(baudrate_setting), + dere_setting, + collision_detect_setting); + } + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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) +{ + ModbusLinkInfo_struct *linkInfoPtr = nullptr; + + + # 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, + collision_detect ? F("Yes") : F("No") + )); + # endif // ifdef MODBUS_DEBUG + + if ((linkIndex >= 0) && (linkIndex < MAX_MODBUS_LINKS)) { + if (_modbus_links[linkIndex] == nullptr) { + linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); + _modbus_links[linkIndex] = linkInfoPtr; + # ifdef MODBUS_DEBUG + log += strformat(F("New link for linkIndex=%d"), linkIndex); + # endif // ifdef MODBUS_DEBUG + } + else { + linkInfoPtr = _modbus_links[linkIndex]; // Link admin already exists, will be reused for the new link configuration + # ifdef MODBUS_DEBUG + log += strformat(F("Existing link for linkIndex=%d"), linkIndex); + # endif // ifdef MODBUS_DEBUG + } + } + else { + # 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 + } + + if (linkInfoPtr != nullptr) { // Sanity check for successful link admin creation + if (linkInfoPtr->link == nullptr) { // Check if link object already exists + // No existing link, create a new one + linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); + } + + if (linkInfoPtr->link != nullptr) { // Sanity check for successful creation + // (re)initialize the new link + if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + // Initialization failed, clean up + delete linkInfoPtr->link; + linkInfoPtr->link = nullptr; + delete linkInfoPtr; + linkInfoPtr = nullptr; + return false; // Initialization failed + } + else { + // Store the link parameters + linkInfoPtr->port = port; + linkInfoPtr->serial_rx = serial_rx; + linkInfoPtr->serial_tx = serial_tx; + linkInfoPtr->baudrate = baudrate; + linkInfoPtr->dere_pin = dere_pin; + linkInfoPtr->rs485_mode = (dere_pin != -1); + linkInfoPtr->collision_detect = collision_detect; + } + } + } + else { + return false; // Failed to create link admin + } + + if (_modbus_links[linkIndex]->kvs == nullptr) { + _modbus_links[linkIndex]->kvs = new (std::nothrow) ESPEasy_key_value_store; + } + + if (_modbus_links[linkIndex]->kvs) { + // Store the link configuration parameters in the key-value store for persistence + linkInfoPtr->kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); + linkInfoPtr->kvs->setValue(MODBUS_RX_KEY_INDEX, static_cast(serial_rx)); + linkInfoPtr->kvs->setValue(MODBUS_TX_KEY_INDEX, static_cast(serial_tx)); + linkInfoPtr->kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); + linkInfoPtr->kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); + linkInfoPtr->kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); + + linkInfoPtr->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..326cf3febd --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.h @@ -0,0 +1,82 @@ +#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" + + +// 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(); + + bool connect(int linkId, + ModbusLINK_struct **link, + uint8_t *deviceID); + + bool disconnect(uint8_t deviceID); + + void processLinks(); + + 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 + static const int MAX_MODBUS_DEVICES = 16; // Maximum number of Modbus devices supported + + // Structure representing the information of a Modbus link, including its configuration and associated ModbusLINK object + 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 + + }; + + // Structure representing the information of a Modbus device, including assocuited ModbusDEVICE object + struct ModbusDeviceInfo_struct { + uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager + struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info + + }; + + ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object + ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices + bool _initialized = false; // Flag indicating if the manager is initialized + bool _testing = false; + + 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 8fd354c5e3..a18702c108 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,10 @@ void run10TimesPerSecond() { Blynk_Run_c015(); } #endif + #if FEATURE_MODBUS_FAC + ModbusMGR_singleton.processLinks(); + #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..f35b3c8695 --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -0,0 +1,243 @@ +#include "../PluginStructs/P183_data_struct.h" + + +#ifdef USES_P183 + +// ####################################################################################################### +// ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### +// ####################################################################################################### +# ifdef BUILD_NO_DEBUG +# undef P183_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + +// 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 + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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, F("P183: Unable to allocate Modbus device object")); + # 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: 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 { + int outputIndex = event->idx; + + if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { + # ifdef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); + # 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; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start iterating over a register range of a Modbus device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_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")); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Read a Modbus register from the device. Wait untial the data is available +// Warning: this may take time as we waith for the Modbus message to be exchanged +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t P183_data_struct::readRegisterWait(uint16_t address) { + uint16_t value = 0; + 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 + } + + return value; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +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..666bc15690 --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.h @@ -0,0 +1,74 @@ +#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 + +# 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 +# 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 PCONFIG(3) +# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) +# define P183_ADDRESS(x) PCONFIG(4 + x) +# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) + +# define P183_DEV_ID_DFLT 1 +# define P183_MODBUS_TIMEOUT 1000 // milliseconds +# define P183_MAX_MODBUS_NODES 247 +# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address + +// 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); + void scan_device(uint8_t node_id, + uint8_t start_reg, + uint8_t end_reg); + void scan_modbus(); + uint16_t readRegisterWait(uint16_t address); + void writeRegister(uint16_t address, + uint16_t value); + +private: + + taskIndex_t _taskIndex = INVALID_TASK_INDEX; + struct ModbusDEVICE_struct *_modbusDevice = nullptr; + 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 c2630004a4..3a4fc86feb 100644 --- a/src/src/WebServer/ESPEasy_WebServer.cpp +++ b/src/src/WebServer/ESPEasy_WebServer.cpp @@ -340,9 +340,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 4599572681..924f7aa6ec 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() { @@ -81,7 +84,7 @@ void handle_interfaces() { navMenuIndex = MENU_INDEX_INTERFACES_I2C; #elif FEATURE_SPI navMenuIndex = MENU_INDEX_INTERFACES_SPI; -#elif FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#elif FEATURE_MODBUS_FAC navMenuIndex = MENU_INDEX_INTERFACES_MODBUS; #elif FEATURE_CAN navMenuIndex = MENU_INDEX_INTERFACES_CAN; @@ -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) { @@ -167,9 +170,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; } @@ -333,11 +336,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) { @@ -539,11 +542,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 2140850cbc..df703e40bb 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)