基于QtPy (PySide6) 的PLC-HMI工程项目(十四)后续完善和改进:上位机数据解析的防粘包、残包、废包、拆包

基于:基于socket的 TCP上位机通信内容解析程序-CSDN博客

改进后的PLC.py:

import socket
import struct
from typing import NamedTuple

import numpy as np
from PySide6.QtCore import QObject, Signal, QTimer, Slot, QMutex, QThread, QMutexLocker

from .settings import *

# 字节长度的字典
type_bytes = {'Byte': 1,'Bool': 1, 'Int': 2, 'Real': 4, 'Word':2,
              'DInt':4, 'DWord':4, 'LInt':4, 'LWord':4, 'LReal':8}

def set_bit(b: bytes,  bit_idx: int, value: int) -> bytes:
    """
    修改 bytes 变量的某一位
    :param b: 原始字节
    :param bit_idx: 要修改的位下标(0~7,0 是最低位)
    :param value: 目标值 0 或 1
    :return: 修改后的新 bytes
    """
    # 转成可变的 bytearray
    arr = bytearray(b)

    # 取出目标字节
    byte_val = arr[0]
    if value == 1:
        # 置 1:按位或 |
        # b = 1 << bit_idx
        # byte_val = byte_val | b
        byte_val |= 1 << bit_idx
    else:
        # 置 0:按位与 & 取反 ~
        byte_val &= ~(1 << bit_idx)
    # 写回
    arr[0] = byte_val
    # 转回 bytes
    return bytes(arr)

def get_bit(byte_val: bytes, bit_idx: int) -> int:
    """
    判断一个字节的某一位是 0 还是 1
    :param byte_val: 输入字节(只判断第一个字节)
    :param bit_idx: 位索引(0~7,0=最低位)
    :return: 0 或 1
    """
    # return 1 if (byte_val[0] & (1 << bit_idx)) != 0 else 0
    return True if (byte_val[0] & (1 << bit_idx)) != 0 else False

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

class VariableFrameParser:
    HEADER = b'\x41\x41'
    TAIL = b'\x42\x42'
    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)
            print("header_pos:", header_pos)
            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)
                print("tail_pos:", tail_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}
                    return payload

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

# 区域变量
class AreaVar(NamedTuple):
    area: str  # I、Q、M、DB
    DBnum: int  # DB号(I/Q/M=0)
    offset: int  # 字节偏移
    byte_count: int  # 字节数
    value_bytes: bytes= b''  # 数据的字节

    def __eq__(self, other):
        return self.value_bytes == other.value_bytes

# UI变量
class UiVari(QObject):
    valueChanged = Signal(object)
    def __init__(self, typ:str, byte_count:int, area:str, offset:int, DBnum=0, bit=0, value=None ):
        """
        UI变量,用来与UI部件交互和触发通信操作
        :param typ:
        :param typ: 类型
        :param byte_count: 字节数
        :param area: 区域
        :param offset: 地址偏移
        :param DBnum: DB编号
        :param bit: 位
        :param value: 值
        """
        super().__init__()
        self.typ = typ
        self.byte_count = byte_count
        self.area = area
        self.DBnum = DBnum
        self.offset = offset
        self.bit = bit
        self._value = value

    def __eq__(self, other):
        return self.value == other.value


    @property
    def value(self):
        """获取变量值"""
        return self._value

    @value.setter
    def value(self, new_val):
        """设置值,变化时自动发射信号"""
        if self._value != new_val:
            self._value = new_val
            self.valueChanged.emit(new_val)

    def set_value(self, val):
        """外部设置值接口(语义更清晰)"""
        self.value = val

# 上行报文变量
class UplinkMsg(QObject):
    valueChanged = Signal(object)
    def __init__(self):
        super().__init__()
        self._value = b''

    @property
    def value(self):
        """获取变量值"""
        return self._value

    @value.setter
    def value(self, new_val):
        """设置值,变化时自动发射信号"""
        if self._value != new_val:
            self._value = new_val
            self.valueChanged.emit(new_val)

    def set_value(self, val):
        """外部设置值接口(语义更清晰)"""
        self.value = val


