Issue with adding PLC4X libraries for S7 Protocol implementation

Hello everyone!
I am working on the implementation of S7 protocol communication between plc and openems component. For that I added two dependencies into cnf/pom.xml:

<!-- S7 -->
<dependency>
    	<groupId>org.apache.plc4x</groupId>
    	<artifactId>plc4j-api</artifactId>
    	<version>0.10.0</version>
</dependency>
<dependency>
      	<groupId>org.apache.plc4x</groupId>
      	<artifactId>plc4j-driver-s7</artifactId>
      	<version>0.10.0</version>
      	<scope>runtime</scope>
</dependency>

and added two bundles in my bnd.bnd file:

-buildpath: \
	${buildpath},\
	...
	org.apache.plc4x.plc4j-api,\
	org.apache.plc4x.plc4j-driver-s7

-testpath: \
	${testpath}

Also after resolving EdgeApp.bndrun org.apache.plc4x.plc4j-api was added to EdgeApp.bndrun -runbundles: section automatically. But when I am trying to establish connection I receive “unable to find driver for protocol ‘s7’” error:

he.plc4x.java.PlcDriverManager] Instantiating new PLC Driver Manager with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@42110406
2023-10-03T11:29:15,309 [32b46a2)] INFO  [he.plc4x.java.PlcDriverManager] Registering available drivers...
org.apache.plc4x.java.api.exceptions.PlcConnectionException: Unable to find driver for protocol 's7'
	at org.apache.plc4x.java.PlcDriverManager.getDriver(PlcDriverManager.java:124)
	at org.apache.plc4x.java.PlcDriverManager.getDriverForUrl(PlcDriverManager.java:139)
	at org.apache.plc4x.java.PlcDriverManager.getConnection(PlcDriverManager.java:76)
	at io.openems.edge.ess.veloce.s7.S7ReadHelper.connect(S7ReadHelper.java:53)
	at io.openems.edge.ess.veloce.s7.S7ReadHelper.<init>(S7ReadHelper.java:45)
	at io.openems.edge.ess.veloce.s7.S7ReadWorker.<init>(S7ReadWorker.java:23)
	at io.openems.edge.ess.veloce.core.CoreVeloceImpl.activate(CoreVeloceImpl.java:60)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)

The problem is that Drivers were not activated. When I am adding org.apache.plc4x.plc4j-driver-s7 bundle manually to EdgeApp.bndrun (or just creating object using the second dependency:

import org.apache.plc4x.java.s7.readwrite.S7Driver;
S7Driver driver = new S7Driver();

in my component), and trying to resolve EdgeApp.bndrun, then I receive an error:

Resolution failed. Capabilities satisfying the following requirements could not be found:
    [<<INITIAL>>]
      ⇒ osgi.identity: (osgi.identity=io.openems.edge.ess.veloce)
          ⇒ [io.openems.edge.ess.veloce version=1.0.0.202310030815]
              ⇒ osgi.wiring.package: (&(osgi.wiring.package=org.apache.plc4x.java.s7.readwrite)(version>=0.10.0)(!(version>=1.0.0)))
                  ⇒ [org.apache.plc4x.plc4j-driver-s7 version=0.10.0]
                      ⇒ osgi.wiring.package: (osgi.wiring.package=org.apache.plc4x.java.osgi)
    [osgi.cmpn version=7.0.0.201802012110]
      ⇒ osgi.unresolvable: (&(must.not.resolve=*)(!(must.not.resolve=*)))
    [ch.qos.logback.classic version=1.2.3]
      ⇒ osgi.wiring.package: (&(osgi.wiring.package=ch.qos.logback.core.util)(version>=1.2.0)(!(version>=2.0.0)))
    [org.slf4j.api version=2.0.6]
      ⇒ osgi.extender: (&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))
    [slf4j.api version=2.0.5]
      ⇒ osgi.extender: (&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))

It looks like there’s a conflict in slf4j version used by the plc4x libraries (MANIFEST.MF Import-Package: …,org.slf4j;version=“[2.0,3)”) and slf4j version used in openems project, so I tried to exclude this transitive dependency in the cnf/pom.xml

<dependency>
      	<groupId>org.apache.plc4x</groupId>
      	<artifactId>plc4j-driver-s7</artifactId>
      	<version>0.10.0</version>
      	<scope>runtime</scope>
	<exclusions>
        	<exclusion>
            		<groupId>org.slf4j</groupId>
            		<artifactId>slf4j-api</artifactId>
        	</exclusion>
    	</exclusions>
</dependency>

but it didn’t fix the issue.
Moreover, when I tried to downgrade versions of the plc4x libraries to those ones which use slf4j version (0.7.0 version, MANIFEST.MF Import-Packages: …org.slf4j;version=“[1.7,2)” in the rank from the error ( osgi.extender: (&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))), problem remains the same.
I’m not an OSGI expert, so any help in solving this problem would be greatly appreciated.

Thanks in advance!

Best regards,
Mykola Skydan

Hi Mykola,

I have no experience with PLC4J, but I know that it is often tricky to add external dependencies to OSGi. I found this thread in the PLC4x archive, but it’s not of much help: Re: Help required on Beckhoff PLC connection to a JAVA OSGi Framework project

I did a quick test, adding following entries to the cnf/pom.xml:

		<dependency>
			<groupId>org.apache.plc4x</groupId>
			<artifactId>plc4j-api</artifactId>
			<version>0.11.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.plc4x</groupId>
			<artifactId>plc4j-driver-s7</artifactId>
			<version>0.11.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.plc4x</groupId>
			<artifactId>plc4j-osgi</artifactId>
			<version>0.11.0</version>
		</dependency>

