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

wirenboard / wb-mqtt-serial / 2

29 Dec 2025 12:28PM UTC coverage: 76.817% (+4.0%) from 72.836%
2

Pull #1045

github

54aa0c
pgasheev
up changelog
Pull Request #1045: Fix firmware version in WB-M1W2 template

6873 of 9161 branches covered (75.02%)

12966 of 16879 relevant lines covered (76.82%)

1651.61 hits per line

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

87.71
/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)
3,660✔
27
    {
28
        return reg.Get16BitWidth();
3,660✔
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)
282✔
33
    {
34
        if (byteOrder == EByteOrder::LittleEndian) {
282✔
35
            dst[0] = static_cast<uint8_t>(val);
56✔
36
            dst[1] = static_cast<uint8_t>(val >> 8);
56✔
37
        } else {
38
            dst[0] = static_cast<uint8_t>(val >> 8);
226✔
39
            dst[1] = static_cast<uint8_t>(val);
226✔
40
        }
41
    }
282✔
42

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

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

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

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

67
    void RethrowSerialDeviceException(const Modbus::TModbusExceptionError& err)
64✔
68
    {
69
        if (err.GetExceptionCode() == Modbus::ILLEGAL_FUNCTION ||
64✔
70
            err.GetExceptionCode() == Modbus::ILLEGAL_DATA_ADDRESS ||
110✔
71
            err.GetExceptionCode() == Modbus::ILLEGAL_DATA_VALUE)
46✔
72
        {
73
            throw TSerialDevicePermanentRegisterException(err.what());
48✔
74
        }
75
        throw TSerialDeviceTransientErrorException(err.what());
16✔
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)
1,114✔
90
        : AverageResponseTime(averageResponseTime),
91
          ResponseTime(averageResponseTime)
1,114✔
92
    {}
1,114✔
93

94
    TModbusRegisterRange::~TModbusRegisterRange()
2,228✔
95
    {
96
        if (Bits)
2,228✔
97
            delete[] Bits;
236✔
98
    }
99

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

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

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

119
        size_t extend;
120
        if (RegisterList().empty()) {
2,074✔
121
            extend = widthInWords;
1,082✔
122
        } else {
123
            if (HasOtherDeviceAndType(reg)) {
992✔
124
                return false;
148✔
125
            }
126

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

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

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

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

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

183
        auto newPduSize = InferReadResponsePDUSize(reg->GetConfig()->Type, Count + extend);
1,448✔
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);
1,448✔
187
        auto duration = sendTime + AverageResponseTime + deviceConfig->RequestDelay;
1,448✔
188
        if (forceFrameTimeout) {
1,448✔
189
            duration += deviceConfig->FrameTimeout;
2✔
190
        }
191
        auto newPollTime = std::chrono::ceil<std::chrono::milliseconds>(duration);
1,448✔
192

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

195
            if (Count == 0) {
1,374✔
196
                Start = addr;
1,008✔
197
            }
198

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

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

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

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

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

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

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

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

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

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

301
    std::chrono::microseconds TModbusRegisterRange::GetResponseTime() const
992✔
302
    {
303
        return ResponseTime;
992✔
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)
66✔
321
    {
322
        s << range.GetCount() << " " << range.TypeName() << "(s) @ " << range.GetStart() << " of device "
66✔
323
          << range.Device()->ToString();
66✔
324
        return s;
66✔
325
    }
326

327
    // choose function code for modbus request
328
    EFunction GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many)
