epics-modules / opcua

EPICS Device Support for OPC UA
Other
19 stars 13 forks source link

Update bidirectional output records without fully processing them #145

Open ralphlange opened 1 year ago

ralphlange commented 1 year ago

Reported by @dirk-zimoch:

When the device sends value updates, output records are fully processed, even if they are set to SCAN="Passive". Generally, "Passive" records are not expected to process on their own - so this behaviour violates the "rule of least surprise".

Also, other records connected through FLNK are being processed, no matter if the output record was written to or updated.

It would be better to just update output records: update the time stamp, check alarms, send monitors. FLNK processing should be limited to being done when the output record processes regularly.

dirk-zimoch commented 3 months ago

This hit me today again when implementing a writable structure. I have an opcuaItem record connected to ns=2;s=Demo.Static.Scalar.Structure of the demo UASDK demo server. I have set its DEFACTN=write. Two ao records are connected to the fields of the structure: element=Low and element=High. All other link options are default. The second record has a FLNK to the opcuaItem, so that when LOW is set, it simply stores the value, but when HIGH is set, the opcuaItem record fetches both values writes the structure to the server.

But the opcuaItem is monitoring the structure and when getting updated in turn updates of the two ao records. But this triggers the FLNK and thus the Item record. Rinse and repeat. This is the output with TPRO=1 in all three records:

OPC UA Client Device Support 0.10.0-dev (v0.9.5-40-g79ec509-dirty); using Unified Automation C++ Client SDK v1.7.0-449
iocRun: All initialization complete
OPC UA: Autoconnecting sessions
cbLow: dbProcess of 'Demo:Low'
Demo:Low: (client time 2024-06-14 10:00:09.159959699) connectionLoss --- remaining queue 0/3
cbLow: dbProcess of 'Demo:High'
Demo:High: (client time 2024-06-14 10:00:09.159959699) connectionLoss --- remaining queue 0/3
cbLow: dbProcess of 'Demo:Structure'
cbLow: dbProcess of 'Demo:Structure'
OPC UA session Demo: connected as 'Anonymous' (sec-mode: None; sec-policy: None)
OPC UA session Demo: WARNING - this session uses *** NO SECURITY ***
cbLow: dbProcess of 'Demo:Low'
Demo:Low: (server time 2024-06-14 10:00:09.187297300) read readComplete (Good)  (OpcUa_Null) as epicsFloat64 --- remaining queue 0/3
cbLow: dbProcess of 'Demo:High'
Demo:High: (server time 2024-06-14 10:00:09.187297300) read readComplete (Good)  (OpcUa_Null) as epicsFloat64 --- remaining queue 0/3
cbLow: dbProcess of 'Demo:Structure'
cbLow: Re-process Demo:Structure
Demo:Low : incoming data () out-of-bounds
Demo:High : incoming data () out-of-bounds
cbLow: dbProcess of 'Demo:Low'
Demo:Low: (client time 2024-06-14 10:00:09.188616260) writeComplete --- remaining queue 0/3
cbLow: dbProcess of 'Demo:High'
Demo:High: (client time 2024-06-14 10:00:09.188616260) writeComplete --- remaining queue 0/3
cbLow: dbProcess of 'Demo:Structure'
cbLow: Re-process Demo:Structure
cbLow: dbProcess of 'Demo:Low'
Demo:Low: (client time 2024-06-14 10:00:09.190475498) writeComplete --- remaining queue 0/3
cbLow: dbProcess of 'Demo:High'
Demo:High: (client time 2024-06-14 10:00:09.190475498) writeComplete --- remaining queue 0/3
cbLow: dbProcess of 'Demo:Structure'
cbLow: Re-process Demo:Structure
[...]

Updating records from the hardware must not trigger their FLNK (but should trigger monitors on the updated records). Thus, it cannot be done by actually processing the record, because EPICS has not way to process the record without triggering the FLNK. Instead, the device support needs to implement its own update() function for each supported record type, which mimics most of the functionality of the record's process() function. For example see https://github.com/paulscherrerinstitute/regdev/blob/68d4e13fe9e53f0db496e2e97e9db6516b11c8e2/regDevSup.c#L803.

It would be nice to have this update functionality already build into output records, but alas ...

As implemented currently, this severely limits the user friendlyness of output structures and basically make DEFACTN pointless, because one cannot FLINK to the Item record to trigger the structure write automatically but instead has to process the Item record separately from setting the fields. In that case one can directly trigger the WRITE field and does not need DEFACTN.

ralphlange commented 3 months ago

I see and agree.

FLNKing to "automatically" trigger the structure write is looping (as you describe) but also blocking many FLNKs that you might need for other things. Because of that, the itemRecord's WOC field/functionality implements that mechanism in the device support layer, without looping or using explicit FLNKs. Would that work better for your use case?

dirk-zimoch commented 3 months ago

Wouldn't that write on any field change? In my case once when LOW is set and the again when HIGH is set? I am currently investigating disabling the FLNK temporarily inside processCallback().

ralphlange commented 3 months ago

Yes, it would. In our use case that was the required behavior: Either "Loading a Snapshot", where WOC is disabled, then many elements are set, then WOC is enabled (which writes once). Or "Run Mode", where changes (by the operator) are rare and single-value, so any change is triggering the structure write.

DEFACTN is needed to decide what to do if the itemRecord is FLNK'd to or CA writes to the PROC field (= remote FLNK). Useful or not, it can happen and the record needs to do something.

ralphlange commented 3 months ago

I had considered another option for the data elements, so that setting 'woc=n' in an element would not trigger writing the structure in WOC mode, but that was deemed unnecessary by my users.

Doable with not too much effort.

dirk-zimoch commented 3 months ago

I will try WOC. For the moment, I have disabled FLNK inside processCallback() by sabotaging and later restoring its lset pointer. That seems to do the trick.

dirk-zimoch commented 3 months ago

FYI: My "hack" in processCallback looks like this:

    dbScanLock(prec);
    ProcessReason oldreason = pvt->reason;
    pvt->reason = reason;
    struct lset *lset = prec->flnk.lset;
    if (reason != writeComplete && prec->scan != menuScanI_O_Intr)
        prec->flnk.lset = NULL;
    if (prec->pact)
        reProcess(prec);
    else
        dbProcess(prec);
    pvt->reason = oldreason;
    prec->flnk.lset = lset;
    dbScanUnlock(prec);