SCADA-LTS / Scada-LTS

Scada-LTS is an Open Source, web-based, multi-platform solution for building your own SCADA (Supervisory Control and Data Acquisition) system.
GNU General Public License v2.0
744 stars 292 forks source link

Support for the OPC UA protocol using the PLC4X library #2119

Open Limraj opened 2 years ago

Limraj commented 2 years ago

Expected Behavior Support for the OPC AU(OPC Unified Architecture) protocol using the PLC4X library.

Actual Behavior OPC AU protocol no supported.

Links https://plc4x.apache.org/ https://opcfoundation.org/about/opc-technologies/opc-ua/ https://plc4x.apache.org/plc4x/latest/users/protocols/opcua.html

Specifications Version: 2.6.12

RenatoExpert commented 8 months ago

How is it going?

RenatoExpert commented 7 months ago

Hi @Limraj ! Siema? I've been leaning and experimenting a lot on OPC UA protocol, specially on TCP binary encoding. At first have tried to use libraries like The one from OPC Foundation and many others, including PLC4X. I am really not being good at Java Libraries.

I know Java language, compilation and most technical stuff related, I am have issues implementing from any library. I did an OPC study by myself, but implementing from zero costs too much more time than using libs. Here is the sample https://github.com/RenatoExpert/opc-web

I don't know which class to instanciate and stuff like this. On JS NPM, we always find simple examples that are enough to implement most of a library functionality. But its not being the case on java. For me, there is just a JAR file and classes descriptions that don't help me at all. I've been trying also reading directly from source codes, but also not very effective, since one class may need another - that may need another and so on...

Could you help me on this implementation? I could dedicate a bunch of days to work on this! I just need a little help...

Limraj commented 7 months ago

Hi @RenatoExpert,

    public static void main(String[] args) throws PlcConnectionException {
        PlcConnection plcConnection = PlcDriverManager.getDefault()
                .getConnectionManager()
                .getConnection("opcua:tcp://127.0.0.1:50000");

        if(plcConnection.isConnected()) {
            logger.info("CONNECTED");
        }

        PlcBrowseRequest plcBrowseRequest = plcConnection.browseRequestBuilder().build();
        CompletableFuture<? extends PlcBrowseResponse> plcResponse = plcBrowseRequest.execute();

        plcResponse.thenAccept(a -> {
            logger.info("Res: " + a);
        });
    }

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>opcua</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.plc4x</groupId>
            <artifactId>plc4j-api</artifactId>
            <version>0.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.plc4x</groupId>
            <artifactId>plc4j-driver-opcua</artifactId>
            <version>0.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.16.1</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-buffer</artifactId>
            <version>4.1.108.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.14.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.12</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-common</artifactId>
            <version>4.1.108.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.plc4x</groupId>
            <artifactId>plc4j-transport-tcp</artifactId>
            <version>0.12.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <configuration>
                    <usedDependencies>
                        <dependency>org.slf4j:slf4j-simple</dependency>
                    </usedDependencies>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

docker-compose.yml:

version: '3'

services:
  opcua_server:
    image: mcr.microsoft.com/iotedge/opc-plc:latest
    ports:
      - 50000:50000
    expose: ["50000"]
    command: "--autoaccept"

We have progress, there is communication with the server: App(Main.java) log: image OPC Server log: image

but I get an error later: org.apache.plc4x.java.api.exceptions.PlcProtocolException: Server returned error BadSecurityPolicyRejected (0x80550000) image

I can't deal with it right now, maybe you can figure it out.

Links: OPC UA server doc OPC UA plc4x

Regards, Kamil Jarmusik

RenatoExpert commented 7 months ago

Could you send the entire Java code? I think my main issue is on imports... I know how to search inside lib to find import path, but its not working here... Compilation gets fine, but when I run code it gets errors like this on module classes:

opc_client  | Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/plc4x/java/api/PlcDriverManager
opc_client  |   at org.scadalts.Main.main(Main.java:14)
opc_client  | Caused by: java.lang.ClassNotFoundException: org.apache.plc4x.java.api.PlcDriverManager
opc_client  |   at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
opc_client  |   at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
opc_client  |   at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:527)
opc_client  |   ... 1 more
opc_client exited with code 1
Limraj commented 7 months ago

Hi @RenatoExpert,

You have all the necessary dependencies in the pom.xml file.

  1. First: image
  2. Then in Intellij, on the name of the "unknown" class, click alt and enter and choose to import the classes that have plc4x in the package name, if they appear on the list: image But sometimes there is a problem with this.

Regards, Kamil Jarmusik

RenatoExpert commented 7 months ago

Thank you @Limraj ! I will dedicate these days on this target, your suggestions are really valuable.

RenatoExpert commented 7 months ago

Status report

Some news, just to let you know my progress.

  1. Intellij helps a lot. I got success on running libraries on it.
  2. I discovered that my compilation process outside Intellij had some issues and fixed them. So its working in the same way on terminal also.
  3. Here I get an exception that says browse is not supported for OPC UA

Demonstration

Code

String url = "opcua:tcp://173.183.147.103:48010/?discovery=false";
try {
    PlcDriver driver = PlcDriverManager.getDefault().getDriver("opcua");
    PlcConnection connection = driver.getConnection(url);
    PlcBrowseRequest.Builder builder = connection.browseRequestBuilder();
    PlcBrowseRequest request = builder.build();
    PlcBrowseResponse response = request.execute().get(5000, TimeUnit.MILLISECONDS);

https://github.com/RenatoExpert/4x-experiment/blob/4a97947c2e76c8e826574520074a2bada23ac083/src/main/java/org/scadalts/Main.java

Output

23:48:05.890 [Thread-0] INFO org.apache.plc4x.java.DefaultPlcDriverManager -- Instantiating new PLC Driver Manager with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@277050dc
23:48:05.909 [Thread-0] INFO org.apache.plc4x.java.DefaultPlcDriverManager -- Registering available drivers...
23:48:05.937 [Thread-0] INFO org.apache.plc4x.java.DefaultPlcDriverManager -- Registering driver for Protocol opcua (Opcua)
The connection does not support browsing
org.apache.plc4x.java.api.exceptions.PlcUnsupportedOperationException: The connection does not support browsing
opc_client exited with code 0

Testing

Other issues

Limraj commented 7 months ago

Hi @RenatoExpert, what is this 173.183.147.103 ?

We want to use some local opc server, preferably accessible from docker, so that we can test and have full control over the configuration of this server.

PlcDriver driver = PlcDriverManager.getDefault().getDriver("opcua");
PlcConnection connection = driver.getConnection("opcua:tcp://127.0.0.1:50000");

This code does not open the connection because the call to the connect() method on the PlcConnection object is missing. In your code you can enter any opc server address that does not exist and you will get the same error in the logs.

However, the call: ...getConnectionManager().getConnection("opcua:tcp://127.0.0.1:50000"); It already returns a PlcConnection object after calling the connect() method: image

That is, in your code it actually went further, but because it did not try to connect to the opc server.

You can use some external opc server and see if it can connect to it, but it's best to use this proposed opc server with docker-compose (The application and opc server logs show that the connection was successfully established, the server responds to the operation of our java application, but there is a problem with BadSecurityPolicyRejected (0x80550000)) and try to solve the problem I mentioned, and use the connection code I proposed, or use your code, but first you need to call the connect() method on the PlcConnection object:

PlcDriver driver = PlcDriverManager.getDefault().getDriver("opcua");
PlcConnection connection = driver.getConnection("opcua:tcp://127.0.0.1:50000");
connection.connect();

However, it was determined that the OpcuaPlcDriver implementation does not support the Browse operation, so you need to use, for example PlcReadRequest, just like you have in your project.

And try with discovery=true, There appears to be support for this operation: image

And support other operations: image

First, it's best to try to reproduce the state of my problem(BadSecurityPolicyRejected (0x80550000)), because I don't know if you managed to recreate it. Try to focus on the essence of the problem, do not get distracted by other things.

Regards, Kamil Jarmusik

Limraj commented 7 months ago

Update: with opcuaserver.com:

        try (PlcConnection connection = PlcDriverManager.getDefault()
                .getConnectionManager()
                //.getConnection("opcua:tcp://127.0.0.1:50000?discovery=false");
                .getConnection("opcua:tcp://opcuaserver.com:48010?discovery=false")) {

            if (connection.isConnected()) {
                logger.info("CONNECTED");
            }

            PlcReadRequest request = connection.readRequestBuilder().build();
            CompletableFuture<? extends PlcResponse> plcResponse = request.execute();
            PlcResponse response = plcResponse.get(5000, TimeUnit.MILLISECONDS);
            logger.info("Res: " + response);
        }

I managed to connect and response: image

And discovery=true: image

You could compare the logs from the connection to the server from docker-compose and the successful connection to opcuaserver.com to determine what is going wrong, or you can look for information on the Internet about BadSecurityPolicyRejected (0x80550000), this does not appear to be a library specific error.

Regards, Kamil Jarmusik

RenatoExpert commented 7 months ago

Hi @Limraj So, as suggested, today I will work on including that image on compose and get information about BadSecurityPolicyRejected.

Some explanation about I was doing

what is this 173.183.147.103 ?

At first I made a python server with FreeOpcUa python library. The goal was to make a very customized opc server that I could debug. I publish it on an AWS EC2 container, as you can see on code.

After getting connection error, I thought it could be some limitation or bug with library. So I tried to use something else, just to make comparison. I found some public server address that may be useful for occasional tests. http://opcuaserver.com/

As error didn't change, I thought it could be something related to domain, then I used that public server IPv4 address instead of DNS url.

I agree that a good customized local server on compose is the best practice, so I am going to work on it right now.

RenatoExpert commented 7 months ago

About secure policy

It seems that Iot Edge do not work with SecurityMode=None. A read in a line of their docs

Note: Make sure that your OPC UA client uses security policy Basic256Sha256 and message security mode Sign & Encrypt to connect.

So, I understand that we need to set certificate and crypt stuff before connect into it.

I think starting things from easier is good, but at same time its a very interesting software. Whats is your opnion: do you think about replacing it or keep working on Edge @Limraj ?

For a while, I will try to reproduce your last code on opcuaserver.com

Limraj commented 7 months ago

@RenatoExpert Both servers will be useful to us, one for connecting without a certificate, and the other case with a certificate. If we have a connection without a certificate, it is worth working on one with a certificate, you can also try basic writing and reading operations in this simple program, then having knowledge of how to communicate with opc servers using plc4x, we can move on to implementing Data Source in Scada-LTS. You can also check if there is any way to configure this server http://opcuaserver.com/ to force it to communicate with the certificate.

RenatoExpert commented 7 months ago

Oh happy day! 🎶 🎶 Read a OPC address with success!! image

Setup

I got sucess using that python as opc server. Commit is this one https://github.com/RenatoExpert/4x-experiment/commit/b54b6284bb12fad89a8a797387b0f050fbf2326b

Code

            String url = "opcua:tcp://server:4840?discovery=false";
            System.out.println("Starting Thread");
            try {
                PlcConnection connection = PlcDriverManager.getDefault().getConnectionManager().getConnection(url);
                if (connection.isConnected()) {
                    System.out.println("Connected with success!");
                } else {
                    System.out.println("Connection not estabilished!");
                }

                boolean canRead = connection.getMetadata().isReadSupported();
                if (canRead) {
                    System.out.println("Read function is supported!");
                    PlcReadRequest.Builder builder = connection.readRequestBuilder();
                    builder.addTagAddress("Pressure", "ns=2;i=11");
                    PlcReadRequest request = builder.build();
                    CompletableFuture<? extends PlcReadResponse> responseFuture = request.execute();
                    PlcReadResponse response = responseFuture.get(5000, TimeUnit.MILLISECONDS);
                    System.out.println("Response:" + response);
                    for (String tagName: response.getTagNames()) {
                        System.out.println(tagName);
                        System.out.println(response.getObject(tagName));
                    }
                } else {
                    System.out.println("Read function is NOT supported!");
                }

Output

opc_client  | Pressure
opc_client  | 15.6

Somethings I learned

Discover mode only works when server has certificate. Even if it wont be used(security policy=none) it must show something. I guess even an all bits on (xFF xFF xFF xFF...) certificate works. Its probably because binary protocol needs a fixed length of bytes on certificate field.

Limraj commented 7 months ago

@RenatoExpert The key is to connect to the certificate, because as we can see, some servers may require it and it cannot be turned off, from what I understand what you wrote. However, we have an example of an opc server that requires this certificate and one that does not.

As for the code, I have a few comments:

  1. In java we don't use static fields as variables, with the addition of the final modifier we use static fields to represent constants. In this situation, it would be good to create a class implementing the Runnable interface. Besides, there is too much going on in this method, but I understand that this is just a quick step.

  2. When creating objects of classes implementing the interface extends AutoCloseable(e.g. PlcConnection) or Closable (e.g. Socket class) interface, it is worth using structures: try-with-resources https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html A block that does not require an explicit call to the close method to free up valuable resources, busy ports. Be mindful of freeing up resources.

  3. I wouldn't use the word Socket, at such a high level of abstraction, sockets are used at a lower layer. A socket is an operating system mechanism that enables two-way interprocess communication. (https://www.ibm.com/docs/en/aix/7.3?topic=concepts-sockets) plc4x library uses netty components tcp transport, and therefore probably also operating system sockets. image image image A method with a 0 at the end of the method name denotes a reference to the native code of the operating system. image

  4. For readability reasons, in Java we use Builder in a sequence of calls; this pattern excludes the possibility of null appearing in any of the builder methods, because such methods usually include "return this". Of course, there are more complex builders, but they too should not return null at any level until the build method is called. What is more concise and readable is:

    PlcReadRequest.Builder builder = connection.readRequestBuilder();
    builder.addTagAddress("Pressure", "ns=2;i=11");
    PlcReadRequest request = builder.build();

    is this:

    PlcReadRequest request = connection.readRequestBuilder()
                                   .addTagAddress("Pressure", "ns=2;i=11")
                                   .build();

    With a more extensive object creation procedure calling more methods from the builder, this is even more clear.

Regards, Kamil Jarmusik

RenatoExpert commented 7 months ago

Well, I did an cleaning update with these tips https://github.com/RenatoExpert/4x-experiment/commit/8701be27f320c829260a4cf8d3f1651b830ca6ab

Limraj commented 7 months ago

Hi @RenatoExpert, I'm glad you were able to apply these subpoints.

  1. It's worth trying to connect to:

    version: '3'
    services:
    opcua_server:
    image: mcr.microsoft.com/iotedge/opc-plc:latest
    ports:
      - 5000:5000
    expose: ["5000"]
    command: "--autoaccept"

    It can be configured in a simple way via the command property.

  2. The simplest way to use docker-compose is that we use ready-made images and not create an image locally and then use it in docker-compose. We strive for simplicity, not multiplication of steps. Unless I misunderstood your solution.

  3. If you have no idea how to name a variable and you have to use words like 'raw', you name it like a type, only with a first lowercase letter.

  4. To create a human-readable string representation of an object, simply override the toString method in the class. Then you throw the class object with the overridden toString method into System.out.println(objectWithOverrideToString); Additionally, if the only function of the class is to override the toString method, you can call it PlcResponseToString, or if you want to add a printing method that uses the String generating method, which is the essence of this class, you can call it PrintPlcResponse.execute() -> {System.out. println(objectWithOverrideToString);}

An alternative solution is to create a static method that accepts a PlcResponse and generates a string, which can then be displayed in another method.

Regards, Kamil Jarmusik

RenatoExpert commented 7 months ago

Hello again, I am testing stuff on source code, and have a small issue: some classes seem to not exist at all. Example from OPC https://github.com/SCADA-LTS/Scada-LTS/blob/ed3ced76f529e8a8faf810eedd3c68b37d1f56a5/src/br/org/scadabr/rt/dataSource/opc/OPCDataSource.java#L10-L11 Where may I find them in source code?

Limraj commented 7 months ago

Hi @RenatoExpert, We have implemented support for the OPC DA protocol... hence the classes with OPC in the name. https://opcfoundation.org/developer-tools/specifications-classic/data-access/

I don't know exactly what you're asking... Have you managed to complete step 1, connect with certificate?

Regards, Kamil Jarmusik