Skip to content

Commit 2bd14aa

Browse files
authored
feat(emulator): add verbose logging for Modbus operations (#316)
Implements verbose logging functionality for the Modbus emulator, enabling detailed output of register read/write operations when the --verbose CLI flag is enabled. Features: - VerboseLogger class with zero overhead when disabled - CLI integration with --verbose flag - 100% statement coverage with 22 new tests - Comprehensive documentation with usage examples Changes: - Add VerboseLogger class with buffer parsing and formatting - Integrate logger into ModbusEmulator and function code handlers - Wire --verbose flag from CLI to emulator config - Eliminate array allocation overhead when verbose disabled - Remove unused --log-requests flag - Extract duplicate formatting logic (DRY refactoring) - Add verbose logging guide to README Fixes #309, #311, #312, #313 Addresses #310, #314
1 parent 03d1e0c commit 2bd14aa

8 files changed

Lines changed: 562 additions & 16 deletions

File tree

packages/emulator/README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,56 @@ Options:
155155
-s, --slave-id <id> Slave ID (required if no config file)
156156
-v, --verbose Enable verbose logging
157157
-q, --quiet Suppress all output except errors
158-
--log-requests Log all Modbus requests/responses
159158
-V, --version output the version number
160159
-h, --help display help for command
161160
```
162161

162+
### Verbose Logging
163+
164+
Enable verbose logging to see detailed Modbus operations for debugging and testing:
165+
166+
**CLI:**
167+
168+
```bash
169+
# With config file
170+
ya-modbus-emulator --config config.yaml --verbose
171+
172+
# Or directly
173+
ya-modbus-emulator --transport rtu --port /dev/ttyUSB0 --slave-id 1 --verbose
174+
```
175+
176+
**Programmatic:**
177+
178+
```typescript
179+
const emulator = new ModbusEmulator({
180+
transport: 'memory',
181+
verbose: true,
182+
})
183+
```
184+
185+
**Output Format:**
186+
187+
```
188+
[VERBOSE] READ slave=1 func=0x03 addr=0x0000 count=2 values=[0x1234, 0x5678]
189+
[VERBOSE] WRITE slave=1 func=0x06 addr=0x0005 count=1 values=[0x9ABC]
190+
[VERBOSE] WRITE slave=2 func=0x10 addr=0x0010 count=3 values=[0xAABB, 0xCCDD, 0xEEFF]
191+
```
192+
193+
Each log entry includes:
194+
195+
- **Operation type**: READ or WRITE
196+
- **Slave ID**: Which device handled the request
197+
- **Function code**: Modbus function (0x03=Read Holding, 0x04=Read Input, 0x06=Write Single, 0x10=Write Multiple)
198+
- **Starting address**: Register address in hexadecimal
199+
- **Register count**: Number of registers accessed
200+
- **Values**: Register values in hexadecimal
201+
202+
Verbose logging is useful for:
203+
204+
- **E2E testing**: Verify exact data flow in integration tests
205+
- **Driver development**: Debug communication with emulated devices
206+
- **Protocol validation**: Ensure correct Modbus message formatting
207+
163208
### Configuration Files
164209

165210
Create a YAML or JSON configuration file to define devices and behaviors:

packages/emulator/src/behaviors/function-codes.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import type { EmulatedDevice } from '../device.js'
6+
import type { VerboseLogger } from '../verbose-logger.js'
67

78
// Modbus exception codes
89
export const ILLEGAL_FUNCTION = 0x01
@@ -12,7 +13,11 @@ export const ILLEGAL_DATA_VALUE = 0x03
1213
/**
1314
* Handle a Modbus request and return the response
1415
*/
15-
export function handleModbusRequest(device: EmulatedDevice, request: Buffer): Buffer {
16+
export function handleModbusRequest(
17+
device: EmulatedDevice,
18+
request: Buffer,
19+
verboseLogger?: VerboseLogger
20+
): Buffer {
1621
if (request.length < 2) {
1722
return createExceptionResponse(request[0] ?? 0, 0x00, ILLEGAL_DATA_VALUE)
1823
}
@@ -26,13 +31,13 @@ export function handleModbusRequest(device: EmulatedDevice, request: Buffer): Bu
2631
try {
2732
switch (functionCode) {
2833
case 0x03:
29-
return handleReadHoldingRegisters(device, request)
34+
return handleReadHoldingRegisters(device, request, verboseLogger)
3035
case 0x04:
31-
return handleReadInputRegisters(device, request)
36+
return handleReadInputRegisters(device, request, verboseLogger)
3237
case 0x06:
33-
return handleWriteSingleRegister(device, request)
38+
return handleWriteSingleRegister(device, request, verboseLogger)
3439
case 0x10:
35-
return handleWriteMultipleRegisters(device, request)
40+
return handleWriteMultipleRegisters(device, request, verboseLogger)
3641
default:
3742
return createExceptionResponse(slaveId, functionCode, ILLEGAL_FUNCTION)
3843
}
@@ -45,7 +50,11 @@ export function handleModbusRequest(device: EmulatedDevice, request: Buffer): Bu
4550
/**
4651
* 0x03 - Read Holding Registers
4752
*/
48-
function handleReadHoldingRegisters(device: EmulatedDevice, request: Buffer): Buffer {
53+
function handleReadHoldingRegisters(
54+
device: EmulatedDevice,
55+
request: Buffer,
56+
verboseLogger?: VerboseLogger
57+
): Buffer {
4958
if (request.length < 6) {
5059
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5160
return createExceptionResponse(request[0]!, 0x03, ILLEGAL_DATA_VALUE)
@@ -68,13 +77,19 @@ function handleReadHoldingRegisters(device: EmulatedDevice, request: Buffer): Bu
6877
response.writeUInt16BE(value, 3 + i * 2)
6978
}
7079

80+
verboseLogger?.logRead(slaveId, 0x03, startAddress, quantity, response)
81+
7182
return response
7283
}
7384

7485
/**
7586
* 0x04 - Read Input Registers
7687
*/
77-
function handleReadInputRegisters(device: EmulatedDevice, request: Buffer): Buffer {
88+
function handleReadInputRegisters(
89+
device: EmulatedDevice,
90+
request: Buffer,
91+
verboseLogger?: VerboseLogger
92+
): Buffer {
7893
if (request.length < 6) {
7994
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
8095
return createExceptionResponse(request[0]!, 0x04, ILLEGAL_DATA_VALUE)
@@ -97,31 +112,47 @@ function handleReadInputRegisters(device: EmulatedDevice, request: Buffer): Buff
97112
response.writeUInt16BE(value, 3 + i * 2)
98113
}
99114

115+
verboseLogger?.logRead(slaveId, 0x04, startAddress, quantity, response)
116+
100117
return response
101118
}
102119

103120
/**
104121
* 0x06 - Write Single Register
105122
*/
106-
function handleWriteSingleRegister(device: EmulatedDevice, request: Buffer): Buffer {
123+
function handleWriteSingleRegister(
124+
device: EmulatedDevice,
125+
request: Buffer,
126+
verboseLogger?: VerboseLogger
127+
): Buffer {
107128
if (request.length < 6) {
108129
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
109130
return createExceptionResponse(request[0]!, 0x06, ILLEGAL_DATA_VALUE)
110131
}
111132

133+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
134+
const slaveId = request[0]!
112135
const address = request.readUInt16BE(2)
113136
const value = request.readUInt16BE(4)
114137

115138
device.setHoldingRegister(address, value)
116139

140+
if (verboseLogger) {
141+
verboseLogger.logWrite(slaveId, 0x06, address, 1, [value])
142+
}
143+
117144
// Echo the request as response
118145
return request
119146
}
120147

121148
/**
122149
* 0x10 - Write Multiple Registers
123150
*/
124-
function handleWriteMultipleRegisters(device: EmulatedDevice, request: Buffer): Buffer {
151+
function handleWriteMultipleRegisters(
152+
device: EmulatedDevice,
153+
request: Buffer,
154+
verboseLogger?: VerboseLogger
155+
): Buffer {
125156
if (request.length < 7) {
126157
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127158
return createExceptionResponse(request[0]!, 0x10, ILLEGAL_DATA_VALUE)
@@ -139,9 +170,19 @@ function handleWriteMultipleRegisters(device: EmulatedDevice, request: Buffer):
139170
}
140171

141172
// Write registers
142-
for (let i = 0; i < quantity; i++) {
143-
const value = request.readUInt16BE(7 + i * 2)
144-
device.setHoldingRegister(startAddress + i, value)
173+
if (verboseLogger) {
174+
const values: number[] = []
175+
for (let i = 0; i < quantity; i++) {
176+
const value = request.readUInt16BE(7 + i * 2)
177+
device.setHoldingRegister(startAddress + i, value)
178+
values.push(value)
179+
}
180+
verboseLogger.logWrite(slaveId, 0x10, startAddress, quantity, values)
181+
} else {
182+
for (let i = 0; i < quantity; i++) {
183+
const value = request.readUInt16BE(7 + i * 2)
184+
device.setHoldingRegister(startAddress + i, value)
185+
}
145186
}
146187

147188
// Response: slave_id + function_code + start_address + quantity

packages/emulator/src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ interface CliOptions {
2020
slaveId?: number
2121
verbose?: boolean
2222
quiet?: boolean
23-
logRequests?: boolean
2423
}
2524

2625
const program = new Command()
@@ -41,7 +40,6 @@ program
4140
.option('-s, --slave-id <id>', 'Slave ID (required if no config file)', parseInt)
4241
.option('-v, --verbose', 'Enable verbose logging')
4342
.option('-q, --quiet', 'Suppress all output except errors')
44-
.option('--log-requests', 'Log all Modbus requests/responses')
4543

4644
program.parse()
4745

@@ -78,6 +76,7 @@ async function main(): Promise<void> {
7876
stopBits: fileConfig.transport.stopBits,
7977
}),
8078
...(fileConfig.transport.lock !== undefined && { lock: fileConfig.transport.lock }),
79+
...(options.verbose === true && { verbose: true }),
8180
}
8281

8382
devices = fileConfig.devices as Array<{ slaveId: number }>
@@ -102,6 +101,7 @@ async function main(): Promise<void> {
102101
parity: options.parity as 'none' | 'even' | 'odd',
103102
}),
104103
...(typeof options.lock === 'boolean' && { lock: options.lock }),
104+
...(options.verbose === true && { verbose: true }),
105105
}
106106

107107
devices = [{ slaveId: options.slaveId }]

0 commit comments

Comments
 (0)