Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[modbus-tcp / s7] Reconnect after disconnection via ethernet #1781

Open
baraldi-l-proxaut opened this issue Sep 23, 2024 · 7 comments
Open
Labels
awaiting-feedback This label is applied when an issue has been opened and we need more information from the issuer. Ethernet/IP https://plc4x.apache.org/users/protocols/eip.html java Pull requests that update Java code Modbus https://plc4x.apache.org/users/protocols/modbus.html S7 https://plc4x.apache.org/users/protocols/s7.html

Comments

@baraldi-l-proxaut
Copy link

I wrote a Camel route that reads signals from a Phoenix I/O via modbus protocol every 500 milliseconds.
The I/o Phoenix is connected to my laptop via ethernet cable.

Initially the connection was opened and closed for each read instruction, but subsequently I moved to a managed pool of connections to reuse the same connection.
In this way I managed to reduce reading times and avoided saturating the PLC connections.
Before, opening and closing the connection, took me a total of 1 (max 2) seconds to read each reading.
With a managed pool of already open connections, I can read within the expected 500 ms.

The problem I'm facing now is that if the ethernet cable is disconnected, an Exception is thrown that I can't catch and subsequently the connection to that IP is no longer usable until the route is restarted.

Do you have any idea on how to catch the java.net.SocketException Connection reset?

This is the java.net.SocketException thrown:

2024-09-23T18:18:44.111+02:00 WARN 29312 --- [ntLoopGroup-3-1] i.netty.channel.DefaultChannelPipeline : An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. java.net.SocketException: Connection reset at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394) ~[na:na] at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:426) ~[na:na] at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:254) ~[netty-buffer-4.1.97.Final.jar:4.1.97.Final] at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) ~[netty-buffer-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:357) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.97.Final.jar:4.1.97.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.97.Final.jar:4.1.97.Final] at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.97.Final.jar:4.1.97.Final] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.97.Final.jar:4.1.97.Final] at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

This is the Camel route:

fromF("timer:%s?period=%d", tagGroupName, interval)
        .routeId(tagGroupName)
        .process(exchange -> {
            Map<String, Object> result = tagReaderService.directRead(tagGroupTriggerData.getHost(),
                    tagGroupTriggerData.getPort(),
                    DevicePlugin.MODBUS,
                    tagGroupTriggerData.getTagList());
            exchange.getIn().setBody(result);
        })
        .marshal().json(JsonLibrary.Jackson)
        .process(exchange -> {
            String body = exchange.getIn().getBody(String.class);
            tagReaderService.put(tagGroupName, body);
        });

This the tagReaderService.directRead method:

    public Map<String, Object> directRead(final String host, final Integer port, final DevicePlugin devicePlugin,
                                          final List<TagTriggerData> tags) throws PlcConnectionException, ExecutionException, InterruptedException {
        if (devicePlugin == DevicePlugin.OMRON_FINS_UDP) {
            Map<String, Object> result = new HashMap<>(tags.size());
            for (var tagDTO : tags) {
                OmronReadRequest request = new OmronReadRequest();
                request.setAddress(tagDTO.getAddress());
                OmronReadResponse response = producerTemplate.requestBody("direct:read-omron-single", request, OmronReadResponse.class);
                result.put(tagDTO.getName(), response.getResponse());
            }
            return result;
        } else {
            return plcPoolConnectionService.readValues(host, port, devicePlugin, tags);
        }
    }

This is the plcPoolConnectionService.readValues invoked:

    public Map<String, Object> readValues(final String host, final Integer port, final DevicePlugin devicePlugin, final List<TagTriggerData> tags) throws PlcConnectionException, ExecutionException, InterruptedException {
        ConnectionWrapper connection = getConnectionWrapper(host, port, devicePlugin);
        return connection.readValues(tags);
    }

This is the getConnectionWrapper:

    private ConnectionWrapper getConnectionWrapper(final String host, final Integer port, final DevicePlugin devicePlugin) throws PlcConnectionException {
        String key = host + ofNullable(port).map(Object::toString).orElse("");
        ConnectionWrapper connection = connections.get(key);
        if (connection == null) {
            connection = createNewConnection(host, port, devicePlugin);
            connections.put(key, connection);
        }
        return connection;
    }

