aldas / modbus-tcp-client
Modbus TCP protocol client library
Installs: 42 812
Dependents: 0
Suggesters: 0
Security: 0
Stars: 145
Watchers: 15
Forks: 45
Open Issues: 2
Requires
- php: ^8.0
- ext-mbstring: *
Requires (Dev)
- phpstan/phpstan: ^1.5
- phpunit/phpunit: ^9.0
- psr/log: ^1.0
- react/child-process: ^0.6
- react/datagram: ^1.8
- react/socket: ^1.10
Suggests
- psr/log: Required for using the Log middleware with BinaryStreamConnection
README
- Modbus TCP/IP specification: http://www.modbus.org/specs.php
- Modbus TCP/IP and RTU simpler description: http://www.simplymodbus.ca/TCP.htm
Installation
Use Composer to install this library as dependency.
composer require aldas/modbus-tcp-client
Supported functions
- FC1 - Read Coils (ReadCoilsRequest / ReadCoilsResponse)
- FC2 - Read Input Discretes (ReadInputDiscretesRequest / ReadInputDiscretesResponse)
- FC3 - Read Holding Registers (ReadHoldingRegistersRequest / ReadHoldingRegistersResponse)
- FC4 - Read Input Registers (ReadInputRegistersRequest / ReadInputRegistersResponse)
- FC5 - Write Single Coil (WriteSingleCoilRequest / WriteSingleCoilResponse)
- FC6 - Write Single Register (WriteSingleRegisterRequest / WriteSingleRegisterResponse)
- FC15 - Write Multiple Coils (WriteMultipleCoilsRequest / WriteMultipleCoilsResponse)
- FC16 - Write Multiple Registers (WriteMultipleRegistersRequest / WriteMultipleRegistersResponse)
- FC22 - Mask Write Register (MaskWriteRegisterRequest / MaskWriteRegisterResponse)
- FC23 - Read / Write Multiple Registers (ReadWriteMultipleRegistersRequest / ReadWriteMultipleRegistersResponse)
Utility functions
- Packet::isCompleteLength - checks if data is complete Modbus TCP packet
- Packet::isCompleteLengthRTU() - checks if data is complete Modbus RTU packet
- ErrorResponse::is - checks if data is Modbus TCP error packet
Requirements
- PHP 8.0+
- Release 2.4.0 was last to support PHP 7 (7.4 might work with v3.0.0)
- Release 0.2.0 was last to support PHP 5.6
Intention
This library is influenced by phpmodbus library and meant to be provide decoupled Modbus protocol (request/response packets) and networking related features so you could build modbus client with our own choice of networking code (ext_sockets/streams/Reactphp/Amp asynchronous streams) or use library provided networking classes (php Streams)
Endianness
Applies to multibyte data that are stored in Word/Double/Quad word registers basically everything that is not (u)int16/byte/char.
So if we receive from network 0x12345678 (bytes: ABCD) and want to convert that to a 32 bit register there could be 4 different ways to interpret bytes and word order depending on modbus server architecture and client architecture. NB: TCP, and UDP, are transmitted in big-endian order so we choose this as base for examples
Library supports following byte and word orders:
- Big endian (ABCD - word1 = 0x1234, word2 = 0x5678)
- Big endian low word first (CDAB - word1 = 0x5678, word2 = 0x1234) (used by Wago-750)
- Little endian (DCBA - word1 = 0x3412, word2 = 0x7856)
- Little endian low word first (BADC - word1 = 0x7856, word2 = 0x3412)
Default (global) endianess used for parsing can be changed with:
Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
For non-global cases see API methods argument list if method support using custom endianess.
See Endian.php for additional info and Types.php for supported data types.
Example of Modbus TCP (fc3 - read holding registers)
Some of the Modbus function examples are in examples/ folder
Advanced usage:
- command line poller with ReachPHP examples/example_cli_poller.php
- send/recieve packets parallel using non-blocking IO:
- using ReactPHP see 'examples/example_parallel_requests_reactphp.php'
- using Amp see 'examples/example_parallel_requests_amp.php'
Request multiple packets with higher level API:
$address = 'tcp://127.0.0.1:5022'; $unitID = 0; // also known as 'slave ID' $fc3 = ReadRegistersBuilder::newReadHoldingRegisters($address, $unitID) ->bit(256, 15, 'pump2_feedbackalarm_do') // will be split into 2 requests as 1 request can return only range of 124 registers max ->int16(657, 'battery3_voltage_wo') // will be another request as uri is different for subsequent int16 register ->useUri('tcp://127.0.0.1:5023') ->string( 669, 10, 'username_plc2', function ($value, $address, $response) { return 'prefix_' . $value; // optional: transform value after extraction }, function (\Exception $exception, Address $address, $response) { // optional: callback called then extraction failed with an error return $address->getType() === Address::TYPE_STRING ? '' : null; // does not make sense but gives you an idea } ) ->build(); // returns array of 3 ReadHoldingRegistersRequest requests // this will use PHP non-blocking stream io to recieve responses $responseContainer = (new NonBlockingClient(['readTimeoutSec' => 0.2]))->sendRequests($fc3); print_r($responseContainer->getData()); // array of assoc. arrays (keyed by address name) print_r($responseContainer->getErrors());
Response structure
[ [ 'pump2_feedbackalarm_do' => true, ], [ 'battery3_voltage_wo' => 12, ], [ 'username_plc2' => 'prefix_admin', ] ]
Low level - send packets:
$connection = BinaryStreamConnection::getBuilder() ->setHost('192.168.0.1') ->build(); $packet = new ReadHoldingRegistersRequest(256, 8); //create FC3 request packet try { $binaryData = $connection->connect()->sendAndReceive($packet); //parse binary data to response object $response = ResponseFactory::parseResponseOrThrow($binaryData); //same as 'foreach ($response->getWords() as $word) {' foreach ($response as $word) { print_r($word->getInt16()); } // print registers as double words in big endian low word first order (as WAGO-750 does) foreach ($response->getDoubleWords() as $dword) { print_r($dword->getInt32(Endian::BIG_ENDIAN_LOW_WORD_FIRST)); } // set internal index to match start address to simplify array access $responseWithStartAddress = $response->withStartAddress(256); print_r($responseWithStartAddress[256]->getBytes()); // use array access to get word print_r($responseWithStartAddress->getDoubleWordAt(257)->getFloat()); } catch (Exception $exception) { echo $exception->getMessage() . PHP_EOL; } finally { $connection->close(); }
Example of Modbus RTU over TCP
Difference between Modbus RTU and Modbus TCP is that:
- RTU header contains only slave id. TCP/IP header contains of transaction id, protocol id, length, unitid
- RTU packed has 2 byte CRC appended
See http://www.simplymodbus.ca/TCP.htm for more detailsed explanation
This library was/is originally meant for Modbus TCP but it has support to convert packet to RTU and from RTU. See this examples/rtu.php for example.
$rtuBinaryPacket = RtuConverter::toRtu(new ReadHoldingRegistersRequest($startAddress, $quantity, $slaveId)); $binaryData = $connection->connect()->sendAndReceive($rtuBinaryPacket); $responseAsTcpPacket = RtuConverter::fromRtu($binaryData);
Example of Modbus RTU over USB to Serial (RS485) adapter
See Linux example in 'examples/rtu_usb_to_serial.php'
Example of Modbus RTU over TCP + higher level API usage
See example in 'examples/rtu_over_tcp_with_higherlevel_api.php'
Example of non-blocking socket IO with ReactPHP/Amp (i.e. modbus request are run in 'parallel')
- 'examples/example_parallel_requests_reactphp.php - example of non-blocking socket IO with ReactPHP socket library (https://github.com/reactphp/socket)
- 'examples/example_parallel_requests_amp.php - example of non-blocking socket IO with Amp socket library https://github.com/amphp/socket
Example Modbus server (accepting requests) with ReactPHP
- 'examples/example_response_server.php - example of modbus server
Try communication with PLCs quickly using php built-in web server
Examples folder has index.php which can be used with php built-in web server to test out communication with our own PLCs.
git clone https://github.com/aldas/modbus-tcp-client.git
cd modbus-tcp-client
composer install
php -S localhost:8080 -t examples/
Now open http://localhost:8080 in browser. See additional query parameters from index.php.
Changelog
See CHANGELOG.md
Tests
- all
composer test
- unit tests
composer test-unit
- integration tests
composer test-integration
For Windows users:
- all
vendor/bin/phpunit
- unit tests
vendor/bin/phpunit --testsuite 'unit-tests'
- integration tests
vendor/bin/phpunit --testsuite 'integration-tests'
Static analysis
Run PHPStan analysis compose check