基于QtPy (PySide6) 的PLC-HMI工程项目(八)在上位机中解析上行报文

  • 来自PLC的上行报文具有以下数据格式:

帧头(2字节) + 区域变量(Area、DBnum、offset、byteCount) + 本区域数据内容( byteCount长度的字节) + 下一区域变量 + 下一区域数据内容 + 。。。 + 帧尾(2字节)+ CRR16(2字节)

  • 数据解析流程:

当接收到上行报文后首先对帧头和帧尾进行校验,如果校验不过就舍弃帧数据。然后经过CRC校验,确保所有数据无误,将帧数据保存。

一、创建一个与PLC中类似的区域数据类型

python 复制代码
from typing import NamedTuple


class AreaVar(NamedTuple):
    area: str  # 数据区域,I、Q、M、DB
    DBnum: int   # DB号
    offset: int   # 字节偏移
    byte_count: int  # 字节数
    content: bytes  #  数据内容

二、解析上行数据

之前的PLC范例中获取到过一个上行帧的数据内容:

以这帧数据为例,演示如何解析上行数据:

python 复制代码
import crcmod

from AreaVar import AreaVar
import numpy as np

# ===================== 配置区域 =====================
SERVER_IP = "127.0.0.1"  # 下位机服务器IP
SERVER_PORT = 2001  # 下位机(PLC)端口
RECONNECT_INTERVAL = 3000  # 断线重连间隔(毫秒)
HEARTBEAT_INTERVAL = 1000  # 心跳包间隔(毫秒)
HEART_CODE = b'XXYY'    # 心跳代码
UP_FRAME_HEAD = b'AA'      # 上行帧头
UP_FRAME_END = b'BB'      # 上行帧尾
DOWN_FRAME_HEAD = b'XX'      # 下行帧头
DOWN_FRAME_END = b'YY'      # 下行帧尾
crc16 = crcmod.predefined.Crc('modbus')
AREA_DICT = {
    0x81: "I",
    0x82: "Q",
    0x83: "M",
    0x84: "DB"
}

# 测试数据(和 PLC 一样)
data = bytes(
    [0x41, 0x41, 0x84, 0x84, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x11, 0x22, 0x33,
     0x44, 0x55, 0x42, 0x42, 0xE3, 0xA5])


def parse_up_datas(datas: bytes):
    """
    解析上行数据
    :param datas: PLC发出的上行数据
    :return: 自定义区域变量AreaVar的列表
    """
    if len(datas) < 21:
        print(f"❌数据校验失败!")
        return False
    head = datas[0:2]    # 帧头
    tail = datas[-4:-2]   # 帧尾
    if head != UP_FRAME_HEAD or tail != UP_FRAME_END:
        print(f"❌数据校验失败!")
        return False
    # CRC
    crc_int = int.from_bytes(datas[-2:], byteorder='big')
    crc16.update(datas[:-2])
    crc_result = crc16.crcValue
    if crc_result != crc_int:
        print(f"❌数据校验失败!")
        return False
    # 截取上行数据的有效部分
    up_data = datas[2:-4]

    pos = 0   # 定义一个指针,用来遍历字节
    AreaVars =[]  # 输出的变量列表

    # 解析数据
    while pos < len(up_data):
        try:
            area = AREA_DICT[up_data[pos]]
        except KeyError:
            print(f"❌数据中区域代码错误!")
            return False
        pos += 2

        DBnum = int.from_bytes(up_data[pos:pos+4], byteorder='big')
        pos += 4

        offset = int.from_bytes(up_data[pos:pos+4], byteorder='big')
        pos += 4

        byte_count = int.from_bytes(up_data[pos:pos+4], byteorder='big')
        pos += 4

        bytes_out = up_data[pos:pos+byte_count]
        pos += byte_count
        AreaVars.append(AreaVar(area, DBnum, offset, byte_count, bytes_out))

    if pos == len(up_data):
            return AreaVars
    else:
        print(f"❌数据长度错误,解析失败!")
        return False


vas = parse_up_datas(data)
print(vas)  # [AreaVar(area='DB', DBnum=1, offset=0, byte_count=5, content=b'\x11"3DU')]
print(vas[0].area)   # DB
print(vas[0].DBnum)  # 1
print(vas[0].offset)  # 0
print(vas[0].byte_count)  # 5
print(vas[0].content.hex())  # 1122334455

三、创建类似于PLC的存储区以及读写方法

python 复制代码
import numpy as np
from typing import NamedTuple


class AreaVar(NamedTuple):
    area: str  # I、Q、M、DB
    DBnum: int  # DB号(I/Q/M 填 0)
    offset: int  # 字节偏移
    byte_count: int  # 字节数
    content: bytes=b''  # 数据内容


