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

wirenboard / wb-mqtt-serial / 673

15 Jul 2025 07:07AM UTC coverage: 73.028% (-0.8%) from 73.863%
673

push

github

web-flow
Add device/Load and device/Set RPC

6463 of 9217 branches covered (70.12%)

51 of 322 new or added lines in 13 files covered. (15.84%)

7 existing lines in 2 files now uncovered.

12368 of 16936 relevant lines covered (73.03%)

373.64 hits per line

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

87.64
/src/modbus_common.cpp
1
#include "modbus_common.h"
2
#include "bin_utils.h"
3
#include "devices/modbus_device.h"
4
#include "log.h"
5
#include "serial_device.h"
6
#include "wb_registers.h"
7

8
using namespace std;
9
using namespace BinUtils;
10

11
#define LOG(logger) logger.Log() << "[modbus] "
12

13
namespace Modbus // modbus protocol declarations
14
{
15
    size_t InferReadResponsePDUSize(int type, size_t registerCount);
16

17
    //! Parses modbus response and stores result
18
    void ParseReadResponse(const std::vector<uint8_t>& data,
19
                           Modbus::EFunction function,
20
                           TModbusRegisterRange& range,
21
                           TRegisterCache& cache);
22
}
23

24
namespace // general utilities
25
{
26
    inline uint32_t GetModbusDataWidthIn16BitWords(const TRegisterConfig& reg)
1,757✔
27
    {
28
        return reg.Get16BitWidth();
1,757✔
29
    }
30

31
    // write 16-bit value to byte array in specified order
32
    inline void WriteAs2Bytes(uint8_t* dst, uint16_t val, EByteOrder byteOrder)
140✔
33
    {
34
        if (byteOrder == EByteOrder::LittleEndian) {
140✔
35
            dst[0] = static_cast<uint8_t>(val);
28✔
36
            dst[1] = static_cast<uint8_t>(val >> 8);
28✔
37
        } else {
38
            dst[0] = static_cast<uint8_t>(val >> 8);
112✔
39
            dst[1] = static_cast<uint8_t>(val);
112✔
40
        }
41
    }
140✔
42

43
    // returns true if multi write needs to be done
44
    inline bool IsPacking(const TRegisterConfig& reg)
226✔
45
    {
46
        return (reg.Type == Modbus::REG_HOLDING_MULTI) ||
442✔
47
               ((reg.Type == Modbus::REG_HOLDING) && (GetModbusDataWidthIn16BitWords(reg) > 1));
442✔
48
    }
49

50
    inline bool IsPacking(const Modbus::TModbusRegisterRange& range)
450✔
51
    {
52
        return (range.Type() == Modbus::REG_HOLDING_MULTI) ||
881✔
53
               ((range.Type() == Modbus::REG_HOLDING) && (range.GetCount() > 1));
881✔
54
    }
55

56
    inline bool IsSingleBitType(int type)
2,325✔
57
    {
58
        return (type == Modbus::REG_COIL) || (type == Modbus::REG_DISCRETE);
2,325✔
59
    }
60

61
    inline bool IsHoldingType(int type)
685✔
62
    {
63
        return (type == Modbus::REG_HOLDING) || (type == Modbus::REG_HOLDING_SINGLE) ||
685✔
64
               (type == Modbus::REG_HOLDING_MULTI);
685✔
65
    }
66

67
    void RethrowSerialDeviceException(const Modbus::TModbusExceptionError& err)
32✔
68
    {
69
        if (err.GetExceptionCode() == Modbus::ILLEGAL_FUNCTION ||
32✔
70
            err.GetExceptionCode() == Modbus::ILLEGAL_DATA_ADDRESS ||
55✔
71
            err.GetExceptionCode() == Modbus::ILLEGAL_DATA_VALUE)
23✔
72
        {
73
            throw TSerialDevicePermanentRegisterException(err.what());
24✔
74
        }
75
        throw TSerialDeviceTransientErrorException(err.what());
8✔
76
    }
77
} // general utilities
78

79
namespace Modbus // modbus protocol common utilities
80
{
81
    enum class OperationType : uint8_t
82
    {
83
        OP_READ = 0,
84
        OP_WRITE
85
    };
86

87
    EFunction GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many);
88

89
    TModbusRegisterRange::TModbusRegisterRange(std::chrono::microseconds averageResponseTime)
515✔
90
        : AverageResponseTime(averageResponseTime),
91
          ResponseTime(averageResponseTime)
515✔
92
    {}
515✔
93

94
    TModbusRegisterRange::~TModbusRegisterRange()
1,030✔
95
    {
96
        if (Bits)
1,030✔
97
            delete[] Bits;
118✔
98
    }
99

100
    bool TModbusRegisterRange::Add(TPort& port, PRegister reg, std::chrono::milliseconds pollLimit)