1,168✔
329
    {
330
        switch (registerType) {
1,168✔
331
            case REG_HOLDING_SINGLE:
974✔
332
            case REG_HOLDING_MULTI:
333
            case REG_HOLDING:
334
                switch (op) {
335
                    case OperationType::OP_READ:
818✔
336
                        return FN_READ_HOLDING;
818✔
337
                    case OperationType::OP_WRITE:
156✔
338
                        return many ? FN_WRITE_MULTIPLE_REGISTERS : FN_WRITE_SINGLE_REGISTER;
156✔
339
                    default:
×
340
                        break;
×
341
                }
342
                break;
×
343
            case REG_INPUT:
40✔
344
                switch (op) {
345
                    case OperationType::OP_READ:
40✔
346
                        return FN_READ_INPUT;
40✔
347
                    default:
×
348
                        break;
×
349
                }
350
                break;
×
351
            case REG_COIL:
128✔
352
                switch (op) {
353
                    case OperationType::OP_READ:
112✔
354
                        return FN_READ_COILS;
112✔
355
                    case OperationType::OP_WRITE:
16✔
356
                        return many ? FN_WRITE_MULTIPLE_COILS : FN_WRITE_SINGLE_COIL;
16✔
357
                    default:
×
358
                        break;
×
359
                }
360
                break;
×
361
            case REG_DISCRETE:
26✔
362
                switch (op) {
363
                    case OperationType::OP_READ:
26✔
364
                        return FN_READ_DISCRETE;
26✔
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)
114✔
385
    {
386
        return IsPacking(reg) ? 1 : GetModbusDataWidthIn16BitWords(reg);
114✔
387
    }
388

389
    // returns number of bytes needed to hold response
390
    size_t InferReadResponsePDUSize(int type, size_t registerCount)
1,448✔
391
    {
392
        if (IsSingleBitType(type)) {
1,448✔
393
            return 2 + std::ceil(static_cast<float>(registerCount) / 8); // coil values are packed into bytes as bitset
404✔
394
        } else {
395
            return 2 + registerCount * 2; // count is for uint16_t, we need byte count
1,044✔
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,
12✔
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();
12✔
408
        auto width = std::min(GetModbusDataWidthIn16BitWords(reg), static_cast<uint32_t>(size));
12✔
409
        words.resize(width);
12✔
410
        for (uint32_t i = 0; i < width; ++i) {
72✔
411
            words[i] = reg.Format == RegisterFormat::String8 ? str[i * 2] << 8 | str[i * 2 + 1] : str[i];
60✔
412
        }
413
    }
12✔
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,
40✔
420
                                        const TRegisterConfig& reg,
421
                                        uint64_t value,
422
                                        const Modbus::TRegisterCache& cache,
423
                                        Modbus::TRegisterCache& tmpCache)
424
    {
425
        uint32_t address = 0;
40✔
426
        uint64_t valueToWrite = 0;
40✔
427
        auto width = GetModbusDataWidthIn16BitWords(reg);
40✔
428
        auto step = 1;
40✔
429
        auto updateCache = false;
40✔
430

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

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

454
        words.resize(width);
40✔
455
        for (uint32_t i = 0; i < width; ++i) {
136✔
456
            words[i] = (valueToWrite >> (width - 1) * 16) & 0xFFFF;
96✔
457
            valueToWrite <<= 16;
96✔
458
            if (updateCache) {
96✔
459
                tmpCache[address] = words[i];
96✔
460
                address += step;
96✔
461
            }
462
        }
463
    }
40✔
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,
52✔
468
                                              const TRegisterConfig& reg,
469
                                              const TRegisterValue& value,
470
                                              const Modbus::TRegisterCache& cache,
471
                                              Modbus::TRegisterCache& tmpCache)
472
    {
473
        std::vector<uint16_t> words;
52✔
474
        if (reg.IsString()) {
52✔
475
            ComposeStringWriteRequestWords(words, reg, value.Get<std::string>());
12✔
476
        } else {
477
            ComposeNumberWriteRequestWords(words, reg, value.Get<uint64_t>(), cache, tmpCache);
40✔
478
        }
479
        if (reg.WordOrder == EWordOrder::LittleEndian) {
52✔
480
            std::reverse(words.begin(), words.end());
16✔
481
        }
482
        auto offset = data.size();
52✔
483
        auto width = words.size();
52✔
484
        data.resize(offset + width * 2);
52✔
485
        for (size_t i = 0; i < width; ++i) {
208✔
486
            WriteAs2Bytes(data.data() + offset + i * 2, words[i], reg.ByteOrder);
156✔
487
        }
488
        return width;
104✔
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,
126✔
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;
126✔
502
        auto address = 0;
126✔
503
        auto bitWidth = reg.GetDataWidth();
126✔
504
        auto updateCache = false;
126✔
505

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

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

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

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

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

540
            coil_count -= coils_in_byte;
184✔
541
            destination += coils_in_byte;
184✔
542
        }
543
        for (auto reg: range.RegisterList()) {
474✔
544
            auto addr = GetUint32RegisterAddress(reg->GetConfig()->GetAddress());
356✔
545
            reg->SetValue(TRegisterValue{range.GetBits()[addr - range.GetStart()]});
356✔
546
        }
547
        return;
236✔
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)
808✔
553
    {
554
        uint64_t value = 0;
808✔
555
        for (size_t i = 0; i < words.size(); ++i) {
1,924✔
556
            value <<= 16;
1,116✔
557
            value |= words[i];
1,116✔
558
        }
559
        value >>= reg.GetDataOffset();
808✔
560
        value &= GetLSBMask(reg.GetDataWidth());
808✔
561
        return value;
808✔
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)
40✔
567
    {
568
        std::string str;
40✔
569
        size_t offset = reg.Format == RegisterFormat::String8 ? 1 : 0;
40✔
570
        size_t shift = offset;
40✔
571
        auto it = words.begin();
40✔
572
        while (it != words.end()) {
332✔
573
            auto ch = static_cast<char>(*it >> shift * 8);
328✔
574
            if (ch == '\0' || ch == '\xFF') {
328✔
575
                break;
576
            }
577
            str.push_back(ch);
292✔
578
            if (shift > 0) {
292✔
579
                --shift;
82✔
580
                continue;
82✔
581
            }
582
            shift = offset;
210✔
583
            ++it;
210✔
584
        }
585
        return str;
80✔
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,
848✔
592
                                    const TRegisterConfig& reg,
593
                                    Modbus::TRegisterCache& cache,
594
                                    uint32_t index = 0)
