FreeOpcUa / opcua-asyncio

OPC UA library for python >= 3.7
GNU Lesser General Public License v3.0
1.08k stars 353 forks source link

Can't connect asynchronously to opcua server #1426

Open Jackarnd opened 12 months ago

Jackarnd commented 12 months ago

I'm trying to get a value of a node in my OPCUA server.

I'm using asyncua on a windows 10 x64 computer. The server is on a PLC.

When I write this in a normal task it works

client = Client("opc.tcp://192.168.1.88:4840")
# client.max_chunkcount = 5000 # in case of refused connection by the server
# client.max_messagesize = 16777216 # in case of refused connection by the server
client.connect()

But when I use the basic example using an async function with the await the line,

await client.connect() Returns a timeoutError with asyncio.exceptions.CancelledError but nothing else to explain why it doesn't work.

I would've kept trying without the the await but when I try to get my value with client.nodes.root.get_child([...]) the returned value prints <coroutine object Node.get_child at 0x000002C38EE45E40> (when it should be a simple integer or boolean) and I don't know what to do with that so I guess I should keep going with the examples.

Do you have any idea why await client.connect() return this exception ?

I also tried with 2 different (and built under different langages) opcua clients just to be sure it wasn't the server that was broken. And the clients can connect properly.

The code can connect with await when I launch locally an opcua server using \Python311\Scripts>uaserver --populate

AndreasHeine commented 12 months ago

timeout happens if the server is not reachable or not responding to a request... the actual reason can only be guessed 😅

AndreasHeine commented 12 months ago

get_child under the hood sends a browse request to the server thats why you need to await it.

possibly every network request could fail so you could use try/excepts to catch the specific errors

Jackarnd commented 12 months ago

@AndreasHeine I tried connecting with 2 opcua client (Opc.Ua.SampleClient from OPC foundation and UaExpert) and they can connect properly so my server is reachable that's why I'm really wondering why I'm stuck. Also I can access the server when I don't use the await. If I run this code :

 client = Client("opc.tcp://192.168.1.88:4840")
client.connect()
root = client.get_root_node()client.connect()

I get the right identifier number. But when I try the same with an async def I can't connect

async def connect_to_opc_server():
    client = Client(url="opc.tcp://192.168.1.88:4840")
    client.name = "Re_Shoes"
    await client.connect()                    # Exception here
    root_node = await client.get_root_node()
    print("root node is: ", root_node)
AndreasHeine commented 12 months ago

@AndreasHeine I tried connecting with 2 opcua client (Opc.Ua.SampleClient from OPC foundation and UaExpert) and they can connect properly so my server is reachable that's why I'm really wondering why I'm stuck. Also I can access the server when I don't use the await. If I run this code :

 client = Client("opc.tcp://192.168.1.88:4840")
client.connect()
root = client.get_root_node()client.connect()

I get the right identifier number. But when I try the same with an async def I can't connect

async def connect_to_opc_server():
    client = Client(url="opc.tcp://192.168.1.88:4840")
    client.name = "Re_Shoes"
    await client.connect()                    # Exception here
    root_node = await client.get_root_node()
    print("root node is: ", root_node)

if so you probably blocking the eventloop...

if you try to run multiple clients in one python program you need to create a asyncio task for each which can run independently

Jackarnd commented 12 months ago

@AndreasHeine How can I unblock the eventloop ? (I'm sorry I'm just starting with Python and asyncua so not ideal I understand) At the end of my code I have this :

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(connect_to_opc_server())

Would it be the reason of all this fuss ?

Because my code is fairly simple and always include a client.disconnect() somewhere. So I'm not running multiple clients in one program.

AndreasHeine commented 12 months ago

maybe this could help you https://github.com/AndreasHeine/opcua-tutorial/blob/main/client/BaseClient.py

if you want to use asyncua you should look into the basics of python asyncio!

schroeder- commented 12 months ago

client.get_root_node() doesn't do anything with the server, so it does work if you are not connected. If you want to know why connectdoesn't work, activate logging and post the result.

Jackarnd commented 12 months ago

@schroeder- The code doesn't go to client.get_root_node() since it gets stuck at connect. Using @AndreasHeine 's code I get this exception at connect :

Exception has occurred: TimeoutError
exception: no description
asyncio.exceptions.CancelledError: 

The above exception was the direct cause of the following exception:

  File "C:\Users\...\Code\Python\OPCUA.py", line 24, in main
    await client.connect()
  File "C:\Users\...\Code\Python\OPCUA.py", line 42, in <module>
    asyncio.run(main())
TimeoutError: 

And the log gives nothing much :

Code\Python>  & 'C:\Users\...\AppData\Local\Programs\Python\Python311\python.exe' 'c:\Users\...\.vscode\extensions\ms-python.python-2023.14.0\pythonFiles\lib\python\debugpy\adapter/../..\debugpy\launcher' '25870' '--' 'c:\Users\...\Code\Python\OPCUA.py'
WARNING:asyncua.client.ua_client.UaClient:disconnect_socket was called but connection is closed
schroeder- commented 12 months ago