1,003✔
101
    {
102
        if (reg->GetAvailable() == TRegisterAvailability::UNAVAILABLE) {
1,003✔
103
            return true;
11✔
104
        }
105

106
        auto deviceConfig = reg->Device()->DeviceConfig();
1,984✔
107
        bool isSingleBit = IsSingleBitType(reg->GetConfig()->Type);
992✔
108
        auto addr = GetUint32RegisterAddress(reg->GetConfig()->GetAddress());
992✔
109
        const auto widthInWords = GetModbusDataWidthIn16BitWords(*reg->GetConfig());
992✔
110

111
        auto device = dynamic_cast<TModbusDevice*>(reg->Device().get());
992✔
112
        auto continuousReadEnabled = false;
992✔
113
        auto forceFrameTimeout = false;
992✔
114
        if (device != nullptr) {
992✔
115
            continuousReadEnabled = device->GetContinuousReadEnabled();
956✔
116
            forceFrameTimeout = device->GetForceFrameTimeout();
956✔
117
        }
118

119
        size_t extend;
120
        if (RegisterList().empty()) {
992✔
121
            extend = widthInWords;
496✔
122
        } else {
123
            if (HasOtherDeviceAndType(reg)) {
496✔
124
                return false;
74✔
125
            }
126

127
            // Can't add register before first register in the range
128
            if (Start > addr) {
422✔
129
                return false;
21✔
130
            }
131

132
            // Can't add register separated from last in the range by more than maxHole registers
133
            int maxHole = 0;
401✔
134
            if (reg->Device()->GetSupportsHoles()) {
401✔
135
                maxHole = isSingleBit ? deviceConfig->MaxBitHole : deviceConfig->MaxRegHole;
362✔
136
            }
137
            if (Start + Count + maxHole < addr) {
401✔
138
                return false;
70✔
139
            }
140

141
            auto hasHoles = Start + Count < addr;
331✔
142
            if (continuousReadEnabled) {
331✔
143
                HasHolesFlg = HasHolesFlg || hasHoles;
3✔
144
            } else {
145
                if (RegisterList().back()->GetAvailable() == TRegisterAvailability::UNKNOWN) {
328✔
146
                    // Read UNKNOWN 2 byte registers one by one to find availability
147
                    if (!isSingleBit) {
199✔
148
                        return false;
113✔
149
                    }
150
                    // Disable holes for reading UNKNOWN registers
151
                    if (hasHoles) {
86✔
152
                        return false;
1✔
153
                    }
154
                    // Can read up to 16 UNKNOWN single bit registers at once
155
                    size_t maxRegs = 16;
85✔
156
                    if ((deviceConfig->MaxReadRegisters > 0) && (deviceConfig->MaxReadRegisters <= MAX_READ_REGISTERS))
85✔
157
                    {
158
                        maxRegs = deviceConfig->MaxReadRegisters;
12✔
159
                    }
160
                    if (reg->GetAvailable() == TRegisterAvailability::UNKNOWN && (RegisterList().size() >= maxRegs)) {
85✔
161
                        return false;
3✔
162
                    }
163
                } else {
164
                    // Don't mix available and unknown registers
165
                    if (reg->GetAvailable() == TRegisterAvailability::UNKNOWN) {
129✔
166
                        return false;
4✔
167
                    }
168
                    HasHolesFlg = HasHolesFlg || hasHoles;
125✔
169
                }
170
            }
171

172
            extend = std::max(0, static_cast<int>(addr + widthInWords) - static_cast<int>(Start + Count));
210✔
173

174
            auto maxRegs = isSingleBit ? MAX_READ_BITS : MAX_READ_REGISTERS;
210✔
175
            if ((deviceConfig->MaxReadRegisters > 0) && (deviceConfig->MaxReadRegisters <= maxRegs)) {
210✔
176
                maxRegs = deviceConfig->MaxReadRegisters;
54✔
177
            }
178
            if (Count + extend > static_cast<size_t>(maxRegs)) {
210✔
179
                return false;
27✔
180
            }
181
        }
182

183
        auto newPduSize = InferReadResponsePDUSize(reg->GetConfig()->Type, Count + extend);
679✔
184
        // Request 8 bytes: SlaveID, Operation, Addr, Count, CRC
185
        // Response 5 bytes except data: SlaveID, Operation, Size, CRC
186
        auto sendTime = port.GetSendTimeBytes(newPduSize + 8 + 5);
679✔
187
        auto duration = sendTime + AverageResponseTime + deviceConfig->RequestDelay;
679✔
188
        if (forceFrameTimeout) {
679✔
189
            duration += deviceConfig->FrameTimeout;
1✔
190
        }
191
        auto newPollTime = std::chrono::ceil<std::chrono::milliseconds>(duration);
679✔
192

193
        if (((Count != 0) && !AddingRegisterIncreasesSize(isSingleBit, extend)) || (newPollTime <= pollLimit)) {
679✔
194

195
            if (Count == 0) {
642✔
196
                Start = addr;
459✔
197
            }
198

199
            RegisterList().push_back(reg);
642✔
200
            Count += extend;
642✔
201
            return true;
642✔
202
        }
203
        if (newPollTime > pollLimit) {
37✔
204
            LOG(Debug) << "Poll time for " << reg->ToString() << " is too long: " << newPollTime.count() << " ms"
74✔
205
                       << " (sendTime=" << sendTime.count() << " us, "
37✔
206
                       << "AverageResponseTime=" << AverageResponseTime.count() << " us, "
37✔
207
                       << "RequestDelay=" << deviceConfig->RequestDelay.count() << " us, "
37✔
208
                       << "FrameTimeout=" << deviceConfig->FrameTimeout.count() << " ms)"
37✔
209
                       << ", limit is " << pollLimit.count() << " ms";
37✔
210
        }
211
        return false;
37✔
212
    }
213

214
    uint8_t* TModbusRegisterRange::GetBits()
237✔
215
    {
216
        if (!IsSingleBitType(Type()))
237✔
217
            throw std::runtime_error("GetBits() for non-bit register");
×
218
        if (!Bits)
237✔
219
            Bits = new uint8_t[Count];
59✔
220
        return Bits;
237✔
221
    }
222

223
    uint32_t TModbusRegisterRange::GetStart() const
1,061✔
224
    {
225
        return Start;
1,061✔
226
    }
227

228
    size_t TModbusRegisterRange::GetCount() const
864✔
229
    {
230
        return Count;
864✔
231
    }
232

233
    bool TModbusRegisterRange::HasHoles() const
19✔
234
    {
235
        return HasHolesFlg;
19✔
236
    }
237

238
    const std::string& TModbusRegisterRange::TypeName() const
482✔
239
    {
240
        return RegisterList().front()->GetConfig()->TypeName;
482✔
241
    }
242

243
    int TModbusRegisterRange::Type() const
1,985✔
244
    {
245
        return RegisterList().front()->GetConfig()->Type;
1,985✔
246
    }
247

248
    PSerialDevice TModbusRegisterRange::Device() const
2,317✔
249
    {
250
        return RegisterList().front()->Device();
2,317✔
251
    }
252

253
    bool TModbusRegisterRange::AddingRegisterIncreasesSize(bool isSingleBit, size_t extend) const
