aldas / modbus-tcp-client

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

How to get data type Double #102

Closed haha8x closed 2 years ago

haha8x commented 2 years ago

Hello aldas

I am trying to read the FC3, HoldingRegister from a Siemens PAC-3200 using this script.

The thing is I am trying to get the value on address 801 from this meter. The manual say, the address 801 have data_type is double, it is quite different from another address, which data type is Float

image

So this is my code

        if ($meter->model->modbus_type == 'inputRegister') {
            $request = ReadRegistersBuilder::newReadInputRegisters("tcp://{$ip}:{$port}", $slaveId);
        } else {
            Endian::$defaultEndian = Endian::BIG_ENDIAN;
            $request = ReadRegistersBuilder::newReadHoldingRegisters("tcp://{$ip}:{$port}", $slaveId);
        }

        foreach ($params as $param) {
            switch ($param->data_type) {
                case 'double':
                    $request->int64($param->address, $param->code, null, null, Endian::BIG_ENDIAN_LOW_WORD_FIRST);
//                    $request->double($param->address, $param->code, function ($result, $readRegisterAddress, $response) {
//                        return $response->getQuadWordAt($readRegisterAddress->getAddress())->getUInt64(\ModbusTcpClient\Utils\Endian::BIG_ENDIAN_LOW_WORD_FIRST);
//                    });
                    break;
                default:
                    $request->float($param->address, $param->code);
            }
        }

        $request = $request->build(); // returns array of 3 requests

        $responseContainer = (new NonBlockingClient(['readTimeoutSec' => 10]))->sendRequests($request);
        echo '<pre>';
        print_r($responseContainer->getData()); // array of assoc. arrays (keyed by address name)
        print_r($responseContainer->getErrors());
        echo '</pre>';

All the value which have float data type are show up correctly Except the value in double data type

Plz help me

aldas commented 2 years ago

Could you use https://github.com/aldas/modbus-tcp-client/blob/master/examples/index.php to request some of these double fields and paste the output here. These HEX dumps of responses would be helpful and if you know approximate value what some double field has - please add it here.

also - those doubles are probably 64bits (double precision floating point values). Float is 32bit.

maybe this works (I have not tried):

$request->float($param->address, $param->code, function ($result, $readRegisterAddress, $response) {
    $tmp = $response->getQuadWordAt($readRegisterAddress->getAddress())->getData();
    return unpack('E', $tmp)[1];  // "E" = double (machine dependent size, big endian byte order) See: https://www.php.net/manual/en/function.pack.php
});

I can add 64bit double precision floating point support to APIs but I need some examples.

haha8x commented 2 years ago

Hello aldas

This is the request I made

Using: function code: 3, ip: 192.168.100.13, port: 8989, address: 801, quantity: 12, endianess: 5
Packet to be sent (in hex): 32810000000616030321000c
Binary received (in hex):   32810000001b16031841c1ccc330104d82000000000000000040423d3604507c3e

image

image

image

I tried the code but it throws address out of bounds

As the manual of the Siemens PAC-3200 To get the double data on 801 address, it needs the swap the low byte and high byte. Then divide it by 1000 something like in nodejs

                    let val = await client.readHoldingRegisters(801, 4);
                    meter_data[kwh] = Number.isNaN(val.buffer.readDoubleBE()) ? 0 : val.buffer.readDoubleBE() / 1000;

I have tried BIG_ENDIAN_LOW_WORD_FIRST but still can not get the correct value

aldas commented 2 years ago

You are trying with nodejs also? What number does node give you. I need approximate (ala 2328 or 597263, should it be 4 digits for integer part or 5,6 digits something like that (before dividing by 1000)) to know if I am on the right path and unpacking it into the right number.

One number I am getting is 597263.96812737 after dividing by 1000 but I have no idea if it is even close to number what nodejs gives.

Do you have some diplay, webinterface for that device which shows that kwh amount?

haha8x commented 2 years ago

Yes the number is 597263.96812737 It is the correct one that show on the meter screen.

aldas commented 2 years ago

Ok, then unpack('E', $tmp)[1] is correct way.

about

I tried the code but it throws address out of bounds

