OPC UA with Flutter – Two Years Later

Oct 4, 2023

Jannis Voelker - basyskom

OPC UA is a platform independent standard for data modeling and communication that plays a major role in the horizontal and vertical integration of industrial automation systems. It is also a popular choice for connecting industrial controllers (PLCs) and entire machines with HMIs. Having an OPC UA client available is an important building block for popularizing Flutter for machine control applications.

Two years have passed since we at basysKom first attempted to use the open62541 OPC UA implementation in Flutter which resulted in a blog post describing our experience and the difficulties we encountered (for context have a quick look at the old post). Now it was time to have a fresh look into the matter to see if any of the blockers have been resolved.

Using open62541 directly via ffigen

The original blog post focused on the implementation of a C++ class providing a small, application specific subset of open62541's functionality to Flutter because it became clear quickly that using the library directly via ffigen wouldn't work.

The main blocker was that the open62541 code base makes heavy use of static inline functions in headers which was not supported by ffigen.

The Inline Problem

Static inline functions are still not supported by ffigen but starting with the upcoming 1.4 release of open62541, there will be a new CMake parameter named UA_ENABLE_INLINABLE_EXPORT which causes all static inline functions to be included into the the library. This solves the problem and ffigen is able to generate bindings for all functions in the open62541 library.

Threads and Callbacks

The next issue is not a blocker per se, but something to keep in mind. Due to the threading model of Dart, there can only be one thread per isolate. This demands that callbacks in the Dart code that are invoked from the native C side must not come from a different thread (e. g. worker thread) or the Dart VM will be terminated with an error

../../third_party/dart/runtime/vm/runtime_entry.cc: 3766: error: Cannot invoke native callback outside an isolate.
version=2.18.6 (stable) (Tue Dec 13 21:15:14 2022 +0000) on "linux_x64"
pid=321562, thread=321593, isolate_group=(nil)((nil)), isolate=(nil)((nil))
isolate_instructions=0, vm_instructions=7f106250d9c0
  pc 0x00007f106263cdac fp 0x00007f0fca9fe8c0 /home/xxxx/PROJECTS/internal/ffi_open62541/build/linux/x64/debug/bundle/lib/libflutter_linux_gtk.so+0x1f08dac
  pc 0x00007f106250dbcc fp 0x00007f0fca9fe9f0 /home/xxxx/PROJECTS/internal/ffi_open62541/build/linux/x64/debug/bundle/lib/libflutter_linux_gtk.so+0x1dd9bcc
  pc 0x00007f105f32a868 fp 0x00007f0fca9fead0 clock_nanosleep+0xc8
-- End of DumpStackTrace
Lost connection to device.

Due to this limitation, the only way to use the asynchronous, callback based API of open62541 would be calling UA_Client_run_iterate() periodically from a timer in the Dart code to ensure that callbacks are invoked from the right thread. As this function is blocking, it will be necessary to experiment with the timeout parameter in order not to block the Dart isolate too long.

Structs and Bitfields

After having sorted out the issues mentioned above, everything looked good until we noticed ffigen spitting errors regarding the code generation for structs

[WARNING]: Removed All Struct Members from UA_DiagnosticInfo(UA_DiagnosticInfo), Bit Field members not supported.
[WARNING]: Removed All Struct Members from UA_ResponseHeader(UA_ResponseHeader), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_ServiceFault(UA_ServiceFault), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_FindServersResponse(UA_FindServersResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_FindServersOnNetworkResponse(UA_FindServersOnNetworkResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_GetEndpointsResponse(UA_GetEndpointsResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_RegisterServerResponse(UA_RegisterServerResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_RegisterServer2Response(UA_RegisterServer2Response), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_OpenSecureChannelResponse(UA_OpenSecureChannelResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_CloseSecureChannelResponse(UA_CloseSecureChannelResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_CreateSessionResponse(UA_CreateSessionResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_ActivateSessionResponse(UA_ActivateSessionResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_CloseSessionResponse(UA_CloseSessionResponse), Incomplete Nested Struct member not supported.
[WARNING]: Removed All Struct Members from UA_CancelResponse(UA_CancelResponse), Incomplete Nested Struct member not supported.
..........

The root issue here is the lack of support for bitfields in the Dart FFI which in turn prevents ffigen from supporting them. The behavior of ffigen in such a case is to generate an empty class because just skipping the bitfield members is not an option due to alignment issues.

The UA_DiagnosticInfo struct contains bitfield members and is present in the header of all OPC UA response types sent by an OPC UA server to a client.

typedef struct UA_DiagnosticInfo {
    UA_Boolean    hasSymbolicId          : 1;
    UA_Boolean    hasNamespaceUri        : 1;
    UA_Boolean    hasLocalizedText       : 1;
    UA_Boolean    hasLocale              : 1;
    UA_Boolean    hasAdditionalInfo      : 1;
    UA_Boolean    hasInnerStatusCode     : 1;
    UA_Boolean    hasInnerDiagnosticInfo : 1;
    UA_Int32      symbolicId;
    UA_Int32      namespaceUri;
    UA_Int32      localizedText;
    UA_Int32      locale;
    UA_String     additionalInfo;
    UA_StatusCode innerStatusCode;
    struct UA_DiagnosticInfo *innerDiagnosticInfo;
} UA_DiagnosticInfo;

Functions using these types as return or parameter type will also be skipped during generation. This unfortunately means that we have yet again found a blocker.

Conclusion

Two years later, it is still not practical to use open62541 directly in Flutter. While it is now possible to establish and maintain a connection to an OPC UA server using the bindings generated by ffigen, most of the API to interact with the server is missing or not usable. Developers wishing to utilize open62541 in their Flutter applications should resort to creating a custom library exposing the necessary functionality as described in our initial blog post until the Dart FFI and ffigen have been updated to deal with bitfield members in structs.

2 comments on "OPC UA with Flutter – Two Years Later"

John Morgan

Dec 1, 2023, 5:27 PM

I am so confident that flutter will become one of the key technologies for the future industries

Mykola Pryhodskyi

Mar 30, 2024, 7:45 PM

Me also

Leave a Comment

Your Email address will not be published

KDAB is committed to ensuring that your privacy is protected.

  • Only the above data is collected about you when you fill out this form.
  • The data will be stored securely.
  • The data will only be used to contact you about possible business together.
  • If we do not engage in business within 3 years, your personal data will be erased from our systems.
  • If you wish for us to erase it earlier, email us at info@kdab.com.

For more information about our Privacy Policy, please read our privacy policy