• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wirenboard / wb-mqtt-opcua / 73

01 Aug 2025 07:55AM UTC coverage: 29.848% (-2.5%) from 32.302%
73

push

github

web-flow
Refactor OPC UA nodes creation routine

104 of 282 branches covered (36.88%)

0 of 54 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

157 of 526 relevant lines covered (29.85%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/OPCUAServer.cpp
1
#include "OPCUAServer.h"
2

3
#include <open62541/plugin/accesscontrol_default.h>
4
#include <open62541/server.h>
5
#include <open62541/server_config_default.h>
6
#include <open62541/statuscodes.h>
7

8
#include <functional>
9
#include <stdexcept>
10
#include <vector>
11

12
#include "log.h"
13

14
#define LOG(logger) ::logger.Log() << "[OPCUA] "
15

16
namespace
17
{
18
    const char* LogCategoryNames[7] =
19
        {"network", "channel", "session", "server", "client", "userland", "securitypolicy"};
20

21
    void PrintLogMessage(WBMQTT::TLogger& logger, UA_LogCategory category, const char* msg, va_list args)
×
22
    {
23
        va_list args2;
24
        va_copy(args2, args);
×
25
        auto bufSize = 1 + vsnprintf(nullptr, 0, msg, args);
×
26
        std::string str(bufSize, '\0');
×
27
        vsnprintf(&str[0], bufSize, msg, args2);
×
28
        va_end(args2);
×
29
        logger.Log() << "[OPCUA] " << LogCategoryNames[category] << ": " << str;
×
30
    }
31

32
    extern "C" {
33
    void Log(void* context, UA_LogLevel level, UA_LogCategory category, const char* msg, va_list args)
×
34
    {
35
        switch (level) {
×
36
            case UA_LOGLEVEL_TRACE:
×
37
            case UA_LOGLEVEL_DEBUG:
38
                PrintLogMessage(Debug, category, msg, args);
×
39
                break;
×
40
            case UA_LOGLEVEL_INFO:
×
41
                PrintLogMessage(Info, category, msg, args);
×
42
                break;
×
43
            case UA_LOGLEVEL_WARNING:
×
44
                PrintLogMessage(Warn, category, msg, args);
×
45
                break;
×
46
            case UA_LOGLEVEL_ERROR:
×
47
            case UA_LOGLEVEL_FATAL:
48
                PrintLogMessage(Error, category, msg, args);
×
49
                break;
×
50
        }
51
    }
52

53
    void LogClear(void* logContext)
×
54
    {}
55

56
    UA_StatusCode ReadVariableCallback(UA_Server* sserver,
57
                                       const UA_NodeId* ssessionId,
58
                                       void* ssessionContext,
59
                                       const UA_NodeId* snodeId,
60
                                       void* snodeContext,
61
                                       UA_Boolean ssourceTimeStamp,
62
                                       const UA_NumericRange* range,
63
                                       UA_DataValue* dataValue);
64

65
    UA_StatusCode WriteVariableCallback(UA_Server* server,
66
                                        const UA_NodeId* sessionId,
67
                                        void* sessionContext,
68
                                        const UA_NodeId* nodeId,
69
                                        void* nodeContext,
70
                                        const UA_NumericRange* range,
71
                                        const UA_DataValue* data);
72
    }
73

74
    UA_Logger MakeLogger()
×
75
    {
76
        UA_Logger logger = {Log, nullptr, LogClear};
×
77
        return logger;
×
78
    }
79

NEW
80
    void SetVariableAttributes(UA_VariableAttributes& attr, WBMQTT::PControl control)
×
81
    {
NEW
82
        attr.accessLevel =
×
NEW
83
            control->IsReadonly() ? UA_ACCESSLEVELMASK_READ : UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
×
NEW
84
        attr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)control->GetId().c_str());
×
85
        attr.valueRank = UA_VALUERANK_SCALAR;
×
86
        attr.dataType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATATYPE);