595
    {
596
        auto address = GetUint32RegisterAddress(reg.GetAddress());
848✔
597
        auto width = GetModbusDataWidthIn16BitWords(reg);
848✔
598
        auto start = data.data() + index * 2;
848✔
599
        std::vector<uint16_t> words(width);
848✔
600
        for (uint32_t i = 0; i < width; i++) {
2,264✔
601
            if (reg.ByteOrder == EByteOrder::LittleEndian) {
1,416✔
602
                words[i] = *(start + 1) << 8 | *start;
180✔
603
            } else {
604
                words[i] = *start << 8 | *(start + 1);
1,236✔
605
            }
606
            if (IsHoldingType(reg.Type)) {
1,416✔
607
                cache[address] = words[i];
1,368✔
608
                ++address;
1,368✔
609
            }
610
            start += 2;
1,416✔
611
        }
612
        if (reg.WordOrder == EWordOrder::LittleEndian) {
848✔
613
            std::reverse(words.begin(), words.end());
60✔
614
        }
615
        return reg.IsString() ? TRegisterValue{GetStringRegisterValue(words, reg)}
1,736✔
616
                              : TRegisterValue{GetNumberRegisterValue(words, reg)};
1,736✔
617
    }
618

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

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

643
    void WriteTransaction(IModbusTraits& traits,
188✔
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);
188✔
655
            auto res = traits.Transaction(port, slaveId, pdu, responsePduSize, responseTimeout, frameTimeout);
204✔
656
            Modbus::ExtractResponseData(fn, res.Pdu);
174✔
657
        } catch (const Modbus::TModbusExceptionError& err) {
42✔
658
            RethrowSerialDeviceException(err);
12✔
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) {
12✔
667
            throw TSerialDeviceTransientErrorException(err.what());
6✔
668
        }