183✔
254
    {
255
        if (!isSingleBit) {
183✔
256
            return (extend != 0);
51✔
257
        }
258
        if (Count % 16 == 0) {
132✔
259
            return true;
1✔
260
        }
261
        auto maxRegsCount = ((Count / 16) + 1) * 16;
131✔
262
        return (Count + extend) > maxRegsCount;
131✔
263
    }
264

265
    void TModbusRegisterRange::ReadRange(IModbusTraits& traits,
450✔
266
                                         TPort& port,
267
                                         uint8_t slaveId,
268
                                         int shift,
269
                                         Modbus::TRegisterCache& cache)
270
    {
271
        try {
272
            const auto& deviceConfig = *(Device()->DeviceConfig());
450✔
273
            if (GetCount() < deviceConfig.MinReadRegisters) {
450✔
274
                Count = deviceConfig.MinReadRegisters;
1✔
275
            }
276
            auto function = GetFunctionImpl(Type(), OperationType::OP_READ, TypeName(), IsPacking(*this));
450✔
277
            auto pdu = Modbus::MakePDU(function, GetStart() + shift, Count, {});
900✔
278
            port.SleepSinceLastInteraction(Device()->DeviceConfig()->RequestDelay);
450✔
279
            auto res = traits.Transaction(port,
280
                                          slaveId,
281
                                          pdu,
282
                                          Modbus::CalcResponsePDUSize(function, Count),
283
                                          Device()->GetResponseTimeout(port),
458✔
284
                                          Device()->GetFrameTimeout(port));
1,354✔
285
            ResponseTime = res.ResponseTime;
446✔
286
            ParseReadResponse(res.Pdu, function, *this, cache);
446✔
287
        } catch (const Modbus::TModbusExceptionError& err) {
59✔
288
            RethrowSerialDeviceException(err);
26✔
289
        } catch (const Modbus::TMalformedResponseError& err) {
4✔
290
            try {
291
                port.SkipNoise();
2✔
292
            } catch (const std::exception& e) {
×
293
                LOG(Warn) << "SkipNoise failed: " << e.what();
×
294
            }
295
            throw TSerialDeviceTransientErrorException(err.what());
2✔
296
        } catch (const Modbus::TErrorBase& err) {
6✔
297
            throw TSerialDeviceTransientErrorException(err.what());
3✔
298
        }
299
    }
417✔
300

301
    std::chrono::microseconds TModbusRegisterRange::GetResponseTime() const
448✔
302
    {
303
        return ResponseTime;
448✔
304
    }
305

306
    // returns count of modbus registers needed to represent TModbusRegisterRange
307
    uint16_t TModbusRegisterRange::GetQuantity() const
×
308
    {
309
        auto type = Type();
×
310

311
        if (!IsSingleBitType(type) && type != REG_HOLDING && type != REG_HOLDING_SINGLE && type != REG_HOLDING_MULTI &&
×
312
            type != REG_INPUT)
313
        {
314
            throw TSerialDeviceException("invalid register type");
×
315
        }
316

317
        return GetCount();
×
318
    }
319

320
    ostream& operator<<(ostream& s, const TModbusRegisterRange& range)
32✔
321
    {
322
        s << range.GetCount() << " " << range.TypeName() << "(s) @ " << range.GetStart() << " of device "
32✔
323
          << range.Device()->ToString();
32✔
324
        return s;
32✔
325
    }
326

327
    // choose function code for modbus request
328
    EFunction GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many)
535✔
329
    {
330
        switch (registerType) {
535✔
331
            case REG_HOLDING_SINGLE:
438✔
332
            case REG_HOLDING_MULTI:
333
            case REG_HOLDING:
334
                switch (op) {
335
                    case OperationType::OP_READ:
361✔
336
                        return FN_READ_HOLDING;
361✔
337
                    case OperationType::OP_WRITE:
77✔
338
                        return many ? FN_WRITE_MULTIPLE_REGISTERS : FN_WRITE_SINGLE_REGISTER;
77✔
339
                    default:
×
340
                        break;
×
341
                }
342
                break;
×
343
            case REG_INPUT:
20✔
344
                switch (op) {
345
                    case OperationType::OP_READ:
20✔
346
                        return FN_READ_INPUT;
20✔
347
                    default:
×
348
                        break;
×
349
                }
350
                break;
×
351
            case REG_COIL:
64✔
352
                switch (op) {
353
                    case OperationType::OP_READ:
56✔
354
                        return FN_READ_COILS;
56✔
355
                    case OperationType::OP_WRITE:
8✔
356
                        return many ? FN_WRITE_MULTIPLE_COILS : FN_WRITE_SINGLE_COIL;
8✔
357
                    default:
×
358
                        break;
×
359
                }
360
                break;
×
361
            case REG_DISCRETE:
13✔
362
                switch (op) {
363
                    case OperationType::OP_READ:
13✔
364
                        return FN_READ_DISCRETE;
13✔
365
                    default:
×
366
                        break;
×
367
                }
368
                break;
×
369
            default:
×
370
                break;
×
371
        }
372

373
        switch (op) {
×
374
            case OperationType::OP_READ:
×
375
                throw TSerialDeviceException("can't read from " + typeName);
×
376
            case OperationType::OP_WRITE:
×
377
                throw TSerialDeviceException("can't write to " + typeName);
×
378
            default:
×
379
                throw TSerialDeviceException("invalid operation type");
×
380
        }
381
    }
382

383
    // returns number of requests needed to write register
384
    size_t InferWriteRequestsCount(const TRegisterConfig& reg)
56✔
385
    {
386
        return IsPacking(reg) ? 1 : GetModbusDataWidthIn16BitWords(reg);
56✔
387
    }
388

389
    // returns number of bytes needed to hold response
390
    size_t InferReadResponsePDUSize(int type, size_t registerCount)
679✔
391
    {
392
        if (IsSingleBitType(type)) {
679✔
393
            return 2 + std::ceil(static_cast<float>(registerCount) / 8); // coil values are packed into bytes as bitset
202✔
394
        } else {
395
            return 2 + registerCount * 2; // count is for uint16_t, we need byte count
477✔
396
        }
397
    }
