Skip to content

Commit 0b63c87

Browse files
committed
Add support for function Get Communication Event Counter
1 parent f1c2e55 commit 0b63c87

15 files changed

+603
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
# [3.5.0] - 2023-12-30
8+
9+
* Adds Function 11 (0x0b) `Get Communication Event Counter` support [#156](https://github.com/aldas/modbus-tcp-client/pull/156)
10+
11+
712
# [3.4.1] - 2023-10-19
813

914
* Debug page for writing registers, similar to index.php which is for reading registers [#148](https://github.com/aldas/modbus-tcp-client/pull/148)

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ composer require aldas/modbus-tcp-client
2222
* FC4 - Read Input Registers ([ReadInputRegistersRequest](src/Packet/ModbusFunction/ReadInputRegistersRequest.php) / [ReadInputRegistersResponse](src/Packet/ModbusFunction/ReadInputRegistersResponse.php))
2323
* FC5 - Write Single Coil ([WriteSingleCoilRequest](src/Packet/ModbusFunction/WriteSingleCoilRequest.php) / [WriteSingleCoilResponse](src/Packet/ModbusFunction/WriteSingleCoilResponse.php))
2424
* FC6 - Write Single Register ([WriteSingleRegisterRequest](src/Packet/ModbusFunction/WriteSingleRegisterRequest.php) / [WriteSingleRegisterResponse](src/Packet/ModbusFunction/WriteSingleRegisterResponse.php))
25+
* FC11 - Get Communication Event Counter ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/GetCommEventCounterRequest.php) / [GetCommEventCounterResponse](src/Packet/ModbusFunction/GetCommEventCounterResponse.php))
2526
* FC15 - Write Multiple Coils ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/WriteMultipleCoilsRequest.php) / [WriteMultipleCoilsResponse](src/Packet/ModbusFunction/WriteMultipleCoilsResponse.php))
2627
* FC16 - Write Multiple Registers ([WriteMultipleRegistersRequest](src/Packet/ModbusFunction/WriteMultipleRegistersRequest.php) / [WriteMultipleRegistersResponse](src/Packet/ModbusFunction/WriteMultipleRegistersResponse.php))
2728
* FC22 - Mask Write Register ([MaskWriteRegisterRequest](src/Packet/ModbusFunction/MaskWriteRegisterRequest.php) / [MaskWriteRegisterResponse](src/Packet/ModbusFunction/MaskWriteRegisterResponse.php))
2829
* FC23 - Read / Write Multiple Registers ([ReadWriteMultipleRegistersRequest](src/Packet/ModbusFunction/ReadWriteMultipleRegistersRequest.php) / [ReadWriteMultipleRegistersResponse](src/Packet/ModbusFunction/ReadWriteMultipleRegistersResponse.php))
2930

3031
### Utility functions
3132

32-
* [Packet::isCompleteLength](src/Utils/Packet.php) - checks if data is complete Modbus TCP packet
33-
* [Packet::isCompleteLengthRTU()](src/Utils/Packet.php) - checks if data is complete Modbus RTU packet
33+
* [Packet::isCompleteLength](src/Utils/Packet.php) - checks if data is complete Modbus TCP request or response packet
34+
* [Packet::isCompleteLengthRTU()](src/Utils/Packet.php) - checks if data is complete Modbus RTU response packet
3435
* [ErrorResponse::is](src/Packet/ErrorResponse.php) - checks if data is Modbus TCP error packet
3536

