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

wirenboard / wb-mqtt-opcua / 6

31 Jul 2025 02:40PM UTC coverage: 30.077%. First build
6

Pull #32

github

u236
fix changelog
Pull Request #32: Fix late created controls bug

104 of 277 branches covered (37.55%)

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

157 of 522 relevant lines covered (30.08%)

0.91 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
        UA_NodeId CreateObjectNode(const std::string& nodeName)
×
166
        {
167
            UA_NodeId nodeId = UA_NODEID_STRING(1, (char*)nodeName.c_str());
×
168
            UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
×
169
            oAttr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)nodeName.c_str());
×
170
            auto res = UA_Server_addObjectNode(Server,
×
171
                                               nodeId,
172
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
173
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
174
                                               UA_QUALIFIEDNAME(1, (char*)nodeName.c_str()),
×
175
                                               UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
176
                                               oAttr,
177
                                               nullptr,
178
                                               nullptr);
179
            if (res != UA_STATUSCODE_GOOD) {
×
180
                throw std::runtime_error("Object node '" + nodeName + "' creation failed: " + UA_StatusCode_name(res));
×
181
            }
182
            return nodeId;
×
183
        }
184

NEW
185
        void CreateVariableNode(const UA_NodeId& parentNodeId, const std::string& nodeName, WBMQTT::PControl control)
×
186
        {
187
            UA_VariableAttributes oAttr = UA_VariableAttributes_default;
×
NEW
188
            SetVariableAttributes(oAttr, control);
×
189

190
            UA_DataSource dataSource;
191
            dataSource.read = ReadVariableCallback;
×
192
            dataSource.write = WriteVariableCallback;
×
193

194
            auto res = UA_Server_addDataSourceVariableNode(Server,
×
NEW
195
                                                           UA_NODEID_STRING(1, (char*)nodeName.c_str()),
×
196
                                                           parentNodeId,
197
                                                           UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
NEW
198
                                                           UA_QUALIFIEDNAME(1, (char*)control->GetId().c_str()),
×
199
                                                           UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
200
                                                           oAttr,
201
                                                           dataSource,
202
                                                           this,
203
                                                           nullptr);
204
            if (res != UA_STATUSCODE_GOOD) {
×
NEW
205
                throw std::runtime_error("Variable node '" + nodeName +
×
206
                                         "' creation failed: " + UA_StatusCode_name(res));
×
207
            }
208
        }
209

210
    public:
211
        TServerImpl(const OPCUA::TServerConfig& config, WBMQTT::PDeviceDriver driver)
×
212
            : Server(UA_Server_new()),
×
213
              IsRunning(true),
214
              Driver(driver)
