基于socket的 TCP上位机通信内容解析程序

  • 下位机发送的数据结构:

帧头0x58 0x58 + 有效数据 ,不定数据长度 + 帧尾0x59 0x59 + CRC16 2B,大端

  • CRC校验范围:

帧头 + 有效数据 + 帧尾头(0x

由于是TCP通信,不可避免粘包、半包、拆包。

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PLC TCP 通信联调测试工具
========================
帧结构: [0x58 0x58] + [有效数据 N B] + [0x59 0x59] + [CRC16 2B大端]
CRC校验范围: 帧头 + 有效数据 + 帧尾

功能:
  1. 作为TCP客户端连接S7-1500
  2. 输入hex数据自动组帧发送
  3. 原始字节发送(不组帧)
  4. CRC16计算预览
  5. 实时显示接收原始字节 + 完整解析过程(找帧头/试帧尾/CRC对比)
  6. 自动处理粘包/拆包/假帧尾/错误帧头(跳帧恢复)

用法:
  python plc_debug_tool.py                # 默认 192.168.100.1:2000
  python plc_debug_tool.py 192.168.1.10 2000
"""

import socket
import struct
import threading
import sys


# ═══════════════════════════════════════════════════════════
#  CRC16 计算(必须与PLC侧完全一致)
#  默认: Modbus标准 --- 多项式0xA001, 初始值0xFFFF
#  如果PLC用的是其他标准(如59ITT 0x1021),修改这个函数
# ═══════════════════════════════════════════════════════════
def crc16_modbus(data: bytes) -> int:
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc


def build_frame(payload: bytes) -> bytes:
    """组帧: 帧头 + 数据 + 帧尾 + CRC16(大端)"""
    body = b'\x58\x58' + payload + b'\x59\x59'
    crc = crc16_modbus(body)
    return body + struct.pack('>H', crc)


def hex_str(data: bytes) -> str:
    """字节转空格分隔的hex字符串"""
    return ' '.join(f'{b:02X}' for b in data)


# ═══════════════════════════════════════════════════════════
#  带详细调试输出的帧解析器(含错误帧头跳帧修复)
# ═══════════════════════════════════════════════════════════
class DebugFrameParser:
    HEADER = b'\x58\x58'
    TAIL = b'\x59\x59'
    CRC_LEN = 2
    MAX_FRAME = 1024  # 单帧最大字节数保护,根据实际调整

    def __init__(self, debug: bool = True):
        self.buffer = bytearray()
        self.debug = debug
        self.frame_count = 0

    def log(self, msg: str):
        if self.debug:
            print(f'    [解析] {msg}')

    def feed(self, data: bytes):
        """喂入任意长度TCP数据,返回解析出的完整帧列表"""
        if not data:
            return []
        self.log(f'喂入 {len(data)} 字节: {hex_str(data)}')
        self.buffer.extend(data)

        frames = []
        while True:
            frame = self._try_parse_one()
            if frame is None:
                break
            frames.append(frame)

        if not frames and self.debug:
            self.log(f'缓冲区现有 {len(self.buffer)} 字节,暂未形成完整帧')
        return frames

    def _try_parse_one(self):
        buf = self.buffer

        # 外层循环: 支持跳帧后重新同步(从新帧头开始)
        while True:
            # ── 第1步: 找帧头 ──
            header_pos = buf.find(self.HEADER)
            if header_pos == -1:
                # 没找到帧头,保留最后1字节(可能是0x58,下一包凑第二个0x58)
                if len(buf) > 1:
                    discarded = bytes(buf[:-1])
                    del buf[:-1]
                return None
            if header_pos > 0:
                discarded = bytes(buf[:header_pos])
                self.log(f'帧头前有 {header_pos} 字节垃圾: {hex_str(discarded)},已丢弃')
                del buf[:header_pos]  # 丢弃帧头前的垃圾数据

            # ── 第2步: 逐个尝试帧尾,用CRC验证真假 ──
            search_pos = 2
            attempt = 0
            while True:
                tail_pos = buf.find(self.TAIL, search_pos)

                if tail_pos == -1:
                    # 缓冲区中所有帧尾 59 59 都已尝试完毕,全部 CRC 失败
                    # 检查是否存在下一个帧头且其后有完整帧结构
                    next_header = buf.find(self.HEADER, 2)
                    if next_header != -1:
                        next_tail = buf.find(self.TAIL, next_header + 2)
                        if next_tail != -1 and len(buf) >= next_tail + 2 + self.CRC_LEN:
                            # 判定当前帧头为错误帧,跳过至新帧头
                            del buf[:next_header]
                            break  # 跳出内层,回到外层重新找帧头

                    # 没有下一个有效帧头,可能是正常拆包,等更多数据
                    if len(buf) > self.MAX_FRAME:
                        # 缓冲区超过{self.MAX_FRAME}字节仍无有效帧,判定当前帧头为假,跳过1字节重新同步
                        del buf[:1]
                        break  # 跳出内层,回到外层重新找帧头
                    # 未找到有效帧尾,等待更多数据
                    return None

                # 单帧长度保护
                if tail_pos + 2 + self.CRC_LEN > self.MAX_FRAME:
                    # 帧尾候选@{tail_pos}超出最大帧长,当前帧头无效,跳过1字节
                    del buf[:1]
                    break  # 跳出内层,回到外层重新找帧头

                attempt += 1
                frame_end = tail_pos + 2 + self.CRC_LEN

                if len(buf) < frame_end:
                    # 帧尾候选{tail_pos},但CRC未到齐,需等待
                    return None

                # ── 第3步: CRC校验 ──
                crc_range = bytes(buf[:tail_pos + 2])  # 帧头+数据+帧尾
                crc_received = struct.unpack('>H', buf[tail_pos + 2:frame_end])[0]
                crc_calc = crc16_modbus(crc_range)

                if crc_calc == crc_received:
                    payload = bytes(buf[2:tail_pos])  # 取出数据
                    raw = bytes(buf[:frame_end])  # 原始字节
                    del buf[:frame_end]   # 删除本帧数据
                    self.frame_count += 1
                    print(f'★ 解析成功! 有效数据({len(payload)}B): {hex_str(payload)}')
                    return {'data': payload, 'raw': raw, 'seq': self.frame_count}

                # CRC不通过 → 这个0x59 0x59是数据中的内容,继续找下一个
                search_pos = tail_pos + 1


# ═══════════════════════════════════════════════════════════
#  后台接收线程
# ═══════════════════════════════════════════════════════════
def recv_loop(sock: socket.socket, parser: DebugFrameParser, stop_event: threading.Event):
    while not stop_event.is_set():
        try:
            sock.settimeout(0.5)
            data = sock.recv(4096)
            if not data:
                print('\n[连接] PLC 已断开连接')
                stop_event.set()
                break

            print(f'\n  [接收] 收到 {len(data)} 字节: {hex_str(data)}')
            frames = parser.feed(data)

            for f in frames:
                print(f'\n  {"─"*55}')
                print(f'  ★ 完整帧 #{f["seq"]}  总长 {len(f["raw"])} 字节')
                print(f'    原始帧  : {hex_str(f["raw"])}')
                print(f'    有效数据: {hex_str(f["data"])}  ({len(f["data"])} 字节)')
                print(f'  {"─"*55}')

            print('\n> ', end='', flush=True)

        except socket.timeout:
            continue
        except Exception as e:
            print(f'\n  [接收错误] {e}')
            stop_event.set()
            break


# ═══════════════════════════════════════════════════════════
#  主程序(交互式命令行)
# ═══════════════════════════════════════════════════════════
def main():
    print('=' * 60)
    print('   PLC TCP 通信联调工具')
    print('   帧结构: [58 58] + [有效数据] + [59 59] + [CRC16大端]')
    print('=' * 60)

    ip = sys.argv[1] if len(sys.argv) >= 2 else '192.168.100.1'
    port = int(sys.argv[2]) if len(sys.argv) >= 3 else 2000

    print(f'\n  目标PLC: {ip}:{port}')
    print('  正在连接...')

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(3)
        sock.connect((ip, port))
        sock.settimeout(None)
        print(f'  [连接] ✓ 已连接到 {ip}:{port}')
    except Exception as e:
        print(f'  [连接失败] {e}')
        print(f'  用法: python {sys.argv[0]} <IP> <端口>')
        return

    parser = DebugFrameParser(debug=True)
    stop_event = threading.Event()
    recv_thread = threading.Thread(
        target=recv_loop, args=(sock, parser, stop_event), daemon=True
    )
    recv_thread.start()

    print('''
  ┌─ 命令说明 ─────────────────────────────────────────┐
  │  <hex数据>   自动组帧发送,如: 01 02 03 04        │
  │  raw <hex>   发送原始字节(不组帧),如: raw 58 58  │
  │  crc <hex>   仅计算CRC16预览(不发送)              │
  │  status      查看当前接收缓冲区                    │
  │  clear       清空接收缓冲区                        │
  │  quit        退出                                  │
  └────────────────────────────────────────────────────┘
''')

    try:
        while not stop_event.is_set():
            try:
                cmd = input('> ').strip()
            except EOFError:
                break

            if not cmd:
                continue

            low = cmd.lower()

            if low in ('quit', 'exit', 'q'):
                break

            if low == 'clear':
                parser.buffer.clear()
                print('  [操作] 接收缓冲区已清空')
                continue

            if low == 'status':
                print(f'  [状态] 缓冲区 {len(parser.buffer)} 字节: '
                      f'{hex_str(bytes(parser.buffer))}')
                continue

            if low.startswith('crc '):
                try:
                    payload = bytes.fromhex(cmd[4:].replace(' ', ''))
                    body = b'\x58\x58' + payload + b'\x59\x59'
                    crc = crc16_modbus(body)
                    full = body + struct.pack('>H', crc)
                    print(f'  [CRC计算]')
                    print(f'    校验范围(帧头+数据+帧尾): {hex_str(body)}')
                    print(f'    CRC16 = 0x{crc:04X}  '
                          f'大端字节序: {crc >> 8:02X} {crc & 0xFF:02X}')
                    print(f'    完整帧({len(full)}B): {hex_str(full)}')
                except ValueError:
                    print('  [错误] hex格式不正确')
                continue

            if low.startswith('raw '):
                try:
                    raw = bytes.fromhex(cmd[4:].replace(' ', ''))
                    sock.sendall(raw)
                    print(f'  [发送原始] {len(raw)} 字节: {hex_str(raw)}')
                except ValueError:
                    print('  [错误] hex格式不正确')
                continue

            # 默认: 输入hex数据 → 自动组帧发送
            try:
                payload = bytes.fromhex(cmd.replace(' ', ''))
                frame = build_frame(payload)
                sock.sendall(frame)
                print(f'  [发送组帧] 有效数据({len(payload)}B): {hex_str(payload)}')
                print(f'             完整帧  ({len(frame)}B): {hex_str(frame)}')
            except ValueError:
                print('  [错误] 无法识别的命令,请检查hex格式或命令')

    except KeyboardInterrupt:
        pass
    finally:
        stop_event.set()
        sock.close()
        print('\n  [退出] 连接已关闭')


if __name__ == '__main__':
    main()
  • 功能测试:
测试场景 结果
错误帧头 + 正确帧粘连 ✓ 跳过错误帧头,正确解析出后面的帧
两帧正常粘连 ✓ 解析出 2 帧
有效数据中含假帧尾 ✓ 正确识别真帧尾,数据完整保留
半帧拆包(分两次到达) ✓ 第一次 0 帧,第二次拼出完整帧
相关推荐
min(a,b)1 小时前
学习第17天:Agent Memory 与 MCP 协议
python·学习
x_x-F1 小时前
网络数据从网卡到用户态:一场基于内存所有权转移的接力赛
开发语言·网络·php
CodeBlog-star2 小时前
LLM安全实战:提示词注入与越狱攻击防御指南
python·agent·提示词攻击·越狱攻击
lifallen2 小时前
模型不是函数:claude-cookbooks/misc 十四篇的公共底层
人工智能·学习·ai·ai编程
小柯南敲键盘2 小时前
电商图片翻译工具推荐,批量处理主图视频字幕,免费试用
大数据·人工智能·python·音视频
比高创意品牌策划设计3 小时前
零售卖场设计软件用CAD还是SketchUp还是酷家乐
python
lucky_syq4 小时前
第5篇 · S1·下:前沿架构:MoE、Reasoning 模型、长上下文、多模态、SSM/Mamba 与模型谱系
人工智能·学习·架构
日拱一卒的小田4 小时前
ZYNQ学习笔记3-ZYNQ的IIC控制器1
笔记·单片机·学习
迷迭香yy4 小时前
行业板块轮动因子实战从板块资金到因子建模的本地化Python全流程
数据库·人工智能·python
W_326004 小时前
Python文件进阶:一维数据与 CSV 文件读写
开发语言·python