Skip to content

Commit b0327cf

Browse files
committed
Added Packet::isCompleteLengthRTU() to help checking if packet is complete RTU packet
1 parent 39f874a commit b0327cf

File tree

6 files changed

+123
-12
lines changed

6 files changed

+123
-12
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.1.0] - 2022-10-16
8+
9+
* Added `Packet::isCompleteLengthRTU()` to help checking if packet is complete RTU packet. Helps when receiving fragmented packets.
10+
* Example for RTU over TCP with higher level API [examples/rtu_over_tcp_with_higherlevel_api.php](examples/rtu_over_tcp_with_higherlevel_api.php)
11+
712
## [3.0.1] - 2022-09-29
813

914
* `ResultContainer.offsetGet` was missing return type

examples/rtu.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use ModbusTcpClient\Network\BinaryStreamConnection;
44
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;
55
use ModbusTcpClient\Packet\RtuConverter;
6+
use ModbusTcpClient\Utils\Packet;
67

78
require __DIR__ . '/../vendor/autoload.php';
89
require __DIR__ . '/logger.php';
@@ -12,11 +13,7 @@
1213
->setHost('127.0.0.1')
1314
->setReadTimeoutSec(3) // increase read timeout to 3 seconds
1415
->setIsCompleteCallback(function ($binaryData, $streamIndex) {
15-
// Do not check for complete TCP packet structure. Default implementation works only for Modbus TCP.
16-
// Modbus TCP has 7 byte header and this function checks for it and whole packet to be complete. RTU does
17-
// not have that.
18-
// Read about differences here: https://www.simplymodbus.ca/TCP.htm
19-
return true;
16+
return Packet::isCompleteLengthRTU($binaryData);
2017
})
2118
->setLogger(new EchoLogger())
2219
->build();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
use ModbusTcpClient\Composer\Read\ReadRegistersBuilder;
4+
use ModbusTcpClient\Composer\Read\Register\ReadRegisterRequest;
5+
use ModbusTcpClient\Network\BinaryStreamConnection;
6+
use ModbusTcpClient\Packet\RtuConverter;
7+
use ModbusTcpClient\Utils\Packet;
8+
9+
require __DIR__ . '/../vendor/autoload.php';
10+
require __DIR__ . '/logger.php';
11+
12+
// Modbus server simulator https://www.modbusdriver.com/diagslave.html
13+
// Start simulator with `./diagslave -m enc -a 1 -p 5020`
14+
15+
$connection = BinaryStreamConnection::getBuilder()
16+
->setPort(5020)
17+
->setHost('127.0.0.1')
18+
->setReadTimeoutSec(0.5)
19+
->setIsCompleteCallback(function ($binaryData, $streamIndex) {
20+
return Packet::isCompleteLengthRTU($binaryData);
21+
})
22+
->setLogger(new EchoLogger())
23+
->build();
24+
25+
$unitID = 1; // RTU packet slave id equivalent is Modbus TCP unitId
26+
$fc3requests = ReadRegistersBuilder::newReadHoldingRegisters('no_address', $unitID) // uri/address does not matter because we use $connection
27+
->int16(1, 'address1_value') // or whatever data type that value is in that register
28+
->uint16(2, 'address2_value')
29+
// See `ReadRegistersBuilder.php` for available data type methods
30+
->build(); // returns array of ReadHoldingRegistersRequest requests
31+
32+
try {
33+
/** @var $request ReadRegisterRequest */
34+
foreach ($fc3requests as $request) {
35+
echo 'Packet to be sent (in hex): ' . $request->getRequest()->toHex() . PHP_EOL;
36+
$rtuPacket = RtuConverter::toRtu($request->getRequest());
37+
38+
$binaryData = $connection->connect()->sendAndReceive($rtuPacket);
39+
echo 'RTU Binary received (in hex): ' . unpack('H*', $binaryData)[1] . PHP_EOL;
40+
41+
$tcpResponsePacket = RtuConverter::fromRtu($binaryData);
42+
43+
echo 'Data parsed from packet (bytes):' . PHP_EOL;
44+
$result = $request->parse($tcpResponsePacket);
45+
print_r($result);
46+
}
47+
} catch (Exception $exception) {
48+
echo 'An exception occurred' . PHP_EOL;
49+
echo $exception->getMessage() . PHP_EOL;
50+
echo $exception->getTraceAsString() . PHP_EOL;
51+
} finally {
52+
$connection->close();
53+
}

examples/rtu_usb_to_serial_stream.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
55
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersResponse;
66
use ModbusTcpClient\Packet\RtuConverter;
7+
use ModbusTcpClient\Utils\Packet;
78