669
    }
158✔
670

671
    void WriteRegister(IModbusTraits& traits,
172✔
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;
344✔
683

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

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

694
        std::vector<uint8_t> data;
344✔
695
        auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()) + shift;
172✔
696
        if (IsPacking(reg)) {
172✔
697
            size_t regCount = ComposeRawMultipleWriteRequestData(data, reg, value, cache, tmpCache);
42✔
698
            WriteTransaction(traits,
84✔
699
                             port,
700
                             slaveId,
701
                             fn,
702
                             Modbus::CalcResponsePDUSize(fn, regCount),
703
                             Modbus::MakePDU(fn, addr, regCount, data),
84✔
704
                             requestDelay,
705
                             responseTimeout,
706
                             frameTimeout);
707
        } else if (reg.Type == REG_COIL) {
130✔
708
            const std::vector<uint8_t> on{0xFF, 0x00};
32✔
709
            const std::vector<uint8_t> off{0x00, 0x00};
24✔
710
            WriteTransaction(traits,
32✔
711
                             port,
712
                             slaveId,
713
                             fn,
714
                             Modbus::CalcResponsePDUSize(fn, 1),
715
                             Modbus::MakePDU(fn, addr, 1, value.Get<uint64_t>() ? on : off),
40✔
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);
114✔
722
            auto val = value.Get<uint64_t>();
114✔
723
            auto responsePduSize = Modbus::CalcResponsePDUSize(fn, 1);
114✔
724
            for (size_t i = 0; i < requestsCount; ++i) {
218✔
725
                auto wordIndex = requestsCount - i - 1;
126✔
726
                ComposeRawSingleWriteRequestData(data, reg, static_cast<uint16_t>(val), wordIndex, cache, tmpCache);
126✔
727
                WriteTransaction(traits,
126✔
728
                                 port,
729
                                 slaveId,
730
                                 fn,
731
                                 responsePduSize,
732
                                 Modbus::MakePDU(fn, addr + wordIndex, 1, data),
274✔
733
                                 requestDelay,
734
                                 responseTimeout,
735
                                 frameTimeout);
736
                val >>= 16;
104✔
737
            }
738
        }
739
        for (const auto& item: tmpCache) {
238✔
740
            cache.insert_or_assign(item.first, item.second);
96✔
741
        }
742
    }
142✔
743

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

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

755
    void ReadRegisterRange(IModbusTraits& traits,
1,028✔
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()) {
1,028✔
764
            return;
32✔
765
        }
766
        try {
767
            range.ReadRange(traits, port, slaveId, shift, cache);
996✔
768
        } catch (const TSerialDevicePermanentRegisterException& e) {
154✔
769
            if (range.HasHoles()) {
38✔
770
                range.Device()->SetSupportsHoles(false);
4✔
771
            } else {
772
                for (auto& reg: range.RegisterList()) {
70✔
773
                    reg->SetAvailable(TRegisterAvailability::UNAVAILABLE);
36✔
774
                    LOG(Warn) << reg->ToString() << " is now marked as unavailable: " << e.what();
36✔
775
                }
776
            }
777
            ProcessRangeException(range, e.what());
38✔
778
            if (breakOnError) {
38✔
779
                throw;
×
780
            }
781
        } catch (const TSerialDeviceException& e) {
156✔
782
            ProcessRangeException(range, e.what());
78✔
783
            if (breakOnError) {
78✔
784
                throw;
×
785
            }
786
        }
787
    }
788

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