# Numpy格式数据区存储(支持 I/Q/M/DB)
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.I_area = np.zeros(area_size, dtype=np.uint8)  # I区
        self.Q_area = np.zeros(area_size, dtype=np.uint8)  # Q区
        self.M_area = np.zeros(area_size, dtype=np.uint8)  # M区

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

# 变量表管理器:按变量名直接读写
class VariManager:
    def __init__(self, store: DataStore):
        """
        变量表管理器:按变量名直接读写
        :param store: 变量存储区
        """
        self.store = store
        self.vari_lock = QMutex()
        self.store_lock = QMutex()

    # UiVari变量转为AreaVar变量
    def UiVari2AreaVar(self, ui_vari:UiVari):
    # def pack_value(self, ui_vari:UiVari, value):
        typ = ui_vari.typ  # 变量格式

        value_bytes = b''

        # 先构造一个AreaVar变量

        area_var = AreaVar(
            area=ui_vari.area,
            DBnum=ui_vari.DBnum,
            offset=ui_vari.offset,
            byte_count=ui_vari.byte_count
        )
        if ui_vari.value is not None:
            value = ui_vari.value
            # 打包成 bytes
            if typ == 'Bool':
                value_bytes = self.read_block(area_var)
                bit = ui_vari.bit
                value_bytes = set_bit(value_bytes, bit, value)
            elif typ == 'Byte':
                value_bytes = value
            elif typ == 'Int':
                value_bytes = struct.pack('>h', value)  # 大端 Int16
            elif typ == 'Real':
                value_bytes = struct.pack('>f', value)  # 大端 Float32
            elif typ == 'Word' or typ == 'DWord' or typ == 'LWord':
                value_bytes = value
            elif typ == 'LInt':
                value_bytes = struct.pack('>i', value)
            elif typ == 'LReal':
                value_bytes = struct.pack('>d', value)
            else:
                raise TypeError(f"不支持类型: {typ}")

        area_var = AreaVar(
            area=ui_vari.area,
            DBnum=ui_vari.DBnum,
            offset=ui_vari.offset,
            byte_count=ui_vari.byte_count,
            value_bytes=value_bytes
        )
        return area_var


    # ------------------- 按变量 写值 -------------------
    def write_vari(self, ui_vari:UiVari, value):
        ui_vari.set_value(value)
        with QMutexLocker(self.vari_lock):
            area_var = self.UiVari2AreaVar(ui_vari)
            self.write_block(area_var)

    # ------------------- 按变量 读值 -------------------
    def read_vari(self, ui_vari:UiVari):
        with QMutexLocker(self.vari_lock):
            typ = ui_vari.typ  # 变量格式
            # 先构造一个AreaVar变量
            area_var =self.UiVari2AreaVar(ui_vari)
            # 读取变量字节串
            data = self.read_block(area_var)
            # 解析值
            if typ == 'Bool':
                # value = 1 if get_bit(data, ui_vari.bit) == True else 0
                value = get_bit(data, ui_vari.bit)
            elif typ == 'Byte':
                value = data
            elif typ == 'Int':
                value = struct.unpack('>h', data)[0]
            elif typ == 'Real':
                value = struct.unpack('>f', data)[0]
            elif typ == 'Word' or typ == 'DWord' or typ == 'LWord':
                value = data
            elif typ == 'LInt':
                value = struct.unpack('>i', data)[0]
            elif typ == 'LReal':
                value = struct.unpack('>d', data)[0]
            else:
                raise TypeError(f"不支持类型: {typ}")

            # 将变量本身的value也修改
            ui_vari.set_value(value)
            # print(ui_vari)
            return value

    # ------------------- 按块 写值 -------------------
    def write_block(self, area_var: AreaVar):
        """统一写入接口:支持 I/Q/M/DB"""
        with QMutexLocker(self.store_lock):
            data = np.frombuffer(area_var.value_bytes, dtype=np.uint8)
            start = area_var.offset
            end = start + area_var.byte_count

            if area_var.area == 'I':
                self.store.I_area[start:end] = data
            elif area_var.area == 'Q':
                self.store.Q_area[start:end] = data
            elif area_var.area == 'M':
                self.store.M_area[start:end] = data
            elif area_var.area == 'DB':
                self.store.DB_area[area_var.DBnum, start:end] = data
            else:
                raise ValueError(f"不支持的区域: {area_var.area}")

    # ------------------- 按块 读值 -------------------
    def read_block(self, area_var: AreaVar) -> bytes:
        """统一读取接口:支持 I/Q/M/DB"""
        with QMutexLocker(self.store_lock):
            start = area_var.offset
            end = start + area_var.byte_count

            if area_var.area == 'I':
                arr = self.store.I_area[start:end]
            elif area_var.area == 'Q':
                arr = self.store.Q_area[start:end]
            elif area_var.area == 'M':
                arr = self.store.M_area[start:end]
            elif area_var.area == 'DB':
                arr = self.store.DB_area[area_var.DBnum, start:end]
            else:
                raise ValueError(f"不支持的区域: {area_var.area}")

            return arr.tobytes()

