Overview
Repository layout
The repository is split along a single clear line: a C++ client
library and command-line client under ualib/ and tools/uaclient/,
and a family of Python GUI / scripting tools under tools/. The
two stacks are independent — the Python tools talk OPC UA directly via
asyncua and do not link the C++ libraries, while the C++ side
carries no Python dependency. Everything builds from one wscript
(waf) or CMakeLists.txt (CMake), installing to a common $PREFIX
layout. Tests live beside the code they exercise: ualib/test/
(gtest unit tests, no network), itest/server/ (the C++ test
server), and test/uatools/ (Robot Framework integration suites
for both the C++ UaClient and the Python tools).
uatools/
|-- ualib/ # C++ client library (ICommAdapter)
| |-- src/
| | |-- include/eso/uatools/ualib/
| | | `-- client.hpp # public API (PIMPL — open62541 hidden)
| | `-- ualib.cpp # open62541 implementation
| `-- test/
| |-- client_test.cpp # gtest contract checks (no network)
| `-- client_itest.cpp # gtest integration tests (server in-thread)
|
|-- tools/ # Applications
| |-- uaclient/ # C++ command-line client on ualib
| |-- uaexplorer/ # Python (PyQt6): browser/inspector GUI
| |-- uashell/ # Python: bash-like CLI + scripting
| | |-- src/ # the single-file UaShell.py
| | `-- examples/ # example .py automation scripts
| |-- uaplot/ # Python (PyQt6): live plotter + client lib
| |-- uasubscription/ # Python (PyQt6): subscription viewer
| `-- uatstsrv/ # Python (asyncua): test server
|
|-- itest/ # YAML-driven C++ test server
| |-- lib/ # libuatoolsItestSrv.so (Serve() API)
| `-- server/ # UaItestServer CLI wrapper binary
|
|-- test/uatools/ # Robot Framework integration suites
|
|-- doc/ # this manual (Sphinx)
|
|-- wscript # waf build entry-point
|-- CMakeLists.txt # CMake build entry-point
`-- uatools.pc.in # pkg-config template
Note
A protocol_old/ tree exists alongside ualib/ for now. It
contains the previous abstraction (IComm + a separate open62541
wrapper) that was retired when ualib was authored against
ICommAdapter. It is not built or installed; it is kept as a
reference until downstream consumers (ifw-fcf, ifw-ccf)
have migrated, and will be deleted then.
Architecture
+-----------------------------------------------+
| ifw::fnd::defs::ICommAdapter |
| (protocol-agnostic contract; in ifw-fnd) |
+----------------------+------------------------+
| implements
+---------------+----------------+
| eso::uatools::Client |
| (ualib — open62541 via PIMPL) |
+---------------+----------------+
|
|
+----------------------+----------------------+
| |
v v
UaClient (CLI) downstream applications
— built on ualib — — link ualib directly —
(ifw-fcf, ifw-ccf, ...)
ualib is the layer that downstream code links against. Its
Client class implements the ifw::fnd::defs::ICommAdapter
interface — a contract that lives in ifw-fnd, not in uatools. This
separation means a caller can write against ICommAdapter without
caring whether the wire is OPC UA, Modbus, or something else; ualib is
just the OPC UA implementation.
ualib depends on ifw-fnd.defs (contracts), fmt (formatting),
and open62541 (vendored, private). open62541 never appears in
downstream consumers’ include path — PIMPL keeps it strictly internal.
The Python tools are independent of the C++ libraries. They speak OPC
UA directly via asyncua (PyQt6 wraps the asyncio loop into the Qt
event loop where needed). Why both stacks? Different audiences:
applications and tools that need maximum throughput and a typed C++ API
use ualib; operators and scripting workflows where ergonomics matter
more use the Python GUIs and UaShell.
Logging: ELT, VLTSW, standalone
uatools is designed to build on both ELT and VLTSW with no build-time conditionals. The only place ELT and VLTSW practically differ is the backend behind the log lines uatools emits:
On ELT, applications want logs routed through CII’s
CiiLogManagerso they end up in the same sinks as the rest of the ELT process.On VLTSW, applications want logs routed through the VLT logging primitives (
errAddfor error stacks,cutePRINTfor stdout-style output).On standalone runs (unit tests, command-line tools), stdout or no-op is enough.
ualib delegates this to ifw-fnd’s Logger abstraction. The
abstraction has a single process-wide install point:
namespace ifw::fnd {
enum class LogLevel : int {
TRACE = 0,
DEBUG = 10,
INFO = 20,
WARNING = 30,
ERROR = 40,
OFF = 100,
};
class Logger {
public:
virtual void Emit(LogLevel level, const std::string& message) = 0;
virtual void Throw(const std::string& message); // default: throws runtime_error
void SetLogLevel(LogLevel level) noexcept;
LogLevel GetLogLevel() const noexcept;
virtual ~Logger() = default;
};
void InstallLogger(std::unique_ptr<Logger>) noexcept;
Logger& Log();
bool HasLogger() noexcept;
std::unique_ptr<Logger> MakeStdoutLogger();
std::unique_ptr<Logger> MakeNullLogger();
}
Call-site code (inside ualib and in any consumer) uses the
FNDTRACE / FNDDEBUG / FNDINFO / FNDWARNING /
FNDERROR / FNDTHROW macros. They are fmt-style and apply a
cheap early-out filter on the configured log level. FNDTRACE()
with no arguments traces the calling function; with a format string,
it appends an extra note.
Installing a logger
Each application installs exactly one logger at startup, before
any other thread logs. If nothing is installed, calls to Log()
throw — ualib refuses to silently swallow log output.
Standalone or unit-test code uses the bundled factories:
#include <ifw/fnd/defs/logger.hpp>
int main() {
ifw::fnd::InstallLogger(ifw::fnd::MakeStdoutLogger()); // or MakeNullLogger()
// ... application code ...
}
ELT applications install a CII-aware subclass (planned to live in
ifw-core, separate library):
class CiiLogger : public ifw::fnd::Logger {
public:
void Emit(LogLevel level, const std::string& message) override {
elt::log::CiiLogManager::GetLogger("uatools").log(level, message);
}
};
int main() {
ifw::fnd::InstallLogger(std::make_unique<CiiLogger>());
// ...
}
VLTSW applications install a CCS-aware subclass that forwards to
VLT logging primitives (errAdd, cutePRINT). The same
Emit override pattern; no log4cplus needed.
open62541’s internal logging
open62541 has its own internal logger (the source of all those
[info/eventloop] / [info/network] lines). ualib wires it
through a separate filter, tunable per Client via
ConnectionOptions::properties.Set("sdk_log_level", ...). Recognised
values: trace / debug / info / warning / error /
fatal / off. Default: off (silent). The UaClient CLI binds
this to its -l flag for one-knob control.