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

wirenboard / wb-mqtt-serial / 1

08 Jul 2025 01:20PM UTC coverage: 73.854% (+1.0%) from 72.836%
1

Pull #963

github

39d9bc
KraPete
Bump version
Pull Request #963: Bump version

6444 of 9057 branches covered (71.15%)

12341 of 16710 relevant lines covered (73.85%)

305.53 hits per line

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

88.93
/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
    inline EFunction GetFunction(const TRegisterConfig& reg, OperationType op)
85✔
384
    {
385
        return GetFunctionImpl(reg.Type, op, reg.TypeName, IsPacking(reg));
85✔
386
    }
387

388
    // returns number of requests needed to write register
389
    size_t InferWriteRequestsCount(const TRegisterConfig& reg)
56✔
390
    {
391
        return IsPacking(reg) ? 1 : GetModbusDataWidthIn16BitWords(reg);
56✔
392
    }
393

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

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

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

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

455
        // Place data
456
        value <<= reg.GetDataOffset();
20✔
457
        valueToWrite |= value;
20✔
458

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

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

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

511
        // use cache only for "partial" registers
512
        if (bitWidth < 16) {
62✔
513
            address = GetUint32RegisterAddress(reg.GetAddress()) + wordIndex;
9✔
514
            if (cache.count(address)) {
9✔
515
                cachedValue = cache.at(address);
8✔
516
            }
517
            updateCache = true;
9✔
518
        }
519

520
        auto bitOffset = std::max(static_cast<int32_t>(reg.GetDataOffset()) - wordIndex * 16, 0);
62✔
521
        auto bitCount = std::min(static_cast<uint32_t>(16 - bitOffset), bitWidth);
62✔
522
        auto mask = GetLSBMask(bitCount) << bitOffset;
62✔
523
        auto valueToWrite = (~mask & cachedValue) | (mask & (value << bitOffset));
62✔
524

525
        data.resize(2);
62✔
526
        WriteAs2Bytes(data.data(), valueToWrite, reg.ByteOrder);
62✔
527

528
        if (updateCache) {
62✔
529
            tmpCache[address] = valueToWrite;
9✔
530
        }
531
    }
62✔
532

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

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

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

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

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

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

643
    PRegisterRange CreateRegisterRange(std::chrono::microseconds averageResponseTime)
494✔
644
    {
645
        return std::make_shared<TModbusRegisterRange>(averageResponseTime);
494✔
646
    }
647

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

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

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

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

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

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

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

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

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

858
            auto width = GetModbusDataWidthIn16BitWords(*item->RegisterConfig);
5✔
859
            if (count && count + width > maxRegs) {
5✔
860
                break;
×
861
            }
862

863
            ComposeRawMultipleWriteRequestData(data, *item->RegisterConfig, item->RawValue, cache, tmpCache);
5✔
864
            LOG(Info) << item->ToString();
5✔
865

866
            count += width;
5✔
867
            last = item;
5✔
868
            ++startIt;
5✔
869
        }
870
        try {
871
            auto function = count > 1 ? FN_WRITE_MULTIPLE_REGISTERS : FN_WRITE_SINGLE_REGISTER;
2✔
872
            WriteTransaction(traits,
4✔
873
                             port,
874
                             slaveId,
875
                             function,
876
                             Modbus::CalcResponsePDUSize(function, count),
877
                             Modbus::MakePDU(function, start + shift, count, data),
4✔
878
                             requestDelay,
879
                             responseTimeout,
880
                             frameTimeout);
881
            for (const auto& item: tmpCache) {
11✔
882
                cache.insert_or_assign(item.first, item.second);
9✔
883
            }
884
        } catch (const TSerialDevicePermanentRegisterException& e) {
×
885
            LOG(Warn) << "Failed to write " << count << " setup items starting from register address " << start << ": "
×
886
                      << e.what();
×
887
        }
888
        return startIt;
2✔
889
    }
890

891
    void WriteSetupRegisters(Modbus::IModbusTraits& traits,
56✔
892
                             TPort& port,
893
                             uint8_t slaveId,
894
                             const TDeviceSetupItems& setupItems,
895
                             Modbus::TRegisterCache& cache,
896
                             std::chrono::microseconds requestDelay,
897
                             std::chrono::milliseconds responseTimeout,
898
                             std::chrono::milliseconds frameTimeout,
899
                             int shift)