835
    TDeviceSetupItems::iterator WriteMultipleSetupRegisters(Modbus::IModbusTraits& traits,
4✔
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;
8✔
849
        std::vector<uint8_t> data;
8✔
850
        PDeviceSetupItem first = *startIt;
8✔
851
        PDeviceSetupItem last = nullptr;
8✔
852
        auto start = GetUint32RegisterAddress(first->RegisterConfig->GetWriteAddress());
4✔
853
        auto count = 0;
4✔
854
        while (startIt != endIt) {
14✔
855
            auto item = *startIt;
12✔
856
            if (item->RegisterConfig->IsPartial()) {
12✔
857
                return startIt;
×
858
            }
859

860
            auto address = GetUint32RegisterAddress(item->RegisterConfig->GetWriteAddress());
12✔
861
            if (last) {
12✔
862
                auto lastAddress = GetUint32RegisterAddress(last->RegisterConfig->GetWriteAddress());
8✔
863
                auto lastWidth = GetModbusDataWidthIn16BitWords(*last->RegisterConfig);
8✔
864
                if (item->RegisterConfig->Type != last->RegisterConfig->Type || address != lastAddress + lastWidth) {
8✔
865
                    break;
2✔
866
                }
867
            }
868

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

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

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

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

980
    bool EnableWbContinuousRead(PSerialDevice device,
26✔
981
                                IModbusTraits& traits,
982
                                TPort& port,
983
                                uint8_t slaveId,
984
                                TRegisterCache& cache)
985
    {
986
        auto config = WbRegisters::GetRegisterConfig("continuous_read");
78✔
987
        try {
988
            Modbus::WriteRegister(traits,
26✔
989
                                  port,
990
                                  slaveId,
991
                                  *config,
26✔
992
                                  TRegisterValue(1),
30✔
993
                                  cache,
994
                                  device->DeviceConfig()->RequestDelay,
54✔
995
                                  device->GetResponseTimeout(port),
26✔
996
                                  device->GetFrameTimeout(port));
26✔
997
            LOG(Info) << "Continuous read enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
24✔
998
            if (device->DeviceConfig()->MaxRegHole < MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS) {
24✔
999
                device->DeviceConfig()->MaxRegHole = MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS;
18✔
1000
            }
1001
            if (device->DeviceConfig()->MaxBitHole < MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS) {
24✔
1002
                device->DeviceConfig()->MaxBitHole = MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS;
18✔
1003
            }
1004
            return true;
24✔
1005
        } catch (const TSerialDevicePermanentRegisterException& e) {
2✔
1006
            // A firmware doesn't support continuous read
1007
            LOG(Warn) << "Continuous read is not enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
2✔
1008
        }
1009
        return false;
2✔
1010
    }
1011

1012
    TRegisterValue ReadRegister(IModbusTraits& traits,
×
1013
                                TPort& port,
1014
                                uint8_t slaveId,
1015
                                const TRegisterConfig& reg,
1016
                                std::chrono::microseconds requestDelay,
1017
                                std::chrono::milliseconds responseTimeout,
1018
                                std::chrono::milliseconds frameTimeout)
1019
    {
1020
        size_t modbusRegisterCount = GetModbusDataWidthIn16BitWords(reg);
×
1021
        auto addr = GetUint32RegisterAddress(reg.GetAddress());
×
1022
        auto function = GetFunctionImpl(reg.Type, OperationType::OP_READ, reg.TypeName, false);
×
1023
        auto requestPdu = Modbus::MakePDU(function, addr, modbusRegisterCount, {});
×
1024

1025
        port.SleepSinceLastInteraction(requestDelay);
×
1026
        auto res = traits.Transaction(port,
1027
                                      slaveId,
1028
                                      requestPdu,
1029
                                      Modbus::CalcResponsePDUSize(function, modbusRegisterCount),
1030
                                      responseTimeout,
1031
                                      frameTimeout);
×
1032

1033
        auto data = Modbus::ExtractResponseData(function, res.Pdu);
×
1034
        if (IsSingleBitType(reg.Type)) {
×
1035
            return TRegisterValue{data[0]};
×
1036
        }
1037

1038
        Modbus::TRegisterCache cache;
×
1039
        return GetRegisterValue(data, reg, cache);
×
1040
    }
1041

1042
} // 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