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

wirenboard / wb-mqtt-smartweb / 42

26 Jun 2025 11:50AM UTC coverage: 34.328%. First build
42

push

github

web-flow
Add more verbose error message (#36)

Add error message about invalid I_AM_PROGRAM frame

---------

Co-authored-by: Nikolay Korotkiy <sikmir@disroot.org>

217 of 649 branches covered (33.44%)

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

473 of 1361 relevant lines covered (34.75%)

9.82 hits per line

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

21.81
/src/SmartWebToMqttGateway.cpp
1
#include "SmartWebToMqttGateway.h"
2

3
#include <string.h>
4
#include <wblib/exceptions.h>
5

6
#include "exceptions.h"
7
#include "log.h"
8

9
#include "MqttToSmartWebGateway.h"
10

11
namespace
12
{
13
    const int16_t SENSOR_SHORT_VALUE = -32768;
14
    const int16_t SENSOR_OPEN_VALUE = -32767;
15
    const int16_t SENSOR_UNDEFINED = -32766;
16
}
17

18
TEnumCodec::TEnumCodec(const std::map<uint8_t, std::string>& values): Values(values)
18✔
19
{
20
    for (const auto& v: values) {
252✔
21
        Keys.insert({v.second, v.first});
234✔
22
    }
23
}
18✔
24

25
std::string TEnumCodec::Decode(const uint8_t* buf) const
×
26
{
27
    auto it = Values.find(*buf);
×
28
    if (it != Values.end()) {
×
29
        return it->second;
×
30
    }
31
    return std::to_string((int)*buf);
×
32
}
33

34
std::vector<uint8_t> TEnumCodec::Encode(const std::string& value) const
×
35
{
36
    auto it = Keys.find(value);
×
37
    if (it != Keys.end()) {
×
38
        return {it->second};
×
39
    }
40
    throw std::runtime_error("unknown value for enum parameter: " + value);
×
41
}
42

43
std::string TEnumCodec::GetName() const
18✔
44
{
45
    std::string res;
18✔
46
    for (const auto& v: Values) {
252✔
47
        res += (res.empty() ? "" : ", ") + std::to_string(v.first) + ": " + v.second;
234✔
48
    }
49
    return "TEnumCodec (" + res + ")";
54✔
50
}
51

52
std::string TSensorCodec::Decode(const uint8_t* buf) const
×
53
{
54
    int16_t v;
55
    memcpy(&v, buf, 2);
×
56
    if (v == SENSOR_UNDEFINED) {
×
57
        throw std::runtime_error("sensor is in undefined state");
×
58
    }
59
    if (v == SENSOR_SHORT_VALUE || v == SENSOR_OPEN_VALUE) {
×
60
        throw std::runtime_error("sensor error " + std::to_string(v));
×
61
    }
62
    return WBMQTT::FormatFloat(v / 10.0);
×
63
}
64

65
std::vector<uint8_t> TSensorCodec::Encode(const std::string& value) const
×
66
{
67
    throw std::runtime_error("sensors are readonly");
×
68
}
69

70
std::string TSensorCodec::GetName() const
26✔
71
{
72
    return "TSensorCodec";
52✔
73
}
74

75
std::string TOnOffSensorCodec::Decode(const uint8_t* buf) const
×
76
{
77
    int16_t v;
78
    memcpy(&v, buf, 2);
×
79
    if (v == SENSOR_SHORT_VALUE) {
×
80
        return "1";
×
81
    }
82
    if (v == SENSOR_OPEN_VALUE) {
×
83
        return "0";
×
84
    }
85
    if (v == SENSOR_UNDEFINED) {
×
86
        throw std::runtime_error("sensor is in undefined state");
×
87
    }
88
    return std::to_string(v);
×
89
}
90

91
std::vector<uint8_t> TOnOffSensorCodec::Encode(const std::string& value) const
×
92
{
93
    std::vector<uint8_t> res;
×
94
    res.push_back(value == "0" ? 0 : 1);
×
95
    return res;
×
96
}
97

98
std::string TOnOffSensorCodec::GetName() const
13✔
99
{
100
    return "TOnOffSensorCodec";
26✔
101
}
102

103
std::string TPwmCodec::Decode(const uint8_t* buf) const
×
104
{
105
    if (buf[0] == 255) {
×
106
        return "100";
×
107
    }
108
    return WBMQTT::FormatFloat(buf[0] / 2.54);
×
109
}
110

111
std::vector<uint8_t> TPwmCodec::Encode(const std::string& value) const
×
112
{
113
    throw std::runtime_error("outputs are readonly");
×
114
}
115

116
std::string TPwmCodec::GetName() const
×
117
{
118
    return "TPwmCodec";
×
119
}
120

121
std::string TOutputCodec::Decode(const uint8_t* buf) const
×
122
{
123
    return (buf[0] == 0 ? "0" : "1");
×
124
}
125

126
std::vector<uint8_t> TOutputCodec::Encode(const std::string& value) const
×
127
{
128
    throw std::runtime_error("outputs are readonly");
×
129
}
130

131
std::string TOutputCodec::GetName() const
1✔
132
{
133
    return "TOutputCodec";
2✔
134
}
135

136
TSmartWebToMqttGateway::TSmartWebToMqttGateway(const TSmartWebToMqttConfig& config,
×
137
                                               std::shared_ptr<CAN::IPort> canPort,
138
                                               WBMQTT::PDeviceDriver driver)
×
139
    : Config(config),
140
      Driver(driver),
141
      RequestIndex(0),
142
      Scheduler(MakeSimpleThreadedScheduler("SW to MQTT"))
×
143
{
144
    EventHandler = Driver->On<WBMQTT::TControlOnValueEvent>([this, canPort](const WBMQTT::TControlOnValueEvent& event) {
×
145
        try {
146
            auto param = event.Control->GetUserData().As<TSmartWebParameterControl>();
×
147
            auto frame = MakeSetParameterValueRequest(param, event.RawValue);
×
148
            canPort->Send(frame);
×
149
            print_frame(DebugSwToMqtt, frame, "Set value request");
×
150
        } catch (const std::exception& e) {
×
151
            ErrorSwToMqtt.Log() << "Set value request: " << e.what();
×
152
        }
153
    });
×
154

155
    Scheduler->AddTask(MakePeriodicTask(
×
156
        config.PollInterval,
×
157
        [this, canPort]() { this->HandleMapping(*canPort); },
×
158
        "SmartWeb->MQTT task"));
×
159

160
    CanReader = std::make_unique<TThreadedCanReader>(
×
161
        "SmartWeb->MQTT reader",
162
        canPort,
163
        100,
×
164
        [this](const CAN::TFrame& frame) { return AcceptFrame(frame); },
×
165
        [this](const CAN::TFrame& frame) { HandleFrame(frame); });
×
166
}
167

168
TSmartWebToMqttGateway::~TSmartWebToMqttGateway()
×
169
{
170
    Scheduler.reset();
×
171
    CanReader.reset();
×
172
    Driver->RemoveEventHandler(EventHandler);
×
173
    auto tx = Driver->BeginTx();
×
174
    for (const auto& d: DeviceIds) {
×
175
        tx->RemoveDeviceById(d).Sync();
×
176
    }
177
}
178

179
void TSmartWebToMqttGateway::HandleFrame(const CAN::TFrame& frame)
×
180
{
181
    SmartWeb::TCanHeader* header = (SmartWeb::TCanHeader*)&frame.can_id;
×
182
    if (header->rec.program_type == SmartWeb::PT_PROGRAM &&
×
183
        header->rec.function_id == SmartWeb::Program::Function::I_AM_PROGRAM)
×
184
    {
185
        AddProgram(frame);
×
186
        return;
×
187
    }
188

189
    if (header->rec.program_type == SmartWeb::PT_REMOTE_CONTROL &&
×
190
        header->rec.function_id == SmartWeb::RemoteControl::Function::GET_PARAMETER_VALUE)
×
191
    {
192
        HandleGetValueResponse(frame);
×
193
    }
194
}
195

196
bool TSmartWebToMqttGateway::AcceptFrame(const CAN::TFrame& frame) const
×
197
{
198
    if (!(frame.can_id & CAN_EFF_FLAG)) {
×
199
        return false;
×
200
    }
201

202
    SmartWeb::TCanHeader* header = (SmartWeb::TCanHeader*)&frame.can_id;
×
203
    if (header->rec.message_type != SmartWeb::MT_MSG_RESPONSE) {
×
204
        return false;
×
205
    }
206

207
    if (header->rec.program_type == SmartWeb::PT_PROGRAM &&
×
208
        header->rec.function_id == SmartWeb::Program::Function::I_AM_PROGRAM)
×
209
    {
210
        return true;
×
211
    }
212

213
    if (header->rec.program_type == SmartWeb::PT_REMOTE_CONTROL &&
×
214
        header->rec.function_id == SmartWeb::RemoteControl::Function::GET_PARAMETER_VALUE)
×
215
    {
216
        return true;
×
217
    }
218

219
    return false;
×
220
}
221

222
void TSmartWebToMqttGateway::HandleMapping(CAN::IPort& canPort)
×
223
{
224
    CAN::TFrame frame;
225
    {
226
        std::unique_lock<std::mutex> lk(RequestMutex);
×
227
        if (Requests.empty()) {
×
228
            return;
×
229
        }
230
        if (RequestIndex == Requests.size()) {
×
231
            RequestIndex = 0;
×
232
        }
233
        frame = Requests[RequestIndex];
×
234
    }
235
    try {
236
        canPort.Send(frame);
×
237
        print_frame(DebugSwToMqtt, frame, "Send request");
×
238
    } catch (const std::exception& e) {
×
239
        print_frame(ErrorSwToMqtt, frame, std::string("Send request: ") + e.what());
×
240
    }
241
    ++RequestIndex;
×
242
}
243

244
CAN::TFrame MakeSetParameterValueRequest(const TSmartWebParameterControl& param, const std::string& value)
1✔
245
{
246
    CAN::TFrame frame{0};
1✔
247
    SmartWeb::TParameterData pd;
248
    pd.program_type = param.Parameter->ProgramClass->Type;
1✔
249
    pd.parameter_id = param.Parameter->Id;
1✔
250
    try {
251
        auto bytes = param.Parameter->Codec->Encode(value);
1✔
252
        memcpy(pd.value, bytes.data(), bytes.size());
1✔
253
        frame.can_dlc = bytes.size() + 2;
1✔
254
        memcpy(frame.data, &pd.raw, frame.can_dlc);
1✔
255
    } catch (std::exception& e) {
×
256
        throw std::runtime_error("Can't encode '" + value + "' for '" + param.Parameter->ProgramClass->Name + "'(" +
×
257
                                 std::to_string(int(param.ProgramId)) + "):'" + param.Parameter->Name + "'");
×
258
    }
259
    SmartWeb::TCanHeader header{0};
1✔
260
    header.rec.program_type = SmartWeb::PT_REMOTE_CONTROL;
1✔
261
    header.rec.program_id = param.ProgramId;
1✔
262
    header.rec.function_id = SmartWeb::RemoteControl::Function::SET_PARAMETER_VALUE;
1✔
263
    header.rec.message_type = SmartWeb::MT_MSG_REQUEST;
1✔
264
    frame.can_id = header.raw | CAN_EFF_FLAG;
1✔
265
    return frame;
2✔
266
}
267

268
void AddRequests(std::vector<CAN::TFrame>& requests,
2✔
269
                 const std::string& programType,
270
                 const TSmartWebClass& cl,
271
                 uint8_t programId,
272
                 const std::unordered_map<uint8_t, std::shared_ptr<TSmartWebClass>>& classes)
273
{
274
    CAN::TFrame frame{0};
2✔
275
    SmartWeb::TCanHeader header{0};
2✔
276
    header.rec.program_type = SmartWeb::PT_REMOTE_CONTROL;
2✔
277
    header.rec.program_id = programId;
2✔
278
    header.rec.function_id = SmartWeb::RemoteControl::Function::GET_PARAMETER_VALUE;
2✔
279
    header.rec.message_type = SmartWeb::MT_MSG_REQUEST;
2✔
280
    frame.can_id = header.raw | CAN_EFF_FLAG;
2✔
281

282
    SmartWeb::TParameterData pd;
283
    pd.program_type = SmartWeb::PT_PROGRAM;
2✔
284
    pd.parameter_id = SmartWeb::RemoteControl::Parameters::SENSOR;
2✔
285
    frame.can_dlc = 3;
2✔
286
    for (const auto& i: cl.Inputs) {
3✔
287
        pd.indexed_parameter.index = i.first;
1✔
288
        memcpy(frame.data, &pd.raw, frame.can_dlc);
1✔
289
        requests.push_back(frame);
1✔
290
    }
291

292
    pd.program_type = SmartWeb::PT_PROGRAM;
2✔
293
    pd.parameter_id = SmartWeb::RemoteControl::Parameters::OUTPUT;
2✔
294
    for (const auto& o: cl.Outputs) {
3✔
295
        pd.indexed_parameter.index = o.first;
1✔
296
        memcpy(frame.data, &pd.raw, frame.can_dlc);
1✔
297
        requests.push_back(frame);
1✔
298
    }
299

300
    pd.program_type = cl.Type;
2✔
301
    frame.can_dlc = 2;
2✔
302
    for (const auto& p: cl.Parameters) {
4✔
303
        pd.parameter_id = p.first;
2✔
304
        memcpy(frame.data, &pd.raw, frame.can_dlc);
2✔
305
        requests.push_back(frame);
2✔
306
    }
307

308
    for (const auto& c: cl.ParentClasses) {
4✔
309
        bool ok = false;
2✔
310
        for (const auto& cl: classes) {
3✔
311
            if (cl.second->Name == c) {
2✔
312
                AddRequests(requests, programType, *cl.second, programId, classes);
1✔
313
                ok = true;
1✔
314
                break;
1✔
315
            }
316
        }
317
        if (!ok && c != "PROGRAM") {
2✔
318
            WarnSwToMqtt.Log() << "Unknown program type: '" << c << "'";
1✔
319
        }
320
    }
321
}
2✔
322

323
void TSmartWebToMqttGateway::AddProgram(const CAN::TFrame& frame)
×
324
{
325
    SmartWeb::TCanHeader* header = (SmartWeb::TCanHeader*)&frame.can_id;
×
326
    if (KnownPrograms.count(header->rec.program_id)) {
×
327
        return;
×
328
    }
NEW
329
    if (frame.can_dlc != 3) {
×
NEW
330
        print_frame(DebugSwToMqtt,
×
331
                    frame,
NEW
332
                    "Invalid I_AM_PROGRAM frame. Expected exactly 3 bytes of data. Possibly the firmware is too old");
×
NEW
333
        return;
×
334
    }
335
    auto cl = Config.Classes.find(frame.data[2]);
×
336
    if (cl == Config.Classes.end()) {
×
337
        print_frame(DebugSwToMqtt, frame, "Unknown program type");
×
338
        return;
×
339
    }
340
    InfoSwToMqtt.Log() << "New program '" << cl->second->Name << "':" << (int)header->rec.program_id << " is found";
×
341
    KnownPrograms.insert({header->rec.program_id, cl->second.get()});
×
342
    std::unique_lock<std::mutex> lk(RequestMutex);
×
343
    AddRequests(Requests, cl->second->Name, *cl->second, header->rec.program_id, Config.Classes);
×
344
}
345

346
WBMQTT::TControlArgs TSmartWebToMqttGateway::MakeControlArgs(uint8_t programId,
×
347
                                                             const TSmartWebParameter& param,
348
                                                             const std::string& value,
349
                                                             bool error)
350
{
351
    const std::unordered_map<std::string, std::string> types({{"temperature", "temperature"},
352
                                                              {"humidity", "rel_humidity"},
353
                                                              {"onOff", "switch"},
354
                                                              {"relay", "switch"},
355
                                                              {"PWM", "range"},
356
                                                              {"%", "range"},
357
                                                              {"id", "text"},
358
                                                              {"picklist", "text"}});
×
359

360
    WBMQTT::TControlArgs res;
×
361
    res.SetId(param.Name);
×
362
    res.SetOrder(param.Order);
×
363
    res.SetReadonly(param.ReadOnly);
×
364
    auto t = types.find(param.Type);
×
365
    res.SetType((t != types.end()) ? t->second : "value");
×
366
    if (!param.ReadOnly) {
×
367
        TSmartWebParameterControl pc;
368
        pc.ProgramId = programId;
×
369
        pc.Parameter = &param;
×
370
        res.SetUserData(pc);
×
371
    }
372
    if (param.Type == "PWM" || param.Type == "%") {
×
373
        res.SetMax(100);
×
374
        res.SetUnits("%");
×
375
    }
376
    if (param.Type == "minutes") {
×
377
        res.SetUnits("min");
×
378
    }
379
    if (error) {
×
380
        res.SetError("r");
×
381
        res.SetRawValue(value.empty() ? "0" : value);
×
382
    } else {
383
        res.SetRawValue(value);
×
384
    }
385
    return res;
×
386
}
387

388
void TSmartWebToMqttGateway::SetParameter(const std::map<uint32_t, std::shared_ptr<TSmartWebParameter>>& params,
×
389
                                          uint8_t parameterId,
390
                                          const uint8_t* data,
391
                                          uint8_t programId)
392
{
393
    auto p = params.find(parameterId);
×
394
    if (p == params.end()) {
×
395
        DebugSwToMqtt.Log() << "Unknown parameter id: " << (int)parameterId;
×
396
        return;
×
397
    }
398
    std::string res;
×
399
    bool error = false;
×
400
    try {
401
        res = p->second->Codec->Decode(data);
×
402
    } catch (const std::exception& e) {
×
403
        WarnSwToMqtt.Log() << "Error reading '" << p->second->ProgramClass->Name << "':" << (int)programId << " "
×
404
                           << p->second->Name << ": " << e.what();
×
405
        error = true;
×
406
    }
407
    std::string deviceName("sw " + p->second->ProgramClass->Name + " " + std::to_string(programId));
×
408
    try {
409
        auto tx = Driver->BeginTx();
×
410
        WBMQTT::PLocalDevice device(std::dynamic_pointer_cast<WBMQTT::TLocalDevice>(tx->GetDevice(deviceName)));
×
411
        if (!device) {
×
412
            device =
413
                tx->CreateDevice(WBMQTT::TLocalDeviceArgs{}.SetId(deviceName).SetTitle(deviceName).SetIsVirtual(true))
×
414
                    .GetValue();
×
415
            DeviceIds.push_back(device->GetId());
×
416
        }
417
        auto control = device->GetControl(p->second->Name);
×
418
        if (control) {
×
419
            if (error) {
×
420
                control->SetError(tx, "r").Sync();
×
421
            } else {
422
                control->SetRawValue(tx, res).Sync();
×
423
            }
424
        } else {
425
            device->CreateControl(tx, MakeControlArgs(programId, *p->second, res, error)).GetValue();
×
426
        }
427
    } catch (const std::exception& e) {
×
428
        ErrorSwToMqtt.Log() << e.what();
×
429
    }
430
}
431

432
void TSmartWebToMqttGateway::HandleGetValueResponse(const CAN::TFrame& frame)
×
433
{
434
    SmartWeb::TCanHeader* header = (SmartWeb::TCanHeader*)&frame.can_id;
×
435
    auto cl = KnownPrograms.find(header->rec.program_id);
×
436
    if (cl == KnownPrograms.end()) {
×
437
        return;
×
438
    }
439

440
    print_frame(DebugSwToMqtt, frame, "Get value response");
×
441

442
    SmartWeb::TParameterData* data = (SmartWeb::TParameterData*)&frame.data;
×
443
    if (data->program_type == SmartWeb::PT_PROGRAM) {
×
444
        if (data->parameter_id == SmartWeb::RemoteControl::Parameters::SENSOR) {
×
445
            SetParameter(cl->second->Inputs,
×
446
                         data->indexed_parameter.index,
447
                         data->indexed_parameter.value,
×
448
                         header->rec.program_id);
449
            return;
×
450
        }
451
        if (data->parameter_id == SmartWeb::RemoteControl::Parameters::OUTPUT) {
×
452
            SetParameter(cl->second->Outputs,
×
453
                         data->indexed_parameter.index,
454
                         data->indexed_parameter.value,
×
455
                         header->rec.program_id);
456
            return;
×
457
        }
458
        DebugSwToMqtt.Log() << "Unknown parameter id: " << (int)data->parameter_id;
×
459
        return;
×
460
    }
461

462
    auto clParam = Config.Classes.find(data->program_type);
×
463
    if (clParam == Config.Classes.end()) {
×
464
        DebugSwToMqtt.Log() << "Unknown program type: " << (int)data->program_type;
×
465
        return;
×
466
    }
467
    SetParameter(clParam->second->Parameters, data->parameter_id, data->value, header->rec.program_id);
×
468
}
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