×
87
        try {
NEW
88
            auto v = control->GetValue();
×
NEW
89
            if (v.Is<bool>()) {
×
NEW
90
                attr.dataType = UA_NODEID_NUMERIC(0, UA_NS0ID_BOOLEAN);
×
NEW
91
                return;
×
92
            }
NEW
93
            if (v.Is<double>()) {
×
NEW
94
                attr.dataType = UA_NODEID_NUMERIC(0, UA_NS0ID_DOUBLE);
×
NEW
95
                return;
×
96
            }
NEW
97
            return;
×
NEW
98
        } catch (...) {
×
99
        }
100
    }
101

102
    void ConfigureOpcUaServer(UA_ServerConfig* serverCfg, const OPCUA::TServerConfig& config)
×
103
    {
104
        serverCfg->logger = MakeLogger();
×
105

106
        UA_ServerConfig_setBasics(serverCfg);
×
107
        serverCfg->allowEmptyVariables = UA_RULEHANDLING_ACCEPT;
×
108

109
        UA_BuildInfo_clear(&serverCfg->buildInfo);
×
110
        UA_ApplicationDescription_clear(&serverCfg->applicationDescription);
×
111
        serverCfg->applicationDescription.applicationUri = UA_STRING_ALLOC("urn:wb-mqtt-opcua.server.application");
×
112
        serverCfg->applicationDescription.productUri = UA_STRING_ALLOC("https://wirenboard.com");
×
113
        serverCfg->applicationDescription.applicationName =
114
            UA_LOCALIZEDTEXT_ALLOC("en", "Wiren Board MQTT to OPC UA gateway");
×
115
        serverCfg->applicationDescription.applicationType = UA_APPLICATIONTYPE_SERVER;
×
116

117
        if (!config.BindIp.empty()) {
×
118
            UA_String_clear(&serverCfg->customHostname);
×
119
            serverCfg->customHostname = UA_String_fromChars(config.BindIp.c_str());
×
120
        }
121

122
        auto res = UA_ServerConfig_addNetworkLayerTCP(serverCfg, config.BindPort, 0, 0);
×
123
        if (res != UA_STATUSCODE_GOOD) {
×
124
            throw std::runtime_error(std::string("OPC UA network layer configuration failed: ") +
×
125
                                     UA_StatusCode_name(res));
×
126
        }
127

128
        res = UA_ServerConfig_addSecurityPolicyNone(serverCfg, nullptr);
×
129
        if (res != UA_STATUSCODE_GOOD) {
×
130
            throw std::runtime_error(std::string("OPC UA security policy addition failed: ") + UA_StatusCode_name(res));
×
131
        }
132

133
        res = UA_AccessControl_default(serverCfg,
×
134
                                       true,
135
                                       &serverCfg->securityPolicies[serverCfg->securityPoliciesSize - 1].policyUri,
×
136
                                       0,
137
                                       nullptr);
138
        if (res != UA_STATUSCODE_GOOD) {
×
139
            throw std::runtime_error(std::string("OPC UA access control configuration failed: ") +
×
140
                                     UA_StatusCode_name(res));
×
141
        }
142

143
        res = UA_ServerConfig_addEndpoint(serverCfg, UA_SECURITY_POLICY_NONE_URI, UA_MESSAGESECURITYMODE_NONE);
×
144
        if (res != UA_STATUSCODE_GOOD) {
×
145
            throw std::runtime_error(std::string("OPC UA server endpoint allocation failed: ") +
×
146
                                     UA_StatusCode_name(res));
×
147
        }
148
    }
149

150
    /**! Basic gateway implementation.
151
     *   The server creates ObjectNodes for groups from config and VariableNodes for MQTT controls.
152
     *   OPC UA variable node id is DEVICE/CONTROL pair string.
153
     *   The server translates writes to VariableNodes to publishing into appropriate "on" topics.
154
     */
155
    class TServerImpl: public OPCUA::IServer
