Skip to content

Commit 8ccb744

Browse files
committed
fix setting endian before creating request will create invalid request
add ability to set delay between sending the request and receiving request (helps with serial devices) add support for serial devices in examples/index.php
1 parent 09bd467 commit 8ccb744

23 files changed

+224
-43
lines changed

examples/index.php

+108-24
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,38 @@
66
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersResponse;
77
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
88
use ModbusTcpClient\Packet\ResponseFactory;
9+
use ModbusTcpClient\Packet\RtuConverter;
910
use ModbusTcpClient\Utils\Endian;
11+
use ModbusTcpClient\Utils\Packet;
1012

11-
$returnJson = filter_var($_GET['json'] ?? false, FILTER_VALIDATE_BOOLEAN);
13+
// To allow Nginx/Apache to read that device add following udev rule
14+
// echo 'KERNEL=="ttyUSB0", GROUP="www-data", MODE="0660"' | sudo tee /etc/udev/rules.d/60-ttyusb-acl.rules
15+
// sudo udevadm control --reload-rules && sudo udevadm trigger
16+
$deviceURI = '/dev/ttyUSB0'; // do not make this changeable from WEB. This could be serious security risk.
17+
$isSerialDevice = false; // change to true to enable reading serial devices. this will disable ip/port logic and uses RTU
18+
if (getenv('MODBUS_SERIAL_ENABLED')) { // can be set from Nginx/Apache fast-cgi conf
19+
$isSerialDevice = filter_var(getenv('MODBUS_SERIAL_ENABLED'), FILTER_VALIDATE_BOOLEAN);
20+
if ($isSerialDevice && getenv('MODBUS_SERIAL_DEVICE')) {
21+
$deviceURI = getenv('MODBUS_SERIAL_DEVICE');
22+
}
23+
}
24+
if ($isSerialDevice && stripos(PHP_OS, 'WIN') === 0) {
25+
echo 'Serial usb example can not be run on Windows!' . PHP_EOL;
26+
exit(0);
27+
}
1228

1329
// if you want to let others specify their own ip/ports for querying data create file named '.allow-change' in this directory
1430
// NB: this is a potential security risk!!!
15-
$canChangeIpPort = file_exists('.allow-change');
16-
31+
$canChangeIpPort = !$isSerialDevice && file_exists('.allow-change');
1732
$ip = '192.168.100.1';
1833
$port = 502;
1934
if ($canChangeIpPort) {
2035
$ip = filter_var($_GET['ip'] ?? '', FILTER_VALIDATE_IP) ? $_GET['ip'] : $ip;
2136
$port = (int)($_GET['port'] ?? $port);
2237
}
2338

39+
$returnJson = filter_var($_GET['json'] ?? false, FILTER_VALIDATE_BOOLEAN);
40+
$isRTU = $isSerialDevice || filter_var($_GET['rtu'] ?? false, FILTER_VALIDATE_BOOLEAN);
2441
$fc = (int)($_GET['fc'] ?? 3);
2542
$unitId = (int)($_GET['unitid'] ?? 0);
2643
$startAddress = (int)($_GET['address'] ?? 256);
@@ -29,24 +46,46 @@
2946
Endian::$defaultEndian = $endianess;
3047

3148
$log = [];
32-
$log[] = "Using: function code: {$fc}, ip: {$ip}, port: {$port}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";
3349

34-
$connection = BinaryStreamConnection::getBuilder()
35-
->setPort($port)
36-
->setHost($ip)
50+
$builder = BinaryStreamConnection::getBuilder()
3751
->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
38-
->setWriteTimeoutSec(0.5) // timeout when writing/sending packet to the server
39-
->setReadTimeoutSec(1.0) // timeout when waiting response from server
40-
->build();
52+
->setWriteTimeoutSec(1.0) // timeout when writing/sending packet to the server
53+
->setReadTimeoutSec(1.0); // timeout when waiting response from server
4154