# 用于TCP通信的周期运行的子线程工作者
class ThreadWorker(QObject):
    _signal_start = Signal()  # 开始运行的信号
    _signal_stop = Signal()  # 停止信号
    _signal_run_now = Signal()  # 打破定时器周期立即运行一次

    def __init__(self, target, looping, loop_interval=0, *args, **kwargs):
        """
        :param target: 需要在子线程中运行的目标函数
        :param looping: 是周期运行还是一次性任务
        :param loop_interval: 周期运行的间隙时间
        :param args: 附带参数
        :param kwargs:
        """
        super().__init__()
        self._loop_timer = None  # 循环运行定时器
        self.looping = looping  # 是否循环运行
        self.loop_interval = loop_interval  # 循环运行间隔
        self.target = target  # 目标函数
        self.args = args
        self.kwargs = kwargs
        self.started = False  # 已经开始
        self._signal_start.connect(self._start)
        self._signal_stop.connect(self._stop)
        self._signal_run_now.connect(self._run_target)

    # 内部接口,在子线程内用槽函数操作定时器
    @Slot()
    def _start(self):
        self._run_target()
        if self.looping:
            if self._loop_timer is None:
                self._loop_timer = QTimer()
                self._loop_timer.setInterval(self.loop_interval)
                self._loop_timer.timeout.connect(self._run_target)
            self._loop_timer.start()

    # 内部接口,在子线程内用槽函数操作定时器
    @Slot()
    def _stop(self):
        if self._loop_timer is not None:
            self._loop_timer.stop()

    # 外部接口,在调用线程内发射信号
    def start(self):
        self.started = True
        self._signal_start.emit()

    # 外部接口,在调用线程内发射信号
    def stop(self):
        self.started = False
        self._signal_stop.emit()

    # 外部接口,在调用线程内发射信号
    def run_now(self):
        self._signal_run_now.emit()

    # 槽函数,运行目标函数
    @Slot()
    def _run_target(self):
        if self._loop_timer is not None:
            self._loop_timer.stop()  # 停止定时器,防止信号堆积

        if self.started:
            self.target(*self.args, **self.kwargs)
        if self._loop_timer is not None:
            self._loop_timer.start()  # 重启定时器

