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 CiiLogManager so 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 (errAdd for error stacks, cutePRINT for 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.