Skip to content

Commit

Permalink
Add support for FC17 Report server ID
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Jan 1, 2024
1 parent 9939eb8 commit 6704b45
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 9 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
63 changes: 63 additions & 0 deletions src/Packet/ModbusFunction/ReportServerIDRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace ModbusTcpClient\Packet\ModbusFunction;

use ModbusTcpClient\Packet\ErrorResponse;
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Packet\ModbusRequest;
use ModbusTcpClient\Packet\ProtocolDataUnit;
use ModbusTcpClient\Utils\Types;

/**
* Request for Report Server ID (FC=17, 0x11)
*
* Example packet: \x81\x80\x00\x00\x00\x02\x10\x11
* \x81\x80 - transaction id
* \x00\x00 - protocol id
* \x00\x08 - number of bytes in the message (PDU = ProtocolDataUnit) to follow
* \x10 - unit id
* \x11 - function code
*
*/
class ReportServerIDRequest extends ProtocolDataUnit implements ModbusRequest
{
public function __construct(int $unitId = 0, int $transactionId = null)
{
parent::__construct($unitId, $transactionId);
}

public function getFunctionCode(): int
{
return ModbusPacket::REPORT_SERVER_ID; // 17 (0x11)
}

protected function getLengthInternal(): int
{
return 1; // size of function code (1 byte)
}

public function __toString(): string
{
return b''
. $this->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);
}
);
}
}
138 changes: 138 additions & 0 deletions src/Packet/ModbusFunction/ReportServerIDResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace ModbusTcpClient\Packet\ModbusFunction;

use ModbusTcpClient\Exception\InvalidArgumentException;
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Packet\ModbusResponse;
use ModbusTcpClient\Packet\ProtocolDataUnit;
use ModbusTcpClient\Utils\Types;

/**
* Response for Report Server ID (FC=17, 0x11)
*
* Example packet: \x81\x80\x00\x00\x00\x08\x10\x11\x02\x01\x02\x00\x01\x02
* \x81\x80 - transaction id
* \x00\x00 - protocol id
* \x00\x08 - number of bytes in the message (PDU = ProtocolDataUnit) to follow
* \x10 - unit id
* \x11 - function code
* \x02 - byte count for server id
* \x01\x02 - N bytes for server id (device specific, variable length)
* \x00 - run status
* \x01\x02 - optional N bytes for additional data (device specific, variable length)
*
*/
class ReportServerIDResponse extends ProtocolDataUnit implements ModbusResponse
{
/**
* @var string server ID bytes as binary string
*/
private string $serverID;

/**
* @var int run status
*/
private int $status;

/**
* @var string|null additional data (optional)
*/
private ?string $additionalData = null;

public function __construct(string $rawData, int $unitId = 0, int $transactionId = null)
{
$serverIDLength = Types::parseByte($rawData[0]);
if (strlen($rawData) < ($serverIDLength + 2)) {
throw new InvalidArgumentException("too few bytes to be a complete report server id packet");
}
$this->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;
}
}
1 change: 1 addition & 0 deletions src/Packet/ModbusPacket.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/Packet/RequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/Packet/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions tests/unit/Packet/ModbusFunction/ReportServerIDRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Tests\unit\Packet\ModbusFunction;

use ModbusTcpClient\Packet\ErrorResponse;
use ModbusTcpClient\Packet\ModbusFunction\ReportServerIDRequest;
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Utils\Endian;
use PHPUnit\Framework\TestCase;

class ReportServerIDRequestTest extends TestCase
{
protected function setUp(): void
{
Endian::$defaultEndian = Endian::LITTLE_ENDIAN; // packets are big endian. setting to default to little should not change output
}

protected function tearDown(): void
{
Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
}

public function testOnPacketToString()
{
// 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)
'';
$this->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);
}
}
Loading

0 comments on commit 6704b45

Please sign in to comment.