ualib — the C++ client library
ualib provides eso::uatools::Client, a single class that
implements the protocol-agnostic ifw::fnd::defs::ICommAdapter
contract (defined in ifw-fnd) over the open62541 OPC UA SDK.
The interface is intentionally OPC UA-free at the public surface:
ICommAdapter is a generic adapter contract (read / write / browse /
call / subscribe / lifecycle), and eso::uatools::Client is the
OPC UA implementation of that contract. Code written against
ICommAdapter can swap in any other adapter (Modbus, ZMQ, in-process
mock) without source changes.
open62541 is vendored inside ualib and never appears in the public
include path — PIMPL hides it. Downstream consumers see only
ifw::fnd::defs types and eso::uatools::Client.
Linking
ualib ships as one shared library, libuatoolsUalib.so. It
depends on ifw-fnd.defs (the contracts) and fmt (formatting).
open62541 is a private dependency.
waf:
declare_cprogram(target='myapp',
use='ualib ifw-fnd.defs fmt')
CMake:
find_package(PkgConfig REQUIRED)
pkg_check_modules(UATOOLS REQUIRED uatools)
target_link_libraries(myapp PRIVATE uatoolsUalib)
The uatools.pc pkg-config file is installed to
$PREFIX/lib64/pkgconfig/.
Includes
#include <eso/uatools/ualib/client.hpp> // eso::uatools::Client
#include <ifw/fnd/defs/iCommAdapter.hpp> // ICommAdapter contract + types
#include <ifw/fnd/defs/logger.hpp> // InstallLogger / FND* macros
The ICommAdapter contract
The contract (full source in
ifw-fnd/defs/src/include/ifw/fnd/defs/iCommAdapter.hpp) gives ualib
both its shape and its expectations. Key types:
Status— protocol-agnostic outcome classification (Ok, Timeout, NotConnected, AlreadyConnected, InvalidState, BadRequest, NotFound, AccessDenied, TypeMismatch, Unsupported, Cancelled, RemoteError, TransportError, InternalError).Value— universal payload: astd::variantof monostate, bool, int32 / uint32 / int64 / uint64, float, double, string, plus the corresponding vector types and aPropertyMap.Result— base of every per-call result, carryingstatusand an explanatorymessage.operator boolreturnstrueiff the status isOk.RequestOptions— per-call knobs:timeout, optionalCancellationToken,PropertyMapfor protocol-specific tuning.ConnectionOptions— endpoint + auth + securityPropertyMap.
The interface itself:
namespace ifw::fnd::defs {
class ICommAdapter {
public:
virtual ~ICommAdapter() = default;
// Capabilities — runtime feature negotiation
virtual Capabilities GetCapabilities() const = 0;
// Lifecycle
virtual Result Configure(const ConnectionOptions&) = 0;
virtual Result Connect() = 0;
virtual Result Disconnect() = 0;
virtual bool IsConnected() const = 0;
virtual ConnectionStatus GetConnectionStatus() const = 0;
virtual RegistrationToken
SetConnectionStateHandler(ConnectionStateHandler) = 0;
// Data plane
virtual DiscoverResult Discover(const DiscoverRequest&) = 0;
virtual BrowseResult Browse (const BrowseRequest&) = 0;
virtual ReadResult Read (const ReadRequest&) = 0;
virtual WriteResult Write (const WriteRequest&) = 0;
virtual CallResult Call (const CallRequest&) = 0;
// Pub/sub (per-node identity; no SubscriptionId in the public API)
virtual SubscribeResult
Subscribe(const std::vector<Address>& nodes,
SubscriptionHandler handler,
const SubscribeOpts& opts = {}) = 0;
virtual UnsubscribeResult
Unsubscribe(const std::vector<Address>& nodes) = 0;
virtual Result UnsubscribeAll() = 0;
};
}
ualib’s view of these
eso::uatools::Client reports the following capabilities:
c.read = true;
c.write = true;
c.browse = true;
c.call = true;
c.subscribe = true;
c.batch = true;
c.connection_state_events = true;
c.cancellation = true;
c.discover = false; // deferred (post-IFW26)
User authentication is supported through
ConnectionOptions::properties["username"] /
["password"]. Anonymous if absent. Encryption (Basic256Sha256 +
SignAndEncrypt) is deferred to post-IFW26.
Minimal usage
#include <eso/uatools/ualib/client.hpp>
#include <ifw/fnd/defs/iCommAdapter.hpp>
#include <ifw/fnd/defs/logger.hpp>
namespace fnd = ifw::fnd::defs;
int main() {
ifw::fnd::InstallLogger(ifw::fnd::MakeStdoutLogger());
eso::uatools::Client client;
fnd::ConnectionOptions opts;
opts.endpoint = "opc.tcp://127.0.0.1:4840";
// Optional: opts.properties.Set("username", "user");
// opts.properties.Set("password", "pswd");
if (auto r = client.Configure(opts); !r) return 2;
if (auto r = client.Connect(); !r) return 2;
// Read
fnd::ReadRequest req;
req.items.push_back({"ns=2;s=Temperature", {}});
fnd::ReadResult res = client.Read(req);
if (res) {
for (const auto& item : res.items) {
if (item.status == fnd::Status::Ok && item.data) {
// item.data->value is a std::variant
}
}
}
client.Disconnect();
}
Write — explicit type and auto type
ualib’s Write is type-aware: the Value you submit must match
the node’s OPC UA DataType. Two patterns:
Explicit: you set the right alternative of Value yourself.
fnd::WriteRequest req;
req.items.push_back({"ns=2;s=Enabled", fnd::Value{true}, {}});
client.Write(req);
Auto-detect (one round trip): call client.ReadDataType(node)
first, then build the Value of the right alternative. UaClient’s
two-arg write NODE VALUE form uses this pattern under the hood.
Browse
fnd::BrowseRequest req;
req.root = "ns=2;s=system"; // optional; default = ns=0;i=85 (Objects)
req.recursive = true; // descend into subtrees
req.include_values = true; // read each Variable's current value
fnd::BrowseResult res = client.Browse(req);
Call (OPC UA Method)
fnd::CallRequest req;
req.method = "ns=2;s=device.Init";
// input arguments, in declaration order:
// req.input_arguments.push_back({"x", fnd::Value{double{23.5}}});
fnd::CallResult res = client.Call(req);
// res.output_arguments holds the method's return values
The parent (Object) NodeId is derived from method by stripping the
trailing dot-segment. For NodeIds where that heuristic doesn’t apply
(numeric NodeIds, dot-free strings), pass it explicitly:
req.options.properties.Set("object", "ns=2;s=device");
Subscribe — per-node handler binding
Each Subscribe() call’s handler is bound to the nodes that call
supplied. Re-subscribing a node replaces that node’s handler;
other nodes’ bindings stay intact. Two consumers of the same Client
can therefore have their own callbacks without stepping on each other:
client.Subscribe({"ns=2;s=Counter"}, on_counter_change);
client.Subscribe({"ns=2;s=Temp"}, on_temp_change);
// Counter still routes to on_counter_change; Temp goes to on_temp_change.
Handlers fire on ualib’s internal delivery thread. They MUST be
non-blocking and MUST NOT throw. Unsubscribe() from inside a handler
is safe; Connect / Disconnect / Configure is not.
Connection-state events
SetConnectionStateHandler registers a callback that fires on every
channel/session transition (Disconnected → Connecting → Connected,
Connected → Faulted on link drop, etc.). The handler MUST be installed
before Connect() — otherwise the initial transitions are
missed.
The returned RegistrationToken is RAII: when it leaves scope (or
Reset() is called), the handler is detached and no more events fire.
eso::uatools::Client client;
auto token = client.SetConnectionStateHandler(
[](const fnd::ConnectionStateChange& evt) {
// evt.previous, evt.current, evt.reason, evt.message
});
client.Configure(opts);
client.Connect(); // fires Disconnected -> Connecting -> Connected
Cancellation (cooperative)
ualib honours RequestOptions::cancellation cooperatively:
A pre-flight check at the top of each op: if the token is already set, the call returns
Status::Cancelledbefore touching the SDK.A per-item check inside batch loops (Read, Write, Browse): the partial result up to that point is returned, with the remaining items marked
Status::Cancelled.
In-flight cancellation of an individual SDK call is not possible — open62541’s sync API has no abort path. The contract calls this cooperative; it lets the caller cleanly abort a long-running batch or BFS traversal while it’s between items.
fnd::CancellationSource src;
fnd::ReadRequest req;
req.options.cancellation = src.Token();
req.items = ... /* many */;
std::thread aborter([&] {
std::this_thread::sleep_for(std::chrono::seconds(2));
src.Cancel();
});
auto res = client.Read(req);
aborter.join();
// res.status will be Cancelled if the batch was interrupted
Logging
ualib uses ifw::fnd’s Logger abstraction. InstallLogger() is
required once at process startup; otherwise the first FND* call
throws. The FNDTRACE / FNDDEBUG / FNDINFO / FNDWARNING /
FNDERROR / FNDTHROW macros include a cheap early-out filter, so
disabled-level calls cost a single integer compare.
ualib emits:
FNDTRACE()at every publicClientmethod entry. Useful for step-tracing.FNDDEBUGat lifecycle transitions and per-item operations.FNDINFOonConnected/Disconnected.FNDWARNINGwhen a public op is rejected (NotConnected guards).FNDERRORon hard failures (alloc, SDK refusal).
open62541’s internal logger is wired through a separate filter,
tunable via
ConnectionOptions::properties.Set("sdk_log_level", "off"|"error"|...).
The default is off (silent). UaClient’s -l/--log-level flag
sets both the ifw-fnd and the SDK threshold together.
See the “Logging — ELT / VLTSW / standalone” section of the README for how to plug a CII or VLT-CCS logger in.