class DataStore:
    def __init__(self, max_db_number: int = 2048, area_size: int = 2048):
        """
        统一数据存储:I、Q、M、DB
        :param max_db_number: 最大 DB 块号
        :param area_size: 每个区域默认大小(字节)
        """
        self.area_size = area_size

        # I / Q / M 区:一维数组(DBnum=0)
        self.inputs = np.zeros(area_size, dtype=np.uint8)  # I区
        self.outputs = np.zeros(area_size, dtype=np.uint8)  # Q区
        self.memory = np.zeros(area_size, dtype=np.uint8)  # M区

        # DB区:二维数组 [DB号][偏移]
        self.db_blocks = np.zeros((max_db_number + 1, area_size), dtype=np.uint8)

    def write(self, var: AreaVar):
        """统一写入接口:支持 I/Q/M/DB"""
        data = np.frombuffer(var.content, dtype=np.uint8)
        start = var.offset
        end = start + var.byte_count

        if var.area == 'I':
            self.inputs[start:end] = data
        elif var.area == 'Q':
            self.outputs[start:end] = data
        elif var.area == 'M':
            self.memory[start:end] = data
        elif var.area == 'DB':
            self.db_blocks[var.DBnum, start:end] = data
        else:
            raise ValueError(f"不支持的区域: {var.area}")

    def read(self, var: AreaVar) -> bytes:
        """统一读取接口:支持 I/Q/M/DB"""
        start = var.offset
        end = start + var.byte_count

        if var.area == 'I':
            arr = self.inputs[start:end]
        elif var.area == 'Q':
            arr = self.outputs[start:end]
        elif var.area == 'M':
            arr = self.memory[start:end]
        elif var.area == 'DB':
            arr = self.db_blocks[var.DBnum, start:end]
        else:
            raise ValueError(f"不支持的区域: {var.area}")

        return arr.tobytes()


# -------------------
# 测试:I/Q/M/DB 全部支持
# -------------------
if __name__ == '__main__':
    store = DataStore()

    # ========== 测试 DB 区 ==========
    db_var = AreaVar('DB', 1, 0, 5, b'\x11"3DU')
    store.write(db_var)
    res_db = store.read(db_var)
    print("DB1 读写一致:", res_db == db_var.content)

    # ========== 测试 I 区 (DBnum=0) ==========
    i_var = AreaVar('I', 0, 10, 3, b'abc')
    store.write(i_var)
    res_i = store.read(i_var)
    print("I 区读写一致:", res_i == i_var.content)

    # ========== 测试 Q 区 ==========
    q_var = AreaVar('Q', 0, 20, 2, b'\x01\x02')
    store.write(q_var)
    res_q = store.read(q_var)
    print("Q 区读写一致:", res_q == q_var.content)

    # ========== 测试 M 区 ==========
    m_var = AreaVar('M', 0, 5, 4, b'test')
    store.write(m_var)
    res_m = store.read(m_var)
    print("M 区读写一致:", res_m == m_var.content)

    # ========== 读取 Q 区一个字节 ==========
    q_var = AreaVar('Q', 0, 21, 1)
    res_q = store.read(q_var)
    print(f"QB21读取结果:, {res_q}")  # QB21读取结果:, b'\x02'
相关推荐
慕涯AI4 小时前
Agent 30 课程开发指南 - 第19课
人工智能·python
2301_764150564 小时前
如何用 some 检测数组中是否存在至少一个满足条件的项
jvm·数据库·python
Johnstons4 小时前
DNS解析慢不是网络差:一次应用卡顿排查实战
网络
我是无敌小恐龙4 小时前
线下班第一课
python·考研·django·ai编程
爱学习的小囧5 小时前
VMware NSX-T Data Center 3.2.3.0 部署后账号密码获取及登录配置教程
linux·运维·服务器·网络·数据库·esxi
_oP_i5 小时前
python 之playwright 介绍
开发语言·python
@不误正业5 小时前
大模型注意力机制源码解析-从MQA到MLA全链路演进与PyTorch实现
人工智能·pytorch·python
weixin_408717775 小时前
CSS如何优化大型项目样式_使用SASS预处理器提升开发效率
jvm·数据库·python
2301_813599555 小时前
CSS如何解决CSS引入后的样式覆盖_理解优先级原则避免重写
jvm·数据库·python
WYiQIU5 小时前
宇树科技Web前端岗(AI方向),这不算泄题吧......
前端·vue.js·人工智能·笔记·科技·面试·职场和发展