Are you sure you are using the correct hostname/ip and port?

AndreasHeine commented 12 months ago

this basic example has to work first:

import asyncio
import logging
from asyncua import Client, ua

logging.basicConfig(level=logging.WARNING)
_logger = logging.getLogger('asyncua')

async def main():
    client = Client(url="opc.tcp://127.0.0.1:48010", timeout=4)

    try:
        await client.connect()
    except Exception as e:
        print(e)
        return

    '''
    clientcode!

    '''

    try:
        await client.disconnect()
    except Exception as e:
        print(e)
        return

if __name__ == "__main__":
    asyncio.run(main())
Jackarnd commented 12 months ago

@schroeder- Yes it works on UA sample client and ua expert, that's why I 'm very confused. On both programs I'm able to browse through the nodes... image image

@AndreasHeine I tried this exemple you sent and I'm still stuck at connect() image

I may add that I'm working on visual studio code and my interpreter is set to the global python in my computer image

Jackarnd commented 12 months ago

@schroeder- I'm sure I'm using the right IP because when I change the port I get a different exception telling me the connection has been refused image

AndreasHeine commented 12 months ago

whats the logging output in the console?

you can set the loglevel from WARNING to DEBUG as well!

Jackarnd commented 12 months ago

@AndreasHeine

PS C:\Users\...\Code\Python>  c:; cd 'c:\Users\...\Code\Python'; & 'C:\Users\...\AppData\Local\Programs\Python\Python311\python.exe' 'c:\Users\...\.vscode\extensions\ms-python.python-2023.14.0\pythonFiles\lib\python\debugpy\adapter/../..\debugpy\launcher' '26121' '--' 'c:\Users\...\Code\Python\OPCUA.py' 
WARNING:asyncua.client.ua_client.UaClient:disconnect_socket was called but connection is closed
AndreasHeine commented 12 months ago

could you try:

logging.basicConfig(level=logging.DEBUG)

and repost output!

Jackarnd commented 12 months ago

@AndreasHeine Yes sorry, here it is :

DEBUG:asyncio:Using proactor: IocpProactor
INFO:asyncua.client.client:connect
INFO:asyncua.client.ua_client.UaClient:opening connection
INFO:asyncua.uaprotocol:updating client limits to: TransportLimits(max_recv_buffer=65536, max_send_buffer=65536, max_chunk_count=10, max_message_size=0)
INFO:asyncua.client.ua_client.UASocketProtocol:open_secure_channel
DEBUG:asyncua.client.ua_client.UASocketProtocol:Sending: OpenSecureChannelRequest(TypeId=FourByteNodeId(Identifier=446, NamespaceIndex=0, NodeIdType=<NodeIdType.FourByte: 1>), RequestHeader_=RequestHeader(AuthenticationToken=NodeId(Identifier=0, NamespaceIndex=0, NodeIdType=<NodeIdType.TwoByte: 0>), Timestamp=datetime.datetime(2023, 9, 7, 8, 34, 11, 975082), RequestHandle=1, ReturnDiagnostics=0, AuditEntryId=None, TimeoutHint=1000, AdditionalHeader=ExtensionObject(TypeId=NodeId(Identifier=0, NamespaceIndex=0, NodeIdType=<NodeIdType.TwoByte: 0>), Body=None)), Parameters=OpenSecureChannelParameters(ClientProtocolVersion=0, RequestType=<SecurityTokenRequestType.Issue: 0>, SecurityMode=<MessageSecurityMode.None_: 1>, ClientNonce=b'', RequestedLifetime=3600000))
INFO:asyncua.client.ua_client.UASocketProtocol:Socket has closed connection
WARNING:asyncua.client.ua_client.UaClient:disconnect_socket was called but connection is closed

I know some server don't like when max_chunk_count=10, max_message_size=0 are set to 0 or a low value so I tried to add this to my code

client.max_chunkcount = 5000
client.max_messagesize = 16777216

However max_chunk_count doesn't seem to be affected and stays at 10, maybe it comes from that ?

DEBUG:asyncio:Using proactor: IocpProactor
INFO:asyncua.client.client:connect
INFO:asyncua.client.ua_client.UaClient:opening connection
INFO:asyncua.uaprotocol:updating client limits to: TransportLimits(max_recv_buffer=65536, max_send_buffer=65536, max_chunk_count=10, max_message_size=16777216)
INFO:asyncua.client.ua_client.UASocketProtocol:open_secure_channel
DEBUG:asyncua.client.ua_client.UASocketProtocol:Sending: OpenSecureChannelRequest(TypeId=FourByteNodeId(Identifier=446, NamespaceIndex=0, NodeIdType=<NodeIdType.FourByte: 1>), RequestHeader_=RequestHeader(AuthenticationToken=NodeId(Identifier=0, NamespaceIndex=0, NodeIdType=<NodeIdType.TwoByte: 0>), Timestamp=datetime.datetime(2023, 9, 7, 8, 35, 24, 328039), RequestHandle=1, ReturnDiagnostics=0, AuditEntryId=None, TimeoutHint=1000, AdditionalHeader=ExtensionObject(TypeId=NodeId(Identifier=0, NamespaceIndex=0, NodeIdType=<NodeIdType.TwoByte: 0>), Body=None)), Parameters=OpenSecureChannelParameters(ClientProtocolVersion=0, RequestType=<SecurityTokenRequestType.Issue: 0>, SecurityMode=<MessageSecurityMode.None_: 1>, ClientNonce=b'', RequestedLifetime=3600000))
INFO:asyncua.client.ua_client.UASocketProtocol:Socket has closed connection
WARNING:asyncua.client.ua_client.UaClient:disconnect_socket was called but connection is closed
AndreasHeine commented 12 months ago