156
    {
157
        std::mutex Mutex;
158
        std::unordered_map<std::string, WBMQTT::PControl> ControlMap;
159

160
        UA_Server* Server;
161
        volatile UA_Boolean IsRunning;
162
        std::thread ServerThread;
163
        WBMQTT::PDeviceDriver Driver;
164

NEW
165
        bool ControlExists(const std::string& nodeName)
×
166
        {
NEW
167
            std::unique_lock<std::mutex> lock(Mutex);
×
NEW
168
            return ControlMap.find(nodeName) != ControlMap.end();
×
169
        }
170

NEW
171
        void AddControl(const std::string& nodeName, WBMQTT::PControl control)
×
172
        {
NEW
173
            std::unique_lock<std::mutex> lock(Mutex);
×
NEW
174
            ControlMap[nodeName] = control;
×
175
        }
176

NEW
177
        WBMQTT::PControl GetControl(const std::string& nodeName)
×
178
        {
NEW
179
            std::unique_lock<std::mutex> lock(Mutex);
×
NEW
180
            auto it = ControlMap.find(nodeName);
×
NEW
181
            return it != ControlMap.end() ? it->second : nullptr;
×
182
        }
183

NEW
184
        UA_NodeId CreateObjectNode(const std::string& nodeName)
×
185
        {
186
            UA_NodeId nodeId = UA_NODEID_STRING(1, (char*)nodeName.c_str());
×
187
            UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
×
188
            oAttr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)nodeName.c_str());
×
189
            auto res = UA_Server_addObjectNode(Server,
×
190
                                               nodeId,
191
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
192
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
193
                                               UA_QUALIFIEDNAME(1, (char*)nodeName.c_str()),
×
194
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
195
                                               oAttr,
196
                                               nullptr,
197
                                               nullptr);
198
            if (res != UA_STATUSCODE_GOOD) {
×
199
                throw std::runtime_error("Object node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
200
            }
201
            return nodeId;
×
202
        }
203

NEW
204
        void CreateVariableNode(const UA_NodeId& parentNodeId, const std::string& nodeName, WBMQTT::PControl control)
×
205
        {
206
            UA_VariableAttributes oAttr = UA_VariableAttributes_default;
×
NEW
207
            SetVariableAttributes(oAttr, control);
×
208

209
            UA_DataSource dataSource;
210
            dataSource.read = ReadVariableCallback;
×
211
            dataSource.write = WriteVariableCallback;
×
212

213
            auto res = UA_Server_addDataSourceVariableNode(Server,
×
NEW
214
                                                           UA_NODEID_STRING(1, (char*)nodeName.c_str()),
×
215
                                                           parentNodeId,
216
                                                           UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
NEW
217
                                                           UA_QUALIFIEDNAME(1, (char*)control->GetId().c_str()),
×
218
                                                           UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
219
                                                           oAttr,
220
                                                           dataSource,
221
                                                           this,
222
                                                           nullptr);
223
            if (res != UA_STATUSCODE_GOOD) {
×
NEW
224
                throw std::runtime_error("Variable node '" + nodeName +
×
225
                                         "' creation failed: " + UA_StatusCode_name(res));
×
226
            }
227
        }
228

229
    public:
230
        TServerImpl(const OPCUA::TServerConfig& config, WBMQTT::PDeviceDriver driver)
×
231
            : Server(UA_Server_new()),
×
232
              IsRunning(true),
233
              Driver(driver)
