aldas/modbus-tcp-client

Modbus TCP协议客户端库

3.5.1 2024-03-10 12:44 UTC

README

Latest Version Packagist Software License codecov

安装

使用Composer将此库作为依赖项安装。

composer require aldas/modbus-tcp-client

支持的功能

实用函数

要求

  • PHP 8.0+
  • 版本 2.4.0 最后支持 PHP 7(v3.0.0 可能与 7.4 兼容)
  • 版本 0.2.0 最后支持 PHP 5.6

目的

此库受 phpmodbus 库的影响,旨在提供解耦的 Modbus 协议(请求/响应报文)和网络相关功能,以便您可以使用自己选择的网络代码(ext_sockets/streams/Reactphp/Amp 异步流)构建 Modbus 客户端,或者使用库提供的网络类(php Streams)

字节序

适用于存储在 Word/Double/Quad word 寄存器中的多字节数据,基本上是除了 (u)int16/byte/char 之外的所有内容。

因此,如果我们从网络接收到 0x12345678(字节:ABCD)并将其转换为 32 位寄存器,则可能有 4 种不同的方式来解释字节和字序,这取决于 Modbus 服务器架构和客户端架构。NB:TCP 和 UDP 以大端序传输,因此我们选择此作为示例的基础

库支持以下字节和字序

  • 大端(ABCD - word1 = 0x1234,word2 = 0x5678)
  • 大端低字首先(CDAB - word1 = 0x5678,word2 = 0x1234)(由 Wago-750 使用)
  • 小端(DCBA - word1 = 0x3412,word2 = 0x7856)
  • 小端低字首先(BADC - word1 = 0x7856,word2 = 0x3412)

用于解析的默认(全局)字节序可以通过以下方式更改

Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;

对于非全局情况,请参阅 API 方法参数列表,如果方法支持使用自定义字节序。

有关更多信息,请参阅 Endian.php,有关支持的数据类型,请参阅 Types.php

数据类型

Modbus 是二进制协议,它围绕寄存器/Word(16位,2字节数据)和线圈(1位数据)的地址展开。线圈是布尔值,但寄存器/Word 或多个寄存器可以存储不同的数据类型。以下是访问寄存器中不同数据类型的方法

大多数数据类型都有可选参数,用于提供字节序。默认情况下,数据以大端低字首先的字节序解析。

示例:以小端低字首先的方式获取 uint32 值

$dword->getUInt32(Endian::LITTLE_ENDIAN | Endian::LOW_WORD_FIRST);

1bit - 16bit 数据类型

1-16bit 数据类型由 Word 类持有,该类持有 2 字节数据。

$address = 100;
$word = $response->getWordAt($address);

以下方法可用于从单个 Word 实例获取不同的类型

  • boolean - 1位,true/false,$word->isBitSet(11)
  • byte - 8位,1字节,范围 0 到 255
    • Word 的第一个字节(0)$word->getHighByteAsInt()
    • Word 的最后一个字节(1)$word->getLowByteAsInt()
  • uint16 - 16位,2字节,范围 0 到 65535 $word->getUInt16()
  • int16 - 16位,2字节,范围 -32768 到 32767 $word->getInt16()

以及以下附加方法

  • $word->getBytes() 返回一个包含两个整数的数组(0-255)

32位数据类型

17-32位数据类型由 DoubleWord 类持有,它包含4字节数据。

$address = 100;
$dword = $response->getDoubleWordAt($address);

以下方法可以用于从单个 DoubleWord 实例获取不同类型的数据

  • uint32 - 32位,4字节,范围0到4294967295,$dword->getUInt32()
  • int32 - 64位,8字节,范围-2147483648到2147483647,$dword->getInt32()
  • float - 64位,8字节,范围-3.4e+38到3.4e+38,$dword->getFloat()

以及以下附加方法

  • $dword->getBytes() 返回一个包含四个整数的数组(0-255),代表 DoubleWord
  • $dword->getHighBytesAsWord() 返回前两个字节作为 Word
  • $dword->getLowBytesAsWord() 返回后两个字节作为 Word

64位数据类型

64位数据类型由 QuadWord 类持有,它包含8字节数据。注意:64位PHP仅支持最多63位(有符号)整数。

$address = 100;
$qword = $response->getQuadWordAt($address);

以下方法可以用于从单个 QuadWord 实例获取不同类型的数据

  • uint32 - 64位,8字节,范围0到9223372036854775807,$dword->getUInt64()
  • int32 - 64位,8字节,范围-9223372036854775808到9223372036854775807,$dword->getInt64()
  • double - 64位,8字节,范围2.2250738585072e-308到1.7976931348623e+308,$dword->getDouble()

以及以下附加方法

  • $qword->getBytes() 返回一个包含8个整数的数组(0-255),代表 QuadWord
  • $qword->getHighBytesAsDoubleWord() 返回前4个字节作为 DoubleWord
  • $qword->getLowBytesAsDoubleWord() 返回后4个字节作为 DoubleWord

字符串

ASCII(8位字符)字符串可以从响应中作为UTF-8字符串提取

$address = 20;
$length = 10;
$string = $response->getAsciiStringAt($address, $length);

