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