基于QtPy (PySide6) 的PLC-HMI工程项目(一)使用自定义socket协议的基本方法

  • 一个基本的demo

DEMO展示了创建socket连接,以及断线重连、数据收发、心跳保活的基本方法,同时展示了从自定义格式的数据包中解析出指定类型数据的方法。

1、代码:

python 复制代码
import sys
import socket
import struct
import time
from PySide6.QtCore import Qt, QThread, Signal, QDateTime
from PySide6.QtWidgets import *

# ==================== 工业配置 ====================
PLC_IP = "127.0.0.1"  # 替换为实际的 PLC IP 地址
PLC_PORT = 5000  # 自定义端口
FRAME_LEN = 21   # 20字节数据 + 校验
HEARTBEAT_INTERVAL = 3   # 心跳间隔
HEARTBEAT_TIMEOUT = 5    # 心跳超时
RECONNECT_DELAY = 2      # 重连间隔

# ==================== 通信线程 ====================
class SocketWorker(QThread):
    log = Signal(str)
    connected = Signal(bool)
    dataUpdate = Signal(dict)

    def __init__(self):
        super().__init__()
        self.sock = None  # 通信套接字
        self.running = True
        self.is_conn = False
        self.sendQueue = []  # 待发送队列
        self.buffer = b""   # 接收缓冲区
        self.last_hb_recv = time.time()   # 最后心跳接收时间
        self.last_hb_send = time.time()    # 最后心跳发送时间

    def run(self):
        while self.running:
            if not self.is_conn:  # 如果未连接,尝试重连
                self.do_connect()  # 连接PLC
                time.sleep(RECONNECT_DELAY)   # 重连间隔
                continue
            # #####################################可以与do_send()合并,如果近期有数据发送,则不用发心跳
            self.do_heartbeat()  # 发送心跳

            try:
                self.do_send()  # 发送数据
                self.do_recv()  # 接收数据
            except Exception as e:
                self.log.emit(f"异常:{str(e)}")
                self.disconnect()  # 断开连接

            time.sleep(0.05)

        self.cleanup()

    # 连接PLC
    def do_connect(self):
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.settimeout(1)
            self.sock.connect((PLC_IP, PLC_PORT))
            self.is_conn = True
            self.connected.emit(True)
            self.log.emit("✅ 已连接 PLC")
            self.buffer = b""
            self.last_hb_recv = time.time()
        except Exception as e:
            self.log.emit(f"❌ 连接失败:{str(e)}")

    def do_heartbeat(self):
        now = time.time()
        if now - self.last_hb_send > HEARTBEAT_INTERVAL:
            self.sendQueue.append(b"\xAA")
            self.last_hb_send = now

        if now - self.last_hb_recv > HEARTBEAT_TIMEOUT:
            self.log.emit("💔 心跳超时,断线")
            self.disconnect()

    def do_send(self):
        while self.sendQueue:
            data = self.sendQueue.pop(0)
            self.sock.sendall(data)

    def do_recv(self):
        recv = self.sock.recv(1024)
        if not recv:
            self.disconnect()
            return

        # 处理心跳########################################可以不处理,因为PLC在持续发出数据
        if b"\xAA" in recv:
            self.sock.send(b"\xBB")
            self.last_hb_recv = time.time()
            return
        if b"\xBB" in recv:
            self.last_hb_recv = time.time()
            return

        self.buffer += recv

        # 防粘包 + 校验
        while len(self.buffer) >= FRAME_LEN:
            frame = self.buffer[:FRAME_LEN]
            self.buffer = self.buffer[FRAME_LEN:]

            if not self.check_sum(frame):
                self.log.emit("⚠️ 校验错误")
                self.buffer = b""  # 校验失败,清空缓冲区
                continue

            parsed = self.parse(frame)  # 解析数据
            if parsed:
                self.dataUpdate.emit(parsed)

    # 校验
    def check_sum(self, frame):
        calc = sum(frame[:20]) & 0xFF
        return calc == frame[20]

    # 解析数据
    def parse(self, frame):
        try:
            temp, press, flow, pump, valve = struct.unpack(">fhhBB10x", frame[:20])
            return {
                "温度": round(temp, 2),
                "压力": press,
                "流量": flow,
                "泵运行": bool(pump),
                "阀打开": bool(valve)
            }
        except:
            return None

    def send_data(self, temp_set, press_set, pump_cmd):
        try:
            payload = struct.pack(">fhhB14x", temp_set, press_set, pump_cmd, 0)
            cs = sum(payload[:20]) & 0xFF
            frame = payload + bytes([cs])
            self.sendQueue.append(frame)
            self.log.emit(f"下发:温度={temp_set} 压力={press_set} 泵={pump_cmd}")
        except:
            self.log.emit("下发失败")

    def disconnect(self):
        self.is_conn = False
        self.connected.emit(False)
        try:
            self.sock.close()
        except:
            pass

    def stop(self):
        self.running = False
        # self.wait()
        self.quit()

    def cleanup(self):
        try:
            self.sock.close()
        except:
            pass

