Skip to content

Commit 6704b45

Browse files
committed
Add support for FC17 Report server ID
1 parent 9939eb8 commit 6704b45

11 files changed

+515
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ 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
7+
# [3.5.0] - 2024-01-01
88

9-
* Adds Function 11 (0x0b) `Get Communication Event Counter` support [#156](https://github.com/aldas/modbus-tcp-client/pull/156)
9+
* Adds Function 11 (0x0b) `Get Communication Event Counter` and Function 17 (0x11) `Report server ID` support [#156](https://github.com/aldas/modbus-tcp-client/pull/156)
1010

1111

1212
# [3.4.1] - 2023-10-19

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ composer require aldas/modbus-tcp-client
2525
* FC11 - Get Communication Event Counter ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/GetCommEventCounterRequest.php) / [GetCommEventCounterResponse](src/Packet/ModbusFunction/GetCommEventCounterResponse.php))
2626
* FC15 - Write Multiple Coils ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/WriteMultipleCoilsRequest.php) / [WriteMultipleCoilsResponse](src/Packet/ModbusFunction/WriteMultipleCoilsResponse.php))
2727
* FC16 - Write Multiple Registers ([WriteMultipleRegistersRequest](src/Packet/ModbusFunction/WriteMultipleRegistersRequest.php) / [WriteMultipleRegistersResponse](src/Packet/ModbusFunction/WriteMultipleRegistersResponse.php))
28+
* FC17 - Report Server ID ([ReportServerIDRequest](src/Packet/ModbusFunction/ReportServerIDRequest.php) / [ReportServerIDResponse](src/Packet/ModbusFunction/ReportServerIDResponse.php))
2829
* FC22 - Mask Write Register ([MaskWriteRegisterRequest](src/Packet/ModbusFunction/MaskWriteRegisterRequest.php) / [MaskWriteRegisterResponse](src/Packet/ModbusFunction/MaskWriteRegisterResponse.php))
2930
* FC23 - Read / Write Multiple Registers ([ReadWriteMultipleRegistersRequest](src/Packet/ModbusFunction/ReadWriteMultipleRegistersRequest.php) / [ReadWriteMultipleRegistersResponse](src/Packet/ModbusFunction/ReadWriteMultipleRegistersResponse.php))
3031

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 Report Server ID (FC=17, 0x11)
13+
*
14+
* Example packet: \x81\x80\x00\x00\x00\x02\x10\x11
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+
* \x11 - function code
20+
*
21+
*/
22+
class ReportServerIDRequest 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::REPORT_SERVER_ID; // 17 (0x11)
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 ReportServerIDRequest or return ErrorResponse on failure
48+
*
49+
* @param string $binaryString
50+
* @return ReportServerIDRequest|ErrorResponse
51+
*/
52+
public static function parse(string $binaryString): ReportServerIDRequest|ErrorResponse
53+
{
54+
return self::parsePacket(
55+
$binaryString,
56+
8,
57+
ModbusPacket::REPORT_SERVER_ID,
58+
function (int $transactionId, int $unitId) {
59+
return new self($unitId, $transactionId);
60+
}
61+
);
62+
}
63+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace ModbusTcpClient\Packet\ModbusFunction;
4+
5+
use ModbusTcpClient\Exception\InvalidArgumentException;
6+
use ModbusTcpClient\Packet\ModbusPacket;
7+
use ModbusTcpClient\Packet\ModbusResponse;
8+
use ModbusTcpClient\Packet\ProtocolDataUnit;
9+
use ModbusTcpClient\Utils\Types;
10+
11+
/**
12+
* Response for Report Server ID (FC=17, 0x11)
13+
*
14+
* Example packet: \x81\x80\x00\x00\x00\x08\x10\x11\x02\x01\x02\x00\x01\x02
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+
* \x11 - function code
20+
* \x02 - byte count for server id
21+
* \x01\x02 - N bytes for server id (device specific, variable length)
22+
* \x00 - run status
23+
* \x01\x02 - optional N bytes for additional data (device specific, variable length)
24+
*
25+
*/
26+
class ReportServerIDResponse extends ProtocolDataUnit implements ModbusResponse
27+
{
28+
/**
29+
* @var string server ID bytes as binary string
30+
*/
31+
private string $serverID;
32+
33+
/**
34+
* @var int run status
35+
*/
36+
private int $status;
37+
38+
/**
39+
* @var string|null additional data (optional)
40+
*/
41+
private ?string $additionalData = null;
42+
43+
public function __construct(string $rawData, int $unitId = 0, int $transactionId = null)
44+
{
45+
$serverIDLength = Types::parseByte($rawData[0]);
46+
if (strlen($rawData) < ($serverIDLength + 2)) {
47+
throw new InvalidArgumentException("too few bytes to be a complete report server id packet");
48+
}
49+
$this->serverID = substr($rawData, 1, $serverIDLength);
50+
$this->status = Types::parseByte($rawData[$serverIDLength + 1]);
51+
if (strlen($rawData) > ($serverIDLength + 2)) {
52+
$this->additionalData = substr($rawData, $serverIDLength + 2);
53+
}
54+
55+
parent::__construct($unitId, $transactionId);
56+
57+
}
58+
59+
public function getStatus(): int
60+
{
61+
return $this->status;
62+
}
63+
64+
/**
65+
* Server ID value as binary string
66+
* @return string
67+
*/
68+
public function getServerID(): string
69+
{
70+
return $this->serverID;
71+
}
72+
73+
/**
74+
* @return int[]
75+
*/
76+
public function getServerIDBytes(): array
77+
{
78+
return Types::parseByteArray($this->serverID);
79+
}
80+
81+
public function getAdditionalData(): ?string
82+
{
83+
return $this->additionalData;
84+
}
85+
86+
/**
87+
* @return int[]
88+
*/
89+
public function getAdditionalDataBytes(): array
90+
{
91+
if ($this->additionalData) {
92+
return Types::parseByteArray($this->additionalData);
93+
}
94+
return [];
95+
}
96+
97+
public function getFunctionCode(): int
98+
{
99+
return ModbusPacket::REPORT_SERVER_ID; // 17 (0x11)
100+
}
101+
102+
public function __toString(): string
103+
{
104+
$serverID = $this->getServerID();
105+
$additionalData = $this->getAdditionalData();
106+
107+
$result = b''
108+
. $this->getHeader()->__toString()
109+
. Types::toByte($this->getFunctionCode())
110+
. Types::toByte(strlen($serverID))
111+
. $serverID
112+
. Types::toByte($this->getStatus());
113+
if ($additionalData !== null) {
114+
$result .= $additionalData;
115+
}
116+
return $result;
117+
}
118+
119+
protected function getLengthInternal(): int
120+
{
121+
$serverIDLength = strlen($this->getServerID());
122+
$additionalDataLength = strlen($this->getAdditionalData());
123+
// size of function code (1 byte) +
124+
// server id byte count (1 byte) +
125+
// server id value bytes (N bytes) +
126+
// status (1 byte) +
127+
// additional data bytes (N bytes)
128+
return 3 + $serverIDLength + $additionalDataLength;
129+
}
130+
131+
public function withStartAddress(int $startAddress): static
132+
{
133+
// Note: I am being stupid and stubborn here. Somehow `ModbusResponse` interface ended up having this method
134+
// and want this response to work with ResponseFactory::parseResponse method.
135+
// TODO: change ModbusResponse interface or ResponseFactory::parseResponse signature
136+
return clone $this;
137+
}
138+
}