89
require __DIR__ . '/../vendor/autoload.php';
910
require __DIR__ . '/logger.php';
@@ -12,11 +13,7 @@
1213
->setUri('/dev/ttyUSB0')
1314
->setProtocol('serial')
1415
->setIsCompleteCallback(static function ($binaryData, $streamIndex): bool {
15-
// NB: returning always true could lead to problems with reading fragmented packets (so we returning too early)
16-
// safest way would be to calculate bytes length that we expect and error response length.
17-
// Example for error: 5 bytes = 1 unit id + 1 byte for function code + 1 byte for error code + 2 bytes for CRC
18-
// Example for FC4 with quantity 2: 8 bytes = 1 unit id + 1 byte for function code + 2 bytes start address + 2 * quantity
19-
return true;
16+
return Packet::isCompleteLengthRTU($binaryData);
2017
})
2118
->setLogger(new EchoLogger())
2219
->build();

src/Utils/Packet.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
use ModbusTcpClient\Network\IOException;
8+
use ModbusTcpClient\Packet\ErrorResponse;
89

910
final class Packet
1011
{
@@ -14,11 +15,11 @@ private function __construct()
1415
}
1516

1617
/**
17-
* isCompleteLength checks if binary string is complete modbus packet
18+
* isCompleteLength checks if binary string is complete modbus TCP packet
1819
* NB: this function works only for MODBUS TCP packets
1920
*
2021
* @param string|null $binaryData binary string to be checked
21-
* @return bool true if data is actual error packet
22+
* @return bool true if data is actual modbus TCP packet
2223
*/
2324
public static function isCompleteLength(string|null $binaryData): bool
2425
{
@@ -37,4 +38,35 @@ public static function isCompleteLength(string|null $binaryData): bool
3738
return $length === $expectedLength;
3839
}
3940

41+
/**
42+
* isCompleteLengthRTU checks if binary string is complete modbus RTU packet
43+
* NB: this function works only for MODBUS RTU packets
44+
*
45+
* @param string|null $binaryData binary string to be checked
46+
* @return bool true if data is actual error packet
47+
*/
48+
public static function isCompleteLengthRTU(string|null $binaryData): bool
49+
{
50+
// minimal RTU packet length is 5 bytes (1 byte unit id + 1 byte function code + 1 byte of error code or byte length + 2 bytes for CRC)
51+
$length = strlen($binaryData);
52+
if ($length < 5) {
53+
return false;
54+
}
55+
if ((ord($binaryData[1]) & ErrorResponse::EXCEPTION_BITMASK) > 0) { // seems to be error response
56+
return true;
57+
}
58+
// if it is not error response then 3rd byte contains data length in bytes
59+
60+
// trailing 3 bytes are = unit id + function code + data length in bytes
61+
// next is N bytes of data that should match 3rd byte value
62+
// and 2 bytes for CRC
63+
// so adding these number is what complete packet would be
64+
$expectedLength = 3 + ord($binaryData[2]) + 2;
65+
66+
if ($length > $expectedLength) {
67+
throw new IOException('packet length more bytes than expected');
68+
}
69+
return $length === $expectedLength;
70+
}
71+
4072
}

tests/unit/Utils/PacketTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,31 @@ public function testIsCompleteLengthTooLong()
3737
Packet::isCompleteLength("\x81\x80\x00\x00\x00\x05\x03\x03\x02\xCD\x6B\xFF");
3838
}
3939

40+
public function isCompleteLengthRTUProvider(): array
41+
{
42+
return [
43+
'complete error packet is complete' => ["\x00\x81\x03\x51\x91", true],
44+
'short incomplete error packet is not complete' => ["\x00\x81\x03\x51", false],
45+
'read holding registers (f3) response packet is complete' => ["\x01\x03\x04\x00\x00\x00\x00\xfa\x33", true],
46+
'incomplete read holding registers (f3) response packet is not complete' => ["\x01\x03\x04\x00\x00\x00\x00", false],
47+
];
48+
}
49+
50+
/**
51+
* @dataProvider isCompleteLengthRTUProvider
52+
*/
53+
public function testIsCompleteLengthRTUh($binaryData, $expect)
54+
{
55+
$is = Packet::isCompleteLengthRTU($binaryData);
56+
$this->assertEquals($expect, $is);
57+
}
58+
59+
public function testIsCompleteLengthRTUTooLong()
60+
{
61+
$this->expectExceptionMessage("packet length more bytes than expected");
62+
$this->expectException(IOException::class);
63+
64+
Packet::isCompleteLengthRTU("\x01\x03\x04\x00\x00\x00\x00\xfa\x33\xFF");
65+
}
66+
4067
}

0 commit comments

Comments
 (0)