|
|
@ -45,31 +45,38 @@ use Exception; |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
class ModbusMaster |
|
|
|
class ModbusMaster |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
/** @var resource Communication socket */ |
|
|
|
private $sock; |
|
|
|
private $sock; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @var string Modbus device IP address */ |
|
|
|
public $host = "192.168.1.1"; |
|
|
|
public $host = "192.168.1.1"; |
|
|
|
|
|
|
|
|
|
|
|
public $port = "502"; |
|
|
|
/** @var string gateway port */ |
|
|
|
|
|
|
|
public $port = 502; |
|
|
|
|
|
|
|
|
|
|
|
public $client = ""; |
|
|
|
/** @var string (optional) client IP address */ |
|
|
|
|
|
|
|
public $client = ""; // TODO explanation? |
|
|
|
|
|
|
|
|
|
|
|
public $client_port = "502"; |
|
|
|
/** @var string client port */ |
|
|
|
|
|
|
|
public $client_port = 502; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @var string ModbusMaster status messages (echo for debugging) */ |
|
|
|
public $status; |
|
|
|
public $status; |
|
|
|
|
|
|
|
|
|
|
|
public $timeout_sec = 5; // Timeout 5 sec |
|
|
|
/** @var float Total response timeout (seconds, decimals allowed) */ |
|
|
|
|
|
|
|
public $timeout_sec = 5; |
|
|
|
|
|
|
|
|
|
|
|
public $timeout_msec = 0; // milliseconds. is added to $timeout_sec when calculating real read timeout |
|
|
|
/** @var float Socket read timeout (seconds, decimals allowed) */ |
|
|
|
|
|
|
|
public $socket_read_timeout_sec = 0.3; // 300 ms |
|
|
|
|
|
|
|
|
|
|
|
public $socket_read_timeout_usec = 300000; // socket read timeout 300 ms |
|
|
|
/** @var float Socket write timeout (seconds, decimals allowed) */ |
|
|
|
|
|
|
|
public $socket_write_timeout_sec = 1; |
|
|
|
|
|
|
|
|
|
|
|
public $socket_write_timeout_sec = 1; // socket write timeout 1 sec |
|
|
|
/** @var int Endianness codding (0 = little endian = 0, 1 = big endian) */ |
|
|
|
|
|
|
|
public $endianness = 0; // |
|
|
|
|
|
|
|
|
|
|
|
public $socket_write_timeout_usec = 0; // socket write timeout microseconds (must be lower than 1 sec e.g. 1000000) |
|
|
|
/** @var string Socket protocol (TCP, UDP) */ |
|
|
|
|
|
|
|
public $socket_protocol = "UDP"; |
|
|
|
public $endianness = 0; // Endianness codding (little endian == 0, big endian == 1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public $socket_protocol = "UDP"; // Socket protocol (TCP, UDP) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* ModbusMaster |
|
|
|
* ModbusMaster |
|
|
@ -95,6 +102,22 @@ class ModbusMaster |
|
|
|
return "<pre>" . $this->status . "</pre>"; |
|
|
|
return "<pre>" . $this->status . "</pre>"; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 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 |
|
|
|
* |
|
|
|
* |
|
|
@ -119,20 +142,21 @@ class ModbusMaster |
|
|
|
if (strlen($this->client) > 0) { |
|
|
|
if (strlen($this->client) > 0) { |
|
|
|
$result = socket_bind($this->sock, $this->client, $this->client_port); |
|
|
|
$result = socket_bind($this->sock, $this->client, $this->client_port); |
|
|
|
if ($result === false) { |
|
|
|
if ($result === false) { |
|
|
|
throw new Exception("socket_bind() failed.</br>Reason: ($result)" . |
|
|
|
throw new Exception("socket_bind() failed. Reason: ($result)" . |
|
|
|
socket_strerror(socket_last_error($this->sock))); |
|
|
|
socket_strerror(socket_last_error($this->sock))); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
$this->status .= "Bound\n"; |
|
|
|
$this->status .= "Bound\n"; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Socket settings (send/write timeout) |
|
|
|
// Socket settings (send/write timeout) |
|
|
|
socket_set_option($this->sock, SOL_SOCKET, SO_SNDTIMEO, |
|
|
|
$writeTimeout = $this->secsToSecUsecArray($this->socket_write_timeout_sec); |
|
|
|
array('sec' => $this->socket_write_timeout_sec, 'usec' => $this->socket_write_timeout_usec) |
|
|
|
socket_set_option($this->sock, SOL_SOCKET, SO_SNDTIMEO, $writeTimeout); |
|
|
|
); |
|
|
|
|
|
|
|
// Connect the socket |
|
|
|
// Connect the socket |
|
|
|
$result = @socket_connect($this->sock, $this->host, $this->port); |
|
|
|
$result = @socket_connect($this->sock, $this->host, $this->port); |
|
|
|
if ($result === false) { |
|
|
|
if ($result === false) { |
|
|
|
throw new Exception("socket_connect() failed.</br>Reason: ($result)" . |
|
|
|
throw new Exception("socket_connect() failed. Reason: ($result)" . |
|
|
|
socket_strerror(socket_last_error($this->sock))); |
|
|
|
socket_strerror(socket_last_error($this->sock))); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
$this->status .= "Connected\n"; |
|
|
|
$this->status .= "Connected\n"; |
|
|
@ -179,22 +203,24 @@ class ModbusMaster |
|
|
|
$writesocks = null; |
|
|
|
$writesocks = null; |
|
|
|
$exceptsocks = null; |
|
|
|
$exceptsocks = null; |
|
|
|
$rec = ""; |
|
|
|
$rec = ""; |
|
|
|
$timeoutInSeconds = $this->timeout_sec + ($this->timeout_msec / 1000); |
|
|
|
$totalReadTimeout = $this->timeout_sec; |
|
|
|
$lastAccess = microtime(true); |
|
|
|
$lastAccess = microtime(true); |
|
|
|
while (socket_select($readsocks, $writesocks, $exceptsocks, 0, $this->socket_read_timeout_usec) !== false) { |
|
|
|
$readTout = $this->secsToSecUsecArray($this->socket_read_timeout_sec); |
|
|
|
$this->status .= "Wait data ... " . PHP_EOL; |
|
|
|
|
|
|
|
|
|
|
|
while (false !== socket_select($readsocks, $writesocks, $exceptsocks, $readTout['sec'], $readTout['usec'])) { |
|
|
|
|
|
|
|
$this->status .= "Wait data ... \n"; |
|
|
|
if (in_array($this->sock, $readsocks)) { |
|
|
|
if (in_array($this->sock, $readsocks)) { |
|
|
|
while (@socket_recv($this->sock, $rec, 2000, 0)) { |
|
|
|
if (@socket_recv($this->sock, $rec, 2000, 0)) { // read max 2000 bytes |
|
|
|
$this->status .= "Data received" . PHP_EOL; |
|
|
|
$this->status .= "Data received \n"; |
|
|
|
return $rec; |
|
|
|
return $rec; |
|
|
|
} |
|
|
|
} |
|
|
|
$lastAccess = microtime(true); |
|
|
|
$lastAccess = microtime(true); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
$timeSpentWaiting = microtime(true) - $lastAccess; |
|
|
|
$timeSpentWaiting = microtime(true) - $lastAccess; |
|
|
|
if ($timeSpentWaiting >= $timeoutInSeconds) { |
|
|
|
if ($timeSpentWaiting >= $totalReadTimeout) { |
|
|
|
throw new Exception("Watchdog time expired [ " . |
|
|
|
throw new Exception( |
|
|
|
$timeoutInSeconds . " sec]!!! Connection to " . |
|
|
|
"Watchdog time expired [ $totalReadTimeout sec ]!!! " . |
|
|
|
$this->host . " is not established."); |
|
|
|
"Connection to $this->host:$this->port is not established."); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
$readsocks[] = $this->sock; |
|
|
|
$readsocks[] = $this->sock; |
|
|
@ -227,7 +253,7 @@ class ModbusMaster |
|
|
|
0x06 => "SLAVE DEVICE BUSY", |
|
|
|
0x06 => "SLAVE DEVICE BUSY", |
|
|
|
0x08 => "MEMORY PARITY ERROR", |
|
|
|
0x08 => "MEMORY PARITY ERROR", |
|
|
|
0x0A => "GATEWAY PATH UNAVAILABLE", |
|
|
|
0x0A => "GATEWAY PATH UNAVAILABLE", |
|
|
|
0x0B => "GATEWAY TARGET DEVICE FAILED TO RESPOND" |
|
|
|
0x0B => "GATEWAY TARGET DEVICE FAILED TO RESPOND", |
|
|
|
); |
|
|
|
); |
|
|
|
// get failure string |
|
|
|
// get failure string |
|
|
|
if (key_exists($failure_code, $failures)) { |
|
|
|
if (key_exists($failure_code, $failures)) { |
|
|
@ -1289,7 +1315,8 @@ class ModbusMaster |
|
|
|
$referenceWrite, |
|
|
|
$referenceWrite, |
|
|
|
$data, |
|
|
|
$data, |
|
|
|
$dataTypes |
|
|
|
$dataTypes |
|
|
|
) { |
|
|
|
) |
|
|
|
|
|
|
|
{ |
|
|
|
$dataLen = 0; |
|
|
|
$dataLen = 0; |
|
|
|
// build data section |
|
|
|
// build data section |
|
|
|
$buffer1 = ""; |
|
|
|
$buffer1 = ""; |
|
|
@ -1336,7 +1363,7 @@ class ModbusMaster |
|
|
|
* FC23 response parser |
|
|
|
* FC23 response parser |
|
|
|
* |
|
|
|
* |
|
|
|
* @param string $packet |
|
|
|
* @param string $packet |
|
|
|
* @return array |
|
|
|
* @return array|false |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private function readWriteRegistersParser($packet) |
|
|
|
private function readWriteRegistersParser($packet) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -1387,15 +1414,33 @@ class ModbusMaster |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* setTimeoutMs |
|
|
|
* Set data receive timeout. |
|
|
|
|
|
|
|
* Writes property timeout_sec |
|
|
|
* |
|
|
|
* |
|
|
|
* Set timeout used when receiving data in milliseconds. NB: Method overrides $timeout_sec variable value. |
|
|
|
* @param float $seconds seconds |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public function setTimeout($seconds) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
$this->timeout_sec = $seconds; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Set socket read/write timeout. Null = no change. |
|
|
|
* |
|
|
|
* |
|
|
|
* @param integer $milliseconds milliseconds |
|
|
|
* @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 setTimeoutMs($milliseconds) |
|
|
|
public function setSocketTimeout($read_timeout_sec, $write_timeout_sec) |
|
|
|
{ |
|
|
|
{ |
|
|
|
$this->timeout_sec = 0; |
|
|
|
// Set read timeout if given |
|
|
|
$this->timeout_msec = $milliseconds; |
|
|
|
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; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|