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

wirenboard / wb-mqtt-opcua / 7

04 Aug 2025 05:01AM UTC coverage: 57.942% (+28.1%) from 29.848%
7

Pull #33

github

u236
add driver->WaitForReady() to test
Pull Request #33: Refactor TServerImpl class

168 of 282 branches covered (59.57%)

106 of 161 new or added lines in 2 files covered. (65.84%)

1 existing line in 1 file now uncovered.

321 of 554 relevant lines covered (57.94%)

1.45 hits per line

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

65.6
/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)
5✔
17
    {
18
        va_list args2;
19
        va_copy(args2, args);
5✔
20
        auto bufSize = 1 + vsnprintf(nullptr, 0, msg, args);
5✔
21
        std::string str(bufSize, '\0');
5✔
22
        vsnprintf(&str[0], bufSize, msg, args2);
5✔
23
        va_end(args2);
5✔
24
        logger.Log() << "[OPCUA] " << LogCategoryNames[category] << ": " << str;
5✔
25
    }
5✔
26

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

48
    void LogClear(void* logContext)
1✔
49
    {}
1✔
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

UNCOV
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
    {
NEW
72
        OPCUA::TServerImpl* s = (OPCUA::TServerImpl*)(nodeContext);
×
NEW
73
        return s->WriteVariable(nodeId, data);
×
74
    }
75
    }
76

77
    UA_Logger MakeLogger()
1✔
78
    {
79
        UA_Logger logger = {Log, nullptr, LogClear};
1✔
80
        return logger;
1✔
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)
1✔
106
    {
107
        serverCfg->logger = MakeLogger();
1✔
108

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

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

120
        if (!config.BindIp.empty()) {
1✔
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);
1✔
126
        if (res != UA_STATUSCODE_GOOD) {
1✔
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);
1✔
132
        if (res != UA_STATUSCODE_GOOD) {
1✔
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,
2✔
137
                                       true,
138
                                       &serverCfg->securityPolicies[serverCfg->securityPoliciesSize - 1].policyUri,
1✔
139
                                       0,
140
                                       nullptr);
141
        if (res != UA_STATUSCODE_GOOD) {
1✔
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);
1✔
147
        if (res != UA_STATUSCODE_GOOD) {
1✔
148
            throw std::runtime_error(std::string("OPC UA server endpoint allocation failed: ") +
×
149
                                     UA_StatusCode_name(res));
×
150
        }
151
    }
1✔
152

153
}
154

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

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

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

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

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

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

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

215
    WBMQTT::PControl TServerImpl::GetControl(const std::string& nodeName)
3✔
216
    {
217
        std::unique_lock<std::mutex> lock(Mutex);
3✔
218
        auto it = ControlMap.find(nodeName);
3✔
219
        return it != ControlMap.end() ? it->second : nullptr;
9✔
220
    }
221

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

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

300
    void TServerImpl::ControlValueEventCallback(const WBMQTT::TControlValueEvent& event)
1✔
301
    {
302
        if (event.RawValue.empty()) {
1✔
NEW
303
            return;
×
304
        }
305
        auto it = Config.ObjectNodes.find(event.Control->GetDevice()->GetId());
1✔
306
        if (it == Config.ObjectNodes.end()) {
1✔
NEW
307
            return;
×
308
        }
309
        std::string nodeName = it->first + "/" + event.Control->GetId();
1✔
310
        if (ControlExists(nodeName)) {
1✔
NEW
311
            return;
×
312
        }
313
        auto browseName = UA_QUALIFIEDNAME(1, (char*)it->first.c_str());
1✔
314
        auto res =
315
            UA_Server_browseSimplifiedBrowsePath(Server, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), 1, &browseName);
1✔
316
        auto parentNodeId =
317
            res.statusCode == UA_STATUSCODE_GOOD ? res.targets[0].targetId.nodeId : CreateObjectNode(it->first);
1✔
318
        for (auto& valueNode: it->second) {
1✔
319
            if (valueNode.DeviceControlPair != nodeName) {
1✔
NEW
320
                continue;
×
321
            }
322
            browseName = UA_QUALIFIEDNAME(1, (char*)event.Control->GetId().c_str());
1✔
323
            res = UA_Server_browseSimplifiedBrowsePath(Server, parentNodeId, 1, &browseName);
1✔
324
            if (res.statusCode != UA_STATUSCODE_GOOD) {
1✔
325
                AddControl(nodeName, event.Control);
1✔
326
                CreateVariableNode(parentNodeId, nodeName, event.Control);
1✔
327
                break;
1✔
328
            }
329
        }
330
    }
331

332
    UA_NodeId TServerImpl::CreateObjectNode(const std::string& nodeName)
1✔
333
    {
334
        UA_NodeId nodeId = UA_NODEID_STRING(1, (char*)nodeName.c_str());
1✔
335
        UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
1✔
336
        oAttr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)nodeName.c_str());
1✔
337
        auto res = UA_Server_addObjectNode(Server,
2✔
338
                                           nodeId,
339
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
340
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
341
                                           UA_QUALIFIEDNAME(1, (char*)nodeName.c_str()),
1✔
342
                                           UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
343
                                           oAttr,
344
                                           nullptr,
345
                                           nullptr);
346
        if (res != UA_STATUSCODE_GOOD) {
1✔
NEW
347
            throw std::runtime_error("Object node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
348
        }
349
        return nodeId;
2✔
350
    }
351

352
    void TServerImpl::CreateVariableNode(const UA_NodeId& parentNodeId,
1✔
353
                                         const std::string& nodeName,
354
                                         WBMQTT::PControl control)
355
    {
356
        UA_VariableAttributes oAttr = UA_VariableAttributes_default;
1✔
357
        SetVariableAttributes(oAttr, control);
1✔
358

359
        UA_DataSource dataSource;
360
        dataSource.read = ReadVariableCallback;
1✔
361
        dataSource.write = WriteVariableCallback;
1✔
362

363
        auto res = UA_Server_addDataSourceVariableNode(Server,
2✔
364
                                                       UA_NODEID_STRING(1, (char*)nodeName.c_str()),
1✔
365
                                                       parentNodeId,
366
                                                       UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
367
                                                       UA_QUALIFIEDNAME(1, (char*)control->GetId().c_str()),
1✔
368
                                                       UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
369
                                                       oAttr,
370
                                                       dataSource,
371
                                                       this,
372
                                                       nullptr);
373
        if (res != UA_STATUSCODE_GOOD) {
1✔
NEW
374
            throw std::runtime_error("Variable node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
375
        }
376
    }
1✔
377

NEW
378
    std::unique_ptr<IServer> MakeServer(const TServerConfig& config, WBMQTT::PDeviceDriver driver)
×
379
    {
NEW
380
        return std::unique_ptr<IServer>(new TServerImpl(config, driver));
×
381
    }
382
}
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