diff --git a/src/IecType.php b/src/IecType.php index ff5da87..c7ff067 100644 --- a/src/IecType.php +++ b/src/IecType.php @@ -22,120 +22,117 @@ namespace PHPModbus; * The class includes set of IEC-1131 data type functions that converts a PHP * data types to a IEC data type. * - * @author Jan Krakora - * @copyright Copyright (c) 2004, 2010 Jan Krakora - * @package Phpmodbus + * @author Jan Krakora + * @copyright Copyright (c) 2004, 2010 Jan Krakora + * @package Phpmodbus */ class IecType { - /** - * iecBYTE - * - * Converts a value to IEC-1131 BYTE data type - * - * @param int $value from 0 to 255 - * @return int IEC BYTE data type - * - */ - public static function iecBYTE($value) - { - return chr($value & 0xFF); - } + /** + * iecINT + * + * Converts a value to IEC-1131 INT data type + * + * @param int $value to be converted + * @return int IEC-1131 INT data type + */ + public static function iecINT($value) + { + return self::iecBYTE(($value >> 8) & 0x00FF) . + self::iecBYTE($value & 0x00FF); + } - /** - * iecINT - * - * Converts a value to IEC-1131 INT data type - * - * @param int $value to be converted - * @return int IEC-1131 INT data type - * - */ - public static function iecINT($value) - { - return self::iecBYTE(($value >> 8) & 0x00FF) . - self::iecBYTE(($value & 0x00FF)); - } + /** + * iecBYTE + * + * Converts a value to IEC-1131 BYTE data type + * + * @param int $value from 0 to 255 + * @return int IEC BYTE data type + */ + public static function iecBYTE($value) + { + return chr($value & 0xFF); + } - /** - * iecDINT - * - * Converts a value to IEC-1131 DINT data type - * - * @param int $value to be converted - * @param int $bigEndian defines endian codding (little endian == 0, big endian == 1) - * @return int IEC-1131 INT data type - * - */ - public static function iecDINT($value, $bigEndian = 0) - { - // result with right endianness - return self::endianness($value, $bigEndian); - } + /** + * iecDINT + * + * Converts a value to IEC-1131 DINT data type + * + * @param int $value to be converted + * @param int $bigEndian defines endian codding (little endian == 0, big endian == 1) + * @return int IEC-1131 INT data type + */ + public static function iecDINT($value, $bigEndian = 0) + { + // result with right endianness + return self::endianness($value, $bigEndian); + } - /** - * iecREAL - * - * Converts a value to IEC-1131 REAL data type. The function uses function @use float2iecReal. - * - * @param int $value to be converted - * @param bool $bigEndian defines endian codding (little endian == 0, big endian == 1) - * @return int IEC-1131 REAL data type - */ - public static function iecREAL($value, $bigEndian = 0) - { - // iecREAL representation - $real = self::float2iecReal($value); - // result with right endianness - return self::endianness($real, $bigEndian); - } + /** + * endianness + * + * Make endianess as required. + * For more see http://en.wikipedia.org/wiki/Endianness + * + * @param int $value + * @param bool $bigEndian + * @return int + */ + private static function endianness($value, $bigEndian = 0) + { + if ($bigEndian == 0) { + return + self::iecBYTE(($value >> 8) & 0x000000FF) . + self::iecBYTE($value & 0x000000FF) . + self::iecBYTE(($value >> 24) & 0x000000FF) . + self::iecBYTE(($value >> 16) & 0x000000FF); + } else { + return + self::iecBYTE(($value >> 24) & 0x000000FF) . + self::iecBYTE(($value >> 16) & 0x000000FF) . + self::iecBYTE(($value >> 8) & 0x000000FF) . + self::iecBYTE($value & 0x000000FF); + } + } - /** - * float2iecReal - * - * This function converts float value to IEC-1131 REAL single precision form. - * - * For more see [{@link http://en.wikipedia.org/wiki/Single_precision Single precision on Wiki}] or - * [{@link http://de.php.net/manual/en/function.base-convert.php PHP base_convert function commentary}, Todd Stokes - * @ Georgia Tech 21-Nov-2007] or - * [{@link http://www.php.net/manual/en/function.pack.php PHP pack/unpack functionality}] - * - * @param float $value to be converted - * @return int IEC REAL data type - */ - private static function float2iecReal($value) - { - // get float binary string - $float = pack("f", $value); - // set 32-bit unsigned integer of the float - $w = unpack("L", $float); - return $w[1]; - } + /** + * iecREAL + * + * Converts a value to IEC-1131 REAL data type. The function uses function @use float2iecReal. + * + * @param int $value to be converted + * @param bool $bigEndian defines endian codding (little endian == 0, big endian == 1) + * @return int IEC-1131 REAL data type + */ + public static function iecREAL($value, $bigEndian = 0) + { + // iecREAL representation + $real = self::float2iecReal($value); + // result with right endianness + return self::endianness($real, $bigEndian); + } - /** - * endianness - * - * Make endianess as required. - * For more see http://en.wikipedia.org/wiki/Endianness - * - * @param int $value - * @param bool $bigEndian - * @return int - */ - private static function endianness($value, $bigEndian = 0) - { - if ($bigEndian == 0) { - return - self::iecBYTE(($value >> 8) & 0x000000FF) . - self::iecBYTE(($value & 0x000000FF)) . - self::iecBYTE(($value >> 24) & 0x000000FF) . - self::iecBYTE(($value >> 16) & 0x000000FF); - } else { - return - self::iecBYTE(($value >> 24) & 0x000000FF) . - self::iecBYTE(($value >> 16) & 0x000000FF) . - self::iecBYTE(($value >> 8) & 0x000000FF) . - self::iecBYTE(($value & 0x000000FF)); - } - } + /** + * float2iecReal + * + * This function converts float value to IEC-1131 REAL single precision form. + * + * For more see [{@link http://en.wikipedia.org/wiki/Single_precision Single precision on Wiki}] or + * [{@link http://de.php.net/manual/en/function.base-convert.php PHP base_convert function commentary}, Todd Stokes + * @ Georgia Tech 21-Nov-2007] or + * [{@link http://www.php.net/manual/en/function.pack.php PHP pack/unpack functionality}] + * + * @param float $value to be converted + * @return int IEC REAL data type + */ + private static function float2iecReal($value) + { + // get float binary string + $float = pack('f', $value); + // set 32-bit unsigned integer of the float + $w = unpack('L', $float); + return $w[1]; + } } diff --git a/src/ModbusMaster.php b/src/ModbusMaster.php index 9c27f53..8e68a21 100644 --- a/src/ModbusMaster.php +++ b/src/ModbusMaster.php @@ -10,14 +10,12 @@ use Exception; * This source file is subject to the "PhpModbus license" that is bundled * with this package in the file license.txt. * - * - * @copyright Copyright (c) 2004, 2013 Jan Krakora - * @license PhpModbus license - * @category Phpmodbus - * @tutorial Phpmodbus.pkg - * @package Phpmodbus - * @version $id$ - * + * @copyright Copyright (c) 2004, 2013 Jan Krakora + * @license PhpModbus license + * @category Phpmodbus + * @tutorial Phpmodbus.pkg + * @package Phpmodbus + * @version $id$ */ @@ -38,1409 +36,1465 @@ use Exception; * - FC 22: mask write register * - FC 23: read write registers * - * @author Jan Krakora - * @copyright Copyright (c) 2004, 2013 Jan Krakora - * @package Phpmodbus - * + * @author Jan Krakora + * @copyright Copyright (c) 2004, 2013 Jan Krakora + * @package Phpmodbus */ class ModbusMaster { - /** @var resource Communication socket */ - private $sock; - - /** @var string Modbus device IP address */ - public $host = "192.168.1.1"; - - /** @var string gateway port */ - public $port = 502; - - /** @var string (optional) client IP address */ - public $client = ""; // TODO explanation? - - /** @var string client port */ - public $client_port = 502; - - /** @var string ModbusMaster status messages (echo for debugging) */ - public $status; - - /** @var float Total response timeout (seconds, decimals allowed) */ - public $timeout_sec = 5; - - /** @var float Socket read timeout (seconds, decimals allowed) */ - public $socket_read_timeout_sec = 0.3; // 300 ms - - /** @var float Socket write timeout (seconds, decimals allowed) */ - public $socket_write_timeout_sec = 1; - - /** @var int Endianness codding (0 = little endian = 0, 1 = big endian) */ - public $endianness = 0; // - - /** @var string Socket protocol (TCP, UDP) */ - public $socket_protocol = "UDP"; - - /** - * ModbusMaster - * - * This is the constructor that defines {@link $host} IP address of the object. - * - * @param String $host An IP address of a Modbus TCP device. E.g. "192.168.1.1" - * @param String $protocol Socket protocol (TCP, UDP) - */ - public function __construct($host, $protocol) - { - $this->socket_protocol = $protocol; - $this->host = $host; - } - - /** - * __toString - * - * Magic method - */ - public function __toString() - { - return "
" . $this->status . "
"; - } - - /** - * Convert float in seconds to array - * - * @param float $secs - * @return array {sec: ..., usec: ...} - */ - private function secsToSecUsecArray($secs) - { - $remainder = $secs - floor($secs); - - return [ - 'sec' => round($secs - $remainder), - 'usec' => round($remainder * 1e6), - ]; - } - - /** - * connect - * - * Connect the socket - * - * @return bool - * @throws Exception - */ - private function connect() - { - // Create a protocol specific socket - if ($this->socket_protocol == "TCP") { - // TCP socket - $this->sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - } elseif ($this->socket_protocol == "UDP") { - // UDP socket - $this->sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - } else { - throw new Exception("Unknown socket protocol, should be 'TCP' or 'UDP'"); - } - // Bind the client socket to a specific local port - if (strlen($this->client) > 0) { - $result = socket_bind($this->sock, $this->client, $this->client_port); - if ($result === false) { - throw new Exception("socket_bind() failed. Reason: ($result)" . - socket_strerror(socket_last_error($this->sock))); - } else { - $this->status .= "Bound\n"; - } - } - - // Socket settings (send/write timeout) - $writeTimeout = $this->secsToSecUsecArray($this->socket_write_timeout_sec); - socket_set_option($this->sock, SOL_SOCKET, SO_SNDTIMEO, $writeTimeout); - - // Connect the socket - $result = @socket_connect($this->sock, $this->host, $this->port); - if ($result === false) { - throw new Exception("socket_connect() failed. Reason: ($result)" . - socket_strerror(socket_last_error($this->sock))); - } else { - $this->status .= "Connected\n"; - return true; - } - } - - /** - * disconnect - * - * Disconnect the socket - */ - private function disconnect() - { - socket_close($this->sock); - $this->status .= "Disconnected\n"; - } - - /** - * send - * - * Send the packet via Modbus - * - * @param string $packet - */ - private function send($packet) - { - socket_write($this->sock, $packet, strlen($packet)); - $this->status .= "Send\n"; - } - - /** - * rec - * - * Receive data from the socket - * - * @return bool - * @throws Exception - */ - private function rec() - { - socket_set_nonblock($this->sock); - $readsocks[] = $this->sock; - $writesocks = null; - $exceptsocks = null; - $rec = ""; - $totalReadTimeout = $this->timeout_sec; - $lastAccess = microtime(true); - $readTout = $this->secsToSecUsecArray($this->socket_read_timeout_sec); - - while (false !== socket_select($readsocks, $writesocks, $exceptsocks, $readTout['sec'], $readTout['usec'])) { - $this->status .= "Wait data ... \n"; - if (in_array($this->sock, $readsocks)) { - if (@socket_recv($this->sock, $rec, 2000, 0)) { // read max 2000 bytes - $this->status .= "Data received \n"; - return $rec; - } - $lastAccess = microtime(true); - } else { - $timeSpentWaiting = microtime(true) - $lastAccess; - if ($timeSpentWaiting >= $totalReadTimeout) { - throw new Exception( - "Watchdog time expired [ $totalReadTimeout sec ]!!! " . - "Connection to $this->host:$this->port is not established."); - } - } - $readsocks[] = $this->sock; - } - - return null; - } - - /** - * responseCode - * - * Check the Modbus response code - * - * @param string $packet - * @return bool - * @throws Exception - */ - private function responseCode($packet) - { - if ((ord($packet[7]) & 0x80) > 0) { - // failure code - $failure_code = ord($packet[8]); - // failure code strings - $failures = array( - 0x01 => "ILLEGAL FUNCTION", - 0x02 => "ILLEGAL DATA ADDRESS", - 0x03 => "ILLEGAL DATA VALUE", - 0x04 => "SLAVE DEVICE FAILURE", - 0x05 => "ACKNOWLEDGE", - 0x06 => "SLAVE DEVICE BUSY", - 0x08 => "MEMORY PARITY ERROR", - 0x0A => "GATEWAY PATH UNAVAILABLE", - 0x0B => "GATEWAY TARGET DEVICE FAILED TO RESPOND", - ); - // get failure string - if (key_exists($failure_code, $failures)) { - $failure_str = $failures[$failure_code]; - } else { - $failure_str = "UNDEFINED FAILURE CODE"; - } - // exception response - throw new Exception("Modbus response error code: $failure_code ($failure_str)"); - } else { - $this->status .= "Modbus response error code: NOERROR\n"; - return true; - } - } - - /** - * readCoils - * - * Modbus function FC 1(0x01) - Read Coils - * - * Reads {@link $quantity} of Coils (boolean) from reference - * {@link $reference} of a memory of a Modbus device given by - * {@link $unitId}. - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return bool[] - */ - public function readCoils($unitId, $reference, $quantity) - { - $this->status .= "readCoils: START\n"; - // connect - $this->connect(); - // send FC 1 - $packet = $this->readCoilsPacketBuilder($unitId, $reference, $quantity); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $receivedData = $this->readCoilsParser($rpacket, $quantity); - // disconnect - $this->disconnect(); - $this->status .= "readCoils: DONE\n"; - // return - return $receivedData; - } - - /** - * fc1 - * - * Alias to {@link readCoils} method - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return bool[] - */ - public function fc1($unitId, $reference, $quantity) - { - return $this->readCoils($unitId, $reference, $quantity); - } - - /** - * readCoilsPacketBuilder - * - * FC1 packet builder - read coils - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return string - */ - private function readCoilsPacketBuilder($unitId, $reference, $quantity) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(1); // FC 1 = 1(0x01) - // build body - read section - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($quantity); // quantity - $dataLen += 5; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * readCoilsParser - * - * FC 1 response parser - * - * @param string $packet - * @param int $quantity - * @return bool[] - */ - private function readCoilsParser($packet, $quantity) - { - $data = array(); - // check Response code - $this->responseCode($packet); - // get data from stream - for ($i = 0; $i < ord($packet[8]); $i++) { - $data[$i] = ord($packet[9 + $i]); - } - // get bool values to array - $data_boolean_array = array(); - $di = 0; - foreach ($data as $value) { - for ($i = 0; $i < 8; $i++) { - if ($di == $quantity) { - continue; - } - // get boolean value - $v = ($value >> $i) & 0x01; - // build boolean array - if ($v == 0) { - $data_boolean_array[] = false; - } else { - $data_boolean_array[] = true; - } - $di++; - } - } - return $data_boolean_array; - } - - /** - * readInputDiscretes - * - * Modbus function FC 2(0x02) - Read Input Discretes - * - * Reads {@link $quantity} of Inputs (boolean) from reference - * {@link $reference} of a memory of a Modbus device given by - * {@link $unitId}. - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return bool[] - */ - public function readInputDiscretes($unitId, $reference, $quantity) - { - $this->status .= "readInputDiscretes: START\n"; - // connect - $this->connect(); - // send FC 2 - $packet = $this->readInputDiscretesPacketBuilder($unitId, $reference, $quantity); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $receivedData = $this->readInputDiscretesParser($rpacket, $quantity); - // disconnect - $this->disconnect(); - $this->status .= "readInputDiscretes: DONE\n"; - // return - return $receivedData; - } - - /** - * fc2 - * - * Alias to {@link readInputDiscretes} method - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return bool[] - */ - public function fc2($unitId, $reference, $quantity) - { - return $this->readInputDiscretes($unitId, $reference, $quantity); - } - - /** - * readInputDiscretesPacketBuilder - * - * FC2 packet builder - read coils - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return string - */ - private function readInputDiscretesPacketBuilder($unitId, $reference, $quantity) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(2); // FC 2 = 2(0x02) - // build body - read section - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($quantity); // quantity - $dataLen += 5; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * readInputDiscretesParser - * - * FC 2 response parser, alias to FC 1 parser i.e. readCoilsParser. - * - * @param string $packet - * @param int $quantity - * @return bool[] - */ - private function readInputDiscretesParser($packet, $quantity) - { - return $this->readCoilsParser($packet, $quantity); - } - - /** - * readMultipleRegisters - * - * Modbus function FC 3(0x03) - Read Multiple Registers. - * - * This function reads {@link $quantity} of Words (2 bytes) from reference - * {@link $referenceRead} of a memory of a Modbus device given by - * {@link $unitId}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory to read data (e.g. in device WAGO 750-841, memory MW0 - * starts at address 12288). - * @param int $quantity Amounth of the data to be read from device. - * @return false|array Success flag or array of received data. - */ - public function readMultipleRegisters($unitId, $reference, $quantity) - { - $this->status .= "readMultipleRegisters: START\n"; - // connect - $this->connect(); - // send FC 3 - $packet = $this->readMultipleRegistersPacketBuilder($unitId, $reference, $quantity); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $receivedData = $this->readMultipleRegistersParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "readMultipleRegisters: DONE\n"; - // return - return $receivedData; - } - - /** - * fc3 - * - * Alias to {@link readMultipleRegisters} method. - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return false|array - */ - public function fc3($unitId, $reference, $quantity) - { - return $this->readMultipleRegisters($unitId, $reference, $quantity); - } - - /** - * readMultipleRegistersPacketBuilder - * - * Packet FC 3 builder - read multiple registers - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return string - */ - private function readMultipleRegistersPacketBuilder($unitId, $reference, $quantity) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(3); // FC 3 = 3(0x03) - // build body - read section - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($quantity); // quantity - $dataLen += 5; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * readMultipleRegistersParser - * - * FC 3 response parser - * - * @param string $packet - * @return array - */ - private function readMultipleRegistersParser($packet) - { - $data = array(); - // check Response code - $this->responseCode($packet); - // get data - for ($i = 0; $i < ord($packet[8]); $i++) { - $data[$i] = ord($packet[9 + $i]); - } - return $data; - } - - /** - * readMultipleInputRegisters - * - * Modbus function FC 4(0x04) - Read Multiple Input Registers. - * - * This function reads {@link $quantity} of Words (2 bytes) from reference - * {@link $referenceRead} of a memory of a Modbus device given by - * {@link $unitId}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory to read data. - * @param int $quantity Amounth of the data to be read from device. - * @return false|array Success flag or array of received data. - */ - public function readMultipleInputRegisters($unitId, $reference, $quantity) - { - $this->status .= "readMultipleInputRegisters: START\n"; - // connect - $this->connect(); - // send FC 4 - $packet = $this->readMultipleInputRegistersPacketBuilder($unitId, $reference, $quantity); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $receivedData = $this->readMultipleInputRegistersParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "readMultipleInputRegisters: DONE\n"; - // return - return $receivedData; - } - - /** - * fc4 - * - * Alias to {@link readMultipleInputRegisters} method. - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return false|array - */ - public function fc4($unitId, $reference, $quantity) - { - return $this->readMultipleInputRegisters($unitId, $reference, $quantity); - } - - /** - * readMultipleInputRegistersPacketBuilder - * - * Packet FC 4 builder - read multiple input registers - * - * @param int $unitId - * @param int $reference - * @param int $quantity - * @return string - */ - private function readMultipleInputRegistersPacketBuilder($unitId, $reference, $quantity) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(4); // FC 4 = 4(0x04) - // build body - read section - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($quantity); // quantity - $dataLen += 5; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); // unit ID - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * readMultipleInputRegistersParser - * - * FC 4 response parser - * - * @param string $packet - * @return array - */ - private function readMultipleInputRegistersParser($packet) - { - $data = array(); - // check Response code - $this->responseCode($packet); - // get data - for ($i = 0; $i < ord($packet[8]); $i++) { - $data[$i] = ord($packet[9 + $i]); - } - return $data; - } - - /** - * writeSingleCoil - * - * Modbus function FC5(0x05) - Write Single Register. - * - * This function writes {@link $data} single coil at {@link $reference} position of - * memory of a Modbus device given by {@link $unitId}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at - * address 12288) - * @param array $data value to be written (TRUE|FALSE). - * @return bool Success flag - */ - public function writeSingleCoil($unitId, $reference, $data) - { - $this->status .= "writeSingleCoil: START\n"; - // connect - $this->connect(); - // send FC5 - $packet = $this->writeSingleCoilPacketBuilder($unitId, $reference, $data); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $this->writeSingleCoilParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "writeSingleCoil: DONE\n"; - return true; - } - - /** - * fc5 - * - * Alias to {@link writeSingleCoil} method - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return bool - */ - public function fc5($unitId, $reference, $data) - { - return $this->writeSingleCoil($unitId, $reference, $data); - } - - /** - * writeSingleCoilPacketBuilder - * - * Packet builder FC5 - WRITE single register - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return string - */ - private function writeSingleCoilPacketBuilder($unitId, $reference, $data) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - foreach ($data as $key => $dataitem) { - if ($dataitem == true) { - $buffer1 = IecType::iecINT(0xFF00); - } else { - $buffer1 = IecType::iecINT(0x0000); - }; - }; - $dataLen += 2; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(5); // FC5 = 5(0x05) - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $dataLen += 3; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * writeSingleCoilParser - * - * FC5 response parser - * - * @param string $packet - * @return bool - */ - private function writeSingleCoilParser($packet) - { - $this->responseCode($packet); - return true; - } - - /** - * writeSingleRegister - * - * Modbus function FC6(0x06) - Write Single Register. - * - * This function writes {@link $data} single word value at {@link $reference} position of - * memory of a Modbus device given by {@link $unitId}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at - * address 12288) - * @param array $data Array of values to be written. - * @return bool Success flag - */ - public function writeSingleRegister($unitId, $reference, $data) - { - $this->status .= "writeSingleRegister: START\n"; - // connect - $this->connect(); - // send FC6 - $packet = $this->writeSingleRegisterPacketBuilder($unitId, $reference, $data); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $this->writeSingleRegisterParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "writeSingleRegister: DONE\n"; - return true; - } - - /** - * fc6 - * - * Alias to {@link writeSingleRegister} method - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return bool - */ - public function fc6($unitId, $reference, $data) - { - return $this->writeSingleRegister($unitId, $reference, $data); - } - - /** - * writeSingleRegisterPacketBuilder - * - * Packet builder FC6 - WRITE single register - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return string - */ - private function writeSingleRegisterPacketBuilder($unitId, $reference, $data) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - foreach ($data as $key => $dataitem) { - $buffer1 .= IecType::iecINT($dataitem); // register values x - $dataLen += 2; - break; - } - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(6); // FC6 = 6(0x06) - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $dataLen += 3; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * writeSingleRegisterParser - * - * FC6 response parser - * - * @param string $packet - * @return bool - */ - private function writeSingleRegisterParser($packet) - { - $this->responseCode($packet); - return true; - } - - /** - * writeMultipleCoils - * - * Modbus function FC15(0x0F) - Write Multiple Coils - * - * This function writes {@link $data} array at {@link $reference} position of - * memory of a Modbus device given by {@link $unitId}. - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return bool - */ - public function writeMultipleCoils($unitId, $reference, $data) - { - $this->status .= "writeMultipleCoils: START\n"; - // connect - $this->connect(); - // send FC15 - $packet = $this->writeMultipleCoilsPacketBuilder($unitId, $reference, $data); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $this->writeMultipleCoilsParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "writeMultipleCoils: DONE\n"; - return true; - } - - /** - * fc15 - * - * Alias to {@link writeMultipleCoils} method - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return bool - */ - public function fc15($unitId, $reference, $data) - { - return $this->writeMultipleCoils($unitId, $reference, $data); - } - - /** - * writeMultipleCoilsPacketBuilder - * - * Packet builder FC15 - Write multiple coils - * - * @param int $unitId - * @param int $reference - * @param array $data - * @return string - */ - private function writeMultipleCoilsPacketBuilder($unitId, $reference, $data) - { - $dataLen = 0; - // build bool stream to the WORD array - $data_word_stream = array(); - $data_word = 0; - $shift = 0; - for ($i = 0; $i < count($data); $i++) { - if ((($i % 8) == 0) && ($i > 0)) { - $data_word_stream[] = $data_word; - $shift = 0; - $data_word = 0; - $data_word |= (0x01 && $data[$i]) << $shift; - $shift++; - } else { - $data_word |= (0x01 && $data[$i]) << $shift; - $shift++; - } - } - $data_word_stream[] = $data_word; - // show binary stream to status string - foreach ($data_word_stream as $d) { - $this->status .= sprintf("byte=b%08b\n", $d); - } - // build data section - $buffer1 = ""; - foreach ($data_word_stream as $key => $dataitem) { - $buffer1 .= IecType::iecBYTE($dataitem); // register values x - $dataLen += 1; - } - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(15); // FC 15 = 15(0x0f) - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT(count($data)); // bit count - $buffer2 .= IecType::iecBYTE((count($data) + 7) / 8); // byte count - $dataLen += 6; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); // unit ID - - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * writeMultipleCoilsParser - * - * FC15 response parser - * - * @param string $packet - * @return bool - */ - private function writeMultipleCoilsParser($packet) - { - $this->responseCode($packet); - return true; - } - - /** - * writeMultipleRegister - * - * Modbus function FC16(0x10) - Write Multiple Register. - * - * This function writes {@link $data} array at {@link $reference} position of - * memory of a Modbus device given by {@link $unitId}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at - * address 12288) - * @param array $data Array of values to be written. - * @param array $dataTypes Array of types of values to be written. The array should consists of string "INT", - * "DINT" and "REAL". - * @return bool Success flag - */ - public function writeMultipleRegister($unitId, $reference, $data, $dataTypes) - { - $this->status .= "writeMultipleRegister: START\n"; - // connect - $this->connect(); - // send FC16 - $packet = $this->writeMultipleRegisterPacketBuilder($unitId, $reference, $data, $dataTypes); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $this->writeMultipleRegisterParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "writeMultipleRegister: DONE\n"; - return true; - } - - /** - * fc16 - * - * Alias to {@link writeMultipleRegister} method - * - * @param int $unitId - * @param int $reference - * @param array $data - * @param array $dataTypes - * @return bool - */ - public function fc16($unitId, $reference, $data, $dataTypes) - { - return $this->writeMultipleRegister($unitId, $reference, $data, $dataTypes); - } - - /** - * writeMultipleRegisterPacketBuilder - * - * Packet builder FC16 - WRITE multiple register - * e.g.: 4dd90000000d0010300000030603e807d00bb8 - * - * @param int $unitId - * @param int $reference - * @param array $data - * @param array $dataTypes - * @return string - */ - private function writeMultipleRegisterPacketBuilder($unitId, $reference, $data, $dataTypes) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - foreach ($data as $key => $dataitem) { - if ($dataTypes[$key] == "INT") { - $buffer1 .= IecType::iecINT($dataitem); // register values x - $dataLen += 2; - } elseif ($dataTypes[$key] == "DINT") { - $buffer1 .= IecType::iecDINT($dataitem, $this->endianness); // register values x - $dataLen += 4; - } elseif ($dataTypes[$key] == "REAL") { - $buffer1 .= IecType::iecREAL($dataitem, $this->endianness); // register values x - $dataLen += 4; - } else { - $buffer1 .= IecType::iecINT($dataitem); // register values x - $dataLen += 2; - } - } - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(16); // FC 16 = 16(0x10) - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($dataLen / 2); // word count - $buffer2 .= IecType::iecBYTE($dataLen); // byte count - $dataLen += 6; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * writeMultipleRegisterParser - * - * FC16 response parser - * - * @param string $packet - * @return bool - */ - private function writeMultipleRegisterParser($packet) - { - $this->responseCode($packet); - return true; - } - - /** - * maskWriteRegister - * - * Modbus function FC22(0x16) - Mask Write Register. - * - * This function alter single bit(s) at {@link $reference} position of - * memory of a Modbus device given by {@link $unitId}. - * - * Result = (Current Contents AND And_Mask) OR (Or_Mask AND (NOT And_Mask)) - * - * @param int $unitId usually ID of Modbus device - * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at address - * 12288) - * @param int $andMask - * @param int $orMask - * @return bool Success flag - */ - public function maskWriteRegister($unitId, $reference, $andMask, $orMask) - { - $this->status .= "maskWriteRegister: START\n"; - // connect - $this->connect(); - // send FC22 - $packet = $this->maskWriteRegisterPacketBuilder($unitId, $reference, $andMask, $orMask); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $this->maskWriteRegisterParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "maskWriteRegister: DONE\n"; - return true; - } - - /** - * fc22 - * - * Alias to {@link maskWriteRegister} method - * - * @param int $unitId - * @param int $reference - * @param int $andMask - * @param int $orMask - * @return bool - */ - public function fc22($unitId, $reference, $andMask, $orMask) - { - return $this->maskWriteRegister($unitId, $reference, $andMask, $orMask); - } - - /** - * maskWriteRegisterPacketBuilder - * - * Packet builder FC22 - MASK WRITE register - * - * @param int $unitId - * @param int $reference - * @param int $andMask - * @param int $orMask - * @return string - */ - private function maskWriteRegisterPacketBuilder($unitId, $reference, $andMask, $orMask) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(22); // FC 22 = 22(0x16) - $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 - $buffer2 .= IecType::iecINT($andMask); // AND mask - $buffer2 .= IecType::iecINT($orMask); // OR mask - $dataLen += 7; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * maskWriteRegisterParser - * - * FC22 response parser - * - * @param string $packet - * @return bool - */ - private function maskWriteRegisterParser($packet) - { - $this->responseCode($packet); - return true; - } - - /** - * readWriteRegisters - * - * Modbus function FC23(0x17) - Read Write Registers. - * - * This function writes {@link $data} array at reference {@link $referenceWrite} - * position of memory of a Modbus device given by {@link $unitId}. Simultanously, - * it returns {@link $quantity} of Words (2 bytes) from reference {@link $referenceRead}. - * - * - * @param int $unitId usually ID of Modbus device - * @param int $referenceRead Reference in the device memory to read data (e.g. in device WAGO 750-841, memory - * MW0 starts at address 12288). - * @param int $quantity Amounth of the data to be read from device. - * @param int $referenceWrite Reference in the device memory to write data. - * @param array $data Array of values to be written. - * @param array $dataTypes Array of types of values to be written. The array should consists of string "INT", - * "DINT" and "REAL". - * @return false|array Success flag or array of data. - */ - public function readWriteRegisters($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes) - { - $this->status .= "readWriteRegisters: START\n"; - // connect - $this->connect(); - // send FC23 - $packet = $this->readWriteRegistersPacketBuilder($unitId, $referenceRead, $quantity, $referenceWrite, $data, - $dataTypes); - $this->status .= $this->printPacket($packet); - $this->send($packet); - // receive response - $rpacket = $this->rec(); - $this->status .= $this->printPacket($rpacket); - // parse packet - $receivedData = $this->readWriteRegistersParser($rpacket); - // disconnect - $this->disconnect(); - $this->status .= "writeMultipleRegister: DONE\n"; - // return - return $receivedData; - } - - /** - * fc23 - * - * Alias to {@link readWriteRegisters} method. - * - * @param int $unitId - * @param int $referenceRead - * @param int $quantity - * @param int $referenceWrite - * @param array $data - * @param array $dataTypes - * @return false|array - */ - public function fc23($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes) - { - return $this->readWriteRegisters($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes); - } - - /** - * readWriteRegistersPacketBuilder - * - * Packet FC23 builder - READ WRITE registers - * - * - * @param int $unitId - * @param int $referenceRead - * @param int $quantity - * @param int $referenceWrite - * @param array $data - * @param array $dataTypes - * @return string - */ - private function readWriteRegistersPacketBuilder( - $unitId, - $referenceRead, - $quantity, - $referenceWrite, - $data, - $dataTypes - ) - { - $dataLen = 0; - // build data section - $buffer1 = ""; - foreach ($data as $key => $dataitem) { - if ($dataTypes[$key] == "INT") { - $buffer1 .= IecType::iecINT($dataitem); // register values x - $dataLen += 2; - } elseif ($dataTypes[$key] == "DINT") { - $buffer1 .= IecType::iecDINT($dataitem, $this->endianness); // register values x - $dataLen += 4; - } elseif ($dataTypes[$key] == "REAL") { - $buffer1 .= IecType::iecREAL($dataitem, $this->endianness); // register values x - $dataLen += 4; - } else { - $buffer1 .= IecType::iecINT($dataitem); // register values x - $dataLen += 2; - } - } - // build body - $buffer2 = ""; - $buffer2 .= IecType::iecBYTE(23); // FC 23 = 23(0x17) - // build body - read section - $buffer2 .= IecType::iecINT($referenceRead); // refnumber = 12288 - $buffer2 .= IecType::iecINT($quantity); // quantity - // build body - write section - $buffer2 .= IecType::iecINT($referenceWrite); // refnumber = 12288 - $buffer2 .= IecType::iecINT($dataLen / 2); // word count - $buffer2 .= IecType::iecBYTE($dataLen); // byte count - $dataLen += 10; - // build header - $buffer3 = ''; - $buffer3 .= IecType::iecINT(rand(0, 65000)); // transaction ID - $buffer3 .= IecType::iecINT(0); // protocol ID - $buffer3 .= IecType::iecINT($dataLen + 1); // lenght - $buffer3 .= IecType::iecBYTE($unitId); //unit ID - - // return packet string - return $buffer3 . $buffer2 . $buffer1; - } - - /** - * readWriteRegistersParser - * - * FC23 response parser - * - * @param string $packet - * @return array|false - */ - private function readWriteRegistersParser($packet) - { - $data = array(); - // if not exception - if (!$this->responseCode($packet)) { - return false; - } - // get data - for ($i = 0; $i < ord($packet[8]); $i++) { - $data[$i] = ord($packet[9 + $i]); - } - return $data; - } - - /** - * byte2hex - * - * Parse data and get it to the Hex form - * - * @param int $value - * @return string - */ - private function byte2hex($value) - { - $h = dechex(($value >> 4) & 0x0F); - $l = dechex($value & 0x0F); - return "$h$l"; - } - - /** - * printPacket - * - * Print a packet in the hex form - * - * @param string $packet - * @return string - */ - private function printPacket($packet) - { - $str = ""; - $str .= "Packet: "; - for ($i = 0; $i < strlen($packet); $i++) { - $str .= $this->byte2hex(ord($packet[$i])); - } - $str .= "\n"; - return $str; - } - - /** - * Set data receive timeout. - * Writes property timeout_sec - * - * @param float $seconds seconds - */ - public function setTimeout($seconds) - { - $this->timeout_sec = $seconds; - } - - /** - * Set socket read/write timeout. Null = no change. - * - * @param float|null $read_timeout_sec data read timeout (seconds, default 0.3) - * @param float|null $write_timeout_sec data write timeout (seconds, default 1.0) - * @internal param float $seconds seconds - */ - public function setSocketTimeout($read_timeout_sec, $write_timeout_sec) - { - // Set read timeout if given - if ($read_timeout_sec !== null) { - $this->socket_read_timeout_sec = $read_timeout_sec; - } - - // Set write timeout if given - if ($write_timeout_sec !== null) { - $this->socket_write_timeout_sec = $write_timeout_sec; - } - } + /** +* + * + * @var string Modbus device IP address +*/ + public $host = '192.168.1.1'; + /** +* + * + * @var string gateway port +*/ + public $port = 502; + /** + * @var string (optional) client IP address when binding client +*/ + public $client = ''; + /** +* + * + * @var string client port set when binding client to local ip&port +*/ + public $client_port = 502; + /** +* + * + * @var string ModbusMaster status messages (echo for debugging) +*/ + public $status; + /** +* + * + * @var float Total response timeout (seconds, decimals allowed) +*/ + public $timeout_sec = 5; + /** + * @var float Socket read timeout (seconds, decimals allowed) +*/ + public $socket_read_timeout_sec = 0.3; + /** +* + * + * @var float Socket write timeout (seconds, decimals allowed) +*/ + public $socket_write_timeout_sec = 1; // 300 ms + /** + * @var int Endianness codding (0 = little endian = 0, 1 = big endian) +*/ + public $endianness = 0; + /** +* + * + * @var string Socket protocol (TCP, UDP) +*/ + public $socket_protocol = 'UDP'; // + /** +* + * + * @var resource Communication socket +*/ + private $sock; + + /** + * ModbusMaster + * + * This is the constructor that defines {@link $host} IP address of the object. + * + * @param String $host An IP address of a Modbus TCP device. E.g. "192.168.1.1" + * @param String $protocol Socket protocol (TCP, UDP) + */ + public function __construct($host, $protocol) + { + $this->socket_protocol = $protocol; + $this->host = $host; + } + + /** + * __toString + * + * Magic method + */ + public function __toString() + { + return '
' . $this->status . '
'; + } + + /** + * fc1 + * + * Alias to {@link readCoils} method + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + public function fc1($unitId, $reference, $quantity) + { + return $this->readCoils($unitId, $reference, $quantity); + } + + /** + * readCoils + * + * Modbus function FC 1(0x01) - Read Coils + * + * Reads {@link $quantity} of Coils (boolean) from reference + * {@link $reference} of a memory of a Modbus device given by + * {@link $unitId}. + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + public function readCoils($unitId, $reference, $quantity) + { + $this->status .= "readCoils: START\n"; + // connect + $this->connect(); + // send FC 1 + $packet = $this->readCoilsPacketBuilder($unitId, $reference, $quantity); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $receivedData = $this->readCoilsParser($rpacket, $quantity); + // disconnect + $this->disconnect(); + $this->status .= "readCoils: DONE\n"; + // return + return $receivedData; + } + + /** + * connect + * + * Connect the socket + * + * @return bool + * @throws Exception + */ + private function connect() + { + // Create a protocol specific socket + if ($this->socket_protocol === 'TCP') { + // TCP socket + $this->sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + } elseif ($this->socket_protocol === 'UDP') { + // UDP socket + $this->sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + } else { + throw new Exception("Unknown socket protocol, should be 'TCP' or 'UDP'"); + } + // Bind the client socket to a specific local port + if (strlen($this->client) > 0) { + $result = socket_bind($this->sock, $this->client, $this->client_port); + if ($result === false) { + throw new Exception( + "socket_bind() failed. Reason: ($result)" . + socket_strerror(socket_last_error($this->sock)) + ); + } else { + $this->status .= "Bound\n"; + } + } + + // Socket settings (send/write timeout) + $writeTimeout = $this->secsToSecUsecArray($this->socket_write_timeout_sec); + socket_set_option($this->sock, SOL_SOCKET, SO_SNDTIMEO, $writeTimeout); + + // Connect the socket + $result = @socket_connect($this->sock, $this->host, $this->port); + if ($result === false) { + throw new Exception( + "socket_connect() failed. Reason: ($result)" . + socket_strerror(socket_last_error($this->sock)) + ); + } else { + $this->status .= "Connected\n"; + return true; + } + } + + /** + * Convert float in seconds to array + * + * @param float $secs + * @return array {sec: ..., usec: ...} + */ + private function secsToSecUsecArray($secs) + { + $remainder = $secs - floor($secs); + + return [ + 'sec' => round($secs - $remainder), + 'usec' => round($remainder * 1e6), + ]; + } + + /** + * readCoilsPacketBuilder + * + * FC1 packet builder - read coils + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return string + */ + private function readCoilsPacketBuilder($unitId, $reference, $quantity) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(1); // FC 1 = 1(0x01) + // build body - read section + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($quantity); // quantity + $dataLen += 5; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * printPacket + * + * Print a packet in the hex form + * + * @param string $packet + * @return string + */ + private function printPacket($packet) + { + $str = 'Packet: '; + for ($i = 0, $len = strlen($packet); $i < $len; $i++) { + $str .= $this->byte2hex(ord($packet[$i])); + } + $str .= "\n"; + return $str; + } + + /** + * byte2hex + * + * Parse data and get it to the Hex form + * + * @param int $value + * @return string + */ + private function byte2hex($value) + { + $h = dechex(($value >> 4) & 0x0F); + $l = dechex($value & 0x0F); + return "$h$l"; + } + + /** + * send + * + * Send the packet via Modbus + * + * @param string $packet + */ + private function send($packet) + { + socket_write($this->sock, $packet, strlen($packet)); + $this->status .= "Send\n"; + } + + /** + * rec + * + * Receive data from the socket + * + * @return bool + * @throws Exception + */ + private function rec() + { + socket_set_nonblock($this->sock); + $readsocks[] = $this->sock; + $writesocks = null; + $exceptsocks = null; + $rec = ""; + $totalReadTimeout = $this->timeout_sec; + $lastAccess = microtime(true); + $readTout = $this->secsToSecUsecArray($this->socket_read_timeout_sec); + + while (false !== socket_select($readsocks, $writesocks, $exceptsocks, $readTout['sec'], $readTout['usec'])) { + $this->status .= "Wait data ... \n"; + if (in_array($this->sock, $readsocks)) { + if (@socket_recv($this->sock, $rec, 2000, 0)) { // read max 2000 bytes + $this->status .= "Data received \n"; + return $rec; + } + $lastAccess = microtime(true); + } else { + $timeSpentWaiting = microtime(true) - $lastAccess; + if ($timeSpentWaiting >= $totalReadTimeout) { + throw new Exception( + "Watchdog time expired [ $totalReadTimeout sec ]!!! " . + "Connection to $this->host:$this->port is not established." + ); + } + } + $readsocks[] = $this->sock; + } + + return null; + } + + /** + * readCoilsParser + * + * FC 1 response parser + * + * @param string $packet + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + private function readCoilsParser($packet, $quantity) + { + $data = array(); + // check Response code + $this->responseCode($packet); + // get data from stream + for ($i = 0, $len = ord($packet[8]); $i < $len; $i++) { + $data[$i] = ord($packet[9 + $i]); + } + // get bool values to array + $data_boolean_array = array(); + $di = 0; + foreach ($data as $value) { + for ($i = 0; $i < 8; $i++) { + if ($di == $quantity) { + continue; + } + // get boolean value + $v = ($value >> $i) & 0x01; + // build boolean array + if ($v == 0) { + $data_boolean_array[] = false; + } else { + $data_boolean_array[] = true; + } + $di++; + } + } + return $data_boolean_array; + } + + /** + * responseCode + * + * Check the Modbus response code + * + * @param string $packet + * @return bool + * @throws Exception + */ + private function responseCode($packet) + { + if ((ord($packet[7]) & 0x80) > 0) { + // failure code + $failure_code = ord($packet[8]); + // failure code strings + $failures = array( + 0x01 => 'ILLEGAL FUNCTION', + 0x02 => 'ILLEGAL DATA ADDRESS', + 0x03 => 'ILLEGAL DATA VALUE', + 0x04 => 'SLAVE DEVICE FAILURE', + 0x05 => 'ACKNOWLEDGE', + 0x06 => 'SLAVE DEVICE BUSY', + 0x08 => 'MEMORY PARITY ERROR', + 0x0A => 'GATEWAY PATH UNAVAILABLE', + 0x0B => 'GATEWAY TARGET DEVICE FAILED TO RESPOND', + ); + // get failure string + if (array_key_exists($failure_code, $failures)) { + $failure_str = $failures[$failure_code]; + } else { + $failure_str = 'UNDEFINED FAILURE CODE'; + } + // exception response + throw new Exception("Modbus response error code: $failure_code ($failure_str)"); + } else { + $this->status .= "Modbus response error code: NOERROR\n"; + return true; + } + } + + /** + * disconnect + * + * Disconnect the socket + */ + private function disconnect() + { + socket_close($this->sock); + $this->status .= "Disconnected\n"; + } + + /** + * fc2 + * + * Alias to {@link readInputDiscretes} method + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + public function fc2($unitId, $reference, $quantity) + { + return $this->readInputDiscretes($unitId, $reference, $quantity); + } + + /** + * readInputDiscretes + * + * Modbus function FC 2(0x02) - Read Input Discretes + * + * Reads {@link $quantity} of Inputs (boolean) from reference + * {@link $reference} of a memory of a Modbus device given by + * {@link $unitId}. + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + public function readInputDiscretes($unitId, $reference, $quantity) + { + $this->status .= "readInputDiscretes: START\n"; + // connect + $this->connect(); + // send FC 2 + $packet = $this->readInputDiscretesPacketBuilder($unitId, $reference, $quantity); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $receivedData = $this->readInputDiscretesParser($rpacket, $quantity); + // disconnect + $this->disconnect(); + $this->status .= "readInputDiscretes: DONE\n"; + // return + return $receivedData; + } + + /** + * readInputDiscretesPacketBuilder + * + * FC2 packet builder - read coils + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return string + */ + private function readInputDiscretesPacketBuilder($unitId, $reference, $quantity) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(2); // FC 2 = 2(0x02) + // build body - read section + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($quantity); // quantity + $dataLen += 5; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // lenght + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * readInputDiscretesParser + * + * FC 2 response parser, alias to FC 1 parser i.e. readCoilsParser. + * + * @param string $packet + * @param int $quantity + * @return bool[] + * @throws \Exception + */ + private function readInputDiscretesParser($packet, $quantity) + { + return $this->readCoilsParser($packet, $quantity); + } + + /** + * fc3 + * + * Alias to {@link readMultipleRegisters} method. + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return false|array + * @throws \Exception + */ + public function fc3($unitId, $reference, $quantity) + { + return $this->readMultipleRegisters($unitId, $reference, $quantity); + } + + /** + * readMultipleRegisters + * + * Modbus function FC 3(0x03) - Read Multiple Registers. + * + * This function reads {@link $quantity} of Words (2 bytes) from reference + * {@link $referenceRead} of a memory of a Modbus device given by + * {@link $unitId}. + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory to read data (e.g. in device WAGO 750-841, memory MW0 + * starts at address 12288). + * @param int $quantity Amounth of the data to be read from device. + * @return false|array Success flag or array of received data. + * @throws \Exception + */ + public function readMultipleRegisters($unitId, $reference, $quantity) + { + $this->status .= "readMultipleRegisters: START\n"; + // connect + $this->connect(); + // send FC 3 + $packet = $this->readMultipleRegistersPacketBuilder($unitId, $reference, $quantity); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $receivedData = $this->readMultipleRegistersParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "readMultipleRegisters: DONE\n"; + // return + return $receivedData; + } + + /** + * readMultipleRegistersPacketBuilder + * + * Packet FC 3 builder - read multiple registers + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return string + */ + private function readMultipleRegistersPacketBuilder($unitId, $reference, $quantity) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(3); // FC 3 = 3(0x03) + // build body - read section + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($quantity); // quantity + $dataLen += 5; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * readMultipleRegistersParser + * + * FC 3 response parser + * + * @param string $packet + * @return array + * @throws \Exception + */ + private function readMultipleRegistersParser($packet) + { + $data = array(); + // check Response code + $this->responseCode($packet); + // get data + for ($i = 0, $len = ord($packet[8]); $i < $len; $i++) { + $data[$i] = ord($packet[9 + $i]); + } + return $data; + } + + /** + * fc4 + * + * Alias to {@link readMultipleInputRegisters} method. + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return false|array + * @throws \Exception + */ + public function fc4($unitId, $reference, $quantity) + { + return $this->readMultipleInputRegisters($unitId, $reference, $quantity); + } + + /** + * readMultipleInputRegisters + * + * Modbus function FC 4(0x04) - Read Multiple Input Registers. + * + * This function reads {@link $quantity} of Words (2 bytes) from reference + * {@link $referenceRead} of a memory of a Modbus device given by + * {@link $unitId}. + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory to read data. + * @param int $quantity Amounth of the data to be read from device. + * @return false|array Success flag or array of received data. + * @throws \Exception + */ + public function readMultipleInputRegisters($unitId, $reference, $quantity) + { + $this->status .= "readMultipleInputRegisters: START\n"; + // connect + $this->connect(); + // send FC 4 + $packet = $this->readMultipleInputRegistersPacketBuilder($unitId, $reference, $quantity); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $receivedData = $this->readMultipleInputRegistersParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "readMultipleInputRegisters: DONE\n"; + // return + return $receivedData; + } + + /** + * readMultipleInputRegistersPacketBuilder + * + * Packet FC 4 builder - read multiple input registers + * + * @param int $unitId + * @param int $reference + * @param int $quantity + * @return string + */ + private function readMultipleInputRegistersPacketBuilder($unitId, $reference, $quantity) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(4); // FC 4 = 4(0x04) + // build body - read section + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($quantity); // quantity + $dataLen += 5; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); // unit ID + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * readMultipleInputRegistersParser + * + * FC 4 response parser + * + * @param string $packet + * @return array + * @throws \Exception + */ + private function readMultipleInputRegistersParser($packet) + { + $data = array(); + // check Response code + $this->responseCode($packet); + // get data + for ($i = 0, $len = ord($packet[8]); $i < $len; $i++) { + $data[$i] = ord($packet[9 + $i]); + } + return $data; + } + + /** + * fc5 + * + * Alias to {@link writeSingleCoil} method + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return bool + * @throws \Exception + */ + public function fc5($unitId, $reference, $data) + { + return $this->writeSingleCoil($unitId, $reference, $data); + } + + /** + * writeSingleCoil + * + * Modbus function FC5(0x05) - Write Single Register. + * + * This function writes {@link $data} single coil at {@link $reference} position of + * memory of a Modbus device given by {@link $unitId}. + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at address 12288) + * address 12288) + * @param array $data value to be written (TRUE|FALSE). + * @return bool Success flag + * @throws \Exception + */ + public function writeSingleCoil($unitId, $reference, $data) + { + $this->status .= "writeSingleCoil: START\n"; + // connect + $this->connect(); + // send FC5 + $packet = $this->writeSingleCoilPacketBuilder($unitId, $reference, $data); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $this->writeSingleCoilParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "writeSingleCoil: DONE\n"; + return true; + } + + /** + * writeSingleCoilPacketBuilder + * + * Packet builder FC5 - WRITE single register + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return string + */ + private function writeSingleCoilPacketBuilder($unitId, $reference, $data) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + foreach ($data as $key => $dataitem) { + if ($dataitem == true) { + $buffer1 = IecType::iecINT(0xFF00); + } else { + $buffer1 = IecType::iecINT(0x0000); + } + } + $dataLen += 2; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(5); // FC5 = 5(0x05) + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $dataLen += 3; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // lenght + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * writeSingleCoilParser + * + * FC5 response parser + * + * @param string $packet + * @return bool + * @throws \Exception + */ + private function writeSingleCoilParser($packet) + { + $this->responseCode($packet); + return true; + } + + /** + * fc6 + * + * Alias to {@link writeSingleRegister} method + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return bool + * @throws \Exception + */ + public function fc6($unitId, $reference, $data) + { + return $this->writeSingleRegister($unitId, $reference, $data); + } + + /** + * writeSingleRegister + * + * Modbus function FC6(0x06) - Write Single Register. + * + * This function writes {@link $data} single word value at {@link $reference} position of + * memory of a Modbus device given by {@link $unitId}. + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at address 12288) + * address 12288) + * @param array $data Array of values to be written. + * @return bool Success flag + * @throws \Exception + */ + public function writeSingleRegister($unitId, $reference, $data) + { + $this->status .= "writeSingleRegister: START\n"; + // connect + $this->connect(); + // send FC6 + $packet = $this->writeSingleRegisterPacketBuilder($unitId, $reference, $data); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $this->writeSingleRegisterParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "writeSingleRegister: DONE\n"; + return true; + } + + /** + * writeSingleRegisterPacketBuilder + * + * Packet builder FC6 - WRITE single register + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return string + */ + private function writeSingleRegisterPacketBuilder($unitId, $reference, array $data) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + foreach ($data as $key => $dataitem) { + $buffer1 .= IecType::iecINT($dataitem); // register values x + $dataLen += 2; + break; + } + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(6); // FC6 = 6(0x06) + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $dataLen += 3; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * writeSingleRegisterParser + * + * FC6 response parser + * + * @param string $packet + * @return bool + * @throws \Exception + */ + private function writeSingleRegisterParser($packet) + { + $this->responseCode($packet); + return true; + } + + /** + * fc15 + * + * Alias to {@link writeMultipleCoils} method + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return bool + * @throws \Exception + */ + public function fc15($unitId, $reference, $data) + { + return $this->writeMultipleCoils($unitId, $reference, $data); + } + + /** + * writeMultipleCoils + * + * Modbus function FC15(0x0F) - Write Multiple Coils + * + * This function writes {@link $data} array at {@link $reference} position of + * memory of a Modbus device given by {@link $unitId}. + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return bool + * @throws \Exception + */ + public function writeMultipleCoils($unitId, $reference, $data) + { + $this->status .= "writeMultipleCoils: START\n"; + // connect + $this->connect(); + // send FC15 + $packet = $this->writeMultipleCoilsPacketBuilder($unitId, $reference, $data); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $this->writeMultipleCoilsParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "writeMultipleCoils: DONE\n"; + return true; + } + + /** + * writeMultipleCoilsPacketBuilder + * + * Packet builder FC15 - Write multiple coils + * + * @param int $unitId + * @param int $reference + * @param array $data + * @return string + */ + private function writeMultipleCoilsPacketBuilder($unitId, $reference, array $data) + { + $dataLen = 0; + // build bool stream to the WORD array + $data_word_stream = array(); + $data_word = 0; + $shift = 0; + for ($i = 0, $len = count($data); $i < $len; $i++) { + if ((($i % 8) === 0) && ($i > 0)) { + $data_word_stream[] = $data_word; + $shift = 0; + $data_word = 0; + $data_word |= (0x01 && $data[$i]) << $shift; + $shift++; + } else { + $data_word |= (0x01 && $data[$i]) << $shift; + $shift++; + } + } + $data_word_stream[] = $data_word; + // show binary stream to status string + foreach ($data_word_stream as $d) { + $this->status .= sprintf("byte=b%08b\n", $d); + } + // build data section + $buffer1 = ""; + foreach ($data_word_stream as $key => $dataitem) { + $buffer1 .= IecType::iecBYTE($dataitem); // register values x + $dataLen += 1; + } + // build body + $buffer2 = ""; + $buffer2 .= IecType::iecBYTE(15); // FC 15 = 15(0x0f) + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT(count($data)); // bit count + $buffer2 .= IecType::iecBYTE((count($data) + 7) / 8); // byte count + $dataLen += 6; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); // unit ID + + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * writeMultipleCoilsParser + * + * FC15 response parser + * + * @param string $packet + * @return bool + * @throws \Exception + */ + private function writeMultipleCoilsParser($packet) + { + $this->responseCode($packet); + return true; + } + + /** + * fc16 + * + * Alias to {@link writeMultipleRegister} method + * + * @param int $unitId + * @param int $reference + * @param array $data + * @param array $dataTypes + * @return bool + * @throws \Exception + */ + public function fc16($unitId, $reference, $data, $dataTypes) + { + return $this->writeMultipleRegister($unitId, $reference, $data, $dataTypes); + } + + /** + * writeMultipleRegister + * + * Modbus function FC16(0x10) - Write Multiple Register. + * + * This function writes {@link $data} array at {@link $reference} position of + * memory of a Modbus device given by {@link $unitId}. + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at address 12288) + * address 12288) + * @param array $data Array of values to be written. + * @param array $dataTypes Array of types of values to be written. The array should consists of string "INT", + * "DINT" and "REAL". + * @return bool Success flag + * @throws \Exception + */ + public function writeMultipleRegister($unitId, $reference, $data, $dataTypes) + { + $this->status .= "writeMultipleRegister: START\n"; + // connect + $this->connect(); + // send FC16 + $packet = $this->writeMultipleRegisterPacketBuilder($unitId, $reference, $data, $dataTypes); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $this->writeMultipleRegisterParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "writeMultipleRegister: DONE\n"; + return true; + } + + /** + * writeMultipleRegisterPacketBuilder + * + * Packet builder FC16 - WRITE multiple register + * e.g.: 4dd90000000d0010300000030603e807d00bb8 + * + * @param int $unitId + * @param int $reference + * @param array $data + * @param array $dataTypes + * @return string + */ + private function writeMultipleRegisterPacketBuilder($unitId, $reference, $data, $dataTypes) + { + $dataLen = 0; + // build data section + $buffer1 = ""; + foreach ($data as $key => $dataitem) { + if ($dataTypes[$key] === 'INT') { + $buffer1 .= IecType::iecINT($dataitem); // register values x + $dataLen += 2; + } elseif ($dataTypes[$key] === 'DINT') { + $buffer1 .= IecType::iecDINT($dataitem, $this->endianness); // register values x + $dataLen += 4; + } elseif ($dataTypes[$key] === 'REAL') { + $buffer1 .= IecType::iecREAL($dataitem, $this->endianness); // register values x + $dataLen += 4; + } else { + $buffer1 .= IecType::iecINT($dataitem); // register values x + $dataLen += 2; + } + } + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(16); // FC 16 = 16(0x10) + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($dataLen / 2); // word count + $buffer2 .= IecType::iecBYTE($dataLen); // byte count + $dataLen += 6; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * writeMultipleRegisterParser + * + * FC16 response parser + * + * @param string $packet + * @return bool + * @throws \Exception + */ + private function writeMultipleRegisterParser($packet) + { + $this->responseCode($packet); + return true; + } + + /** + * fc22 + * + * Alias to {@link maskWriteRegister} method + * + * @param int $unitId + * @param int $reference + * @param int $andMask + * @param int $orMask + * @return bool + * @throws \Exception + */ + public function fc22($unitId, $reference, $andMask, $orMask) + { + return $this->maskWriteRegister($unitId, $reference, $andMask, $orMask); + } + + /** + * maskWriteRegister + * + * Modbus function FC22(0x16) - Mask Write Register. + * + * This function alter single bit(s) at {@link $reference} position of + * memory of a Modbus device given by {@link $unitId}. + * + * Result = (Current Contents AND And_Mask) OR (Or_Mask AND (NOT And_Mask)) + * + * @param int $unitId usually ID of Modbus device + * @param int $reference Reference in the device memory (e.g. in device WAGO 750-841, memory MW0 starts at address + * 12288) + * @param int $andMask + * @param int $orMask + * @return bool Success flag + * @throws \Exception + */ + public function maskWriteRegister($unitId, $reference, $andMask, $orMask) + { + $this->status .= "maskWriteRegister: START\n"; + // connect + $this->connect(); + // send FC22 + $packet = $this->maskWriteRegisterPacketBuilder($unitId, $reference, $andMask, $orMask); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $this->maskWriteRegisterParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "maskWriteRegister: DONE\n"; + return true; + } + + /** + * maskWriteRegisterPacketBuilder + * + * Packet builder FC22 - MASK WRITE register + * + * @param int $unitId + * @param int $reference + * @param int $andMask + * @param int $orMask + * @return string + */ + private function maskWriteRegisterPacketBuilder($unitId, $reference, $andMask, $orMask) + { + $dataLen = 0; + // build data section + $buffer1 = ''; + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(22); // FC 22 = 22(0x16) + $buffer2 .= IecType::iecINT($reference); // refnumber = 12288 + $buffer2 .= IecType::iecINT($andMask); // AND mask + $buffer2 .= IecType::iecINT($orMask); // OR mask + $dataLen += 7; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // lenght + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * maskWriteRegisterParser + * + * FC22 response parser + * + * @param string $packet + * @return bool + * @throws \Exception + */ + private function maskWriteRegisterParser($packet) + { + $this->responseCode($packet); + return true; + } + + /** + * fc23 + * + * Alias to {@link readWriteRegisters} method. + * + * @param int $unitId + * @param int $referenceRead + * @param int $quantity + * @param int $referenceWrite + * @param array $data + * @param array $dataTypes + * @return false|array + * @throws \Exception + */ + public function fc23($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes) + { + return $this->readWriteRegisters($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes); + } + + /** + * readWriteRegisters + * + * Modbus function FC23(0x17) - Read Write Registers. + * + * This function writes {@link $data} array at reference {@link $referenceWrite} + * position of memory of a Modbus device given by {@link $unitId}. Simultanously, + * it returns {@link $quantity} of Words (2 bytes) from reference {@link $referenceRead}. + * + * @param int $unitId usually ID of Modbus device + * @param int $referenceRead Reference in the device memory to read data (e.g. in device WAGO 750-841, memory MW0 starts at address 12288). MW0 starts at address 12288). + * MW0 starts at address 12288). + * @param int $quantity Amounth of the data to be read from device. + * @param int $referenceWrite Reference in the device memory to write data. + * @param array $data Array of values to be written. + * @param array $dataTypes Array of types of values to be written. The array should consists of string "INT", "DINT" and "REAL". + * "DINT" and "REAL". + * @return false|array Success flag or array of data. + * @throws \Exception + */ + public function readWriteRegisters($unitId, $referenceRead, $quantity, $referenceWrite, $data, $dataTypes) + { + $this->status .= "readWriteRegisters: START\n"; + // connect + $this->connect(); + // send FC23 + $packet = $this->readWriteRegistersPacketBuilder( + $unitId, $referenceRead, $quantity, $referenceWrite, $data, + $dataTypes + ); + $this->status .= $this->printPacket($packet); + $this->send($packet); + // receive response + $rpacket = $this->rec(); + $this->status .= $this->printPacket($rpacket); + // parse packet + $receivedData = $this->readWriteRegistersParser($rpacket); + // disconnect + $this->disconnect(); + $this->status .= "writeMultipleRegister: DONE\n"; + // return + return $receivedData; + } + + /** + * readWriteRegistersPacketBuilder + * + * Packet FC23 builder - READ WRITE registers + * + * @param int $unitId + * @param int $referenceRead + * @param int $quantity + * @param int $referenceWrite + * @param array $data + * @param array $dataTypes + * @return string + */ + private function readWriteRegistersPacketBuilder( + $unitId, + $referenceRead, + $quantity, + $referenceWrite, + $data, + $dataTypes + ) { + + $dataLen = 0; + // build data section + $buffer1 = ''; + foreach ($data as $key => $dataitem) { + if ($dataTypes[$key] === 'INT') { + $buffer1 .= IecType::iecINT($dataitem); // register values x + $dataLen += 2; + } elseif ($dataTypes[$key] === 'DINT') { + $buffer1 .= IecType::iecDINT($dataitem, $this->endianness); // register values x + $dataLen += 4; + } elseif ($dataTypes[$key] === 'REAL') { + $buffer1 .= IecType::iecREAL($dataitem, $this->endianness); // register values x + $dataLen += 4; + } else { + $buffer1 .= IecType::iecINT($dataitem); // register values x + $dataLen += 2; + } + } + // build body + $buffer2 = ''; + $buffer2 .= IecType::iecBYTE(23); // FC 23 = 23(0x17) + // build body - read section + $buffer2 .= IecType::iecINT($referenceRead); // refnumber = 12288 + $buffer2 .= IecType::iecINT($quantity); // quantity + // build body - write section + $buffer2 .= IecType::iecINT($referenceWrite); // refnumber = 12288 + $buffer2 .= IecType::iecINT($dataLen / 2); // word count + $buffer2 .= IecType::iecBYTE($dataLen); // byte count + $dataLen += 10; + // build header + $buffer3 = ''; + $buffer3 .= IecType::iecINT(mt_rand(0, 65000)); // transaction ID + $buffer3 .= IecType::iecINT(0); // protocol ID + $buffer3 .= IecType::iecINT($dataLen + 1); // length + $buffer3 .= IecType::iecBYTE($unitId); //unit ID + + // return packet string + return $buffer3 . $buffer2 . $buffer1; + } + + /** + * readWriteRegistersParser + * + * FC23 response parser + * + * @param string $packet + * @return array|false + * @throws \Exception + */ + private function readWriteRegistersParser($packet) + { + $data = array(); + // if not exception + if (!$this->responseCode($packet)) { + return false; + } + // get data + for ($i = 0, $len = ord($packet[8]); $i < $len; $i++) { + $data[$i] = ord($packet[9 + $i]); + } + return $data; + } + + /** + * Set data receive timeout. + * Writes property timeout_sec + * + * @param float $seconds seconds + */ + public function setTimeout($seconds) + { + $this->timeout_sec = $seconds; + } + + /** + * Set socket read/write timeout. Null = no change. + * + * @param float|null $read_timeout_sec data read timeout (seconds, default 0.3) + * @param float|null $write_timeout_sec data write timeout (seconds, default 1.0) + * @internal param float $seconds seconds + */ + public function setSocketTimeout($read_timeout_sec, $write_timeout_sec) + { + // Set read timeout if given + if ($read_timeout_sec !== null) { + $this->socket_read_timeout_sec = $read_timeout_sec; + } + + // Set write timeout if given + if ($write_timeout_sec !== null) { + $this->socket_write_timeout_sec = $write_timeout_sec; + } + } } diff --git a/src/ModbusMasterTcp.php b/src/ModbusMasterTcp.php index e524af8..1296fac 100644 --- a/src/ModbusMasterTcp.php +++ b/src/ModbusMasterTcp.php @@ -8,14 +8,12 @@ namespace PHPModbus; * This source file is subject to the "PhpModbus license" that is bundled * with this package in the file license.txt. * - * - * @copyright Copyright (c) 2004, 2012 Jan Krakora - * @license PhpModbus license - * @category Phpmodbus - * @tutorial Phpmodbus.pkg - * @package Phpmodbus - * @version $id$ - * + * @copyright Copyright (c) 2004, 2012 Jan Krakora + * @license PhpModbus license + * @category Phpmodbus + * @tutorial Phpmodbus.pkg + * @package Phpmodbus + * @version $id$ */ /** @@ -23,29 +21,21 @@ namespace PHPModbus; * * This class deals with the MODBUS master using TCP. Extends ModbusMaster class. * - * Implemented MODBUS functions: - * - FC 1: read coils - * - FC 3: read multiple registers - * - FC 15: write multiple coils - * - FC 16: write multiple registers - * - FC 23: read write registers - * - * @author Jan Krakora - * @copyright Copyright (c) 2004, 2012 Jan Krakora - * @package Phpmodbus - * + * @author Jan Krakora + * @copyright Copyright (c) 2004, 2012 Jan Krakora + * @package Phpmodbus */ class ModbusMasterTcp extends ModbusMaster { - /** - * ModbusMasterTcp - * - * This is the constructor that defines {@link $host} IP address of the object. - * - * @param String $host An IP address of a Modbus TCP device. E.g. "192.168.1.1". - */ - public function __construct($host) - { - parent::__construct($host, "TCP"); - } + /** + * ModbusMasterTcp + * + * This is the constructor that defines {@link $host} IP address of the object. + * + * @param String $host An IP address of a Modbus TCP device. E.g. "192.168.1.1". + */ + public function __construct($host) + { + parent::__construct($host, "TCP"); + } } diff --git a/src/ModbusMasterUdp.php b/src/ModbusMasterUdp.php index 159244f..59beac8 100644 --- a/src/ModbusMasterUdp.php +++ b/src/ModbusMasterUdp.php @@ -8,14 +8,12 @@ namespace PHPModbus; * This source file is subject to the "PhpModbus license" that is bundled * with this package in the file license.txt. * - * - * @copyright Copyright (c) 2004, 2012 Jan Krakora - * @license PhpModbus license - * @category Phpmodbus - * @tutorial Phpmodbus.pkg - * @package Phpmodbus - * @version $id$ - * + * @copyright Copyright (c) 2004, 2012 Jan Krakora + * @license PhpModbus license + * @category Phpmodbus + * @tutorial Phpmodbus.pkg + * @package Phpmodbus + * @version $id$ */ /** @@ -23,29 +21,21 @@ namespace PHPModbus; * * This class deals with the MODBUS master using UDP stack. * - * Implemented MODBUS master functions: - * - FC 1: read coils - * - FC 3: read multiple registers - * - FC 15: write multiple coils - * - FC 16: write multiple registers - * - FC 23: read write registers - * - * @author Jan Krakora - * @copyright Copyright (c) 2004, 2012 Jan Krakora - * @package Phpmodbus - * + * @author Jan Krakora + * @copyright Copyright (c) 2004, 2012 Jan Krakora + * @package Phpmodbus */ class ModbusMasterUdp extends ModbusMaster { - /** - * ModbusMasterUdp - * - * This is the constructor that defines {@link $host} IP address of the object. - * - * @param String $host An IP address of a Modbus UDP device. E.g. "192.168.1.1". - */ - public function __construct($host) - { - parent::__construct($host, "UDP"); - } + /** + * ModbusMasterUdp + * + * This is the constructor that defines {@link $host} IP address of the object. + * + * @param String $host An IP address of a Modbus UDP device. E.g. "192.168.1.1". + */ + public function __construct($host) + { + parent::__construct($host, "UDP"); + } } diff --git a/src/PhpType.php b/src/PhpType.php index fbe58a7..51d98ff 100644 --- a/src/PhpType.php +++ b/src/PhpType.php @@ -1,6 +1,7 @@ 0)) { - $int = 0xFFFF8000 | $int; - } - // Convert the value - return (int)self::dword2signedInt($int); - } + /** + * bytes2signedInt + * + * The function converts array of 2 or 4 bytes to signed integer. + * The return value depends on order of the input bytes (endianning). + * + * @param array $values + * @param bool $bigEndian + * @return int + */ + public static function bytes2signedInt($values, $bigEndian = 0) + { + $data = array(); + $int = 0; + // Set the array to correct form + $data = self::checkData($values); + // Combine bytes + $int = self::combineBytes($data, $bigEndian); + // In the case of signed 2 byte value convert it to 4 byte one + if ((count($values) === 2) && ((0x8000 & $int) > 0)) { + $int = 0xFFFF8000 | $int; + } + // Convert the value + return (int)self::dword2signedInt($int); + } - /** - * bytes2unsignedInt - * - * The function converts array of 2 or 4 bytes to unsigned integer. - * The return value depends on order of the input bytes (endianning). - * - * @param array $values - * @param bool $bigEndian - * @return int|float - */ - public static function bytes2unsignedInt($values, $bigEndian = 0) - { - $data = array(); - $int = 0; - // Set the array to correct form - $data = self::checkData($values); - // Combine bytes - $int = self::combineBytes($data, $bigEndian); - // Convert the value - return self::dword2unsignedInt($int); - } + /** + * bytes2unsignedInt + * + * The function converts array of 2 or 4 bytes to unsigned integer. + * The return value depends on order of the input bytes (endianning). + * + * @param array $values + * @param bool $bigEndian + * @return int|float + */ + public static function bytes2unsignedInt($values, $bigEndian = 0) + { + $data = array(); + $int = 0; + // Set the array to correct form + $data = self::checkData($values); + // Combine bytes + $int = self::combineBytes($data, $bigEndian); + // Convert the value + return self::dword2unsignedInt($int); + } - /** - * bytes2string - * - * The function converts an values array to the string. The function detects - * the end of the string by 0x00 character as defined by string standards. - * - * @param array $values - * @param bool $bigEndian - * @return string - */ - public static function bytes2string($values, $bigEndian = 0) - { - // Prepare string variable - $str = ""; - // Parse the received data word array - for ($i = 0; $i < count($values); $i += 2) { - if ($bigEndian) { - if ($values[$i] != 0) { - $str .= chr($values[$i]); - } else { - break; - } - if ($values[$i + 1] != 0) { - $str .= chr($values[$i + 1]); - } else { - break; - } - } else { - if ($values[$i + 1] != 0) { - $str .= chr($values[$i + 1]); - } else { - break; - } - if ($values[$i] != 0) { - $str .= chr($values[$i]); - } else { - break; - } - } - } - // return string - return $str; - } + /** + * bytes2string + * + * The function converts an values array to the string. The function detects + * the end of the string by 0x00 character as defined by string standards. + * + * @param array $values + * @param bool $bigEndian + * @return string + */ + public static function bytes2string($values, $bigEndian = 0) + { + // Prepare string variable + $str = ""; + // Parse the received data word array + for ($i = 0; $i < count($values); $i += 2) { + if ($bigEndian) { + if ($values[$i] != 0) { + $str .= chr($values[$i]); + } else { + break; + } + if ($values[$i + 1] != 0) { + $str .= chr($values[$i + 1]); + } else { + break; + } + } else { + if ($values[$i + 1] != 0) { + $str .= chr($values[$i + 1]); + } else { + break; + } + if ($values[$i] != 0) { + $str .= chr($values[$i]); + } else { + break; + } + } + } + // return string + return $str; + } - /** - * real2float - * - * This function converts a value in IEC-1131 REAL single precision form to float. - * - * For more see [{@link http://en.wikipedia.org/wiki/Single_precision Single precision on Wiki}] or - * [{@link http://de.php.net/manual/en/function.base-convert.php PHP base_convert function commentary}, Todd Stokes - * @ Georgia Tech 21-Nov-2007] or - * [{@link http://www.php.net/manual/en/function.pack.php PHP pack/unpack functionality}] - * - * @param int $value in IEC REAL data type to be converted - * @return float float value - */ - private static function real2float($value) - { - // get unsigned long - $ulong = pack("L", $value); - // set float - $float = unpack("f", $ulong); + /** + * real2float + * + * This function converts a value in IEC-1131 REAL single precision form to float. + * + * For more see [{@link http://en.wikipedia.org/wiki/Single_precision Single precision on Wiki}] or + * [{@link http://de.php.net/manual/en/function.base-convert.php PHP base_convert function commentary}, Todd Stokes + * @ Georgia Tech 21-Nov-2007] or + * [{@link http://www.php.net/manual/en/function.pack.php PHP pack/unpack functionality}] + * + * @param int $value in IEC REAL data type to be converted + * @return float float value + */ + private static function real2float($value) + { + // get unsigned long + $ulong = pack('L', $value); + // set float + $float = unpack('f', $ulong); - return $float[1]; - } + return $float[1]; + } - /** - * dword2signedInt - * - * Switch double word to signed integer - * - * @param int $value - * @return int - */ - private static function dword2signedInt($value) - { - if ((0x80000000 & $value) != 0) { - return -(0x7FFFFFFF & ~$value) - 1; - } else { - return (0x7FFFFFFF & $value); - } - } + /** + * dword2signedInt + * + * Switch double word to signed integer + * + * @param int $value + * @return int + */ + private static function dword2signedInt($value) + { + if ((0x80000000 & $value) !== 0) { + return -(0x7FFFFFFF & ~$value) - 1; + } else { + return (0x7FFFFFFF & $value); + } + } - /** - * dword2signedInt - * - * Switch double word to unsigned integer - * - * @param int $value - * @return int|float - */ - private static function dword2unsignedInt($value) - { - if ((0x80000000 & $value) != 0) { - return ((float)(0x7FFFFFFF & $value)) + 2147483648; - } else { - return (int)(0x7FFFFFFF & $value); - } - } + /** + * dword2signedInt + * + * Switch double word to unsigned integer + * + * @param int $value + * @return int|float + */ + private static function dword2unsignedInt($value) + { + if ((0x80000000 & $value) !== 0) { + return ((float)(0x7FFFFFFF & $value)) + 2147483648; + } else { + return (int)(0x7FFFFFFF & $value); + } + } - /** - * checkData - * - * Check if the data variable is array, and check if the values are numeric - * - * @param int[] $data - * @return int - * @throws Exception - */ - private static function checkData($data) - { - // Check the data - if (!is_array($data) || - count($data) < 2 || - count($data) > 4 || - count($data) == 3 - ) { - throw new Exception('The input data should be an array of 2 or 4 bytes.'); - } - // Fill the rest of array by zeroes - if (count($data) == 2) { - $data[2] = 0; - $data[3] = 0; - } - // Check the values to be number - if (!is_numeric($data[0]) || - !is_numeric($data[1]) || - !is_numeric($data[2]) || - !is_numeric($data[3]) - ) { - throw new Exception('Data are not numeric or the array keys are not indexed by 0,1,2 and 3'); - } + /** + * checkData + * + * Check if the data variable is array, and check if the values are numeric + * + * @param int[] $data + * @return array|\int[] + * @throws Exception + */ + private static function checkData($data) + { + // Check the data + $count = count($data); + if (!is_array($data) + || $count < 2 + || $count > 4 + || $count === 3 + ) { + throw new Exception('The input data should be an array of 2 or 4 bytes.'); + } + // Fill the rest of array by zeroes + if ($count === 2) { + $data[2] = 0; + $data[3] = 0; + } + // Check the values to be number + if (!is_numeric($data[0]) + || !is_numeric($data[1]) + || !is_numeric($data[2]) + || !is_numeric($data[3]) + ) { + throw new Exception('Data are not numeric or the array keys are not indexed by 0,1,2 and 3'); + } - return $data; - } + return $data; + } - /** - * combineBytes - * - * Combine bytes together - * - * @param int $data - * @param bool $bigEndian - * @return int - */ - private static function combineBytes($data, $bigEndian) - { - $value = 0; - // Combine bytes - if ($bigEndian == 0) { - $value = (($data[3] & 0xFF) << 16) | - (($data[2] & 0xFF) << 24) | - (($data[1] & 0xFF)) | - (($data[0] & 0xFF) << 8); - } else { - $value = (($data[3] & 0xFF) << 24) | - (($data[2] & 0xFF) << 16) | - (($data[1] & 0xFF) << 8) | - (($data[0] & 0xFF)); - } + /** + * combineBytes + * + * Combine bytes together + * + * @param int $data + * @param bool $bigEndian + * @return int + */ + private static function combineBytes($data, $bigEndian) + { + $value = 0; + // Combine bytes + if ($bigEndian == 0) { + $value = (($data[3] & 0xFF) << 16) | + (($data[2] & 0xFF) << 24) | + ($data[1] & 0xFF) | + (($data[0] & 0xFF) << 8); + } else { + $value = (($data[3] & 0xFF) << 24) | + (($data[2] & 0xFF) << 16) | + (($data[1] & 0xFF) << 8) | + ($data[0] & 0xFF); + } - return $value; - } + return $value; + } }