src/Packet/ModbusPacket.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface ModbusPacket
1515
const GET_COMM_EVENT_COUNTER = 11; // 0x0B
1616
const WRITE_MULTIPLE_COILS = 15; // 0x0F
1717
const WRITE_MULTIPLE_REGISTERS = 16; // 0x10
18+
const REPORT_SERVER_ID = 17; // 0x11
1819
const MASK_WRITE_REGISTER = 22; // 0x16
1920
const READ_WRITE_MULTIPLE_REGISTERS = 23; // 0x17
2021

src/Packet/RequestFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesRequest;
1313
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
1414
use ModbusTcpClient\Packet\ModbusFunction\ReadWriteMultipleRegistersRequest;
15+
use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDRequest;
1516
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleCoilsRequest;
1617
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest;
1718
use ModbusTcpClient\Packet\ModbusFunction\WriteSingleCoilRequest;
@@ -55,6 +56,8 @@ public static function parseRequest(string|null $binaryString): ModbusRequest|Er
5556
return WriteMultipleCoilsRequest::parse($binaryString);
5657
case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10)
5758
return WriteMultipleRegistersRequest::parse($binaryString);
59+
case ModbusPacket::REPORT_SERVER_ID: // 17 (0x11)
60+
return ReportServerIDRequest::parse($binaryString);
5861
case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16)
5962
return MaskWriteRegisterRequest::parse($binaryString);
6063
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17)

