Technical article
Industrial ingress that stays off by default: OPC UA DataValue normalization and VDA 5050 dispatch
AvevaOpcUaSrv and RobotVda5050Srv show the delivery pattern for plant-floor integration: native Camel endpoints, an industrial processor set that normalizes and guards, and configuration that refuses to connect until the site says so. OPC UA readings leave the route as standard JSON; VDA 5050 orders leave as compliant envelopes on configured topic patterns.
Plant protocols differ; the danger is connecting by accident
Every plant floor speaks a different protocol — OPC UA on the SCADA side, VDA 5050 over MQTT on the robot side. LightESB-Camel does not hide that behind a new abstraction: routes still use native Camel milo-client:, paho-mqtt5: and plc4x: endpoints, while the industrial processor set handles validation, masking, standard JSON and error mapping.
Routes hot-load on file change, so a sample that dials real equipment on load is a real incident. Both industrial samples therefore ship with server.running=false and PLACEHOLDER_CONFIGURE_IN_SITE credentials — the safe default is part of the template, and you flip it only after site configuration.
OPC UA: DataValue in, standard JSON out
AvevaOpcUaSrv enables the wrapper with system.components=undertowhttp,industrial and points at the server through industrial.* keys:
HTTP.Listener=true
server.running=false
server.port=19110
system.components=undertowhttp,industrial
industrial.opcua.endpoint.uri=tcp://127.0.0.1:4840
industrial.opcua.client.id=lightesb-aveva-opcua
industrial.opcua.read.node=ns=2;s=Plant.Area.Line1.Pump101.Speed
industrial.opcua.write.node=ns=2;s=Plant.Area.Line1.Pump101.SetPoint
industrial.opcua.username=PLACEHOLDER_CONFIGURE_IN_SITEThe telemetry route subscribes through milo-client:, then opcUaTelemetryNormalizeProcessor unwraps the Milo DataValue and rewrites the reading as standard JSON with a value field. The route declares the aveva-opcua-telemetry action with contract-stage=normalized bound to that processor ref — a oneWayConsumer that is not agent-callable and has no synchronous output schema.
<from uri="milo-client:{{industrial.opcua.endpoint.uri}}?node={{industrial.opcua.read.node}}&clientId={{industrial.opcua.client.id}}-reader"/>
<process ref="opcUaTelemetryNormalizeProcessor"/>
<toD uri="{{industrial.target.http.uri}}?httpMethod=POST&bridgeEndpoint=true&connectTimeout={{industrial.target.http.connectTimeout}}&socketTimeout={{industrial.target.http.socketTimeout}}"/>Write control is a fixed-target API: POST /api/aveva/opcua/write with {"value":55.5} writes only industrial.opcua.write.node. A body that carries node or topic is rejected — the protocol target is configuration, not input.
VDA 5050: envelope, topics and idempotency stay server-side
RobotVda5050Srv takes business commands (vda_order / vda_instant_action) through action and robot allowlists plus capability and online checks, then deduplicates by commandId. robotVdaOrderEnvelopeProcessor builds a compliant VDA 5050 body (baseline 3.0.0, not a LightESB envelope), filling version and manufacturer from robot.vda5050.* keys and serialNumber from the robotId.
Topics come only from configured patterns — uagv/{siteId}/{robotId}/order — never from the request body; payload fields like topic, node, register or service are rejected outright. A repeated commandId with identical content returns {"status":"duplicate"} without touching the sink; the same id with different content is refused. The sample is mock-only: server.running=false, mock: sinks, no broker.
robot.command.allowedActions=vda_order,vda_instant_action
robot.command.allowedRobotIds=amr-001
robot.vda5050.order.topic.pattern=uagv/{siteId}/{robotId}/order
robot.vda5050.instantAction.topic.pattern=uagv/{siteId}/{robotId}/instantActions
robot.mock.vda5050.order.sink=mock:robotVda5050OrderSinkVerify offline before you ever connect
Without a real server or broker, the doc-mock entry proves the boundary: a write carrying only value returns the fixed industrial.opcua.write.node summary with externalWriteInvoked=false, while adding a dynamic node answers 422 — a dynamic-target rejection:
curl -X POST "http://localhost:19189/api/doc-mock/aveva/opcua/write" \
-H "Content-Type: application/json" \
-d '{"value":55.5}'
curl -X POST "http://localhost:19189/api/doc-mock/aveva/opcua/write" \
-H "Content-Type: application/json" \
-d '{"value":55.5,"node":"ns=9;s=Override"}'The VDA 5050 package verifies the same way — one Maven command exercises topic patterns, required-field validation, dynamic-topic rejection and commandId idempotency, with no broker and no AMR:
mvn -q -pl lightesb-camel-core "-Dtest=RobotVda5050RouteTest,RobotExampleServicePackageTest" testOnly then comes real integration: configure the actual OPC UA server or MQTT 5 broker, load site credentials and certificates, flip server.running=true, confirm the route is STARTED, and check that the downstream receives the standard JSON.
What the mock proves — and what it does not
Offline verification proves route structure, the fixed-target boundary and error responses. It says nothing about field interoperability with a real OPC UA server, certificates, node permissions or PLC byte order — the delivery note must state which validations were done and which were not.
When something breaks, check in order: processor not found → system.components must contain industrial; route fails to start → server/broker address and credentials; MQTT wildcard topics → handle # per Camel URI rules; write rejected → the body must not specify node or topic.