# TCP客户端
class TcpClient(QObject):
    def __init__(self, ip, port):
        super().__init__()

        self.ip = ip
        self.port = port
        self.socket = None

        self.is_connected = False
        self.send_bytes = b""  # 发送的字节
        self.rsv_msg = UplinkMsg() # 接收到的字节

        self.rsv_lock = QMutex()
        self.send_lock = QMutex()

        ############## 创建线程和worker ################
        # 连接服务器
        self.connect_thread = QThread()  # 线程
        self.connect_worker = ThreadWorker(target=self.connect_server,
                                           looping=True,
                                           loop_interval=RECONNECT_INTERVAL)  # worker
        self.connect_worker.moveToThread(self.connect_thread)  # 把worker移动到线程中
        self.connect_thread.start()  # 启动线程

        # 在子线程中周期发送数据
        self.send_thread = QThread()  # 线程
        self.send_worker = ThreadWorker(target=self.send_data,
                                        looping=True,
                                        loop_interval=HEARTBEAT_INTERVAL)  # worker
        self.send_worker.moveToThread(self.send_thread)  # 把worker移动到线程中
        self.send_thread.start()  # 启动线程

        # 在子线程中持续接收数据
        self.recv_thread = QThread()  # 线程
        self.recv_worker = ThreadWorker(target=self.recv_data,
                                        looping=True,
                                        loop_interval=0)  # worker
        self.recv_worker.moveToThread(self.recv_thread)  # 把worker移动到线程中
        self.recv_thread.start()  # 启动线程
        self.uplink_parser = VariableFrameParser()  # 上行数据的解析程序

    def connect_server(self):
        """建立 TCP 连接"""
        if self.is_connected:
            return
        try:
            # 停止发送工作者
            self.send_worker.stop()
            # 停止接收工作者
            self.recv_worker.stop()

            # 创建 TCP socket
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(CONNECT_TIMEOUT)
            self.socket.connect((self.ip, self.port))  # 连接服务器
            self.is_connected = True
            self.connect_worker.stop()  # 连接完成,停止连接worker
            print(f"✅ 成功连接到服务器 {self.ip}:{self.port}")
            # 启动发送工作者
            self.send_worker.start()
            # 启动接收工作者
            self.recv_worker.start()

        except Exception as e:
            print(f"❌ 连接失败,{RECONNECT_INTERVAL}秒后重试... 错误:{e}")
            self.is_connected = False
            # self.connect_worker.stop()
            # self.send_worker.stop()
            # self.recv_worker.stop()

    def send_data(self):
        """发送数据"""
        if not self.is_connected:
            print("⚠️ 未连接,无法发送")
            return False

        try:
            with QMutexLocker(self.send_lock):
                # self.send_bytes =
                # 发送数据
                self.socket.send(self.send_bytes)
                # print(f"📤 已发送:{self.send_bytes}")
                self.send_bytes = DOWNLINK_FRAME_HEAD + DOWNLINK_FRAME_END  # 发送完成后清空发送区
                return True
        except Exception as e:
            print(f"❌ 发送失败:{e}")
            self.is_connected = False
            self.send_worker.stop()
            self.recv_worker.stop()
            self.connect_worker.start()
            return False

    # 打破周期立即发送一个数据
    def send_now(self):
        self.send_worker.run_now()

    def recv_data(self):
        """接收数据(处理半包/粘包)"""
        if not self.is_connected:
            print("⚠️ 未连接,无法接收")
            return False
        # buffer = b''  # 清空接收缓存区

        try:
            data = self.socket.recv(1024)
            # print("data: ", type(data))
            if not data:
                print("🔌 服务器断开连接")
                self.is_connected = False
                self.send_worker.stop()
                self.recv_worker.stop()
                self.connect_worker.start()
                return False
            frames = self.uplink_parser.feed(data) # 解析到的帧数据
            # print("frames: ", frames)
            with QMutexLocker(self.rsv_lock):
                for frame in frames:
                    self.rsv_msg.set_value(frame)
                    print(f"📥 收到数据:{frame}")
        except socket.timeout:
            return True
        except Exception as e:
            print(f"❌ 接收异常:{e}")
            self.is_connected = False
            self.send_worker.stop()
            self.recv_worker.stop()
            self.connect_worker.start()

    def start(self):
        """启动客户端"""
        print(f"🚀 启动 TCP 客户端,目标:{self.ip}:{self.port}")
        self.connect_worker.start()

    def quit(self):
        self.is_connected = False
        self.connect_worker.stop()
        self.send_worker.stop()
        self.recv_worker.stop()
        self.connect_thread.quit()
        self.recv_thread.quit()
        self.send_thread.quit()