src/Packet/ResponseFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesResponse;
1414
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersResponse;
1515
use ModbusTcpClient\Packet\ModbusFunction\ReadWriteMultipleRegistersResponse;
16+
use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDResponse;
1617
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleCoilsResponse;
1718
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersResponse;
1819
use ModbusTcpClient\Packet\ModbusFunction\WriteSingleCoilResponse;
@@ -66,6 +67,8 @@ public static function parseResponse(string|null $binaryString): ModbusResponse|
6667
return new WriteMultipleCoilsResponse($rawData, $unitId, $transactionId);
6768
case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10)
6869
return new WriteMultipleRegistersResponse($rawData, $unitId, $transactionId);
70+
case ModbusPacket::REPORT_SERVER_ID: // 17 (0x11)
71+
return new ReportServerIDResponse($rawData, $unitId, $transactionId);
6972
case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16)
7073
return new MaskWriteRegisterResponse($rawData, $unitId, $transactionId);
7174
case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace Tests\unit\Packet\ModbusFunction;
4+
5+
use ModbusTcpClient\Packet\ErrorResponse;
6+
use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDRequest;
7+
use ModbusTcpClient\Packet\ModbusPacket;
8+
use ModbusTcpClient\Utils\Endian;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ReportServerIDRequestTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
Endian::$defaultEndian = Endian::LITTLE_ENDIAN; // packets are big endian. setting to default to little should not change output
16+
}
17+
18+
protected function tearDown(): void
19+
{
20+
Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
21+
}
22+
23+
public function testOnPacketToString()
24+
{
25+
// Field: Size in packet
26+
$payload = "\x01\x38" . // transaction id: 0138 (2 bytes)
27+
"\x00\x00" . // protocol id: 0000 (2 bytes)
28+
"\x00\x02" . // length: 0002 (2 bytes) (2 bytes after this field)
29+
"\x11" . // unit id: 11 (1 byte)
30+
"\x11" . // function code: 11 (1 byte)
31+
'';
32+
$this->assertEquals(
33+
$payload,
34+
(new ReportServerIDRequest(
35+
0x11,
36+
0x0138,
37+
))->__toString()
38+
);
39+
}
40+
41+
public function testOnPacketProperties()
42+
{
43+
$packet = new ReportServerIDRequest(
44+
0x11,
45+
0x0138
46+
);
47+
$this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode());
48+
49+
$header = $packet->getHeader();
50+
$this->assertEquals(0x0138, $header->getTransactionId());
51+
$this->assertEquals(0, $header->getProtocolId());
52+
$this->assertEquals(2, $header->getLength());
53+
$this->assertEquals(0x11, $header->getUnitId());
54+
}
55+
56+
public function testParse()
57+
{
58+
// Field: Size in packet
59+
$payload = "\x01\x38" . // transaction id: 0138 (2 bytes)
60+
"\x00\x00" . // protocol id: 0000 (2 bytes)
61+
"\x00\x02" . // length: 0002 (2 bytes) (2 bytes after this field)
62+
"\x11" . // unit id: 11 (1 byte)
63+
"\x11" . // function code: 11 (1 byte)
64+
'';
65+
$packet = ReportServerIDRequest::parse($payload);
66+
$this->assertEquals($packet, (new ReportServerIDRequest(
67+
0x11,
68+
0x0138
69+
))->__toString());
70+
$this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode());
71+
72+
$header = $packet->getHeader();
73+
$this->assertEquals(0x0138, $header->getTransactionId());
74+
$this->assertEquals(0, $header->getProtocolId());
75+
$this->assertEquals(2, $header->getLength());
76+
$this->assertEquals(0x11, $header->getUnitId());
77+
}
78+
79+
public function testParseShouldReturnErrorResponseForTooShortPacket()
80+
{
81+
// Field: Size in packet
82+
$payload = "\x01\x38" . // transaction id: 0138 (2 bytes)
83+
"\x00\x00" . // protocol id: 0000 (2 bytes)
84+
"\x00\x01" . // length: 0001 (2 bytes) (2 bytes after this field)
85+
"\x11" . // unit id: 11 (1 byte)
86+
"\x11" . // function code: 11 (1 byte)
87+
'';
88+
$packet = ReportServerIDRequest::parse($payload);
89+
self::assertInstanceOf(ErrorResponse::class, $packet);
90+
$toString = $packet->__toString();
91+
// transaction id is random
92+
$toString[0] = "\x00";
93+
$toString[1] = "\x00";
94+
self::assertEquals("\x00\x00\x00\x00\x00\x03\x11\x91\x03", $toString);
95+
}
96+
97+
public function testParseShouldReturnErrorResponseForInvalidFunction()
98+
{
99+
// Field: Size in packet
100+
$payload = "\x01\x38" . // transaction id: 0138 (2 bytes)
101+
"\x00\x00" . // protocol id: 0000 (2 bytes)
102+
"\x00\x02" . // length: 0002 (2 bytes) (2 bytes after this field)
103+
"\x11" . // unit id: 11 (1 byte)
104+
"\x01" . // function code: 01 (1 byte) <-- should be 0x11
105+
'';
106+
$packet = ReportServerIDRequest::parse($payload);
107+
self::assertInstanceOf(ErrorResponse::class, $packet);
108+
$toString = $packet->__toString();
109+
self::assertEquals("\x01\x38\x00\x00\x00\x03\x11\x91\x01", $toString);
110+
}
111+
}

0 commit comments

Comments
 (0)