398

399
    // Composes array of data words filled with string chars according to string register format.
400
    // Word order corresponds to the order of string chars.
401
    // Byte order for "String8" format also corresponds to the order of string chars.
402
    // For "String" format every char placed to least significant byte of every word.
403
    void ComposeStringWriteRequestWords(std::vector<uint16_t>& words,
6✔
404
                                        const TRegisterConfig& reg,
405
                                        const std::string& str)
406
    {
407
        auto size = reg.Format == RegisterFormat::String8 ? str.size() / 2 + str.size() % 2 : str.size();
6✔
408
        auto width = std::min(GetModbusDataWidthIn16BitWords(reg), static_cast<uint32_t>(size));
6✔
409
        words.resize(width);
6✔
410
        for (uint32_t i = 0; i < width; ++i) {
36✔
411
            words[i] = reg.Format == RegisterFormat::String8 ? str[i * 2] << 8 | str[i * 2 + 1] : str[i];
30✔
412
        }
413
    }
6✔
414

415
    // Composes array of data words filled with numeric register value.
416
    // Uses holding register data cache to fill data that is not affected by the new register value.
417
    // Word order corresponds to defaul Modbus order (big endian).
418
    // Byte order corresponds to value byte order.
419
    void ComposeNumberWriteRequestWords(std::vector<uint16_t>& words,
20✔
420
                                        const TRegisterConfig& reg,
421
                                        uint64_t value,
422
                                        const Modbus::TRegisterCache& cache,
423
                                        Modbus::TRegisterCache& tmpCache)
424
    {
425
        uint32_t address = 0;
20✔
426
        uint64_t valueToWrite = 0;
20✔
427
        auto width = GetModbusDataWidthIn16BitWords(reg);
20✔
428
        auto step = 1;
20✔
429
        auto updateCache = false;
20✔
430

431
        if (reg.AccessType != TRegisterConfig::EAccessType::WRITE_ONLY) {
20✔
432
            address = GetUint32RegisterAddress(reg.GetAddress());
20✔
433
            if (reg.WordOrder == EWordOrder::LittleEndian) {
20✔
434
                address += width - 1;
6✔
435
                step = -1;
6✔
436
            }
437
            auto cacheAddress = address;
20✔
438
            for (uint32_t i = 0; i < width; ++i) {
68✔
439
                valueToWrite <<= 16;
48✔
440
                if (cache.count(cacheAddress)) {
48✔
441
                    valueToWrite |= cache.at(cacheAddress);
26✔
442
                }
443
                cacheAddress += step;
48✔
444
            }
445
            // Clear place for data to be written
446
            valueToWrite &= ~(GetLSBMask(reg.GetDataWidth()) << reg.GetDataOffset());
20✔
447
            updateCache = true;
20✔
448
        }
449

450
        // Place data
451
        value <<= reg.GetDataOffset();
20✔
452
        valueToWrite |= value;
20✔
453

454
        words.resize(width);
20✔
455
        for (uint32_t i = 0; i < width; ++i) {
68✔
456
            words[i] = (valueToWrite >> (width - 1) * 16) & 0xFFFF;
48✔
457
            valueToWrite <<= 16;
48✔
458
            if (updateCache) {
48✔
459
                tmpCache[address] = words[i];
48✔
460
                address += step;
48✔
461
            }
462
        }
463
    }
20✔
464

465
    // Composes writing data buffer containing multiple words register data.
466
    // Word order and byte order corresponds to register settings.
467
    size_t ComposeRawMultipleWriteRequestData(std::vector<uint8_t>& data,
26✔
468
                                              const TRegisterConfig& reg,
469
                                              const TRegisterValue& value,
470
                                              const Modbus::TRegisterCache& cache,
471
                                              Modbus::TRegisterCache& tmpCache)
472
    {
473
        std::vector<uint16_t> words;
26✔
474
        if (reg.IsString()) {
26✔
475
            ComposeStringWriteRequestWords(words, reg, value.Get<std::string>());
6✔
476
        } else {
477
            ComposeNumberWriteRequestWords(words, reg, value.Get<uint64_t>(), cache, tmpCache);
20✔
478
        }
479
        if (reg.WordOrder == EWordOrder::LittleEndian) {
26✔
480
            std::reverse(words.begin(), words.end());
8✔
481
        }
482
        auto offset = data.size();
26✔
483
        auto width = words.size();
26✔
484
        data.resize(offset + width * 2);
26✔
485
        for (size_t i = 0; i < width; ++i) {
104✔
486
            WriteAs2Bytes(data.data() + offset + i * 2, words[i], reg.ByteOrder);
78✔
487
        }
488
        return width;
52✔
489
    }
490

491
    // Composes writing data buffer containtig single word filled with "partial" register data.
492
    // Uses holding register data cache to fill data that is not affected by the new register value.
493
    // Byte order corresponds to register settings.
494
    void ComposeRawSingleWriteRequestData(std::vector<uint8_t>& data,
62✔
495
                                          const TRegisterConfig& reg,
496
                                          uint16_t value,
497
                                          uint8_t wordIndex,
498
                                          const Modbus::TRegisterCache& cache,
499
                                          Modbus::TRegisterCache& tmpCache)
500
    {
501
        uint16_t cachedValue = 0;
62✔
502
        auto address = 0;
62✔
503
        auto bitWidth = reg.GetDataWidth();
62✔
504
        auto updateCache = false;
62✔
505

506
        // use cache only for "partial" registers
507
        if (bitWidth < 16) {
62✔
508
            address = GetUint32RegisterAddress(reg.GetAddress()) + wordIndex;
9✔
509
            if (cache.count(address)) {
9✔
510
                cachedValue = cache.at(address);
8✔
511
            }
512
            updateCache = true;
9✔
513
        }
514

515
        auto bitOffset = std::max(static_cast<int32_t>(reg.GetDataOffset()) - wordIndex * 16, 0);
62✔
516
        auto bitCount = std::min(static_cast<uint32_t>(16 - bitOffset), bitWidth);
62✔
517
        auto mask = GetLSBMask(bitCount) << bitOffset;
62✔
518
        auto valueToWrite = (~mask & cachedValue) | (mask & (value << bitOffset));
62✔
519

520
        data.resize(2);
62✔
521
        WriteAs2Bytes(data.data(), valueToWrite, reg.ByteOrder);
62✔
522

523
        if (updateCache) {
62✔
524
            tmpCache[address] = valueToWrite;
9✔
525
        }
526
    }