Modbus TCP(fc3 - 读取保持寄存器)的示例

一些Modbus功能示例在 examples/ 文件夹中

高级用法

使用高级API请求多个数据包

$address = 'tcp://127.0.0.1:5022';
$unitID = 0; // also known as 'slave ID'
$fc3 = ReadRegistersBuilder::newReadHoldingRegisters($address, $unitID)
    ->unaddressableRanges([[100,110], [1512]])
    ->bit(256, 15, 'pump2_feedbackalarm_do')
    // will be split into 2 requests as 1 request can return only range of 124 registers max
    ->int16(657, 'battery3_voltage_wo')
    // will be another request as uri is different for subsequent int16 register
    ->useUri('tcp://127.0.0.1:5023')
    ->string(
        669,
        10,
        'username_plc2',
        function ($value, $address, $response) {
            return 'prefix_' . $value; // optional: transform value after extraction
        },
        function (\Exception $exception, Address $address, $response) {
            // optional: callback called then extraction failed with an error
            return $address->getType() === Address::TYPE_STRING ? '' : null; // does not make sense but gives you an idea
        }
    )
    ->build(); // returns array of 3 ReadHoldingRegistersRequest requests

// this will use PHP non-blocking stream io to recieve responses
$responseContainer = (new NonBlockingClient(['readTimeoutSec' => 0.2]))->sendRequests($fc3);
print_r($responseContainer->getData()); // array of assoc. arrays (keyed by address name)
print_r($responseContainer->getErrors());

响应结构

[
    [ 'pump2_feedbackalarm_do' => true, ],
    [ 'battery3_voltage_wo' => 12, ],
    [ 'username_plc2' => 'prefix_admin', ]
]

低级 - 发送数据包

$connection = BinaryStreamConnection::getBuilder()
    ->setHost('192.168.0.1')
    ->build();
    
$packet = new ReadHoldingRegistersRequest(256, 8); //create FC3 request packet

try {
    $binaryData = $connection->connect()->sendAndReceive($packet);

    //parse binary data to response object
    $response = ResponseFactory::parseResponseOrThrow($binaryData);
    
    //same as 'foreach ($response->getWords() as $word) {'
    foreach ($response as $word) { 
        print_r($word->getInt16());
    }
    // print registers as double words in big endian low word first order (as WAGO-750 does)
    foreach ($response->getDoubleWords() as $dword) {
        print_r($dword->getInt32(Endian::BIG_ENDIAN_LOW_WORD_FIRST));
    }
        
    // set internal index to match start address to simplify array access
    $responseWithStartAddress = $response->withStartAddress(256);
    print_r($responseWithStartAddress[256]->getBytes()); // use array access to get word
    print_r($responseWithStartAddress->getDoubleWordAt(257)->getFloat());
} catch (Exception $exception) {
    echo $exception->getMessage() . PHP_EOL;
} finally {
    $connection->close();
}

Modbus RTU over TCP 的示例

Modbus RTU 和 Modbus TCP 之间的区别在于

  1. RTU 报头仅包含从站 ID。TCP/IP 报头包含事务 ID、协议 ID、长度和单元 ID
  2. RTU 封包附加了2字节 CRC

有关更详细说明,请参阅 http://www.simplymodbus.ca/TCP.htm

这个库最初是为 Modbus TCP 而设计的,但它支持将数据包转换为 RTU,并从 RTU 转换。请参阅此 examples/rtu.php 示例。

$rtuBinaryPacket = RtuConverter::toRtu(new ReadHoldingRegistersRequest($startAddress, $quantity, $slaveId));
$binaryData = $connection->connect()->sendAndReceive($rtuBinaryPacket);
$responseAsTcpPacket = RtuConverter::fromRtu($binaryData);

Modbus RTU over USB to Serial(RS485)适配器的示例

请参阅 'examples/rtu_usb_to_serial.php' 中的 Linux 示例

Modbus RTU over TCP + 高级API使用的示例

请参阅 'examples/rtu_over_tcp_with_higherlevel_api.php' 中的示例

使用 ReactPHP/Amp 的非阻塞套接字IO的示例(即,modbus 请求在“并行”中运行)

使用 ReactPHP 的 Modbus 服务器示例(接受请求)

使用 PHP 内置的 web 服务器快速尝试与 PLC 进行通信

示例文件夹中有 index.php,可以与 PHP 内置的 web 服务器一起使用来测试与自己的 PLC 的通信。

git clone https://github.com/aldas/modbus-tcp-client.git
cd modbus-tcp-client
composer install
php -S localhost:8080 -t examples/

现在在浏览器中打开 https://:8080。查看来自 index.php 的附加查询参数。

变更日志

查看 CHANGELOG.md

测试

  • 所有 composer test
  • 单元测试 composer test-unit
  • 集成测试 composer test-integration

针对 Windows 用户

  • 所有 vendor/bin/phpunit
  • 单元测试 vendor/bin/phpunit --testsuite 'unit-tests'
  • 集成测试 vendor/bin/phpunit --testsuite 'integration-tests'

静态分析

运行 PHPStan 分析 compose check