macchina-io / macchina.io

macchina.io EDGE is a powerful C++ and JavaScript SDK for edge devices, multi-service IoT gateways and connected embedded systems.
https://macchina.io
GNU General Public License v3.0
512 stars 152 forks source link

TCP server with multiple TCP client connections #110

Closed zaleksa closed 2 years ago

zaleksa commented 2 years ago

Hi,

I am trying to implement Modbus TCP server with multiple Modbus TCP client connections which can access Service Registry references' data.

User implementation class makes instance of TCPPort class which handles connect/disconnect receive/send to/from chosen IP address/port. On connect, TCPPort class makes instance of TCPServer class with associated ServerSocket object.

I have followed sample in macchina.io/platform/Net/samples/tcpserver and used the same mechanism of TCPFactory/TCPServerDispatcher/TCPServerConnectionFactoryImpl/ClientConnection classes.

ClientConnection class has StreamSocket object which handles data from/to TCP client.

I have succeed to connect with multiple TCP clients and got their data requests in associated StreamSockets.

My problem is how to get data from ClientConnection's StreamSockets in TCPPort class object. Objects of these classes are in different threads, so I thought that using of Notifications or NotificationQueue will be the right way.

I kindly ask you to give your advice before I go much deeper in the wrong way.

Any example will be great help.

Best regards,

Aleksandar

aleks-f commented 2 years ago

NotificationQueue s fine, and here's the example.

But I wonder what is the rationale for using TCPPort directly, when there is ModbusMaster service?

zaleksa commented 2 years ago

Thank you for the response.

ModbusMaster service is intended for reading Modbus clients (slave devices) and I already used it to implement Modbus Master poll application (Serial RTU/TCP). Correct me if I am wrong.

In this case I need opposite, ModbusSlave service (Serial RTU/TCP server) so it can accept Modbus requests and provide Modbus responses. I have implemented Serial RTU server, and next is the TCP server.

ModbusMasterImpl class was my starting point for writing ModbusSlaveImpl class and so forth.

I will consider the example and if there are any questions I will be free to ask them.

Best regards,

Aleksandar

aleks-f commented 2 years ago

I'm having trouble understanding the architecture description. A TCP ModbusSlave is a TCP server, to which a ModbusMaster (via TCPPort - a TCP client) connects, right? What exactly does a TCPPort in the ModbusSlave implementation do?

In any case, one thing I would recommend to keep in mind is that Poco TCPServer model uses a thread per connection.

BTW, we would definitely benefit from having ModbusSlave in macchina, which would allow us to write self-contained Modbus unit tests. Is there any chance you could open source the ModbusSlave implementation? I would be willing to assist with it.

obiltschnig commented 2 years ago

I don't think re-using TCPPort for a Modbus server (slave) is a good idea. TCPPort is built such that it actively opens the connection (and handles auto reconnect). Also, TCPPort is designed to work with ModbusMaster. The only things I'd use from the Modbus library are PDUReader and PDUWriter.

Then create a ModbusSlaveConnection class, derived from Poco::Net::TCPServerConnection. In that class, implement sendFrame(), decodeFrame() and receiveFrame() methods (take the implementations from TCPPort as basis, but leave out the reconnection logic), and in your ModbusSlaveConnection::run() method, you'll run a loop calling poll(), and if data is available, call receiveFrame() and handle the request. There will be one thread per connection, but typically there is a very limited number of connections to a Modbus device, so this is not an issue.