62✔
527

528
    void ParseSingleBitReadResponse(const std::vector<uint8_t>& data, TModbusRegisterRange& range)
59✔
529
    {
530
        auto start = data.begin();
59✔
531
        auto destination = range.GetBits();
59✔
532
        auto coil_count = range.GetCount();
59✔
533
        while (start != data.end()) {
151✔
534
            std::bitset<8> coils(*start++);
92✔
535
            auto coils_in_byte = std::min(coil_count, size_t(8));
92✔
536
            for (size_t i = 0; i < coils_in_byte; ++i) {
476✔
537
                destination[i] = coils[i];
384✔
538
            }
539

540
            coil_count -= coils_in_byte;
92✔
541
            destination += coils_in_byte;
92✔
542
        }
543
        for (auto reg: range.RegisterList()) {
237✔
544
            auto addr = GetUint32RegisterAddress(reg->GetConfig()->GetAddress());
178✔
545
            reg->SetValue(TRegisterValue{range.GetBits()[addr - range.GetStart()]});
178✔
546
        }
547
        return;
118✔
548
    }
549

550
    // Extracts numeric register data from data words array, ordered according
551
    // to the register word order and byte order settings.
552
    uint64_t GetNumberRegisterValue(const std::vector<uint16_t>& words, const TRegisterConfig& reg)
381✔
553
    {
554
        uint64_t value = 0;
381✔
555
        for (size_t i = 0; i < words.size(); ++i) {
916✔
556
            value <<= 16;
535✔
557
            value |= words[i];
535✔
558
        }
559
        value >>= reg.GetDataOffset();
381✔
560
        value &= GetLSBMask(reg.GetDataWidth());
381✔
561
        return value;
381✔
562
    }
563

564
    // Extracts string register data from data words array, ordered according
565
    // to the register word order and byte order settings.
566
    std::string GetStringRegisterValue(const std::vector<uint16_t>& words, const TRegisterConfig& reg)
20✔
567
    {
568
        std::string str;
20✔
569
        size_t offset = reg.Format == RegisterFormat::String8 ? 1 : 0;
20✔
570
        size_t shift = offset;
20✔
571
        auto it = words.begin();
20✔
572
        while (it != words.end()) {
166✔
573
            auto ch = static_cast<char>(*it >> shift * 8);
164✔
574
            if (ch == '\0' || ch == '\xFF') {
164✔
575
                break;
576
            }
577
            str.push_back(ch);
146✔
578
            if (shift > 0) {
146✔
579
                --shift;
41✔
580
                continue;
41✔
581
            }
582
            shift = offset;
105✔
583
            ++it;
105✔
584
        }
585
        return str;
40✔
586
    }
587

588
    // Orders read data buffer according to register word order and byte order settings.
589
    // Fills register data cache for holding registers.
590
    // Returns TRegister value.
591
    TRegisterValue GetRegisterValue(const std::vector<uint8_t>& data,
401✔
592
                                    const TRegisterConfig& reg,
593
                                    Modbus::TRegisterCache& cache,
594
                                    uint32_t index = 0)
595
    {
596
        auto address = GetUint32RegisterAddress(reg.GetAddress());
401✔
597
        auto width = GetModbusDataWidthIn16BitWords(reg);
401✔
598
        auto start = data.data() + index * 2;
401✔
599
        std::vector<uint16_t> words(width);
401✔
600
        for (uint32_t i = 0; i < width; i++) {
1,086✔
601
            if (reg.ByteOrder == EByteOrder::LittleEndian) {
685✔
602
                words[i] = *(start + 1) << 8 | *start;
90✔
603
            } else {
604
                words[i] = *start << 8 | *(start + 1);
595✔
605
            }
606
            if (IsHoldingType(reg.Type)) {
685✔
607
                cache[address] = words[i];
661✔
608
                ++address;
661✔
609
            }
610
            start += 2;
685✔
611
        }
612
        if (reg.WordOrder == EWordOrder::LittleEndian) {
401✔
613
            std::reverse(words.begin(), words.end());
30✔
614
        }
615
        return reg.IsString() ? TRegisterValue{GetStringRegisterValue(words, reg)}
822✔
616
                              : TRegisterValue{GetNumberRegisterValue(words, reg)};
822✔
617
    }
618

619
    // Parses modbus response and stores result.
620
    void ParseReadResponse(const std::vector<uint8_t>& pdu,
446✔
621
                           Modbus::EFunction function,
622
                           TModbusRegisterRange& range,
623
                           Modbus::TRegisterCache& cache)
624
    {
625
        auto data = Modbus::ExtractResponseData(function, pdu);
446✔
626
        range.Device()->SetTransferResult(true);
417✔
627
        if (IsSingleBitType(range.Type())) {
417✔
628
            ParseSingleBitReadResponse(data, range);
59✔
629
            return;
59✔
630
        }
631
        for (auto reg: range.RegisterList()) {
759✔
632
            auto config = reg->GetConfig();
401✔
633
            auto index = GetUint32RegisterAddress(config->GetAddress()) - range.GetStart();
401✔
634
            reg->SetValue(GetRegisterValue(data, *config, cache, index));
401✔
635
        }
636
    }
637

638
    PRegisterRange CreateRegisterRange(std::chrono::microseconds averageResponseTime)
494✔
639
    {
640
        return std::make_shared<TModbusRegisterRange>(averageResponseTime);
494✔
641
    }
642

643
    void WriteTransaction(IModbusTraits& traits,
93✔
644
                          TPort& port,
645
                          uint8_t slaveId,
646
                          Modbus::EFunction fn,
647
                          size_t responsePduSize,
648
                          const std::vector<uint8_t>& pdu,
649
                          std::chrono::microseconds requestDelay,
650
                          std::chrono::milliseconds responseTimeout,
651
                          std::chrono::milliseconds frameTimeout)