And here i create the connection:

    private ConnectionWrapper createNewConnection(final String host, final Integer port, final DevicePlugin devicePlugin) throws PlcConnectionException {
        return switch (devicePlugin) {
            case S7COMM -> ConnectionWrapper.s7Connection(driverManager, host);
            case MODBUS -> ConnectionWrapper.modbusTcpConnection(driverManager, host, port);
            default -> throw new RuntimeException("Unsupported device plugin: " + devicePlugin);
        };
    }

    public static ConnectionWrapper modbusTcpConnection(final PlcDriverManager driverManager, final String host, final Integer port) throws PlcConnectionException {
        String connectionString = String.format(MODBUS_TCP_CONNECTION_STRING, host);
        return new ConnectionWrapper(driverManager, connectionString);
    }

    private ConnectionWrapper(final PlcDriverManager driverManager, final String connectionString) throws PlcConnectionException {
        connection = driverManager.getConnection(connectionString);
    }

TagGroupRouteBuilder.zip

Feel free to write to me for any information and if there are any points to explore further.
Thank you,
Luca

@baraldi-l-proxaut baraldi-l-proxaut changed the title [modbus-tcp / s7] Reconnect after disconnection [modbus-tcp / s7] Reconnect after disconnection via ethernet Sep 23, 2024
@chrisdutz
Copy link
Contributor

Hi Luca,

first off all ... do you know about our connection-cache component? It sounds like this is exactly what you would have been looking for before you built your own ... https://plc4x.apache.org/users/tools/connection-cache.html

And regarding the "pulling the plug" or other network or plc-releated outages ... admittedly this is one of the things we need to harden all of our drivers a bit more for.

My main problem is: time ... I currently only have 4 hours per week to work on stuff like this.

Perhaps someone else here will be able to jump in?

@glcj
Copy link
Contributor

glcj commented Sep 24, 2024

How is it?

For S7 the reconnection is parameterizable in the URL. You can even work with two connections (CPU + CP) and it should reconnect.

Kind regards,

@baraldi-l-proxaut
Copy link
Author

Hi @glcj
I was just thinking of a solution like this, For each PLC I would open 2 connections.
One is a connection, that read data every 500 milliseconds (manageable by configurable parameter), this one takes care of reading the data from the PLC and a second connection managed by a method that always remains active and which in theory should capture the SocketException that Camel currently does not manages.
When the SocketException is catched i will try to reopen the connection before the Camel route start.

@baraldi-l-proxaut
Copy link
Author

Hi @chrisdutz ,
my colleague tried to integrate the connection cache, but now I'm going more into the topic of the cache. Thank you :)

For disconnection problems, we are thinking about how we can handle this type of inconvenience. Maybe with a second Camel route.
We are using plc4x in a factory data reading project. My colleague and I can contribute but I don't know how skilled we are to be able to contribute effectively, I'm afraid that we don't have enough experience.

These days I'm trying to understand how this type of problem can be solved and maybe I'll keep this issue updated.

Luca

@chrisdutz
Copy link
Contributor

Well ... we recently added a Listener interface to the PlcConnection ... ConnectionStateListener ... it's the idea to pass in such a listener and the core of PLC4X should call that if a connection disconnects. However, as I mentioned before ... I do think we need to test all of our drivers a bit more with such stuff.

@glcj
Copy link
Contributor

glcj commented Sep 24, 2024

Hi,

The S7 driver already implements the feature you mention and others, supported by the S7-300 or S7-400.

It also implements support for the ConnectionListener interface as Chris points out, so you can know when the driver is disconnected/connected.

Kind regards,

@ottlukas ottlukas added java Pull requests that update Java code S7 https://plc4x.apache.org/users/protocols/s7.html Modbus https://plc4x.apache.org/users/protocols/modbus.html Ethernet/IP https://plc4x.apache.org/users/protocols/eip.html labels Sep 25, 2024
@chrisdutz
Copy link
Contributor

Soooo ... not quite sure what should be done here...

@chrisdutz chrisdutz added the awaiting-feedback This label is applied when an issue has been opened and we need more information from the issuer. label Oct 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting-feedback This label is applied when an issue has been opened and we need more information from the issuer. Ethernet/IP https://plc4x.apache.org/users/protocols/eip.html java Pull requests that update Java code Modbus https://plc4x.apache.org/users/protocols/modbus.html S7 https://plc4x.apache.org/users/protocols/s7.html
Projects
None yet
Development

No branches or pull requests

4 participants