如何通过PHP+Socket读取Teltonika TRB145网关的电力数据?
看起来你已经推进了不少步骤——网关能成功连到你的PHP服务器,但就是拿不到电表的Modbus数据,这种“差一步”的情况确实挺闹心的。我来结合你的问题和代码,拆解下核心问题和调整方向:
首先搞懂核心误解:Serial Over IP的本质是「RTU透传」,不是Modbus TCP
Teltonika TRB145的Serial Over IP功能,简单说就是把RS485串口的Modbus RTU数据通过TCP通道直接透传给你的服务器,它不会帮你把RTU转换成Modbus TCP格式。而你现在的代码用的是ModbusTcpClient库来构造TCP格式的请求,这相当于给只懂RTU的电表发了一堆它完全看不懂的“外星文”,自然不会有回应。
你的代码需要做这几个关键调整
1. 把Modbus请求从TCP格式改成RTU格式
Modbus RTU的帧结构没有TCP的MBAP头(事务ID、协议ID、长度组成的头),而是由「设备地址字节 + 功能码字节 + 数据段 + CRC16校验(2字节)」组成。你需要构造这种格式的请求,发给网关(网关会透传给电表)。
比如,构造一个读取地址为1的电表、起始寄存器0、共10个保持寄存器的RTU请求:
// 电表的Modbus地址(比如1) $deviceAddress = 1; // 功能码:读取保持寄存器是0x03 $functionCode = 0x03; // 起始寄存器地址(大端字节序) $startAddress = 0; // 要读取的寄存器数量(大端字节序) $registerCount = 10; // 组装RTU请求的核心数据段 $dataSegment = pack('n*', $startAddress, $registerCount); // 组装完整帧(地址+功能码+数据段) $rtuFrame = pack('CC', $deviceAddress, $functionCode) . $dataSegment; // 计算Modbus RTU的CRC16校验 function calculateModbusCrc16($data) { $crc = 0xFFFF; for ($i = 0; $i < strlen($data); $i++) { $crc ^= ord($data[$i]); for ($j = 0; $j < 8; $j++) { if ($crc & 0x0001) { $crc >>= 1; $crc ^= 0xA001; } else { $crc >>= 1; } } } // CRC结果是低字节在前,高字节在后 return pack('v', $crc); } // 拼接校验码,得到最终的RTU请求包 $rtuRequest = $rtuFrame . calculateModbusCrc16($rtuFrame); // 在连接建立后,发送这个请求 $conn->write($rtuRequest);
2. 处理TCP流的「粘包/拆包」问题
TCP是流协议,你收到的$data可能是不完整的RTU帧,或者多个帧粘在一起。你需要缓存收到的数据,通过CRC校验来识别完整的帧,再解析:
在你的data事件处理逻辑里修改:
$receivedData .= $data; // 循环处理缓存中的数据,直到找不到完整帧 while (strlen($receivedData) >= 4) { // RTU帧最小长度:1+1+2=4字节 $frameFound = false; // 尝试从缓存中找到完整的RTU帧(通过CRC校验) for ($frameLength = 4; $frameLength <= strlen($receivedData); $frameLength++) { $candidateFrame = substr($receivedData, 0, $frameLength); // 计算候选帧的CRC(去掉最后2字节的校验码) $calculatedCrc = calculateModbusCrc16(substr($candidateFrame, 0, $frameLength - 2)); // 取出候选帧末尾的2字节作为收到的CRC $receivedCrc = substr($candidateFrame, -2); if ($calculatedCrc === $receivedCrc) { // 找到完整帧,解析它 parseRtuResponse($candidateFrame); // 从缓存中移除已处理的帧 $receivedData = substr($receivedData, $frameLength); $frameFound = true; break; } } if (!$frameFound) { // 没有找到完整帧,继续等待后续数据 break; } } // 新增解析RTU响应的函数 function parseRtuResponse($frame) { global $logger; $deviceAddress = ord(substr($frame, 0, 1)); $functionCode = ord(substr($frame, 1, 1)); if ($functionCode & 0x80) { // 功能码最高位为1,说明是错误响应 $errorCode = ord(substr($frame, 2, 1)); $logger->error("Device {$deviceAddress} returned error: code {$errorCode}"); return; } // 以读取保持寄存器的响应为例,解析数据 if ($functionCode === 0x03) { $byteCount = ord(substr($frame, 2, 1)); $registerData = substr($frame, 3, $byteCount); $registerCount = $byteCount / 2; // 每个寄存器2字节 $registers = []; for ($i = 0; $i < $registerCount; $i++) { // 把2字节的大端数据转成整数 $regValue = unpack('n', substr($registerData, $i*2, 2))[1]; $registers[] = $regValue; } $logger->info("Device {$deviceAddress} returned registers: " . implode(', ', $registers)); } }
3. 移除不需要的Modbus TCP相关代码
你现在引入的ModbusTcpClient\Packet\ModbusApplicationHeader、ReadHoldingRegistersRequest这些都是Modbus TCP的类,在RTU透传场景下完全用不上,可以直接删掉,减少代码复杂度。同时注意修正require 'vendor/autoload.php'的换行语法错误。
先做「前置排查」,排除硬件/配置问题
在调整代码之前,先用工具验证网关和电表的通信是否正常:
- 用「Modbus Poll」这类工具,设置为RTU over TCP模式,连接到你的服务器监听的端口(或者直接连网关的Serial Over IP端口,以网关配置为准)。
- 发送和你代码中相同的Modbus请求,如果能拿到数据,说明网关、电表、接线都是正常的,问题确实在代码;如果拿不到,先排查:
- 网关的RS485参数(波特率、数据位、停止位、奇偶校验)是否和电表完全一致;
- 网关的Serial Over IP配置是否正确(客户端模式、目标IP/端口和你的服务器一致);
- 电表的Modbus地址是否正确,RS485接线是否接对(A/A、B/B,不要反接)。
最后再检查你的代码逻辑
- 确保你的PHP CLI脚本有足够的权限,并且依赖包(比如React Socket)已经正确安装;
- 连接建立后,不要只发
Server ready:{}\n,要马上发送RTU请求——电表不会主动推送数据,必须你主动请求; - 开启调试日志,打印所有收到的原始数据(用
unpack('H*', $data)转成十六进制),方便排查是没收到数据,还是收到了但解析错误。
备注:内容来源于stack exchange,提问作者user27972404