652
    {
653
        try {
654
            port.SleepSinceLastInteraction(requestDelay);
93✔
655
            auto res = traits.Transaction(port, slaveId, pdu, responsePduSize, responseTimeout, frameTimeout);
101✔
656
            Modbus::ExtractResponseData(fn, res.Pdu);
83✔
657
        } catch (const Modbus::TModbusExceptionError& err) {
24✔
658
            RethrowSerialDeviceException(err);
6✔
659
        } catch (const Modbus::TMalformedResponseError& err) {
×
660
            try {
661
                port.SkipNoise();
×
662
            } catch (const std::exception& e) {
×
663
                LOG(Warn) << "SkipNoise failed: " << e.what();
×
664
            }
665
            throw TSerialDeviceTransientErrorException(err.what());
×
666
        } catch (const Modbus::TErrorBase& err) {
6✔
667
            throw TSerialDeviceTransientErrorException(err.what());
3✔
668
        }
669
    }
75✔
670

671
    void WriteRegister(IModbusTraits& traits,
85✔
672
                       TPort& port,
673
                       uint8_t slaveId,
674
                       const TRegisterConfig& reg,
675
                       const TRegisterValue& value,
676
                       Modbus::TRegisterCache& cache,
677
                       std::chrono::microseconds requestDelay,
678
                       std::chrono::milliseconds responseTimeout,
679
                       std::chrono::milliseconds frameTimeout,
680
                       int shift)
681
    {
682
        Modbus::TRegisterCache tmpCache;
170✔
683

684
        LOG(Debug) << port.GetDescription() << " modbus:" << std::to_string(slaveId) << " write "
170✔
685
                   << GetModbusDataWidthIn16BitWords(reg) << " " << reg.TypeName << "(s) @ " << reg.GetWriteAddress();
85✔
686

687
        EFunction fn;
688
        try {
689
            fn = GetFunctionImpl(reg.Type, OperationType::OP_WRITE, reg.TypeName, IsPacking(reg));
85✔
NEW
690
        } catch (const TSerialDeviceException& e) {
×
NEW
691
            throw TSerialDeviceException("failed to write register <" + reg.ToString() + ">: " + e.what());
×
692
        }
693

694
        std::vector<uint8_t> data;
170✔
695
        auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()) + shift;
85✔
696
        if (IsPacking(reg)) {
85✔
697
            size_t regCount = ComposeRawMultipleWriteRequestData(data, reg, value, cache, tmpCache);
21✔
698
            WriteTransaction(traits,
42✔
699
                             port,
700
                             slaveId,
701
                             fn,
702
                             Modbus::CalcResponsePDUSize(fn, regCount),
703
                             Modbus::MakePDU(fn, addr, regCount, data),
42✔
704
                             requestDelay,
705
                             responseTimeout,
706
                             frameTimeout);
707
        } else if (reg.Type == REG_COIL) {
64✔
708
            const std::vector<uint8_t> on{0xFF, 0x00};
16✔
709
            const std::vector<uint8_t> off{0x00, 0x00};
12✔
710
            WriteTransaction(traits,
16✔
711
                             port,
712
                             slaveId,
713
                             fn,
714
                             Modbus::CalcResponsePDUSize(fn, 1),
715
                             Modbus::MakePDU(fn, addr, 1, value.Get<uint64_t>() ? on : off),
20✔
716
                             requestDelay,
717
                             responseTimeout,
718
                             frameTimeout);
719
        } else {
720
            // Note: word order feature currently is not supported by HOLDING_SINGLE registers.
721
            auto requestsCount = InferWriteRequestsCount(reg);
56✔
722
            auto val = value.Get<uint64_t>();
56✔
723
            auto responsePduSize = Modbus::CalcResponsePDUSize(fn, 1);
56✔
724
            for (size_t i = 0; i < requestsCount; ++i) {
104✔
725
                auto wordIndex = requestsCount - i - 1;
62✔
726
                ComposeRawSingleWriteRequestData(data, reg, static_cast<uint16_t>(val), wordIndex, cache, tmpCache);
62✔
727
                WriteTransaction(traits,
62✔
728
                                 port,
729
                                 slaveId,
730
                                 fn,
731
                                 responsePduSize,
732
                                 Modbus::MakePDU(fn, addr + wordIndex, 1, data),
138✔
733
                                 requestDelay,
734
                                 responseTimeout,
735
                                 frameTimeout);
736
                val >>= 16;
48✔
737
            }
738
        }
739
        for (const auto& item: tmpCache) {
115✔
740
            cache.insert_or_assign(item.first, item.second);
48✔
741
        }
742
    }
67✔
743

744
    void ProcessRangeException(TModbusRegisterRange& range, const char* msg)
33✔
745
    {
746
        for (auto& reg: range.RegisterList()) {
85✔
747
            reg->SetError(TRegister::TError::ReadError);
52✔
748
        }
749

750
        auto& logger = (range.Device()->GetConnectionState() == TDeviceConnectionState::DISCONNECTED) ? Debug : Warn;
33✔
751
        LOG(logger) << "failed to read " << range << ": " << msg;
33✔
752
        range.Device()->SetTransferResult(false);
33✔
753
    }
33✔
754

755
    void ReadRegisterRange(IModbusTraits& traits,
469✔
756
                           TPort& port,
757
                           uint8_t slaveId,
758
                           TModbusRegisterRange& range,
759
                           Modbus::TRegisterCache& cache,
760
                           bool breakOnError,
761
                           int shift)