55+
$protocolType = "Modbus TCP";
56+
if ($isRTU) {
57+
$protocolType = "Modbus RTU";
58+
$builder->setIsCompleteCallback(static function ($binaryData, $streamIndex): bool {
59+
return Packet::isCompleteLengthRTU($binaryData);
60+
});
61+
}
62+
63+
if ($isSerialDevice) {
64+
$log[] = "Using: {$protocolType} function code: {$fc}, device: {$deviceURI}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";
65+
$builder->setUri($deviceURI)
66+
->setProtocol('serial')
67+
// delay this is crucial for some serial devices and delay needs to be long as 100ms (depending on the quantity)
68+
// or you will experience read errors ("stream_select interrupted") or invalid CRCs
69+
->setDelayRead(100_000); // 100 milliseconds
70+
} else {
71+
$log[] = "Using: {$protocolType} function code: {$fc}, ip: {$ip}, port: {$port}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";
72+
$builder->setPort($port)->setHost($ip);
73+
}
74+
75+
$connection = $builder->build();
4276

4377
if ($fc === 4) {
4478
$packet = new ReadInputRegistersRequest($startAddress, $quantity, $unitId);
4579
} else {
4680
$fc = 3;
4781
$packet = new ReadHoldingRegistersRequest($startAddress, $quantity, $unitId);
4882
}
49-
$log[] = 'Packet to be sent (in hex): ' . $packet->toHex();
83+
if ($isRTU) {
84+
$packet = RtuConverter::toRtu($packet);
85+
$log[] = 'Modbus RTU Packet to be sent (in hex): ' . unpack('H*', $packet)[1];
86+
} else {
87+
$log[] = 'Modbus TCP Packet to be sent (in hex): ' . $packet->toHex();
88+
}
5089

5190
$startTime = round(microtime(true) * 1000, 3);
5291
$result = [];
@@ -56,7 +95,12 @@
5695
$log[] = 'Binary received (in hex): ' . unpack('H*', $binaryData)[1];
5796

5897
/** @var $response ReadHoldingRegistersResponse */
59-
$response = ResponseFactory::parseResponseOrThrow($binaryData)->withStartAddress($startAddress);
98+
if ($isRTU) {
99+
$response = RtuConverter::fromRtuOrThrow($binaryData);
100+
} else {
101+
$response = ResponseFactory::parseResponseOrThrow($binaryData);
102+
}
103+
$response = $response->withStartAddress($startAddress);
60104