# ==================== 主界面 ====================
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Socket TCP 工业上位机(校验+心跳+重连)")
        self.setFixedSize(680, 600)
        self.worker = SocketWorker()
        self.worker.log.connect(self.log)
        self.worker.connected.connect(self.on_conn)
        self.worker.dataUpdate.connect(self.show_data)
        self.worker.start()
        self.init_ui()

    def init_ui(self):
        w = QWidget()
        self.setCentralWidget(w)
        layout = QVBoxLayout(w)
        layout.setSpacing(10)

        self.status_lab = QLabel("🔴 未连接")
        layout.addWidget(self.status_lab)

        form = QFormLayout()
        self.temp_lab = QLabel("0.0 ℃")
        self.press_lab = QLabel("0")
        self.flow_lab = QLabel("0")
        self.pump_lab = QLabel("停止")
        self.valve_lab = QLabel("关闭")
        form.addRow("温度:", self.temp_lab)
        form.addRow("压力:", self.press_lab)
        form.addRow("流量:", self.flow_lab)
        form.addRow("泵状态:", self.pump_lab)
        form.addRow("阀状态:", self.valve_lab)
        layout.addLayout(form)

        h1 = QHBoxLayout()
        self.temp_set = QLineEdit()
        self.press_set = QLineEdit()
        self.pump_on = QPushButton("泵启动")
        self.pump_off = QPushButton("泵停止")
        self.send_btn = QPushButton("下发参数")
        h1.addWidget(QLabel("温度设定:"))
        h1.addWidget(self.temp_set)
        h1.addWidget(QLabel("压力设定:"))
        h1.addWidget(self.press_set)
        layout.addLayout(h1)

        h2 = QHBoxLayout()
        h2.addWidget(self.pump_on)
        h2.addWidget(self.pump_off)
        h2.addWidget(self.send_btn)
        layout.addLayout(h2)

        self.log_box = QTextEdit()
        self.log_box.setReadOnly(True)
        layout.addWidget(QLabel("日志:"))
        layout.addWidget(self.log_box)

        self.send_btn.clicked.connect(self.do_send)
        self.pump_on.clicked.connect(lambda: self.do_send(1))
        self.pump_off.clicked.connect(lambda: self.do_send(0))

    def show_data(self, d):
        self.temp_lab.setText(f"{d['温度']} ℃")
        self.press_lab.setText(str(d['压力']))
        self.flow_lab.setText(str(d['流量']))
        self.pump_lab.setText("运行" if d['泵运行'] else "停止")
        self.valve_lab.setText("打开" if d['阀打开'] else "关闭")

    def do_send(self, pump=None):
        t = float(self.temp_set.text() or 0)
        p = int(self.press_set.text() or 0)
        cmd = pump if pump is not None else 0
        self.worker.send_data(t, p, cmd)

    def on_conn(self, st):
        self.status_lab.setText("🟢 已连接" if st else "🔴 未连接")

    def log(self, msg):
        t = QDateTime.currentDateTime().toString("HH:mm:ss")
        self.log_box.append(f"[{t}] {msg}")
        self.log_box.verticalScrollBar().setValue(self.log_box.verticalScrollBar().maximum())

    def closeEvent(self, e):
        self.worker.stop()
        e.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec())

2、模拟运行

  • 打开TCP调试软件,创建本地服务器

运行程序:

当然了,这只是一个最基本的示意程序。在实际的工程应用中,还需要配合变量定义、PLC寻址、更新周期定义、用户管理、数据库管理等等功能。

相关推荐
秃头狂魔2 小时前
【HOT100】DAY2
python·算法
程序员三藏2 小时前
接口自动化测试思路和实战:编写线性测试脚本实战
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
丁当粑粑2 小时前
Pydantic的主要用法
python
哈伦20192 小时前
第二章 Python语法基础
python·语法·anaconda3
Clavis2 小时前
我给 Mac 的 Photo Booth 写了自动化脚本。为什么隐私比你想的重要得多
人工智能·python
龙文浩_2 小时前
AI机器学习中NumPy随机种子的应用
人工智能·python·深度学习·神经网络·机器学习
大江东去浪淘尽千古风流人物2 小时前
【Basalt】 VIO(sqrt_keypoint_vio)主流程measure函数梳理
数据库·人工智能·python·机器学习·oracle
FelixZhang0282 小时前
从 PDF 到 AI 知识库:RAG 数据预处理的六步标准流水线 (SOP)
人工智能·python·目标检测·计算机视觉·语言模型·ocr·numpy
凌盛羽2 小时前
在MDK-ARM编译后用python解析map文件在编译窗口输出Flash和RAM使用及剩余情况
arm开发·python·stm32·单片机·mysql·链表·esp32