762
    {
763
        if (range.RegisterList().empty()) {
469✔
764
            return;
19✔
765
        }
766
        try {
767
            range.ReadRange(traits, port, slaveId, shift, cache);
450✔
768
        } catch (const TSerialDevicePermanentRegisterException& e) {
52✔
769
            if (range.HasHoles()) {
19✔
770
                range.Device()->SetSupportsHoles(false);
2✔
771
            } else {
772
                for (auto& reg: range.RegisterList()) {
35✔
773
                    reg->SetAvailable(TRegisterAvailability::UNAVAILABLE);
18✔
774
                    LOG(Warn) << reg->ToString() << " is now marked as unavailable: " << e.what();
18✔
775
                }
776
            }
777
            ProcessRangeException(range, e.what());
19✔
778
            if (breakOnError) {
19✔
NEW
779
                throw;
×
780
            }
781
        } catch (const TSerialDeviceException& e) {
28✔
782
            ProcessRangeException(range, e.what());
14✔
783
            if (breakOnError) {
14✔
NEW
784
                throw;
×
785
            }
786
        }
787
    }
788

789
    bool FillSetupRegistersCache(Modbus::IModbusTraits& traits,
56✔
790
                                 TPort& port,
791
                                 uint8_t slaveId,
792
                                 const TDeviceSetupItems& setupItems,
793
                                 Modbus::TRegisterCache& cache,
794
                                 int shift)
795
    {
796
        auto it = setupItems.begin();
56✔
797
        while (it != setupItems.end()) {
77✔
798
            TModbusRegisterRange range(std::chrono::microseconds::max());
21✔
799
            while (it != setupItems.end()) {
56✔
800
                auto item = *it;
36✔
801
                if (!item->RegisterConfig->IsPartial()) {
36✔
802
                    ++it;
30✔
803
                    continue;
30✔
804
                }
805
                auto address = GetUint32RegisterAddress(item->RegisterConfig->GetAddress());
6✔
806
                auto cached = true;
6✔
807
                for (uint32_t i = 0; i < item->RegisterConfig->Get16BitWidth(); ++i) {
6✔
808
                    if (cache.count(address) == 0) {
6✔
809
                        cached = false;
6✔
810
                        break;
6✔
811
                    }
812
                    ++address;
×
813
                }
814
                if (cached) {
6✔
815
                    ++it;
×
816
                    continue;
×
817
                }
818
                auto reg = std::make_shared<TRegister>(item->Device, item->RegisterConfig);
6✔
819
                reg->SetAvailable(TRegisterAvailability::AVAILABLE);
6✔
820
                if (!range.Add(port, reg, std::chrono::milliseconds::max())) {
6✔
821
                    break;
1✔
822
                }
823
                ++it;
5✔
824
            }
825
            ReadRegisterRange(traits, port, slaveId, range, cache, shift);
21✔
826
            for (auto reg: range.RegisterList()) {
26✔
827
                if (reg->GetErrorState().count()) {
5✔
828
                    return false;
×
829
                }
830
            }
831
        }
832
        return true;
56✔
833
    }
834

835
    TDeviceSetupItems::iterator WriteMultipleSetupRegisters(Modbus::IModbusTraits& traits,
2✔
836
                                                            TPort& port,
837
                                                            uint8_t slaveId,
838
                                                            TDeviceSetupItems::iterator startIt,
839
                                                            TDeviceSetupItems::iterator endIt,
840
                                                            size_t maxRegs,
841
                                                            Modbus::TRegisterCache& cache,
842
                                                            std::chrono::microseconds requestDelay,
843
                                                            std::chrono::milliseconds responseTimeout,
844
                                                            std::chrono::milliseconds frameTimeout,
845
                                                            bool breakOnError,
846
                                                            int shift)
847
    {
848
        Modbus::TRegisterCache tmpCache;
4✔
849
        std::vector<uint8_t> data;
4✔
850
        PDeviceSetupItem first = *startIt;
4✔
851
        PDeviceSetupItem last = nullptr;
4✔
852
        auto start = GetUint32RegisterAddress(first->RegisterConfig->GetWriteAddress());
2✔
853
        auto count = 0;
2✔
854
        while (startIt != endIt) {
7✔
855
            auto item = *startIt;
6✔
856
            if (item->RegisterConfig->IsPartial()) {
6✔
857
                return startIt;
×
858
            }
859
            auto address = GetUint32RegisterAddress(item->RegisterConfig->GetWriteAddress());
6✔
860
            if (last) {
6✔
861
                auto lastAddress = GetUint32RegisterAddress(last->RegisterConfig->GetWriteAddress());
4✔
862
                auto lastWidth = GetModbusDataWidthIn16BitWords(*last->RegisterConfig);
4✔
863
                if (item->RegisterConfig->Type != last->RegisterConfig->Type || address != lastAddress + lastWidth) {
4✔
864
                    break;
1✔
865
                }
866
            }
867

868
            auto width = GetModbusDataWidthIn16BitWords(*item->RegisterConfig);
5✔
869
            if (count && count + width > maxRegs) {
5✔
870
                break;
×
871
            }
872

873
            ComposeRawMultipleWriteRequestData(data, *item->RegisterConfig, item->RawValue, cache, tmpCache);
5✔
874
            LOG(Info) << item->ToString();
5✔
875

876
            count += width;
5✔
877
            last = item;
5✔
878
            ++startIt;
5✔
879
        }
880
        try {
881
            auto function = count > 1 ? FN_WRITE_MULTIPLE_REGISTERS : FN_WRITE_SINGLE_REGISTER;
2✔
882
            WriteTransaction(traits,
4✔
883
                             port,
884
                             slaveId,
885
                             function,
886
                             Modbus::CalcResponsePDUSize(function, count),
887
                             Modbus::MakePDU(function, start + shift, count, data),
4✔
888
                             requestDelay,
889
                             responseTimeout,
890
                             frameTimeout);
891
            for (const auto& item: tmpCache) {
11✔
892
                cache.insert_or_assign(item.first, item.second);
9✔
893
            }
894
        } catch (const TSerialDevicePermanentRegisterException& e) {
×
NEW
895
            std::string error = "Failed to write " + std::to_string(count) + " registers starting from <" +
×
NEW
896
                                first->RegisterConfig->ToString() + ">: " + e.what();
×
NEW
897
            LOG(Warn) << error;
×
NEW
898
            if (breakOnError) {
×
NEW
899
                throw TSerialDeviceException(error);
×
900
            }
901
        }
902
        return startIt;
2✔
903
    }