I guess a "generic" Modbus server/slave would be hard to do, but you could at least build a base class for handling all the different Modbus messages (let's call it ModbusSlave). One could then re-use the implementation by passing different implementations of ModbusSlave to the ModbusSlaveConnection class.

Ideally, the slave implementation for Modbus/RTU should use the same ModbusSlave interface.

zaleksa commented 2 years ago

Thank you, I think last answer is exactly what I needed.

Let me describe my concept again:

I've built ModbusSlave class which handles all types of Modbus messages (similar to ModbusMaster, request and responses messages are reversed). PDUReader and PDUWriter classes are used for formatting requests/responses in structures defined in ModbusSlave class. These classes are also adapted according to Modbus slave request/responses.

Next, there is ModbusSlaveImpl template class (similar to ModbusMasterImpl) which implements ModbusSlave class on chosen port (Serial RTU/TCP) and processes frames (Modbus Function requests and makes responses).

For Serial RTU port I've reused RTUPort class and its sendFrame(), receiveFrame(), decodeFrame(), poll() methods.

At start, in BundleActivator, serial port is opened and its pointer is passed to ModbusSlaveImpl object.

So far this works excellent.

For TCP port (interface) I have to built new class with same methods (sendFrame(), receiveFrame(), decodeFrame(), poll()) in order to be able to use the same ModbusSlaveImpl class. I agree that TCPPort is not right class, but I use it as a start point. I also agree with suggestion to create ModbusSlaveConnection class, derived from TCPServerConnection and implement sendFrame(), receiveFrame(), decodeFrame(), poll() methods. (This suggestion is key point for me, so in this way I don"t need NotificatinQueue class)

So, in BundleActivator I will start TCPServer with TCPServerConnectionFactoryImpl < ModbusSlaveConnection > and waits for client connection. When the client connects, ModbusSlaveImpl object is instantiated with 'this' pointer as pointer to 'port' similar to serial port.

Does this makes sense? Does TCPServer need reconnect mechanism or it handles by itself?

Thank you for your time and willingness to answer my questions.

Aleksandar

zaleksa commented 2 years ago

I'm having trouble understanding the architecture description. A TCP ModbusSlave is a TCP server, to which a ModbusMaster (via TCPPort - a TCP client) connects, right? What exactly does a TCPPort in the ModbusSlave implementation do?

TCPPort should be actually ModbusSlaveConnection class, derived from Poco::Net::TCPServerConnection as Günter Obiltschnig proposed.

In any case, one thing I would recommend to keep in mind is that Poco TCPServer model uses a thread per connection.

I count on this so Modbus TCP server can accept multiple connections.

BTW, we would definitely benefit from having ModbusSlave in macchina, which would allow us to write self-contained Modbus unit tests. Is there any chance you could open source the ModbusSlave implementation? I would be willing to assist with it.

In this moment ModbusSlave isn't written as service but as self-contained bundle containing application level which instantiates ModbusSlaveImpl with associated Serial port/TCPServer. ModbusSlaveImpl class, beside implementing ModbusSlave interface, additionally handles read/write data (from/to Service Registry) and does type conversions.

Maybe it would be better to split this into service and application bundle. In this way ModbusSlave as service would have Poco::BasicEvent to notify application bundle that there is Modbus request, application bundle would process it and call ModbusSlave method to send Modbus response back to caller. Application bundle would instantiate service and associated Serial port/TCPServer. These are my thoughts about ModbusSlave as service.

Aleksandar

zaleksa commented 2 years ago

I've followed Günter Obiltschnig advice and created ModbusSlaveConnection class, derived from Poco::Net::TCPServerConnection. In that class, I've implemented sendFrame(), decodeFrame() and receiveFrame() methods, and in ModbusSlaveConnection::run() method, run a loop calling poll(), and if data is available, call receiveFrame() and handle the request.

Also, I've built ModbusSlave class which handles different Modbus messages.

In BundleActivator class, TCPServer is started on dedicated portNo:

typedef Poco::Net::TCPServerConnectionFactoryImpl < ModbusSlaveConnection > TCPFactory; _pModbusTcpServer = new Poco::Net::TCPServer(new TCPFactory(), portNo); _pModbusTcpServer->start();

I succeed to connect multiple Modbus clients and get their requests.

For Modbus responses I need some parameters from BundleActivator class and I don't know how to pass them to ModbusSlaveConnection class as object of latter is instantiated on client connection. If this is possible then I could use the same ModbusSlaveImpl implementation of ModbusSlave class as I do for Modbus Serial RTU slave.

Also, is it possible to get BundleContext of bundle in instantiated object without passing pointer to it?

Aleksandar

aleks-f commented 2 years ago

For Modbus responses I need some parameters from BundleActivator class and I don't know how to pass them to ModbusSlaveConnection class as object of latter is instantiated on client connection. If this is possible then I could use the same ModbusSlaveImpl implementation of ModbusSlave class as I do for Modbus Serial RTU slave.

Also, is it possible to get BundleContext of bundle in instantiated object without passing pointer to it?

AFAIK no, but you could inherit your own TCPFactory and pass it the parameters you then pass to the connections as they are created.

zaleksa commented 2 years ago

I followed advice of Aleksandar Fabijanic and inherited my own TCPServerConnectionFactoryImpl from TCPServerConnectionFactory class. This way I can pass parameters to ModbusSlaveConnection class.

Finally I can use the same ModbusSlaveImpl implementation of ModbusSlave class for Serial RTU slave and TCP server (for each ModbusSlaveConnection instance). The ModbusSlaveImpl class is not Runnable but the RTUPort and ModbusSlaveConnection classes are.

Thanks very much to both, Aleksandar Fabijanic and Günter Obiltschnig.

p.s. Regarding to Aleksandar Fabijanic comment:

"BTW, we would definitely benefit from having ModbusSlave in macchina, which would allow us to write self-contained Modbus unit tests. Is there any chance you could open source the ModbusSlave implementation? I would be willing to assist with it."

As you probably know, I work in DECODE company, Belgrade, Serbia, so this bundle is intended for commercial use (we have licensed version of macchina.io). Maybe you can talk to DECODE management, but I'm sure that we can give source code of ModbusSlave class (Modbus request/response messages structures) and ModbusSlaveImpl class (without data conversion and read/write to ServiceRegistry references).

Best regards, Aleksandar