基于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寻址、更新周期定义、用户管理、数据库管理等等功能。

相关推荐
唐装鼠11 分钟前
Nginx + Gunicorn + Python Web 应用 架构(Claude)
python·nginx·gunicorn
梦想三三16 分钟前
【PYthon词频统计与文本向量化】苏宁易购评论分析实战
开发语言·python
biter down1 小时前
9:JSONSchema
python
日晨难再1 小时前
C语言&Python&Bash&Tcl:全局变量和局部变量
c语言·python·bash·tcl
麻雀飞吧1 小时前
期货量化主连和具体合约怎么切:天勤 KQ.m 与 KQ.i 用法
python·区块链
先吃饱再说1 小时前
Python List 切片与 LLM Prompt 设计:从数据结构到接口调用
python
一只专注api接口开发的技术猿2 小时前
OpenClaw 对接淘宝商品 API,低成本实现全天候选品监控|附可运行 Python 实操代码
大数据·开发语言·数据库·python
xingpanvip2 小时前
星盘接口开发文档:马盘次限盘接口指南
android·开发语言·python·php·lua
FBI HackerHarry浩2 小时前
第二阶段Day07【Python生成器、yield关键字、property、正则表达式】
开发语言·python·正则表达式
梦想不只是梦与想2 小时前
Python 中的 4 种作用域
python·作用域