×
215
        {
216
            if (!Server) {
×
217
                throw std::runtime_error("OPC UA server initilization failed");
×
218
            }
219

NEW
220
            Driver->On<WBMQTT::TControlValueEvent>([&](const WBMQTT::TControlValueEvent& event) {
×
NEW
221
                if (event.RawValue.empty()) {
×
NEW
222
                    return;
×
223
                }
224

NEW
225
                auto it = config.ObjectNodes.find(event.Control->GetDevice()->GetId());
×
NEW
226
                if (it == config.ObjectNodes.end()) {
×
NEW
227
                    return;
×
228
                }
229

NEW
230
                auto browseName = UA_QUALIFIEDNAME(1, (char*)it->first.c_str());
×
NEW
231
                auto res = UA_Server_browseSimplifiedBrowsePath(Server,
×
232
                                                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
233
                                                                1,
NEW
234
                                                                &browseName);
×
235
                auto parentNodeId =
NEW
236
                    res.statusCode == UA_STATUSCODE_GOOD ? res.targets[0].targetId.nodeId : CreateObjectNode(it->first);
×
NEW
237
                std::string nodeName = it->first + "/" + event.Control->GetId();
×
NEW
238
                for (auto& valueNode: it->second) {
×
NEW
239
                    if (valueNode.DeviceControlPair != nodeName) {
×
NEW
240
                        continue;
×
241
                    }
NEW
242
                    browseName = UA_QUALIFIEDNAME(1, (char*)event.Control->GetId().c_str());
×
NEW
243
                    res = UA_Server_browseSimplifiedBrowsePath(Server, parentNodeId, 1, &browseName);
×
NEW
244
                    if (res.statusCode != UA_STATUSCODE_GOOD) {
×
245
                        {
NEW
246
                            std::unique_lock<std::mutex> lock(Mutex);
×
NEW
247
                            ControlMap[nodeName] = event.Control;
×
248
                        }
NEW
249
                        CreateVariableNode(parentNodeId, nodeName, event.Control);
×
NEW
250
                        break;
×
251
                    }
252
                }
NEW
253
            });
×
254

255
            // Load external controls
256
            std::vector<std::string> deviceIds;
×
257
            for (const auto& device: config.ObjectNodes) {
×
258
                LOG(Debug) << "'" << device.first << "' is added to filter";
×
259
                deviceIds.emplace_back(device.first);
×
260
            }
261
            Driver->SetFilter(WBMQTT::GetDeviceListFilter(deviceIds));
×
262
            Driver->WaitForReady();
×
263

264
            // Setup and run OPC UA server
265
            ConfigureOpcUaServer(UA_Server_getConfig(Server), config);
×
266
            ServerThread = std::thread([this]() {
×
267
                auto res = UA_Server_run(Server, &IsRunning);
×
268
                if (res != UA_STATUSCODE_GOOD) {
×
269
                    LOG(Error) << UA_StatusCode_name(res);
×
270
                    exit(1);
×
271
                }
272
            });
×
273
        }
274

275
        ~TServerImpl()
×
276
        {
277
            if (IsRunning) {
×
278
                IsRunning = false;
×
279
                if (ServerThread.joinable()) {
×
280
                    ServerThread.join();
×
281
                }
282
            }
283
            if (Server) {
×
284
                UA_Server_delete(Server);
×
285
            }
286
        }
287

288
        UA_StatusCode writeVariable(const UA_NodeId* snodeId, const UA_DataValue* dataValue)
×
289
        {
290
            std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
×
NEW
291
            WBMQTT::PControl control;
×
292
            {
NEW
293
                std::unique_lock<std::mutex> lock(Mutex);
×
NEW
294
                auto it = ControlMap.find(nodeIdName);
×
NEW
295
                if (it == ControlMap.end() || it->second->IsReadonly()) {
×
NEW
296
                    LOG(Error) << "Variable node '" + nodeIdName + "' writing failed. It is "
×
NEW
297
                               << (it == ControlMap.end() ? "not presented in MQTT" : "read only");
×
NEW
298
                    return UA_STATUSCODE_BADDEVICEFAILURE;
×
299
                }
NEW
300
                control = it->second;
×
301
            }
NEW
302
            auto tx = Driver->BeginTx();
×
303
            try {
304
                if (dataValue->hasValue) {
×
305
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_BOOLEAN])) {
×
306
                        auto value = *(UA_Boolean*)dataValue->value.data;
×
NEW
307
                        control->SetValue(tx, value).Sync();
×
308
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
309
                        return UA_STATUSCODE_GOOD;
×
310
                    }
311
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_DOUBLE])) {
×
312
                        auto value = *(UA_Double*)dataValue->value.data;
×
NEW
313
                        control->SetValue(tx, value).Sync();
×
314
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
315
                        return UA_STATUSCODE_GOOD;
×
316
                    }
317
                    if (UA_Variant_hasScalarType(&dataValue->value, &UA_TYPES[UA_TYPES_STRING])) {
×
318
                        auto value = (char*)((UA_String*)dataValue->value.data)->data;
×
NEW
319
                        control->SetRawValue(tx, value).Sync();
×
320
                        LOG(Info) << "Variable node '" + nodeIdName + "' = " << value;
×
321
                        return UA_STATUSCODE_GOOD;
×
322
                    }
323
                }
324
                return UA_STATUSCODE_BADDATATYPEIDUNKNOWN;
