P183 modbus RTU#5390
Conversation
|
Can you look at the timing stats for this plugin you added? Please take a look at what I did for the Eastron plugin, where I set a queue of registers to read. My intention is to make all ESPEasy plugins using modbus use this approach to share access to the same Modbus bus. Also the ESPEasySerial for (nearly) all plugins is using the same set of PCONFIG() for the serial config. |
|
I have been busy for some time. Try to pickup the comments soon. Indeed the exchange takes some time and I will look into creating a queue. Eastron was my inspiration, but I did not look in most of the details as it was mainly device specific handling. Serial settings were copied from Eastron. To share the modbus I need some more insights how the sharing is intended. I have too little experience with some details in ESPEasy. |
|
A small introduction of myself. I started as embedded software developer using mainly C, but I am for quite some time software architect and not doing professional coding anymore. I am definitely not a C++ expert. I am working for a large company building complex machines. I like to pickup smaller embedded software projects and domotica. And I really like the way ESPEasy is set up. I see some issues with the current P078 implementation. The modbus and Eastron device specific features are mixed over the various "layers". There is the "plugin" that takes care of the configuration and external data (variable, config, web representation). Then the "data_struct" to put the behavior of the plugin in a C++ friendly environment (away from .ino). And there is a "Eastron library" in SDM.cpp. If understood you well the requirements are:
The P078 implementation uses a queue that can manage multiple plugins in theory. For me it is unclear how it can differentiate between multiple links. The plugin defines the serial link, if there are multiple plugins connected it seems the last plugin that initializes defines the link properties. Is this desired behavior? The queue knows it is handling SDM messages and delivers the received holding register directly into the uservar: My proposal would be to split the code into the following classes:
Broker and link should be outside any plugin as they can be shared by multiple plugin classes. As they are both Modbus specific they can be in a single file sharing a header file. I still need to think about the details how to exchange data and fit the queue neatly in the design. What has to be in and how does it return results. The Modbus link should be able to handle both the RTU binary and ASCII format. It should be able to handle other Modbus message types. Both option for future only :-) One thing to consider: |
|
Well hats off to you sir, as you seem to have a very good idea of the layers we have right now :) Right now, I am working on another pull-request to do a complete rewrite for ESPEasy's WiFi/network implementation. When clicking "Edit" on such an adapter, you get the specific settings, very similar to how controllers and tasks are being dealt with. My next idea for a follow-up on this is to add a tab for "Interfaces" (or buses, name is not yet clear). Especially some of those interfaces which share a bus for multiple devices, need a handler to deal with all devices and pending transactions on the bus. This idea is already implemented (in a bit too specific way) by keeping track of a list of registers to read and where to put the result. So to "fix" this, I imagine there might be some new Then adding a main handler would probably be something for a next pull-request so we can think of a more generic way to manage modbus handlers. The main advantage with this is that there is no longer a collision when accessing modbus devices on the same bus and there is no active blocking wait for a reply from the device, which may take quite some time. |
|
Looks good. By the way, I am not in any hurry to push my branch. Please continue the framework and let me know where I can contribute for something modbus specific. A suggesting is to remove the word MODBUS in the event and keep is a bit more abstract like BUS_READ_RESULT. This can then be any pending bus transaction. I think that I2C could also benefit. If we add this callback trough an event and a central bus or link administration the singleton management layer will be very simple. The plugin know the bus type and index and the manager returns the associated bus object. Maybe do some admin to check how many active plugins are coupled to the bus; and do initialization termination when the first joins or the last leaves. By the way, I saw a Modbus_RTU file. Is this where the new bus management stuff should grow? |
|
Not likely that this will remain the (file) structure. For the WiFi/network rewrite, I'm introducing namespaces like The idea of having a generic bus callback/event also seems OK, as long as the bus manager/handler does keep in mind which task may be expecting specific bus responses. |
|
Sorry it is still work in progress I wanted to set aside. I had some other duties and am just picking up this project. Will update it soon with a more crisp design. Keep in mind: I am not a C++ coder, any corrections and suggestions are welcomed.
Main issues to resolve:
Is there a way to store design documentation with a plugin? |
| addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER INIT"); | ||
| P183_ModbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); | ||
|
|
||
| addLogMove(LOG_LEVEL_DEBUG, "AFTER TIMEOUT"); |
There was a problem hiding this comment.
Please check to see if we have debug code (including this LOG_LEVEL_DEBUG define) included in the build.
#ifndef BUILD_NO_DEBUG
addLogMove(LOG_LEVEL_DEBUG, F("AFTER TIMEOUT"));
#endifAnd also please use the F() macro as I showed here when handling strings.
| if (_modbus_link != nullptr) { | ||
| _modbus_link->freeTransactions(this); | ||
| ModbusMGR_singleton.disconnect(_deviceID); | ||
| _modbus_link = nullptr; |
There was a problem hiding this comment.
Why not use some (weak) std::shared_ptr like structure for this?
| String log = F("---> ModbusDevice Init: Slave address = "); | ||
| log += slaveAddress; | ||
| log += F(", This = "); | ||
| log += (ulong)this; |
There was a problem hiding this comment.
Why does this compile? And what is an ulong?
In C++ there is a special integer type which is by definition large enough to hold a pointer. This is the size_t
On our ESP boards, this happens to be 32-bit in size, so essentially it is the same as an uint32_t, which happens to be the same as unsigned int.
Though it is good practice to use size_t for these kind of things.
There was a problem hiding this comment.
Forgot about size_t :-(.
| } else { | ||
| // Invalid CRC | ||
| _state = ModbusQueueState_t::ERROR; | ||
| String log = F("---> Device callback: "); |
There was a problem hiding this comment.
No check to see if req is a valid pointer.
|
|
||
| log += req->_id; | ||
| log += F(", Message = "); | ||
| log += (uint8_t)req->_messageType; |
There was a problem hiding this comment.
Please only use those C-style casts only when needed. (e.g. when literally casting a pointer to an int to get the address is a good example)
In C++ we should use static_cast<uint8_t>(....)
There was a problem hiding this comment.
Thanks for reviewing. Please note this is a preliminary version. Don't integrate it in mega yet! I will go over the comments and fix them. But expect also more fundamental changes as it does not work as I want it to work. Debug stuff will be cleaned up before final pull request.
|
|
||
| case PLUGIN_WEBFORM_LOAD: | ||
| { | ||
| addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); |
There was a problem hiding this comment.
That magic number 3 should be based on the #define set elsewhere
| # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER | ||
| # endif | ||
| # ifndef DAT_MODBUS_INTERFACE_OFFSET | ||
| # define DAT_MODBUS_INTERFACE_OFFSET 6144 // each Modbus link config = 256 bytes, 4 max |
There was a problem hiding this comment.
This is different for ESP8266 and ESP32.
There was a problem hiding this comment.
I wonder if we should enable the Modbus link on ESP8266 at all due to the strict size limitations. But that is a different discussion. Where should this land for the ESP8266? Where can we find the layout in a human readable/understandable format? I already fort how I came to this magic number
There was a problem hiding this comment.
The 'basic settings' are stored from the start, so you can base it on the DAT_BASIC_SETTINGS_SIZE
However for ESP8266 we do have some FEATURE_NON_STANDARD_24_TASKS (not for official builds, only for some custom builds), so I would suggest to take that into consideration for the defines.
Something like this:
# ifndef DAT_MODBUS_INTERFACE_OFFSET
# ifdef FEATURE_NON_STANDARD_24_TASKS
# 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
# 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| int16_t ModbusLINK_struct::getSerialRX(void) const | ||
| { | ||
| return _easySerial != nullptr ? _easySerial->getRxPin() : -1; | ||
| return _easySerial != nullptr ? _easySerial->getRxPin() : -1; |
There was a problem hiding this comment.
Should isInitialized() be used here instead? Same for the other return values
| @@ -152,7 +145,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { | |||
|
|
|||
| while (state == ModbusResultState::Busy) { | |||
| delay(50); | |||
There was a problem hiding this comment.
Hmm, delays should be avoided like the plague, processCommands() should be called in 50/sec or 10/sec events instead
There was a problem hiding this comment.
Indeed, I am working on it. This is due to the fact that the bus takes time for a transaction. This was the reason to go for a separate link object that is triggered outside the device code. But for some functions the device shall return the result of the Modbus transaction.
There was a problem hiding this comment.
Sure, but then the internal state of the object should be like "X_pending".
Some Modbus devices (e.g. SenseAir S8 and older devices) easily take upto 200 msec or more to give a reply.
This is way too long to literally wait and this is also why a sommand should be put into a queue and get notified when there is a reply.
Either via some PLUGIN_xyz function call or with some function pointer kept along with the pending request.
On a shared bus you simply can't expect to be immediately serviced as there may be pending calls already sent. At best you could set some priority like "this has to be the first in line"
I got the Modbus engine running. Plugins can use de Modbus_device to queue transactions to the Modbus_link object. The singleton Modbus_manager manages all links and implements toe link configuration web page. Needs some cleaning, but it works. Next task is to get rid of plugins waiting for the transaction result. Currently this only works for the plugin_read and causes a delay of one full sample time. I want to implement a "callback" using the event mechanism. Can we design a generic event for responses from autonomous modules like the Modbus link? I can imagine there are other processes that may take time to generate a value. Using a PLUGIN_MODBUS_READ_RESULT sounds very specific for the Modbus. |
|
We currently have the A typical work flow can be something like this:
N.B. the task does 'know' it is a modbus task, so there is no ambiguity in what's intended with the |
|
I will give it a try with PLUGIN_TASKTIMER_IN. Let's do some research about the event mechanism and structure. |
You can start by looking at |
Co-authored-by: Copilot <copilot@github.com>
| addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); | ||
| return false; | ||
| } | ||
| UserVar.setFloat(event->TaskIndex, outputIndex, event->Par1); // Update the user variable with the value read from Modbus |
There was a problem hiding this comment.
The value may not always be a float.
So maybe we could have a look at how to deal with those.
However a 16-bit value can still be stored in a float without loosing bits.
|
I just approved running your GH Actions build, just to see if there are any build issues so far. |
|
OK, that didn't run for long.... As you may not yet know... For ESP8266 builds, all .cpp files are concatenated into a single file (well all except the new
However this has a major consequence, where you need to make the include path a bit longer as the concatenated file will be in a different directory from the .h files. So the |
|
|
||
| # include "../Helpers/Modbus_device.h" | ||
| # include "../Helpers/Modbus_mgr.h" | ||
| # include "Modbus_device.h" |
There was a problem hiding this comment.
Prefix include path with ../Helpers/
| #if FEATURE_MODBUS_FAC | ||
|
|
||
| # include "Modbus_device.h" | ||
| # include "Modbus_link.h" |
There was a problem hiding this comment.
Here also missing ../Helpers/ in the includes
| #if FEATURE_MODBUS_FAC | ||
|
|
||
| # include <ESPeasySerial.h> | ||
| # include "Modbus_mgr.h" |
There was a problem hiding this comment.
Here also missing ../Helpers/ in the include
Initial version of a simple Modbus RTU plugin. This plugin handles a Modbus device as a set of holding registers. Up to 4 holding registers can be read into the 4 values of the plugin. The plugin number is 183 as agreed elsewhere.