update server

This commit is contained in:
2026-07-30 11:12:31 +08:00
commit 4312cb878c
99 changed files with 24034 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
# control_engine.py
"""控制引擎:管理控制主循环,支持 PID / RL / MANUAL 三种模式。
纯业务逻辑,无 UI 依赖。通过回调与 UI 层通信。
设计原则:所有操作在主线程执行(QTimer 驱动),避免 Cython 编译后
在 PyInstaller 子线程中 segfault。
"""
import time
import os
import traceback
import sys
import logging
import numpy as np
from controllers import IncrementalPID
logger = logging.getLogger("ReinLoop.ControlEngine")
class ControlEngine:
"""控制主引擎"""
def __init__(self, pid: IncrementalPID):
self.pid = pid
self._running = False
self._cycle_count = 0
self._last_tick = 0.0
# 缓存参数(start 时从 UI 获取)
self.mode = "PID"
self.flow = 0.0
self.volume = 0.0
self.target_pressure = 80.0
self.manual_valve = 0.0
self.dz = None
self.motor_max = None
self.xa_full = 1062.5
self.collect_data = False
# 压力 EMA 滤波
self.pressure_alpha = 1 # 平滑系数(0~1),越小越平滑
self._pressure_filtered = None # 滤波后的压力值
# 外部依赖
self._conn_mgr = None
self._model_mgr = None
self._data_collector = None
# RL 模式相关
self.last_target_rl = None
self.Kp_0 = 1.0
self.Ki_0 = 0.4
# 回调
self._on_log = None
self._on_display_update = None
self._on_pid_ui_update = None
self._on_started = None
self._on_stopped = None
# ---- 依赖注入 ----
def set_connection_manager(self, mgr):
self._conn_mgr = mgr
def set_model_manager(self, mgr):
self._model_mgr = mgr
def set_data_collector(self, collector):
self._data_collector = collector
# ---- 回调设置 ----
def set_log_callback(self, cb):
self._on_log = cb
def set_display_update_callback(self, cb):
self._on_display_update = cb
def set_pid_ui_update_callback(self, cb):
self._on_pid_ui_update = cb
def set_started_callback(self, cb):
self._on_started = cb
def set_stopped_callback(self, cb):
self._on_stopped = cb
def log(self, message):
if self._on_log:
self._on_log(message)
@property
def is_running(self) -> bool:
return self._running
# ---- 启动/停止 ----
def start(self):
"""启动控制循环(主线程调用)"""
self.log("正在启动控制循环...")
if not self._conn_mgr or not self._conn_mgr.is_connected():
self.log("启动失败: 请先连接压力表")
return
if self.mode == "RL":
if not self._model_mgr or not self._model_mgr.is_model_loaded():
self.log("启动失败: 模型未加载,请先选择工况并点击【加载模型】按钮")
return
# 预置初始阀位(读取当前电机位置)
# try:
# position_x = self._conn_mgr.read_motor_position()
# initial_valve = self.pid.init_v(position_x)
# self.pid.output = initial_valve
# self.log(f"预置初始阀位 {initial_valve:.1f}%")
# except Exception as e:
# self.log(f"读取初始开度失败,将使用 80% 启动: {e}")
# self.pid.output = 80.0
self.pid.output = 100.0
# 设置死区
if self.dz is not None:
self.pid.dead_area = self.dz
# RL 模式:在主线程预先完成模型预测
if self.mode == "RL":
try:
current_p = self._conn_mgr.read_pressure()
if current_p is None:
current_p = 0.0
self._rl_predict(current_p, self.target_pressure)
self.log(f"RL 初始预测: Kp={self.pid.kp:.4f}, Ki={self.pid.ki:.4f}")
except Exception as e:
self.log(f"模型调用异常,使用默认pid: {e}")
# 重置状态
self.last_target_rl = None
self._pressure_filtered = None # 复位滤波器
self._cycle_count = 0
self._last_tick = time.perf_counter()
if self._data_collector:
self._data_collector.reset()
self._running = True
if self._on_started:
self._on_started()
self.log(f"控制循环已启动 (模式: {self.mode}, 目标: {self.target_pressure} kPa)")
def control_tick(self):
"""主线程 QTimer 每次触发时调用——执行一个控制周期"""
if not self._running:
return
cycle_start = time.perf_counter()
try:
# 1. 读取当前压力(原始值)
raw_pressure = self._conn_mgr.read_pressure()
if raw_pressure is None:
self.log("读取当前压力失败,检查地址和连接")
return
# EMA 低通滤波:平滑毛刺
if self._pressure_filtered is None:
self._pressure_filtered = raw_pressure
else:
self._pressure_filtered = (self.pressure_alpha * raw_pressure
+ (1 - self.pressure_alpha) * self._pressure_filtered)
current_pressure = self._pressure_filtered
target_pressure = self.target_pressure
mode = self.mode
# 2. 根据模式计算阀门开度
if mode == "PID":
valve_opening = self._pid_step(current_pressure, target_pressure)
elif mode == "RL":
valve_opening = self._rl_step(current_pressure, target_pressure)
elif mode == "MANUAL":
valve_opening = self._manual_step()
else:
self.log("错误!未知控制模式")
valve_opening = 0.0
# 3. 数据采集
if self.collect_data and self._data_collector:
self._data_collector.record_step(
cycle_count=self._cycle_count,
current_pressure=current_pressure,
target_pressure=target_pressure,
valve_opening=valve_opening,
kp=self.pid.kp, ki=self.pid.ki, kd=self.pid.kd,
q_in=self.flow, v=self.volume
)
# 4. 更新 UI 显示
if self._on_display_update:
self._on_display_update(current_pressure, target_pressure, valve_opening)
self._cycle_count += 1
# 5. 周期精确计时:若本周期用时不满 dt,sleep 补足
elapsed = time.perf_counter() - cycle_start
dt = self.pid.dt
# dt = 0.2
if elapsed < dt:
time.sleep(dt - elapsed)
# 6. 记录实际周期时长
now = time.perf_counter()
tick_time = now - cycle_start
# print(f"本周期用时 {tick_time*1000:.1f}ms (目标 {dt*1000:.0f}ms)")
self._last_tick = now
except Exception as e:
# control_tick 原本会捕获异常,因此异常不会进入 main.py 的
# sys.excepthook。这里必须主动把完整 traceback 打到控制台。
err_detail = traceback.format_exc()
print("\n" + "=" * 80, file=sys.stderr, flush=True)
print("ControlEngine.control_tick 发生异常:", file=sys.stderr, flush=True)
print(err_detail, file=sys.stderr, flush=True)
print("=" * 80, file=sys.stderr, flush=True)
# main.py 已配置控制台和文件日志;这里会同步写入 logs 目录。
logger.error("控制周期错误:\n%s", err_detail)
# UI 中保留一行简要信息,避免多行文本被控件截断。
self.log(
f"控制周期错误: {type(e).__name__}: {e}"
f"完整 traceback 请看运行控制台或 logs 日志"
)
def stop(self):
"""停止控制循环"""
self._running = False
if self._data_collector:
self._data_collector.finalize_and_upload(self.flow, self.volume)
self.log("控制循环已停止")
if self._on_stopped:
self._on_stopped()
# ---- PID 模式 ----
def _pid_step(self, current_pressure, target_pressure):
"""PID 控制单步"""
self.pid.update_pressure_values(current_pressure, target_pressure)
valve_opening = self.pid.update()
xa = self.xa_full * (100 - valve_opening) / 100
self._conn_mgr.set_motor_position(xa)
return valve_opening
# ---- RL 模式 ----
def _rl_step(self, current_pressure, target_pressure):
"""RL 增强控制单步"""
# 跟踪目标压力变化,触发 RL 重预测
if self.last_target_rl is None:
self.last_target_rl = target_pressure
elif self.last_target_rl != target_pressure:
self.last_target_rl = target_pressure
try:
self._rl_predict(current_pressure, target_pressure)
except Exception as e:
self.log(f"模型调用异常,使用默认pid: {e}")
# 检查高级设置中的单步限幅是否有填入,如果有,使用填入的值;如果没有,使用默认函数
# if self.motor_max is not None:
# self.pid.set_du_max(self.motor_max * self.pid.dt)
# else:
# self.pid.get_du_max(target_pressure)
self.pid.update_pressure_values(current_pressure, target_pressure)
if self.motor_max is not None:
du_max = self.motor_max * self.pid.dt
else:
du_max = None
# PID 计算
valve_opening = self.pid.update(du_max)
# 位置换算(考虑死区)
xa = self.pid.dead_area + (100 - valve_opening) * (self.xa_full - self.pid.dead_area) / 100
self._conn_mgr.set_motor_position(xa)
return valve_opening
def _rl_predict(self, current_p, target_p):
"""执行 RL 模型预测并更新 PID 参数(只在主线程调用)"""
model = self._model_mgr.rl_model
if model is None:
print("[RL] 错误: rl_model 为 None,跳过预测")
return
obs = np.array([
self.flow / 100,
current_p / 100,
(target_p - current_p) / 100
], dtype=np.float32)
print(f"[RL] 预测 obs={obs}", flush=True)
action, _ = model.predict(obs, deterministic=True)
print(f"[RL] model.predict 完成, action={action}")
action_space = model.action_space
Kp_0 = float(action_space.high[0])
Ki_0 = float(action_space.high[1])
kp = float(Kp_0 + action[0])
ki = float(Ki_0 + action[1])
self.Kp_0 = Kp_0
self.Ki_0 = Ki_0
self.pid.update_parameters(kp, ki, self.pid.kd)
print(f"[RL] PID 参数已更新: Kp={kp:.4f}, Ki={ki:.4f}")
if self._on_pid_ui_update:
self._on_pid_ui_update(kp, ki, self.pid.kd)
# ---- MANUAL 模式 ----
def _manual_step(self):
"""手动模式单步"""
xa = self.pid.dead_area + (100 - self.manual_valve) * (self.xa_full - self.pid.dead_area) / 100
self._conn_mgr.set_motor_position(xa)
return self.manual_valve