904

905
    void WriteSetupRegisters(Modbus::IModbusTraits& traits,
56✔
906
                             TPort& port,
907
                             uint8_t slaveId,
908
                             const TDeviceSetupItems& setupItems,
909
                             Modbus::TRegisterCache& cache,
910
                             std::chrono::microseconds requestDelay,
911
                             std::chrono::milliseconds responseTimeout,
912
                             std::chrono::milliseconds frameTimeout,
913
                             bool breakOnError,
914
                             int shift)
915
    {
916
        if (!FillSetupRegistersCache(traits, port, slaveId, setupItems, cache, shift)) {
56✔
NEW
917
            throw TSerialDeviceException("unable to write setup items because unable to read data needed to set values "
×
NEW
918
                                         "of \"partial\" setup registers");
×
919
        }
920
        auto it = setupItems.begin();
56✔
921
        while (it != setupItems.end()) {
78✔
922
            auto item = *it;
64✔
923
            auto config = item->Device->DeviceConfig();
64✔
924
            size_t maxRegs = MAX_WRITE_REGISTERS;
32✔
925
            if (config->MaxWriteRegisters > 0 && config->MaxWriteRegisters < maxRegs) {
32✔
926
                maxRegs = config->MaxWriteRegisters;
30✔
927
            }
928
            if (maxRegs > 1 && item->RegisterConfig->Type == REG_HOLDING && !item->RegisterConfig->IsPartial()) {
32✔
929
                it = WriteMultipleSetupRegisters(traits,
930
                                                 port,
931
                                                 slaveId,
932
                                                 it,
933
                                                 setupItems.end(),
934
                                                 maxRegs,
935
                                                 cache,
936
                                                 requestDelay,
937
                                                 responseTimeout,
938
                                                 frameTimeout,
939
                                                 breakOnError,
940
                                                 shift);
2✔
941
            } else {
942
                try {
943
                    WriteRegister(traits,
30✔
944
                                  port,
945
                                  slaveId,
946
                                  *item->RegisterConfig,
30✔
947
                                  item->RawValue,
30✔
948
                                  cache,
949
                                  requestDelay,
950
                                  responseTimeout,
951
                                  frameTimeout,
952
                                  shift);
953
                    LOG(Info) << item->ToString();
19✔
954
                } catch (const TSerialDevicePermanentRegisterException& e) {
12✔
955
                    std::string error =
956
                        "Failed to write register <" + item->RegisterConfig->ToString() + ">: " + e.what();
3✔
957
                    LOG(Warn) << error;
1✔
958
                    if (breakOnError) {
1✔
NEW
959
                        throw TSerialDeviceException(error);
×
960
                    }
961
                }
962
                ++it;
20✔
963
            }
964
        }
965
        if (!setupItems.empty()) {
46✔
966
            auto item = *setupItems.begin();
20✔
967
            if (item->Device) {
10✔
968
                item->Device->SetTransferResult(true);
10✔
969
            }
970
        }
971
    }
46✔
972

973
    bool EnableWbContinuousRead(PSerialDevice device,
9✔
974
                                IModbusTraits& traits,
975
                                TPort& port,
976
                                uint8_t slaveId,
977
                                TRegisterCache& cache)
978
    {
979
        auto config = WbRegisters::GetRegisterConfig("continuous_read");
27✔
980
        try {
981
            Modbus::WriteRegister(traits,
9✔
982
                                  port,
983
                                  slaveId,
984
                                  *config,
9✔
985
                                  TRegisterValue(1),
11✔
986
                                  cache,
987
                                  device->DeviceConfig()->RequestDelay,
19✔
988
                                  device->GetResponseTimeout(port),
9✔
989
                                  device->GetFrameTimeout(port));
9✔
990
            LOG(Info) << "Continuous read enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
8✔
991
            if (device->DeviceConfig()->MaxRegHole < MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS) {
8✔
992
                device->DeviceConfig()->MaxRegHole = MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS;
7✔
993
            }
994
            if (device->DeviceConfig()->MaxBitHole < MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS) {
8✔
995
                device->DeviceConfig()->MaxBitHole = MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS;
7✔
996
            }
997
            return true;
8✔
998
        } catch (const TSerialDevicePermanentRegisterException& e) {
1✔
999
            // A firmware doesn't support continuous read
1000
            LOG(Warn) << "Continuous read is not enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
1✔
1001
        }
1002
        return false;
1✔
1003
    }
1004

1005
    TRegisterValue ReadRegister(IModbusTraits& traits,
×
1006
                                TPort& port,
1007
                                uint8_t slaveId,
1008
                                const TRegisterConfig& reg,
1009
                                std::chrono::microseconds requestDelay,
1010
                                std::chrono::milliseconds responseTimeout,
1011
                                std::chrono::milliseconds frameTimeout)
1012
    {
1013
        size_t modbusRegisterCount = GetModbusDataWidthIn16BitWords(reg);
×
1014
        auto addr = GetUint32RegisterAddress(reg.GetAddress());
×
1015
        auto function = GetFunctionImpl(reg.Type, OperationType::OP_READ, reg.TypeName, false);
×
1016
        auto requestPdu = Modbus::MakePDU(function, addr, modbusRegisterCount, {});
×
1017

1018
        port.SleepSinceLastInteraction(requestDelay);
×
1019
        auto res = traits.Transaction(port,
1020
                                      slaveId,
1021
                                      requestPdu,
1022
                                      Modbus::CalcResponsePDUSize(function, modbusRegisterCount),
1023
                                      responseTimeout,
1024
                                      frameTimeout);
×
1025

1026
        auto data = Modbus::ExtractResponseData(function, res.Pdu);
×
1027
        if (IsSingleBitType(reg.Type)) {
×
1028
            return TRegisterValue{data[0]};
×
1029
        }
1030

1031
        Modbus::TRegisterCache cache;
×
1032
        return GetRegisterValue(data, reg, cache);
×
1033
    }
1034

1035
} // modbus protocol utilities
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