I think this is related to that $request->float( requests only 2 registers worth of data so we should wrap this workaround inside 4 register type. ala $request->int64( as you had previously.

$request->int64($param->address, $param->code, function ($result, $readRegisterAddress, $response) {
    $tmp = $response->getQuadWordAt($readRegisterAddress->getAddress())->getData();
    return unpack('E', $tmp)[1];  // "E" = double (machine dependent size, big endian byte order) See: https://www.php.net/manual/en/function.pack.php
});

you can try this as a workaround meanwhile I add double support to API.

haha8x commented 2 years ago

Very nice, the Above code is working flawlessly. But I got another problem that, since I use the parallel request the read the fc3. Here is the address list image

It is working very well on my Macbook But on the production server, It got connection refuse from modbus server

Turn out if I remove the address 801, the connection is accpeted again on production server

But on the localhost, that error never happen

if ($meter->model->modbus_type == 'inputRegister') {
                    $request = ReadRegistersBuilder::newReadInputRegisters("tcp://{$this->gateway->ip}:{$this->gateway->port}", $meter->slave_id);
                } else {
                    Endian::$defaultEndian = Endian::BIG_ENDIAN;
                    $request = ReadRegistersBuilder::newReadHoldingRegisters("tcp://{$this->gateway->ip}:{$this->gateway->port}", $meter->slave_id);
                }

                foreach ($params as $param) {
                    switch ($param->data_type) {
                        case 'double':
                            $request->int64($param->address, $param->code, function ($result, $readRegisterAddress, $response) {
                                $tmp = $response->getQuadWordAt($readRegisterAddress->getAddress())->getData();
                                return unpack('E', $tmp)[1];
                                // "E" = double (machine dependent size, big endian byte order) See: https://www.php.net/manual/en/function.pack.php
                            });
                            break;
                        default:
                            $request->float($param->address, $param->code);
                    }
                }

                try {
                    $request = $request->build(); // returns array of 3 requests

                    $client = new NonBlockingClient([
                        'readTimeoutSec' => 10, // timeout when waiting response from server
                        'connectTimeoutSec' => 10,  // timeout when establishing connection to the server
                    ]);

                    $responseContainer = ($client)->sendRequests($request);

                    $error = $responseContainer->getErrors();

                    if ($error) {
                        Log::error(json_encode($error));
                    } else {
                        $data = $responseContainer->getData() + ['meter_id' => $meter->id];

                        MeterDataNew::create($data);
                        event(new MeterDataEvent($data));
                    }
                } catch (Exception $exception) {
                    Log::error('Can not connect ' . $meter->id);
                    Log::error(json_encode($exception));
                }

The production enviroment I use is

So I think the alpine is the root cause, since it may missing some function, which make the parallel can not split the address into mutiple request

aldas commented 2 years ago

Check how many requests are created before you add 801 and after - is it N (before) and N+1 (after)?.

Connection refused is quite hard to debug. From my personal experience is that I have seen PLCs crash/hang when there are too many connections/requests sent to them in parallel.

As a tests try sending request in serially - If your cycle time is not too low (ala 1+hz) then you are probably fine sending them not in parallel.

Sidenote: If you can - upgrade your PHP version. PHP 7.4 active support has ended and there are only security fixes released for it. See https://www.php.net/supported-versions.php

haha8x commented 2 years ago

From this table image

There is 11 request before the 801 as the final All 11 request have lenght = 2 while the 801 has lenght = 4

As I check the ReadRegistersBuilder::newReadHoldingRegisters if it detect request is over 124 registers, it will split into 2 request

But somehow, it doesn't work in docker php:7.4-fpm-alpine3.12

I ll tried upgrade to latest php 8.1 and see how it will go

aldas commented 2 years ago

Request splitting done because maximum amount of registers modbus response can return is limited to 124 sequential register contents . If you address range has huge caps (ala 600, 801) then you end up more then one requests as their addresses are too far apart in memory and can not fit into single response.

aldas commented 2 years ago

@haha8x I have added double support. It is in master branch. You can get it with composer require aldas/modbus-tcp-client:dev-master. If it works I will tag a release for it.

haha8x commented 2 years ago

Thank you The double support work flawlessly

about the Connection refuse which mean the code will keep sending request from address 69 + 124 until it reach 801 which will cause the timeout?

aldas commented 2 years ago

Try sending all those request serially and see if it timeouts

$connection = BinaryStreamConnection::getBuilder()
    ->setPort(5020)
    ->setHost('127.0.0.1')
    ->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
    ->setWriteTimeoutSec(0.5) // timeout when writing/sending packet to the server
    ->setReadTimeoutSec(0.3) // timeout when waiting response from server
    ->setLogger(new EchoLogger())
    ->build();

$requests = ReadRegistersBuilder::newReadHoldingRegisters('tcp://127.0.0.1:5022')
    ->bit(256, 15, 'pump2_feedbackalarm_do')
    // ...
    //...
    ->build(); // returns array of N requests

$connection->connect();
foreach ($requests as $request) { // send requests serially
    $binaryData = $connection->sendAndReceive($request->getRequest());
    $result = $request->parse($binaryData);
    print_r($result); 
}