×
325
            } catch (const std::exception& e) {
×
326
                LOG(Error) << "Variable node '" + nodeIdName + "' write error: " << e.what();
×
327
                return UA_STATUSCODE_BADDEVICEFAILURE;
×
328
            }
329
        }
330

331
        UA_StatusCode readVariable(const UA_NodeId* snodeId, UA_DataValue* dataValue)
×
332
        {
333
            std::string nodeIdName((const char*)snodeId->identifier.string.data, snodeId->identifier.string.length);
×
NEW
334
            WBMQTT::PControl control;
×
335
            {
NEW
336
                std::unique_lock<std::mutex> lock(Mutex);
×
NEW
337
                auto it = ControlMap.find(nodeIdName);
×
NEW
338
                if (it == ControlMap.end()) {
×
NEW
339
                    LOG(Error) << "Control is not found '" + nodeIdName + "'";
×
NEW
340
                    dataValue->hasStatus = true;
×
NEW
341
                    dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
NEW
342
                    return UA_STATUSCODE_GOOD;
×
343
                }
NEW
344
                control = it->second;
×
345
            }
346
            try {
347
                dataValue->hasStatus = true;
×
NEW
348
                if (control->GetError().find("r") != std::string::npos) {
×
349
                    dataValue->status = UA_STATUSCODE_BAD;
×
350
                } else {
351
                    dataValue->status = UA_STATUSCODE_GOOD;
×
352
                }
NEW
353
                auto v = control->GetValue();
×
354
                if (v.Is<bool>()) {
×
355
                    auto value = v.As<bool>();
×
356
                    UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_BOOLEAN]);
×
357
                } else {
358
                    if (v.Is<double>()) {
×
359
                        auto value = v.As<double>();
×
360
                        UA_Variant_setScalarCopy(&dataValue->value, &value, &UA_TYPES[UA_TYPES_DOUBLE]);
×
361
                    } else {
362
                        UA_String stringValue = UA_String_fromChars((char*)v.As<std::string>().c_str());
×
363
                        UA_Variant_setScalarCopy(&dataValue->value, &stringValue, &UA_TYPES[UA_TYPES_STRING]);
×
364
                        UA_String_clear(&stringValue);
×
365
                    }
366
                }
367
                dataValue->hasValue = true;
×
368
            } catch (const std::exception& e) {
×
369
                LOG(Error) << "Variable node '" + nodeIdName + "' read error: " << e.what();
×
370
                dataValue->hasStatus = true;
×
371
                dataValue->status = UA_STATUSCODE_BADNOCOMMUNICATION;
×
372
            }
373
            return UA_STATUSCODE_GOOD;
×
374
        }
375
    };
376

377
    extern "C" {
378
    UA_StatusCode ReadVariableCallback(UA_Server* sserver,
×
379
                                       const UA_NodeId* ssessionId,
380
                                       void* ssessionContext,
381
                                       const UA_NodeId* snodeId,
382
                                       void* snodeContext,
383
                                       UA_Boolean ssourceTimeStamp,
384
                                       const UA_NumericRange* range,
385
                                       UA_DataValue* dataValue)
386
    {
387
        TServerImpl* server = (TServerImpl*)(snodeContext);
×
388
        return server->readVariable(snodeId, dataValue);
×
389
    }
390

391
    UA_StatusCode WriteVariableCallback(UA_Server* server,
×
392
                                        const UA_NodeId* sessionId,
393
                                        void* sessionContext,
394
                                        const UA_NodeId* nodeId,
395
                                        void* nodeContext,
396
                                        const UA_NumericRange* range,
397
                                        const UA_DataValue* data)
398
    {
399
        TServerImpl* s = (TServerImpl*)(nodeContext);
×
400
        return s->writeVariable(nodeId, data);
×
401
    }
402
    }
403
}
404

405
std::unique_ptr<OPCUA::IServer> OPCUA::MakeServer(const OPCUA::TServerConfig& config, WBMQTT::PDeviceDriver driver)
×
406
{
407
    return std::unique_ptr<OPCUA::IServer>(new TServerImpl(config, driver));
×
408
}
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