×
234
        {
235
            if (!Server) {
×
236
                throw std::runtime_error("OPC UA server initilization failed");
×
237
            }
238

NEW
239
            Driver->On<WBMQTT::TControlValueEvent>([&](const WBMQTT::TControlValueEvent& event) {
×
NEW
240
                if (event.RawValue.empty()) {
×
NEW
241
                    return;
×
242
                }
NEW
243
                auto it = config.ObjectNodes.find(event.Control->GetDevice()->GetId());
×
NEW
244
                if (it == config.ObjectNodes.end()) {
×
NEW
245
                    return;
×
246
                }
NEW
247
                std::string nodeName = it->first + "/" + event.Control->GetId();
×
NEW
248
                if (ControlExists(nodeName)) {
×
NEW
249
                    return;
×
250
                }
NEW
251
                auto browseName = UA_QUALIFIEDNAME(1, (char*)it->first.c_str());
×
252
                auto res = UA_Server_browseSimplifiedBrowsePath(Server,
253
                                                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
254
                                                                1,
NEW
255
                                                                &browseName);
×
256
                auto parentNodeId =
NEW
257
                    res.statusCode == UA_STATUSCODE_GOOD ? res.targets[0].targetId.nodeId : CreateObjectNode(it->first);
×
NEW
258
                for (auto& valueNode: it->second) {
×
NEW
259
                    if (valueNode.DeviceControlPair != nodeName) {
×
NEW
260
                        continue;
×
261
                    }
NEW
262
                    browseName = UA_QUALIFIEDNAME(1, (char*)event.Control->GetId().c_str());
×
NEW
263
                    res = UA_Server_browseSimplifiedBrowsePath(Server, parentNodeId, 1, &browseName);
×
NEW
264
                    if (res.statusCode != UA_STATUSCODE_GOOD) {
×
NEW
265
                        AddControl(nodeName, event.Control);
×
NEW
266
                        CreateVariableNode(parentNodeId, nodeName, event.Control);
×
NEW
267
                        break;
×
268
                    }
269
                }
NEW
270
            });
×
271

272
            // Load external controls
273
            std::vector<std::string> deviceIds;
×
274
            for (const auto& device: config.ObjectNodes) {
×
275
                LOG(Debug) << "'" << device.first << "' is added to filter";
×
276
                deviceIds.emplace_back(device.first);
×
277
            }
278
            Driver->SetFilter(WBMQTT::GetDeviceListFilter(deviceIds));
×
279
            Driver->WaitForReady();
×
280

281
            // Setup and run OPC UA server
282
            ConfigureOpcUaServer(UA_Server_getConfig(Server), config);
×
UNCOV
283
            ServerThread = std::thread([this]() {
×
284
                auto res = UA_Server_run(Server, &IsRunning);
×
285
                if (res != UA_STATUSCODE_GOOD) {
×
286
                    LOG(Error) << UA_StatusCode_name(res);
×
287
                    exit(1);
×
288
                }
289
            });
×
290
        }
291

292
        ~TServerImpl()
×
293
        {
294
            if (IsRunning) {
×
295
                IsRunning = false;
×
296
                if (ServerThread.joinable()) {
×
297
                    ServerThread.join();
×
298
                }
299
            }
300
            if (Server) {
×
301
                UA_Server_delete(Server);
×
302
            }
303
        }
304

305
        UA_StatusCode writeVariable(const UA_NodeId* snodeId, const UA_DataValue* dataValue)
×
306
        {
307
            std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
×
NEW
308
            auto ctrl = GetControl(nodeIdName);
×
309
            if (!ctrl || ctrl->IsReadonly()) {
×
310
                LOG(Error) << "Variable node '" + nodeIdName + "' writing failed. "
×
311
                           << (ctrl ? "It is read only" : "It is not presented in MQTT");
×
312
                return UA_STATUSCODE_BADDEVICEFAILURE;
×
313
            }
NEW
314
            auto tx = Driver->BeginTx();
×
315
            try {
316
                if (dataValue->hasValue) {
×
317
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_BOOLEAN])) {
×
318
                        auto value = *(UA_Boolean*)dataValue->value.data;
×
319
                        ctrl->SetValue(tx, value).Sync();
×
320
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
321
                        return UA_STATUSCODE_GOOD;
×
322
                    }
323
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_DOUBLE])) {
×
324
                        auto value = *(UA_Double*)dataValue->value.data;
×
325
                        ctrl->SetValue(tx, value).Sync();
×
326
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
327
                        return UA_STATUSCODE_GOOD;
×
328
                    }