61105
foreach ($response as $address => $word) {
62106
$doubleWord = isset($response[$address + 1]) ? $response->getDoubleWordAt($address) : null;
@@ -127,22 +171,62 @@
127171

128172
?>
129173

130-
<h2>Example Modbus TCP FC3/FC4 request</h2>
174+
<h2>Example Modbus TCP/RTU FC3/FC4 request</h2>
131175
<form>
176+
Modbus TCP or RTU: <select name="rtu"<?php if ($isSerialDevice) {
177+
echo ' disabled';
178+
} ?>>
179+
<option value="0" <?php if (!$isRTU) {
180+
echo 'selected';
181+
} ?>>Modbus TCP
182+
</option>
183+
<option value="1" <?php if ($isRTU) {
184+
echo 'selected';
185+
} ?>>Modbus RTU
186+
</option>
187+
</select><br>
132188
Function code: <select name="fc">
133-
<option value="3" <?php if ($fc === 3) { echo 'selected'; } ?>>Read Holding Registers (FC=03)</option>
134-
<option value="4" <?php if ($fc === 4) { echo 'selected'; } ?>>Read Input Registers (FC=04)</option>
189+
<option value="3" <?php if ($fc === 3) {
190+
echo 'selected';
191+
} ?>>Read Holding Registers (FC=03)
192+
</option>
193+
<option value="4" <?php if ($fc === 4) {
194+
echo 'selected';
195+
} ?>>Read Input Registers (FC=04)
196+
</option>
135197
</select><br>
136-
IP: <input type="text" name="ip" value="<?php echo $ip; ?>" <?php if (!$canChangeIpPort) { echo 'disabled'; } ?>><br>
137-
Port: <input type="number" name="port" value="<?php echo $port; ?>"><br>
138-
UnitID (SlaveID): <input type="number" name="unitid" value="<?php echo $unitId; ?>"><br>
139-
Address: <input type="number" name="address" value="<?php echo $startAddress; ?>"> (NB: does your modbus server use `0` based addressing or `1` based?)<br>
140-
Quantity: <input type="number" name="quantity" value="<?php echo $quantity; ?>"><br>
198+
<?php if ($isSerialDevice) {
199+
echo "Device: {$deviceURI}<br>";
200+
} else {
201+
echo "IP: <input type=\"text\" name=\"ip\" value=\"{$ip}\"";
202+
if (!$canChangeIpPort) {
203+
echo ' disabled';
204+
}
205+
echo "><br>";
206+
echo "Port: <input type=\"number\" name=\"port\" value=\"{$port}\"><br>";
207+
} ?>
208+
UnitID (SlaveID): <input type="number" min="0" max="247" name="unitid" value="<?php echo $unitId; ?>"><br>
209+
Address: <input type="number" name="address" value="<?php echo $startAddress; ?>"> (NB: does your modbus server
210+
documentation uses
211+
`0` based addressing or `1` based?)<br>
212+
Quantity: <input type="number" min="1" max="124" name="quantity" value="<?php echo $quantity; ?>"><br>
141213
Endianess: <select name="endianess">
142-
<option value="1" <?php if ($endianess === 1) { echo 'selected'; } ?>>BIG_ENDIAN</option>
143-
<option value="5" <?php if ($endianess === 5) { echo 'selected'; } ?>>BIG_ENDIAN_LOW_WORD_FIRST</option>
144-
<option value="2" <?php if ($endianess === 2) { echo 'selected'; } ?>>LITTLE_ENDIAN</option>
145-
<option value="6" <?php if ($endianess === 6) { echo 'selected'; } ?>>LITTLE_ENDIAN_LOW_WORD_FIRST</option>
214+
<option value="1" <?php if ($endianess === 1) {
215+
echo 'selected';
216+
} ?>>BIG_ENDIAN
217+
</option>
218+
<option value="5" <?php if ($endianess === 5) {
219+
echo 'selected';
220+
} ?>>BIG_ENDIAN_LOW_WORD_FIRST
221+
</option>
222+
<option value="2" <?php if ($endianess === 2) {
223+
echo 'selected';
224+
} ?>>LITTLE_ENDIAN
225+
</option>
226+
<option value="6" <?php if ($endianess === 6) {
227+
echo 'selected';
228+
} ?>>LITTLE_ENDIAN_LOW_WORD_FIRST
229+
</option>
146230
</select><br>
147231
<button type="submit">Send</button>
148232
</form>

examples/rtu_usb_to_serial.php

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
$sttyModes = implode(' ', [
2626
'cs8', // enable character size 8 bits
2727
'9600', // enable baud rate 9600
28+
'-cstopb', // 1 stop bit
29+
'-parenb', // parity none
30+
2831
'-icanon', // disable enable special characters: erase, kill, werase, rprnt
2932
'min 0', // with -icanon, set N characters minimum for a completed read
3033
'ignbrk', // enable ignore break characters
@@ -59,6 +62,8 @@
5962

6063
do {
6164
// give sensor (5ms) some time to respond. SHT20 modbus minimal response time seems to be 20ms and more
65+
// this is crucial for some serial devices and delay needs to be even longer (100ms) or you will experience
66+
// read errors or invalid CRCs
6267
usleep(5000);
6368
$binaryData = fread($fd, 255);
6469
} while ($binaryData === '');

examples/rtu_usb_to_serial_stream.php

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
->setIsCompleteCallback(static function ($binaryData, $streamIndex): bool {
1616
return Packet::isCompleteLengthRTU($binaryData);
1717
})
18+
// delay this is crucial for some serial devices and delay needs to be long as 100ms (depending on the quantity)
19+
// or you will experience read errors ("stream_select interrupted") or invalid CRCs
20+
->setDelayRead(100_000) // 100 milliseconds, serial devices may need delay between sending and received
1821
->setLogger(new EchoLogger())
1922
->build();
2023

src/Network/BinaryStreamConnection.php

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function __construct(BinaryStreamConnectionBuilder $builder)
2525
$this->connectTimeoutSec = $builder->getConnectTimeoutSec();
2626
$this->readTimeoutSec = $builder->getReadTimeoutSec();
2727
$this->writeTimeoutSec = $builder->getWriteTimeoutSec();
28+
$this->delayRead = $builder->getDelayRead();
2829
$this->protocol = $builder->getProtocol();
2930
$this->logger = $builder->getLogger();
3031
$this->createStreamCallback = $builder->getCreateStreamCallback();
@@ -57,6 +58,12 @@ public function connect(): BinaryStreamConnection
5758

5859
public function receive(): string
5960
{
61+
$delay = $this->getDelayRead();
62+
if ($delay > 0) {
63+
// this is useful slow serial devices that need delay between writing request to the serial device
64+
// and receiving response from device.
65+
usleep($delay);
66+
}
6067
$result = $this->receiveFrom([$this->stream], $this->getReadTimeoutSec(), $this->getLogger());
6168
return reset($result);
6269
}

src/Network/BinaryStreamConnectionBuilder.php

+11
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ public function setWriteTimeoutSec(float $writeTimeoutSec): static
102102
return $this;
103103
}
104104

105+
/**
106+
* @param int $delayReadMicroSec delay before read in done (microseconds). This is useful for (USB) Serial
107+
* devices that need time between writing request to the device and reading the response from device.
108+
* @return static
109+
*/
110+
public function setDelayRead(int $delayReadMicroSec): static
111+
{
112+
$this->delayRead = $delayReadMicroSec;
113+
return $this;
114+
}
115+
105116
/**
106117
* @param string $protocol
107118
* @return static

src/Network/BinaryStreamConnectionProperties.php

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ abstract class BinaryStreamConnectionProperties
3737
*/
3838
protected float $writeTimeoutSec = 1;
3939

40+
/**
41+
* @var int delay before read in done (microseconds). This is useful for (USB) Serial devices that need time between
42+
* writing the request to the device and reading the response from device.
43+
*/
44+
protected int $delayRead = 0;
45+
4046
/**
4147
* @var string|null uri to connect to. Has higher priority than $protocol/$host/$port. Example: 'tcp://192.168.0.1:502'
4248
*/
@@ -165,6 +171,14 @@ public function getCreateStreamCallback(): callable
165171
return $this->createStreamCallback;
166172
}
167173

174+
/**
175+
* @return int
176+
*/
177+
public function getDelayRead(): int
178+
{
179+
return $this->delayRead;
180+
}
181+
168182
/**
169183
* @return callable
170184
*/

src/Network/SerialStreamCreator.php

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class SerialStreamCreator implements StreamCreator
1414
const DEFAULT_STTY_MODES = [
1515
'cs8', // set character size 8 bits
1616
'9600', // set baud rate 9600
17+
'-cstopb', // 1 stop bit
18+
'-parenb', // parity none
19+
1720
'-icanon', // disable enable special characters: erase, kill, werase, rprnt
1821
'min 0', // with -icanon, set N characters minimum for a completed read
1922
'ignbrk', // enable ignore break characters

src/Packet/ModbusFunction/MaskWriteRegisterRequest.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use ModbusTcpClient\Packet\ModbusRequest;
1111
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
1212
use ModbusTcpClient\Packet\Word;
13+
use ModbusTcpClient\Utils\Endian;
1314
use ModbusTcpClient\Utils\Types;
1415

1516
/**
@@ -121,8 +122,8 @@ public static function parse(string $binaryString): MaskWriteRegisterRequest|Err
121122
14,
122123
ModbusPacket::MASK_WRITE_REGISTER,
123124
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
124-
$andMask = Types::parseInt16($binaryString[10] . $binaryString[11]);
125-
$orMask = Types::parseInt16($binaryString[12] . $binaryString[13]);
125+
$andMask = Types::parseInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
126+
$orMask = Types::parseInt16($binaryString[12] . $binaryString[13], Endian::BIG_ENDIAN);
126127
return new self($startAddress, $andMask, $orMask, $unitId, $transactionId);
127128
}
128129
);

src/Packet/ModbusFunction/MaskWriteRegisterResponse.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ModbusTcpClient\Packet\ModbusPacket;
77
use ModbusTcpClient\Packet\StartAddressResponse;
88
use ModbusTcpClient\Packet\Word;
9+
use ModbusTcpClient\Utils\Endian;
910
use ModbusTcpClient\Utils\Types;
1011

1112
/**
@@ -37,8 +38,8 @@ class MaskWriteRegisterResponse extends StartAddressResponse
3738
public function __construct(string $rawData, int $unitId = 0, int $transactionId = null)
3839
{
3940
parent::__construct($rawData, $unitId, $transactionId);
40-
$this->andMask = Types::parseUInt16(substr($rawData, 2, 2));
41-
$this->orMask = Types::parseUInt16(substr($rawData, 4, 2));
41+
$this->andMask = Types::parseUInt16(substr($rawData, 2, 2), Endian::BIG_ENDIAN);
42+
$this->orMask = Types::parseUInt16(substr($rawData, 4, 2), Endian::BIG_ENDIAN);
4243
}
4344

4445
public function getFunctionCode(): int

src/Packet/ModbusFunction/ReadCoilsRequest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use ModbusTcpClient\Packet\ModbusPacket;
1010
use ModbusTcpClient\Packet\ModbusRequest;
1111
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
12+
use ModbusTcpClient\Utils\Endian;
1213
use ModbusTcpClient\Utils\Types;
1314

1415
/**
@@ -87,7 +88,7 @@ public static function parse(string $binaryString): ErrorResponse|ReadCoilsReque
8788
12,
8889
ModbusPacket::READ_COILS,
8990
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
90-
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
91+
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
9192
return new self($startAddress, $quantity, $unitId, $transactionId);
9293
}
9394
);

src/Packet/ModbusFunction/ReadHoldingRegistersRequest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use ModbusTcpClient\Packet\ModbusPacket;
99
use ModbusTcpClient\Packet\ModbusRequest;
1010
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
11+
use ModbusTcpClient\Utils\Endian;
1112
use ModbusTcpClient\Utils\Types;
1213

1314
/**
@@ -86,7 +87,7 @@ public static function parse(string $binaryString): ErrorResponse|ReadHoldingReg
8687
12,
8788
ModbusPacket::READ_HOLDING_REGISTERS,
8889
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
89-
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
90+
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
9091
return new self($startAddress, $quantity, $unitId, $transactionId);
9192
}
9293
);

src/Packet/ModbusFunction/ReadInputDiscretesRequest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use ModbusTcpClient\Packet\ErrorResponse;
77
use ModbusTcpClient\Packet\ModbusPacket;
8+
use ModbusTcpClient\Utils\Endian;
89
use ModbusTcpClient\Utils\Types;
910

1011
/**
@@ -40,7 +41,7 @@ public static function parse(string $binaryString): ReadInputDiscretesRequest|Er
4041
12,
4142
ModbusPacket::READ_INPUT_DISCRETES,
4243
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
43-
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
44+
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
4445
return new self($startAddress, $quantity, $unitId, $transactionId);
4546
}
4647
);

src/Packet/ModbusFunction/ReadInputRegistersRequest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use ModbusTcpClient\Packet\ErrorResponse;
77
use ModbusTcpClient\Packet\ModbusPacket;
8+
use ModbusTcpClient\Utils\Endian;
89
use ModbusTcpClient\Utils\Types;
910

1011
/**
@@ -40,7 +41,7 @@ public static function parse(string $binaryString): ReadInputRegistersRequest|Er
4041
12,
4142
ModbusPacket::READ_INPUT_REGISTERS,
4243
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
43-
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
44+
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
4445
return new self($startAddress, $quantity, $unitId, $transactionId);
4546
}
4647
);

0 commit comments

Comments
 (0)