This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 089c1994e634 CAMEL-25617: camel-plc4x - Upgrade to PLC4X 1.0.0 (#28387)
089c1994e634 is described below

commit 089c1994e634e8ec1d1352733b3266d2c0f8ae27
Author: Serdar Gökay <[email protected]>
AuthorDate: Tue Oct 27 09:54:07 2026 +0300

    CAMEL-25617: camel-plc4x - Upgrade to PLC4X 1.0.0 (#28387)
    
    Upgrades camel-plc4x from PLC4X 0.13.1 to 1.0.0, which requires Java 21 and 
removed the plc4j-scraper module the triggered consumer was built on.
    
    The triggered consumer is now a Camel ScheduledPollConsumer on the 
endpoint's connection. It reads the trigger and the data tags in one request 
and keeps the rising-edge semantics, recovers after a failed read, and leaves 
no scraper thread pools behind. A non-boolean trigger or a read without an 
answer within 5 s fails the poll.
    
    A lost connection is closed and replaced instead of reconnected, since the 
1.0.0 S7 and EtherNet/IP connections cannot connect again. The connection is 
replaced under the endpoint lock, a live connection is kept on restart, a lost 
one is closed on stop, and a request without a connection fails with 
PlcConnectionException instead of an NPE. The driver no longer receives Camel's 
own endpoint options in its connection string.
    
    The upgrade guide covers the PLC4X 1.0.0 changes: Java 21, the array 
notation of tag addresses, the removed s7-light driver, renamed connection 
parameters and the polled trigger.
    
    Closes #28387
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/docs/plc4x-component.adoc |  35 ++
 components/camel-plc4x/pom.xml                     |   5 -
 .../camel-plc4x/src/main/docs/plc4x-component.adoc |  35 ++
 .../apache/camel/component/plc4x/Constants.java    |   1 -
 .../camel/component/plc4x/Plc4XConsumer.java       |  74 +---
 .../camel/component/plc4x/Plc4XEndpoint.java       | 142 ++++++--
 .../component/plc4x/Plc4XPollingConsumer.java      |  12 +-
 .../camel/component/plc4x/Plc4XProducer.java       |   2 +-
 .../component/plc4x/Plc4XTriggeredConsumer.java    | 150 ++++++++
 .../apache/camel/component/plc4x/MockDriver.java   | 258 ++++++++++---
 .../component/plc4x/Plc4XConsumerRouteTest.java    | 397 +++++++++++++++++++++
 .../component/plc4x/Plc4XConsumerStopTest.java     |  67 ----
 .../camel/component/plc4x/Plc4XConsumerTest.java   |  90 -----
 .../camel/component/plc4x/Plc4XEndpointTest.java   |  25 +-
 .../camel/component/plc4x/Plc4XFailureTest.java    |   4 -
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  44 +++
 parent/pom.xml                                     |   2 +-
 17 files changed, 1016 insertions(+), 327 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc
index 294a7b699882..37f10570ac51 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc
@@ -91,6 +91,41 @@ no PLC. With a real PLC, the driver in the URI and the 
addresses of the tags fol
             message: "Tags read: ${body}"
 ----
 
+With the `trigger` option, the consumer reads the tags each time a boolean 
trigger tag rises. It polls the trigger tag
+every `period` milliseconds on the connection of the endpoint, starting when 
the route starts; `period` is required
+with `trigger` and must be positive. On a rising edge of the trigger, from 
`false` (or from no value read yet) to
+`true`, the consumer delivers one exchange whose body is a 
`Map<String,Object>` of the tags, read in the same request
+as the trigger. The trigger tag is not part of the body; it is read under the 
reserved name `TRIGGER_VAR`, which no tag
+may use. While the trigger stays `true`, no further exchange is delivered.
+
+A poll that fails, for example because a tag cannot be read, the PLC cannot be 
reached or does not answer within five
+seconds, or the trigger reads as a value that is not a boolean (such as a 
number or an array), is reported to the
+exception handler of the consumer (or to the error handler of the route with 
`bridgeErrorHandler=true`), and polling
+continues.
+The trigger value read before the failure is forgotten, so a trigger that is 
`true` at the first successful poll after
+a failure counts as a rising edge. Automatic recovery after a lost connection 
needs `autoReconnect=true`: a poll then
+replaces a lost connection with a new one. With the default 
`autoReconnect=false`, the endpoint stops on the first lost
+connection and does not recover. Likewise, a connection that cannot be set up 
when the route starts is retried by the
+polls with `autoReconnect=true`, and fails the start of the route without it.
+
+The following route reads a register of a Modbus TCP device each time coil 1 
goes from `false` to `true`, checking the
+coil every 100 milliseconds:
+
+[source,yaml]
+----
+- route:
+    from:
+      uri: plc4x:modbus-tcp://192.168.0.10:502
+      parameters:
+        trigger: "coil:1"
+        period: 100
+        autoReconnect: true
+        tag.counter: "holding-register:2:INT"
+      steps:
+        - log:
+            message: "Counter read on trigger: ${body}"
+----
+
 === Polling Consumer
 
 The polling consumer supports consecutive reading. The input and output are 
the same as for the regular consumer.
diff --git a/components/camel-plc4x/pom.xml b/components/camel-plc4x/pom.xml
index 32f826e824f0..0bea7af6f9eb 100644
--- a/components/camel-plc4x/pom.xml
+++ b/components/camel-plc4x/pom.xml
@@ -44,11 +44,6 @@
             <artifactId>plc4j-api</artifactId>
             <version>${plc4x-version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.apache.plc4x</groupId>
-            <artifactId>plc4j-scraper</artifactId>
-            <version>${plc4x-version}</version>
-        </dependency>
 
         <!-- Include all drivers -->
         <dependency>
diff --git a/components/camel-plc4x/src/main/docs/plc4x-component.adoc 
b/components/camel-plc4x/src/main/docs/plc4x-component.adoc
index 294a7b699882..37f10570ac51 100644
--- a/components/camel-plc4x/src/main/docs/plc4x-component.adoc
+++ b/components/camel-plc4x/src/main/docs/plc4x-component.adoc
@@ -91,6 +91,41 @@ no PLC. With a real PLC, the driver in the URI and the 
addresses of the tags fol
             message: "Tags read: ${body}"
 ----
 
+With the `trigger` option, the consumer reads the tags each time a boolean 
trigger tag rises. It polls the trigger tag
+every `period` milliseconds on the connection of the endpoint, starting when 
the route starts; `period` is required
+with `trigger` and must be positive. On a rising edge of the trigger, from 
`false` (or from no value read yet) to
+`true`, the consumer delivers one exchange whose body is a 
`Map<String,Object>` of the tags, read in the same request
+as the trigger. The trigger tag is not part of the body; it is read under the 
reserved name `TRIGGER_VAR`, which no tag
+may use. While the trigger stays `true`, no further exchange is delivered.
+
+A poll that fails, for example because a tag cannot be read, the PLC cannot be 
reached or does not answer within five
+seconds, or the trigger reads as a value that is not a boolean (such as a 
number or an array), is reported to the
+exception handler of the consumer (or to the error handler of the route with 
`bridgeErrorHandler=true`), and polling
+continues.
+The trigger value read before the failure is forgotten, so a trigger that is 
`true` at the first successful poll after
+a failure counts as a rising edge. Automatic recovery after a lost connection 
needs `autoReconnect=true`: a poll then
+replaces a lost connection with a new one. With the default 
`autoReconnect=false`, the endpoint stops on the first lost
+connection and does not recover. Likewise, a connection that cannot be set up 
when the route starts is retried by the
+polls with `autoReconnect=true`, and fails the start of the route without it.
+
+The following route reads a register of a Modbus TCP device each time coil 1 
goes from `false` to `true`, checking the
+coil every 100 milliseconds:
+
+[source,yaml]
+----
+- route:
+    from:
+      uri: plc4x:modbus-tcp://192.168.0.10:502
+      parameters:
+        trigger: "coil:1"
+        period: 100
+        autoReconnect: true
+        tag.counter: "holding-register:2:INT"
+      steps:
+        - log:
+            message: "Counter read on trigger: ${body}"
+----
+
 === Polling Consumer
 
 The polling consumer supports consecutive reading. The input and output are 
the same as for the regular consumer.
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Constants.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Constants.java
index d973e4e37ddd..6f995dde53fa 100644
--- 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Constants.java
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Constants.java
@@ -21,7 +21,6 @@ public final class Constants {
     public static final String FIELD_NAME_HEADER = "fieldName";
     public static final String FIELD_QUERY_HEADER = "fieldQuery";
     public static final String TRIGGER = "TRIGGER_VAR";
-    public static final String PLC_NAME = "PLC";
 
     private Constants() {
         throw new IllegalStateException("Utility class!");
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XConsumer.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XConsumer.java
index 581191a7929d..bb4d8dbe7686 100644
--- 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XConsumer.java
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XConsumer.java
@@ -16,10 +16,6 @@
  */
 package org.apache.camel.component.plc4x;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -30,33 +26,20 @@ import org.apache.camel.Processor;
 import org.apache.camel.support.DefaultConsumer;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
-import org.apache.plc4x.java.scraper.config.JobConfigurationImpl;
-import org.apache.plc4x.java.scraper.config.ScraperConfiguration;
-import 
org.apache.plc4x.java.scraper.config.triggeredscraper.ScraperConfigurationTriggeredImpl;
-import org.apache.plc4x.java.scraper.exception.ScraperException;
-import org.apache.plc4x.java.scraper.triggeredscraper.TriggeredScraperImpl;
-import 
org.apache.plc4x.java.scraper.triggeredscraper.triggerhandler.collector.TriggerCollector;
-import 
org.apache.plc4x.java.scraper.triggeredscraper.triggerhandler.collector.TriggerCollectorImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class Plc4XConsumer extends DefaultConsumer {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(Plc4XConsumer.class);
 
-    private final Map<String, String> tags;
-    private final String trigger;
     private final Plc4XEndpoint plc4XEndpoint;
 
     private ScheduledExecutorService executorService;
     private ScheduledFuture<?> future;
-    private TriggeredScraperImpl scraper;
-    private TriggerCollector collector;
 
     public Plc4XConsumer(Plc4XEndpoint endpoint, Processor processor) {
         super(endpoint, processor);
         this.plc4XEndpoint = endpoint;
-        this.tags = endpoint.getTags();
-        this.trigger = endpoint.getTrigger();
     }
 
     @Override
@@ -82,20 +65,13 @@ public class Plc4XConsumer extends DefaultConsumer {
             }
             doStop();
         }
-        if (trigger == null) {
-            startUnTriggered();
-        } else {
-            startTriggered();
-        }
+        startUnTriggered();
     }
 
     private void startUnTriggered() {
         PlcReadRequest request;
         try {
             plc4XEndpoint.reconnectIfNeeded();
-            if (plc4XEndpoint.connection == null) {
-                throw new PlcConnectionException("Cannot establish 
connection");
-            }
             request = plc4XEndpoint.buildPlcReadRequest();
         } catch (Exception e) {
             if (LOGGER.isTraceEnabled()) {
@@ -111,11 +87,7 @@ public class Plc4XConsumer extends DefaultConsumer {
         future = executorService.schedule(() -> 
request.execute().thenAccept(response -> {
             try {
                 Exchange exchange = plc4XEndpoint.createExchange();
-                Map<String, Object> rsp = new HashMap<>();
-                for (String field : response.getTagNames()) {
-                    rsp.put(field, response.getObject(field));
-                }
-                exchange.getIn().setBody(rsp);
+                exchange.getIn().setBody(Plc4XEndpoint.tagValues(response));
                 getProcessor().process(exchange);
             } catch (Exception e) {
                 getExceptionHandler().handleException(e);
@@ -123,39 +95,6 @@ public class Plc4XConsumer extends DefaultConsumer {
         }), 500, TimeUnit.MILLISECONDS);
     }
 
-    private void startTriggered() throws ScraperException {
-        ScraperConfiguration configuration = getScraperConfig(tags);
-        collector = new 
TriggerCollectorImpl(plc4XEndpoint.getPlcDriverManager());
-
-        scraper = new TriggeredScraperImpl(configuration, (job, alias, 
response) -> {
-            try {
-                plc4XEndpoint.reconnectIfNeeded();
-
-                Exchange exchange = plc4XEndpoint.createExchange();
-                exchange.getIn().setBody(response);
-                getProcessor().process(exchange);
-            } catch (PlcConnectionException e) {
-                if (LOGGER.isTraceEnabled()) {
-                    LOGGER.warn("Unable to reconnect, skipping request", e);
-                } else {
-                    LOGGER.warn("Unable to reconnect, skipping request");
-                }
-            } catch (Exception e) {
-                getExceptionHandler().handleException(e);
-            }
-        }, collector);
-        scraper.start();
-        collector.start();
-    }
-
-    private ScraperConfigurationTriggeredImpl getScraperConfig(Map<String, 
String> tagList) {
-        String config = "(TRIGGER_VAR," + plc4XEndpoint.getPeriod() + ",(" + 
plc4XEndpoint.getTrigger() + ")==(true))";
-        List<JobConfigurationImpl> job = Collections.singletonList(
-                new JobConfigurationImpl("PLC4X-Camel", config, 0, 
Collections.singletonList(Constants.PLC_NAME), tagList));
-        Map<String, String> source = 
Collections.singletonMap(Constants.PLC_NAME, plc4XEndpoint.getUri());
-        return new ScraperConfigurationTriggeredImpl(source, job);
-    }
-
     @Override
     protected void doStop() throws Exception {
         // First stop the polling process
@@ -167,15 +106,6 @@ public class Plc4XConsumer extends DefaultConsumer {
             
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(executorService);
             executorService = null;
         }
-        // a scraper that is not stopped keeps reading the PLC and sending 
exchanges to the route
-        if (scraper != null) {
-            scraper.stop();
-            scraper = null;
-        }
-        if (collector != null) {
-            collector.stop();
-            collector = null;
-        }
         super.doStop();
     }
 
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java
index 55fd8e923881..c541fb899801 100644
--- 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.component.plc4x;
 
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import org.apache.camel.Category;
@@ -28,17 +31,20 @@ import org.apache.camel.spi.BodyType;
 import org.apache.camel.spi.BodyType.Carrier;
 import org.apache.camel.spi.BodyTypes;
 import org.apache.camel.spi.EndpointServiceLocation;
+import org.apache.camel.spi.EndpointUriFactory;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.UriEndpoint;
 import org.apache.camel.spi.UriParam;
 import org.apache.camel.spi.UriPath;
 import org.apache.camel.support.DefaultEndpoint;
 import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.URISupport;
 import org.apache.plc4x.java.DefaultPlcDriverManager;
 import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.exceptions.PlcIncompatibleDatatypeException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
 import org.apache.plc4x.java.api.messages.PlcWriteRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,8 +67,11 @@ import org.slf4j.LoggerFactory;
 public class Plc4XEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(Plc4XEndpoint.class);
 
+    // how long the producer and the triggered consumer wait for the PLC to 
answer a request
+    static final long REQUEST_TIMEOUT_MILLIS = 5000;
+
     protected DefaultPlcDriverManager plcDriverManager;
-    protected PlcConnection connection;
+    protected volatile PlcConnection connection;
 
     @UriPath
     @Metadata(required = true, description = "PLC4X connection string for the 
connection to the target")
@@ -82,6 +91,8 @@ public class Plc4XEndpoint extends DefaultEndpoint implements 
EndpointServiceLoc
     private boolean autoReconnect;
 
     private String uri;
+    // the connection string handed to the driver: the uri without the options 
of this endpoint
+    private String connectionString;
 
     public Plc4XEndpoint(String endpointUri, Component component) {
         super(endpointUri, component);
@@ -133,7 +144,13 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
      */
     public void setupConnection() throws PlcConnectionException {
         try {
-            connection = plcDriverManager.getConnection(this.uri);
+            synchronized (this) {
+                // a restarted route or another consumer or producer of the 
endpoint keeps a live connection
+                if (!isConnected()) {
+                    closeConnection();
+                    connection = 
plcDriverManager.getConnection(connectionString);
+                }
+            }
             if (!isConnected()) {
                 reconnectIfNeeded();
             }
@@ -158,24 +175,34 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
      * @throws PlcConnectionException If reconnect failed and auto-reconnect 
is turned on
      */
     public void reconnectIfNeeded() throws PlcConnectionException {
-        if (isConnected()) {
-            LOGGER.trace("No reconnect needed, already connected");
-        } else if (autoReconnect && connection == null) {
-            connection = plcDriverManager.getConnection(uri);
-            LOGGER.debug("Successfully reconnected");
-        } else if (autoReconnect && !isConnected()) {
-            if (connection != null) {
-                connection.connect();
+        // the producer and the consumers of the endpoint share the 
connection: one of them replaces it at a time
+        synchronized (this) {
+            if (isConnected()) {
+                LOGGER.trace("No reconnect needed, already connected");
+                return;
             }
-            // If reconnection fails without Exception, reset connection
-            if (!isConnected()) {
-                LOGGER.debug("No connection established after connect, 
resetting connection");
-                connection = plcDriverManager.getConnection(uri);
+            if (autoReconnect) {
+                // a lost PLC4X connection cannot always connect again (the S7 
and EtherNet/IP drivers fail on a closed
+                // transport), so it is replaced by a new one
+                closeConnection();
+                connection = plcDriverManager.getConnection(connectionString);
+                LOGGER.debug("Successfully reconnected");
+                return;
+            }
+        }
+        // stop() is called outside the lock, as a concurrent stop() holds the 
service lock while doStop() waits for it
+        LOGGER.warn("Connection lost and auto-reconnect is turned off, 
shutting down Plc4XEndpoint");
+        stop();
+    }
+
+    private void closeConnection() {
+        if (connection != null) {
+            try {
+                connection.close();
+            } catch (Exception e) {
+                LOGGER.debug("Closing the connection failed: {}", 
e.getMessage());
             }
-            LOGGER.debug("Successfully reconnected");
-        } else {
-            LOGGER.warn("Connection lost and auto-reconnect is turned off, 
shutting down Plc4XEndpoint");
-            stop();
+            connection = null;
         }
     }
 
@@ -183,14 +210,16 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
      * @return true if connection supports writing, else false
      */
     public boolean canWrite() {
-        return connection != null && 
connection.getMetadata().isWriteSupported();
+        PlcConnection current = connection;
+        return current != null && current.getMetadata().isWriteSupported();
     }
 
     /**
      * @return true if connection is established and still connected
      */
     public boolean isConnected() {
-        return connection != null && connection.isConnected();
+        PlcConnection current = connection;
+        return current != null && current.isConnected();
     }
 
     @Override
@@ -200,7 +229,8 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
 
     @Override
     public Consumer createConsumer(Processor processor) throws Exception {
-        Plc4XConsumer consumer = new Plc4XConsumer(this, processor);
+        Consumer consumer = trigger == null
+                ? new Plc4XConsumer(this, processor) : new 
Plc4XTriggeredConsumer(this, processor);
         configureConsumer(consumer);
         return consumer;
     }
@@ -213,8 +243,25 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
     /**
      * Build a {@link PlcReadRequest} using the tags specified in the endpoint.
      */
-    public PlcReadRequest buildPlcReadRequest() {
-        PlcReadRequest.Builder builder = connection.readRequestBuilder();
+    public PlcReadRequest buildPlcReadRequest() throws PlcConnectionException {
+        return buildPlcReadRequest(Map.of());
+    }
+
+    /**
+     * Build a {@link PlcReadRequest} using the given tags and the tags 
specified in the endpoint.
+     *
+     * @param  additionalTags         tag addresses by name to read in 
addition to the tags of the endpoint
+     * @return                        {@link PlcReadRequest}
+     * @throws PlcConnectionException if there is no connection
+     */
+    PlcReadRequest buildPlcReadRequest(Map<String, String> additionalTags) 
throws PlcConnectionException {
+        // read the field once: a concurrent stop or reconnect can clear it at 
any time
+        PlcConnection current = connection;
+        if (current == null) {
+            throw new PlcConnectionException("Cannot establish connection");
+        }
+        PlcReadRequest.Builder builder = current.readRequestBuilder();
+        additionalTags.forEach(builder::addTagAddress);
         if (tags != null) {
             for (Map.Entry<String, String> tag : tags.entrySet()) {
                 try {
@@ -228,15 +275,32 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
         return builder.build();
     }
 
+    /**
+     * @return the values of a read response by tag name
+     */
+    static Map<String, Object> tagValues(PlcReadResponse response) {
+        Map<String, Object> values = new HashMap<>();
+        for (String tag : response.getTagNames()) {
+            values.put(tag, response.getObject(tag));
+        }
+        return values;
+    }
+
     /**
      * Build a {@link PlcWriteRequest}.
      * <p>
      *
-     * @param  tags tags to add to write request
-     * @return      {@link PlcWriteRequest}
+     * @param  tags                   tags to add to write request
+     * @return                        {@link PlcWriteRequest}
+     * @throws PlcConnectionException if there is no connection
      */
-    public PlcWriteRequest buildPlcWriteRequest(Map<String, Map<String, 
Object>> tags) {
-        PlcWriteRequest.Builder builder = connection.writeRequestBuilder();
+    public PlcWriteRequest buildPlcWriteRequest(Map<String, Map<String, 
Object>> tags) throws PlcConnectionException {
+        // read the field once: a concurrent stop or reconnect can clear it at 
any time
+        PlcConnection current = connection;
+        if (current == null) {
+            throw new PlcConnectionException("Cannot establish connection");
+        }
+        PlcWriteRequest.Builder builder = current.writeRequestBuilder();
 
         for (Map.Entry<String, Map<String, Object>> entry : tags.entrySet()) {
             //Tags are stored like this --> Map<Tagname,Map<Query,Value>> for 
writing
@@ -271,16 +335,34 @@ public class Plc4XEndpoint extends DefaultEndpoint 
implements EndpointServiceLoc
     @Override
     protected void doStart() throws Exception {
         super.doStart();
+        connectionString = withoutEndpointOptions(uri);
         ClassLoader cl = getCamelContext().getApplicationContextClassLoader();
         this.plcDriverManager = new DefaultPlcDriverManager(cl);
     }
 
+    /**
+     * Removes the options of this endpoint from a connection string, so the 
PLC4X driver only gets the parameters it
+     * knows.
+     */
+    private String withoutEndpointOptions(String endpointUri) throws 
URISyntaxException {
+        EndpointUriFactory factory = 
getCamelContext().getCamelContextExtension().getEndpointUriFactory("plc4x");
+        if (factory == null) {
+            throw new IllegalStateException("Cannot find the 
EndpointUriFactory of the plc4x component");
+        }
+        Map<String, Object> parameters
+                = new 
LinkedHashMap<>(URISupport.parseQuery(URISupport.extractQuery(endpointUri), 
true));
+        parameters.keySet().removeIf(name -> 
factory.propertyNames().contains(name)
+                || 
factory.multiValuePrefixes().values().stream().anyMatch(name::startsWith));
+        String query = URISupport.createQueryString(parameters, false);
+        String path = URISupport.stripQuery(endpointUri);
+        return query.isEmpty() ? path : path + "?" + query;
+    }
+
     @Override
     public void doStop() throws Exception {
         super.doStop();
-        if (isConnected() && connection != null) {
-            connection.close();
-            connection = null;
+        synchronized (this) {
+            closeConnection();
         }
     }
 
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java
index 6fcf7242244f..b35c2d306e6e 100644
--- 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.component.plc4x;
 
-import java.util.HashMap;
-import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -86,10 +84,6 @@ public class Plc4XPollingConsumer extends 
EventDrivenPollingConsumer {
         Exchange exchange = plc4XEndpoint.createExchange();
         try {
             plc4XEndpoint.reconnectIfNeeded();
-            if (plc4XEndpoint.connection == null) {
-                throw new PlcConnectionException("Cannot establish 
connection");
-            }
-
             PlcReadRequest request = plc4XEndpoint.buildPlcReadRequest();
             CompletableFuture<? extends PlcReadResponse> future
                     = request.execute().whenComplete((plcReadResponse, 
throwable) -> {
@@ -101,11 +95,7 @@ public class Plc4XPollingConsumer extends 
EventDrivenPollingConsumer {
                 response = future.get();
             }
 
-            Map<String, Object> rsp = new HashMap<>();
-            for (String field : response.getTagNames()) {
-                rsp.put(field, response.getObject(field));
-            }
-            exchange.getIn().setBody(rsp);
+            exchange.getIn().setBody(Plc4XEndpoint.tagValues(response));
         } catch (TimeoutException e) {
             // nothing received within the timeout
             LOGGER.debug("No response from the PLC within {} millis", timeout);
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java
index 6b82e831ead0..13719137f77e 100644
--- 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java
@@ -83,7 +83,7 @@ public class Plc4XProducer extends DefaultAsyncProducer {
         int currentlyOpenRequests = openRequests.incrementAndGet();
         try {
             log.debug("Currently open requests including {}:{}", exchange, 
currentlyOpenRequests);
-            Object plcWriteResponse = completableFuture.get(5000, 
TimeUnit.MILLISECONDS);
+            Object plcWriteResponse = 
completableFuture.get(Plc4XEndpoint.REQUEST_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS);
             if (exchange.getPattern().isOutCapable()) {
                 Message out = exchange.getMessage();
                 out.copyFrom(exchange.getIn());
diff --git 
a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XTriggeredConsumer.java
 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XTriggeredConsumer.java
new file mode 100644
index 000000000000..a742ac2a878b
--- /dev/null
+++ 
b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XTriggeredConsumer.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.plc4x;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.support.ScheduledPollConsumer;
+import org.apache.plc4x.java.api.exceptions.PlcException;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+/**
+ * Reads the trigger tag together with the tags every {@code period} 
milliseconds and emits the tags once on each rising
+ * edge of the trigger.
+ */
+public class Plc4XTriggeredConsumer extends ScheduledPollConsumer {
+    private final Plc4XEndpoint plc4XEndpoint;
+    private final String trigger;
+    private final int period;
+
+    // whether the trigger was true on the last poll; null when unknown, 
before the first poll or after a failed one
+    private Boolean lastTrigger;
+
+    public Plc4XTriggeredConsumer(Plc4XEndpoint endpoint, Processor processor) 
{
+        super(endpoint, processor);
+        this.plc4XEndpoint = endpoint;
+        this.trigger = endpoint.getTrigger();
+        this.period = endpoint.getPeriod();
+        setInitialDelay(0);
+        setDelay(period);
+        setTimeUnit(TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public Plc4XEndpoint getEndpoint() {
+        return plc4XEndpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (period <= 0) {
+            throw new IllegalArgumentException(
+                    "The period must be a positive number of milliseconds when 
a trigger is set, but was " + period);
+        }
+        Map<String, String> tags = plc4XEndpoint.getTags();
+        if (tags != null && tags.containsKey(Constants.TRIGGER)) {
+            throw new IllegalArgumentException(
+                    "The tag name " + Constants.TRIGGER + " is reserved for 
the trigger when a trigger is set");
+        }
+        lastTrigger = null;
+        // without autoReconnect a connection that cannot be set up fails the 
start, as no poll would reconnect
+        plc4XEndpoint.setupConnection();
+        super.doStart();
+    }
+
+    @Override
+    protected int poll() throws Exception {
+        PlcReadResponse response;
+        try {
+            response = read();
+        } catch (Exception e) {
+            lastTrigger = null;
+            throw e;
+        }
+        if (!isRunAllowed()) {
+            // the consumer stopped while the read was in progress
+            return 0;
+        }
+        boolean triggered = (Boolean) response.getObject(Constants.TRIGGER);
+        boolean risingEdge = triggered && !Boolean.TRUE.equals(lastTrigger);
+        lastTrigger = triggered;
+        if (!risingEdge) {
+            return 0;
+        }
+
+        Map<String, Object> tags = Plc4XEndpoint.tagValues(response);
+        tags.remove(Constants.TRIGGER);
+        Exchange exchange = createExchange(false);
+        exchange.getIn().setBody(tags);
+        try {
+            getProcessor().process(exchange);
+        } catch (Exception e) {
+            exchange.setException(e);
+        } finally {
+            if (exchange.getException() != null) {
+                getExceptionHandler().handleException("Error processing 
exchange", exchange, exchange.getException());
+            }
+            releaseExchange(exchange, false);
+        }
+        return 1;
+    }
+
+    /**
+     * Reads the trigger and the tags in one request; fails unless every tag 
was read and the trigger is a boolean.
+     */
+    private PlcReadResponse read() throws Exception {
+        plc4XEndpoint.reconnectIfNeeded();
+        CompletableFuture<? extends PlcReadResponse> future
+                = plc4XEndpoint.buildPlcReadRequest(Map.of(Constants.TRIGGER, 
trigger)).execute();
+        PlcReadResponse response;
+        try {
+            response = future.get(Plc4XEndpoint.REQUEST_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS);
+        } catch (ExecutionException e) {
+            throw e.getCause() instanceof Exception cause ? cause : e;
+        } catch (TimeoutException e) {
+            future.cancel(true);
+            throw new TimeoutException(
+                    "No response to the read of the trigger " + trigger + " 
within "
+                                       + Plc4XEndpoint.REQUEST_TIMEOUT_MILLIS 
+ " millis");
+        }
+        for (String tag : response.getTagNames()) {
+            PlcResponseCode code = response.getResponseCode(tag);
+            if (code != PlcResponseCode.OK) {
+                throw new PlcException("Reading the tag " + tag + " failed 
with response code " + code);
+            }
+        }
+        Object value = response.getObject(Constants.TRIGGER);
+        if (!(value instanceof Boolean)) {
+            throw new PlcException(
+                    "The trigger " + trigger + " must read as a Boolean, but 
read "
+                                   + (value == null ? "no value" : "a " + 
value.getClass().getName()));
+        }
+        return response;
+    }
+
+    @Override
+    public String toString() {
+        return "Plc4XTriggeredConsumer[" + plc4XEndpoint + "]";
+    }
+}
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/MockDriver.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/MockDriver.java
index 71815047e3a7..a27f979dc6f3 100644
--- 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/MockDriver.java
+++ 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/MockDriver.java
@@ -16,32 +16,150 @@
  */
 package org.apache.camel.component.plc4x;
 
-import java.util.HashMap;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.PlcDriver;
 import org.apache.plc4x.java.api.authentication.PlcAuthentication;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
 import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
-import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse;
 import org.apache.plc4x.java.api.messages.PlcUnsubscriptionRequest;
 import org.apache.plc4x.java.api.messages.PlcWriteRequest;
-import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionResponse;
-import org.apache.plc4x.java.spi.messages.PlcSubscriber;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
 
 import static org.mockito.Mockito.*;
 
+/**
+ * Test driver for the {@code mock} protocol. A read returns the values a test 
programmed with
+ * {@link #programRead(String, Object...)} when every tag in the request is 
programmed, and Mockito deep stubs
+ * otherwise. A programmed {@link #failedRead()} fails the whole read, a 
programmed
+ * {@link #responseCode(PlcResponseCode)} answers that tag with an error code. 
A connection reports that it is connected
+ * from {@code connect()} until {@link #dropConnections()} drops it; a dropped 
connection cannot connect again, as after
+ * a PLC restart. The programming is static because every driver manager loads 
its own driver instance; tests call
+ * {@link #reset()}.
+ */
 public class MockDriver implements PlcDriver {
 
-    public static final Logger LOGGER = 
LoggerFactory.getLogger(MockDriver.class);
+    private static final Map<String, ProgrammedTag> PROGRAMMED_TAGS = new 
ConcurrentHashMap<>();
+    private static volatile CompletableFuture<Void> heldReads = 
CompletableFuture.completedFuture(null);
+    private static volatile CompletableFuture<Void> heldRead = new 
CompletableFuture<>();
+    private static final AtomicInteger REFUSED_CONNECTIONS = new 
AtomicInteger();
+    private static final List<String> CONNECTION_STRINGS = new 
CopyOnWriteArrayList<>();
+    private static final Object FAILED_READ = new Object();
+    private static volatile boolean refuseConnections;
+    private static final AtomicInteger CREATED_CONNECTIONS = new 
AtomicInteger();
+    private static volatile int droppedConnections;
+
+    /**
+     * Programs the values that successive reads of a tag address return; the 
last value repeats once they are used up.
+     */
+    public static void programRead(String tagAddress, Object... values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("At least one value is required 
for " + tagAddress);
+        }
+        PROGRAMMED_TAGS.put(tagAddress, new 
ProgrammedTag(Arrays.asList(values)));
+    }
+
+    /**
+     * A programmed value that makes the whole read in which it is due fail.
+     */
+    public static Object failedRead() {
+        return FAILED_READ;
+    }
+
+    /**
+     * A programmed value that makes the read in which it is due answer the 
tag with the given response code and no
+     * value.
+     */
+    public static Object responseCode(PlcResponseCode responseCode) {
+        return new FailedTag(responseCode);
+    }
+
+    /**
+     * Holds the following reads until {@link #releaseReads()}; a held read 
ignores interrupts, as a read that a PLC has
+     * already answered does.
+     */
+    public static void holdReads() {
+        heldRead = new CompletableFuture<>();
+        heldReads = new CompletableFuture<>();
+    }
+
+    /**
+     * Waits until a read is held by {@link #holdReads()}.
+     */
+    public static void awaitHeldRead() throws Exception {
+        heldRead.get(10, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Lets the reads held by {@link #holdReads()} complete.
+     */
+    public static void releaseReads() {
+        heldReads.complete(null);
+    }
+
+    /**
+     * Makes every following connection attempt fail.
+     */
+    public static void refuseConnections() {
+        refuseConnections = true;
+    }
+
+    /**
+     * Makes the following connection attempts succeed again.
+     */
+    public static void acceptConnections() {
+        refuseConnections = false;
+    }
 
-    ExecutorService executorService = Executors.newFixedThreadPool(10);
+    /**
+     * Drops the connections made so far, as a restarted PLC does: they report 
that they are not connected from now on
+     * and fail to connect again, while a new connection succeeds.
+     */
+    public static void dropConnections() {
+        droppedConnections = CREATED_CONNECTIONS.get();
+    }
+
+    /**
+     * @return the number of connection attempts refused since the last {@link 
#reset()}
+     */
+    public static int refusedConnections() {
+        return REFUSED_CONNECTIONS.get();
+    }
+
+    /**
+     * @return the connection strings the driver was asked to connect with 
since the last {@link #reset()}
+     */
+    public static List<String> connectionStrings() {
+        return CONNECTION_STRINGS;
+    }
+
+    /**
+     * Forgets all programmed tag values and accepts connections again.
+     */
+    public static void reset() {
+        releaseReads();
+        PROGRAMMED_TAGS.clear();
+        refuseConnections = false;
+        REFUSED_CONNECTIONS.set(0);
+        CREATED_CONNECTIONS.set(0);
+        droppedConnections = 0;
+        CONNECTION_STRINGS.clear();
+    }
 
     @Override
     public String getProtocolCode() {
@@ -55,52 +173,108 @@ public class MockDriver implements PlcDriver {
 
     @Override
     public PlcConnection getConnection(String url) throws 
PlcConnectionException {
+        CONNECTION_STRINGS.add(url);
+        if (refuseConnections) {
+            REFUSED_CONNECTIONS.incrementAndGet();
+            throw new PlcConnectionException("Connection refused by the mock 
driver");
+        }
         // Mock a connection.
         PlcConnection plcConnectionMock = mock(PlcConnection.class, 
RETURNS_DEEP_STUBS);
+        int connectionNumber = CREATED_CONNECTIONS.getAndIncrement();
+        AtomicBoolean connected = new AtomicBoolean();
+        doAnswer(invocation -> {
+            if (connectionNumber < droppedConnections) {
+                throw new PlcConnectionException("Transport is not open");
+            }
+            connected.set(true);
+            return null;
+        }).when(plcConnectionMock).connect();
+        when(plcConnectionMock.isConnected())
+                .thenAnswer(invocation -> connected.get() && connectionNumber 
>= droppedConnections);
         
when(plcConnectionMock.getMetadata().isReadSupported()).thenReturn(true);
         
when(plcConnectionMock.getMetadata().isWriteSupported()).thenReturn(true);
-        
when(plcConnectionMock.readRequestBuilder()).thenReturn(mock(PlcReadRequest.Builder.class,
 RETURNS_DEEP_STUBS));
+        when(plcConnectionMock.readRequestBuilder()).thenAnswer(invocation -> 
readRequestBuilder());
         
when(plcConnectionMock.writeRequestBuilder()).thenReturn(mock(PlcWriteRequest.Builder.class,
 RETURNS_DEEP_STUBS));
         when(plcConnectionMock.subscriptionRequestBuilder())
                 .thenReturn(mock(PlcSubscriptionRequest.Builder.class, 
RETURNS_DEEP_STUBS));
         when(plcConnectionMock.unsubscriptionRequestBuilder())
                 .thenReturn(mock(PlcUnsubscriptionRequest.Builder.class, 
RETURNS_DEEP_STUBS));
-
-        // Mock a typical subscriber.
-        PlcSubscriber plcSubscriber = mock(PlcSubscriber.class, 
RETURNS_DEEP_STUBS);
-        
when(plcSubscriber.subscribe(any(PlcSubscriptionRequest.class))).thenAnswer(invocation
 -> {
-            LOGGER.info("Received {}", invocation);
-            // TODO: Translate this so it actually does something ...
-            /*PlcSubscriptionRequest subscriptionRequest = 
invocation.getArgument(0);
-            List<PlcSubscriptionResponse> responseItems =
-                subscriptionRequest.getFieldNames().stream().map(
-                    fieldName -> 
subscriptionRequest.getField(fieldName)).map(field -> {
-                    Consumer consumer = subscriptionRequestItem.getConsumer();
-                    executorService.submit(() -> {
-                        while (!Thread.currentThread().isInterrupted()) {
-                            consumer.accept(new SubscriptionEventItem<>(null, 
Calendar.getInstance(), Collections.singletonList("HelloWorld")));
-                            try {
-                                TimeUnit.MILLISECONDS.sleep(100);
-                            } catch (InterruptedException e) {
-                                Thread.currentThread().interrupt();
-                                throw new RuntimeException(e);
-                            }
-                        }
-                    });
-                    return new 
SubscriptionResponseItem<>(subscriptionRequestItem,
-                        mock(PlcSubscriptionHandle.class, RETURNS_DEEP_STUBS), 
PlcResponseCode.OK);
-                }).collect(Collectors.toList());
-            PlcSubscriptionResponse response = new 
PlcSubscriptionResponse(subscriptionRequest, responseItems);*/
-            PlcSubscriptionResponse response
-                    = new 
DefaultPlcSubscriptionResponse(mock(PlcSubscriptionRequest.class), new 
HashMap<>());
-            return CompletableFuture.completedFuture(response);
-        });
         return plcConnectionMock;
     }
 
     @Override
     public PlcConnection getConnection(String url, PlcAuthentication 
authentication) throws PlcConnectionException {
-        return getConnection(null);
+        return getConnection(url);
+    }
+
+    private static PlcReadRequest.Builder readRequestBuilder() {
+        PlcReadRequest.Builder unprogrammed = 
mock(PlcReadRequest.Builder.class, RETURNS_DEEP_STUBS);
+        Map<String, String> tags = new LinkedHashMap<>();
+        return mock(PlcReadRequest.Builder.class, invocation -> switch 
(invocation.getMethod().getName()) {
+            case "addTagAddress" -> {
+                tags.put(invocation.getArgument(0), invocation.getArgument(1));
+                unprogrammed.addTagAddress(invocation.getArgument(0), 
invocation.getArgument(1));
+                yield invocation.getMock();
+            }
+            case "build" -> !tags.isEmpty() && 
PROGRAMMED_TAGS.keySet().containsAll(tags.values())
+                    ? programmedRequest(tags) : unprogrammed.build();
+            default -> RETURNS_DEFAULTS.answer(invocation);
+        });
+    }
+
+    private static PlcReadRequest programmedRequest(Map<String, String> tags) {
+        return mock(PlcReadRequest.class, invocation -> switch 
(invocation.getMethod().getName()) {
+            case "execute" -> read(tags);
+            case "getTagNames" -> new LinkedHashSet<>(tags.keySet());
+            default -> RETURNS_DEFAULTS.answer(invocation);
+        });
+    }
+
+    private static CompletableFuture<PlcReadResponse> read(Map<String, String> 
tags) {
+        heldRead.complete(null);
+        // join() does not return early on an interrupt
+        heldReads.join();
+        Map<String, Object> values = new LinkedHashMap<>();
+        tags.forEach((name, address) -> values.put(name, 
PROGRAMMED_TAGS.get(address).next()));
+        if (values.containsValue(FAILED_READ)) {
+            return CompletableFuture.failedFuture(new PlcException("Read 
failed in the mock driver"));
+        }
+        return CompletableFuture.completedFuture(mock(PlcReadResponse.class,
+                invocation -> switch (invocation.getMethod().getName()) {
+                    case "getTagNames" -> List.copyOf(values.keySet());
+                    case "getObject" -> value(values.get(invocation.<String> 
getArgument(0)));
+                    case "getResponseCode" -> values.get(invocation.<String> 
getArgument(0)) instanceof FailedTag failed
+                            ? failed.responseCode() : PlcResponseCode.OK;
+                    default -> RETURNS_DEFAULTS.answer(invocation);
+                }));
+    }
+
+    private static Object value(Object programmed) {
+        if (programmed instanceof FailedTag failed) {
+            throw new PlcRuntimeException("Tag read failed with response code 
" + failed.responseCode());
+        }
+        return programmed;
+    }
+
+    private record FailedTag(PlcResponseCode responseCode) {
+    }
+
+    private static final class ProgrammedTag {
+
+        private final List<Object> values;
+        private int next;
+
+        ProgrammedTag(List<Object> values) {
+            this.values = values;
+        }
+
+        synchronized Object next() {
+            Object value = values.get(next);
+            if (next < values.size() - 1) {
+                next++;
+            }
+            return value;
+        }
     }
 
 }
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerRouteTest.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerRouteTest.java
new file mode 100644
index 000000000000..6a94252023fd
--- /dev/null
+++ 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerRouteTest.java
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.plc4x;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.ExceptionHandler;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The consumer's contract at the route seam: a route reads a {@code 
plc4x:mock:} endpoint whose tag values are
+ * programmed in {@link MockDriver}, and only what arrives at the mock 
endpoint is asserted.
+ */
+public class Plc4XConsumerRouteTest extends CamelTestSupport {
+
+    private static final String PLC = 
"plc4x:mock:10.10.10.1/1/1?autoReconnect=true";
+    private static final String TRIGGERED = PLC + 
"&trigger=TRIG&period=100&tag.temperature=TEMP&tag.pressure=PRES";
+    private static final String UNTRIGGERED = PLC + 
"&tag.temperature=TEMP&tag.pressure=PRES";
+    private static final String REPORTED = PLC + 
"&trigger=TRIG&period=50&tag.temperature=TEMP&tag.pressure=PRES"
+                                           + "&exceptionHandler=#pollFailures";
+    private static final String WITHOUT_AUTO_RECONNECT
+            = 
"plc4x:mock:10.10.10.1/1/1?trigger=TRIG&period=50&tag.temperature=TEMP";
+    private static final String RESERVED_TAG_NAME = PLC + 
"&trigger=TRIG&period=50&tag.TRIGGER_VAR=TEMP";
+    private static final String WITHOUT_PERIOD = PLC + 
"&trigger=TRIG&tag.temperature=TEMP";
+    private static final String DRIVER_PARAMETERS = PLC + 
"&trigger=TRIG&period=100&tag.temperature=TEMP"
+                                                    + 
"&exceptionHandler=#pollFailures&exchangePattern=InOnly"
+                                                    + 
"&remote-rack=1&request-timeout-ms=500&percent-encoded=%25DB1";
+
+    private final PollFailures pollFailures = new PollFailures();
+
+    @BeforeEach
+    void resetMockDriver() {
+        MockDriver.reset();
+    }
+
+    @AfterAll
+    static void leaveMockDriverUnprogrammed() {
+        MockDriver.reset();
+    }
+
+    @Test
+    public void emitsOncePerRisingEdgeOfTheTrigger() throws Exception {
+        MockDriver.programRead("TRIG", false, false, true, true, false, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:triggered");
+        result.expectedMessageCount(2);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("triggered");
+
+        result.assertIsSatisfied(10000);
+    }
+
+    @Test
+    public void deliversTheDataTagsWithoutTheTrigger() throws Exception {
+        MockDriver.programRead("TRIG", false, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:triggered");
+        result.expectedMessageCount(1);
+
+        context.getRouteController().startRoute("triggered");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(Map.of("temperature", 21, "pressure", 1013), 
result.getExchanges().get(0).getIn().getBody(),
+                "the body holds the data tags, not the trigger");
+    }
+
+    @Test
+    public void emitsOnceForATriggerThatIsAlreadyTrueOnTheFirstRead() throws 
Exception {
+        MockDriver.programRead("TRIG", true, true, false);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:triggered");
+        result.expectedMessageCount(1);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("triggered");
+
+        result.assertIsSatisfied(10000);
+    }
+
+    @Test
+    public void 
stopsReadingWhenTheRouteStopsAndResumesWithoutDuplicatesAfterARestart() throws 
Exception {
+        MockDriver.programRead("TRIG", false, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:triggered");
+        result.expectedMessageCount(3);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("triggered");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
result.getReceivedCounter() == 1);
+        context.getRouteController().stopRoute("triggered");
+
+        MockDriver.programRead("TRIG", false, true, false, true, false);
+        await().during(1, TimeUnit.SECONDS).atMost(2, 
TimeUnit.SECONDS).until(() -> result.getReceivedCounter() == 1);
+
+        context.getRouteController().startRoute("triggered");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(1, MockDriver.connectionStrings().size(), "the restarted 
route uses the connection that is still open");
+    }
+
+    @Test
+    public void readsTheTagsOnceWithoutATrigger() throws Exception {
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:untriggered");
+        result.expectedMessageCount(1);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("untriggered");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(Map.of("temperature", 21, "pressure", 1013), 
result.getExchanges().get(0).getIn().getBody());
+    }
+
+    @Test
+    public void deliversTheDataTagsOfTheReadThatSawTheEdge() throws Exception {
+        MockDriver.programRead("TRIG", false, true, false, true);
+        MockDriver.programRead("TEMP", 21, 22, 23, 24);
+        MockDriver.programRead("PRES", 1011, 1012, 1013, 1014);
+        MockEndpoint result = getMockEndpoint("mock:triggered");
+        result.expectedMessageCount(2);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("triggered");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(List.of(Map.of("temperature", 22, "pressure", 1012), 
Map.of("temperature", 24, "pressure", 1014)),
+                result.getExchanges().stream().map(exchange -> 
exchange.getIn().getBody()).toList());
+    }
+
+    @Test
+    public void reportsEachFailedPollAndResumesWhenThePlcReturns() throws 
Exception {
+        MockDriver.programRead("TRIG", false, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockDriver.refuseConnections();
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(1);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("reported");
+        
assertTrue(context.getRouteController().getRouteStatus("reported").isStarted());
+        await().atMost(10, TimeUnit.SECONDS).until(() -> pollFailures.count() 
>= 3);
+        assertEquals(0, result.getReceivedCounter(), "nothing is delivered 
while the PLC refuses connections");
+
+        MockDriver.acceptConnections();
+
+        result.assertIsSatisfied(10000);
+        
assertTrue(pollFailures.causes().stream().allMatch(PlcConnectionException.class::isInstance),
+                () -> "Unexpected causes: " + pollFailures.causes());
+    }
+
+    @Test
+    public void pollsOnTheOneConnectionOfTheEndpoint() throws Exception {
+        MockDriver.programRead("TRIG", false, false, false, false, false, 
true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(1);
+
+        context.getRouteController().startRoute("reported");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(1, MockDriver.connectionStrings().size(), "six polls use 
the connection set up on start");
+    }
+
+    @Test
+    public void resumesOnANewConnectionWhenTheDroppedOneCannotConnectAgain() 
throws Exception {
+        MockDriver.programRead("TRIG", false, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(2);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("reported");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
result.getReceivedCounter() == 1);
+        MockDriver.dropConnections();
+        MockDriver.programRead("TRIG", false, true);
+
+        result.assertIsSatisfied(10000);
+    }
+
+    @Test
+    public void aFailedPollForgetsTheTriggerSoAStillTrueTriggerEmitsAgain() 
throws Exception {
+        MockDriver.programRead("TRIG", true, true, MockDriver.failedRead(), 
true, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(2);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("reported");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(1, pollFailures.count());
+    }
+
+    @Test
+    public void aTagReadWithAnErrorCodeFailsThePoll() throws Exception {
+        MockDriver.programRead("TRIG", false, true, true);
+        MockDriver.programRead("TEMP", 21, 
MockDriver.responseCode(PlcResponseCode.NOT_FOUND), 23);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(1);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("reported");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(Map.of("temperature", 23, "pressure", 1013), 
result.getExchanges().get(0).getIn().getBody());
+        assertEquals(1, pollFailures.count());
+        
assertTrue(pollFailures.causes().get(0).getMessage().contains("NOT_FOUND"),
+                () -> "Unexpected cause: " + pollFailures.causes().get(0));
+    }
+
+    @Test
+    public void aTriggerThatIsNotABooleanFailsThePoll() throws Exception {
+        MockDriver.programRead("TRIG", 1, true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(1);
+        result.setAssertPeriod(1000);
+
+        context.getRouteController().startRoute("reported");
+
+        result.assertIsSatisfied(10000);
+        assertEquals(1, pollFailures.count(), () -> "Unexpected failures: " + 
pollFailures.causes());
+        String message = pollFailures.causes().get(0).getMessage();
+        assertTrue(message.contains("TRIG") && 
message.contains(Integer.class.getName()), message);
+    }
+
+    @Test
+    public void aReadThatCompletesWhileTheRouteStopsIsNotDelivered() throws 
Exception {
+        MockDriver.programRead("TRIG", true);
+        MockDriver.programRead("TEMP", 21);
+        MockDriver.programRead("PRES", 1013);
+        MockDriver.holdReads();
+        MockEndpoint result = getMockEndpoint("mock:reported");
+        result.expectedMessageCount(0);
+
+        context.getRouteController().startRoute("reported");
+        MockDriver.awaitHeldRead();
+        context.getRouteController().stopRoute("reported");
+        MockDriver.releaseReads();
+
+        // with no message expected, the mock endpoint waits the whole time 
before it is satisfied
+        result.assertIsSatisfied(1000);
+        assertEquals(0, pollFailures.count(), () -> "Unexpected failures: " + 
pollFailures.causes());
+    }
+
+    @Test
+    public void aRefusedConnectionFailsTheRouteStartWithoutAutoReconnect() {
+        MockDriver.refuseConnections();
+
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.getRouteController().startRoute("withoutAutoReconnect"));
+
+        assertNotNull(causeOfType(e, PlcConnectionException.class), () -> "No 
PlcConnectionException in " + e);
+    }
+
+    @Test
+    public void aTagNamedLikeTheTriggerFailsTheRouteStart() {
+        Exception e = assertThrows(Exception.class, () -> 
context.getRouteController().startRoute("reservedTagName"));
+
+        Throwable cause = causeOfType(e, IllegalArgumentException.class);
+        assertNotNull(cause, () -> "No IllegalArgumentException in " + e);
+        assertTrue(cause.getMessage().contains(Constants.TRIGGER), 
cause::getMessage);
+    }
+
+    @Test
+    public void aTriggerWithoutAPeriodFailsTheRouteStart() {
+        Exception e = assertThrows(Exception.class, () -> 
context.getRouteController().startRoute("withoutPeriod"));
+
+        Throwable cause = causeOfType(e, IllegalArgumentException.class);
+        assertNotNull(cause, () -> "No IllegalArgumentException in " + e);
+        assertTrue(cause.getMessage().contains("period"), cause::getMessage);
+    }
+
+    @Test
+    public void handsTheDriverOnlyItsOwnConnectionParameters() throws 
Exception {
+        MockDriver.programRead("TRIG", true);
+        MockDriver.programRead("TEMP", 21);
+        MockEndpoint result = getMockEndpoint("mock:driverParameters");
+        result.expectedMessageCount(1);
+
+        context.getRouteController().startRoute("driverParameters");
+
+        result.assertIsSatisfied(10000);
+        
assertEquals(Set.of("mock:10.10.10.1/1/1?percent-encoded=%25DB1&remote-rack=1&request-timeout-ms=500"),
+                Set.copyOf(MockDriver.connectionStrings()));
+        String uri = context.getEndpoint(DRIVER_PARAMETERS, 
Plc4XEndpoint.class).getUri();
+        assertTrue(uri.contains("trigger=TRIG") && 
uri.contains("autoReconnect=true"),
+                () -> "The endpoint no longer reports its configured uri: " + 
uri);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from(TRIGGERED).routeId("triggered").autoStartup(false).to("mock:triggered");
+                
from(UNTRIGGERED).routeId("untriggered").autoStartup(false).to("mock:untriggered");
+                
from(REPORTED).routeId("reported").autoStartup(false).to("mock:reported");
+                
from(WITHOUT_AUTO_RECONNECT).routeId("withoutAutoReconnect").autoStartup(false)
+                        .to("mock:withoutAutoReconnect");
+                
from(RESERVED_TAG_NAME).routeId("reservedTagName").autoStartup(false).to("mock:reservedTagName");
+                
from(WITHOUT_PERIOD).routeId("withoutPeriod").autoStartup(false).to("mock:withoutPeriod");
+                
from(DRIVER_PARAMETERS).routeId("driverParameters").autoStartup(false).to("mock:driverParameters");
+            }
+        };
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("pollFailures", pollFailures);
+    }
+
+    private static Throwable causeOfType(Throwable exception, Class<? extends 
Throwable> type) {
+        Throwable cause = exception;
+        while (cause != null && !type.isInstance(cause)) {
+            cause = cause.getCause();
+        }
+        return cause;
+    }
+
+    /**
+     * Collects the failures the consumer reports to its exception handler.
+     */
+    private static final class PollFailures implements ExceptionHandler {
+
+        private final List<Throwable> causes = new CopyOnWriteArrayList<>();
+
+        @Override
+        public void handleException(Throwable exception) {
+            causes.add(exception);
+        }
+
+        @Override
+        public void handleException(String message, Throwable exception) {
+            causes.add(exception);
+        }
+
+        @Override
+        public void handleException(String message, Exchange exchange, 
Throwable exception) {
+            causes.add(exception);
+        }
+
+        int count() {
+            return causes.size();
+        }
+
+        List<Throwable> causes() {
+            return causes;
+        }
+    }
+
+}
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerStopTest.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerStopTest.java
deleted file mode 100644
index f2362156a147..000000000000
--- 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerStopTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.component.plc4x;
-
-import java.util.Map;
-
-import org.apache.camel.Processor;
-import org.apache.camel.impl.DefaultCamelContext;
-import org.apache.plc4x.java.scraper.triggeredscraper.TriggeredScraperImpl;
-import 
org.apache.plc4x.java.scraper.triggeredscraper.triggerhandler.collector.TriggerCollectorImpl;
-import org.junit.jupiter.api.Test;
-import org.mockito.MockedConstruction;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockConstruction;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * A stopped triggered consumer must stop its scraper, or the scraper keeps 
reading the PLC and sending exchanges to the
- * route, also after the route is started again with a new consumer 
(duplicates).
- */
-class Plc4XConsumerStopTest {
-
-    @Test
-    void testStopStopsTheScraper() throws Exception {
-        Plc4XEndpoint endpoint = mock(Plc4XEndpoint.class);
-        when(endpoint.getCamelContext()).thenReturn(new DefaultCamelContext());
-        
when(endpoint.getTrigger()).thenReturn("(PLC4X_TRIGGER_VAR,10,(%DB1:DBX0.0:BOOL)==(true))");
-        when(endpoint.getTags()).thenReturn(Map.of("tag1", "%DB1:DBW2:INT"));
-        when(endpoint.getPeriod()).thenReturn(1000);
-        when(endpoint.getUri()).thenReturn("mock:plc");
-
-        try (MockedConstruction<TriggeredScraperImpl> scrapers = 
mockConstruction(TriggeredScraperImpl.class);
-             MockedConstruction<TriggerCollectorImpl> collectors = 
mockConstruction(TriggerCollectorImpl.class)) {
-            Plc4XConsumer consumer = new Plc4XConsumer(endpoint, 
mock(Processor.class));
-            consumer.start();
-
-            assertEquals(1, scrapers.constructed().size());
-            TriggeredScraperImpl scraper = scrapers.constructed().get(0);
-            TriggerCollectorImpl collector = collectors.constructed().get(0);
-            verify(scraper).start();
-            verify(scraper, never()).stop();
-
-            consumer.stop();
-
-            verify(scraper).stop();
-            verify(collector).stop();
-        }
-    }
-}
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerTest.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerTest.java
deleted file mode 100644
index fa595463a25e..000000000000
--- 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XConsumerTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.component.plc4x;
-
-import java.util.Collections;
-import java.util.Map;
-
-import org.apache.camel.Exchange;
-import org.apache.camel.Processor;
-import org.apache.camel.impl.DefaultCamelContext;
-import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-class Plc4XConsumerTest {
-
-    private Plc4XEndpoint endpoint;
-    private Processor processor;
-    private Plc4XConsumer consumer;
-
-    @BeforeEach
-    void setUp() {
-        endpoint = mock(Plc4XEndpoint.class);
-        processor = mock(Processor.class);
-
-        when(endpoint.getTags()).thenReturn(Collections.emptyMap());
-        when(endpoint.getTrigger()).thenReturn(null); // untriggered
-        when(endpoint.getCamelContext()).thenReturn(new DefaultCamelContext());
-
-        consumer = new Plc4XConsumer(endpoint, processor);
-    }
-
-    @Test
-    void doStart() throws Exception {
-
-        doNothing().when(endpoint).setupConnection();
-        consumer.doStart();
-        verify(endpoint, times(1)).setupConnection();
-    }
-
-    @Test
-    void doStartBadStart() throws Exception {
-
-        doThrow(new PlcConnectionException("fail"))
-                .when(endpoint).setupConnection();
-        consumer.doStart();
-        verify(endpoint).setupConnection();
-        assertFalse(consumer.isStarted());
-    }
-
-    @Test
-    void shouldStartTriggeredScraperWhenTriggerPresent() throws Exception {
-
-        when(endpoint.getTrigger()).thenReturn("someTrigger");
-        when(endpoint.getTags()).thenReturn(Map.of("tag1", "value1"));
-        when(endpoint.getPeriod()).thenReturn(1000);
-        when(endpoint.getUri()).thenReturn("mock:plc");
-
-        // Avoid real connection
-        doNothing().when(endpoint).setupConnection();
-        doNothing().when(endpoint).reconnectIfNeeded();
-        when(endpoint.createExchange()).thenReturn(mock(Exchange.class));
-        consumer.doStart();
-        verify(endpoint, atLeastOnce()).getTrigger();
-    }
-
-    @Test
-    void doStop() throws Exception {
-        consumer.doStop();
-        assertFalse(consumer.isStarted(), "Consumer should not be started 
after doStop");
-    }
-
-}
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java
index 24cdd40c11d4..28df776b2c83 100644
--- 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java
+++ 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java
@@ -16,16 +16,21 @@
  */
 package org.apache.camel.component.plc4x;
 
+import java.util.Map;
+
 import org.apache.camel.Component;
 import org.apache.camel.Processor;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.*;
 
 class Plc4XEndpointTest {
@@ -56,12 +61,26 @@ class Plc4XEndpointTest {
     }
 
     @Test
-    void doStopDisconnectedConnection() throws Exception {
+    void doStopClosesALostConnection() throws Exception {
         PlcConnection plcConnectionMock = mock(PlcConnection.class);
+        // isConnected() returns false on the mock, and close() fails as on a 
dead transport
+        doThrow(new PlcConnectionException("Transport is not 
open")).when(plcConnectionMock).close();
         sut.connection = plcConnectionMock;
-        // isConnected() returns false by default on the mock, so doStop() 
should skip close()
+
         sut.doStop();
-        verify(plcConnectionMock, never()).close();
+
+        verify(plcConnectionMock).close();
+        assertThat(sut.connection, nullValue());
+    }
+
+    @Test
+    void buildPlcReadRequestWithoutAConnection() {
+        assertThrows(PlcConnectionException.class, () -> 
sut.buildPlcReadRequest());
+    }
+
+    @Test
+    void buildPlcWriteRequestWithoutAConnection() {
+        assertThrows(PlcConnectionException.class, () -> 
sut.buildPlcWriteRequest(Map.of()));
     }
 
 }
diff --git 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java
 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java
index 54ac03fe2253..e1fd611d8f82 100644
--- 
a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java
+++ 
b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java
@@ -24,7 +24,6 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.support.DefaultExchange;
-import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
@@ -111,7 +110,6 @@ class Plc4XFailureTest {
         PlcRuntimeException cause = new PlcRuntimeException("read failed");
         PlcReadRequest request = mock(PlcReadRequest.class);
         when(request.execute()).thenAnswer(invocation -> 
CompletableFuture.failedFuture(cause));
-        endpoint.connection = mock(PlcConnection.class);
         when(endpoint.buildPlcReadRequest()).thenReturn(request);
 
         Exchange exchange = new Plc4XPollingConsumer(endpoint).receive(1000);
@@ -124,7 +122,6 @@ class Plc4XFailureTest {
     void testPollingConsumerReturnsNullWithoutResponse() throws Exception {
         PlcReadRequest request = mock(PlcReadRequest.class);
         when(request.execute()).thenAnswer(invocation -> new 
CompletableFuture<>());
-        endpoint.connection = mock(PlcConnection.class);
         when(endpoint.buildPlcReadRequest()).thenReturn(request);
 
         // receiveNoWait and receive(timeout) return null when nothing is 
received in time
@@ -134,7 +131,6 @@ class Plc4XFailureTest {
 
     @Test
     void testPollingConsumerReadsTags() throws Exception {
-        endpoint.connection = mock(PlcConnection.class);
         PlcReadRequest request = mock(PlcReadRequest.class, 
RETURNS_DEEP_STUBS);
         when(endpoint.buildPlcReadRequest()).thenReturn(request);
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index dac0f26b258c..0be69c592ec1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3600,3 +3600,47 @@ Use `0.0.0.0` as the host to accept connections on all 
network interfaces.
 
 `AvroRpcHttpServerFactory` has a new method `create(SpecificResponder, String, 
int)` that receives the host. A custom factory should implement it.
 
+=== camel-plc4x - PLC4X 1.0.0 and the triggered consumer
+
+`camel-plc4x` now uses PLC4X 1.0.0, which requires Java 21, as Camel 4.24 does.
+
+PLC4X 1.0.0 changes the connection strings of its drivers. Several parameters 
are renamed, for example
+`request-timeout` to `request-timeout-ms`, and the S7 driver reads the rack 
and slot as `cotp.remote-rack` and
+`cotp.remote-slot` instead of `remote-rack` and `remote-slot`. A parameter 
that a driver does not know, such as an old
+name, is logged as a warning and does not apply. The OPC UA driver now uses 
the security policy `Basic256Sha256` by
+default instead of `NONE`, `insecure-certificate-verification=true` becomes 
`tls.verify=false`, and a user name and
+password are refused over a channel that is neither signed nor encrypted 
unless `allow-insecure-credentials` is set.
+See the incompatible changes in the 
https://github.com/apache/plc4x/releases/tag/v1.0.0[PLC4X 1.0.0 release notes]
+for the full list.
+
+PLC4X 1.0.0 writes the array selection of a tag address before the data type, 
and as a range instead of a count. An
+address written as `holding-register:1:INT[4]` or `%DB42:28.0:BYTE[4]` for 
four elements is now written
+`holding-register:1[0..3]:INT` or `%DB42:28.0[0..3]:BYTE`; `[4]` before the 
data type selects the single element at
+index 4. Addresses in the old form no longer parse. This applies to the 
`tag.*` options, to `trigger` and to the
+addresses in the body of the producer.
+
+The S7 driver of PLC4X 1.0.0 no longer includes the `s7-light` driver, so 
`plc4x:s7-light://` endpoints no longer find
+a driver. Use `plc4x:s7://` instead.
+
+A consumer with `trigger` now reads the trigger tag and the tags in one 
request on the connection of the endpoint,
+every `period` milliseconds from the start of the route. Before, it sampled 
the trigger first after about one second,
+and read the tags in a later, separate request on another connection. A failed 
read is now reported to the exception
+handler of the consumer (or to the error handler of the route with 
`bridgeErrorHandler=true`), and polling continues;
+before, the consumer stopped triggering after the first failed read of the 
trigger. After a failed poll the remembered
+trigger value is unknown, so a trigger that is still `true` when the PLC 
returns emits once. Since the consumer now uses
+the connection of the endpoint, `autoReconnect` governs its reconnects. A 
connection that cannot be set up when the
+route starts fails the start of the route, unless `autoReconnect=true`, in 
which case the polls retry it. A route that
+starts with the CamelContext then fails the start of the CamelContext too, 
unless the route is supervised by the
+xref:route-controller.adoc[supervising route controller] or has 
`autoStartup=false`. As before, `period` must be
+positive when `trigger` is set. A trigger that reads as a value that is not a 
boolean, such as a number or an array,
+fails the poll. See the xref:components::plc4x-component.adoc[PLC4X component] 
for how triggered reading works.
+
+With `autoReconnect=true`, a lost connection is now closed and replaced with a 
new one from the driver instead of being
+reconnected, because the PLC4X 1.0.0 S7 and EtherNet/IP drivers cannot connect 
a connection again once it is lost. This
+applies to the producer and to the consumers alike.
+
+The component now removes its own options (`trigger`, `period`, `tag.*`, 
`autoReconnect` and the generic consumer
+options such as `exceptionHandler`) from the connection string it hands to the 
driver. Before, the driver received them
+as connection string parameters too.
+
+The constant `Constants.PLC_NAME`, which was only used with the PLC4X scraper, 
was removed.
diff --git a/parent/pom.xml b/parent/pom.xml
index 2646cc902415..2e25b597926c 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -448,7 +448,7 @@
         <phase2-lib-version>6.2.2</phase2-lib-version>
         <picocli-version>4.7.7</picocli-version>
         <pinecone-client-version>3.1.0</pinecone-client-version>
-        <plc4x-version>0.13.1</plc4x-version>
+        <plc4x-version>1.0.0</plc4x-version>
         <plexus-archiver-version>4.14.0</plexus-archiver-version>
         <plexus-build-api-version>1.2.0</plexus-build-api-version>
         
<plexus-container-default-version>2.1.1</plexus-container-default-version>

Reply via email to