FreeOpcUa / opcua-asyncio

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

Setting publishing interval of subscription doesn't change behaviour #1561

Closed f-ritter closed 5 months ago

f-ritter commented 5 months ago

I use a B&R PLC as OPC UA Server and changed the sampling interval of one tag to 50ms. The tag is subscribed by opcua-asyncio. (see code below)

Current behaviour: The Python Client still respects the default timer of 1s Expected behaviour: The python client gets a new value every ~50ms

Tag: image Default timers: image

But if I change the default timer to 50ms (from Timer7 to Timer3), the python client works as desired. (But only this one Tag should have an interval of 50ms) If I then change the tag to a higher limit e.g. to 10s, it also behaves as desired. I tested the same with UaExpert as a Client:

Client Default timer Tag timer result
Python 1000ms 50ms every ~1s
UA-Expert 1000ms 50ms every ~200ms
Python 50ms default every ~50ms
UA-Expert 50ms default every ~200ms
Python 50ms 10s every ~10s
UA-Expert 50ms 10s every ~10s

I use the following code to make a subscription:

class MyHandler(SubHandler):
    def __init__(self):
        self._queue = asyncio.Queue()

    def datachange_notification(self, node: Node, value, data: DataChangeNotif) -> None:
        self._queue.put_nowait([node, value, data])
        print(f'Data change notification was received and queued.')

    async def process(self) -> None:
        try:
            while True:
                # Get data in a queue.
                [node, value, data] = self._queue.get_nowait()
                path = await node.get_path(as_string=True)

                print(f'New value {value} of "{path}" was processed.')

        except asyncio.QueueEmpty:
            pass

async def main() -> None:
    async with Client(url=ENDPOINT) as client:
        node = client.get_node(ua.NodeId(Identifier='::001234:test1', NamespaceIndex=6))

        handler = MyHandler()
        subscription = await client.create_subscription(handler=handler, period=0)
        print(subscription.parameters)
        await subscription.subscribe_data_change(node)

        while True:
            await handler.process()
            await asyncio.sleep(0.05)

My print statement returns the following subscription values:

Revised values returned differ from subscription values: CreateSubscriptionResult(SubscriptionId=1473704468, RevisedPublishingInterval=50.0, RevisedLifetimeCount=10000, RevisedMaxKeepAliveCount=2700)
CreateSubscriptionParameters(RequestedPublishingInterval=50.0, RequestedLifetimeCount=10000, RequestedMaxKeepAliveCount=54000, MaxNotificationsPerPublish=10000, PublishingEnabled=True, Priority=0)

I don't know why it says RequestedPublishingInterval=50.0, I set it to 0.

Ua Expert logs the following settings:

Creating new subscription: ClientHandle=17, PublishingEnable=1, LifeTimeCount=360, MaxKeepAliveCount=10, Priority=0, PublishingInterval=50
Revised values: LifeTimeCount=360, MaxKeepAliveCount=10, Priority=0, PublishingInterval=50, SubscriptionId=1473704473
Item [NS6|String|:::001234:test1] succeeded : RevisedSamplingInterval=200, RevisedQueueSize=1, MonitoredItemId=1 [ret = Good]

I also can only change the PublishingInterval, but the RevisedSamplingInterval stays at 200ms. Is this the limiting factor for python as well? Does the sampling interval respect the default timer?

Versions Python-Version: 3.11.4 opcua-asyncio Version: 1.0.6 AS Runtime: C4.92

AndreasHeine commented 5 months ago

revised values come from server!

AndreasHeine commented 5 months ago

Expected behaviour: The python client gets a new value every ~50ms

You create a MonitoredItem and tell him Sample every 50ms -> so every 50ms a Notification will be Queued, thats the Samplinginterval.

The PublishingInterval is how often the Subscriptions Queue gets delivered to the Client.

So you will have values each PublishingInterval (e.g. 1s) -> which should contain 1000ms/50ms notifications (+/- 1 or 2 depends on the timing and latency)

f-ritter commented 5 months ago

As shown above, the RevisedPublishingInterval and the RequestedPublishingInterval are 50ms.

The exact same code works as expected, if I change the default timer serverside. Other Clients work as expected even without changing the default timer.

Therefore I assume a client issue. Or can you tell me what change I have to make to get a sample every ~50ms with default timer 1000ms and tag timer 50ms like with UaExpert?

AndreasHeine commented 5 months ago

what do you mean with default timer?

the client just asks the server to send him the queued changes like letters in a mailbox. the transport interval is the publishing interval the sample rate is just the rate of how many messages are in the mailbox!

i dont think uaexpert shows all values tbh 50ms is kind of hard to see... guess they skip some while rendering.

AndreasHeine commented 5 months ago

OPC UA Subscription = Long polling

