Skip to content

Commit 283111f

Browse files
committed
Implement FC01/FC02 across Protocol, transports, tests, and docs
- Add ReadCoilStatus (FC01) and ReadInputStatus (FC02) to Protocol NVI API and hooks\n- Implement FC01/FC02 for TCPIPProtocol and RTUProtocol; add DummyProtocol overrides\n- Extend tests with FC01/FC02 coverage for TCP/IP, Dummy, and RTU endpoints\n- Update README, TECHNICAL_DOCS, CLAUDE guidance, and regenerate Doxygen HTML output
1 parent b674eb5 commit 283111f

File tree

62 files changed

+3299
-1009
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3299
-1009
lines changed

CLAUDE.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Protocol (abstract base, Modbus.h)
7373
└── DummyProtocol — No-op stub for tests (ModbusDummy.h)
7474
```
7575

76-
**`Modbus::Master::Protocol`** is the abstract base. Subclasses implement protected `Do*()` virtual hooks for transport I/O and connection management. Public methods (FC03, FC04, FC06, FC16, FC22) are implemented in the base class on top of those hooks.
76+
**`Modbus::Master::Protocol`** is the abstract base. Subclasses implement protected `Do*()` virtual hooks for transport I/O and connection management. Public methods (FC01, FC02, FC03, FC04, FC06, FC16, FC22) are implemented in the base class on top of those hooks.
7777

7878
**`Modbus::Master::SessionManager`** is an RAII guard: calls `Open()` on construction, `Close()` on destruction.
7979

@@ -89,7 +89,7 @@ Protocol (abstract base, Modbus.h)
8989

9090
## Test Suite
9191

92-
`Test/ModbusTest.cpp` uses **Boost.Test 1.89** (`boost/test/included/unit_test.hpp`). A `ServerFixture` global fixture starts an embedded Modbus slave on `127.0.0.1:5020` in a `std::thread` before tests run and stops it after. Tests cover FC03, FC04, FC06, FC16, FC22, and exception handling.
92+
`Test/ModbusTest.cpp` uses **Boost.Test 1.89** (`boost/test/included/unit_test.hpp`). A `ServerFixture` global fixture starts an embedded Modbus slave on `127.0.0.1:5020` in a `std::thread` before tests run and stops it after. Tests cover FC01, FC02, FC03, FC04, FC06, FC16, FC22, and exception handling across TCP/IP, Dummy, and RTU endpoints.
9393

9494
CMake test build details and known pitfalls (SysInit.o linking, PCH2 force-include) are documented in [Test/README-cmake.md](Test/README-cmake.md) and [TECHNICAL_DOCS.md](TECHNICAL_DOCS.md).
9595

@@ -100,23 +100,23 @@ CMake test build details and known pitfalls (SysInit.o linking, PCH2 force-inclu
100100
- Correct: `_D("Modbus TCP")`, `String(DEFAULT_MODBUS_TCPIP_HOST)`
101101
- Incorrect: `_D(DEFAULT_MODBUS_TCPIP_HOST)`
102102

103-
**Non-Virtual Interface (NVI) Pattern:**
104-
105-
This project implements the NVI pattern throughout the protocol hierarchy to enforce consistent API contracts across all transports:
106-
107-
- **Public methods** (Open, Close, ReadHoldingRegisters, PresetMultipleRegisters, etc.) are non-virtual concrete functions in the base class.
108-
- **Virtual hooks** follow the "`Do`" prefix naming convention: DoOpen, DoClose, DoReadHoldingRegisters, DoPresetMultipleRegisters, etc.
109-
- **Hook placement:** Virtual hooks are declared in the `protected` section of abstract base classes.
110-
- **Subclass responsibility:** Concrete transport subclasses (RTUProtocol, TCPProtocolIndy, TCPProtocolWinSock, etc.) implement ONLY the Do…() virtual hooks; they do NOT override public methods.
111-
- **Benefit:** This enforces contract validation, logging, and error handling at the base class level, ensuring consistency across RTU, TCP, UDP, and Dummy transports.
112-
113-
Example: To add a new transport (e.g., SerialProtocolCustom extending RTUProtocol), override only the Do…() methods—never override the public API.
114-
115-
**Namespaces:**
116-
117-
- `Modbus::` — types, exceptions, `Context`
118-
- `Modbus::Master::``Protocol`, `SessionManager`, all transport classes
119-
- `Modbus::Utils::` — serial enumeration (`SerEnum.h`)
103+
**Non-Virtual Interface (NVI) Pattern:**
104+
105+
This project implements the NVI pattern throughout the protocol hierarchy to enforce consistent API contracts across all transports:
106+
107+
- **Public methods** (Open, Close, ReadHoldingRegisters, PresetMultipleRegisters, etc.) are non-virtual concrete functions in the base class.
108+
- **Virtual hooks** follow the "`Do`" prefix naming convention: DoOpen, DoClose, DoReadHoldingRegisters, DoPresetMultipleRegisters, etc.
109+
- **Hook placement:** Virtual hooks are declared in the `protected` section of abstract base classes.
110+
- **Subclass responsibility:** Concrete transport subclasses (RTUProtocol, TCPProtocolIndy, TCPProtocolWinSock, etc.) implement ONLY the Do…() virtual hooks; they do NOT override public methods.
111+
- **Benefit:** This enforces contract validation, logging, and error handling at the base class level, ensuring consistency across RTU, TCP, UDP, and Dummy transports.
112+
113+
Example: To add a new transport (e.g., SerialProtocolCustom extending RTUProtocol), override only the Do…() methods—never override the public API.
114+
115+
**Namespaces:**
116+
117+
- `Modbus::` — types, exceptions, `Context`
118+
- `Modbus::Master::``Protocol`, `SessionManager`, all transport classes
119+
- `Modbus::Utils::` — serial enumeration (`SerEnum.h`)
120120

121121
**Compiler compatibility:** `[[noreturn]]` attribute placement differs between BCC32 and BCC32C/BCC64 — see existing exception class declarations for the guarded pattern.
122122

Modbus.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ void Protocol::Close()
163163
// DoReadCoilStatus
164164
// DoReadInputStatus
165165

166+
void Protocol::DoReadCoilStatus( Context const & /* Context */,
167+
CoilAddrType /*StartAddr*/,
168+
CoilCountType /*PointCount*/,
169+
CoilDataType* /*Data*/ )
170+
{
171+
RaiseFunctionCodeNotImplementedException( FunctionCode::ReadCoilStatus );
172+
}
173+
//---------------------------------------------------------------------------
174+
175+
void Protocol::DoReadInputStatus( Context const & /* Context */,
176+
CoilAddrType /*StartAddr*/,
177+
CoilCountType /*PointCount*/,
178+
CoilDataType* /*Data*/ )
179+
{
180+
RaiseFunctionCodeNotImplementedException( FunctionCode::ReadInputStatus );
181+
}
182+
//---------------------------------------------------------------------------
183+
166184
void Protocol::DoReadHoldingRegisters( Context const & /* Context */,
167185
RegAddrType /*StartAddr*/,
168186
RegCountType /*PointCount*/,

Modbus.h

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,47 @@ class Protocol {
386386
/** @brief Returns a human-readable string describing the protocol parameters (host:port, COM port settings, etc.). */
387387
String GetProtocolParamsStr() const { return DoGetProtocolParamsStr(); }
388388

389-
// ReadCoilStatus
390-
// ReadInputStatus
389+
/**
390+
* @brief Reads one or more coil statuses (FC01).
391+
* @param Context Transaction context (slave address, transaction ID).
392+
* @param StartAddr Zero-based start coil address.
393+
* @param PointCount Number of coils to read (max 2000 by Modbus spec).
394+
* @param[out] Data Pointer to packed coil bytes; required size is
395+
* @c (PointCount + 7) / 8 bytes.
396+
*
397+
* @details Coil values are bit-packed in LSB-first order per Modbus spec.
398+
* Bit 0 of @c Data[0] corresponds to @p StartAddr.
399+
*
400+
* @throws EIllegalDataAddress if the address or count is out of range on the slave.
401+
* @throws EBaseException on communication error or timeout.
402+
*/
403+
void ReadCoilStatus( Context const & Context,
404+
CoilAddrType StartAddr, CoilCountType PointCount,
405+
CoilDataType* Data )
406+
{
407+
DoReadCoilStatus( Context, StartAddr, PointCount, Data );
408+
}
409+
410+
/**
411+
* @brief Reads one or more discrete input statuses (FC02).
412+
* @param Context Transaction context (slave address, transaction ID).
413+
* @param StartAddr Zero-based start input address.
414+
* @param PointCount Number of discrete inputs to read (max 2000 by Modbus spec).
415+
* @param[out] Data Pointer to packed input bytes; required size is
416+
* @c (PointCount + 7) / 8 bytes.
417+
*
418+
* @details Input values are bit-packed in LSB-first order per Modbus spec.
419+
* Bit 0 of @c Data[0] corresponds to @p StartAddr.
420+
*
421+
* @throws EIllegalDataAddress if the address or count is out of range on the slave.
422+
* @throws EBaseException on communication error or timeout.
423+
*/
424+
void ReadInputStatus( Context const & Context,
425+
CoilAddrType StartAddr, CoilCountType PointCount,
426+
CoilDataType* Data )
427+
{
428+
DoReadInputStatus( Context, StartAddr, PointCount, Data );
429+
}
391430

392431
/**
393432
* @brief Reads one or more holding registers (FC03).
@@ -532,7 +571,15 @@ class Protocol {
532571
* 4. Extract the register values and populate the Data buffer.
533572
*/
534573

535-
// DoReadInputStatus
574+
virtual void DoReadCoilStatus( Context const & Context,
575+
CoilAddrType StartAddr,
576+
CoilCountType PointCount,
577+
CoilDataType* Data ) = 0;
578+
virtual void DoReadInputStatus( Context const & Context,
579+
CoilAddrType StartAddr,
580+
CoilCountType PointCount,
581+
CoilDataType* Data ) = 0;
582+
536583
virtual void DoReadHoldingRegisters( Context const & Context,
537584
RegAddrType StartAddr,
538585
RegCountType PointCount,

ModbusDummy.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ class DummyProtocol : public Protocol {
5454
virtual void DoClose() override { active_ = false; }
5555
virtual bool DoIsConnected() const override { return active_; }
5656

57-
// DoReadCoilStatus
58-
// DoReadInputStatus
57+
virtual void DoReadCoilStatus( Context const & Context,
58+
CoilAddrType StartAddr,
59+
CoilCountType PointCount,
60+
CoilDataType* Data ) override {}
61+
virtual void DoReadInputStatus( Context const & Context,
62+
CoilAddrType StartAddr,
63+
CoilCountType PointCount,
64+
CoilDataType* Data ) override {}
5965

6066
virtual void DoReadHoldingRegisters( Context const & Context,
6167
RegAddrType StartAddr,

ModbusRTU.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,70 @@ bool RTUProtocol::DoIsConnected() const
201201
// RTUProtocol::DoReadCoilStatus
202202
// RTUProtocol::DoReadInputStatus
203203

204+
void RTUProtocol::ReadBits( FunctionCode FnCode, Context const & Context,
205+
CoilAddrType StartAddr, CoilCountType PointCount,
206+
CoilDataType* Data )
207+
{
208+
if ( PointCount == 0 || PointCount > 2000 ) {
209+
throw EContextException(
210+
Context, _D( "Too many points have been requested" )
211+
);
212+
}
213+
214+
FrameCont TxFrame;
215+
const FrameCont::size_type ExpectedByteCount( ( PointCount + 7 ) / 8 );
216+
const FrameCont::size_type ExpectedRxFramelength( ExpectedByteCount + 5 );
217+
FrameCont RxFrame;
218+
219+
RxFrame.reserve( ExpectedRxFramelength );
220+
TxFrame.reserve( 8 );
221+
222+
back_insert_iterator<FrameCont> TxFrameBkInsIt( TxFrame );
223+
*TxFrameBkInsIt++ = Context.GetSlaveAddr();
224+
*TxFrameBkInsIt++ = static_cast<RegDataType>( FnCode );
225+
TxFrameBkInsIt =
226+
WriteAddressPointCountPair( TxFrameBkInsIt, StartAddr, PointCount );
227+
WriteCRC( TxFrameBkInsIt, TxFrame.begin(), TxFrame.end() );
228+
229+
SendAndReceiveFrames(
230+
Context, TxFrame, back_inserter( RxFrame ),
231+
ExpectedRxFramelength, retryCount_
232+
);
233+
234+
FrameCont::const_iterator RxInIt = RxFrame.begin();
235+
236+
const FrameCont::size_type RxByteCount =
237+
static_cast<FrameCont::size_type>( *RxInIt++ );
238+
239+
if ( RxByteCount != ExpectedByteCount ) {
240+
throw EContextException( Context, _D( "Byte count mismatch" ) );
241+
}
242+
243+
for ( FrameCont::size_type Idx = 0; Idx < RxByteCount; ++Idx ) {
244+
*Data++ = *RxInIt++;
245+
}
246+
}
247+
//---------------------------------------------------------------------------
248+
249+
void RTUProtocol::DoReadCoilStatus( Context const & Context,
250+
CoilAddrType StartAddr,
251+
CoilCountType PointCount,
252+
CoilDataType* Data )
253+
{
254+
ReadBits( FunctionCode::ReadCoilStatus, Context, StartAddr,
255+
PointCount, Data );
256+
}
257+
//---------------------------------------------------------------------------
258+
259+
void RTUProtocol::DoReadInputStatus( Context const & Context,
260+
CoilAddrType StartAddr,
261+
CoilCountType PointCount,
262+
CoilDataType* Data )
263+
{
264+
ReadBits( FunctionCode::ReadInputStatus, Context, StartAddr,
265+
PointCount, Data );
266+
}
267+
204268
//---------------------------------------------------------------------------
205269

206270
void RTUProtocol::ReadRegisters( FunctionCode FnCode, Context const & Context,

ModbusRTU.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,14 @@ class RTUProtocol : public Protocol {
156156
virtual void DoClose() override;
157157
virtual bool DoIsConnected() const override;
158158

159-
// DoReadCoilStatus
160-
// DoReadInputStatus
159+
virtual void DoReadCoilStatus( Context const & Context,
160+
CoilAddrType StartAddr,
161+
CoilCountType PointCount,
162+
CoilDataType* Data ) override;
163+
virtual void DoReadInputStatus( Context const & Context,
164+
CoilAddrType StartAddr,
165+
CoilCountType PointCount,
166+
CoilDataType* Data ) override;
161167

162168
virtual void DoReadHoldingRegisters( Context const & Context,
163169
RegAddrType StartAddr,
@@ -258,6 +264,10 @@ class RTUProtocol : public Protocol {
258264
RegAddrType StartAddr, RegCountType PointCount,
259265
RegDataType* Data );
260266

267+
void ReadBits( FunctionCode FnCode, Context const & Context,
268+
CoilAddrType StartAddr, CoilCountType PointCount,
269+
CoilDataType* Data );
270+
261271
public:
262272
/**
263273
* @brief Computes and appends the Modbus CRC16 to an output iterator range.

ModbusTCP_IP.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,76 @@ String TCPIPProtocol::DoGetProtocolParamsStr() const
260260
//---------------------------------------------------------------------------
261261

262262
// TCPIPProtocol::DoReadCoilStatus
263+
void TCPIPProtocol::ReadBits( FunctionCode FnCode, Context const & Context,
264+
CoilAddrType StartAddr, CoilCountType PointCount,
265+
CoilDataType* Data )
266+
{
267+
if ( PointCount == 0 || PointCount > 2000 ) {
268+
throw EContextException( Context, _D( "Invalid point count" ) );
269+
}
270+
271+
const uint8_t ExpectedByteCount =
272+
static_cast<uint8_t>( ( PointCount + 7 ) / 8 );
273+
274+
// Send
275+
TBytes OutBuffer;
276+
SetLength( OutBuffer, GetBMAPHeaderLength() + 1 + GetAddressPointCountPairLength() );
277+
int Idx = WriteBMAPHeader( OutBuffer, 0, Context );
278+
OutBuffer[Idx++] = static_cast<RegDataType>( FnCode );
279+
Idx = WriteAddressPointCountPair( OutBuffer, Idx, StartAddr, PointCount );
280+
DoInputBufferClear();
281+
DoWrite( OutBuffer );
282+
283+
// Receive
284+
TBytes ReplyBMAPBuffer;
285+
SetLength( ReplyBMAPBuffer, GetBMAPHeaderLength() );
286+
DoRead( ReplyBMAPBuffer, GetLength( ReplyBMAPBuffer ) );
287+
288+
// Validate BMAP and payload function code
289+
RaiseExceptionIfBMAPIsNotEQ( Context, OutBuffer, ReplyBMAPBuffer );
290+
TBytes ReplyBuffer;
291+
SetLength( ReplyBuffer, GetBMAPDataLength( ReplyBMAPBuffer ) - 1 );
292+
DoRead( ReplyBuffer, GetLength( ReplyBuffer ) );
293+
RaiseExceptionIfReplyIsNotValid( Context, ReplyBuffer, FnCode );
294+
295+
if ( GetLength( ReplyBuffer ) < 2 ) {
296+
throw EContextException( Context, _D( "Invalid reply length" ) );
297+
}
298+
299+
const uint8_t ByteCount = ReplyBuffer[1];
300+
if ( ByteCount != ExpectedByteCount || GetLength( ReplyBuffer ) != ByteCount + 2 ) {
301+
throw EContextException( Context, _D( "Byte count mismatch" ) );
302+
}
303+
304+
for ( uint8_t I = 0; I < ByteCount; ++I ) {
305+
Data[I] = ReplyBuffer[2 + I];
306+
}
307+
}
263308
//---------------------------------------------------------------------------
264309

265310
// TCPIPProtocol::DoReadInputStatus
311+
void TCPIPProtocol::DoReadCoilStatus( Context const & Context,
312+
CoilAddrType StartAddr,
313+
CoilCountType PointCount,
314+
CoilDataType* Data )
315+
{
316+
RaiseExceptionIfIsNotConnected( _D( "ReadCoilStatus failed" ) );
317+
318+
ReadBits( FunctionCode::ReadCoilStatus, Context, StartAddr,
319+
PointCount, Data );
320+
}
321+
//---------------------------------------------------------------------------
322+
323+
void TCPIPProtocol::DoReadInputStatus( Context const & Context,
324+
CoilAddrType StartAddr,
325+
CoilCountType PointCount,
326+
CoilDataType* Data )
327+
{
328+
RaiseExceptionIfIsNotConnected( _D( "ReadInputStatus failed" ) );
329+
330+
ReadBits( FunctionCode::ReadInputStatus, Context, StartAddr,
331+
PointCount, Data );
332+
}
266333
//---------------------------------------------------------------------------
267334

268335
void TCPIPProtocol::ReadRegisters( FunctionCode FnCode, Context const & Context,

ModbusTCP_IP.h

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ namespace Master {
7171
* - Builds Modbus request frames with proper MBAP headers.
7272
* - Delegates I/O to pure virtual DoWrite() and DoRead() hooks.
7373
* - Validates MBAP response headers (transaction ID, protocol ID = 0, unit identifier).
74-
* - Implements all Modbus function codes (FC03, FC04, FC06, FC16, FC22) inherited by TCP/UDP transports.
74+
* - Implements all Modbus function codes (FC01, FC02, FC03, FC04, FC06, FC16, FC22)
75+
* inherited by TCP/UDP transports.
7576
*
7677
* **NVI Architecture:**
7778
* - Concrete TCP/UDP subclasses (e.g., TCPProtocolIndy, UDPProtocolWinSock) override the
@@ -86,7 +87,8 @@ namespace Master {
8687
* - DoRead() — reads exactly @p Length bytes from the response stream or datagram cache.
8788
* (other Do…() methods for FC03, FC04, etc. are defined in Protocol and inherited here).
8889
*
89-
* All Modbus function codes supported by this library (FC03, FC04, FC06, FC16, FC22) are
90+
* All Modbus function codes supported by this library
91+
* (FC01, FC02, FC03, FC04, FC06, FC16, FC22) are
9092
* fully implemented in Protocol and inherited by TCPIPProtocol; concrete TCP/UDP subclasses
9193
* do not need to reimplement function code logic.
9294
*/
@@ -142,8 +144,14 @@ class TCPIPProtocol : public Protocol {
142144
*/
143145
virtual void DoRead( TBytes& InBuffer, size_t Length ) = 0;
144146

145-
// DoReadCoilStatus
146-
// DoReadInputStatus
147+
virtual void DoReadCoilStatus( Context const & Context,
148+
CoilAddrType StartAddr,
149+
CoilCountType PointCount,
150+
CoilDataType* Data ) override;
151+
virtual void DoReadInputStatus( Context const & Context,
152+
CoilAddrType StartAddr,
153+
CoilCountType PointCount,
154+
CoilDataType* Data ) override;
147155
virtual void DoReadHoldingRegisters( Context const & Context,
148156
RegAddrType StartAddr,
149157
RegCountType PointCount,
@@ -217,6 +225,10 @@ class TCPIPProtocol : public Protocol {
217225
RegAddrType StartAddr, RegCountType PointCount,
218226
RegDataType* Data );
219227

228+
void ReadBits( FunctionCode FnCode, Context const & Context,
229+
CoilAddrType StartAddr, CoilCountType PointCount,
230+
CoilDataType* Data );
231+
220232
protected:
221233
template<typename T>
222234
static uint16_t GetLength( T const & Data ) {

0 commit comments

Comments
 (0)