diff --git a/README.md b/README.md index 2349b3b..b37d407 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Implementation of the basic functionality of the Modbus TCP and UDP based protocol using PHP. -**NOTE: This is a fork to fix & update the library code (and code alone).** +**NOTE: This is a fork to fix & update the library code.** -> **What's new** -> -> This fork adds a namespace and fixes issues encountered when porting to PHP 7, fixes old MS Windows specific tests +##What's new + +* This fork adds a namespace and fixes issues encountered when porting to PHP 7 +* Removes dependency to [sockets extension](http://at2.php.net/manual/en/book.sockets.php). Now uses built-in [Stream API](http://www.php.net/manual/en/function.stream-socket-client.php) +* Fixes/replaces old MS Windows specific tests ## Implemented features @@ -25,7 +27,6 @@ Implementation of the basic functionality of the Modbus TCP and UDP based protoc ## Requirements - * The PHP extension php_sockets.dll should be enabled (server php.ini file) * PHP 5.5+ diff --git a/composer.json b/composer.json index 012daf7..5730ece 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,7 @@ "description": "PhpModbus with namespaces and updated to PHP 7", "license": "LGPL", "require": { - "php": "^5.5 || ^7.0", - "ext-sockets": "*" + "php": "^5.5 || ^7.0" }, "require-dev": { "react/socket": "~0.4.0", diff --git a/src/ModbusMaster.php b/src/ModbusMaster.php index 95f2fc3..a4349ff 100644 --- a/src/ModbusMaster.php +++ b/src/ModbusMaster.php @@ -76,14 +76,18 @@ class ModbusMaster * @var float Total response timeout (seconds, decimals allowed) */ public $timeout_sec = 5; + /** + * @var float Socket connect timeout (seconds, decimals allowed) + */ + public $socket_connect_timeout_sec = 1; /** * @var float Socket read timeout (seconds, decimals allowed) */ - public $socket_read_timeout_sec = 0.3; + public $socket_read_timeout_sec = 0.3; // 300 ms /** * @var float Socket write timeout (seconds, decimals allowed) */ - public $socket_write_timeout_sec = 1; // 300 ms + public $socket_write_timeout_sec = 1; /** * @var int Endianness codding (0 = little endian = 0, 1 = big endian) */ @@ -470,6 +474,7 @@ class ModbusMaster ->setTimeoutSec($this->timeout_sec) ->setSocketReadTimeoutSec($this->socket_read_timeout_sec) ->setSocketWriteTimeoutSec($this->socket_write_timeout_sec) + ->setSocketConnectTimeoutSec($this->socket_connect_timeout_sec) ->build(); $socket->connect(); diff --git a/src/ModbusSocket.php b/src/ModbusSocket.php index e4bb075..a1fa61a 100644 --- a/src/ModbusSocket.php +++ b/src/ModbusSocket.php @@ -33,14 +33,18 @@ class ModbusSocket * @var float Total response timeout (seconds, decimals allowed) */ protected $timeout_sec = 5; + /** + * @var float Socket connect timeout (seconds, decimals allowed) + */ + protected $socket_connect_timeout_sec = 1; /** * @var float Socket read timeout (seconds, decimals allowed) */ - protected $socket_read_timeout_sec = 0.3; + protected $socket_read_timeout_sec = 0.3; // 300 ms /** * @var float Socket write timeout (seconds, decimals allowed) */ - protected $socket_write_timeout_sec = 1; // 300 ms + protected $socket_write_timeout_sec = 1; /** * @var string Socket protocol (TCP, UDP) */ @@ -56,13 +60,14 @@ class ModbusSocket /** * @var resource Communication socket */ - protected $sock; + private $streamSocket; /** * @var array status messages */ protected $statusMessages = []; - public static function getBuilder() { + public static function getBuilder() + { return new ModbusSocketBuilder(); } @@ -77,44 +82,54 @@ class ModbusSocket */ public 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 InvalidArgumentException("Unknown socket protocol, should be 'TCP' or 'UDP'"); + $protocol = null; + switch ($this->socket_protocol) { + case 'TCP': + case 'UDP': + $protocol = strtolower($this->socket_protocol); + break; + default: + throw new InvalidArgumentException("Unknown socket protocol, should be 'TCP' or 'UDP'"); } - // Bind the client socket to a specific local port + + $opts = []; if (strlen($this->client) > 0) { - $result = socket_bind($this->sock, $this->client, $this->client_port); - if ($result === false) { - throw new IOException( - "socket_bind() failed. Reason: ($result)" . - socket_strerror(socket_last_error($this->sock)) - ); - } else { - $this->statusMessages[] = 'Bound'; - } + // Bind the client stream to a specific local port + $opts = array( + 'socket' => array( + 'bindto' => "{$this->client}:{$this->client_port}", + ), + ); + } + $context = stream_context_create($opts); + + $this->streamSocket = @stream_socket_client( + "$protocol://$this->host:$this->port", + $errno, + $errstr, + $this->socket_connect_timeout_sec, + STREAM_CLIENT_CONNECT, + $context + ); + + if (false === $this->streamSocket) { + $message = "Unable to create client socket to {$protocol}://{$this->host}:{$this->port}: {$errstr}"; + throw new IOException($message, $errno); } - // 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 IOException( - "socket_connect() failed. Reason: ($result)" . - socket_strerror(socket_last_error($this->sock)) - ); - } else { - $this->statusMessages[] = 'Connected'; - return true; + if (strlen($this->client) > 0) { + $this->statusMessages[] = 'Bound'; } + $this->statusMessages[] = 'Connected'; + + stream_set_blocking($this->streamSocket, false); // use non-blocking stream + + $writeTimeoutParts = $this->secsToSecUsecArray($this->socket_write_timeout_sec); + // set as stream timeout as we use 'stream_select' to read data and this method has its own timeout + // this call will only affect our fwrite parts (send data method) + stream_set_timeout($this->streamSocket, $writeTimeoutParts['sec'], $writeTimeoutParts['usec']); + + return true; } /** @@ -127,35 +142,37 @@ class ModbusSocket */ public function receive() { - 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->statusMessages[] = 'Wait data ... '; - if (in_array($this->sock, $readsocks, false)) { - if (@socket_recv($this->sock, $rec, 2000, 0)) { // read max 2000 bytes - $this->statusMessages[] = 'Data received'; - return $rec; + + $readTimeout = $this->secsToSecUsecArray($this->socket_read_timeout_sec); + while (true) { + $read = array($this->streamSocket); + $write = null; + $except = null; + if (false !== stream_select($read, $write, $except, $readTimeout['sec'], $readTimeout['usec'])) { + $this->statusMessages[] = 'Wait data ... '; + + if (in_array($this->streamSocket, $read, false)) { + $data = fread($this->streamSocket, 2048); // read max 2048 bytes + if (!empty($data)) { + $this->statusMessages[] = 'Data received'; + return $data; //FIXME what if we are waiting for more than that? + } + $lastAccess = microtime(true); + } else { + $timeSpentWaiting = microtime(true) - $lastAccess; + if ($timeSpentWaiting >= $totalReadTimeout) { + throw new IOException( + "Watchdog time expired [ {$totalReadTimeout} sec ]!!! " . + "Connection to {$this->host}:{$this->port} is not established." + ); + } } - $lastAccess = microtime(true); } else { - $timeSpentWaiting = microtime(true) - $lastAccess; - if ($timeSpentWaiting >= $totalReadTimeout) { - throw new IOException( - "Watchdog time expired [ $totalReadTimeout sec ]!!! " . - "Connection to $this->host:$this->port is not established." - ); - } + throw new IOException("Failed to read data from {$this->host}:{$this->port}."); } - $readsocks[] = $this->sock; } - return null; } @@ -168,7 +185,7 @@ class ModbusSocket */ public function send($packet) { - socket_write($this->sock, $packet, strlen($packet)); + fwrite($this->streamSocket, $packet, strlen($packet)); $this->statusMessages[] = 'Send'; } @@ -179,8 +196,8 @@ class ModbusSocket */ public function close() { - if (is_resource($this->sock)) { - socket_close($this->sock); + if (is_resource($this->streamSocket)) { + fclose($this->streamSocket); $this->statusMessages[] = 'Disconnected'; } } @@ -322,4 +339,14 @@ class ModbusSocketBuilder extends ModbusSocket return $this; } + /** + * @param float $socket_connect_timeout_sec + * @return ModbusSocketBuilder + */ + public function setSocketConnectTimeoutSec($socket_connect_timeout_sec) + { + $this->modbusSocket->socket_connect_timeout_sec = $socket_connect_timeout_sec; + return $this; + } + } diff --git a/tests/ModbusMaster/ModbusExceptionTest.php b/tests/ModbusMaster/ModbusExceptionTest.php index 3d0db1f..ac7e3a4 100644 --- a/tests/ModbusMaster/ModbusExceptionTest.php +++ b/tests/ModbusMaster/ModbusExceptionTest.php @@ -21,7 +21,7 @@ class ModbusExceptionTest extends MockServerTestCase public function testPortClosedException() { $this->expectException(IOException::class); - $this->expectExceptionMessage('socket_connect() failed. Reason:'); + $this->expectExceptionMessage('Unable to create client socket to'); $modbus = new ModbusMasterTcp('127.0.0.1'); $modbus->setSocketTimeout(0.2, 0.2);