329
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_STRING])) {
×
330
                        auto value = (char*)((UA_String*)dataValue->value.data)->data;
×
331
                        ctrl->SetRawValue(tx, value).Sync();
×
332
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
333
                        return UA_STATUSCODE_GOOD;
×
334
                    }
335
                }
336
                return UA_STATUSCODE_BADDATATYPEIDUNKNOWN;
×
337
            } catch (const std::exception& e) {
×
338
                LOG(Error) << "Variable node '" + nodeIdName + "' write error: " << e.what();
×
339
                return UA_STATUSCODE_BADDEVICEFAILURE;
×
340
            }
341
        }
342

343
        UA_StatusCode readVariable(const UA_NodeId* snodeId, UA_DataValue* dataValue)
×
344
        {
345
            std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
×
NEW
346
            auto ctrl = GetControl(nodeIdName);
×
347
            if (!ctrl) {
×
348
                LOG(Error) << "Control is not found '" + nodeIdName + "'";
×
349
                dataValue->hasStatus = true;
×
350
                dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
351
                return UA_STATUSCODE_GOOD;
×
352
            }
353
            try {
354
                dataValue->hasStatus = true;
×
355
                if (ctrl->GetError().find("r") != std::string::npos) {
×
356
                    dataValue->status = UA_STATUSCODE_BAD;
×
357
                } else {
358
                    dataValue->status = UA_STATUSCODE_GOOD;
×
359
                }
360
                auto v = ctrl->GetValue();
×
361
                if (v.Is<bool>()) {
×
362
                    auto value = v.As<bool>();
×
363
                    UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_BOOLEAN]);
×
364
                } else {
365
                    if (v.Is<double>()) {
×
366
                        auto value = v.As<double>();
×
367
                        UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_DOUBLE]);
×
368
                    } else {
369
                        UA_String stringValue = UA_String_fromChars((char*)v.As<std::string>().c_str());
×
370
                        UA_Variant_setScalarCopy(&dataValue->value, &stringValue, &UA_TYPES[UA_TYPES_STRING]);
×
371
                        UA_String_clear(&stringValue);
×
372
                    }
373
                }
374
                dataValue->hasValue = true;
×
375
            } catch (const std::exception& e) {
×
376
                LOG(Error) << "Variable node '" + nodeIdName + "' read error: " << e.what();
×
377
                dataValue->hasStatus = true;
×
378
                dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
379
            }
380
            return UA_STATUSCODE_GOOD;
×
381
        }
382
    };
383

384
    extern "C" {
385
    UA_StatusCode ReadVariableCallback(UA_Server* sserver,
×
386
                                       const UA_NodeId* ssessionId,
387
                                       void* ssessionContext,
388
                                       const UA_NodeId* snodeId,
389
                                       void* snodeContext,
390
                                       UA_Boolean ssourceTimeStamp,
391
                                       const UA_NumericRange* range,
392
                                       UA_DataValue* dataValue)
393
    {
394
        TServerImpl* server = (TServerImpl*)(snodeContext);
×
395
        return server->readVariable(snodeId, dataValue);
×
396
    }
397

398
    UA_StatusCode WriteVariableCallback(UA_Server* server,
×
399
                                        const UA_NodeId* sessionId,
400
                                        void* sessionContext,
401
                                        const UA_NodeId* nodeId,
402
                                        void* nodeContext,
403
                                        const UA_NumericRange* range,
404
                                        const UA_DataValue* data)
405
    {
406
        TServerImpl* s = (TServerImpl*)(nodeContext);
×
407
        return s->writeVariable(nodeId, data);
×
408
    }
409
    }
410
}
411

412
std::unique_ptr<OPCUA::IServer> OPCUA::MakeServer(const OPCUA::TServerConfig& config, WBMQTT::PDeviceDriver driver)
×
413
{
414
    return std::unique_ptr<OPCUA::IServer>(new TServerImpl(config, driver));
×
415
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc