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

wirenboard / wb-mqtt-opcua / 1

30 Mar 2026 07:04AM UTC coverage: 59.625% (+2.1%) from 57.527%
1

push

github

web-flow
Fix crash on adding controls (#35)

189 of 306 branches covered (61.76%)

40 of 41 new or added lines in 2 files covered. (97.56%)

350 of 587 relevant lines covered (59.63%)

1.86 hits per line

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

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

3
#include <functional>
4
#include <stdexcept>
5
#include <vector>
6

7
#include "log.h"
8

9
#define LOG(logger) ::logger.Log() << "[OPCUA] "
10

11
namespace
12
{
13
    const char* LogCategoryNames[7] =
14
        {"network", "channel", "session", "server", "client", "userland", "securitypolicy"};
15

16
    void PrintLogMessage(WBMQTT::TLogger& logger, UA_LogCategory category, const char* msg, va_list args)
10✔
17
    {
18
        va_list args2;
19
        va_copy(args2, args);
10✔
20
        auto bufSize = 1 + vsnprintf(nullptr, 0, msg, args);
10✔
21
        std::string str(bufSize, '\0');
10✔
22
        vsnprintf(&str[0], bufSize, msg, args2);
10✔
23
        va_end(args2);
10✔
24
        logger.Log() << "[OPCUA] " << LogCategoryNames[category] << ": " << str;
10✔
25
    }
10✔
26

27
    extern "C" {
28
    void Log(void* context, UA_LogLevel level, UA_LogCategory category, const char* msg, va_list args)
10✔
29
    {
30
        switch (level) {
10✔
31
            case UA_LOGLEVEL_TRACE:
×
32
            case UA_LOGLEVEL_DEBUG:
33
                PrintLogMessage(Debug, category, msg, args);
×
34
                break;
×
35
            case UA_LOGLEVEL_INFO:
6✔
36
                PrintLogMessage(Info, category, msg, args);
6✔
37
                break;
6✔
38
            case UA_LOGLEVEL_WARNING:
4✔
39
                PrintLogMessage(Warn, category, msg, args);
4✔
40
                break;
4✔
41
            case UA_LOGLEVEL_ERROR:
×
42
            case UA_LOGLEVEL_FATAL:
43
                PrintLogMessage(Error, category, msg, args);
×
44
                break;
×
45
        }
46
    }
10✔
47

48
    void LogClear(void* logContext)
2✔
49
    {}
2✔
50

51
    UA_StatusCode ReadVariableCallback(UA_Server* sserver,
2✔
52
                                       const UA_NodeId* ssessionId,
53
                                       void* ssessionContext,
54
                                       const UA_NodeId* snodeId,
55
                                       void* snodeContext,
56
                                       UA_Boolean ssourceTimeStamp,
57
                                       const UA_NumericRange* range,
58
                                       UA_DataValue* dataValue)
59
    {
60
        OPCUA::TServerImpl* server = (OPCUA::TServerImpl*)(snodeContext);
2✔
61
        return server->ReadVariable(snodeId, dataValue);
2✔
62
    }
63

64
    UA_StatusCode WriteVariableCallback(UA_Server* server,
×
65
                                        const UA_NodeId* sessionId,
66
                                        void* sessionContext,
67
                                        const UA_NodeId* nodeId,
68
                                        void* nodeContext,
69
                                        const UA_NumericRange* range,
70
                                        const UA_DataValue* data)
71
    {
72
        OPCUA::TServerImpl* s = (OPCUA::TServerImpl*)(nodeContext);
×
73
        return s->WriteVariable(nodeId, data);
×
74
    }
75
    }
76

77
    UA_Logger MakeLogger()
2✔
78
    {
79
        UA_Logger logger = {Log, nullptr, LogClear};
2✔
80
        return logger;
2✔
81
    }
82

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

105
    void ConfigureOpcUaServer(UA_ServerConfig* serverCfg, const OPCUA::TServerConfig& config)
2✔
106
    {
107
        serverCfg->logger = MakeLogger();
2✔
108

109
        UA_ServerConfig_setBasics(serverCfg);
2✔
110
        serverCfg->allowEmptyVariables = UA_RULEHANDLING_ACCEPT;
2✔
111

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

120
        if (!config.BindIp.empty()) {
2✔
121
            UA_String_clear(&serverCfg->customHostname);
×
122
            serverCfg->customHostname = UA_String_fromChars(config.BindIp.c_str());
×
123
        }
124

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

131
        res = UA_ServerConfig_addSecurityPolicyNone(serverCfg, nullptr);
2✔
132
        if (res != UA_STATUSCODE_GOOD) {
2✔
133
            throw std::runtime_error(std::string("OPC UA security policy addition failed: ") + UA_StatusCode_name(res));
×
134
        }
135

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

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

153
}
154

155
namespace OPCUA
156
{
157
    TServerImpl::TServerImpl(const TServerConfig& config, WBMQTT::PDeviceDriver driver)
2✔
158
        : Server(UA_Server_new()),
2✔
159
          IsRunning(true),
160
          Config(config),
161
          Driver(driver)
4✔
162
    {
163
        if (!Server) {
2✔
164
            throw std::runtime_error("OPC UA server initilization failed");
×
165
        }
166

167
        Driver->On<WBMQTT::TControlValueEvent>(
4✔
168
            [&](const WBMQTT::TControlValueEvent& event) { ControlValueEventCallback(event); });
4✔
169

170
        // Load external controls
171
        std::vector<std::string> deviceIds;
2✔
172
        for (const auto& device: config.ObjectNodes) {
4✔
173
            LOG(Debug) << "'" << device.first << "' is added to filter";
2✔
174
            deviceIds.emplace_back(device.first);
2✔
175
        }
176
        Driver->SetFilter(WBMQTT::GetDeviceListFilter(deviceIds));
2✔
177
        Driver->WaitForReady();
2✔
178

179
        // Setup and run OPC UA server
180
        ConfigureOpcUaServer(UA_Server_getConfig(Server), config);
2✔
181
        ServerThread = std::thread([this]() {
2✔
182
            auto res = UA_Server_run(Server, &IsRunning);
2✔
183
            if (res != UA_STATUSCODE_GOOD) {
2✔
184
                LOG(Error) << UA_StatusCode_name(res);
×
185
                exit(1);
×
186
            }
187
        });
6✔
188
    }
2✔
189

190
    TServerImpl::~TServerImpl()
6✔
191
    {
192
        if (IsRunning) {
4✔
193
            IsRunning = false;
4✔
194
            if (ServerThread.joinable()) {
4✔
195
                ServerThread.join();
4✔
196
            }
197
        }
198
        if (Server) {
4✔
199
            UA_Server_delete(Server);
4✔
200
        }
201
    }
6✔
202

203
    bool TServerImpl::ControlExists(const std::string& nodeName)
2✔
204
    {
205
        std::unique_lock<std::mutex> lock(Mutex);
2✔
206
        return ControlMap.find(nodeName) != ControlMap.end();
4✔
207
    }
208

209
    void TServerImpl::AddControl(const std::string& nodeName, WBMQTT::PControl control)
2✔
210
    {
211
        std::unique_lock<std::mutex> lock(Mutex);
4✔
212
        ControlMap[nodeName] = control;
2✔
213
    }
2✔
214

215
    void TServerImpl::RemoveControl(const std::string& nodeName)
1✔
216
    {
217
        std::unique_lock<std::mutex> lock(Mutex);
2✔
218
        ControlMap.erase(nodeName);
1✔
219
    }
1✔
220

221
    WBMQTT::PControl TServerImpl::GetControl(const std::string& nodeName)
4✔
222
    {
223
        std::unique_lock<std::mutex> lock(Mutex);
4✔
224
        auto it = ControlMap.find(nodeName);
4✔
225
        return it != ControlMap.end() ? it->second : nullptr;
12✔
226
    }
227

228
    UA_StatusCode TServerImpl::WriteVariable(const UA_NodeId* snodeId, const UA_DataValue* dataValue)
×
229
    {
230
        std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
×
231
        auto ctrl = GetControl(nodeIdName);
×
232
        if (!ctrl || ctrl->IsReadonly()) {
×
233
            LOG(Error) << "Variable node '" + nodeIdName + "' writing failed. "
×
234
                       << (ctrl ? "It is read only" : "It is not presented in MQTT");
×
235
            return UA_STATUSCODE_BADDEVICEFAILURE;
×
236
        }
237
        auto tx = Driver->BeginTx();
×
238
        try {
239
            if (dataValue->hasValue) {
×
240
                if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_BOOLEAN])) {
×
241
                    auto value = *(UA_Boolean*)dataValue->value.data;
×
242
                    ctrl->SetValue(tx, value).Sync();
×
243
                    LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
244
                    return UA_STATUSCODE_GOOD;
×
245
                }
246
                if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_DOUBLE])) {
×
247
                    auto value = *(UA_Double*)dataValue->value.data;
×
248
                    ctrl->SetValue(tx, value).Sync();
×
249
                    LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
250
                    return UA_STATUSCODE_GOOD;
×
251
                }
252
                if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_STRING])) {
×
253
                    auto value = (char*)((UA_String*)dataValue->value.data)->data;
×
254
                    ctrl->SetRawValue(tx, value).Sync();
×
255
                    LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
256
                    return UA_STATUSCODE_GOOD;
×
257
                }
258
            }
259
            return UA_STATUSCODE_BADDATATYPEIDUNKNOWN;
×
260
        } catch (const std::exception& e) {
×
261
            LOG(Error) << "Variable node '" + nodeIdName + "' write error: " << e.what();
×
262
            return UA_STATUSCODE_BADDEVICEFAILURE;
×
263
        }
264
    }
265

266
    UA_StatusCode TServerImpl::ReadVariable(const UA_NodeId* snodeId, UA_DataValue* dataValue)
2✔
267
    {
268
        std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
4✔
269
        auto ctrl = GetControl(nodeIdName);
4✔
270
        if (!ctrl) {
2✔
271
            LOG(Error) << "Control is not found '" + nodeIdName + "'";
×
272
            dataValue->hasStatus = true;
×
273
            dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
274
            return UA_STATUSCODE_GOOD;
×
275
        }
276
        try {
277
            dataValue->hasStatus = true;
2✔
278
            if (ctrl->GetError().find("r") != std::string::npos) {
2✔
279
                dataValue->status = UA_STATUSCODE_BAD;
×
280
            } else {
281
                dataValue->status = UA_STATUSCODE_GOOD;
2✔
282
            }
283
            auto v = ctrl->GetValue();
2✔
284
            if (v.Is<bool>()) {
2✔
285
                auto value = v.As<bool>();
×
286
                UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_BOOLEAN]);
×
287
            } else {
288
                if (v.Is<double>()) {
2✔
289
                    auto value = v.As<double>();
2✔
290
                    UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_DOUBLE]);
2✔
291
                } else {
292
                    UA_String stringValue = UA_String_fromChars((char*)v.As<std::string>().c_str());
×
293
                    UA_Variant_setScalarCopy(&dataValue->value, &stringValue, &UA_TYPES[UA_TYPES_STRING]);
×
294
                    UA_String_clear(&stringValue);
×
295
                }
296
            }
297
            dataValue->hasValue = true;
2✔
298
        } catch (const std::exception& e) {
×
299
            LOG(Error) << "Variable node '" + nodeIdName + "' read error: " << e.what();
×
300
            dataValue->hasStatus = true;
×
301
            dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
302
        }
303
        return UA_STATUSCODE_GOOD;
2✔
304
    }
305

306
    void TServerImpl::ControlValueEventCallback(const WBMQTT::TControlValueEvent& event)
2✔
307
    {
308
        if (event.RawValue.empty()) {
2✔
309
            return;
×
310
        }
311
        auto it = Config.ObjectNodes.find(event.Control->GetDevice()->GetId());
2✔
312
        if (it == Config.ObjectNodes.end()) {
2✔
313
            return;
×
314
        }
315
        std::string nodeName = it->first + "/" + event.Control->GetId();
2✔
316
        if (ControlExists(nodeName)) {
2✔
317
            return;
×
318
        }
319
        try {
320
            auto browseName = UA_QUALIFIEDNAME(1, (char*)it->first.c_str());
2✔
321
            auto res = UA_Server_browseSimplifiedBrowsePath(Server,
322
                                                            UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
323
                                                            1,
324
                                                            &browseName);
2✔
325
            auto parentNodeId =
326
                res.statusCode == UA_STATUSCODE_GOOD ? res.targets[0].targetId.nodeId : CreateObjectNode(it->first);
2✔
327
            for (auto& valueNode: it->second) {
2✔
328
                if (valueNode.DeviceControlPair != nodeName) {
2✔
NEW
329
                    continue;
×
330
                }
331
                browseName = UA_QUALIFIEDNAME(1, (char*)event.Control->GetId().c_str());
2✔
332
                res = UA_Server_browseSimplifiedBrowsePath(Server, parentNodeId, 1, &browseName);
2✔
333
                if (res.statusCode != UA_STATUSCODE_GOOD) {
2✔
334
                    AddControl(nodeName, event.Control);
2✔
335
                    try {
336
                        CreateVariableNode(parentNodeId, nodeName, event.Control);
3✔
337
                    } catch (...) {
2✔
338
                        RemoveControl(nodeName);
1✔
339
                        throw;
1✔
340
                    }
341
                    break;
1✔
342
                }
343
            }
344
        } catch (const std::exception& e) {
1✔
345
            LOG(Error) << "Failed to add control '" << nodeName << "': " << e.what();
1✔
346
        }
347
    }
348

349
    UA_NodeId TServerImpl::CreateObjectNode(const std::string& nodeName)
2✔
350
    {
351
        UA_NodeId nodeId = UA_NODEID_STRING(1, (char*)nodeName.c_str());
2✔
352
        UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
2✔
353
        oAttr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)nodeName.c_str());
2✔
354
        auto res = UA_Server_addObjectNode(Server,
4✔
355
                                           nodeId,
356
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
357
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
358
                                           UA_QUALIFIEDNAME(1, (char*)nodeName.c_str()),
2✔
359
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
360
                                           oAttr,
361
                                           nullptr,
362
                                           nullptr);
363
        if (res != UA_STATUSCODE_GOOD) {
2✔
364
            throw std::runtime_error("Object node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
365
        }
366
        return nodeId;
4✔
367
    }
368

369
    void TServerImpl::CreateVariableNode(const UA_NodeId& parentNodeId,
1✔
370
                                         const std::string& nodeName,
371
                                         WBMQTT::PControl control)
372
    {
373
        UA_VariableAttributes oAttr = UA_VariableAttributes_default;
1✔
374
        SetVariableAttributes(oAttr, control);
1✔
375

376
        UA_DataSource dataSource;
377
        dataSource.read = ReadVariableCallback;
1✔
378
        dataSource.write = WriteVariableCallback;
1✔
379

380
        auto res = UA_Server_addDataSourceVariableNode(Server,
2✔
381
                                                       UA_NODEID_STRING(1, (char*)nodeName.c_str()),
1✔
382
                                                       parentNodeId,
383
                                                       UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
384
                                                       UA_QUALIFIEDNAME(1, (char*)control->GetId().c_str()),
1✔
385
                                                       UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
386
                                                       oAttr,
387
                                                       dataSource,
388
                                                       this,
389
                                                       nullptr);
390
        if (res != UA_STATUSCODE_GOOD) {
1✔
391
            throw std::runtime_error("Variable node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
392
        }
393
    }
1✔
394

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

© 2026 Coveralls, Inc