# 把TcpClient和PlcPartner整合成一个,然后定义一个处理上行帧的函数,下行数据加进去CRC
# PLC通信伙伴
class PlcPartner(QObject):
    def __init__(self, plc_ip, plc_port, vari_table):
        super().__init__()
        self.all_vari_list = None
        self.vs = DataStore()  # 创建变量存储区
        self.vm = VariManager(self.vs)    # 创建变量管理器
        self.client = TcpClient(plc_ip, plc_port)  # 客户端
        self.vari_table = vari_table  # 变量表
        self.update_workers =[]  # 周期更新变量的工作者们
        self.parse_uplink_worker = None  # 解析上行报文的工作者



    # 写PLC变量
    def write_vari(self, ui_vari: UiVari, value):
        self.vm.write_vari(ui_vari, value)  # 先写入本地

        # 如果是写一个单bit,把位(Int格式)打包成字节
        if ui_vari.typ == 'Bool':
            bit = ui_vari.bit
            bit_bytes = struct.pack('>i', bit)
        else:
            bit_bytes = b'\xff\xff\xff\xff'  # 如果不是写单个的bit,位字节为 FFFFFFFF

        # 将Ui变量转为区域变量
        area_var = self.vm.UiVari2AreaVar(ui_vari)
        # 拼接下行报文
        area_var_bytes = self.AreaVar2msg(area_var)  # 把区域变量的信息转成字节携带在下行报文中
        send_bytes = b"".join([DOWNLINK_FRAME_HEAD, area_var_bytes, bit_bytes, area_var.value_bytes, DOWNLINK_FRAME_END])
        # print(send_bytes)
        #生成CRC
        crc_bytes = CRC16.new(send_bytes).crcValue.to_bytes(2, byteorder='big')
        # crc_bytes = struct.pack('>i', crc_result)
        # print(crc_bytes)
        with QMutexLocker(self.client.send_lock):  #DOWNLINK_FRAME_HEAD + DOWNLINK_FRAME_END
            self.client.send_bytes = send_bytes + crc_bytes
        # 发送报文到PLC
        self.send_now()

    def read_vari(self, ui_vari):
        # print('2')
        # with QMutexLocker(self.lock):
        v = self.vm.read_vari(ui_vari)
        return v

    def write_block(self, area_var: AreaVar):
        # with QMutexLocker(self.lock):
        self.vm.write_block(area_var)

    def read_block(self, area_var: AreaVar) -> bytes:
        # with QMutexLocker(self.lock):
        v = self.vm.read_block(area_var)
        return v

    def start(self):
        self.client.start()  # 启动客户端
        self.start_updater()  # 启动变量自动更新

    def quit(self):
        self.client.quit()
        self.quit_updater()

    # 打破周期立即发送
    def send_now(self):
        self.client.send_now()

    def AreaVar2msg(self, area_vari: AreaVar):
        # 把area变量的信息转换成字节
        if area_vari.area == "I":
            area_bytes = b'\x81\x81'
        elif area_vari.area == "Q":
            area_bytes = b'\x82\x82'
        elif area_vari.area == "M":
            area_bytes = b'\x83\x83'
        elif area_vari.area == "DB":
            area_bytes = b'\x84\x84'
        else:
            raise ValueError("下行报文组态错误:未识别的分区")

        # 把DB号转换成字节
        db_num_bytes = struct.pack('>i', area_vari.DBnum)
        #  把offset转换成字节
        offset_bytes = struct.pack('>i', area_vari.offset)
        # 把byte_count转换成字节
        byte_count_bytes = struct.pack('>i', area_vari.byte_count)

        out_bytes = b''.join([area_bytes, db_num_bytes, offset_bytes, byte_count_bytes])
        # print(out_bytes)
        return out_bytes

    def get_vari_update_table(self,varies: list):
        """
        从变量表的所有变量列表中获取到每个周期需要更新的变量内容
        :param varies:
        :return:
        """
        periods = set()
        for v in varies:
            periods.add(v[1])

        periods = sorted(list(periods))

        varis = []  # 最终的输出结果
        for p in periods:  # 所有周期
            # print(p)
            o = []
            for v in varies:
                if v[1] == p:
                    o.append(v[0])
            varis.append(o)
        # print(periods, varis)
        return periods, varis

    # 开始更新变量
    def start_updater(self):
        variables = self.vari_table.variables
        all_period, self.all_vari_list = self.get_vari_update_table(variables)
        for i, p in enumerate(all_period):
            worker = ThreadWorker(target=self.update_vari,
                                  looping=True,
                                  loop_interval=p
                                  )
            th = QThread()
            worker.moveToThread(th)
            worker.start()
            th.start()
            self.update_workers.append([worker,th])  # 新建的更新worker和thread
        # 解析上行报文的工作者
        self.parse_uplink_worker = ThreadWorker(target=self.parse_uplink_msg,
                              looping=False  # 非定时器周期运行,每次run_now()运行一次
                              )
        th = QThread()
        self.parse_uplink_worker.moveToThread(th)
        self.parse_uplink_worker.start()
        th.start()
        self.update_workers.append([self.parse_uplink_worker,th])
        # 当上行报文的内容发生变化就解析一次
        self.client.rsv_msg.valueChanged.connect(self.parse_uplink_worker.run_now)

    def quit_updater(self):
        for worker in self.update_workers:
            worker[0].stop()
            worker[1].quit()

    # 更新变量(通过读取变量表中的变量和周期定义)
    def update_vari(self):
        for vari_list in self.all_vari_list:
            for vari in vari_list:
                self.read_vari(vari)
                # if value != vari.value:
                #     self.vm.write_vari(vari, value)

    @Slot()
    def parse_uplink_msg(self):
        msg_bytes = self.client.rsv_msg.value
        # print("msg_bytes", msg_bytes)
        # print(msg_bytes)
        if msg_bytes == b'':
            return

        if len(msg_bytes) < 19:
            raise BufferError(msg_bytes, "❌:数据长度不足")
        # head = msg_bytes[0:2]  # 帧头
        # tail = msg_bytes[-4:-2]  # 帧尾
        # if head != UPLINK_FRAME_HEAD or tail != UPLINK_FRAME_END:
        #     raise BufferError(f"❌数据校验失败:帧头尾错误")
        # CRC
        # crc_int = int.from_bytes(msg_bytes[-2:], byteorder='big')
        # crc_result = CRC16.new(msg_bytes[:-2]).crcValue
        # if crc_result != crc_int:
        #     raise BufferError(f"❌数据校验失败:CRC失败")
        # # 截取上行数据的有效部分
        up_data = msg_bytes
        # lenxxx = len(up_data)
        # print(f"up_data: {lenxxx}",up_data)

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

        # 解析数据
        while pos < len(up_data):
            try:
                area = AREA_DICT[up_data[pos]]
            except KeyError:
                raise BufferError(f"❌数据校验失败:数据中区域代码错误")
            pos += 2

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

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

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

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

        if pos != len(up_data):
            raise BufferError(f"❌数据校验失败:数据长度错误,解析失败!")

        # with QMutexLocker(self.lock):
        for var in AreaVars:
            self.write_block(var)

    
  • 测试:

        下位将DB1.DBB0--DBB4的字节内容:“11 22 33 44 55”传输到上位,并映射到上位数据池,更新显示

        下位将油泵1的压力设定内容:“10.0”传输到上位,并映射到上位数据池,更新显示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深蓝海拓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值