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

wirenboard / wb-mqtt-gpio / 93

31 Jul 2026 10:43AM UTC coverage: 44.281% (+4.5%) from 39.772%
93

push

github

web-flow
Use exported vars instead of MAKEFLAGS for coverage options (#82)

493 of 1086 branches covered (45.4%)

875 of 1976 relevant lines covered (44.28%)

3.96 hits per line

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

52.75
/src/config.cpp
1
#include "config.h"
2
#include "exceptions.h"
3
#include "file_utils.h"
4
#include "gpio_chip.h"
5
#include "gpio_line.h"
6
#include "log.h"
7
#include "utils.h"
8

9
#include <wblib/json_utils.h>
10
#include <wblib/utils.h>
11

12
#include <algorithm>
13
#include <fstream>
14
#include <iostream>
15
#include <unordered_set>
16

17
#define LOG(logger) ::logger.Log() << "[config] "
18

19
using namespace std;
20
using namespace Utils;
21
using namespace WBMQTT::JSON;
22

23
namespace
24
{
25
    const string ProtectedProperties[] = {"gpio", "direction", "inverted", "open_drain", "open_source"};
26

27
    void AppendLine(TGpioDriverConfig& cfg, const std::string& gpioChipPath, const TGpioLineConfig& line)
6✔
28
    {
29
        auto chipConfig =
30
            find_if(cfg.Chips.begin(), cfg.Chips.end(), [&](const auto& c) { return c.Path == gpioChipPath; });
7✔
31
        if (chipConfig == cfg.Chips.end()) {
6✔
32
            cfg.Chips.emplace_back(gpioChipPath);
6✔
33
            chipConfig = cfg.Chips.end();
6✔
34
            --chipConfig;
6✔
35
        }
36

37
        auto itLine = find_if(chipConfig->Lines.begin(), chipConfig->Lines.end(), [&](const auto& l) {
6✔
38
            return l.Offset == line.Offset;
×
39
        });
40
        if (itLine != chipConfig->Lines.end()) {
6✔
41
            wb_throw(TGpioDriverException,
×
42
                     "duplicate GPIO offset in config: '" + to_string(line.Offset) + "' at chip '" + chipConfig->Path +
43
                         "' defined as '" + line.Name + "'. It is already defined as '" + itLine->Name +
44
                         "'. To override set similar MQTT id (name).");
45
        }
46

47
        chipConfig->Lines.push_back(line);
6✔
48
    }
6✔
49

50
    TGpioDriverConfig LoadFromJSON(const Json::Value& root)
5✔
51
    {
52
        TGpioDriverConfig cfg;
5✔
53
        const auto& channels = root["channels"];
5✔
54

55
        cfg.Debug = root.isMember("debug") && root["debug"].asBool();
5✔
56

57
        if (root.isMember("device_name")) {
5✔
58
            cfg.DeviceName = root["device_name"].asString();
5✔
59
        }
60

61
        int32_t maxUnchangedInterval = -1;
5✔
62
        Get(root, "max_unchanged_interval", maxUnchangedInterval);
5✔
63
        cfg.PublishParameters.Set(maxUnchangedInterval);
5✔
64

65
        for (const auto& channel: channels) {
11✔
66
            if (!channel.isMember("gpio")) {
6✔
67
                LOG(Warn) << "Skip GPIO \"" << channel["name"].asString()
×
68
                          << "\", it is unavailable or badly configured";
×
69
                continue;
×
70
            }
71
            TGpioLineConfig lineConfig;
6✔
72
            string path;
6✔
73
            if (channel["gpio"].isUInt()) {
6✔
74
                uint32_t gpioNumber = channel["gpio"].asUInt();
×
75
                uint32_t chipNumber;
76
                try {
77
                    tie(chipNumber, lineConfig.Offset) = FromSysfsGpio(gpioNumber);
×
78
                } catch (const TGpioDriverException& e) {
×
79
                    LOG(Error) << "Skipping GPIO " << gpioNumber << " reason: " << e.what();
×
80
                    continue;
×
81
                }
×
82
                path = GpioChipNumberToPath(chipNumber);
×
83
            } else {
84
                lineConfig.Offset = channel["gpio"]["offset"].asUInt();
6✔
85
                path = channel["gpio"]["chip"].asString();
6✔
86
            }
87

88
            lineConfig.Name = channel["name"].asString();
6✔
89

90
            Get(channel, "inverted", lineConfig.IsActiveLow);
12✔
91
            Get(channel, "open_drain", lineConfig.IsOpenDrain);
12✔
92
            Get(channel, "open_source", lineConfig.IsOpenSource);
12✔
93
            Get(channel, "type", lineConfig.Type);
12✔
94
            Get(channel, "multiplier", lineConfig.Multiplier);
12✔
95
            Get(channel, "decimal_points_current", lineConfig.DecimalPlacesCurrent);
12✔
96
            Get(channel, "decimal_points_total", lineConfig.DecimalPlacesTotal);
12✔
97
            Get(channel, "initial_state", lineConfig.InitialState);
12✔
98
            Get(channel, "load_previous_state", lineConfig.LoadPreviousState);
12✔
99
            Get(channel, "debounce", lineConfig.DebounceTimeout);
6✔
100

101
            if (channel.isMember("direction") && channel["direction"].asString() == "input")
6✔
102
                lineConfig.Direction = EGpioDirection::Input;
3✔
103

104
            if (channel.isMember("edge")) {
6✔
105
                if (lineConfig.Type.empty()) {
6✔
106
                    LOG(Warn) << "Edge setting for GPIO \"" << lineConfig.Name
×
107
                              << "\" is not used. It can be set only for GPIO with "
108
                                 "\"type\" option";
×
109
                } else {
110
                    EnumerateGpioEdge(channel["edge"].asString(), lineConfig.InterruptEdge);
6✔
111
                }
112
            }
113

114
            AppendLine(cfg, path, lineConfig);
6✔
115
        }
6✔
116
        return cfg;
5✔
117
    }
×
118

119
    Json::Value RemoveDeviceNameRequirement(const Json::Value& schema)
14✔
120
    {
121
        auto res = schema;
14✔
122
        Json::Value newArray = Json::arrayValue;
14✔
123
        for (auto& v: schema["required"]) {
70✔
124
            if (v.asString() != "device_name") {
28✔
125
                newArray.append(v);
14✔
126
            }
127
        }
128
        res["required"] = newArray;
14✔
129
        return res;
14✔
130
    }
14✔
131

132
    template<class T, class Pred> void erase_if(T& c, Pred pred)
5✔
133
    {
134
        c.erase(std::remove_if(c.begin(), c.end(), pred), c.end());
5✔
135
    }
5✔
136

137
    void RemoveUnusedChips(TGpioDriverConfig& cfg)
5✔
138
    {
139
        erase_if(cfg.Chips, [](const auto& c) { return c.Lines.empty(); });
11✔
140
    }
5✔
141

142
    TGpioDriverConfig LoadConfigInternal(const std::string& mainConfigFile,
18✔
143
                                         const std::string& optionalConfigFile,
144
                                         const std::string& systemConfigsDir,
145
                                         const std::string& schemaFile)
146
    {
147
        Json::Value schema = Parse(schemaFile);
18✔
148

149
        if (!optionalConfigFile.empty()) {
17✔
150
            auto cfg = Parse(optionalConfigFile);
3✔
151
            Validate(cfg, schema);
2✔
152
            return LoadFromJSON(cfg);
1✔
153
        }
2✔
154

155
        TMergeParams mergeParams;
14✔
156
        mergeParams.LogPrefix = "[config] ";
14✔
157
        mergeParams.InfoLogger = &Info;
14✔
158
        mergeParams.WarnLogger = &Warn;
14✔
159
        mergeParams.MergeArraysOn["/channels"] = "name";
14✔
160

161
        Json::Value resultingConfig;
14✔
162
        resultingConfig["channels"] = Json::Value(Json::arrayValue);
14✔
163

164
        Json::Value noDeviceNameSchema = RemoveDeviceNameRequirement(schema);
14✔
165
        try {
166
            IterateDirByPattern(systemConfigsDir, ".conf", [&](const string& f) {
40✔
167
                auto cfg = Parse(f);
2✔
168
                Validate(cfg, noDeviceNameSchema);
2✔
169
                Merge(resultingConfig, cfg, mergeParams);
2✔
170
                return false;
2✔
171
            });
2✔
172
        } catch (const TNoDirError&) {
12✔
173
        }
12✔
174
        {
175
            for (const auto& pr: ProtectedProperties) {
84✔
176
                mergeParams.ProtectedParameters.insert("/channels/" + pr);
70✔
177
            }
178
            auto cfg = Parse(mainConfigFile);
14✔
179
            Validate(cfg, schema);
13✔
180
            Merge(resultingConfig, cfg, mergeParams);
4✔
181
        }
13✔
182
        return LoadFromJSON(resultingConfig);
4✔
183
    }
47✔
184
} // namespace
185

186
TGpioDriverConfig LoadConfig(const std::string& mainConfigFile,
18✔
187
                             const std::string& optionalConfigFile,
188
                             const std::string& systemConfigsDir,
189
                             const std::string& schemaFile,
190
                             const TConfigValidationHints& validationHints)
191
{
192
    TGpioDriverConfig cfg(LoadConfigInternal(mainConfigFile, optionalConfigFile, systemConfigsDir, schemaFile));
18✔
193
    RemoveUnusedChips(cfg);
5✔
194
    if (validationHints.WarnAboutCountersWithInvertedInput) {
5✔
195
        for (const auto& chip: cfg.Chips) {
×
196
            for (const auto& line: chip.Lines) {
×
197
                if (!line.Type.empty() && line.IsActiveLow) {
×
198
                    LOG(Warn) << line.Name << "(" << chip.Path << ":" << to_string(line.Offset)
×
199
                              << ") is used as counter and has inverted option. "
×
200
                              << "Impulse counting could be wrong because of a kernel bug. It "
201
                                 "is recommended to upgrade kernel to v5.3 or newer";
×
202
                }
203
            }
204
        }
205
    }
206
    return cfg;
5✔
207
}
×
208

209
void MakeJsonForConfed(const string& configFile, const string& systemConfigsDir, const string& schemaFile)
×
210
{
211
    Json::Value schema = Parse(schemaFile);
×
212
    Json::Value noDeviceNameSchema = RemoveDeviceNameRequirement(schema);
×
213
    auto config = Parse(configFile);
×
214
    Validate(config, schema);
×
215
    unordered_map<string, Json::Value> configuredChannels;
×
216
    for (const auto& ch: config["channels"]) {
×
217
        configuredChannels.emplace(ch["name"].asString(), ch);
×
218
    }
219
    Json::Value newChannels(Json::arrayValue);
×
220
    try {
221
        IterateDirByPattern(systemConfigsDir, ".conf", [&](const string& f) {
×
222
            auto cfg = Parse(f);
×
223
            Validate(cfg, noDeviceNameSchema);
×
224
            for (const auto& ch: cfg["channels"]) {
×
225
                auto name = ch["name"].asString();
×
226
                auto it = configuredChannels.find(name);
×
227
                if (it != configuredChannels.end()) {
×
228
                    newChannels.append(it->second);
×
229
                    configuredChannels.erase(name);
×
230
                } else {
231
                    Json::Value v;
×
232
                    v["name"] = ch["name"];
×
233
                    v["direction"] = ch["direction"];
×
234
                    newChannels.append(v);
×
235
                }
×
236
            }
×
237
            return false;
×
238
        });
×
239
    } catch (const TNoDirError&) {
×
240
    }
×
241

242
    // Add custom channels.
243
    // They must contain "gpio" property,
244
    // otherwise it is a config for unavailable channel and must be skipped
245
    for (const auto& ch: config["channels"]) {
×
246
        auto it = configuredChannels.find(ch["name"].asString());
×
247
        if (it != configuredChannels.end() && ch.isMember("gpio")) {
×
248
            newChannels.append(ch);
×
249
        }
250
    }
251
    config["channels"].swap(newChannels);
×
252
    MakeWriter("", "None")->write(config, &cout);
×
253
}
×
254

255
void MakeConfigFromConfed(const string& systemConfigsDir, const string& schemaFile)
×
256
{
257
    Json::Value noDeviceNameSchema = RemoveDeviceNameRequirement(Parse(schemaFile));
×
258
    unordered_set<string> systemChannels;
×
259
    try {
260
        IterateDirByPattern(systemConfigsDir, ".conf", [&](const string& f) {
×
261
            auto cfg = Parse(f);
×
262
            Validate(cfg, noDeviceNameSchema);
×
263
            for (const auto& ch: cfg["channels"]) {
×
264
                systemChannels.insert(ch["name"].asString());
×
265
            }
266
            return false;
×
267
        });
×
268
    } catch (const TNoDirError&) {
×
269
    }
×
270

271
    Json::Value config;
×
272
    Json::CharReaderBuilder readerBuilder;
×
273
    Json::String errs;
×
274

275
    if (!Json::parseFromStream(readerBuilder, cin, &config, &errs)) {
×
276
        throw runtime_error("Failed to parse JSON:" + errs);
×
277
    }
278

279
    Json::Value newChannels(Json::arrayValue);
×
280
    for (auto& ch: config["channels"]) {
×
281
        auto it = systemChannels.find(ch["name"].asString());
×
282
        if (it != systemChannels.end()) {
×
283
            for (const auto& pr: ProtectedProperties) {
×
284
                ch.removeMember(pr);
×
285
            }
286
            if (ch.size() > 1) {
×
287
                newChannels.append(ch);
×
288
            }
289
        } else {
290
            newChannels.append(ch);
×
291
        }
292
    }
293
    config["channels"].swap(newChannels);
×
294
    MakeWriter("  ", "None")->write(config, &cout);
×
295
}
×
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