max_chunksize max_messagesize

will be negotiated while Hello/Ack Message

AndreasHeine commented 12 months ago

i am currently not on windows... not sure if "IocpProactor" is the correct EventLoop

there was a thing for post python 3.7 with the default eventloops

from sys import platform
from os import name

if __name__ == "__main__":
    if platform.lower() == "win32" or name.lower() == "nt":
        from asyncio import (
            set_event_loop_policy,
            WindowsSelectorEventLoopPolicy
        )
        set_event_loop_policy(WindowsSelectorEventLoopPolicy())
    asyncio.run(main())
Jackarnd commented 12 months ago

@AndreasHeine Still doesn't work, I tried on another computer and I still get the same output. I'm wondering if there isn't a problem in the hello/Ack message, I see that the chunk stays at 10 but what if my server can't negotiate and needs a bigger chunk ? Because on UAExpert the MaxChunkCount is set to 5000 and it can connect properly.

AndreasHeine commented 12 months ago

actually the server decides in last position what to use for the communication. the client just sends his possible max and the server uses them if possible or lower if the server cant handle large messages. even if UA Expert shows the setting at 5k does it not mean that it actually uses it...

if there is an issue with that there should be an UaError of some kind but you get an socket error after you try to create a secure channel (OpenSecureChannelRequest) which does not get an response, so the issue might be there!

OPC UA Reference: https://reference.opcfoundation.org/Core/Part6/v104/docs/7.1.2.3 https://reference.opcfoundation.org/Core/Part6/v104/docs/7.1.2.4

AndreasHeine commented 12 months ago

do you have access to the logs of the server? maybe there is a hint why it closes the connection!?

maybe the server does not like a empty Nonce -> ClientNonce=b''

Jackarnd commented 12 months ago

@AndreasHeine I am trying to get in touch with the manufacturer regarding this, still waiting...

How can I modify the hello message ? Or just not send it ? Because in UAExpert's log I don't see a hello message being sent. I found this in the client.py file in C:\Users\...\AppData\Local\Programs\Python\Python311\Lib\site-packages\asyncua\client. Is it what I'm supposed to modify or is it something else somewhere ?

async def send_hello(self):
        """
        Send OPC-UA hello to server
        """
        ack = await self.uaclient.send_hello(self.server_url.geturl(), self.max_messagesize, self.max_chunkcount)
        if isinstance(ack, ua.UaStatusCodeError):
            raise ack

FYI : here's the connection log of UAExpert image

AndreasHeine commented 12 months ago

even if you modify the settings in the client the revised values will be dictated by the server! i assume the open secure channel with empty nonce could be a reason for the server to drop the connection!

Jackarnd commented 12 months ago

@AndreasHeine I'm sorry I'm not sure what you mean, how do I change the nonce or even open a not secure channel? In all the program I've used, I have never used a secure connection. For example in UaExpert these settings are used (and they work) : image

AndreasHeine commented 12 months ago

https://reference.opcfoundation.org/api/image/get/18/image016.png

https://reference.opcfoundation.org/Core/Part4/v105/docs/5.5

Jackarnd commented 12 months ago

@AndreasHeine So I have made some researches based on what you've told me and I tried multiple things and it was indeed due to the nonce being empty. Thanks to another issue discussion on the freeopcua lib I added this code before the connect() method and it works, I can now connect.

    client.security_policy.secure_channel_nonce_length = 8
    def signature(self, data):
        return None
    fixed_signature = types.MethodType(signature, CryptographyNone)
    client.security_policy.asymmetric_cryptography.signature = fixed_signature

I think I should leave this open as it's still opened in the freeopcua repo and doesn't seem to have been fixed in asyncua ?

schroeder- commented 12 months ago

I check for the source. It looks like os.urandom(0) generates None on some platforms and on others it creates b''. This translates to either -1 or 0 as nonce. As of the docs in python 3.11 the urandom source got changed on windows.

Jackarnd commented 11 months ago

@schroeder- I'm on windows, os.random(0) returns b''. I have tried to bypass os.random(0) in the create.nonce function and make it return a None and the result is the same. So I think my server just doesn't like something other than a positive sized string...

schroeder- commented 11 months ago

I like to fix this issues somehow. But I fear some servers will followed the specs and raise an error if we send a nonce, that is not null. Maybe we add a parameter to client class?

Jackarnd commented 11 months ago

@schroeder- Maybe that would be the best option an optional argument with a null default value. If you need me to try something I have the server for about two more weeks !