Then I used the io.openems.edge.application bundle for a test. I added to bnd.bnd-buildpath:

	org.apache.plc4x.plc4j-api,\
	org.apache.plc4x.plc4j-driver-s7,\
	org.apache.plc4x.plc4j-osgi

Then I am able to call in the activate()-method:

		S7Driver driver = new S7Driver();
		System.out.println(driver.toString());

Resolving EdgeApp.bndrun works for me and I can also execute this code block without exceptions.

Does that help?

Regards,
Stefan

Hi Stefan,
Thanks for your response
Yes, I used this library (plc4j-osgi), but than another error appeared, with Transport object. That’s all because of visibility of resources between isolated class-loaders

Ok. Can you give more details on the exact error message? Or how can I trigger the error with “Transport object”, that you mention?

Error message is
org.apache.plc4x.java.api.exceptions.PlcConnectionException: Unsupported transport tcp at org.apache.plc4x.java.s7.readwrite.S7HGeneratedDriverBase.getConnection(S7HGeneratedDriverBase.java:99),
you can reproduce it by trying get PlcConnection from the driver:

try {
String connectionString = “s7://10.0.0.1:101”;
S7Driver driver = new S7Driver();
PlcConnection plcConnection = driver.getConnection(connectionString);
} catch (PlcConnectionException e) {
e.printStackTrace();
}

Generally, as I know the correct way to get connection is to call getConnection method from new DefaultPlcDriverManager(), not directly from the driver, but then there will be “Unable to find driver for protocol ‘s7’” error.

It’s always difficult to include libraries in OSGi, that use Classpath-magic. There might be native ways to enable this behaviour inside OSGi, but if I were you, I would try the following:

When you follow S7HGeneratedDriverBase.getConnection(), internally it creates an Instance of S7HDefaultNettyPlcConnection:

        return new S7HDefaultNettyPlcConnection(
            canPing(), canRead(), canWrite(), canSubscribe(), canBrowse(),
            getTagHandler(),
            getValueHandler(),
            configuration,
            channelFactory,
            secondaryChannelFactory,
            fireDiscoverEvent,
            awaitSetupComplete,
            awaitDisconnectComplete,
            awaitDiscoverComplete,
            getStackConfigurer(transport),
            getOptimizer(),
            getAuthentication());

There is not much magic here. All the inherited methods are coming from GeneratedDriverBase (inside the plc4j-spi dependency). Add following depencency in pom.xml:

<dependency>
	<groupId>org.apache.plc4x</groupId>
	<artifactId>plc4j-spi</artifactId>
	<version>0.11.0</version>
</dependency>

So, I would just inherit from GeneratedDriverBase and create my own instance of S7HDefaultNettyPlcConnection. This way I avoid all the Classpath magic and get a proper Connection class.

Does that help you in anyway?

Regards,
Stefan

Hello Stefan,

Thank you again for the time you spend on this topic.

Actually I’ve tried already to extend GeneratedDriverBase to avoid getting Transport from the ClassLoader (sorry for not mentioning this earlier)

        Transport transport = null;
        ServiceLoader<Transport> transportLoader = ServiceLoader.load(
            Transport.class, Thread.currentThread().getContextClassLoader());
        for (Transport curTransport : transportLoader) {
            if (curTransport.getTransportCode().equals(transportCode)) {
                transport = curTransport;
                break;
            }
        }
        if (transport == null) {
            throw new PlcConnectionException("Unsupported transport " + transportCode);
        }

I just used new TcpTransport() object, and that’s how I was able to create my own instance of DefaultNettyPlcConnection (I am using 0.9.0 version of plc4x libraries, but I also tried with the latest ones), but I got other errors trying to get PlcReadResponse from this connection (with null protocol in the DefaultNettyPlcConnection object, so I was forced to add new S7ProtocolLogic directly in it, then with null S7DriverContext and so on).

And as I understood there’s no need to add such dependencies as plc4j-spi or plc4j-transport-tcp into pom.xml directly, they are transitive dependencies of plc4j-driver-s7 and already available after adding plc4j-driver-s7. But I also tried to add them directly, and it didn’t help.

Overall, it seems to me that without enabling all plc4j features natively this will not work, and it is strange that it is so difficult to do because I see DriverActivator and TransportActivator from plc4j-osgi being executed during EdgeApp.bndrun startup and how I I realized they should add the Driver and Transport objects to the ClassPath.

Thank you again

Best regards,
Mykola

Hello, I am committer of Apache PLC4X project, I have contributed some work there, between others the CAN and CANopen integration. The OSGi integration was made a few years back, mainly to serve Apache Camel integration which dropped support for OSGi a year ago or so. Long story short - this layer is not very well tested.
On counter side my company publish a plc4x-extras project which we use with openHAB addon development.

The main point of this extras repo is to ensure that plc4x can be deployed in OSGi runtime. Since openHAB rely on Karaf it differs from openEMS Edge, but you can source list of bundles here: plc4x-extras/features/org.connectorio.plc4x.extras.feature.osgi/src/main/feature/feature.xml at master · ConnectorIO/plc4x-extras · GitHub.

As you can see there are some adjustments with plc4x manifests done through wrap: protocol, to address some of import ranges etc… Finally, due to the way how drivers are initialized I also made a dedicated driver manager. Earlier it was managing classloader of drivers, nowadays its simply rely on published services.