900
    {
901
        if (!FillSetupRegistersCache(traits, port, slaveId, setupItems, cache, shift)) {
56✔
902
            throw TSerialDeviceException("unable to write setup registers because unable to read data needed to set "
×
903
                                         "values of \"partial\" setup registers");
×
904
        }
905
        auto it = setupItems.begin();
56✔
906
        while (it != setupItems.end()) {
78✔
907
            auto item = *it;
64✔
908
            auto config = item->Device->DeviceConfig();
64✔
909
            size_t maxRegs = MAX_WRITE_REGISTERS;
32✔
910
            if (config->MaxWriteRegisters > 0 && config->MaxWriteRegisters < maxRegs) {
32✔
911
                maxRegs = config->MaxWriteRegisters;
30✔
912
            }
913
            if (maxRegs > 1 && item->RegisterConfig->Type == REG_HOLDING && !item->RegisterConfig->IsPartial()) {
32✔
914
                it = WriteMultipleSetupRegisters(traits,
915
                                                 port,
916
                                                 slaveId,
917
                                                 it,
918
                                                 setupItems.end(),
919
                                                 maxRegs,
920
                                                 cache,
921
                                                 requestDelay,
922
                                                 responseTimeout,
923
                                                 frameTimeout,
924
                                                 shift);
2✔
925
            } else {
926
                try {
927
                    WriteRegister(traits,
30✔
928
                                  port,
929
                                  slaveId,
930
                                  *item->RegisterConfig,
30✔
931
                                  item->RawValue,
30✔
932
                                  cache,
933
                                  requestDelay,
934
                                  responseTimeout,
935
                                  frameTimeout,
936
                                  shift);
937
                    LOG(Info) << item->ToString();
19✔
938
                } catch (const TSerialDevicePermanentRegisterException& e) {
11✔
939
                    LOG(Warn) << "Failed to write setup item \"" << item->Name << "\": " << e.what();
1✔
940
                }
941
                ++it;
20✔
942
            }
943
        }
944
        if (!setupItems.empty()) {
46✔
945
            auto item = *setupItems.begin();
20✔
946
            if (item->Device) {
10✔
947
                item->Device->SetTransferResult(true);
10✔
948
            }
949
        }
950
    }
46✔
951

952
    bool EnableWbContinuousRead(PSerialDevice device,
9✔
953
                                IModbusTraits& traits,
954
                                TPort& port,
955
                                uint8_t slaveId,
956
                                TRegisterCache& cache)
957
    {
958
        auto config = WbRegisters::GetRegisterConfig("continuous_read");
27✔
959
        try {
960
            Modbus::WriteRegister(traits,
9✔
961
                                  port,
962
                                  slaveId,
963
                                  *config,
9✔
964
                                  TRegisterValue(1),
11✔
965
                                  cache,
966
                                  device->DeviceConfig()->RequestDelay,
19✔
967
                                  device->GetResponseTimeout(port),
9✔
968
                                  device->GetFrameTimeout(port));
9✔
969
            LOG(Info) << "Continuous read enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
8✔
970
            if (device->DeviceConfig()->MaxRegHole < MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS) {
8✔
971
                device->DeviceConfig()->MaxRegHole = MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS;
7✔
972
            }
973
            if (device->DeviceConfig()->MaxBitHole < MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS) {
8✔
974
                device->DeviceConfig()->MaxBitHole = MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS;
7✔
975
            }
976
            return true;
8✔
977
        } catch (const TSerialDevicePermanentRegisterException& e) {
1✔
978
            // A firmware doesn't support continuous read
979
            LOG(Warn) << "Continuous read is not enabled [slave_id is " << device->DeviceConfig()->SlaveId + "]";
1✔
980
        }
981
        return false;
1✔
982
    }
983

984
    TRegisterValue ReadRegister(IModbusTraits& traits,
×
985
                                TPort& port,
986
                                uint8_t slaveId,
987
                                const TRegisterConfig& reg,
988
                                std::chrono::microseconds requestDelay,
989
                                std::chrono::milliseconds responseTimeout,
990
                                std::chrono::milliseconds frameTimeout)
991
    {
992
        size_t modbusRegisterCount = GetModbusDataWidthIn16BitWords(reg);
×
993
        auto addr = GetUint32RegisterAddress(reg.GetAddress());
×
994
        auto function = GetFunctionImpl(reg.Type, OperationType::OP_READ, reg.TypeName, false);
×
995
        auto requestPdu = Modbus::MakePDU(function, addr, modbusRegisterCount, {});
×
996

997
        port.SleepSinceLastInteraction(requestDelay);
×
998
        auto res = traits.Transaction(port,
999
                                      slaveId,
1000
                                      requestPdu,
1001
                                      Modbus::CalcResponsePDUSize(function, modbusRegisterCount),
1002
                                      responseTimeout,
1003
                                      frameTimeout);
×
1004

1005
        auto data = Modbus::ExtractResponseData(function, res.Pdu);
×
1006
        if (IsSingleBitType(reg.Type)) {
×
1007
            return TRegisterValue{data[0]};
×
1008
        }
1009

1010
        Modbus::TRegisterCache cache;
×
1011
        return GetRegisterValue(data, reg, cache);
×
1012
    }
1013

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