edit: https://documentation.unified-automation.com/uasdkhp/1.4.1/html/_l2_ua_subscription.html

f-ritter commented 5 months ago

what do you mean with default timer?

If I have multiple tags on the PLC, they all have the default sampling interval. In my case it's set to 1 second. I can change each tag to a different interval, though. For example if I want to have 99 tags at the default 1 second interval and one tag at 50ms.

i dont think uaexpert shows all values tbh 50ms is kind of hard to see... guess they skip some while rendering.

Correct, the limit of UaExpert is 200ms. That is part of what I'm trying to explain. Sorry if it's a little confusing.

UaExpert tells me 3 values:

So even if the tag updates every 50ms on the server and the publishing interval is 100ms, I still get the value every 200ms in UaExpert. (Makes sense)

We don't have this second sampling interval in the python library, right? So I'd expect to get a value every 100ms in the same scenario. And the weird thing is, that it actually does that, if I change the default value to 50ms on the server. But the drawback is, that that setting changes all my tags to 50ms... That default setting shouldn't do anything else. Just saves me from changing the settings for every Tag individually.

Since UaExpert recieves values at 200ms and the python client at 1s with the same server setup, I assume it's a client issue.

AndreasHeine commented 5 months ago

But the drawback is, that that setting changes all my tags to 50ms...

in the Server?

f-ritter commented 5 months ago

in the Server?

Yes, the default timer changes the sampling intervals of all tags on the server that don't have this value overwritten.

f-ritter commented 5 months ago

I found a workaround. I looked into the code and noticed that the subscribe_data_change function has a parameter sampling_interval that is set to ua.Duration(0.0). If I set this manually to 50ms, I get the desired behaviour.

Still doesn't explain why setting it to 0 uses the default server interval instead of the correct tag interval, though. Didn't check the exact reason. Maybe a server issue after all, maybe fixable in the client.

Feel free to close this issue if it's not client related.

AndreasHeine commented 5 months ago

cannot really help you on the B&R stuff... but i can tell you that the client-subscription is kind of the dump side and the smart logic belongs to the server...

if you do: subscription = await client.create_subscription(handler=handler, period=0) the server revises the 0.0 to whatever he can do as fast as possible according to spec: https://reference.opcfoundation.org/Core/Part4/v104/docs/5.13.1 https://reference.opcfoundation.org/Core/Part4/v104/docs/5.13.2

requestedPublishingInterval: If the requested value is 0 or negative, the Server shall revise with the fastest supported publishing interval.

revisedPublishingInterval: The actual publishing interval that the Server will use, expressed in milliseconds. The Server should attempt to honour the Client request for this parameter, but may negotiate this value up or down to meet its own constraints.

if you do: await subscription.subscribe_data_change(node)

https://github.com/FreeOpcUa/opcua-asyncio/blob/3268568df3b2c314920d97bb059cc48b3a98d5a1/asyncua/common/subscription.py#L259


        nodes: Union[Node, Iterable[Node]],
        attr: ua.AttributeIds = ua.AttributeIds.Value,
        queuesize: int = 0,
        monitoring=ua.MonitoringMode.Reporting,
        sampling_interval: ua.Duration = 0.0
    ) -> Union[int, List[Union[int, ua.StatusCode]]]:

you request to create the monitored item with samplinginterval of 0.0 means

according to spec: https://reference.opcfoundation.org/Core/Part4/v104/docs/5.12.1.2 and https://reference.opcfoundation.org/Core/Part4/v104/docs/5.12.2 https://reference.opcfoundation.org/Core/Part4/v104/docs/7.16

samplingInterval: The interval that defines the fastest rate at which the MonitoredItem(s) should be accessed and evaluated. This interval is defined in milliseconds.

The value 0 indicates that the Server should use the fastest practical rate.

The value -1 indicates that the default sampling interval defined by the publishing interval of the Subscription is requested. A different sampling interval is used if the publishing interval is not a supported sampling interval. Any negative number is interpreted as -1. The sampling interval is not changed if the publishing interval is changed by a subsequent call to the ModifySubscription Service.

The Server uses this parameter to assign the MonitoredItems to a sampling interval that it supports.

The assigned interval is provided in the revisedSamplingInterval parameter. The Server shall always return a revisedSamplingInterval that is equal or higher than the requested samplingInterval. If the requested samplingInterval is higher than the maximum sampling interval supported by the Server, the maximum sampling interval is returned.

f-ritter commented 5 months ago

The value 0 indicates that the Server should use the fastest practical rate.

The value -1 indicates that the default sampling interval defined by the publishing interval of the Subscription is requested.

Thanks for this explanation. As described in my last comment, this was the workaround. Setting it to 0 doesn't use the fastest rate, so I assume thats a server issue. Setting it to -1 works as described and will be my solution.