3637
## Requirements
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace ModbusTcpClient\Packet\ModbusFunction;
4+
5+
use ModbusTcpClient\Packet\ErrorResponse;
6+
use ModbusTcpClient\Packet\ModbusPacket;
7+
use ModbusTcpClient\Packet\ModbusRequest;
8+
use ModbusTcpClient\Packet\ProtocolDataUnit;
9+
use ModbusTcpClient\Utils\Types;
10+
11+
/**
12+
* Request for Get Communication Event Counter (FC=11, 0x0b)
13+
*
14+
* Example packet: \x81\x80\x00\x00\x00\x02\x10\x0b
15+
* \x81\x80 - transaction id
16+
* \x00\x00 - protocol id
17+
* \x00\x08 - number of bytes in the message (PDU = ProtocolDataUnit) to follow
18+
* \x10 - unit id
19+
* \x0b - function code
20+
*
21+
*/
22+
class GetCommEventCounterRequest extends ProtocolDataUnit implements ModbusRequest
23+
{
24+
public function __construct(int $unitId = 0, int $transactionId = null)
25+
{
26+
parent::__construct($unitId, $transactionId);
27+
}
28+
29+
public function getFunctionCode(): int
30+
{
31+
return ModbusPacket::GET_COMM_EVENT_COUNTER; // 11 (0x0b)
32+
}
33+
34+
protected function getLengthInternal(): int
35+
{
36+
return 1; // size of function code (1 byte)
37+
}
38+
39+
public function __toString(): string
40+
{
41+
return b''
42+
. $this->getHeader()->__toString()
43+
. Types::toByte($this->getFunctionCode());
44+
}
45+
46+
/**
47+
* Parses binary string to GetCommEventCounterRequest or return ErrorResponse on failure
48+
*
49+
* @param string $binaryString
50+
* @return GetCommEventCounterRequest|ErrorResponse
51+
*/
52+
public static function parse(string $binaryString): GetCommEventCounterRequest|ErrorResponse
53+
{
54+
return self::parsePacket(
55+
$binaryString,
56+
8,
57+
ModbusPacket::GET_COMM_EVENT_COUNTER,
58+
function (int $transactionId, int $unitId) use ($binaryString) {
59+
return new self($unitId, $transactionId);
60+
}
61+
);
62+
}
63+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace ModbusTcpClient\Packet\ModbusFunction;
4+
5+
use ModbusTcpClient\Packet\ModbusPacket;
6+
use ModbusTcpClient\Packet\ModbusResponse;
7+
use ModbusTcpClient\Packet\ProtocolDataUnit;
8+
use ModbusTcpClient\Utils\Endian;
9+
use ModbusTcpClient\Utils\Types;
10+
11+
/**
12+
* Response for Get Communication Event Counter (FC=11, 0x0b)
13+
*
14+
* Example packet: \x81\x80\x00\x00\x00\x08\x10\x0b\xFF\xFF\x00\x01
15+
* \x81\x80 - transaction id
16+
* \x00\x00 - protocol id
17+
* \x00\x08 - number of bytes in the message (PDU = ProtocolDataUnit) to follow
18+
* \x10 - unit id
19+
* \x0b - function code
20+
* \xFF\xFF - status (0xFFFF = previously request command is still being run. ok, 0x0000 = no running)
21+
* \x00\x01 - event counter value (0 to 65535)
22+
*
23+
*/
24+
class GetCommEventCounterResponse extends ProtocolDataUnit implements ModbusResponse
25+
{
26+
/**
27+
* @var int function code of event counter being requested
28+
*/
29+
private int $status;
30+
31+
/**
32+
* @var int event counter value (0 to 65535)
33+
*/
34+
private int $eventCount;
35+
36+
public function __construct(string $rawData, int $unitId = 0, int $transactionId = null)
37+
{
38+
parent::__construct($unitId, $transactionId);
39+
$this->status = Types::parseUInt16(substr($rawData, 0, 2), Endian::BIG_ENDIAN);
40+
$this->eventCount = Types::parseUInt16(substr($rawData, 2, 2), Endian::BIG_ENDIAN);
41+
}
42+
43+
public function getStatus(): int
44+
{
45+
return $this->status;
46+
}
47+
48+
public function getEventCount(): int
49+
{
50+
return $this->eventCount;
51+
}
52+
53+
public function getFunctionCode(): int
54+
{
55+
return ModbusPacket::GET_COMM_EVENT_COUNTER; // 11 (0x0b)
56+
}
57+
58+
public function __toString(): string
59+
{
60+
return b''
61+
. $this->getHeader()->__toString()
62+
. Types::toByte($this->getFunctionCode())
63+
. Types::toRegister($this->getStatus()) // 0xFFFF or 0x0000
64+
. Types::toUint16($this->getEventCount(), Endian::BIG_ENDIAN);
65+
}
66+
67+
protected function getLengthInternal(): int
68+
{
69+
return 5; // size of function code (1 byte) + status (2 bytes) + event count (2 bytes)
70+
}
71+
72+
public function withStartAddress(int $startAddress): static
73+
{
74+
// Note: I am being stupid and stubborn here. Somehow `ModbusResponse` interface ended up having this method
75+
// and want this response to work with ResponseFactory::parseResponse method.
76+
// TODO: change ModbusResponse interface or ResponseFactory::parseResponse signature
77+
return clone $this;
78+
}
79+
}

src/Packet/ModbusPacket.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66

77
interface ModbusPacket
88
{
9-
const READ_COILS = 1;
10-
const READ_INPUT_DISCRETES = 2;
11-
const READ_HOLDING_REGISTERS = 3;
12-
const READ_INPUT_REGISTERS = 4;
13-
const WRITE_SINGLE_COIL = 5;
14-
const WRITE_SINGLE_REGISTER = 6;
15-
const WRITE_MULTIPLE_COILS = 15;
16-
const WRITE_MULTIPLE_REGISTERS = 16;
17-
const MASK_WRITE_REGISTER = 22;
18-
const READ_WRITE_MULTIPLE_REGISTERS = 23;
9+
const READ_COILS = 1; // 0x01
10+
const READ_INPUT_DISCRETES = 2; // 0x02
11+
const READ_HOLDING_REGISTERS = 3; // 0x03
12+
const READ_INPUT_REGISTERS = 4; // 0x04
13+
const WRITE_SINGLE_COIL = 5; // 0x05
14+
const WRITE_SINGLE_REGISTER = 6; // 0x06
15+
const GET_COMM_EVENT_COUNTER = 11; // 0x0B
16+
const WRITE_MULTIPLE_COILS = 15; // 0x0F
17+
const WRITE_MULTIPLE_REGISTERS = 16; // 0x10
18+
const MASK_WRITE_REGISTER = 22; // 0x16
19+
const READ_WRITE_MULTIPLE_REGISTERS = 23; // 0x17
1920

2021

2122
/**

src/Packet/ProtocolDataUnit.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
* 0003: The total number of registers requested. (read 3 registers 40108 to 40110)
1414
*/
1515

16+
use ModbusTcpClient\Exception\InvalidArgumentException;
17+
use ModbusTcpClient\Utils\Endian;
18+
use ModbusTcpClient\Utils\Types;
19+
1620
abstract class ProtocolDataUnit implements ModbusPacket
1721
{
1822
/**
@@ -40,4 +44,52 @@ public function toHex(): string
4044
{
4145
return unpack('H*', $this->__toString())[1];
4246
}
47+
48+
/**
49+
* @param string|null $binaryString
50+
* @param int $minLength
51+
* @param int $functionCode
52+
* @param callable $createFunctor
53+
* @return mixed|ErrorResponse
54+
*/
55+
protected static function parsePacket(string|null $binaryString, int $minLength, int $functionCode, callable $createFunctor): mixed
56+
{
57+
if ($binaryString === null || strlen($binaryString) < $minLength) {
58+
return new ErrorResponse(new ModbusApplicationHeader(2, 0, 0),
59+
$functionCode,
60+
4 // Server failure
61+
);
62+
}
63+
64+
$transactionId = Types::parseUInt16($binaryString[0] . $binaryString[1], Endian::BIG_ENDIAN);
65+
$unitId = Types::parseByte($binaryString[6]);
66+
if ($functionCode !== ord($binaryString[7])) {
67+
return new ErrorResponse(
68+
new ModbusApplicationHeader(2, $unitId, $transactionId),
69+
$functionCode,
70+
1 // Illegal function
71+
);
72+
}
73+
$pduLength = Types::parseUInt16($binaryString[4] . $binaryString[5], Endian::BIG_ENDIAN);
74+
if (($pduLength + 6) !== strlen($binaryString)) {
75+
return new ErrorResponse(
76+
new ModbusApplicationHeader(2, $unitId, $transactionId),
77+
$functionCode,
78+
3 // Illegal data value
79+
);
80+
}
81+
82+
try {
83+
return $createFunctor($transactionId, $unitId);
84+
} catch (\Exception $exception) {
85+
// constructor does validation and throws exception so not to mix returning errors and throwing exceptions
86+
// we catch exception here and return it as a error response.
87+
$errorCode = $exception instanceof InvalidArgumentException ? $exception->getCode() : 3; // Illegal data value
88+
return new ErrorResponse(
89+
new ModbusApplicationHeader(2, $unitId, $transactionId),
90+
$functionCode,
91+
$errorCode
92+
);
93+
}
94+
}
4395
}

src/Packet/RequestFactory.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66

77
use ModbusTcpClient\Exception\ModbusException;
8+
use ModbusTcpClient\Packet\ModbusFunction\GetCommEventCounterRequest;
9+
use ModbusTcpClient\Packet\ModbusFunction\MaskWriteRegisterRequest;
810
use ModbusTcpClient\Packet\ModbusFunction\ReadCoilsRequest;
911
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;
1012
use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesRequest;
@@ -24,7 +26,7 @@ class RequestFactory
2426
*/
2527
public static function parseRequest(string|null $binaryString): ModbusRequest|ErrorResponse
2628
{
27-
if ($binaryString === null || strlen($binaryString) < 9) { // 7 bytes for MBAP header and at least 2 bytes for PDU
29+
if ($binaryString === null || strlen($binaryString) < 8) { // 7 bytes for MBAP header and at least 1 bytes for PDU
2830
throw new ModbusException('Request null or data length too short to be valid packet!');
2931
}
3032

@@ -35,23 +37,27 @@ public static function parseRequest(string|null $binaryString): ModbusRequest|Er
3537
}
3638

3739
switch ($functionCode) {
38-
case ModbusPacket::READ_HOLDING_REGISTERS:
40+
case ModbusPacket::READ_HOLDING_REGISTERS: // 3 (0x03)
3941
return ReadHoldingRegistersRequest::parse($binaryString);
40-
case ModbusPacket::READ_INPUT_REGISTERS:
42+
case ModbusPacket::READ_INPUT_REGISTERS: // 4 (0x04)
4143
return ReadInputRegistersRequest::parse($binaryString);
42-
case ModbusPacket::READ_COILS:
44+
case ModbusPacket::READ_COILS: // 1 (0x01)
4345
return ReadCoilsRequest::parse($binaryString);
44-
case ModbusPacket::READ_INPUT_DISCRETES:
46+
case ModbusPacket::READ_INPUT_DISCRETES: // 2 (0x02)
4547
return ReadInputDiscretesRequest::parse($binaryString);
46-
case ModbusPacket::WRITE_SINGLE_COIL:
48+
case ModbusPacket::WRITE_SINGLE_COIL: // 5 (0x05)
4749
return WriteSingleCoilRequest::parse($binaryString);
48-
case ModbusPacket::WRITE_SINGLE_REGISTER:
50+
case ModbusPacket::WRITE_SINGLE_REGISTER: // 6 (0x06)
4951
return WriteSingleRegisterRequest::parse($binaryString);
50-
case ModbusPacket::WRITE_MULTIPLE_COILS:
52+
case ModbusPacket::GET_COMM_EVENT_COUNTER: // 11 (0x0B)
53+
return GetCommEventCounterRequest::parse($binaryString);
54+
case ModbusPacket::WRITE_MULTIPLE_COILS: // 15 (0x0F)
5155
return WriteMultipleCoilsRequest::parse($binaryString);
52-
case ModbusPacket::WRITE_MULTIPLE_REGISTERS:
56+
case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10)
5357
return WriteMultipleRegistersRequest::parse($binaryString);
54-
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS:
58+
case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16)
59+
return MaskWriteRegisterRequest::parse($binaryString);
60+
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17)
5561
return ReadWriteMultipleRegistersRequest::parse($binaryString);
5662
default:
5763
return new ErrorResponse(ModbusApplicationHeader::parse($binaryString), $functionCode, 1);

src/Packet/ResponseFactory.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use ModbusTcpClient\Exception\ModbusException;
88
use ModbusTcpClient\Exception\ParseException;
9+
use ModbusTcpClient\Packet\ModbusFunction\GetCommEventCounterResponse;
910
use ModbusTcpClient\Packet\ModbusFunction\MaskWriteRegisterResponse;
1011
use ModbusTcpClient\Packet\ModbusFunction\ReadCoilsResponse;
1112
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersResponse;
@@ -47,25 +48,27 @@ public static function parseResponse(string|null $binaryString): ModbusResponse|
4748
$rawData = substr($binaryString, 8);
4849

4950
switch ($functionCode) {
50-
case ModbusPacket::READ_HOLDING_REGISTERS:
51+
case ModbusPacket::READ_HOLDING_REGISTERS: // 3 (0x03)
5152
return new ReadHoldingRegistersResponse($rawData, $unitId, $transactionId);
52-
case ModbusPacket::READ_INPUT_REGISTERS:
53+
case ModbusPacket::READ_INPUT_REGISTERS: // 4 (0x04)
5354
return new ReadInputRegistersResponse($rawData, $unitId, $transactionId);
54-
case ModbusPacket::READ_COILS:
55+
case ModbusPacket::READ_COILS: // 1 (0x01)
5556
return new ReadCoilsResponse($rawData, $unitId, $transactionId);
56-
case ModbusPacket::READ_INPUT_DISCRETES:
57+
case ModbusPacket::READ_INPUT_DISCRETES: // 2 (0x02)
5758
return new ReadInputDiscretesResponse($rawData, $unitId, $transactionId);
58-
case ModbusPacket::WRITE_SINGLE_COIL:
59+
case ModbusPacket::WRITE_SINGLE_COIL: // 5 (0x05)
5960
return new WriteSingleCoilResponse($rawData, $unitId, $transactionId);
60-
case ModbusPacket::WRITE_SINGLE_REGISTER:
61+
case ModbusPacket::WRITE_SINGLE_REGISTER: // 6 (0x06)
6162
return new WriteSingleRegisterResponse($rawData, $unitId, $transactionId);
62-
case ModbusPacket::WRITE_MULTIPLE_COILS:
63+
case ModbusPacket::GET_COMM_EVENT_COUNTER: // 11 (0x0B)
64+
return new GetCommEventCounterResponse($rawData, $unitId, $transactionId);
65+
case ModbusPacket::WRITE_MULTIPLE_COILS: // 15 (0x0F)
6366
return new WriteMultipleCoilsResponse($rawData, $unitId, $transactionId);
64-
case ModbusPacket::WRITE_MULTIPLE_REGISTERS:
67+
case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10)
6568
return new WriteMultipleRegistersResponse($rawData, $unitId, $transactionId);
66-
case ModbusPacket::MASK_WRITE_REGISTER:
69+
case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16)
6770
return new MaskWriteRegisterResponse($rawData, $unitId, $transactionId);
68-
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS:
71+
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17)
6972
return new ReadWriteMultipleRegistersResponse($rawData, $unitId, $transactionId);
7073
default:
7174
throw new ParseException("Unknown function code '{$functionCode}' read from response packet");

0 commit comments

Comments
 (0)