open62541 for Dart
Dart FFI bindings to the open62541 OPC UA stack. This package provides idiomatic Dart client and server APIs for OPC UA over TCP, including subscriptions, custom types and encrypted (mbedTLS) connections.
Status: work in progress. The API is functional but still evolving and may change between releases.
Features
- Client: connect / reconnect, browse and recursive tree browsing, read,
subscriptions and monitored items, and secure connections with certificates
(
SignAndEncrypt). - Server: expose scalar, array and structure (custom type) variable nodes, data-type nodes, and monitor variables via streams.
- PubSub (OPC UA Part 14, UDP + UADP): publish variable nodes via
PublishedDataSets / WriterGroups / DataSetWriters, and subscribe with
ReaderGroups / DataSetReaders that map received fields into local variable
nodes (both sides configured on
Server, per the OPC UA PubSub model). Received values can be observed withServer.onValueChanged. - Encryption via mbedTLS.
DynamicValuefor ergonomic access to OPC UA values, including structures and arrays.
Supported platforms
Linux, macOS, Windows, Android and iOS. (Web is not supported — this is a native FFI binding.)
How the native library is built
This package does not ship a precompiled binary. It uses Dart's
native build hooks (hook/build.dart
together with the hooks, code_assets and native_toolchain_cmake packages).
The first time the package is built, the hook:
- Downloads the open62541 and mbedTLS source archives over the network.
- Builds them from source with CMake for your target platform.
- Bundles the resulting shared library as a code asset.
Because of this you need a working C/C++ toolchain, CMake, and network access available at build time. The first build is slow; subsequent builds are cached.
Native build hooks are stable since Dart 3.10, so consumers need no experiment flag.
Installation
dependencies:
open62541: ^1.5.7
Then run dart pub get (allow extra time for the first native build).
Versioning
The package version mirrors the bundled open62541 release exactly: 1.5.7 wraps
open62541 v1.5.7. Dart-side fixes that ship the same native library version
use a build-metadata suffix — 1.5.7+1, 1.5.7+2, … — which pub.flutter-io.cn orders
after 1.5.7. So open62541: ^1.5.7 accepts 1.5.7, any binding-only +N
revision, and later 1.x upstream releases.
Usage
Read the current server time from an OPC UA server:
import 'package:open62541/open62541.dart';
void main(List<String> args) async {
final client = Client();
client.connect('opc.tcp://localhost:4840');
// Drive the client event loop.
() async {
while (client.runIterate(Duration(milliseconds: 10))) {
await Future.delayed(Duration(milliseconds: 10));
}
}();
await client.awaitConnect();
final time = await client.read(NodeId.serverStatusCurrentTime);
print('Server time: ${time.asDateTime}');
client.disconnect();
await client.delete();
}
More examples are in the example/ directory: a minimal client
(example.dart), address-space browsing (browse_test.dart), a server
(server_example.dart) and a secure self-healing client
(resilient_client.dart).
Threading note: open62541 is built with multithreading disabled, so the client/server event loop must be driven periodically by calling
runIterateas shown above.
PubSub (UDP multicast)
Both PubSub roles are configured on a Server (per OPC UA Part 14 the
subscriber also lives on a server instance). Publisher:
final connection = server.addPubSubConnection(
name: 'UADP Connection',
url: 'opc.udp://224.0.0.22:4840/',
publisherId: PubSubPublisherId.uint16(2234),
);
final pds = server.addPublishedDataSet(name: 'Demo PDS');
server.addDataSetField(pds, name: 'Counter', publishedVariable: counterNodeId);
final group = server.addWriterGroup(connection,
name: 'WG', writerGroupId: 100, publishingInterval: Duration(milliseconds: 100));
server.addDataSetWriter(group, pds, name: 'DSW', dataSetWriterId: 62541);
server.enableAllPubSubComponents();
Subscriber (on another — or the same — server):
final connection = server.addPubSubConnection(name: 'Sub', url: 'opc.udp://224.0.0.22:4840/');
final readerGroup = server.addReaderGroup(connection, name: 'RG');
final reader = server.addDataSetReader(readerGroup,
name: 'DSR',
publisherId: PubSubPublisherId.uint16(2234),
writerGroupId: 100,
dataSetWriterId: 62541,
dataSetName: 'Demo PDS',
fields: [DataSetFieldMeta(name: 'Counter', dataType: NodeId.int32)]);
server.setDataSetReaderTargetVariables(reader, [targetNodeId]);
server.onValueChanged(targetNodeId).listen((v) => print('received: ${v.value}'));
server.enableAllPubSubComponents();
Development
The sections below are only relevant if you are hacking on the package itself (for example regenerating the FFI bindings), not for normal use.
Regenerating the bindings
The native library is built automatically by hook/build.dart, which downloads a
pinned open62541 source archive and builds it with CMake (amalgamation enabled,
so it also produces a single open62541.h). You do not need to build
open62541 by hand for normal use.
To regenerate the FFI bindings (for example after bumping the open62541 version), build once, then copy the amalgamated header the hook produced and run the generator:
dart test test/verify_version_test.dart # triggers the hook build
# Locate the amalgamated header (the path is OS/arch-specific):
find .dart_tool/hooks_runner -name open62541.h
# e.g. on macOS/arm64:
cp .dart_tool/hooks_runner/shared/open62541/build/dl/src/build/macos/arm64/open62541.h \
third_party/open62541/open62541.h
bash open62541_tooling/patch_header.sh
dart run tool/ffigen.dart
ffigen needs libclang; on some Linux setups you may need to prefix the last
command with CPATH=/usr/lib/clang/<version>/include:/usr/include.
The header patch removes the bitfields from UA_DiagnosticInfo, UA_DataValue,
UA_DataTypeMember and UA_DataType, replacing each with a single field (the
struct size is unchanged) so the generator does not drop the surrounding members.
Known limitations
- A multi-dimensional array that is a member of a structure is not modeled and decodes as an empty array. (Top-level multi-dimensional arrays are supported.)
- Structure-field descriptions are not carried over the wire by open62541
(v1.5.x), so they do not surface from a remote server. For an in-process Dart
Server+Client, descriptions are restored from the locally registered schema. - Monitored-item notifications do not surface per-notification status codes or
timestamps on the value stream: a notification with a non-Good status is
delivered as an error event on the stream (carrying the status), and its
value/timestamps are dropped. Use
Client.readValueto observe a node's value together with its status code and source/server timestamps. Server.statisticsexposes aggregate counters only (sessions, secure channels, subscriptions, total monitored items). Per-session and per-subscription diagnostic detail (client identity, publish rates, queue overflows, ...) exists in the NS0 diagnostics nodes but is not surfaced as a typed Dart API.- PubSub: only the UDP + UADP transport is enabled (no MQTT/raw Ethernet), and message security (SKS / PubSub security policies), delta frames and DataSetMetaData ConfigurationVersion handling are not exposed. Dataset fields on the subscriber side must use builtin namespace-0 data types.
License
MIT. See LICENSE.