aldas / modbus-tcp-client

PHP client for Modbus TCP and Modbus RTU over TCP (can be used for serial)
Apache License 2.0
191 stars 55 forks source link

Timing problem with higher level API #133

Closed jonofe closed 1 year ago

jonofe commented 1 year ago

Hi guys, I have a problem, when using higher level API in comparison to low level API. I'm reading from a WAGO energy meter via modbus. The energy meter is connected via a Waveshare RS485 to RJ45 Ethernet Converter Module, Industrial Rail-Mount Module. When using low level API, I'm able to read every 200-500ms and I'm getting always updated values. When using the higher level API, I'm getting around 10 times the same value and then a "read total timeout expired".

What is really strange is, when I'm reading via the higher level API in a loop with the behaviour as mentioned above and then I'm starting a parallel connection to the modbus converter with for example QModbusMaster (Windows TCP Modbus Client), all of a sudden the higher level API is working correctly, means getting updated data every 200ms. When disconnecting the QModusMaster it's switching to the timeout behaviour. Same is true for modpoll. If I run modpoll in parallel, the higher level API is working correctly, when stopped, it changes back to the faulty behaviour. Again more strange is, that I only need a connection via QModbusMaster, I don't need to read data to make my higher level API script working.

Here is my code with higher level API ($option=1) and with low level API ($option=2). I'm calling the script with 2 parameters: 1st parameter: $interval in ms to read via Modbus in a loop 2nd parameter: $option: 1=higher level API / 2=low level API

<?php
$option = 1;
$interval = 1000;

if ($argc > 1)
    $interval = $argv[1];
if ($argc > 2)
    $option = $argv[2];

$ip = '10.0.80.4';
$port = 502;
$uid = 3;

require_once  '/usr/local/edomi/main/include/php/modbus-tcp-client/vendor/autoload.php';
use ModbusTcpClient\Composer\Read\ReadRegistersBuilder;
use ModbusTcpClient\Utils\Packet;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Network\NonBlockingClient;
use ModbusTcpClient\Utils\Types;
use ModbusTcpClient\Network\BinaryStreamConnection;
use ModbusTcpClient\Packet\ModbusFunction\WriteSingleRegisterRequest;
use ModbusTcpClient\Packet\ModbusFunction\WriteSingleRegisterResponse;
use ModbusTcpClient\Packet\ResponseFactory;
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest;
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersResponse;
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;

//
// higher level API
//
if ($option==1) {
    $requests = ReadRegistersBuilder::newReadHoldingRegisters()
        ->allFromArray([
            ['uri' => 'tcp://' . $ip . ':' . $port, 'unitId' => $uid, 'type' => 'float', 'address' => 0x5012, 'name' => 'Totale Wirkleistung', 'endian' => Endian::BIG_ENDIAN],
        ])->build(); // returns array of 3 requests
    $client = new NonBlockingClient();
    while (1) {
        try {
            $response = $client->sendRequests($requests);
            $data = $response->getData();
            echo "Wirkleistung: " . round($data['Totale Wirkleistung'] * 1000) . " W" . PHP_EOL;
            usleep($interval * 1000);
        } catch (Exception $exception) {
            echo "EXCEPTION: " . $exception->getMessage() . PHP_EOL;
        }
    }
}
//
// low level API
//
elseif ($option==2) {
    $connection = BinaryStreamConnection::getBuilder()
        ->setHost($ip)
        ->setPort($port)
        ->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
        ->setWriteTimeoutSec(0.5) // timeout when writing/sending packet to the server
        ->setReadTimeoutSec(0.3)
        ->build();

    $packet = new ReadHoldingRegistersRequest(0x5012, 2, $uid); //create FC3 request packet

    while (1) {
        try {
            $binaryData = $connection->connect()->sendAndReceive($packet);
            $response = ResponseFactory::parseResponseOrThrow($binaryData);
            foreach ($response->asDoubleWords() as $doubleWord) {
                $data = $doubleWord->getFloat(Endian::BIG_ENDIAN);
                echo "Wirkleistung: " . round($data * 1000) . " W" . PHP_EOL;
            }
            usleep($interval * 1000);
        } catch (Exception $exception) {
            echo $exception->getMessage() . PHP_EOL;
        }
    }
}

How can a secondary Modbus connection (QModbusMaster or modpoll) influence the behaviour of the higher level API requests. Is there something missing in my higher level API code? I tried all the different timing settings, without any change.

Any help is very much appreciated!

aldas commented 1 year ago

Hi,

For me this seems to be question about difference between NonBlockingClient and BinaryStreamConnection. Let me try to explain

BinaryStreamConnection is mean to transfer single modbus packet. It opens single connection and reads data from it. This means that during $connection->sendAndReceive($packet) you have single connection open. Note: $connection->close()/connect()

NonBlockingClient is meant to transer multiple packets at the same time. It opens connection for each packet and waits until all packet connections have received their responses. It handles $connection->close()/connect() for you. NB: NonBlockingClient uses internally BinaryStreamConnection for its connection.

Both these use same code to receive data from the connection

At the moment I see some differences between these 2 in your code - they have different timeout options.

Try to provide NonBlockingClient same options has BinaryStreamConnection has.

    $client = new NonBlockingClient([
        'connectTimeoutSec' => 1.5,
        'readTimeoutSec' => 0.5,
        'writeTimeoutSec' => 0.5,
    ]);

Also worth noting: these embbeded devices have limitations how many open connections and lingering connections they can have. As NonBlockingClient calls $connection->close() inside sendRequests and BinaryStreamConnection needs explicit $connection->close() call (that you do no have inside for loop) - you have different amount of closed sockets after couple for loop iterations. I do not know this could make the difference at the moment but I have experienced that closing/reusing connections is a big deal for these low-powered devices.

but I assume at the moment that problems is about these timeout. Your NonBlockingClient block defaults to smaller timeouts.

jonofe commented 1 year ago

Martti, thanks for your feedback. I took it into consideration for further debugging with the following results. I'm now able to reproduce the "faulty" behaviour by using the low level API. This is possible by placing a $connection->close() inside the while loop. This means, when closing the connection explicitely (and no other connection to the Modbus Master ist open, then I get this timeout behaviour (getting 12 times the same value followed by an timeout exception). When I look into the connection states of my TCP connections I find a lot of connection in the TIME_WAIT state, i.e. my client (low level API or high level API) has closed connection and is waiting whether delayed packets will still arrive. New connections seem to be delayed then by 6 seconds, so for 6 seconds the Modbus Master sends the old data and then the timeout happens and a new connection is allowed. What is not clear to me is, why another connection from another PC to the Modbus Master is preventing that behaviour to happen. In general it seems to me a kind of problem with the TCP connection management of my TCP Modbus Master.

Is there any way to use the high level API by reusing an existing connection?

aldas commented 1 year ago

I think I have not done good job with the readme. Higher lever API just refers to some components that do something extra. And you can mix these *builder components/classes with lower level network components/classes and vice versa.

See https://github.com/aldas/modbus-tcp-client/blob/master/examples/rtu_over_tcp_with_higherlevel_api.php That example is using ReadRegistersBuilder with BinaryStreamConnection - so for your needs - create the connection , connect to it and reuse it in you while loop. Probably needs some catching to detect problems with the connection and then reconnect.

NB: you could use even ReactPHP networking. See this example: https://github.com/aldas/modbus-tcp-client/blob/master/examples/example_cli_poller.php