From 0d249f20f9c150438c469fc3b127029f0bb8cd6d Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 1 Jan 2024 19:23:38 +0200 Subject: [PATCH] Add support for FC17 Report server ID --- CHANGELOG.md | 4 +- README.md | 1 + .../ModbusFunction/ReportServerIDRequest.php | 63 ++++++++ .../ModbusFunction/ReportServerIDResponse.php | 138 ++++++++++++++++ src/Packet/ModbusPacket.php | 1 + src/Packet/RequestFactory.php | 3 + src/Packet/ResponseFactory.php | 3 + .../ReportServerIDRequestTest.php | 111 +++++++++++++ .../ReportServerIDResponseTest.php | 153 ++++++++++++++++++ tests/unit/Packet/RequestFactoryTest.php | 10 +- tests/unit/Packet/ResponseFactoryTest.php | 37 ++++- 11 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 src/Packet/ModbusFunction/ReportServerIDRequest.php create mode 100644 src/Packet/ModbusFunction/ReportServerIDResponse.php create mode 100644 tests/unit/Packet/ModbusFunction/ReportServerIDRequestTest.php create mode 100644 tests/unit/Packet/ModbusFunction/ReportServerIDResponseTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2380d..aef24ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [3.5.0] - 2023-12-30 +# [3.5.0] - 2024-01-01 -* Adds Function 11 (0x0b) `Get Communication Event Counter` support [#156](https://github.com/aldas/modbus-tcp-client/pull/156) +* 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) # [3.4.1] - 2023-10-19 diff --git a/README.md b/README.md index 4d0705f..0694319 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ composer require aldas/modbus-tcp-client * FC11 - Get Communication Event Counter ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/GetCommEventCounterRequest.php) / [GetCommEventCounterResponse](src/Packet/ModbusFunction/GetCommEventCounterResponse.php)) * FC15 - Write Multiple Coils ([WriteMultipleCoilsRequest](src/Packet/ModbusFunction/WriteMultipleCoilsRequest.php) / [WriteMultipleCoilsResponse](src/Packet/ModbusFunction/WriteMultipleCoilsResponse.php)) * FC16 - Write Multiple Registers ([WriteMultipleRegistersRequest](src/Packet/ModbusFunction/WriteMultipleRegistersRequest.php) / [WriteMultipleRegistersResponse](src/Packet/ModbusFunction/WriteMultipleRegistersResponse.php)) +* FC17 - Report Server ID ([ReportServerIDRequest](src/Packet/ModbusFunction/ReportServerIDRequest.php) / [ReportServerIDResponse](src/Packet/ModbusFunction/ReportServerIDResponse.php)) * FC22 - Mask Write Register ([MaskWriteRegisterRequest](src/Packet/ModbusFunction/MaskWriteRegisterRequest.php) / [MaskWriteRegisterResponse](src/Packet/ModbusFunction/MaskWriteRegisterResponse.php)) * FC23 - Read / Write Multiple Registers ([ReadWriteMultipleRegistersRequest](src/Packet/ModbusFunction/ReadWriteMultipleRegistersRequest.php) / [ReadWriteMultipleRegistersResponse](src/Packet/ModbusFunction/ReadWriteMultipleRegistersResponse.php)) diff --git a/src/Packet/ModbusFunction/ReportServerIDRequest.php b/src/Packet/ModbusFunction/ReportServerIDRequest.php new file mode 100644 index 0000000..ca9915b --- /dev/null +++ b/src/Packet/ModbusFunction/ReportServerIDRequest.php @@ -0,0 +1,63 @@ +getHeader()->__toString() + . Types::toByte($this->getFunctionCode()); + } + + /** + * Parses binary string to ReportServerIDRequest or return ErrorResponse on failure + * + * @param string $binaryString + * @return ReportServerIDRequest|ErrorResponse + */ + public static function parse(string $binaryString): ReportServerIDRequest|ErrorResponse + { + return self::parsePacket( + $binaryString, + 8, + ModbusPacket::REPORT_SERVER_ID, + function (int $transactionId, int $unitId) { + return new self($unitId, $transactionId); + } + ); + } +} \ No newline at end of file diff --git a/src/Packet/ModbusFunction/ReportServerIDResponse.php b/src/Packet/ModbusFunction/ReportServerIDResponse.php new file mode 100644 index 0000000..e57ea0e --- /dev/null +++ b/src/Packet/ModbusFunction/ReportServerIDResponse.php @@ -0,0 +1,138 @@ +serverID = substr($rawData, 1, $serverIDLength); + $this->status = Types::parseByte($rawData[$serverIDLength + 1]); + if (strlen($rawData) > ($serverIDLength + 2)) { + $this->additionalData = substr($rawData, $serverIDLength + 2); + } + + parent::__construct($unitId, $transactionId); + + } + + public function getStatus(): int + { + return $this->status; + } + + /** + * Server ID value as binary string + * @return string + */ + public function getServerID(): string + { + return $this->serverID; + } + + /** + * @return int[] + */ + public function getServerIDBytes(): array + { + return Types::parseByteArray($this->serverID); + } + + public function getAdditionalData(): ?string + { + return $this->additionalData; + } + + /** + * @return int[] + */ + public function getAdditionalDataBytes(): array + { + if ($this->additionalData) { + return Types::parseByteArray($this->additionalData); + } + return []; + } + + public function getFunctionCode(): int + { + return ModbusPacket::REPORT_SERVER_ID; // 17 (0x11) + } + + public function __toString(): string + { + $serverID = $this->getServerID(); + $additionalData = $this->getAdditionalData(); + + $result = b'' + . $this->getHeader()->__toString() + . Types::toByte($this->getFunctionCode()) + . Types::toByte(strlen($serverID)) + . $serverID + . Types::toByte($this->getStatus()); + if ($additionalData !== null) { + $result .= $additionalData; + } + return $result; + } + + protected function getLengthInternal(): int + { + $serverIDLength = strlen($this->getServerID()); + $additionalDataLength = strlen($this->getAdditionalData()); + // size of function code (1 byte) + + // server id byte count (1 byte) + + // server id value bytes (N bytes) + + // status (1 byte) + + // additional data bytes (N bytes) + return 3 + $serverIDLength + $additionalDataLength; + } + + public function withStartAddress(int $startAddress): static + { + // Note: I am being stupid and stubborn here. Somehow `ModbusResponse` interface ended up having this method + // and want this response to work with ResponseFactory::parseResponse method. + // TODO: change ModbusResponse interface or ResponseFactory::parseResponse signature + return clone $this; + } +} \ No newline at end of file diff --git a/src/Packet/ModbusPacket.php b/src/Packet/ModbusPacket.php index 9e9e97e..293e9d9 100644 --- a/src/Packet/ModbusPacket.php +++ b/src/Packet/ModbusPacket.php @@ -15,6 +15,7 @@ interface ModbusPacket const GET_COMM_EVENT_COUNTER = 11; // 0x0B const WRITE_MULTIPLE_COILS = 15; // 0x0F const WRITE_MULTIPLE_REGISTERS = 16; // 0x10 + const REPORT_SERVER_ID = 17; // 0x11 const MASK_WRITE_REGISTER = 22; // 0x16 const READ_WRITE_MULTIPLE_REGISTERS = 23; // 0x17 diff --git a/src/Packet/RequestFactory.php b/src/Packet/RequestFactory.php index 7b90c98..c755aeb 100644 --- a/src/Packet/RequestFactory.php +++ b/src/Packet/RequestFactory.php @@ -12,6 +12,7 @@ use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesRequest; use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest; use ModbusTcpClient\Packet\ModbusFunction\ReadWriteMultipleRegistersRequest; +use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleCoilsRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteSingleCoilRequest; @@ -55,6 +56,8 @@ public static function parseRequest(string|null $binaryString): ModbusRequest|Er return WriteMultipleCoilsRequest::parse($binaryString); case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10) return WriteMultipleRegistersRequest::parse($binaryString); + case ModbusPacket::REPORT_SERVER_ID: // 17 (0x11) + return ReportServerIDRequest::parse($binaryString); case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16) return MaskWriteRegisterRequest::parse($binaryString); case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17) diff --git a/src/Packet/ResponseFactory.php b/src/Packet/ResponseFactory.php index a74625d..079f6f5 100644 --- a/src/Packet/ResponseFactory.php +++ b/src/Packet/ResponseFactory.php @@ -13,6 +13,7 @@ use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesResponse; use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersResponse; use ModbusTcpClient\Packet\ModbusFunction\ReadWriteMultipleRegistersResponse; +use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDResponse; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleCoilsResponse; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersResponse; use ModbusTcpClient\Packet\ModbusFunction\WriteSingleCoilResponse; @@ -66,6 +67,8 @@ public static function parseResponse(string|null $binaryString): ModbusResponse| return new WriteMultipleCoilsResponse($rawData, $unitId, $transactionId); case ModbusPacket::WRITE_MULTIPLE_REGISTERS: // 16 (0x10) return new WriteMultipleRegistersResponse($rawData, $unitId, $transactionId); + case ModbusPacket::REPORT_SERVER_ID: // 17 (0x11) + return new ReportServerIDResponse($rawData, $unitId, $transactionId); case ModbusPacket::MASK_WRITE_REGISTER: // 22 (0x16) return new MaskWriteRegisterResponse($rawData, $unitId, $transactionId); case ModbusPacket::READ_WRITE_MULTIPLE_REGISTERS: // 23 (0x17) diff --git a/tests/unit/Packet/ModbusFunction/ReportServerIDRequestTest.php b/tests/unit/Packet/ModbusFunction/ReportServerIDRequestTest.php new file mode 100644 index 0000000..729dc06 --- /dev/null +++ b/tests/unit/Packet/ModbusFunction/ReportServerIDRequestTest.php @@ -0,0 +1,111 @@ +assertEquals( + $payload, + (new ReportServerIDRequest( + 0x11, + 0x0138, + ))->__toString() + ); + } + + public function testOnPacketProperties() + { + $packet = new ReportServerIDRequest( + 0x11, + 0x0138 + ); + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode()); + + $header = $packet->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(2, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } + + public function testParse() + { + // Field: Size in packet + $payload = "\x01\x38" . // transaction id: 0138 (2 bytes) + "\x00\x00" . // protocol id: 0000 (2 bytes) + "\x00\x02" . // length: 0002 (2 bytes) (2 bytes after this field) + "\x11" . // unit id: 11 (1 byte) + "\x11" . // function code: 11 (1 byte) + ''; + $packet = ReportServerIDRequest::parse($payload); + $this->assertEquals($packet, (new ReportServerIDRequest( + 0x11, + 0x0138 + ))->__toString()); + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode()); + + $header = $packet->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(2, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } + + public function testParseShouldReturnErrorResponseForTooShortPacket() + { + // Field: Size in packet + $payload = "\x01\x38" . // transaction id: 0138 (2 bytes) + "\x00\x00" . // protocol id: 0000 (2 bytes) + "\x00\x01" . // length: 0001 (2 bytes) (2 bytes after this field) + "\x11" . // unit id: 11 (1 byte) + "\x11" . // function code: 11 (1 byte) + ''; + $packet = ReportServerIDRequest::parse($payload); + self::assertInstanceOf(ErrorResponse::class, $packet); + $toString = $packet->__toString(); + // transaction id is random + $toString[0] = "\x00"; + $toString[1] = "\x00"; + self::assertEquals("\x00\x00\x00\x00\x00\x03\x11\x91\x03", $toString); + } + + public function testParseShouldReturnErrorResponseForInvalidFunction() + { + // Field: Size in packet + $payload = "\x01\x38" . // transaction id: 0138 (2 bytes) + "\x00\x00" . // protocol id: 0000 (2 bytes) + "\x00\x02" . // length: 0002 (2 bytes) (2 bytes after this field) + "\x11" . // unit id: 11 (1 byte) + "\x01" . // function code: 01 (1 byte) <-- should be 0x11 + ''; + $packet = ReportServerIDRequest::parse($payload); + self::assertInstanceOf(ErrorResponse::class, $packet); + $toString = $packet->__toString(); + self::assertEquals("\x01\x38\x00\x00\x00\x03\x11\x91\x01", $toString); + } +} diff --git a/tests/unit/Packet/ModbusFunction/ReportServerIDResponseTest.php b/tests/unit/Packet/ModbusFunction/ReportServerIDResponseTest.php new file mode 100644 index 0000000..3b95ad8 --- /dev/null +++ b/tests/unit/Packet/ModbusFunction/ReportServerIDResponseTest.php @@ -0,0 +1,153 @@ +assertEquals( + $payload, + (new ReportServerIDResponse( + "\x02\x01\x02\xFF\x03\x04", + 0x11, + 0x0138, + ))->__toString() + ); + } + + public function testOnPacketToStringWithoutAdditionalData() + { + // Field: Size in packet + $payload = "\x01\x38" . // transaction id: 0138 (2 bytes) + "\x00\x00" . // protocol id: 0000 (2 bytes) + "\x00\x06" . // length: 0006 (2 bytes) (6 bytes after this field) + "\x11" . // unit id: 11 (1 byte) + "\x11" . // function code: 0b (1 byte) + "\x02" . // server id byte count (1 bytes) + "\x01\x02" . // server id (0x0102) (N bytes) + "\xFF" . // status: FF (1 bytes) + ''; // no additional data + $this->assertEquals( + $payload, + (new ReportServerIDResponse( + "\x02\x01\x02\xFF", + 0x11, + 0x0138, + ))->__toString() + ); + } + + public function testValidateServerIDbytesLengthTooShort() + { + $this->expectExceptionMessage("too few bytes to be a complete report server id packet"); + $this->expectException(InvalidArgumentException::class); + + new ReportServerIDResponse( + "\x03\x01\x02\xFF", + 0x11, + 0x0138, + ); + } + + public function testOnPacketProperties() + { + $packet = new ReportServerIDResponse( + "\x02\x01\x02\xFF\x03\x04", + 0x11, + 0x0138, + ); + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode()); + + $this->assertEquals("\x01\x02", $packet->getServerID()); + $this->assertEquals([0x01, 0x02], $packet->getServerIDBytes()); + + $this->assertEquals(0xFF, $packet->getStatus()); + + $this->assertEquals("\x03\x04", $packet->getAdditionalData()); + $this->assertEquals([0x03, 0x04], $packet->getAdditionalDataBytes()); + + $header = $packet->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(8, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } + + public function testOnPacketPropertiesWithoutAdditionalData() + { + $packet = new ReportServerIDResponse( + "\x02\x01\x02\xFF", + 0x11, + 0x0138, + ); + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode()); + + $this->assertEquals("\x01\x02", $packet->getServerID()); + $this->assertEquals([0x01, 0x02], $packet->getServerIDBytes()); + + $this->assertEquals(0xFF, $packet->getStatus()); + + $this->assertEquals(null, $packet->getAdditionalData()); + $this->assertEquals([], $packet->getAdditionalDataBytes()); + + $header = $packet->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(6, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } + + public function testWithStartAddress() + { + $packet = new ReportServerIDResponse( + "\x02\x01\x02\xFF\x03\x04", + 0x11, + 0x0138, + ); + $packetWithStartAddress = $packet->withStartAddress(1); + + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packetWithStartAddress->getFunctionCode()); + $this->assertEquals("\x01\x02", $packet->getServerID()); + $this->assertEquals([0x01, 0x02], $packet->getServerIDBytes()); + + $this->assertEquals(0xFF, $packet->getStatus()); + + $this->assertEquals("\x03\x04", $packet->getAdditionalData()); + $this->assertEquals([0x03, 0x04], $packet->getAdditionalDataBytes()); + + + $header = $packetWithStartAddress->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(8, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } +} diff --git a/tests/unit/Packet/RequestFactoryTest.php b/tests/unit/Packet/RequestFactoryTest.php index bd536c2..f433ef9 100644 --- a/tests/unit/Packet/RequestFactoryTest.php +++ b/tests/unit/Packet/RequestFactoryTest.php @@ -13,6 +13,7 @@ use ModbusTcpClient\Packet\ModbusFunction\ReadInputDiscretesRequest; use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest; use ModbusTcpClient\Packet\ModbusFunction\ReadWriteMultipleRegistersRequest; +use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleCoilsRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest; use ModbusTcpClient\Packet\ModbusFunction\WriteSingleCoilRequest; @@ -37,6 +38,7 @@ public function everyFunctionTypePacket() "ok, parse WriteSingleRegisterRequest" => ["\x00\x01\x00\x00\x00\x06\x11\x06\x00\x6B\x01\x01", WriteSingleRegisterRequest::class], "ok, parse GetCommEventCounterRequest" => ["\x01\x38\x00\x00\x00\x02\x11\x0b", GetCommEventCounterRequest::class], "ok, parse MaskWriteRegisterRequest" => ["\x01\x38\x00\x00\x00\x08\x11\x16\x04\x10\x00\x01\x00\x02", MaskWriteRegisterRequest::class], + "ok, parse ReportServerIDRequest" => ["\x01\x38\x00\x00\x00\x02\x11\x11", ReportServerIDRequest::class], ]; } @@ -87,8 +89,8 @@ public function testShouldParseReadHoldingRegistersResponse() public function testInvalidFunctionCodeParse() { //trans + proto + len + uid + fc + addr + number of coils - //81 80 + 00 00 + 00 05 + 03 + 11 + 00 01 + 00 0A - $data = "\x81\x80\x00\x00\x00\x06\x03\x11\x00\x01\x00\x0A"; + //81 80 + 00 00 + 00 05 + 03 + 20 + 00 01 + 00 0A + $data = "\x81\x80\x00\x00\x00\x06\x03\x20\x00\x01\x00\x0A"; /** @var ErrorResponse $response */ $response = RequestFactory::parseRequest($data); @@ -114,8 +116,8 @@ public function testInvalidFunctionCodeParseWithThrow() $this->expectException(ModbusException::class); //trans + proto + len + uid + fc + addr + number of coils - //81 80 + 00 00 + 00 05 + 03 + 11 + 00 01 + 00 0A - $data = "\x81\x80\x00\x00\x00\x06\x03\x11\x00\x01\x00\x0A"; + //81 80 + 00 00 + 00 05 + 03 + 20 + 00 01 + 00 0A + $data = "\x81\x80\x00\x00\x00\x06\x03\x20\x00\x01\x00\x0A"; RequestFactory::parseRequestOrThrow($data); } diff --git a/tests/unit/Packet/ResponseFactoryTest.php b/tests/unit/Packet/ResponseFactoryTest.php index 814ad4f..828fb8d 100644 --- a/tests/unit/Packet/ResponseFactoryTest.php +++ b/tests/unit/Packet/ResponseFactoryTest.php @@ -291,14 +291,45 @@ public function testShouldParseGetCommEventCounterResponse() $this->assertEquals(0x11, $header->getUnitId()); } + public function testOnPacketProperties() + { + $data = "\x01\x38" . // transaction id: 0138 (2 bytes) + "\x00\x00" . // protocol id: 0000 (2 bytes) + "\x00\x08" . // length: 0008 (2 bytes) (8 bytes after this field) + "\x11" . // unit id: 11 (1 byte) + "\x11" . // function code: 0b (1 byte) + "\x02" . // server id byte count (1 bytes) + "\x01\x02" . // server id (0x0102) (N bytes) + "\xFF" . // status: FF (1 bytes) + "\x03\x04" . // additional data ( (optionally N bytes) + ''; + + $packet = ResponseFactory::parseResponse($data); + $this->assertEquals(ModbusPacket::REPORT_SERVER_ID, $packet->getFunctionCode()); + + $this->assertEquals("\x01\x02", $packet->getServerID()); + $this->assertEquals([0x01, 0x02], $packet->getServerIDBytes()); + + $this->assertEquals(0xFF, $packet->getStatus()); + + $this->assertEquals("\x03\x04", $packet->getAdditionalData()); + $this->assertEquals([0x03, 0x04], $packet->getAdditionalDataBytes()); + + $header = $packet->getHeader(); + $this->assertEquals(0x0138, $header->getTransactionId()); + $this->assertEquals(0, $header->getProtocolId()); + $this->assertEquals(8, $header->getLength()); + $this->assertEquals(0x11, $header->getUnitId()); + } + public function testInvalidFunctionCodeParse() { - $this->expectExceptionMessage("Unknown function code '17' read from response packet"); + $this->expectExceptionMessage("Unknown function code '32' read from response packet"); $this->expectException(ParseException::class); //trans + proto + len + uid + fc + addr + number of coils - //81 80 + 00 00 + 00 05 + 03 + 11 + 00 01 + 00 0A - $data = "\x81\x80\x00\x00\x00\x06\x03\x11\x00\x01\x00\x0A"; + //81 80 + 00 00 + 00 05 + 03 + 20 + 00 01 + 00 0A + $data = "\x81\x80\x00\x00\x00\x06\x03\x20\x00\x01\x00\x0A"; ResponseFactory::parseResponse($data); }