|
6 | 6 | use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersResponse;
|
7 | 7 | use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
|
8 | 8 | use ModbusTcpClient\Packet\ResponseFactory;
|
| 9 | +use ModbusTcpClient\Packet\RtuConverter; |
9 | 10 | use ModbusTcpClient\Utils\Endian;
|
| 11 | +use ModbusTcpClient\Utils\Packet; |
10 | 12 |
|
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 | +} |
12 | 28 |
|
13 | 29 | // if you want to let others specify their own ip/ports for querying data create file named '.allow-change' in this directory
|
14 | 30 | // NB: this is a potential security risk!!!
|
15 |
| -$canChangeIpPort = file_exists('.allow-change'); |
16 |
| - |
| 31 | +$canChangeIpPort = !$isSerialDevice && file_exists('.allow-change'); |
17 | 32 | $ip = '192.168.100.1';
|
18 | 33 | $port = 502;
|
19 | 34 | if ($canChangeIpPort) {
|
20 | 35 | $ip = filter_var($_GET['ip'] ?? '', FILTER_VALIDATE_IP) ? $_GET['ip'] : $ip;
|
21 | 36 | $port = (int)($_GET['port'] ?? $port);
|
22 | 37 | }
|
23 | 38 |
|
| 39 | +$returnJson = filter_var($_GET['json'] ?? false, FILTER_VALIDATE_BOOLEAN); |
| 40 | +$isRTU = $isSerialDevice || filter_var($_GET['rtu'] ?? false, FILTER_VALIDATE_BOOLEAN); |
24 | 41 | $fc = (int)($_GET['fc'] ?? 3);
|
25 | 42 | $unitId = (int)($_GET['unitid'] ?? 0);
|
26 | 43 | $startAddress = (int)($_GET['address'] ?? 256);
|
|
29 | 46 | Endian::$defaultEndian = $endianess;
|
30 | 47 |
|
31 | 48 | $log = [];
|
32 |
| -$log[] = "Using: function code: {$fc}, ip: {$ip}, port: {$port}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}"; |
33 | 49 |
|
34 |
| -$connection = BinaryStreamConnection::getBuilder() |
35 |
| - ->setPort($port) |
36 |
| - ->setHost($ip) |
| 50 | +$builder = BinaryStreamConnection::getBuilder() |
37 | 51 | ->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 |
41 | 54 |
|
| 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(); |
42 | 76 |
|
43 | 77 | if ($fc === 4) {
|
44 | 78 | $packet = new ReadInputRegistersRequest($startAddress, $quantity, $unitId);
|
45 | 79 | } else {
|
46 | 80 | $fc = 3;
|
47 | 81 | $packet = new ReadHoldingRegistersRequest($startAddress, $quantity, $unitId);
|
48 | 82 | }
|
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 | +} |
50 | 89 |
|
51 | 90 | $startTime = round(microtime(true) * 1000, 3);
|
52 | 91 | $result = [];
|
|
56 | 95 | $log[] = 'Binary received (in hex): ' . unpack('H*', $binaryData)[1];
|
57 | 96 |
|
58 | 97 | /** @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); |
60 | 104 |
|
61 | 105 | foreach ($response as $address => $word) {
|
62 | 106 | $doubleWord = isset($response[$address + 1]) ? $response->getDoubleWordAt($address) : null;
|
|
127 | 171 |
|
128 | 172 | ?>
|
129 | 173 |
|
130 |
| -<h2>Example Modbus TCP FC3/FC4 request</h2> |
| 174 | +<h2>Example Modbus TCP/RTU FC3/FC4 request</h2> |
131 | 175 | <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> |
132 | 188 | 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> |
135 | 197 | </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> |
141 | 213 | 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> |
146 | 230 | </select><br>
|
147 | 231 | <button type="submit">Send</button>
|
148 | 232 | </form>
|
|
0 commit comments