完善流量控制 GUI 的开环扫点与手动操作流程
新增可配置开环扫点、多 CSV 前馈表拟合及结果文件生成;补充闭环停止、手动开度安全互锁和 PID 未启用状态显示,并同步更新自动化测试、使用文档、打包配置与 Windows 可执行程序。
This commit is contained in:
@@ -80,7 +80,7 @@ python -m pip install pymodbus matplotlib pyinstaller
|
||||
- `valve_model_004.json`:内置模型的外置备份和自定义模型格式参考,删除该外置副本不会影响内置模型;
|
||||
- `使用说明.txt`:现场操作、安全策略和故障说明。
|
||||
|
||||
双击 `FlowControl.exe` 后,核对 PLC 地址、端口和站号,再点击“连接设备”。连接成功后程序保持 `100%` 开度并以 `0.25 s` 周期监测阀后流量、阀前压力和阀后压力;首次设置 `0~300 SLM` 目标时启动 10 Hz 闭环,运行中可继续修改目标。界面显示的“下发总开度”为最终命令:
|
||||
双击 `FlowControl.exe` 后,核对 PLC 地址、端口和站号,再点击“连接设备”。连接成功后程序保持 `100%` 开度并以 `0.25 s` 周期监测阀后流量、阀前压力和阀后压力;输入 `0~300 SLM` 目标并点击“设置目标流量”时启动 10 Hz 闭环。闭环运行中该输入框和按钮保持可用,可直接输入新目标并再次点击以切换目标;其右侧的独立“停止流量控制”按钮只在闭环运行时可用,点击后程序停止闭环、立即下发 `100%` 总开度并返回监测状态。界面显示的“下发总开度”为最终命令:
|
||||
|
||||
```text
|
||||
总开度 = clamp(前馈开度 + PI 修正量, 0, 100)
|
||||
@@ -90,6 +90,14 @@ GUI 的 Modbus 读写与控制周期全部在单一后台线程中串行执行
|
||||
|
||||
默认模型只能在未连接状态下切换。点击“选择 JSON…”可加载其他模型,程序会在连接前检查 JSON 结构、有限数值、标定点数量、`800~1000` 行程范围以及 `motor_open` / `motor_closed` 与当前配置是否一致;无效模型不会进入控制。点击“使用内置”可恢复 `valve_model_004.json`。
|
||||
|
||||
连接设备后可点击模型选择按钮左侧的“开环扫点”。弹窗可设置起止开度、开度间隔、随机种子、采样周期、稳态窗口、相对/绝对标准差阈值、单点最长等待时间,以及是否追加闭阀端精细行程扫描。程序按随机顺序直接下发各开度,每个点在阀后流量稳定或达到最长等待时间后切换;扫点期间继续显示流量、压力、总开度和电机行程。若当前正在闭环控制,开始前会确认并停止闭环;扫点启动后按钮变为“停止扫点”,点击即可提前停止并生成已有数据的结果图。正常完成、用户停止、断开、退出或故障时都会保留已经采集的数据,并尝试把阀门恢复到 `100%` 开度。
|
||||
|
||||
开环数据写到程序所在目录的 `open_loop_data/`:采样过程中逐行落盘 CSV,结束时生成一张包含压力、流量和绝对压力比三组子图的 PNG。扫点结束后设备保持连接并回到监测状态;安全越限或通信故障仍会执行安全断开。
|
||||
|
||||
“目标流量”区域下方提供手动开度输入和“设置开度”按钮,允许范围为 `0~100%`。手动开度只允许在设备已连接、闭环和开环扫点均停止时下发;闭环运行中点击该按钮只会提示先点击“停止流量控制”,关闭提示后不会停止闭环,也不会发送任何开度命令。监测卡片会显示手动总开度和换算后的电机行程;因为 PID 没有参与手动下发,PID 模式显示为“未启用”。刚连接、停止闭环后以及其他仅监测状态也统一显示“未启用”。
|
||||
|
||||
“拟合前馈表”是离线工具,不会在每次扫点结束时自动生成 JSON:点击后可一次选择多份开环 CSV,再指定输出 JSON。程序以每个扫描步末尾 `20` 个样本求稳态均值,将多次实验中相同行程的数据合并平均,按 `A_eff = Q / (P1_abs × F(r))` 反解并生成可由“选择 JSON…”直接加载的阀模型;同时在同目录生成一张同名诊断 PNG。拟合在独立线程运行,只能在未连接、故障已断开或已连接监测状态启动,闭环及扫点过程中禁用。
|
||||
|
||||
源码运行与重新打包命令为:
|
||||
|
||||
```powershell
|
||||
@@ -101,6 +109,12 @@ Copy-Item valve_model/valve_model_004.json 可执行程序/valve_model_004.json
|
||||
|
||||
`--smoke-test` 只检查内置模型和关键依赖,不打开界面,也不会连接真实设备。
|
||||
|
||||
`gui_main.py` 是 GUI 入口而不是单文件源码:直接运行时仍会导入本工程的 `control_runtime.py`、`config.py`、`controllers.py`、`flow_control.py`、`PcControl.py`、`open_loop.py`、`valve_model.py`,并需要 `pymodbus`、`matplotlib` 和系统可用的 `tkinter`。PyInstaller 会分析这些导入,把 Python 解释器、项目模块、第三方库、Tcl/Tk 运行文件和内置 JSON 收集进单文件 EXE。双击 EXE 时,PyInstaller 先把内部文件释放到临时目录,再启动程序,所以交付后的 `FlowControl.exe` 不依赖工作区源码或现场安装 Python;它仍然依赖 Windows、可写的程序/临时目录、正常网络以及可访问的 PLC。
|
||||
|
||||
打包使用的 `build/flow_control_gui/` 是 PyInstaller 的中间工作目录,包含模块分析、归档、链接缓存和缺失模块告警,不是交付文件,也不是 EXE 的运行依赖;删除后可由上面的打包命令重新生成。`可执行程序/` 才是最终交付目录。
|
||||
|
||||
窗口版 EXE 的 `--smoke-test` 正常通过时不会显示窗口或输出文本,应通过进程退出码判断。PowerShell 中可执行 `$p = Start-Process .\可执行程序\FlowControl.exe -ArgumentList '--smoke-test' -Wait -PassThru; $p.ExitCode`,`0` 为通过,非 `0` 为失败。当前检查范围包括配置、内置模型、Modbus 类,以及实际创建一张临时 PNG 来验证开环绘图库;它不连接真实设备,因此不能替代 PLC 联机测试。
|
||||
|
||||
## 闭环控制
|
||||
|
||||
### 运行
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -14,20 +14,23 @@ Types of import:
|
||||
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
|
||||
tracking down the missing module yourself. Thanks!
|
||||
|
||||
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional), netrc (delayed, conditional), getpass (delayed, optional)
|
||||
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional)
|
||||
missing module named 'collections.abc' - imported by traceback (top-level), inspect (top-level), logging (top-level), typing (top-level), importlib.resources.readers (top-level), selectors (top-level), tracemalloc (top-level), http.client (top-level), asyncio.base_events (top-level), asyncio.coroutines (top-level), multidict._abc (top-level), attr._compat (top-level), yarl._query (top-level), yarl._url (top-level), propcache._helpers_py (top-level), yarl._path (top-level), aiohttp.web (top-level), aiohttp.abc (top-level), frozenlist (top-level), aiohttp.payload (conditional), aiohttp.web_response (top-level)
|
||||
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
|
||||
missing module named fcntl - imported by subprocess (optional), serial.serialposix (top-level)
|
||||
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
|
||||
missing module named _scproxy - imported by urllib.request (conditional)
|
||||
missing module named termios - imported by serial.serialposix (top-level), getpass (optional)
|
||||
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
missing module named 'collections.abc' - imported by traceback (top-level), inspect (top-level), logging (top-level), typing (top-level), importlib.resources.readers (top-level), selectors (top-level), tracemalloc (top-level), http.client (top-level), matplotlib (top-level), matplotlib.cbook (top-level), numpy._typing._array_like (top-level), numpy._typing._nested_sequence (conditional), typing_extensions (top-level), numpy._typing._shape (top-level), numpy._typing._dtype_like (top-level), asyncio.base_events (top-level), asyncio.coroutines (top-level), numpy.lib._function_base_impl (top-level), numpy.lib._npyio_impl (top-level), yaml.constructor (top-level), numpy.random._common (top-level), numpy.random._generator (top-level), numpy.random.bit_generator (top-level), numpy.random.mtrand (top-level), numpy.polynomial._polybase (top-level), matplotlib._path (top-level), matplotlib.colors (top-level), PIL.Image (top-level), PIL._typing (top-level), xml.etree.ElementTree (top-level), PIL.ImageFilter (top-level), PIL.ImagePalette (top-level), PIL.TiffImagePlugin (top-level), PIL.ImageOps (top-level), PIL.PngImagePlugin (top-level), pyparsing.core (top-level), pyparsing.results (top-level), markupsafe (top-level), matplotlib.rcsetup (top-level), matplotlib.axes._base (top-level), matplotlib.text (top-level), matplotlib.markers (top-level), matplotlib._text_helpers (top-level), fontTools.misc.loggingTools (top-level), matplotlib._mathtext (conditional), matplotlib.spines (top-level), matplotlib.cm (top-level), matplotlib._style_helpers (top-level), tornado.gen (top-level), tornado.httputil (top-level), matplotlib.pyplot (conditional), matplotlib.typing (top-level), multidict._abc (top-level), attr._compat (top-level), yarl._query (top-level), yarl._url (top-level), propcache._helpers_py (top-level), yarl._path (top-level), aiohttp.web (top-level), aiohttp.abc (top-level), frozenlist (top-level), aiohttp.payload (conditional), aiohttp.web_response (top-level), PIL.Jpeg2KImagePlugin (top-level), PIL.IptcImagePlugin (top-level)
|
||||
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional), http.server (delayed, optional), psutil (optional), netrc (delayed, conditional), getpass (delayed, optional)
|
||||
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional)
|
||||
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
|
||||
missing module named resource - imported by posix (top-level)
|
||||
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
|
||||
missing module named fcntl - imported by subprocess (optional), psutil._compat (delayed, optional), xmlrpc.server (optional), serial.serialposix (top-level)
|
||||
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
|
||||
missing module named termios - imported by tty (top-level), _pyrepl.pager (delayed, optional), psutil._compat (delayed, optional), getpass (optional), serial.serialposix (top-level)
|
||||
missing module named vms_lib - imported by platform (delayed, optional)
|
||||
missing module named 'java.lang' - imported by platform (delayed, optional)
|
||||
missing module named java - imported by platform (delayed)
|
||||
missing module named _scproxy - imported by urllib.request (conditional)
|
||||
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
|
||||
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
|
||||
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
@@ -39,11 +42,213 @@ missing module named System - imported by serial.serialcli (top-level)
|
||||
missing module named async_timeout - imported by aiohttp.helpers (conditional), aiohttp.web_ws (conditional), aiohttp.client_ws (conditional)
|
||||
missing module named brotlicffi - imported by aiohttp.compression_utils (optional)
|
||||
missing module named aiodns - imported by aiohttp.resolver (optional)
|
||||
missing module named vms_lib - imported by platform (delayed, optional)
|
||||
missing module named 'java.lang' - imported by platform (delayed, optional)
|
||||
missing module named java - imported by platform (delayed)
|
||||
missing module named annotationlib - imported by attr._compat (conditional)
|
||||
missing module named uvloop - imported by aiohttp.worker (delayed)
|
||||
missing module named 'gunicorn.workers' - imported by aiohttp.worker (top-level)
|
||||
missing module named gunicorn - imported by aiohttp.worker (top-level)
|
||||
missing module named _suggestions - imported by traceback (delayed, optional)
|
||||
missing module named dummy_threading - imported by psutil._compat (optional)
|
||||
missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), rlcompleter (optional), pdb (delayed, optional)
|
||||
missing module named numpy._core.result_type - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.number - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.max - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.array2string - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.signbit - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.isscalar - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy.lib._utils_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isnat - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.float32 - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.array_repr - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.arange - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.sign - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.nextafter - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.longlong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log10 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.exp2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named _dummy_thread - imported by numpy._core.arrayprint (optional)
|
||||
missing module named pyodide_js - imported by threadpoolctl (delayed, optional)
|
||||
missing module named numpy._core.vstack - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._function_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._array_utils_impl (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named railroad - imported by pyparsing.diagram (top-level)
|
||||
missing module named pyparsing.Word - imported by pyparsing (delayed), pyparsing.unicode (delayed)
|
||||
missing module named six.moves.range - imported by six.moves (top-level), dateutil.rrule (top-level)
|
||||
runtime module named six.moves - imported by dateutil.tz.tz (top-level), dateutil.tz._factories (top-level), dateutil.tz.win (top-level), dateutil.rrule (top-level)
|
||||
missing module named StringIO - imported by six (conditional)
|
||||
missing module named dateutil.tz.tzfile - imported by dateutil.tz (top-level), dateutil.zoneinfo (top-level)
|
||||
excluded module named pandas - imported by matplotlib.pyplot (conditional)
|
||||
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
|
||||
missing module named xmlrpclib - imported by defusedxml.xmlrpc (conditional)
|
||||
missing module named numpy.VisibleDeprecationWarning - imported by numpy (optional), matplotlib.cbook (optional)
|
||||
missing module named 'numpy_distutils.cpuinfo' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.fcompiler' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.command' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named numpy_distutils - imported by numpy.f2py.diagnose (delayed, optional)
|
||||
missing module named _typeshed - imported by numpy.random.bit_generator (top-level)
|
||||
missing module named numpy.random.RandomState - imported by numpy.random (top-level), numpy.random._generator (top-level)
|
||||
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
|
||||
missing module named _curses - imported by curses (top-level), curses.has_key (top-level)
|
||||
missing module named gi - imported by matplotlib.cbook (delayed, conditional)
|
||||
missing module named setuptools_scm - imported by matplotlib (delayed, conditional, optional)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+605
-5
@@ -19,13 +19,31 @@ import time
|
||||
|
||||
import config
|
||||
from controllers import IncrementalPID
|
||||
from flow_control import FlowControlFault, FlowControlLoop
|
||||
from flow_control import FlowControlFault, FlowControlLoop, opening_to_motor_position
|
||||
from open_loop import (
|
||||
MAX_CONSECUTIVE_READ_FAILURES,
|
||||
MIN_ABS_STD_SLM,
|
||||
OPENING_MAX_PCT,
|
||||
OPENING_MIN_PCT,
|
||||
OPENING_STEP_PCT,
|
||||
OUTPUT_DIRECTORY as OPEN_LOOP_OUTPUT_DIRECTORY,
|
||||
RANDOM_SEED,
|
||||
SAMPLE_PERIOD_S,
|
||||
OpenLoopRecorder,
|
||||
SteadyStateDetector,
|
||||
check_safety,
|
||||
generate_fine_strokes,
|
||||
generate_openings,
|
||||
motor_position_to_opening,
|
||||
read_sensors,
|
||||
)
|
||||
from valve_model import ValveModel
|
||||
|
||||
|
||||
APP_NAME = "FlowControl"
|
||||
BUILTIN_MODEL_NAME = "valve_model_004.json"
|
||||
MONITOR_PERIOD_S = 0.25
|
||||
MAX_GUI_OPEN_LOOP_POINTS = 2000
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -49,6 +67,40 @@ class ModelInfo:
|
||||
is_monotonic: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenLoopSettings:
|
||||
"""GUI 开环扫点参数。"""
|
||||
|
||||
min_opening_pct: float = OPENING_MIN_PCT
|
||||
max_opening_pct: float = OPENING_MAX_PCT
|
||||
opening_step_pct: float = OPENING_STEP_PCT
|
||||
random_seed: int | None = RANDOM_SEED
|
||||
sample_period_s: float = SAMPLE_PERIOD_S
|
||||
steady_window_s: float = config.STEADY_STATE_WINDOW_S
|
||||
steady_std_pct: float = config.STEADY_STATE_STD_PCT
|
||||
max_wait_s: float = config.STEADY_STATE_MAX_WAIT_S
|
||||
min_abs_std_slm: float = MIN_ABS_STD_SLM
|
||||
include_fine_scan: bool = True
|
||||
fine_stroke_start: float = config.OPEN_LOOP_FINE_STROKE_START
|
||||
fine_stroke_step: float = config.OPEN_LOOP_FINE_STROKE_STEP
|
||||
|
||||
|
||||
@dataclass
|
||||
class _OpenLoopSession:
|
||||
"""仅由后台线程访问的开环运行状态。"""
|
||||
|
||||
settings: OpenLoopSettings
|
||||
points: list
|
||||
recorder: OpenLoopRecorder
|
||||
detector: SteadyStateDetector
|
||||
run_start: float
|
||||
step_index: int
|
||||
step_start: float
|
||||
opening_pct: float
|
||||
motor_position: float
|
||||
read_failures: int = 0
|
||||
|
||||
|
||||
def application_directory() -> Path:
|
||||
"""返回日志等可写文件的程序目录。"""
|
||||
if getattr(sys, "frozen", False):
|
||||
@@ -129,6 +181,158 @@ def validate_target(target) -> float:
|
||||
return value
|
||||
|
||||
|
||||
def validate_opening(opening) -> float:
|
||||
"""解析并校验 GUI 手动开度。"""
|
||||
try:
|
||||
value = float(str(opening).strip())
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("手动开度必须是数字") from exc
|
||||
if not math.isfinite(value):
|
||||
raise ValueError("手动开度必须是有限数值")
|
||||
if not 0.0 <= value <= 100.0:
|
||||
raise ValueError("手动开度必须位于 0~100%")
|
||||
return value
|
||||
|
||||
|
||||
def _finite_float(value, label) -> float:
|
||||
try:
|
||||
number = float(str(value).strip())
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"{label}必须是数字") from exc
|
||||
if not math.isfinite(number):
|
||||
raise ValueError(f"{label}必须是有限数值")
|
||||
return number
|
||||
|
||||
|
||||
def validate_open_loop_settings(
|
||||
*,
|
||||
min_opening_pct=OPENING_MIN_PCT,
|
||||
max_opening_pct=OPENING_MAX_PCT,
|
||||
opening_step_pct=OPENING_STEP_PCT,
|
||||
random_seed=RANDOM_SEED,
|
||||
sample_period_s=SAMPLE_PERIOD_S,
|
||||
steady_window_s=config.STEADY_STATE_WINDOW_S,
|
||||
steady_std_pct=config.STEADY_STATE_STD_PCT,
|
||||
max_wait_s=config.STEADY_STATE_MAX_WAIT_S,
|
||||
min_abs_std_slm=MIN_ABS_STD_SLM,
|
||||
include_fine_scan=True,
|
||||
fine_stroke_start=config.OPEN_LOOP_FINE_STROKE_START,
|
||||
fine_stroke_step=config.OPEN_LOOP_FINE_STROKE_STEP,
|
||||
) -> OpenLoopSettings:
|
||||
"""解析弹窗参数并拒绝无法执行或规模异常的扫点配置。"""
|
||||
min_opening = _finite_float(min_opening_pct, "起始开度")
|
||||
max_opening = _finite_float(max_opening_pct, "结束开度")
|
||||
opening_step = _finite_float(opening_step_pct, "开度间隔")
|
||||
sample_period = _finite_float(sample_period_s, "采样周期")
|
||||
steady_window = _finite_float(steady_window_s, "稳态窗口")
|
||||
steady_std = _finite_float(steady_std_pct, "相对标准差阈值")
|
||||
max_wait = _finite_float(max_wait_s, "单点最长等待时间")
|
||||
min_abs_std = _finite_float(min_abs_std_slm, "绝对标准差下限")
|
||||
fine_start = _finite_float(fine_stroke_start, "精扫起始行程")
|
||||
fine_step = _finite_float(fine_stroke_step, "精扫行程间隔")
|
||||
|
||||
if not 0.0 <= min_opening < max_opening <= 100.0:
|
||||
raise ValueError("开度范围必须满足 0 ≤ 起始开度 < 结束开度 ≤ 100")
|
||||
if opening_step <= 0.0:
|
||||
raise ValueError("开度间隔必须大于 0")
|
||||
if sample_period <= 0.0:
|
||||
raise ValueError("采样周期必须大于 0")
|
||||
if steady_window <= sample_period:
|
||||
raise ValueError("稳态窗口必须大于采样周期")
|
||||
if steady_std < 0.0 or min_abs_std < 0.0:
|
||||
raise ValueError("稳态标准差阈值不得为负数")
|
||||
if max_wait < steady_window:
|
||||
raise ValueError("单点最长等待时间不得小于稳态窗口")
|
||||
|
||||
seed_text = "" if random_seed is None else str(random_seed).strip()
|
||||
if seed_text == "":
|
||||
seed = None
|
||||
else:
|
||||
try:
|
||||
seed = int(seed_text)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("随机种子必须是整数或留空") from exc
|
||||
|
||||
include_fine = bool(include_fine_scan)
|
||||
if include_fine:
|
||||
if not config.MOTOR_OPEN_POSITION <= fine_start < config.MOTOR_CLOSED_POSITION:
|
||||
raise ValueError(
|
||||
"精扫起始行程必须位于 "
|
||||
f"{config.MOTOR_OPEN_POSITION:g}~{config.MOTOR_CLOSED_POSITION:g}"
|
||||
)
|
||||
if fine_step <= 0.0:
|
||||
raise ValueError("精扫行程间隔必须大于 0")
|
||||
|
||||
settings = OpenLoopSettings(
|
||||
min_opening_pct=min_opening,
|
||||
max_opening_pct=max_opening,
|
||||
opening_step_pct=opening_step,
|
||||
random_seed=seed,
|
||||
sample_period_s=sample_period,
|
||||
steady_window_s=steady_window,
|
||||
steady_std_pct=steady_std,
|
||||
max_wait_s=max_wait,
|
||||
min_abs_std_slm=min_abs_std,
|
||||
include_fine_scan=include_fine,
|
||||
fine_stroke_start=fine_start,
|
||||
fine_stroke_step=fine_step,
|
||||
)
|
||||
point_count = len(build_open_loop_points(settings))
|
||||
if point_count > MAX_GUI_OPEN_LOOP_POINTS:
|
||||
raise ValueError(
|
||||
f"扫描点数 {point_count} 超过上限 {MAX_GUI_OPEN_LOOP_POINTS},"
|
||||
"请增大开度或精扫间隔"
|
||||
)
|
||||
return settings
|
||||
|
||||
|
||||
def build_open_loop_points(settings: OpenLoopSettings):
|
||||
"""按 GUI 参数生成随机开度点,并可选追加闭阀端精细行程点。"""
|
||||
openings = generate_openings(
|
||||
settings.min_opening_pct,
|
||||
settings.max_opening_pct,
|
||||
settings.opening_step_pct,
|
||||
settings.random_seed,
|
||||
)
|
||||
points = [
|
||||
(
|
||||
opening,
|
||||
opening_to_motor_position(
|
||||
opening,
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_CLOSED_POSITION,
|
||||
),
|
||||
)
|
||||
for opening in openings
|
||||
]
|
||||
if settings.include_fine_scan:
|
||||
fine_strokes = generate_fine_strokes(
|
||||
settings.fine_stroke_start,
|
||||
config.MOTOR_CLOSED_POSITION,
|
||||
settings.fine_stroke_step,
|
||||
)
|
||||
points.extend(
|
||||
(
|
||||
motor_position_to_opening(
|
||||
stroke,
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_CLOSED_POSITION,
|
||||
),
|
||||
stroke,
|
||||
)
|
||||
for stroke in fine_strokes
|
||||
)
|
||||
return points
|
||||
|
||||
|
||||
def create_open_loop_recorder():
|
||||
"""创建写到程序目录的开环 CSV/PNG 记录器。"""
|
||||
return OpenLoopRecorder(
|
||||
application_directory() / OPEN_LOOP_OUTPUT_DIRECTORY,
|
||||
plot_dpi=config.PLOT_DPI,
|
||||
)
|
||||
|
||||
|
||||
def load_model(model_path: str | Path | None = None) -> ModelInfo:
|
||||
"""加载内置或外部阀模型,并检查其是否匹配当前行程配置。"""
|
||||
is_builtin = model_path is None
|
||||
@@ -280,11 +484,13 @@ class ControlWorker:
|
||||
hardware_factory=create_hardware,
|
||||
model_loader=load_model,
|
||||
controller_factory=build_control_loop,
|
||||
open_loop_recorder_factory=create_open_loop_recorder,
|
||||
):
|
||||
self.logger = logger
|
||||
self.hardware_factory = hardware_factory
|
||||
self.model_loader = model_loader
|
||||
self.controller_factory = controller_factory
|
||||
self.open_loop_recorder_factory = open_loop_recorder_factory
|
||||
self.commands = Queue()
|
||||
self.events = Queue()
|
||||
self.thread = threading.Thread(
|
||||
@@ -296,6 +502,10 @@ class ControlWorker:
|
||||
self._state = "DISCONNECTED"
|
||||
self._hardware = None
|
||||
self._controller = None
|
||||
self._open_loop = None
|
||||
self._monitor_opening_pct = 100.0
|
||||
self._monitor_motor_position = config.MOTOR_OPEN_POSITION
|
||||
self._monitor_mode = "未启用"
|
||||
self._next_cycle = 0.0
|
||||
self._monitor_failures = {
|
||||
"FLOW": 0,
|
||||
@@ -319,9 +529,21 @@ class ControlWorker:
|
||||
def request_target(self, target):
|
||||
self.commands.put(("SET_TARGET", {"target": target}))
|
||||
|
||||
def request_stop_control(self):
|
||||
self.commands.put(("STOP_CONTROL", {}))
|
||||
|
||||
def request_manual_opening(self, opening):
|
||||
self.commands.put(("SET_MANUAL_OPENING", {"opening": opening}))
|
||||
|
||||
def request_disconnect(self):
|
||||
self.commands.put(("DISCONNECT", {}))
|
||||
|
||||
def request_open_loop_start(self, settings: OpenLoopSettings):
|
||||
self.commands.put(("START_OPEN_LOOP", {"settings": settings}))
|
||||
|
||||
def request_open_loop_stop(self):
|
||||
self.commands.put(("STOP_OPEN_LOOP", {}))
|
||||
|
||||
def request_shutdown(self):
|
||||
self.commands.put(("SHUTDOWN", {}))
|
||||
|
||||
@@ -356,6 +578,11 @@ class ControlWorker:
|
||||
elif self._state == "CONTROLLING" and now >= self._next_cycle:
|
||||
self._control_once(now)
|
||||
self._advance_deadline(now, config.CONTROL_PERIOD_S)
|
||||
elif self._state == "OPEN_LOOP" and now >= self._next_cycle:
|
||||
period = self._open_loop.settings.sample_period_s
|
||||
self._open_loop_once(now)
|
||||
if self._state == "OPEN_LOOP":
|
||||
self._advance_deadline(now, period)
|
||||
finally:
|
||||
errors = self._safe_disconnect()
|
||||
if errors:
|
||||
@@ -363,7 +590,7 @@ class ControlWorker:
|
||||
self._emit("shutdown_complete", errors=errors)
|
||||
|
||||
def _command_timeout(self):
|
||||
if self._state in {"MONITORING", "CONTROLLING"}:
|
||||
if self._state in {"MONITORING", "CONTROLLING", "OPEN_LOOP"}:
|
||||
return max(0.0, min(0.05, self._next_cycle - time.perf_counter()))
|
||||
return 0.1
|
||||
|
||||
@@ -377,8 +604,16 @@ class ControlWorker:
|
||||
self._connect(payload["settings"], payload.get("model_path"))
|
||||
elif command == "SET_TARGET":
|
||||
self._set_target(payload["target"])
|
||||
elif command == "STOP_CONTROL":
|
||||
self._stop_control()
|
||||
elif command == "SET_MANUAL_OPENING":
|
||||
self._set_manual_opening(payload["opening"])
|
||||
elif command == "DISCONNECT":
|
||||
self._disconnect()
|
||||
elif command == "START_OPEN_LOOP":
|
||||
self._start_open_loop(payload["settings"])
|
||||
elif command == "STOP_OPEN_LOOP":
|
||||
self._stop_open_loop()
|
||||
elif command == "SHUTDOWN":
|
||||
self._set_state("STOPPING", "正在安全退出…")
|
||||
self._shutdown_requested = True
|
||||
@@ -409,6 +644,9 @@ class ControlWorker:
|
||||
model_info.model,
|
||||
self.logger,
|
||||
)
|
||||
self._monitor_opening_pct = 100.0
|
||||
self._monitor_motor_position = config.MOTOR_OPEN_POSITION
|
||||
self._monitor_mode = "未启用"
|
||||
for sensor in self._monitor_failures:
|
||||
self._monitor_failures[sensor] = 0
|
||||
self._next_cycle = time.perf_counter()
|
||||
@@ -451,6 +689,88 @@ class ControlWorker:
|
||||
except Exception as exc:
|
||||
self._emit("warning", message=f"目标流量设置失败:{exc}")
|
||||
|
||||
def _stop_control(self):
|
||||
if self._state != "CONTROLLING" or self._controller is None:
|
||||
self._emit("warning", message="当前没有正在运行的闭环流量控制")
|
||||
return
|
||||
try:
|
||||
self._controller.stop()
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"CONTROL_STOP_OPEN_FAILED",
|
||||
"STOP_CONTROL",
|
||||
"停止流量控制后设置阀门 100% 开度失败",
|
||||
)
|
||||
self._monitor_opening_pct = 100.0
|
||||
self._monitor_motor_position = config.MOTOR_OPEN_POSITION
|
||||
self._monitor_mode = "未启用"
|
||||
self._next_cycle = time.perf_counter()
|
||||
self.logger.info("闭环流量控制已停止,阀门已设置为 100%% 开度")
|
||||
self._emit(
|
||||
"control_stopped",
|
||||
opening_pct=self._monitor_opening_pct,
|
||||
motor_position=self._monitor_motor_position,
|
||||
)
|
||||
self._set_state("MONITORING", "流量控制已停止,阀门为 100% 开度")
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _set_manual_opening(self, opening):
|
||||
if self._state == "CONTROLLING":
|
||||
self._emit("warning", message="请先停止流量控制,再设置手动开度")
|
||||
return
|
||||
if self._state == "OPEN_LOOP":
|
||||
self._emit("warning", message="请先停止开环扫点,再设置手动开度")
|
||||
return
|
||||
if self._state != "MONITORING" or self._hardware is None:
|
||||
self._emit("warning", message="请先连接设备,再设置手动开度")
|
||||
return
|
||||
try:
|
||||
value = validate_opening(opening)
|
||||
position = opening_to_motor_position(
|
||||
value,
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_CLOSED_POSITION,
|
||||
)
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
position,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"MANUAL_OPENING_WRITE_FAILED",
|
||||
"SET_MANUAL_OPENING",
|
||||
f"手动开度 {value:.3f}% 写入失败",
|
||||
)
|
||||
self._monitor_opening_pct = value
|
||||
self._monitor_motor_position = position
|
||||
self._monitor_mode = "未启用"
|
||||
self._next_cycle = time.perf_counter()
|
||||
self.logger.info(
|
||||
"手动开度已设置:%.3f%%,电机行程 %.3f",
|
||||
value,
|
||||
position,
|
||||
)
|
||||
self._emit(
|
||||
"manual_opening_applied",
|
||||
opening_pct=value,
|
||||
motor_position=position,
|
||||
)
|
||||
self._set_state(
|
||||
"MONITORING",
|
||||
f"手动开度 {value:g}% 已生效,设备继续监测",
|
||||
)
|
||||
except ValueError as exc:
|
||||
self._emit("warning", message=str(exc))
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _disconnect(self):
|
||||
if self._state == "DISCONNECTED":
|
||||
return
|
||||
@@ -465,6 +785,275 @@ class ControlWorker:
|
||||
self.logger.info("设备已安全断开")
|
||||
self._set_state("DISCONNECTED", "设备已断开")
|
||||
|
||||
def _start_open_loop(self, settings):
|
||||
if self._state not in {"MONITORING", "CONTROLLING"}:
|
||||
self._emit("warning", message="请先连接设备,再启动开环扫点")
|
||||
return
|
||||
if not isinstance(settings, OpenLoopSettings):
|
||||
self._emit("warning", message="开环扫点参数类型无效")
|
||||
return
|
||||
|
||||
try:
|
||||
points = build_open_loop_points(settings)
|
||||
if not points:
|
||||
raise ValueError("没有生成任何扫描点")
|
||||
if self._controller is not None and self._controller.running:
|
||||
self._controller.stop()
|
||||
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_INITIAL_OPEN_FAILED",
|
||||
"OPEN_LOOP_START",
|
||||
"开环扫点前设置阀门 100% 开度失败",
|
||||
)
|
||||
|
||||
recorder = self.open_loop_recorder_factory()
|
||||
detector = SteadyStateDetector(
|
||||
settings.steady_window_s,
|
||||
settings.steady_std_pct,
|
||||
sample_period_s=settings.sample_period_s,
|
||||
min_abs_std_slm=settings.min_abs_std_slm,
|
||||
)
|
||||
now = time.perf_counter()
|
||||
opening, position = points[0]
|
||||
self._open_loop = _OpenLoopSession(
|
||||
settings=settings,
|
||||
points=points,
|
||||
recorder=recorder,
|
||||
detector=detector,
|
||||
run_start=now,
|
||||
step_index=0,
|
||||
step_start=now,
|
||||
opening_pct=opening,
|
||||
motor_position=position,
|
||||
)
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
position,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_MOTOR_COMMAND_FAILED",
|
||||
"OPEN_LOOP_START",
|
||||
f"写入首个扫描点失败:开度 {opening:.3f}% / 行程 {position:.3f}",
|
||||
)
|
||||
|
||||
self._next_cycle = now
|
||||
self.logger.info(
|
||||
"开环扫点启动:共 %d 点,开度 %.3f%%~%.3f%%,间隔 %.3f%%,"
|
||||
"采样周期 %.3f s,单点最长 %.3f s,CSV=%s",
|
||||
len(points),
|
||||
settings.min_opening_pct,
|
||||
settings.max_opening_pct,
|
||||
settings.opening_step_pct,
|
||||
settings.sample_period_s,
|
||||
settings.max_wait_s,
|
||||
recorder.csv_path,
|
||||
)
|
||||
self._set_state(
|
||||
"OPEN_LOOP",
|
||||
f"开环扫点 1/{len(points)}:开度 {opening:.2f}% / 行程 {position:.1f}",
|
||||
)
|
||||
self._emit(
|
||||
"open_loop_started",
|
||||
total_steps=len(points),
|
||||
csv_path=str(recorder.csv_path),
|
||||
)
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _stop_open_loop(self):
|
||||
if self._state != "OPEN_LOOP" or self._open_loop is None:
|
||||
self._emit("warning", message="当前没有正在运行的开环扫点")
|
||||
return
|
||||
try:
|
||||
self._finish_open_loop(completed=False, reason="用户停止")
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _open_loop_once(self, now):
|
||||
session = self._open_loop
|
||||
if session is None:
|
||||
self._handle_runtime_fault(RuntimeError("开环扫点状态丢失"))
|
||||
return
|
||||
try:
|
||||
pressure_before, flow_before, pressure_after, flow_after = read_sensors(
|
||||
self._hardware
|
||||
)
|
||||
elapsed_s = now - session.run_start
|
||||
session.recorder.record(
|
||||
time_s=elapsed_s,
|
||||
step_index=session.step_index,
|
||||
opening_pct=session.opening_pct,
|
||||
motor_position=session.motor_position,
|
||||
flow_before_slm=flow_before,
|
||||
flow_after_slm=flow_after,
|
||||
pressure_before_kpa=pressure_before,
|
||||
pressure_after_kpa=pressure_after,
|
||||
)
|
||||
|
||||
fault = check_safety(
|
||||
pressure_before,
|
||||
flow_before,
|
||||
pressure_after,
|
||||
flow_after,
|
||||
)
|
||||
if fault is not None:
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_SAFETY_LIMIT",
|
||||
"OPEN_LOOP_SAMPLE",
|
||||
f"开环扫点安全阈值触发:{fault}",
|
||||
)
|
||||
|
||||
if flow_after is None:
|
||||
session.read_failures += 1
|
||||
else:
|
||||
session.read_failures = 0
|
||||
if session.read_failures >= MAX_CONSECUTIVE_READ_FAILURES:
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_FLOW_READ_FAILED",
|
||||
"OPEN_LOOP_SAMPLE",
|
||||
f"阀后流量连续读取失败 {session.read_failures} 次",
|
||||
)
|
||||
|
||||
step_elapsed = now - session.step_start
|
||||
stable = session.detector.observe(elapsed_s, flow_after)
|
||||
timed_out = step_elapsed >= session.settings.max_wait_s
|
||||
total_steps = len(session.points)
|
||||
self._emit(
|
||||
"telemetry",
|
||||
measured_flow_slm=flow_after,
|
||||
pressure_before_kpa=pressure_before,
|
||||
pressure_kpa=pressure_after,
|
||||
target_flow_slm=None,
|
||||
opening_pct=session.opening_pct,
|
||||
motor_position=session.motor_position,
|
||||
feedforward_pct=None,
|
||||
correction_pct=None,
|
||||
pid_mode="开环",
|
||||
status="OPEN_LOOP",
|
||||
)
|
||||
self._emit(
|
||||
"open_loop_progress",
|
||||
current_step=session.step_index + 1,
|
||||
total_steps=total_steps,
|
||||
opening_pct=session.opening_pct,
|
||||
motor_position=session.motor_position,
|
||||
step_elapsed_s=step_elapsed,
|
||||
)
|
||||
|
||||
if stable or timed_out:
|
||||
completion = "流量已稳定" if stable else "达到单点最长等待时间"
|
||||
self.logger.info(
|
||||
"开环步 %d/%d 完成:开度 %.3f%% / 行程 %.3f,%s,耗时 %.3f s",
|
||||
session.step_index + 1,
|
||||
total_steps,
|
||||
session.opening_pct,
|
||||
session.motor_position,
|
||||
completion,
|
||||
step_elapsed,
|
||||
)
|
||||
self._advance_open_loop_step(now)
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _advance_open_loop_step(self, now):
|
||||
session = self._open_loop
|
||||
next_index = session.step_index + 1
|
||||
if next_index >= len(session.points):
|
||||
self._finish_open_loop(completed=True, reason="扫描完成")
|
||||
return
|
||||
|
||||
opening, position = session.points[next_index]
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
position,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_MOTOR_COMMAND_FAILED",
|
||||
"OPEN_LOOP_STEP_CHANGE",
|
||||
f"写入扫描点失败:开度 {opening:.3f}% / 行程 {position:.3f}",
|
||||
)
|
||||
session.step_index = next_index
|
||||
session.step_start = now
|
||||
session.opening_pct = opening
|
||||
session.motor_position = position
|
||||
session.detector.reset()
|
||||
self._set_state(
|
||||
"OPEN_LOOP",
|
||||
f"开环扫点 {next_index + 1}/{len(session.points)}:"
|
||||
f"开度 {opening:.2f}% / 行程 {position:.1f}",
|
||||
)
|
||||
|
||||
def _finish_open_loop(self, *, completed, reason):
|
||||
if self._open_loop is None:
|
||||
return
|
||||
self._set_state("FINALIZING", "正在恢复 100% 开度并生成开环数据图…")
|
||||
if not bool(
|
||||
self._hardware.set_motor_position(
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"OPEN_LOOP_FINAL_OPEN_FAILED",
|
||||
"OPEN_LOOP_FINALIZE",
|
||||
"开环扫点结束后设置阀门 100% 开度失败",
|
||||
)
|
||||
self._monitor_opening_pct = 100.0
|
||||
self._monitor_motor_position = config.MOTOR_OPEN_POSITION
|
||||
self._monitor_mode = "未启用"
|
||||
errors, payload = self._finalize_open_loop_data(completed, reason)
|
||||
if payload is not None:
|
||||
self._emit("open_loop_finished", **payload)
|
||||
if errors:
|
||||
self._emit("warning", message=";".join(errors))
|
||||
self._next_cycle = time.perf_counter()
|
||||
self._set_state("MONITORING", f"开环扫点已结束({reason}),设备保持连接")
|
||||
|
||||
def _finalize_open_loop_data(self, completed, reason):
|
||||
session = self._open_loop
|
||||
if session is None:
|
||||
return [], None
|
||||
self._open_loop = None
|
||||
recorder = session.recorder
|
||||
errors = []
|
||||
image_path = None
|
||||
try:
|
||||
image = recorder.finalize()
|
||||
if image is not None:
|
||||
image_path = str(image)
|
||||
except Exception as exc:
|
||||
try:
|
||||
recorder.close()
|
||||
except Exception:
|
||||
pass
|
||||
errors.append(f"生成开环曲线图失败,CSV 已保留:{exc}")
|
||||
self.logger.exception("生成开环曲线图失败")
|
||||
payload = {
|
||||
"completed": bool(completed),
|
||||
"reason": str(reason),
|
||||
"csv_path": str(recorder.csv_path),
|
||||
"image_path": image_path,
|
||||
"sample_count": int(getattr(recorder, "sample_count", 0)),
|
||||
}
|
||||
self.logger.info(
|
||||
"开环扫点结束:%s,采样 %d 条,CSV=%s,PNG=%s",
|
||||
reason,
|
||||
payload["sample_count"],
|
||||
payload["csv_path"],
|
||||
image_path or "未生成",
|
||||
)
|
||||
return errors, payload
|
||||
|
||||
def _monitor_once(self):
|
||||
try:
|
||||
flow = self._read_monitor_value(
|
||||
@@ -515,11 +1104,11 @@ class ControlWorker:
|
||||
pressure_before_kpa=pressure_before,
|
||||
pressure_kpa=pressure_after,
|
||||
target_flow_slm=None,
|
||||
opening_pct=100.0,
|
||||
motor_position=config.MOTOR_OPEN_POSITION,
|
||||
opening_pct=self._monitor_opening_pct,
|
||||
motor_position=self._monitor_motor_position,
|
||||
feedforward_pct=None,
|
||||
correction_pct=None,
|
||||
pid_mode="--",
|
||||
pid_mode=self._monitor_mode,
|
||||
status=status,
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -588,6 +1177,9 @@ class ControlWorker:
|
||||
hardware = self._hardware
|
||||
self._controller = None
|
||||
self._hardware = None
|
||||
self._monitor_opening_pct = 100.0
|
||||
self._monitor_motor_position = config.MOTOR_OPEN_POSITION
|
||||
self._monitor_mode = "未启用"
|
||||
|
||||
if controller is not None:
|
||||
try:
|
||||
@@ -610,4 +1202,12 @@ class ControlWorker:
|
||||
hardware.disconnect()
|
||||
except Exception as exc:
|
||||
errors.append(f"断开 PLC 失败:{exc}")
|
||||
|
||||
finalize_errors, payload = self._finalize_open_loop_data(
|
||||
completed=False,
|
||||
reason="设备断开或程序退出",
|
||||
)
|
||||
errors.extend(finalize_errors)
|
||||
if payload is not None:
|
||||
self._emit("open_loop_finished", **payload)
|
||||
return errors
|
||||
|
||||
@@ -12,9 +12,16 @@ a = Analysis(
|
||||
datas=[("valve_model/valve_model_004.json", ".")],
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
hooksconfig={"matplotlib": {"backends": "Agg"}},
|
||||
runtime_hooks=[],
|
||||
excludes=["matplotlib", "numpy", "pytest"],
|
||||
excludes=[
|
||||
"pytest",
|
||||
"pandas",
|
||||
"scipy",
|
||||
"IPython",
|
||||
"PyQt5",
|
||||
"PySide6",
|
||||
],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
|
||||
+556
-16
@@ -5,8 +5,11 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import ctypes
|
||||
from datetime import datetime
|
||||
from queue import Empty
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
@@ -14,16 +17,202 @@ import config
|
||||
from control_runtime import (
|
||||
BUILTIN_MODEL_NAME,
|
||||
ControlWorker,
|
||||
OpenLoopSettings,
|
||||
application_directory,
|
||||
build_open_loop_points,
|
||||
build_runtime_logger,
|
||||
load_model,
|
||||
validate_device_settings,
|
||||
validate_opening,
|
||||
validate_open_loop_settings,
|
||||
validate_target,
|
||||
)
|
||||
from identify_valve import DEFAULT_TAIL, create_diagnostic_plot, identify
|
||||
|
||||
|
||||
WINDOW_TITLE = "气体流量控制"
|
||||
|
||||
|
||||
class OpenLoopDialog:
|
||||
"""开环扫点参数弹窗。"""
|
||||
|
||||
def __init__(self, parent, on_start):
|
||||
self.parent = parent
|
||||
self.on_start = on_start
|
||||
defaults = OpenLoopSettings()
|
||||
|
||||
self.window = tk.Toplevel(parent)
|
||||
self.window.title("开环扫点参数")
|
||||
self.window.transient(parent)
|
||||
self.window.resizable(False, False)
|
||||
self.window.protocol("WM_DELETE_WINDOW", self._cancel)
|
||||
|
||||
self.values = {
|
||||
"min_opening_pct": tk.StringVar(value=f"{defaults.min_opening_pct:g}"),
|
||||
"max_opening_pct": tk.StringVar(value=f"{defaults.max_opening_pct:g}"),
|
||||
"opening_step_pct": tk.StringVar(value=f"{defaults.opening_step_pct:g}"),
|
||||
"random_seed": tk.StringVar(value=str(defaults.random_seed or "")),
|
||||
"sample_period_s": tk.StringVar(value=f"{defaults.sample_period_s:g}"),
|
||||
"steady_window_s": tk.StringVar(value=f"{defaults.steady_window_s:g}"),
|
||||
"steady_std_pct": tk.StringVar(value=f"{defaults.steady_std_pct:g}"),
|
||||
"max_wait_s": tk.StringVar(value=f"{defaults.max_wait_s:g}"),
|
||||
"min_abs_std_slm": tk.StringVar(value=f"{defaults.min_abs_std_slm:g}"),
|
||||
"fine_stroke_start": tk.StringVar(value=f"{defaults.fine_stroke_start:g}"),
|
||||
"fine_stroke_step": tk.StringVar(value=f"{defaults.fine_stroke_step:g}"),
|
||||
}
|
||||
self.include_fine_scan = tk.BooleanVar(value=defaults.include_fine_scan)
|
||||
self.summary_var = tk.StringVar()
|
||||
self.fine_entries = []
|
||||
|
||||
self._build_ui()
|
||||
for variable in self.values.values():
|
||||
variable.trace_add("write", self._refresh_summary)
|
||||
self.include_fine_scan.trace_add("write", self._fine_scan_changed)
|
||||
self._fine_scan_changed()
|
||||
|
||||
self.window.update_idletasks()
|
||||
x = parent.winfo_rootx() + (parent.winfo_width() - self.window.winfo_width()) // 2
|
||||
y = parent.winfo_rooty() + (parent.winfo_height() - self.window.winfo_height()) // 2
|
||||
self.window.geometry(f"+{max(0, x)}+{max(0, y)}")
|
||||
self.window.grab_set()
|
||||
self.window.focus_set()
|
||||
|
||||
def _build_ui(self):
|
||||
outer = ttk.Frame(self.window, padding=14)
|
||||
outer.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(
|
||||
outer,
|
||||
text="开环扫点会停止闭环,并直接按扫描序列下发阀门开度。",
|
||||
foreground="#8a4b00",
|
||||
wraplength=560,
|
||||
).pack(anchor="w", pady=(0, 9))
|
||||
|
||||
basic = ttk.LabelFrame(outer, text="扫描范围", padding=9)
|
||||
basic.pack(fill="x")
|
||||
self._add_field(basic, 0, 0, "起始开度", "min_opening_pct", "%")
|
||||
self._add_field(basic, 0, 3, "结束开度", "max_opening_pct", "%")
|
||||
self._add_field(basic, 1, 0, "开度间隔", "opening_step_pct", "%")
|
||||
self._add_field(basic, 1, 3, "随机种子", "random_seed", "(留空=随机)")
|
||||
|
||||
steady = ttk.LabelFrame(outer, text="采样与稳态判定", padding=9)
|
||||
steady.pack(fill="x", pady=(9, 0))
|
||||
self._add_field(steady, 0, 0, "采样周期", "sample_period_s", "s")
|
||||
self._add_field(steady, 0, 3, "稳态窗口", "steady_window_s", "s")
|
||||
self._add_field(steady, 1, 0, "相对标准差", "steady_std_pct", "%")
|
||||
self._add_field(steady, 1, 3, "绝对标准差下限", "min_abs_std_slm", "SLM")
|
||||
self._add_field(steady, 2, 0, "单点最长等待", "max_wait_s", "s")
|
||||
|
||||
fine = ttk.LabelFrame(outer, text="闭阀端精细扫描", padding=9)
|
||||
fine.pack(fill="x", pady=(9, 0))
|
||||
ttk.Checkbutton(
|
||||
fine,
|
||||
text="追加精细行程点",
|
||||
variable=self.include_fine_scan,
|
||||
).grid(row=0, column=0, columnspan=3, sticky="w")
|
||||
start_entry = self._add_field(
|
||||
fine, 1, 0, "起始行程", "fine_stroke_start", "(终点 1000)"
|
||||
)
|
||||
step_entry = self._add_field(
|
||||
fine, 1, 3, "行程间隔", "fine_stroke_step", ""
|
||||
)
|
||||
self.fine_entries.extend((start_entry, step_entry))
|
||||
|
||||
summary = tk.Label(
|
||||
outer,
|
||||
textvariable=self.summary_var,
|
||||
bg="#edf5ff",
|
||||
fg="#174f85",
|
||||
anchor="w",
|
||||
justify="left",
|
||||
padx=9,
|
||||
pady=7,
|
||||
)
|
||||
summary.pack(fill="x", pady=(9, 0))
|
||||
output_dir = application_directory() / "open_loop_data"
|
||||
ttk.Label(
|
||||
outer,
|
||||
text=f"输出目录:{output_dir}(每次生成 CSV,存在采样时同时生成 PNG)",
|
||||
foreground="#5f6368",
|
||||
wraplength=560,
|
||||
).pack(anchor="w", pady=(7, 0))
|
||||
|
||||
buttons = ttk.Frame(outer)
|
||||
buttons.pack(fill="x", pady=(12, 0))
|
||||
ttk.Button(buttons, text="取消", command=self._cancel, width=12).pack(
|
||||
side="right"
|
||||
)
|
||||
ttk.Button(
|
||||
buttons,
|
||||
text="开始扫点",
|
||||
command=self._start,
|
||||
style="Primary.TButton",
|
||||
width=14,
|
||||
).pack(side="right", padx=(0, 8))
|
||||
|
||||
def _add_field(self, parent, row, column, label, key, unit):
|
||||
ttk.Label(parent, text=label).grid(
|
||||
row=row, column=column, sticky="w", pady=(5 if row else 0, 0)
|
||||
)
|
||||
entry = ttk.Entry(parent, textvariable=self.values[key], width=10)
|
||||
entry.grid(
|
||||
row=row,
|
||||
column=column + 1,
|
||||
sticky="w",
|
||||
padx=(6, 4),
|
||||
pady=(5 if row else 0, 0),
|
||||
)
|
||||
ttk.Label(parent, text=unit).grid(
|
||||
row=row,
|
||||
column=column + 2,
|
||||
sticky="w",
|
||||
padx=(0, 16),
|
||||
pady=(5 if row else 0, 0),
|
||||
)
|
||||
return entry
|
||||
|
||||
def _settings(self):
|
||||
return validate_open_loop_settings(
|
||||
**{key: variable.get() for key, variable in self.values.items()},
|
||||
include_fine_scan=self.include_fine_scan.get(),
|
||||
)
|
||||
|
||||
def _refresh_summary(self, *_args):
|
||||
try:
|
||||
settings = self._settings()
|
||||
point_count = len(build_open_loop_points(settings))
|
||||
maximum_minutes = point_count * settings.max_wait_s / 60.0
|
||||
self.summary_var.set(
|
||||
f"共 {point_count} 个扫描点;按单点最长等待估算,上限约 "
|
||||
f"{maximum_minutes:.1f} 分钟(稳定后会提前切换)。"
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.summary_var.set(f"参数待修正:{exc}")
|
||||
|
||||
def _fine_scan_changed(self, *_args):
|
||||
state = "normal" if self.include_fine_scan.get() else "disabled"
|
||||
for entry in self.fine_entries:
|
||||
entry.configure(state=state)
|
||||
self._refresh_summary()
|
||||
|
||||
def _start(self):
|
||||
try:
|
||||
settings = self._settings()
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("扫点参数无效", str(exc), parent=self.window)
|
||||
return
|
||||
if self.on_start(settings):
|
||||
self.window.grab_release()
|
||||
self.window.destroy()
|
||||
|
||||
def _cancel(self):
|
||||
try:
|
||||
self.window.grab_release()
|
||||
except tk.TclError:
|
||||
pass
|
||||
self.window.destroy()
|
||||
|
||||
|
||||
class FlowControlApp:
|
||||
"""单窗口操作界面。"""
|
||||
|
||||
@@ -32,6 +221,8 @@ class FlowControlApp:
|
||||
"CONNECTING": "正在连接",
|
||||
"MONITORING": "已连接 / 监测中",
|
||||
"CONTROLLING": "闭环运行中",
|
||||
"OPEN_LOOP": "开环扫点中",
|
||||
"FINALIZING": "正在生成结果",
|
||||
"DISCONNECTING": "正在断开",
|
||||
"STOPPING": "正在退出",
|
||||
"FAULT": "故障 / 已断开",
|
||||
@@ -42,6 +233,8 @@ class FlowControlApp:
|
||||
"CONNECTING": ("#875a00", "#fff3cd"),
|
||||
"MONITORING": ("#0b6b3a", "#dff4e8"),
|
||||
"CONTROLLING": ("#075cad", "#deefff"),
|
||||
"OPEN_LOOP": ("#7a3e00", "#ffe9cc"),
|
||||
"FINALIZING": ("#875a00", "#fff3cd"),
|
||||
"DISCONNECTING": ("#875a00", "#fff3cd"),
|
||||
"STOPPING": ("#875a00", "#fff3cd"),
|
||||
"FAULT": ("#a61b1b", "#fde5e5"),
|
||||
@@ -50,8 +243,8 @@ class FlowControlApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title(WINDOW_TITLE)
|
||||
self.root.geometry("860x720")
|
||||
self.root.minsize(780, 640)
|
||||
self.root.geometry("860x760")
|
||||
self.root.minsize(780, 700)
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
self.logger = build_runtime_logger()
|
||||
@@ -60,12 +253,15 @@ class FlowControlApp:
|
||||
self.closing = False
|
||||
self.selected_model_path = None
|
||||
self.model_info = load_model()
|
||||
self.fit_events = Queue()
|
||||
self.fit_in_progress = False
|
||||
|
||||
self.host_var = tk.StringVar(value=config.PLC_HOST)
|
||||
self.port_var = tk.StringVar(value=str(config.PLC_PORT))
|
||||
self.slave_var = tk.StringVar(value=str(config.PLC_SLAVE_ID))
|
||||
self.model_var = tk.StringVar(value=self.model_info.display_name)
|
||||
self.target_var = tk.StringVar(value="")
|
||||
self.manual_opening_var = tk.StringVar(value="100")
|
||||
self.active_target_var = tk.StringVar(value="--")
|
||||
self.status_var = tk.StringVar(value="未连接")
|
||||
self.detail_var = tk.StringVar(value="请选择模型并连接设备")
|
||||
@@ -156,8 +352,12 @@ class FlowControlApp:
|
||||
foreground="#174f85",
|
||||
)
|
||||
self.model_label.grid(
|
||||
row=1, column=1, columnspan=3, sticky="w", padx=(7, 10), pady=(10, 0)
|
||||
row=1, column=1, columnspan=2, sticky="w", padx=(7, 10), pady=(10, 0)
|
||||
)
|
||||
self.open_loop_button = ttk.Button(
|
||||
connection, text="开环扫点", command=self._open_open_loop_dialog
|
||||
)
|
||||
self.open_loop_button.grid(row=1, column=3, pady=(10, 0), padx=(0, 7))
|
||||
self.model_browse_button = ttk.Button(
|
||||
connection, text="选择 JSON…", command=self._choose_model
|
||||
)
|
||||
@@ -185,6 +385,13 @@ class FlowControlApp:
|
||||
width=16,
|
||||
)
|
||||
self.disconnect_button.pack(side="left", padx=(10, 0))
|
||||
self.fit_model_button = ttk.Button(
|
||||
button_row,
|
||||
text="拟合前馈表",
|
||||
command=self._fit_feedforward_model,
|
||||
width=16,
|
||||
)
|
||||
self.fit_model_button.pack(side="right")
|
||||
|
||||
target_frame = ttk.LabelFrame(
|
||||
outer,
|
||||
@@ -195,25 +402,34 @@ class FlowControlApp:
|
||||
target_frame.pack(fill="x", pady=(10, 0))
|
||||
target_frame.columnconfigure(1, weight=1)
|
||||
ttk.Label(target_frame, text="目标值").grid(row=0, column=0, sticky="w")
|
||||
target_input = ttk.Frame(target_frame)
|
||||
target_input.grid(row=0, column=1, sticky="w", padx=(8, 5))
|
||||
self.target_entry = ttk.Entry(
|
||||
target_frame,
|
||||
target_input,
|
||||
textvariable=self.target_var,
|
||||
width=18,
|
||||
width=9,
|
||||
font=("Microsoft YaHei UI", 12),
|
||||
)
|
||||
self.target_entry.grid(row=0, column=1, sticky="w", padx=(8, 5))
|
||||
self.target_entry.pack(side="left")
|
||||
self.target_entry.bind("<Return>", lambda _event: self._set_target())
|
||||
ttk.Label(target_frame, text="SLM").grid(row=0, column=2, sticky="w")
|
||||
ttk.Label(target_input, text="SLM").pack(side="left", padx=(5, 0))
|
||||
self.target_button = ttk.Button(
|
||||
target_frame,
|
||||
text="设置目标流量",
|
||||
command=self._set_target,
|
||||
style="Primary.TButton",
|
||||
width=17,
|
||||
width=15,
|
||||
)
|
||||
self.target_button.grid(row=0, column=3, padx=(18, 0))
|
||||
self.target_button.grid(row=0, column=2, padx=(12, 0))
|
||||
self.stop_control_button = ttk.Button(
|
||||
target_frame,
|
||||
text="停止流量控制",
|
||||
command=self._stop_control,
|
||||
width=15,
|
||||
)
|
||||
self.stop_control_button.grid(row=0, column=3, padx=(8, 0))
|
||||
ttk.Label(target_frame, text="当前目标:").grid(
|
||||
row=0, column=4, padx=(24, 0)
|
||||
row=0, column=4, padx=(18, 0)
|
||||
)
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
@@ -221,6 +437,39 @@ class FlowControlApp:
|
||||
font=("Microsoft YaHei UI", 11, "bold"),
|
||||
foreground="#075cad",
|
||||
).grid(row=0, column=5, sticky="w")
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
text="手动开度",
|
||||
).grid(row=1, column=0, sticky="w", pady=(9, 0))
|
||||
manual_opening_input = ttk.Frame(target_frame)
|
||||
manual_opening_input.grid(
|
||||
row=1, column=1, sticky="w", padx=(8, 5), pady=(9, 0)
|
||||
)
|
||||
self.manual_opening_entry = ttk.Entry(
|
||||
manual_opening_input,
|
||||
textvariable=self.manual_opening_var,
|
||||
width=9,
|
||||
font=("Microsoft YaHei UI", 12),
|
||||
)
|
||||
self.manual_opening_entry.pack(side="left")
|
||||
self.manual_opening_entry.bind(
|
||||
"<Return>", lambda _event: self._set_manual_opening()
|
||||
)
|
||||
ttk.Label(manual_opening_input, text="%").pack(side="left", padx=(5, 0))
|
||||
self.manual_opening_button = ttk.Button(
|
||||
target_frame,
|
||||
text="设置开度",
|
||||
command=self._set_manual_opening,
|
||||
width=15,
|
||||
)
|
||||
self.manual_opening_button.grid(
|
||||
row=1, column=2, padx=(12, 0), pady=(9, 0)
|
||||
)
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
text="仅在闭环和扫点均停止后生效",
|
||||
foreground="#5f6368",
|
||||
).grid(row=1, column=4, columnspan=2, sticky="w", padx=(24, 0), pady=(9, 0))
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
text=(
|
||||
@@ -228,7 +477,7 @@ class FlowControlApp:
|
||||
f"{config.TARGET_FLOW_MAX_SLM:g} SLM;首次设置即启动闭环"
|
||||
),
|
||||
foreground="#5f6368",
|
||||
).grid(row=1, column=0, columnspan=6, sticky="w", pady=(7, 0))
|
||||
).grid(row=2, column=0, columnspan=6, sticky="w", pady=(7, 0))
|
||||
|
||||
telemetry = ttk.LabelFrame(
|
||||
outer,
|
||||
@@ -354,6 +603,161 @@ class FlowControlApp:
|
||||
self.detail_var.set(f"正在设置目标流量 {value:g} SLM…")
|
||||
self.worker.request_target(value)
|
||||
|
||||
def _stop_control(self):
|
||||
if self.current_state != "CONTROLLING":
|
||||
return
|
||||
self.detail_var.set("正在停止流量控制并下发 100% 开度…")
|
||||
self._append_log("请求停止流量控制并下发 100% 开度")
|
||||
self.stop_control_button.configure(state="disabled")
|
||||
self.worker.request_stop_control()
|
||||
|
||||
def _set_manual_opening(self):
|
||||
if self.current_state == "CONTROLLING":
|
||||
messagebox.showwarning(
|
||||
"请先停止流量控制",
|
||||
"闭环流量控制正在运行。请先点击“停止流量控制”,再设置手动开度。",
|
||||
parent=self.root,
|
||||
)
|
||||
return
|
||||
if self.current_state == "OPEN_LOOP":
|
||||
messagebox.showwarning(
|
||||
"请先停止开环扫点",
|
||||
"开环扫点正在运行。请先点击“停止扫点”,再设置手动开度。",
|
||||
parent=self.root,
|
||||
)
|
||||
return
|
||||
if self.current_state != "MONITORING":
|
||||
messagebox.showwarning(
|
||||
"无法设置开度",
|
||||
"请先连接设备,再设置手动开度。",
|
||||
parent=self.root,
|
||||
)
|
||||
return
|
||||
try:
|
||||
opening = validate_opening(self.manual_opening_var.get())
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("手动开度无效", str(exc), parent=self.root)
|
||||
return
|
||||
self.detail_var.set(f"正在下发手动开度 {opening:g}%…")
|
||||
self.worker.request_manual_opening(opening)
|
||||
|
||||
def _open_open_loop_dialog(self):
|
||||
if self.current_state == "OPEN_LOOP":
|
||||
self.detail_var.set("正在停止扫点并生成结果…")
|
||||
self._append_log("请求提前停止开环扫点")
|
||||
self.open_loop_button.configure(state="disabled")
|
||||
self.worker.request_open_loop_stop()
|
||||
return
|
||||
if self.current_state not in {"MONITORING", "CONTROLLING"}:
|
||||
messagebox.showwarning(
|
||||
"无法开始扫点",
|
||||
"请先连接设备并确认传感器数据正常。",
|
||||
parent=self.root,
|
||||
)
|
||||
return
|
||||
OpenLoopDialog(self.root, self._start_open_loop)
|
||||
|
||||
def _start_open_loop(self, settings):
|
||||
if self.current_state not in {"MONITORING", "CONTROLLING"}:
|
||||
messagebox.showwarning(
|
||||
"无法开始扫点", "设备当前未处于可扫点状态。", parent=self.root
|
||||
)
|
||||
return False
|
||||
if self.current_state == "CONTROLLING" and not messagebox.askyesno(
|
||||
"切换到开环扫点",
|
||||
"开始扫点将停止当前闭环控制,并按扫描序列直接下发阀门开度。是否继续?",
|
||||
parent=self.root,
|
||||
):
|
||||
return False
|
||||
point_count = len(build_open_loop_points(settings))
|
||||
self.active_target_var.set("--")
|
||||
self.detail_var.set(f"正在启动开环扫点,共 {point_count} 点…")
|
||||
self._append_log(f"请求启动开环扫点,共 {point_count} 个扫描点")
|
||||
self.worker.request_open_loop_start(settings)
|
||||
return True
|
||||
|
||||
def _fit_feedforward_model(self):
|
||||
if self.fit_in_progress:
|
||||
return
|
||||
initial_directory = application_directory() / "open_loop_data"
|
||||
if not initial_directory.is_dir():
|
||||
initial_directory = application_directory()
|
||||
csv_paths = filedialog.askopenfilenames(
|
||||
parent=self.root,
|
||||
title="选择一份或多份开环扫点 CSV",
|
||||
initialdir=str(initial_directory),
|
||||
filetypes=(("开环扫点 CSV", "*.csv"), ("所有文件", "*.*")),
|
||||
)
|
||||
if not csv_paths:
|
||||
return
|
||||
default_name = datetime.now().strftime("valve_model_%Y%m%d_%H%M%S.json")
|
||||
output_path = filedialog.asksaveasfilename(
|
||||
parent=self.root,
|
||||
title="保存拟合后的前馈表",
|
||||
initialdir=str(Path(csv_paths[0]).resolve().parent),
|
||||
initialfile=default_name,
|
||||
defaultextension=".json",
|
||||
filetypes=(("阀模型 JSON", "*.json"),),
|
||||
confirmoverwrite=True,
|
||||
)
|
||||
if not output_path:
|
||||
return
|
||||
|
||||
self.fit_in_progress = True
|
||||
self.fit_model_button.configure(text="正在拟合…", state="disabled")
|
||||
self._append_log(
|
||||
f"开始合并拟合 {len(csv_paths)} 份开环 CSV;输出:{output_path}"
|
||||
)
|
||||
threading.Thread(
|
||||
target=self._run_feedforward_fit,
|
||||
args=(tuple(csv_paths), output_path),
|
||||
name="feedforward-model-fit",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _run_feedforward_fit(self, csv_paths, output_path):
|
||||
try:
|
||||
model, steady, table, dropped, stats, area_points = identify(
|
||||
csv_paths,
|
||||
tail=DEFAULT_TAIL,
|
||||
out_path=output_path,
|
||||
)
|
||||
image_path = str(Path(output_path).with_suffix(".png"))
|
||||
plot_warning = None
|
||||
try:
|
||||
create_diagnostic_plot(
|
||||
steady,
|
||||
table,
|
||||
dropped,
|
||||
area_points,
|
||||
image_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
image_path = None
|
||||
plot_warning = str(exc)
|
||||
self.logger.exception("前馈表诊断图生成失败")
|
||||
self.fit_events.put(
|
||||
{
|
||||
"kind": "model_fit_finished",
|
||||
"csv_count": len(csv_paths),
|
||||
"output_path": str(Path(output_path).resolve()),
|
||||
"image_path": image_path,
|
||||
"point_count": len(model.area_table),
|
||||
"steady_count": len(steady),
|
||||
"dropped_count": len(dropped),
|
||||
"stats": stats,
|
||||
"plot_warning": plot_warning,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
self.logger.exception("拟合前馈表失败")
|
||||
self.fit_events.put(
|
||||
{
|
||||
"kind": "model_fit_failed",
|
||||
"message": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
def _choose_model(self):
|
||||
path = filedialog.askopenfilename(
|
||||
parent=self.root,
|
||||
@@ -391,6 +795,11 @@ class FlowControlApp:
|
||||
self._handle_worker_event(event)
|
||||
except Empty:
|
||||
pass
|
||||
try:
|
||||
while True:
|
||||
self._handle_fit_event(self.fit_events.get_nowait())
|
||||
except Empty:
|
||||
pass
|
||||
if not self.closing:
|
||||
self.root.after(80, self._poll_worker_events)
|
||||
elif self.worker.is_alive():
|
||||
@@ -412,8 +821,52 @@ class FlowControlApp:
|
||||
target = event["target"]
|
||||
self.active_target_var.set(f"{target:g} SLM")
|
||||
self._append_log(f"目标流量已生效:{target:g} SLM")
|
||||
elif kind == "control_stopped":
|
||||
self.active_target_var.set("--")
|
||||
self.value_vars["opening"].set(
|
||||
self._format_number(event.get("opening_pct"), digits=2)
|
||||
)
|
||||
self.value_vars["motor"].set(
|
||||
self._format_number(event.get("motor_position"), digits=1)
|
||||
)
|
||||
self.value_vars["feedforward"].set("--")
|
||||
self.value_vars["correction"].set("--")
|
||||
self.value_vars["mode"].set("未启用")
|
||||
self._append_log("流量控制已停止;总开度已下发为 100%")
|
||||
elif kind == "manual_opening_applied":
|
||||
opening = event["opening_pct"]
|
||||
self.manual_opening_var.set(f"{opening:g}")
|
||||
self.value_vars["opening"].set(f"{opening:.2f}")
|
||||
self.value_vars["motor"].set(f"{event['motor_position']:.1f}")
|
||||
self.value_vars["feedforward"].set("--")
|
||||
self.value_vars["correction"].set("--")
|
||||
self.value_vars["mode"].set("未启用")
|
||||
self._append_log(f"手动开度已生效:{opening:g}%")
|
||||
elif kind == "model_active":
|
||||
self._append_log(f"当前控制模型:{event['display_name']}")
|
||||
elif kind == "open_loop_started":
|
||||
self.active_target_var.set("--")
|
||||
self._append_log(
|
||||
f"开环扫点已启动,共 {event['total_steps']} 点;CSV:"
|
||||
f"{event['csv_path']}"
|
||||
)
|
||||
elif kind == "open_loop_progress":
|
||||
self.detail_var.set(
|
||||
f"扫点 {event['current_step']}/{event['total_steps']}:"
|
||||
f"开度 {event['opening_pct']:.2f}% / "
|
||||
f"行程 {event['motor_position']:.1f},"
|
||||
f"本点 {event['step_elapsed_s']:.1f} s"
|
||||
)
|
||||
elif kind == "open_loop_finished":
|
||||
result = (
|
||||
f"开环扫点{event['reason']},记录 {event['sample_count']} 条采样。\n"
|
||||
f"CSV:{event['csv_path']}"
|
||||
)
|
||||
if event.get("image_path"):
|
||||
result += f"\nPNG:{event['image_path']}"
|
||||
self._append_log(result.replace("\n", ";"))
|
||||
if event.get("completed") and not self.closing:
|
||||
messagebox.showinfo("开环扫点完成", result, parent=self.root)
|
||||
elif kind == "warning":
|
||||
message = event.get("message", "未知警告")
|
||||
self.detail_var.set(message)
|
||||
@@ -429,6 +882,36 @@ class FlowControlApp:
|
||||
if errors:
|
||||
self._append_log("退出警告:" + ";".join(errors))
|
||||
|
||||
def _handle_fit_event(self, event):
|
||||
self.fit_in_progress = False
|
||||
if hasattr(self, "fit_model_button"):
|
||||
fit_enabled = self.current_state in {"DISCONNECTED", "FAULT", "MONITORING"}
|
||||
self.fit_model_button.configure(
|
||||
text="拟合前馈表",
|
||||
state="normal" if fit_enabled and not self.closing else "disabled",
|
||||
)
|
||||
if event["kind"] == "model_fit_failed":
|
||||
message = event.get("message", "未知错误")
|
||||
self._append_log("拟合前馈表失败:" + message)
|
||||
if not self.closing:
|
||||
messagebox.showerror("拟合前馈表失败", message, parent=self.root)
|
||||
return
|
||||
|
||||
message = (
|
||||
f"已合并 {event['csv_count']} 份 CSV,读取 {event['steady_count']} 个稳态点,"
|
||||
f"生成 {event['point_count']} 个 A_eff 标定点。\n"
|
||||
f"JSON:{event['output_path']}"
|
||||
)
|
||||
if event.get("image_path"):
|
||||
message += f"\n诊断图:{event['image_path']}"
|
||||
if event.get("dropped_count"):
|
||||
message += f"\n非单调检查丢弃 {event['dropped_count']} 个点。"
|
||||
if event.get("plot_warning"):
|
||||
message += f"\n诊断图生成失败,但 JSON 已保留:{event['plot_warning']}"
|
||||
self._append_log(message.replace("\n", ";"))
|
||||
if not self.closing:
|
||||
messagebox.showinfo("拟合前馈表完成", message, parent=self.root)
|
||||
|
||||
def _update_telemetry(self, event):
|
||||
self.value_vars["flow"].set(self._format_number(event.get("measured_flow_slm")))
|
||||
self.value_vars["pressure_before"].set(
|
||||
@@ -453,7 +936,7 @@ class FlowControlApp:
|
||||
self.value_vars["mode"].set(str(event.get("pid_mode") or "--"))
|
||||
|
||||
status = event.get("status", "")
|
||||
if status and status not in {"OK", "MONITORING"}:
|
||||
if status and status not in {"OK", "MONITORING", "OPEN_LOOP"}:
|
||||
self.detail_var.set(f"采样状态:{status}")
|
||||
|
||||
@staticmethod
|
||||
@@ -473,7 +956,17 @@ class FlowControlApp:
|
||||
self.status_badge.configure(fg=foreground, bg=background)
|
||||
|
||||
editable = state in {"DISCONNECTED", "FAULT"}
|
||||
connected = state in {"MONITORING", "CONTROLLING"}
|
||||
connected = state in {"MONITORING", "CONTROLLING", "OPEN_LOOP"}
|
||||
target_button_enabled = state in {"MONITORING", "CONTROLLING"}
|
||||
target_entry_enabled = state in {"MONITORING", "CONTROLLING"}
|
||||
stop_control_enabled = state == "CONTROLLING"
|
||||
manual_button_enabled = state in {"MONITORING", "CONTROLLING", "OPEN_LOOP"}
|
||||
manual_entry_enabled = state == "MONITORING"
|
||||
open_loop_enabled = state in {"MONITORING", "CONTROLLING", "OPEN_LOOP"}
|
||||
fit_enabled = state in {"DISCONNECTED", "FAULT", "MONITORING"}
|
||||
self.open_loop_button.configure(
|
||||
text="停止扫点" if state == "OPEN_LOOP" else "开环扫点"
|
||||
)
|
||||
self._set_state(self.host_entry, "normal" if editable else "disabled")
|
||||
self._set_state(self.port_entry, "normal" if editable else "disabled")
|
||||
self._set_state(self.slave_entry, "normal" if editable else "disabled")
|
||||
@@ -483,12 +976,39 @@ class FlowControlApp:
|
||||
self._set_state(
|
||||
self.model_builtin_button, "normal" if editable else "disabled"
|
||||
)
|
||||
self._set_state(
|
||||
self.open_loop_button, "normal" if open_loop_enabled else "disabled"
|
||||
)
|
||||
self._set_state(self.connect_button, "normal" if editable else "disabled")
|
||||
self._set_state(
|
||||
self.disconnect_button, "normal" if connected else "disabled"
|
||||
)
|
||||
self._set_state(self.target_entry, "normal" if connected else "disabled")
|
||||
self._set_state(self.target_button, "normal" if connected else "disabled")
|
||||
self._set_state(
|
||||
self.target_entry, "normal" if target_entry_enabled else "disabled"
|
||||
)
|
||||
self._set_state(
|
||||
self.target_button, "normal" if target_button_enabled else "disabled"
|
||||
)
|
||||
self._set_state(
|
||||
self.stop_control_button,
|
||||
"normal" if stop_control_enabled else "disabled",
|
||||
)
|
||||
self._set_state(
|
||||
self.manual_opening_entry,
|
||||
"normal" if manual_entry_enabled else "disabled",
|
||||
)
|
||||
self._set_state(
|
||||
self.manual_opening_button,
|
||||
"normal" if manual_button_enabled else "disabled",
|
||||
)
|
||||
if hasattr(self, "fit_model_button"):
|
||||
self.fit_model_button.configure(
|
||||
state=(
|
||||
"normal"
|
||||
if fit_enabled and not self.fit_in_progress and not self.closing
|
||||
else "disabled"
|
||||
)
|
||||
)
|
||||
if state in {"DISCONNECTED", "FAULT"}:
|
||||
self.active_target_var.set("--")
|
||||
|
||||
@@ -506,6 +1026,13 @@ class FlowControlApp:
|
||||
def _on_close(self):
|
||||
if self.closing:
|
||||
return
|
||||
if self.fit_in_progress:
|
||||
messagebox.showinfo(
|
||||
"正在拟合前馈表",
|
||||
"请等待拟合完成后再关闭程序,以免输出文件不完整。",
|
||||
parent=self.root,
|
||||
)
|
||||
return
|
||||
self.closing = True
|
||||
self._apply_state("STOPPING", "正在安全退出,请稍候…")
|
||||
self._append_log("正在停止控制、恢复 100% 开度并关闭连接")
|
||||
@@ -532,6 +1059,19 @@ def smoke_test():
|
||||
if info.point_count < 2 or info.path.name != BUILTIN_MODEL_NAME:
|
||||
raise RuntimeError("内置阀模型校验失败")
|
||||
from PcControl import Easy521ModbusClient # noqa: F401
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
with TemporaryDirectory(prefix="flow_control_smoke_") as temp_directory:
|
||||
image_path = Path(temp_directory) / "matplotlib_smoke.png"
|
||||
figure, axis = plt.subplots(figsize=(2, 1))
|
||||
axis.plot([0, 1], [0, 1])
|
||||
figure.savefig(image_path, dpi=50)
|
||||
plt.close(figure)
|
||||
if not image_path.is_file() or image_path.stat().st_size == 0:
|
||||
raise RuntimeError("开环曲线图依赖校验失败")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -11625,3 +11625,17 @@ PID 参数: Kp=0.100 Ki=0.100 Kd=0.000
|
||||
2026-09-03 15:16:18.515 INFO 控制数据已保存:D:\flow_control\data\flow_run_20260903_150333.csv
|
||||
2026-09-03 15:16:18.516 INFO 四联曲线图已保存:D:\flow_control\data\flow_run_20260903_150333.png
|
||||
2026-09-03 15:16:18.516 INFO 程序结束,退出码=0
|
||||
2026-09-04 15:21:55.535 ERROR 连接设备失败:无法连接 PLC
|
||||
Traceback (most recent call last):
|
||||
File "D:\flow_control\control_runtime.py", line 607, in _connect
|
||||
raise RuntimeError("无法连接 PLC")
|
||||
RuntimeError: 无法连接 PLC
|
||||
2026-09-04 15:26:38.463 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 15:36:04.665 INFO 设备已安全断开
|
||||
2026-09-04 15:36:22.936 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 15:38:39.544 INFO 开环扫点启动:共 3 点,开度 0.000%~100.000%,间隔 50.000%,采样周期 0.100 s,单点最长 30.000 s,CSV=D:\flow_control\open_loop_data\open_loop_20260904_153839.csv
|
||||
2026-09-04 15:38:47.954 INFO 开环步 1/3 完成:开度 50.000% / 行程 900.000,流量已稳定,耗时 8.409 s
|
||||
2026-09-04 15:38:55.657 INFO 开环步 2/3 完成:开度 0.000% / 行程 1000.000,流量已稳定,耗时 7.703 s
|
||||
2026-09-04 15:39:03.859 INFO 开环步 3/3 完成:开度 100.000% / 行程 800.000,流量已稳定,耗时 8.203 s
|
||||
2026-09-04 15:39:04.471 INFO 开环扫点结束:扫描完成,采样 244 条,CSV=D:\flow_control\open_loop_data\open_loop_20260904_153839.csv,PNG=D:\flow_control\open_loop_data\open_loop_20260904_153839.png
|
||||
2026-09-04 15:39:56.828 INFO 设备已安全断开
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
time_s,step_index,opening_pct,motor_position,flow_before_slm,flow_after_slm,pressure_before_kpa,pressure_after_kpa,P_abs_ratio,is_ratio_smaller_than_0.528
|
||||
0.000465,0,50.000000,900.000000,,187.434375,163.284375,4.675000,0.400590,Y
|
||||
0.115005,0,50.000000,900.000000,,187.453125,163.200000,3.812500,0.397458,Y
|
||||
0.208341,0,50.000000,900.000000,,187.237500,163.706250,1.600000,0.388350,Y
|
||||
0.315752,0,50.000000,900.000000,,187.275000,163.575000,0.450000,0.384202,Y
|
||||
0.409407,0,50.000000,900.000000,,174.984375,174.009375,0.612500,0.370232,Y
|
||||
0.503825,0,50.000000,900.000000,,174.984375,187.556250,0.737500,0.353303,Y
|
||||
0.612713,0,50.000000,900.000000,,151.237500,187.734375,0.875000,0.353561,Y
|
||||
0.706207,0,50.000000,900.000000,,151.256250,250.359375,0.875000,0.290601,Y
|
||||
0.815751,0,50.000000,900.000000,,136.387500,258.984375,0.912500,0.283749,Y
|
||||
0.909120,0,50.000000,900.000000,,136.387500,259.125000,0.712500,0.283084,Y
|
||||
1.003840,0,50.000000,900.000000,,124.678125,306.000000,0.675000,0.250414,Y
|
||||
1.113433,0,50.000000,900.000000,,124.678125,306.000000,0.612500,0.250261,Y
|
||||
1.208068,0,50.000000,900.000000,,113.653125,306.000000,0.550000,0.250107,Y
|
||||
1.302374,0,50.000000,900.000000,,113.634375,306.000000,0.537500,0.250077,Y
|
||||
1.411903,0,50.000000,900.000000,,104.278125,306.000000,0.550000,0.250107,Y
|
||||
1.505329,0,50.000000,900.000000,,104.259375,300.337500,0.537500,0.253602,Y
|
||||
1.613030,0,50.000000,900.000000,,97.809375,299.381250,0.512500,0.254145,Y
|
||||
1.704960,0,50.000000,900.000000,,92.306250,298.706250,0.500000,0.254543,Y
|
||||
1.814130,0,50.000000,900.000000,,88.050000,287.250000,0.500000,0.262047,Y
|
||||
1.906917,0,50.000000,900.000000,,84.450000,287.109375,0.500000,0.262142,Y
|
||||
2.015949,0,50.000000,900.000000,,81.553125,282.403125,0.512500,0.265390,Y
|
||||
2.108880,0,50.000000,900.000000,,79.181250,281.812500,0.500000,0.265766,Y
|
||||
2.204449,0,50.000000,900.000000,,77.353125,281.803125,0.475000,0.265707,Y
|
||||
2.315526,0,50.000000,900.000000,,75.834375,279.300000,0.575000,0.267718,Y
|
||||
2.410493,0,50.000000,900.000000,,74.906250,279.225000,0.512500,0.267606,Y
|
||||
2.505011,0,50.000000,900.000000,,74.259375,278.231250,0.500000,0.268274,Y
|
||||
2.614640,0,50.000000,900.000000,,73.650000,277.106250,0.437500,0.268906,Y
|
||||
2.707723,0,50.000000,900.000000,,73.162500,276.956250,0.537500,0.269277,Y
|
||||
2.815972,0,50.000000,900.000000,,72.506250,275.681250,0.500000,0.270088,Y
|
||||
2.909923,0,50.000000,900.000000,,72.112500,275.587500,0.475000,0.270089,Y
|
||||
3.003411,0,50.000000,900.000000,,71.831250,275.653125,0.550000,0.270241,Y
|
||||
3.111938,0,50.000000,900.000000,,71.550000,275.156250,0.500000,0.270465,Y
|
||||
3.206307,0,50.000000,900.000000,,71.259375,275.175000,0.500000,0.270452,Y
|
||||
3.315727,0,50.000000,900.000000,,71.034375,274.462500,0.450000,0.270831,Y
|
||||
3.408268,0,50.000000,900.000000,,70.884375,274.275000,0.475000,0.271033,Y
|
||||
3.502133,0,50.000000,900.000000,,70.678125,274.378125,0.475000,0.270959,Y
|
||||
3.612188,0,50.000000,900.000000,,70.500000,273.506250,0.475000,0.271589,Y
|
||||
3.705111,0,50.000000,900.000000,,70.509375,273.506250,0.450000,0.271522,Y
|
||||
3.815028,0,50.000000,900.000000,,70.228125,273.234375,0.475000,0.271786,Y
|
||||
3.908329,0,50.000000,900.000000,,70.031250,272.906250,0.475000,0.272024,Y
|
||||
4.001333,0,50.000000,900.000000,,69.853125,272.878125,0.475000,0.272045,Y
|
||||
4.111656,0,50.000000,900.000000,,69.759375,272.250000,0.475000,0.272502,Y
|
||||
4.203667,0,50.000000,900.000000,,69.675000,272.203125,0.450000,0.272469,Y
|
||||
4.313407,0,50.000000,900.000000,,69.562500,272.175000,0.475000,0.272557,Y
|
||||
4.408214,0,50.000000,900.000000,,69.562500,271.603125,0.450000,0.272908,Y
|
||||
4.500841,0,50.000000,900.000000,,69.562500,271.612500,0.475000,0.272968,Y
|
||||
4.611167,0,50.000000,900.000000,,69.478125,271.331250,0.450000,0.273107,Y
|
||||
4.705481,0,50.000000,900.000000,,69.384375,271.003125,0.475000,0.273415,Y
|
||||
4.801380,0,50.000000,900.000000,,69.384375,270.975000,0.450000,0.273368,Y
|
||||
4.910974,0,50.000000,900.000000,,69.281250,270.450000,0.450000,0.273754,Y
|
||||
5.003746,0,50.000000,900.000000,,69.281250,270.431250,0.450000,0.273768,Y
|
||||
5.112206,0,50.000000,900.000000,,69.084375,270.403125,0.450000,0.273789,Y
|
||||
5.204731,0,50.000000,900.000000,,69.084375,269.887500,0.437500,0.274135,Y
|
||||
5.311266,0,50.000000,900.000000,,69.084375,269.878125,0.450000,0.274176,Y
|
||||
5.404248,0,50.000000,900.000000,,69.084375,269.353125,0.450000,0.274564,Y
|
||||
5.512417,0,50.000000,900.000000,,68.981250,269.250000,0.450000,0.274641,Y
|
||||
5.606275,0,50.000000,900.000000,,69.000000,269.250000,0.437500,0.274607,Y
|
||||
5.715410,0,50.000000,900.000000,,68.803125,268.659375,0.450000,0.275079,Y
|
||||
5.810028,0,50.000000,900.000000,,68.803125,268.687500,0.475000,0.275126,Y
|
||||
5.902565,0,50.000000,900.000000,,68.803125,268.509375,0.450000,0.275191,Y
|
||||
6.010803,0,50.000000,900.000000,,68.803125,268.153125,0.450000,0.275456,Y
|
||||
6.103719,0,50.000000,900.000000,,68.700000,268.228125,0.450000,0.275400,Y
|
||||
6.213087,0,50.000000,900.000000,,68.700000,267.825000,0.437500,0.275667,Y
|
||||
6.305449,0,50.000000,900.000000,,68.709375,267.806250,0.450000,0.275715,Y
|
||||
6.413893,0,50.000000,900.000000,,68.709375,267.778125,0.450000,0.275736,Y
|
||||
6.508643,0,50.000000,900.000000,,68.709375,267.506250,0.450000,0.275939,Y
|
||||
6.602832,0,50.000000,900.000000,,68.625000,267.459375,0.450000,0.275974,Y
|
||||
6.711216,0,50.000000,900.000000,,68.625000,267.300000,0.437500,0.276060,Y
|
||||
6.804358,0,50.000000,900.000000,,68.531250,267.187500,0.450000,0.276178,Y
|
||||
6.911832,0,50.000000,900.000000,,68.531250,267.131250,0.450000,0.276220,Y
|
||||
7.003922,0,50.000000,900.000000,,68.512500,266.906250,0.450000,0.276389,Y
|
||||
7.111563,0,50.000000,900.000000,,68.512500,266.934375,0.450000,0.276368,Y
|
||||
7.206722,0,50.000000,900.000000,,68.428125,266.812500,0.450000,0.276459,Y
|
||||
7.315515,0,50.000000,900.000000,,68.428125,266.634375,0.450000,0.276593,Y
|
||||
7.409301,0,50.000000,900.000000,,68.437500,266.559375,0.437500,0.276615,Y
|
||||
7.502297,0,50.000000,900.000000,,68.428125,266.456250,0.437500,0.276693,Y
|
||||
7.610953,0,50.000000,900.000000,,68.428125,266.437500,0.450000,0.276741,Y
|
||||
7.705697,0,50.000000,900.000000,,68.437500,266.400000,0.450000,0.276769,Y
|
||||
7.815214,0,50.000000,900.000000,,68.231250,266.156250,0.412500,0.276851,Y
|
||||
7.910243,0,50.000000,900.000000,,68.250000,266.137500,0.450000,0.276967,Y
|
||||
8.003274,0,50.000000,900.000000,,68.053125,266.025000,0.450000,0.277052,Y
|
||||
8.111469,0,50.000000,900.000000,,68.034375,265.931250,0.475000,0.277191,Y
|
||||
8.204880,0,50.000000,900.000000,,68.034375,265.856250,0.450000,0.277179,Y
|
||||
8.315357,0,50.000000,900.000000,,68.053125,265.659375,0.450000,0.277328,Y
|
||||
8.408876,0,50.000000,900.000000,,67.950000,265.584375,0.437500,0.277351,Y
|
||||
8.501716,1,0.000000,1000.000000,,67.931250,265.556250,0.275000,0.276929,Y
|
||||
8.610688,1,0.000000,1000.000000,,67.959375,265.509375,0.075000,0.276419,Y
|
||||
8.704642,1,0.000000,1000.000000,,67.200000,265.500000,-0.412500,0.275097,Y
|
||||
8.815052,1,0.000000,1000.000000,,63.356250,272.728125,-0.400000,0.269815,Y
|
||||
8.907913,1,0.000000,1000.000000,,63.356250,274.050000,-0.437500,0.268765,Y
|
||||
9.002216,1,0.000000,1000.000000,,46.575000,273.984375,-0.437500,0.268812,Y
|
||||
9.110095,1,0.000000,1000.000000,,46.575000,302.512500,-0.437500,0.249822,Y
|
||||
9.203972,1,0.000000,1000.000000,,34.575000,303.628125,-0.437500,0.249134,Y
|
||||
9.312962,1,0.000000,1000.000000,,34.575000,306.000000,-0.437500,0.247683,Y
|
||||
9.407728,1,0.000000,1000.000000,,26.062500,306.000000,-0.437500,0.247683,Y
|
||||
9.516282,1,0.000000,1000.000000,,26.053125,306.000000,-0.412500,0.247744,Y
|
||||
9.610539,1,0.000000,1000.000000,,19.903125,306.000000,-0.412500,0.247744,Y
|
||||
9.703702,1,0.000000,1000.000000,,19.903125,306.000000,-0.412500,0.247744,Y
|
||||
9.813100,1,0.000000,1000.000000,,14.981250,306.000000,-0.412500,0.247744,Y
|
||||
9.906359,1,0.000000,1000.000000,,15.000000,306.000000,-0.437500,0.247683,Y
|
||||
10.014850,1,0.000000,1000.000000,,12.262500,306.000000,-0.437500,0.247683,Y
|
||||
10.108935,1,0.000000,1000.000000,,12.262500,306.000000,-0.412500,0.247744,Y
|
||||
10.202016,1,0.000000,1000.000000,,10.031250,306.000000,-0.412500,0.247744,Y
|
||||
10.310346,1,0.000000,1000.000000,,10.012500,306.000000,-0.437500,0.247683,Y
|
||||
10.403813,1,0.000000,1000.000000,,8.259375,306.000000,-0.400000,0.247775,Y
|
||||
10.511780,1,0.000000,1000.000000,,6.862500,306.000000,-0.437500,0.247683,Y
|
||||
10.604479,1,0.000000,1000.000000,,5.775000,306.000000,-0.437500,0.247683,Y
|
||||
10.715498,1,0.000000,1000.000000,,4.875000,306.000000,-0.437500,0.247683,Y
|
||||
10.809545,1,0.000000,1000.000000,,4.237500,306.000000,-0.412500,0.247744,Y
|
||||
10.903266,1,0.000000,1000.000000,,3.609375,306.000000,-0.412500,0.247744,Y
|
||||
11.014938,1,0.000000,1000.000000,,3.159375,306.000000,-0.437500,0.247683,Y
|
||||
11.109451,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
11.204439,1,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
11.314392,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
11.410480,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
11.504635,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
11.613545,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
11.706208,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
11.813378,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
11.907555,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
12.001490,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
12.110144,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
12.204164,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
12.312489,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
12.407025,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
12.515291,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
12.609668,1,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
12.702590,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
12.811330,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
12.905740,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
13.015580,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
13.110274,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
13.203983,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
13.312423,1,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
13.404399,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
13.514171,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
13.606372,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
13.714786,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
13.806688,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
13.900811,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
14.009408,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
14.101423,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
14.211493,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
14.303768,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
14.413902,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
14.508788,1,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
14.602868,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
14.711554,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
14.804333,1,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
14.913899,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.008987,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
15.100892,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
15.210617,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.304107,1,0.000000,1000.000000,,0.900000,306.000000,-0.412500,0.247744,Y
|
||||
15.412886,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
15.505917,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.615302,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.708620,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.801537,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
15.910002,1,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
16.002593,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
16.111551,1,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
16.206975,2,100.000000,800.000000,,0.909375,306.000000,-0.137500,0.248420,Y
|
||||
16.300432,2,100.000000,800.000000,,0.900000,306.000000,0.075000,0.248941,Y
|
||||
16.408520,2,100.000000,800.000000,,3.131250,306.000000,1.575000,0.252624,Y
|
||||
16.501544,2,100.000000,800.000000,,3.131250,306.000000,5.500000,0.262260,Y
|
||||
16.608562,2,100.000000,800.000000,,12.262500,306.000000,5.237500,0.261615,Y
|
||||
16.700756,2,100.000000,800.000000,,12.253125,257.906250,5.700000,0.297928,Y
|
||||
16.810034,2,100.000000,800.000000,,30.806250,255.384375,6.112500,0.301191,Y
|
||||
16.903183,2,100.000000,800.000000,,30.806250,242.981250,6.437500,0.312984,Y
|
||||
17.011672,2,100.000000,800.000000,,51.731250,201.309375,6.812500,0.357321,Y
|
||||
17.104987,2,100.000000,800.000000,,51.731250,201.009375,7.050000,0.358461,Y
|
||||
17.213785,2,100.000000,800.000000,,73.509375,203.850000,7.100000,0.355288,Y
|
||||
17.306299,2,100.000000,800.000000,,73.500000,204.150000,6.950000,0.354448,Y
|
||||
17.415664,2,100.000000,800.000000,,96.956250,204.234375,6.512500,0.352918,Y
|
||||
17.509765,2,100.000000,800.000000,,96.956250,218.062500,6.212500,0.336699,Y
|
||||
17.603175,2,100.000000,800.000000,,118.537500,218.212500,5.875000,0.335485,Y
|
||||
17.713140,2,100.000000,800.000000,,137.062500,214.106250,5.637500,0.339099,Y
|
||||
17.805074,2,100.000000,800.000000,,137.081250,213.009375,5.537500,0.339964,Y
|
||||
17.913759,2,100.000000,800.000000,,149.475000,212.906250,5.350000,0.339479,Y
|
||||
18.006096,2,100.000000,800.000000,,149.475000,197.559375,5.250000,0.356576,Y
|
||||
18.101236,2,100.000000,800.000000,,158.887500,196.687500,5.312500,0.357829,Y
|
||||
18.209808,2,100.000000,800.000000,,158.906250,195.084375,5.137500,0.359174,Y
|
||||
18.302507,2,100.000000,800.000000,,165.853125,184.762500,5.050000,0.371827,Y
|
||||
18.409788,2,100.000000,800.000000,,165.853125,184.528125,5.012500,0.372000,Y
|
||||
18.502780,2,100.000000,800.000000,,171.009375,178.837500,4.975000,0.379423,Y
|
||||
18.611535,2,100.000000,800.000000,,171.009375,177.356250,4.937500,0.381305,Y
|
||||
18.706653,2,100.000000,800.000000,,175.509375,177.262500,4.912500,0.381343,Y
|
||||
18.813850,2,100.000000,800.000000,,175.509375,173.353125,4.900000,0.386725,Y
|
||||
18.907567,2,100.000000,800.000000,,178.828125,173.175000,4.875000,0.386885,Y
|
||||
19.015508,2,100.000000,800.000000,,178.828125,171.909375,4.850000,0.388586,Y
|
||||
19.109109,2,100.000000,800.000000,,181.425000,171.281250,4.850000,0.389481,Y
|
||||
19.203673,2,100.000000,800.000000,,181.406250,171.178125,4.850000,0.389629,Y
|
||||
19.313439,2,100.000000,800.000000,,182.934375,170.006250,4.812500,0.391173,Y
|
||||
19.406323,2,100.000000,800.000000,,182.934375,169.959375,4.837500,0.391333,Y
|
||||
19.514776,2,100.000000,800.000000,,184.481250,169.903125,4.812500,0.391322,Y
|
||||
19.606611,2,100.000000,800.000000,,184.481250,169.228125,4.800000,0.392252,Y
|
||||
19.714587,2,100.000000,800.000000,,185.559375,169.162500,4.775000,0.392255,Y
|
||||
19.808214,2,100.000000,800.000000,,185.559375,168.975000,4.812500,0.392666,Y
|
||||
19.902203,2,100.000000,800.000000,,186.656250,168.806250,4.800000,0.392865,Y
|
||||
20.010558,2,100.000000,800.000000,,186.656250,168.787500,4.837500,0.393031,Y
|
||||
20.104165,2,100.000000,800.000000,,187.509375,168.628125,4.812500,0.393170,Y
|
||||
20.212786,2,100.000000,800.000000,,187.509375,168.637500,4.850000,0.393295,Y
|
||||
20.306803,2,100.000000,800.000000,,188.062500,168.600000,4.837500,0.393304,Y
|
||||
20.401358,2,100.000000,800.000000,,188.062500,168.487500,4.812500,0.393375,Y
|
||||
20.509285,2,100.000000,800.000000,,188.428125,168.684375,4.812500,0.393088,Y
|
||||
20.603665,2,100.000000,800.000000,,188.428125,168.525000,4.800000,0.393274,Y
|
||||
20.713313,2,100.000000,800.000000,,188.634375,168.506250,4.837500,0.393440,Y
|
||||
20.807792,2,100.000000,800.000000,,188.625000,168.609375,4.812500,0.393197,Y
|
||||
20.902149,2,100.000000,800.000000,,188.709375,168.562500,4.812500,0.393266,Y
|
||||
21.012919,2,100.000000,800.000000,,188.709375,168.506250,4.775000,0.393209,Y
|
||||
21.107750,2,100.000000,800.000000,,188.812500,168.525000,4.812500,0.393320,Y
|
||||
21.200795,2,100.000000,800.000000,,188.812500,168.656250,4.812500,0.393129,Y
|
||||
21.310606,2,100.000000,800.000000,,188.981250,168.609375,4.812500,0.393197,Y
|
||||
21.405638,2,100.000000,800.000000,,189.000000,168.656250,4.800000,0.393083,Y
|
||||
21.514632,2,100.000000,800.000000,,189.178125,168.628125,4.837500,0.393263,Y
|
||||
21.607468,2,100.000000,800.000000,,189.187500,168.628125,4.812500,0.393170,Y
|
||||
21.714539,2,100.000000,800.000000,,189.262500,168.684375,4.812500,0.393088,Y
|
||||
21.806778,2,100.000000,800.000000,,189.262500,168.703125,4.837500,0.393153,Y
|
||||
21.914006,2,100.000000,800.000000,,189.084375,168.628125,4.750000,0.392939,Y
|
||||
22.007598,2,100.000000,800.000000,,189.084375,168.675000,4.950000,0.393611,Y
|
||||
22.100228,2,100.000000,800.000000,,189.928125,168.562500,4.800000,0.393219,Y
|
||||
22.208966,2,100.000000,800.000000,,189.909375,167.906250,4.850000,0.394364,Y
|
||||
22.302995,2,100.000000,800.000000,,189.750000,167.878125,4.812500,0.394265,Y
|
||||
22.412299,2,100.000000,800.000000,,189.750000,167.550000,4.812500,0.394747,Y
|
||||
22.507080,2,100.000000,800.000000,,189.853125,167.278125,4.812500,0.395146,Y
|
||||
22.615736,2,100.000000,800.000000,,189.853125,167.250000,4.837500,0.395281,Y
|
||||
22.708125,2,100.000000,800.000000,,190.050000,167.362500,4.850000,0.395162,Y
|
||||
22.803544,2,100.000000,800.000000,,190.031250,167.362500,4.837500,0.395115,Y
|
||||
22.912848,2,100.000000,800.000000,,190.031250,167.156250,4.812500,0.395326,Y
|
||||
23.005556,2,100.000000,800.000000,,190.031250,167.428125,4.800000,0.394879,Y
|
||||
23.114711,2,100.000000,800.000000,,190.125000,167.409375,4.812500,0.394953,Y
|
||||
23.208776,2,100.000000,800.000000,,190.106250,167.475000,4.837500,0.394950,Y
|
||||
23.301839,2,100.000000,800.000000,,190.106250,167.428125,4.812500,0.394926,Y
|
||||
23.411973,2,100.000000,800.000000,,190.125000,167.550000,4.837500,0.394840,Y
|
||||
23.505934,2,100.000000,800.000000,,190.125000,167.400000,4.850000,0.395107,Y
|
||||
23.613543,2,100.000000,800.000000,,190.125000,167.428125,4.812500,0.394926,Y
|
||||
23.705710,2,100.000000,800.000000,,190.012500,167.484375,4.812500,0.394843,Y
|
||||
23.814689,2,100.000000,800.000000,,190.012500,167.362500,4.812500,0.395022,Y
|
||||
23.908181,2,100.000000,800.000000,,190.125000,167.381250,4.800000,0.394948,Y
|
||||
24.002726,2,100.000000,800.000000,,190.134375,167.306250,4.812500,0.395105,Y
|
||||
24.112077,2,100.000000,800.000000,,190.106250,167.203125,4.812500,0.395257,Y
|
||||
24.205615,2,100.000000,800.000000,,190.134375,167.437500,4.837500,0.395005,Y
|
||||
24.314128,2,100.000000,800.000000,,190.050000,167.306250,4.800000,0.395058,Y
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
@@ -1,12 +1,18 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from queue import Empty
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import config
|
||||
from control_runtime import (
|
||||
ControlWorker,
|
||||
build_open_loop_points,
|
||||
load_model,
|
||||
validate_device_settings,
|
||||
validate_opening,
|
||||
validate_open_loop_settings,
|
||||
validate_target,
|
||||
)
|
||||
|
||||
@@ -46,6 +52,26 @@ class OverpressureHardware(FakeHardware):
|
||||
return 100.0
|
||||
|
||||
|
||||
class MemoryOpenLoopRecorder:
|
||||
def __init__(self):
|
||||
self.csv_path = Path("memory_open_loop.csv")
|
||||
self.image_path = Path("memory_open_loop.png")
|
||||
self.sample_count = 0
|
||||
self.rows = []
|
||||
self.finalize_count = 0
|
||||
|
||||
def record(self, **row):
|
||||
self.rows.append(row)
|
||||
self.sample_count += 1
|
||||
|
||||
def finalize(self):
|
||||
self.finalize_count += 1
|
||||
return self.image_path if self.sample_count else None
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
def wait_for_event(worker, predicate, timeout=2.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
@@ -70,6 +96,9 @@ def test_validate_ui_values():
|
||||
assert settings.port == 502
|
||||
assert settings.slave_id == 1
|
||||
assert validate_target("50.5") == 50.5
|
||||
assert validate_opening("25.5") == 25.5
|
||||
with pytest.raises(ValueError, match="0~100"):
|
||||
validate_opening("100.1")
|
||||
|
||||
|
||||
def test_builtin_model_is_valve_model_004():
|
||||
@@ -81,6 +110,29 @@ def test_builtin_model_is_valve_model_004():
|
||||
assert info.model.motor_closed == config.MOTOR_CLOSED_POSITION
|
||||
|
||||
|
||||
def test_validate_and_build_gui_open_loop_points():
|
||||
settings = validate_open_loop_settings(
|
||||
min_opening_pct="0",
|
||||
max_opening_pct="10",
|
||||
opening_step_pct="5",
|
||||
random_seed="42",
|
||||
include_fine_scan=False,
|
||||
)
|
||||
points = build_open_loop_points(settings)
|
||||
assert sorted(opening for opening, _position in points) == [0.0, 5.0, 10.0]
|
||||
assert all(
|
||||
config.MOTOR_OPEN_POSITION <= position <= config.MOTOR_CLOSED_POSITION
|
||||
for _opening, position in points
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="开度范围"):
|
||||
validate_open_loop_settings(
|
||||
min_opening_pct=50,
|
||||
max_opening_pct=50,
|
||||
include_fine_scan=False,
|
||||
)
|
||||
|
||||
|
||||
def test_worker_connect_control_and_safe_disconnect():
|
||||
hardware = FakeHardware()
|
||||
worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware)
|
||||
@@ -137,6 +189,142 @@ def test_worker_connect_control_and_safe_disconnect():
|
||||
assert not worker.is_alive()
|
||||
|
||||
|
||||
def test_worker_runs_open_loop_scan_and_keeps_connection():
|
||||
hardware = FakeHardware()
|
||||
recorder = MemoryOpenLoopRecorder()
|
||||
worker = ControlWorker(
|
||||
make_logger(),
|
||||
hardware_factory=lambda _settings: hardware,
|
||||
open_loop_recorder_factory=lambda: recorder,
|
||||
)
|
||||
worker.start()
|
||||
wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED")
|
||||
worker.request_connect(validate_device_settings("192.168.1.88", 502, 1))
|
||||
wait_for_event(worker, lambda event: event.get("state") == "MONITORING")
|
||||
|
||||
settings = validate_open_loop_settings(
|
||||
min_opening_pct=0,
|
||||
max_opening_pct=1,
|
||||
opening_step_pct=1,
|
||||
random_seed=1,
|
||||
sample_period_s=0.005,
|
||||
steady_window_s=0.01,
|
||||
max_wait_s=0.03,
|
||||
include_fine_scan=False,
|
||||
)
|
||||
worker.request_open_loop_start(settings)
|
||||
started = wait_for_event(
|
||||
worker, lambda event: event.get("kind") == "open_loop_started"
|
||||
)
|
||||
assert started["total_steps"] == 2
|
||||
finished = wait_for_event(
|
||||
worker, lambda event: event.get("kind") == "open_loop_finished"
|
||||
)
|
||||
assert finished["completed"] is True
|
||||
assert finished["sample_count"] == recorder.sample_count
|
||||
assert recorder.sample_count >= 4
|
||||
assert recorder.finalize_count == 1
|
||||
wait_for_event(worker, lambda event: event.get("state") == "MONITORING")
|
||||
assert hardware.connected is True
|
||||
assert hardware.motor_commands[-1] == (
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
|
||||
worker.request_shutdown()
|
||||
wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete")
|
||||
worker.thread.join(timeout=1.0)
|
||||
assert not worker.is_alive()
|
||||
|
||||
|
||||
def test_worker_stops_closed_loop_at_full_open_and_accepts_manual_opening():
|
||||
hardware = FakeHardware()
|
||||
worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware)
|
||||
worker.start()
|
||||
wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED")
|
||||
worker.request_connect(validate_device_settings("192.168.1.88", 502, 1))
|
||||
wait_for_event(worker, lambda event: event.get("state") == "MONITORING")
|
||||
|
||||
worker.request_target(50.0)
|
||||
wait_for_event(worker, lambda event: event.get("kind") == "target_applied")
|
||||
worker.request_manual_opening(25.0)
|
||||
warning = wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "warning"
|
||||
and "先停止流量控制" in event.get("message", ""),
|
||||
)
|
||||
assert "先停止流量控制" in warning["message"]
|
||||
|
||||
worker.request_stop_control()
|
||||
stopped = wait_for_event(
|
||||
worker, lambda event: event.get("kind") == "control_stopped"
|
||||
)
|
||||
assert stopped["opening_pct"] == 100.0
|
||||
assert stopped["motor_position"] == config.MOTOR_OPEN_POSITION
|
||||
wait_for_event(worker, lambda event: event.get("state") == "MONITORING")
|
||||
assert hardware.motor_commands[-1] == (
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
|
||||
worker.request_manual_opening(25.0)
|
||||
manual = wait_for_event(
|
||||
worker, lambda event: event.get("kind") == "manual_opening_applied"
|
||||
)
|
||||
assert manual["opening_pct"] == 25.0
|
||||
assert manual["motor_position"] == pytest.approx(950.0)
|
||||
wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "telemetry"
|
||||
and event.get("pid_mode") == "未启用",
|
||||
)
|
||||
assert hardware.motor_commands[-1] == (950.0, config.MOTOR_OUTPUT_ADDR)
|
||||
|
||||
worker.request_shutdown()
|
||||
wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete")
|
||||
worker.thread.join(timeout=1.0)
|
||||
assert not worker.is_alive()
|
||||
|
||||
|
||||
def test_disconnect_during_open_loop_finalizes_partial_data():
|
||||
hardware = FakeHardware()
|
||||
recorder = MemoryOpenLoopRecorder()
|
||||
worker = ControlWorker(
|
||||
make_logger(),
|
||||
hardware_factory=lambda _settings: hardware,
|
||||
open_loop_recorder_factory=lambda: recorder,
|
||||
)
|
||||
worker.start()
|
||||
wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED")
|
||||
worker.request_connect(validate_device_settings("192.168.1.88", 502, 1))
|
||||
wait_for_event(worker, lambda event: event.get("state") == "MONITORING")
|
||||
settings = validate_open_loop_settings(
|
||||
min_opening_pct=0,
|
||||
max_opening_pct=100,
|
||||
opening_step_pct=1,
|
||||
sample_period_s=0.02,
|
||||
steady_window_s=1,
|
||||
max_wait_s=1,
|
||||
include_fine_scan=False,
|
||||
)
|
||||
worker.request_open_loop_start(settings)
|
||||
wait_for_event(worker, lambda event: event.get("kind") == "open_loop_started")
|
||||
worker.request_disconnect()
|
||||
finished = wait_for_event(
|
||||
worker, lambda event: event.get("kind") == "open_loop_finished"
|
||||
)
|
||||
assert finished["completed"] is False
|
||||
assert finished["reason"] == "设备断开或程序退出"
|
||||
assert recorder.finalize_count == 1
|
||||
wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED")
|
||||
assert hardware.connected is False
|
||||
|
||||
worker.request_shutdown()
|
||||
wait_for_event(worker, lambda event: event.get("kind") == "shutdown_complete")
|
||||
worker.thread.join(timeout=1.0)
|
||||
assert not worker.is_alive()
|
||||
|
||||
|
||||
def test_worker_connection_failure_reports_fault_without_hardware_access():
|
||||
hardware = FakeHardware(connect_ok=False)
|
||||
worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import gui_main
|
||||
|
||||
|
||||
class DummyVar:
|
||||
def __init__(self, value=""):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
class DummyWidget:
|
||||
def __init__(self):
|
||||
self.options = {}
|
||||
|
||||
def configure(self, **options):
|
||||
self.options.update(options)
|
||||
|
||||
|
||||
class WorkerSpy:
|
||||
def __init__(self):
|
||||
self.stop_control_count = 0
|
||||
self.stop_open_loop_count = 0
|
||||
self.manual_openings = []
|
||||
self.targets = []
|
||||
|
||||
def request_target(self, target):
|
||||
self.targets.append(target)
|
||||
|
||||
def request_stop_control(self):
|
||||
self.stop_control_count += 1
|
||||
|
||||
def request_open_loop_stop(self):
|
||||
self.stop_open_loop_count += 1
|
||||
|
||||
def request_manual_opening(self, opening):
|
||||
self.manual_openings.append(opening)
|
||||
|
||||
|
||||
def _fake_app(state):
|
||||
return SimpleNamespace(
|
||||
current_state=state,
|
||||
root=object(),
|
||||
worker=WorkerSpy(),
|
||||
detail_var=DummyVar(),
|
||||
target_var=DummyVar("50"),
|
||||
manual_opening_var=DummyVar("25"),
|
||||
target_button=DummyWidget(),
|
||||
stop_control_button=DummyWidget(),
|
||||
open_loop_button=DummyWidget(),
|
||||
_append_log=lambda _message: None,
|
||||
)
|
||||
|
||||
|
||||
def test_open_loop_button_stops_immediately_without_confirmation(monkeypatch):
|
||||
app = _fake_app("OPEN_LOOP")
|
||||
confirmations = []
|
||||
monkeypatch.setattr(
|
||||
gui_main.messagebox,
|
||||
"askyesno",
|
||||
lambda *args, **kwargs: confirmations.append((args, kwargs)),
|
||||
)
|
||||
|
||||
gui_main.FlowControlApp._open_open_loop_dialog(app)
|
||||
|
||||
assert app.worker.stop_open_loop_count == 1
|
||||
assert confirmations == []
|
||||
|
||||
|
||||
def test_separate_stop_control_button_stops_closed_loop():
|
||||
app = _fake_app("CONTROLLING")
|
||||
|
||||
gui_main.FlowControlApp._stop_control(app)
|
||||
|
||||
assert app.worker.stop_control_count == 1
|
||||
assert app.stop_control_button.options["state"] == "disabled"
|
||||
|
||||
|
||||
def test_target_can_be_changed_while_closed_loop_is_running():
|
||||
app = _fake_app("CONTROLLING")
|
||||
app.target_var.set("72.5")
|
||||
|
||||
gui_main.FlowControlApp._set_target(app)
|
||||
|
||||
assert app.worker.targets == [72.5]
|
||||
assert app.detail_var.get() == "正在设置目标流量 72.5 SLM…"
|
||||
|
||||
|
||||
def test_controlling_state_keeps_target_controls_and_stop_button_enabled():
|
||||
app = object.__new__(gui_main.FlowControlApp)
|
||||
app.status_var = DummyVar()
|
||||
app.detail_var = DummyVar()
|
||||
app.active_target_var = DummyVar()
|
||||
app.fit_in_progress = False
|
||||
app.closing = False
|
||||
for name in (
|
||||
"status_badge",
|
||||
"host_entry",
|
||||
"port_entry",
|
||||
"slave_entry",
|
||||
"model_browse_button",
|
||||
"model_builtin_button",
|
||||
"open_loop_button",
|
||||
"connect_button",
|
||||
"disconnect_button",
|
||||
"target_entry",
|
||||
"target_button",
|
||||
"stop_control_button",
|
||||
"manual_opening_entry",
|
||||
"manual_opening_button",
|
||||
"fit_model_button",
|
||||
):
|
||||
setattr(app, name, DummyWidget())
|
||||
|
||||
gui_main.FlowControlApp._apply_state(app, "CONTROLLING", "闭环运行中")
|
||||
|
||||
assert app.target_entry.options["state"] == "normal"
|
||||
assert app.target_button.options["state"] == "normal"
|
||||
assert "text" not in app.target_button.options
|
||||
assert app.stop_control_button.options["state"] == "normal"
|
||||
|
||||
|
||||
def test_manual_opening_during_control_only_shows_warning(monkeypatch):
|
||||
app = _fake_app("CONTROLLING")
|
||||
warnings = []
|
||||
monkeypatch.setattr(
|
||||
gui_main.messagebox,
|
||||
"showwarning",
|
||||
lambda *args, **kwargs: warnings.append((args, kwargs)),
|
||||
)
|
||||
|
||||
gui_main.FlowControlApp._set_manual_opening(app)
|
||||
|
||||
assert len(warnings) == 1
|
||||
assert "停止流量控制" in warnings[0][0][0]
|
||||
assert app.worker.stop_control_count == 0
|
||||
assert app.worker.manual_openings == []
|
||||
Binary file not shown.
@@ -2,3 +2,55 @@
|
||||
2026-09-03 16:11:54.165 INFO 目标流量设置为 60.000 SLM
|
||||
2026-09-03 16:12:01.872 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.056 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-03 16:12:35.171 INFO 设备已安全断开
|
||||
2026-09-04 15:40:07.607 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 15:40:14.826 INFO 开环扫点启动:共 6 点,开度 0.000%~100.000%,间隔 20.000%,采样周期 0.100 s,单点最长 30.000 s,CSV=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_154014.csv
|
||||
2026-09-04 15:40:23.337 INFO 开环步 1/6 完成:开度 60.000% / 行程 880.000,流量已稳定,耗时 8.510 s
|
||||
2026-09-04 15:40:31.642 INFO 开环步 2/6 完成:开度 20.000% / 行程 960.000,流量已稳定,耗时 8.305 s
|
||||
2026-09-04 15:40:37.941 INFO 开环步 3/6 完成:开度 40.000% / 行程 920.000,流量已稳定,耗时 6.300 s
|
||||
2026-09-04 15:40:46.439 INFO 开环步 4/6 完成:开度 80.000% / 行程 840.000,流量已稳定,耗时 8.498 s
|
||||
2026-09-04 15:40:53.638 INFO 开环步 5/6 完成:开度 0.000% / 行程 1000.000,流量已稳定,耗时 7.199 s
|
||||
2026-09-04 15:41:01.641 INFO 开环步 6/6 完成:开度 100.000% / 行程 800.000,流量已稳定,耗时 8.003 s
|
||||
2026-09-04 15:41:03.529 INFO 开环扫点结束:扫描完成,采样 469 条,CSV=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_154014.csv,PNG=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_154014.png
|
||||
2026-09-04 15:44:05.152 INFO 目标流量设置为 50.000 SLM
|
||||
2026-09-04 15:44:08.664 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.212 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 15:44:20.184 INFO 目标流量设置为 50.000 SLM
|
||||
2026-09-04 15:44:56.963 INFO 设备已安全断开
|
||||
2026-09-04 15:45:49.991 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:00:34.942 INFO 设备已安全断开
|
||||
2026-09-04 16:17:49.120 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:18:00.575 INFO 开环扫点启动:共 32 点,开度 0.000%~100.000%,间隔 5.000%,采样周期 0.100 s,单点最长 30.000 s,CSV=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_161800.csv
|
||||
2026-09-04 16:18:06.001 INFO 开环扫点结束:用户停止,采样 36 条,CSV=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_161800.csv,PNG=D:\flow_control\可执行程序\open_loop_data\open_loop_20260904_161800.png
|
||||
2026-09-04 16:19:53.760 INFO 手动开度已设置:50.000%,电机行程 900.000
|
||||
2026-09-04 16:20:21.337 INFO 手动开度已设置:100.000%,电机行程 800.000
|
||||
2026-09-04 16:20:25.488 INFO 目标流量设置为 50.000 SLM
|
||||
2026-09-04 16:20:27.695 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.519 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:20:33.625 INFO 闭环流量控制已停止,阀门已设置为 100%% 开度
|
||||
2026-09-04 16:23:14.055 INFO 手动开度已设置:50.000%,电机行程 900.000
|
||||
2026-09-04 16:23:33.127 INFO PID 模式 NEAR -> FAR:基础 Kp=0.500 Ki=0.500 Kd=0.000,有效 Kp=0.500 Ki=0.500(scale=1.000),最大开度速度=200.000%/s;原因=控制启动
|
||||
2026-09-04 16:23:33.127 INFO 目标流量设置为 50.000 SLM
|
||||
2026-09-04 16:23:35.441 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.594 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:24:04.153 INFO 闭环流量控制已停止,阀门已设置为 100%% 开度
|
||||
2026-09-04 16:24:06.513 INFO PID 模式 NEAR -> FAR:基础 Kp=0.500 Ki=0.500 Kd=0.000,有效 Kp=0.500 Ki=0.500(scale=1.000),最大开度速度=200.000%/s;原因=控制启动
|
||||
2026-09-04 16:24:06.513 INFO 目标流量设置为 50.000 SLM
|
||||
2026-09-04 16:24:10.523 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.934 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:24:18.266 INFO 闭环流量控制已停止,阀门已设置为 100%% 开度
|
||||
2026-09-04 16:24:38.930 INFO 设备已安全断开
|
||||
2026-09-04 16:30:54.312 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:31:00.288 INFO 目标流量设置为 60.000 SLM
|
||||
2026-09-04 16:31:03.204 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=1.566 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:31:10.689 INFO 闭环流量控制已停止,阀门已设置为 100%% 开度
|
||||
2026-09-04 16:31:19.760 INFO 手动开度已设置:50.000%,电机行程 900.000
|
||||
2026-09-04 16:31:41.425 INFO 手动开度已设置:100.000%,电机行程 800.000
|
||||
2026-09-04 16:31:45.554 INFO 设备已安全断开
|
||||
2026-09-04 16:31:51.090 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:32:06.244 INFO 设备已安全断开
|
||||
2026-09-04 16:35:26.832 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:35:28.659 INFO 设备已安全断开
|
||||
2026-09-04 16:43:08.208 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
2026-09-04 16:43:15.724 INFO 手动开度已设置:50.000%,电机行程 900.000
|
||||
2026-09-04 16:43:32.683 INFO 目标流量设置为 60.000 SLM
|
||||
2026-09-04 16:43:33.587 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=1.500 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:43:41.331 INFO PID 模式 NEAR -> FAR:基础 Kp=0.500 Ki=0.500 Kd=0.000,有效 Kp=0.500 Ki=0.500(scale=1.000),最大开度速度=200.000%/s;原因=目标流量切换后 |误差|=60.488 SLM > NEAR 阈值=2.000 SLM
|
||||
2026-09-04 16:43:41.331 INFO 目标流量设置为 120.000 SLM
|
||||
2026-09-04 16:43:45.642 INFO PID 模式 FAR -> NEAR:基础 Kp=0.100 Ki=0.100 Kd=0.000,有效 Kp=0.100 Ki=0.100(scale=1.000),最大开度速度=2.000%/s;原因=|误差|=0.591 SLM <= 阈值=2.000 SLM,连续 10 个周期
|
||||
2026-09-04 16:43:52.344 INFO 设备已安全断开
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
time_s,step_index,opening_pct,motor_position,flow_before_slm,flow_after_slm,pressure_before_kpa,pressure_after_kpa,P_abs_ratio,is_ratio_smaller_than_0.528
|
||||
0.016064,0,60.000000,880.000000,,188.681250,165.562500,4.637500,0.397031,Y
|
||||
0.106803,0,60.000000,880.000000,,188.381250,165.600000,3.875000,0.394118,Y
|
||||
0.201446,0,60.000000,880.000000,,188.400000,165.581250,1.700000,0.385997,Y
|
||||
0.309713,0,60.000000,880.000000,,183.628125,167.906250,1.000000,0.380064,Y
|
||||
0.404886,0,60.000000,880.000000,,168.112500,167.850000,1.137500,0.380654,Y
|
||||
0.512460,0,60.000000,880.000000,,151.725000,198.834375,1.312500,0.341943,Y
|
||||
0.606028,0,60.000000,880.000000,,139.781250,204.759375,1.450000,0.335773,Y
|
||||
0.715904,0,60.000000,880.000000,,131.850000,204.909375,1.512500,0.335813,Y
|
||||
0.809327,0,60.000000,880.000000,,126.262500,269.850000,1.500000,0.277026,Y
|
||||
0.903706,0,60.000000,880.000000,,122.184375,272.212500,1.412500,0.275039,Y
|
||||
1.013253,0,60.000000,880.000000,,118.781250,289.087500,1.375000,0.263055,Y
|
||||
1.107751,0,60.000000,880.000000,,115.912500,306.000000,1.312500,0.251979,Y
|
||||
1.202827,0,60.000000,880.000000,,112.987500,306.000000,1.275000,0.251887,Y
|
||||
1.312481,0,60.000000,880.000000,,112.987500,306.000000,1.212500,0.251734,Y
|
||||
1.405571,0,60.000000,880.000000,,109.903125,305.906250,1.175000,0.251700,Y
|
||||
1.513112,0,60.000000,880.000000,,107.081250,305.878125,1.150000,0.251656,Y
|
||||
1.605373,0,60.000000,880.000000,,107.081250,291.956250,1.150000,0.260564,Y
|
||||
1.715817,0,60.000000,880.000000,,104.634375,291.656250,1.112500,0.260668,Y
|
||||
1.807602,0,60.000000,880.000000,,102.675000,285.150000,1.150000,0.265153,Y
|
||||
1.916153,0,60.000000,880.000000,,102.703125,281.081250,1.112500,0.267876,Y
|
||||
2.008038,0,60.000000,880.000000,,101.034375,281.062500,1.100000,0.267857,Y
|
||||
2.103033,0,60.000000,880.000000,,101.034375,275.728125,1.075000,0.271580,Y
|
||||
2.211689,0,60.000000,880.000000,,99.759375,275.156250,1.100000,0.272059,Y
|
||||
2.304878,0,60.000000,880.000000,,99.759375,275.175000,1.112500,0.272078,Y
|
||||
2.415720,0,60.000000,880.000000,,98.559375,272.053125,1.250000,0.274722,Y
|
||||
2.509340,0,60.000000,880.000000,,98.559375,272.025000,1.175000,0.274541,Y
|
||||
2.603400,0,60.000000,880.000000,,97.837500,270.281250,1.100000,0.275628,Y
|
||||
2.712730,0,60.000000,880.000000,,97.828125,269.737500,1.075000,0.275964,Y
|
||||
2.806274,0,60.000000,880.000000,,97.181250,269.784375,1.100000,0.275997,Y
|
||||
2.915162,0,60.000000,880.000000,,97.200000,268.209375,1.100000,0.277173,Y
|
||||
3.008924,0,60.000000,880.000000,,96.356250,268.031250,1.100000,0.277307,Y
|
||||
3.103052,0,60.000000,880.000000,,95.784375,267.853125,1.112500,0.277474,Y
|
||||
3.211491,0,60.000000,880.000000,,95.775000,267.356250,1.100000,0.277815,Y
|
||||
3.304457,0,60.000000,880.000000,,95.306250,267.506250,1.075000,0.277634,Y
|
||||
3.412136,0,60.000000,880.000000,,95.325000,266.906250,1.100000,0.278154,Y
|
||||
3.505867,0,60.000000,880.000000,,94.912500,267.009375,1.100000,0.278076,Y
|
||||
3.600949,0,60.000000,880.000000,,94.931250,266.962500,1.075000,0.278044,Y
|
||||
3.710756,0,60.000000,880.000000,,94.650000,266.184375,1.050000,0.278564,Y
|
||||
3.803215,0,60.000000,880.000000,,94.650000,266.137500,1.100000,0.278736,Y
|
||||
3.913127,0,60.000000,880.000000,,94.181250,265.687500,1.100000,0.279078,Y
|
||||
4.007702,0,60.000000,880.000000,,94.181250,265.537500,1.050000,0.279056,Y
|
||||
4.101932,0,60.000000,880.000000,,93.984375,265.509375,1.075000,0.279145,Y
|
||||
4.213057,0,60.000000,880.000000,,93.984375,265.012500,1.050000,0.279455,Y
|
||||
4.306237,0,60.000000,880.000000,,93.431250,264.937500,1.075000,0.279581,Y
|
||||
4.414890,0,60.000000,880.000000,,93.450000,264.900000,1.050000,0.279541,Y
|
||||
4.508697,0,60.000000,880.000000,,93.253125,264.450000,1.075000,0.279954,Y
|
||||
4.602556,0,60.000000,880.000000,,93.234375,264.459375,1.050000,0.279878,Y
|
||||
4.712164,0,60.000000,880.000000,,93.056250,264.112500,1.150000,0.280417,Y
|
||||
4.806583,0,60.000000,880.000000,,92.962500,264.075000,1.075000,0.280241,Y
|
||||
4.916033,0,60.000000,880.000000,,92.962500,264.131250,1.050000,0.280129,Y
|
||||
5.008110,0,60.000000,880.000000,,92.784375,263.409375,1.075000,0.280752,Y
|
||||
5.101394,0,60.000000,880.000000,,92.578125,263.353125,1.100000,0.280864,Y
|
||||
5.208766,0,60.000000,880.000000,,92.578125,263.128125,1.075000,0.280969,Y
|
||||
5.302469,0,60.000000,880.000000,,92.503125,262.978125,1.050000,0.281016,Y
|
||||
5.411419,0,60.000000,880.000000,,92.475000,262.950000,1.050000,0.281038,Y
|
||||
5.505036,0,60.000000,880.000000,,92.306250,263.006250,1.050000,0.280994,Y
|
||||
5.613238,0,60.000000,880.000000,,92.306250,263.053125,1.037500,0.280924,Y
|
||||
5.706367,0,60.000000,880.000000,,92.306250,263.053125,1.037500,0.280924,Y
|
||||
5.814893,0,60.000000,880.000000,,92.287500,262.725000,1.037500,0.281177,Y
|
||||
5.908093,0,60.000000,880.000000,,92.203125,262.650000,1.037500,0.281235,Y
|
||||
6.002248,0,60.000000,880.000000,,92.212500,262.537500,1.050000,0.281356,Y
|
||||
6.111137,0,60.000000,880.000000,,92.109375,262.481250,1.050000,0.281400,Y
|
||||
6.203013,0,60.000000,880.000000,,92.109375,262.481250,1.037500,0.281365,Y
|
||||
6.312115,0,60.000000,880.000000,,92.109375,262.284375,1.050000,0.281552,Y
|
||||
6.405178,0,60.000000,880.000000,,92.109375,262.331250,1.050000,0.281516,Y
|
||||
6.514467,0,60.000000,880.000000,,92.128125,262.200000,1.050000,0.281617,Y
|
||||
6.607386,0,60.000000,880.000000,,92.109375,262.059375,1.050000,0.281726,Y
|
||||
6.701670,0,60.000000,880.000000,,91.912500,262.078125,1.050000,0.281712,Y
|
||||
6.811400,0,60.000000,880.000000,,91.912500,261.806250,1.050000,0.281923,Y
|
||||
6.903796,0,60.000000,880.000000,,91.912500,261.628125,1.050000,0.282061,Y
|
||||
7.012085,0,60.000000,880.000000,,91.903125,261.862500,1.050000,0.281879,Y
|
||||
7.106330,0,60.000000,880.000000,,91.912500,261.600000,1.050000,0.282083,Y
|
||||
7.214515,0,60.000000,880.000000,,91.912500,261.609375,1.050000,0.282076,Y
|
||||
7.307163,0,60.000000,880.000000,,91.828125,261.609375,1.050000,0.282076,Y
|
||||
7.415235,0,60.000000,880.000000,,91.828125,261.487500,1.012500,0.282067,Y
|
||||
7.507584,0,60.000000,880.000000,,91.828125,261.506250,1.037500,0.282122,Y
|
||||
7.601099,0,60.000000,880.000000,,91.828125,261.300000,1.050000,0.282316,Y
|
||||
7.711082,0,60.000000,880.000000,,91.725000,261.337500,1.037500,0.282253,Y
|
||||
7.804182,0,60.000000,880.000000,,91.734375,261.262500,1.050000,0.282346,Y
|
||||
7.913281,0,60.000000,880.000000,,91.631250,261.009375,1.037500,0.282508,Y
|
||||
8.008287,0,60.000000,880.000000,,91.537500,261.009375,1.037500,0.282508,Y
|
||||
8.102140,0,60.000000,880.000000,,91.453125,260.859375,1.037500,0.282625,Y
|
||||
8.213432,0,60.000000,880.000000,,91.453125,260.700000,1.037500,0.282750,Y
|
||||
8.307220,0,60.000000,880.000000,,91.453125,260.728125,1.037500,0.282728,Y
|
||||
8.401244,0,60.000000,880.000000,,91.453125,260.559375,1.012500,0.282791,Y
|
||||
8.509553,0,60.000000,880.000000,,91.434375,260.550000,1.037500,0.282867,Y
|
||||
8.603031,1,20.000000,960.000000,,91.434375,260.559375,0.675000,0.281858,Y
|
||||
8.711363,1,20.000000,960.000000,,91.087500,260.362500,0.137500,0.280525,Y
|
||||
8.804107,1,20.000000,960.000000,,88.237500,260.353125,0.100000,0.280429,Y
|
||||
8.911841,1,20.000000,960.000000,,88.228125,267.731250,0.050000,0.274687,Y
|
||||
9.004310,1,20.000000,960.000000,,79.753125,268.303125,0.075000,0.274330,Y
|
||||
9.113209,1,20.000000,960.000000,,79.734375,268.350000,0.075000,0.274295,Y
|
||||
9.207194,1,20.000000,960.000000,,73.256250,280.950000,0.075000,0.265254,Y
|
||||
9.315231,1,20.000000,960.000000,,73.237500,281.175000,0.050000,0.265033,Y
|
||||
9.407563,1,20.000000,960.000000,,68.334375,282.909375,0.075000,0.263901,Y
|
||||
9.500685,1,20.000000,960.000000,,68.334375,283.612500,0.075000,0.263419,Y
|
||||
9.611418,1,20.000000,960.000000,,64.284375,283.631250,0.075000,0.263407,Y
|
||||
9.703906,1,20.000000,960.000000,,64.284375,280.828125,0.050000,0.265273,Y
|
||||
9.813382,1,20.000000,960.000000,,61.106250,280.575000,0.050000,0.265449,Y
|
||||
9.906138,1,20.000000,960.000000,,61.106250,280.528125,0.050000,0.265482,Y
|
||||
10.015166,1,20.000000,960.000000,,58.950000,279.375000,0.050000,0.266286,Y
|
||||
10.109132,1,20.000000,960.000000,,58.950000,279.403125,0.050000,0.266266,Y
|
||||
10.201979,1,20.000000,960.000000,,57.375000,278.878125,0.075000,0.266700,Y
|
||||
10.310806,1,20.000000,960.000000,,57.375000,278.756250,0.050000,0.266719,Y
|
||||
10.404921,1,20.000000,960.000000,,56.137500,278.709375,0.050000,0.266752,Y
|
||||
10.515497,1,20.000000,960.000000,,56.137500,278.287500,0.075000,0.267114,Y
|
||||
10.608387,1,20.000000,960.000000,,55.031250,278.184375,0.075000,0.267187,Y
|
||||
10.702427,1,20.000000,960.000000,,55.031250,278.100000,0.075000,0.267246,Y
|
||||
10.811183,1,20.000000,960.000000,,54.225000,277.875000,0.050000,0.267339,Y
|
||||
10.904486,1,20.000000,960.000000,,54.225000,277.912500,0.100000,0.267445,Y
|
||||
11.013915,1,20.000000,960.000000,,53.475000,277.706250,0.075000,0.267524,Y
|
||||
11.105884,1,20.000000,960.000000,,53.484375,277.631250,0.050000,0.267511,Y
|
||||
11.213166,1,20.000000,960.000000,,53.128125,277.650000,0.037500,0.267465,Y
|
||||
11.306464,1,20.000000,960.000000,,53.109375,277.087500,0.075000,0.267962,Y
|
||||
11.414383,1,20.000000,960.000000,,52.762500,277.106250,0.050000,0.267882,Y
|
||||
11.507622,1,20.000000,960.000000,,52.781250,276.900000,0.050000,0.268028,Y
|
||||
11.602753,1,20.000000,960.000000,,52.303125,276.787500,0.037500,0.268075,Y
|
||||
11.711181,1,20.000000,960.000000,,52.303125,276.806250,0.037500,0.268062,Y
|
||||
11.804764,1,20.000000,960.000000,,52.125000,276.975000,0.075000,0.268041,Y
|
||||
11.914251,1,20.000000,960.000000,,51.853125,277.012500,0.075000,0.268015,Y
|
||||
12.007005,1,20.000000,960.000000,,51.834375,277.003125,0.075000,0.268021,Y
|
||||
12.100348,1,20.000000,960.000000,,51.656250,276.900000,0.050000,0.268028,Y
|
||||
12.207095,1,20.000000,960.000000,,51.675000,276.909375,0.050000,0.268022,Y
|
||||
12.301029,1,20.000000,960.000000,,51.403125,276.881250,0.050000,0.268042,Y
|
||||
12.407901,1,20.000000,960.000000,,51.300000,276.834375,0.037500,0.268042,Y
|
||||
12.503345,1,20.000000,960.000000,,51.309375,276.900000,0.037500,0.267995,Y
|
||||
12.614866,1,20.000000,960.000000,,51.206250,276.675000,0.050000,0.268188,Y
|
||||
12.708620,1,20.000000,960.000000,,51.206250,276.731250,0.075000,0.268214,Y
|
||||
12.802486,1,20.000000,960.000000,,51.037500,276.628125,0.075000,0.268287,Y
|
||||
12.911985,1,20.000000,960.000000,,51.028125,276.609375,0.050000,0.268234,Y
|
||||
13.005728,1,20.000000,960.000000,,50.934375,276.656250,0.100000,0.268333,Y
|
||||
13.113697,1,20.000000,960.000000,,50.934375,276.525000,0.075000,0.268360,Y
|
||||
13.207032,1,20.000000,960.000000,,50.850000,276.534375,0.075000,0.268354,Y
|
||||
13.300790,1,20.000000,960.000000,,50.850000,276.412500,0.050000,0.268374,Y
|
||||
13.411738,1,20.000000,960.000000,,50.756250,276.328125,0.075000,0.268500,Y
|
||||
13.505349,1,20.000000,960.000000,,50.756250,276.262500,0.075000,0.268547,Y
|
||||
13.614048,1,20.000000,960.000000,,50.756250,276.159375,0.075000,0.268620,Y
|
||||
13.706753,1,20.000000,960.000000,,50.756250,276.103125,0.100000,0.268727,Y
|
||||
13.815733,1,20.000000,960.000000,,50.653125,276.056250,0.100000,0.268760,Y
|
||||
13.907699,1,20.000000,960.000000,,50.662500,275.934375,0.075000,0.268781,Y
|
||||
14.001788,1,20.000000,960.000000,,50.653125,275.906250,0.050000,0.268734,Y
|
||||
14.111306,1,20.000000,960.000000,,50.653125,275.831250,0.075000,0.268854,Y
|
||||
14.206496,1,20.000000,960.000000,,50.578125,275.756250,0.075000,0.268908,Y
|
||||
14.314839,1,20.000000,960.000000,,50.559375,275.803125,0.050000,0.268808,Y
|
||||
14.409199,1,20.000000,960.000000,,50.578125,275.653125,0.075000,0.268981,Y
|
||||
14.503629,1,20.000000,960.000000,,50.578125,275.587500,0.075000,0.269028,Y
|
||||
14.612775,1,20.000000,960.000000,,50.559375,275.578125,0.100000,0.269101,Y
|
||||
14.706160,1,20.000000,960.000000,,50.578125,275.503125,0.100000,0.269155,Y
|
||||
14.815482,1,20.000000,960.000000,,50.559375,275.456250,0.075000,0.269122,Y
|
||||
14.907379,1,20.000000,960.000000,,50.578125,275.428125,0.050000,0.269075,Y
|
||||
15.000630,1,20.000000,960.000000,,50.559375,275.325000,0.075000,0.269215,Y
|
||||
15.110895,1,20.000000,960.000000,,50.578125,275.259375,0.075000,0.269262,Y
|
||||
15.203524,1,20.000000,960.000000,,50.381250,275.184375,0.050000,0.269250,Y
|
||||
15.312638,1,20.000000,960.000000,,50.381250,275.184375,0.075000,0.269316,Y
|
||||
15.406493,1,20.000000,960.000000,,50.400000,275.212500,0.075000,0.269296,Y
|
||||
15.516385,1,20.000000,960.000000,,50.381250,274.987500,0.075000,0.269457,Y
|
||||
15.610509,1,20.000000,960.000000,,50.381250,275.053125,0.075000,0.269410,Y
|
||||
15.703464,1,20.000000,960.000000,,50.381250,274.884375,0.075000,0.269531,Y
|
||||
15.811599,1,20.000000,960.000000,,50.381250,274.856250,0.075000,0.269551,Y
|
||||
15.905289,1,20.000000,960.000000,,50.362500,274.856250,0.050000,0.269484,Y
|
||||
16.014386,1,20.000000,960.000000,,50.287500,274.800000,0.037500,0.269492,Y
|
||||
16.109014,1,20.000000,960.000000,,50.287500,274.781250,0.050000,0.269538,Y
|
||||
16.202010,1,20.000000,960.000000,,50.278125,274.800000,0.050000,0.269525,Y
|
||||
16.311680,1,20.000000,960.000000,,50.287500,274.687500,0.075000,0.269672,Y
|
||||
16.405137,1,20.000000,960.000000,,50.278125,274.659375,0.050000,0.269626,Y
|
||||
16.514482,1,20.000000,960.000000,,50.287500,274.378125,0.050000,0.269827,Y
|
||||
16.608402,1,20.000000,960.000000,,50.203125,274.378125,0.050000,0.269827,Y
|
||||
16.703391,1,20.000000,960.000000,,50.203125,274.425000,0.050000,0.269794,Y
|
||||
16.814411,1,20.000000,960.000000,,50.203125,274.275000,0.075000,0.269968,Y
|
||||
16.908164,2,40.000000,920.000000,,50.203125,274.256250,0.037500,0.269882,Y
|
||||
17.001623,2,40.000000,920.000000,,50.203125,274.134375,0.150000,0.270269,Y
|
||||
17.110800,2,40.000000,920.000000,,50.212500,274.106250,0.150000,0.270289,Y
|
||||
17.203652,2,40.000000,920.000000,,51.187500,274.087500,0.150000,0.270303,Y
|
||||
17.312440,2,40.000000,920.000000,,51.187500,271.556250,0.175000,0.272205,Y
|
||||
17.405974,2,40.000000,920.000000,,52.106250,271.434375,0.150000,0.272227,Y
|
||||
17.515198,2,40.000000,920.000000,,52.106250,270.056250,0.150000,0.273237,Y
|
||||
17.606942,2,40.000000,920.000000,,52.912500,268.200000,0.175000,0.274677,Y
|
||||
17.700776,2,40.000000,920.000000,,52.931250,268.256250,0.175000,0.274635,Y
|
||||
17.809654,2,40.000000,920.000000,,53.550000,267.712500,0.150000,0.274972,Y
|
||||
17.901598,2,40.000000,920.000000,,53.559375,267.731250,0.175000,0.275026,Y
|
||||
18.010680,2,40.000000,920.000000,,54.187500,267.684375,0.175000,0.275061,Y
|
||||
18.104138,2,40.000000,920.000000,,54.187500,267.937500,0.150000,0.274805,Y
|
||||
18.213294,2,40.000000,920.000000,,54.562500,267.881250,0.150000,0.274846,Y
|
||||
18.305500,2,40.000000,920.000000,,54.562500,267.900000,0.175000,0.274900,Y
|
||||
18.414558,2,40.000000,920.000000,,54.928125,267.853125,0.175000,0.274935,Y
|
||||
18.506420,2,40.000000,920.000000,,54.937500,267.853125,0.175000,0.274935,Y
|
||||
18.612863,2,40.000000,920.000000,,55.106250,267.731250,0.150000,0.274958,Y
|
||||
18.706523,2,40.000000,920.000000,,55.125000,267.703125,0.150000,0.274979,Y
|
||||
18.815345,2,40.000000,920.000000,,55.284375,267.703125,0.150000,0.274979,Y
|
||||
18.908371,2,40.000000,920.000000,,55.303125,267.750000,0.150000,0.274944,Y
|
||||
19.001499,2,40.000000,920.000000,,55.462500,267.656250,0.150000,0.275014,Y
|
||||
19.110301,2,40.000000,920.000000,,55.481250,267.628125,0.175000,0.275103,Y
|
||||
19.203566,2,40.000000,920.000000,,55.659375,267.712500,0.150000,0.274972,Y
|
||||
19.313180,2,40.000000,920.000000,,55.659375,267.581250,0.150000,0.275070,Y
|
||||
19.406078,2,40.000000,920.000000,,55.762500,267.656250,0.175000,0.275082,Y
|
||||
19.515104,2,40.000000,920.000000,,55.753125,267.684375,0.150000,0.274993,Y
|
||||
19.608687,2,40.000000,920.000000,,55.950000,267.787500,0.175000,0.274984,Y
|
||||
19.702817,2,40.000000,920.000000,,55.950000,267.712500,0.150000,0.274972,Y
|
||||
19.810582,2,40.000000,920.000000,,55.950000,267.759375,0.150000,0.274937,Y
|
||||
19.903449,2,40.000000,920.000000,,55.931250,267.778125,0.150000,0.274923,Y
|
||||
20.011866,2,40.000000,920.000000,,56.034375,267.834375,0.175000,0.274949,Y
|
||||
20.105658,2,40.000000,920.000000,,56.034375,267.759375,0.175000,0.275005,Y
|
||||
20.214880,2,40.000000,920.000000,,56.034375,267.834375,0.175000,0.274949,Y
|
||||
20.308493,2,40.000000,920.000000,,56.034375,267.909375,0.175000,0.274893,Y
|
||||
20.402761,2,40.000000,920.000000,,56.128125,267.928125,0.175000,0.274879,Y
|
||||
20.511549,2,40.000000,920.000000,,56.137500,268.106250,0.175000,0.274747,Y
|
||||
20.605671,2,40.000000,920.000000,,56.128125,268.087500,0.175000,0.274761,Y
|
||||
20.715556,2,40.000000,920.000000,,56.128125,268.228125,0.175000,0.274656,Y
|
||||
20.809475,2,40.000000,920.000000,,56.203125,268.162500,0.175000,0.274705,Y
|
||||
20.903114,2,40.000000,920.000000,,56.212500,268.181250,0.150000,0.274623,Y
|
||||
21.013121,2,40.000000,920.000000,,56.231250,268.303125,0.175000,0.274600,Y
|
||||
21.108009,2,40.000000,920.000000,,56.212500,268.228125,0.150000,0.274588,Y
|
||||
21.215628,2,40.000000,920.000000,,56.231250,268.200000,0.150000,0.274609,Y
|
||||
21.308372,2,40.000000,920.000000,,56.231250,268.228125,0.175000,0.274656,Y
|
||||
21.401712,2,40.000000,920.000000,,56.231250,268.275000,0.150000,0.274554,Y
|
||||
21.510951,2,40.000000,920.000000,,56.212500,268.134375,0.175000,0.274726,Y
|
||||
21.603383,2,40.000000,920.000000,,56.212500,268.200000,0.150000,0.274609,Y
|
||||
21.712163,2,40.000000,920.000000,,56.212500,268.275000,0.175000,0.274621,Y
|
||||
21.805488,2,40.000000,920.000000,,56.203125,268.275000,0.150000,0.274554,Y
|
||||
21.915660,2,40.000000,920.000000,,56.231250,268.181250,0.175000,0.274691,Y
|
||||
22.007823,2,40.000000,920.000000,,56.231250,268.162500,0.150000,0.274637,Y
|
||||
22.101594,2,40.000000,920.000000,,56.231250,268.153125,0.175000,0.274712,Y
|
||||
22.210672,2,40.000000,920.000000,,56.203125,268.275000,0.175000,0.274621,Y
|
||||
22.304408,2,40.000000,920.000000,,56.231250,268.228125,0.175000,0.274656,Y
|
||||
22.413083,2,40.000000,920.000000,,56.212500,268.275000,0.175000,0.274621,Y
|
||||
22.506561,2,40.000000,920.000000,,56.212500,268.200000,0.175000,0.274677,Y
|
||||
22.614856,2,40.000000,920.000000,,56.325000,268.181250,0.175000,0.274691,Y
|
||||
22.709187,2,40.000000,920.000000,,56.306250,268.228125,0.200000,0.274724,Y
|
||||
22.803926,2,40.000000,920.000000,,56.212500,268.012500,0.150000,0.274749,Y
|
||||
22.912210,2,40.000000,920.000000,,56.212500,267.956250,0.175000,0.274858,Y
|
||||
23.005599,2,40.000000,920.000000,,56.212500,268.003125,0.200000,0.274891,Y
|
||||
23.114059,2,40.000000,920.000000,,56.212500,267.900000,0.175000,0.274900,Y
|
||||
23.208148,3,80.000000,840.000000,,56.212500,267.909375,0.275000,0.275164,Y
|
||||
23.302597,3,80.000000,840.000000,,56.212500,267.787500,1.137500,0.277592,Y
|
||||
23.411767,3,80.000000,840.000000,,57.431250,267.900000,3.037500,0.282653,Y
|
||||
23.504967,3,80.000000,840.000000,,57.431250,267.853125,2.937500,0.282418,Y
|
||||
23.612205,3,80.000000,840.000000,,70.378125,250.875000,3.050000,0.296352,Y
|
||||
23.705500,3,80.000000,840.000000,,70.378125,250.528125,3.112500,0.296821,Y
|
||||
23.814019,3,80.000000,840.000000,,83.475000,233.053125,3.200000,0.312595,Y
|
||||
23.907314,3,80.000000,840.000000,,83.475000,226.678125,3.275000,0.318899,Y
|
||||
24.000815,3,80.000000,840.000000,,95.550000,226.453125,3.312500,0.319233,Y
|
||||
24.112969,3,80.000000,840.000000,,95.531250,225.459375,3.337500,0.320280,Y
|
||||
24.205289,3,80.000000,840.000000,,105.253125,225.356250,3.400000,0.320572,Y
|
||||
24.314078,3,80.000000,840.000000,,105.253125,226.528125,3.412500,0.319465,Y
|
||||
24.406546,3,80.000000,840.000000,,115.687500,234.337500,3.437500,0.312107,Y
|
||||
24.514569,3,80.000000,840.000000,,115.687500,234.384375,3.450000,0.312100,Y
|
||||
24.608511,3,80.000000,840.000000,,124.350000,238.809375,3.450000,0.308040,Y
|
||||
24.702741,3,80.000000,840.000000,,124.359375,239.775000,3.500000,0.307315,Y
|
||||
24.812059,3,80.000000,840.000000,,131.503125,239.784375,3.500000,0.307306,Y
|
||||
24.906776,3,80.000000,840.000000,,131.512500,243.103125,3.512500,0.304381,Y
|
||||
25.001291,3,80.000000,840.000000,,137.353125,243.159375,3.500000,0.304295,Y
|
||||
25.108889,3,80.000000,840.000000,,137.353125,244.134375,3.537500,0.303545,Y
|
||||
25.204892,3,80.000000,840.000000,,142.331250,245.409375,3.550000,0.302465,Y
|
||||
25.313982,3,80.000000,840.000000,,142.312500,245.409375,3.537500,0.302429,Y
|
||||
25.406776,3,80.000000,840.000000,,146.184375,246.984375,3.575000,0.301169,Y
|
||||
25.514708,3,80.000000,840.000000,,146.175000,247.153125,3.600000,0.301095,Y
|
||||
25.606826,3,80.000000,840.000000,,149.456250,247.181250,3.600000,0.301071,Y
|
||||
25.715342,3,80.000000,840.000000,,149.456250,248.587500,3.600000,0.299861,Y
|
||||
25.808056,3,80.000000,840.000000,,152.137500,248.681250,3.612500,0.299816,Y
|
||||
25.901422,3,80.000000,840.000000,,152.128125,249.328125,3.600000,0.299227,Y
|
||||
26.010650,3,80.000000,840.000000,,154.462500,249.928125,3.600000,0.298716,Y
|
||||
26.102189,3,80.000000,840.000000,,154.462500,249.853125,3.637500,0.298887,Y
|
||||
26.211814,3,80.000000,840.000000,,156.253125,250.612500,3.550000,0.297993,Y
|
||||
26.304707,3,80.000000,840.000000,,156.253125,250.734375,3.775000,0.298529,Y
|
||||
26.415279,3,80.000000,840.000000,,158.137500,250.828125,3.612500,0.297988,Y
|
||||
26.507682,3,80.000000,840.000000,,158.137500,250.781250,3.612500,0.298028,Y
|
||||
26.602299,3,80.000000,840.000000,,159.478125,250.950000,3.600000,0.297850,Y
|
||||
26.711748,3,80.000000,840.000000,,159.478125,251.531250,3.612500,0.297394,Y
|
||||
26.804083,3,80.000000,840.000000,,160.228125,251.709375,3.612500,0.297244,Y
|
||||
26.912784,3,80.000000,840.000000,,160.228125,251.756250,3.637500,0.297276,Y
|
||||
27.007908,3,80.000000,840.000000,,161.081250,252.834375,3.612500,0.296300,Y
|
||||
27.101215,3,80.000000,840.000000,,161.081250,252.937500,3.612500,0.296214,Y
|
||||
27.211221,3,80.000000,840.000000,,161.559375,253.059375,3.637500,0.296183,Y
|
||||
27.303557,3,80.000000,840.000000,,161.559375,253.209375,3.612500,0.295987,Y
|
||||
27.411382,3,80.000000,840.000000,,162.112500,253.162500,3.637500,0.296096,Y
|
||||
27.505451,3,80.000000,840.000000,,162.131250,253.406250,3.612500,0.295823,Y
|
||||
27.613892,3,80.000000,840.000000,,162.600000,253.350000,3.612500,0.295869,Y
|
||||
27.706805,3,80.000000,840.000000,,162.600000,253.462500,3.637500,0.295846,Y
|
||||
27.815971,3,80.000000,840.000000,,163.078125,253.603125,3.637500,0.295729,Y
|
||||
27.908607,3,80.000000,840.000000,,163.078125,253.603125,3.612500,0.295658,Y
|
||||
28.001817,3,80.000000,840.000000,,163.434375,253.725000,3.637500,0.295627,Y
|
||||
28.110511,3,80.000000,840.000000,,163.434375,253.800000,3.612500,0.295495,Y
|
||||
28.203363,3,80.000000,840.000000,,163.650000,253.800000,3.650000,0.295600,Y
|
||||
28.310691,3,80.000000,840.000000,,163.612500,253.959375,3.650000,0.295468,Y
|
||||
28.404058,3,80.000000,840.000000,,163.903125,253.978125,3.637500,0.295417,Y
|
||||
28.511904,3,80.000000,840.000000,,163.884375,253.987500,3.650000,0.295444,Y
|
||||
28.605858,3,80.000000,840.000000,,164.100000,254.081250,3.637500,0.295331,Y
|
||||
28.713477,3,80.000000,840.000000,,164.081250,254.128125,3.637500,0.295292,Y
|
||||
28.806791,3,80.000000,840.000000,,164.184375,254.100000,3.675000,0.295421,Y
|
||||
28.915465,3,80.000000,840.000000,,164.184375,254.175000,3.650000,0.295288,Y
|
||||
29.009183,3,80.000000,840.000000,,164.362500,254.137500,3.637500,0.295284,Y
|
||||
29.101410,3,80.000000,840.000000,,164.362500,254.306250,3.637500,0.295144,Y
|
||||
29.211492,3,80.000000,840.000000,,164.559375,254.278125,3.675000,0.295273,Y
|
||||
29.304525,3,80.000000,840.000000,,164.559375,254.278125,3.950000,0.296046,Y
|
||||
29.415173,3,80.000000,840.000000,,164.737500,254.409375,3.675000,0.295164,Y
|
||||
29.509396,3,80.000000,840.000000,,164.737500,254.353125,3.700000,0.295281,Y
|
||||
29.601246,3,80.000000,840.000000,,164.934375,253.959375,3.700000,0.295608,Y
|
||||
29.709213,3,80.000000,840.000000,,164.925000,253.950000,3.700000,0.295616,Y
|
||||
29.801803,3,80.000000,840.000000,,164.925000,253.912500,3.650000,0.295507,Y
|
||||
29.910944,3,80.000000,840.000000,,164.934375,253.987500,3.675000,0.295515,Y
|
||||
30.005981,3,80.000000,840.000000,,165.028125,253.987500,3.675000,0.295515,Y
|
||||
30.114781,3,80.000000,840.000000,,165.009375,254.550000,3.675000,0.295047,Y
|
||||
30.208553,3,80.000000,840.000000,,165.112500,254.681250,3.675000,0.294939,Y
|
||||
30.301492,3,80.000000,840.000000,,165.112500,254.681250,3.675000,0.294939,Y
|
||||
30.410354,3,80.000000,840.000000,,165.206250,254.803125,3.675000,0.294838,Y
|
||||
30.504218,3,80.000000,840.000000,,165.225000,254.756250,3.675000,0.294877,Y
|
||||
30.613473,3,80.000000,840.000000,,165.487500,254.887500,3.675000,0.294768,Y
|
||||
30.706572,3,80.000000,840.000000,,165.478125,254.850000,3.675000,0.294799,Y
|
||||
30.800050,3,80.000000,840.000000,,165.581250,254.925000,3.675000,0.294737,Y
|
||||
30.908651,3,80.000000,840.000000,,165.562500,255.056250,3.675000,0.294628,Y
|
||||
31.002314,3,80.000000,840.000000,,165.581250,255.028125,3.675000,0.294652,Y
|
||||
31.111114,3,80.000000,840.000000,,165.581250,255.009375,3.675000,0.294667,Y
|
||||
31.206070,3,80.000000,840.000000,,165.581250,255.075000,3.712500,0.294718,Y
|
||||
31.315251,3,80.000000,840.000000,,165.562500,255.084375,3.700000,0.294675,Y
|
||||
31.408919,3,80.000000,840.000000,,165.581250,255.253125,3.700000,0.294536,Y
|
||||
31.502643,3,80.000000,840.000000,,165.562500,255.187500,3.675000,0.294520,Y
|
||||
31.611827,3,80.000000,840.000000,,165.778125,255.234375,3.712500,0.294586,Y
|
||||
31.705115,4,0.000000,1000.000000,,165.759375,255.403125,2.950000,0.292309,Y
|
||||
31.815795,4,0.000000,1000.000000,,165.309375,255.234375,0.637500,0.285962,Y
|
||||
31.908992,4,0.000000,1000.000000,,160.434375,257.400000,0.137500,0.282842,Y
|
||||
32.003128,4,0.000000,1000.000000,,144.731250,264.637500,-0.412500,0.275745,Y
|
||||
32.113105,4,0.000000,1000.000000,,113.156250,264.750000,-0.412500,0.275661,Y
|
||||
32.205667,4,0.000000,1000.000000,,77.878125,301.978125,-0.437500,0.250153,Y
|
||||
32.313669,4,0.000000,1000.000000,,55.387500,306.000000,-0.437500,0.247683,Y
|
||||
32.406316,4,0.000000,1000.000000,,41.128125,306.000000,-0.437500,0.247683,Y
|
||||
32.500895,4,0.000000,1000.000000,,30.356250,306.000000,-0.412500,0.247744,Y
|
||||
32.611033,4,0.000000,1000.000000,,23.334375,306.000000,-0.437500,0.247683,Y
|
||||
32.704017,4,0.000000,1000.000000,,17.606250,306.000000,-0.437500,0.247683,Y
|
||||
32.812848,4,0.000000,1000.000000,,13.753125,306.000000,-0.412500,0.247744,Y
|
||||
32.906039,4,0.000000,1000.000000,,11.334375,306.000000,-0.437500,0.247683,Y
|
||||
33.014065,4,0.000000,1000.000000,,9.262500,306.000000,-0.412500,0.247744,Y
|
||||
33.107276,4,0.000000,1000.000000,,7.678125,306.000000,-0.437500,0.247683,Y
|
||||
33.202567,4,0.000000,1000.000000,,6.412500,306.000000,-0.437500,0.247683,Y
|
||||
33.311125,4,0.000000,1000.000000,,5.409375,306.000000,-0.412500,0.247744,Y
|
||||
33.404451,4,0.000000,1000.000000,,4.584375,306.000000,-0.412500,0.247744,Y
|
||||
33.513153,4,0.000000,1000.000000,,3.956250,306.000000,-0.412500,0.247744,Y
|
||||
33.608247,4,0.000000,1000.000000,,3.403125,306.000000,-0.412500,0.247744,Y
|
||||
33.701926,4,0.000000,1000.000000,,2.953125,306.000000,-0.437500,0.247683,Y
|
||||
33.810209,4,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
33.904562,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
34.012472,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
34.106641,4,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
34.214426,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
34.307228,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
34.401083,4,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
34.510004,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
34.604002,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
34.713273,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
34.808090,4,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
34.915344,4,0.000000,1000.000000,,0.928125,306.000000,-0.437500,0.247683,Y
|
||||
35.008131,4,0.000000,1000.000000,,0.928125,306.000000,-0.400000,0.247775,Y
|
||||
35.101293,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
35.208944,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
35.302712,4,0.000000,1000.000000,,0.909375,306.000000,-0.400000,0.247775,Y
|
||||
35.412249,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
35.505971,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
35.601904,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
35.709759,4,0.000000,1000.000000,,0.900000,306.000000,-0.437500,0.247683,Y
|
||||
35.803196,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
35.911358,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
36.006079,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
36.114989,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
36.209193,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
36.302809,4,0.000000,1000.000000,,0.900000,306.000000,-0.450000,0.247652,Y
|
||||
36.411002,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
36.505740,4,0.000000,1000.000000,,0.909375,306.000000,-0.437500,0.247683,Y
|
||||
36.613579,4,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
36.707051,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
36.801656,4,0.000000,1000.000000,,0.928125,306.000000,-0.412500,0.247744,Y
|
||||
36.909407,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.003064,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.111916,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.206246,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.315576,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.409364,4,0.000000,1000.000000,,0.900000,306.000000,-0.412500,0.247744,Y
|
||||
37.502178,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.610752,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.703607,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.814525,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
37.909563,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
38.002126,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
38.111686,4,0.000000,1000.000000,,0.909375,306.000000,-0.400000,0.247775,Y
|
||||
38.205738,4,0.000000,1000.000000,,0.900000,306.000000,-0.437500,0.247683,Y
|
||||
38.314242,4,0.000000,1000.000000,,0.900000,306.000000,-0.412500,0.247744,Y
|
||||
38.407691,4,0.000000,1000.000000,,0.909375,306.000000,-0.400000,0.247775,Y
|
||||
38.500235,4,0.000000,1000.000000,,0.909375,306.000000,-0.400000,0.247775,Y
|
||||
38.610146,4,0.000000,1000.000000,,0.900000,306.000000,-0.437500,0.247683,Y
|
||||
38.702971,4,0.000000,1000.000000,,0.900000,306.000000,-0.412500,0.247744,Y
|
||||
38.811219,4,0.000000,1000.000000,,0.909375,306.000000,-0.412500,0.247744,Y
|
||||
38.905610,5,100.000000,800.000000,,0.909375,306.000000,-0.112500,0.248481,Y
|
||||
39.015748,5,100.000000,800.000000,,0.909375,306.000000,0.075000,0.248941,Y
|
||||
39.109532,5,100.000000,800.000000,,5.400000,306.000000,1.600000,0.252685,Y
|
||||
39.202577,5,100.000000,800.000000,,5.400000,306.000000,5.412500,0.262045,Y
|
||||
39.310497,5,100.000000,800.000000,,16.481250,279.075000,5.212500,0.280067,Y
|
||||
39.404330,5,100.000000,800.000000,,16.481250,278.034375,5.600000,0.281857,Y
|
||||
39.513598,5,100.000000,800.000000,,36.431250,243.825000,6.050000,0.311097,Y
|
||||
39.608196,5,100.000000,800.000000,,36.431250,213.206250,6.400000,0.342494,Y
|
||||
39.702918,5,100.000000,800.000000,,56.859375,212.737500,6.612500,0.343682,Y
|
||||
39.810545,5,100.000000,800.000000,,56.859375,194.812500,6.975000,0.365708,Y
|
||||
39.905377,5,100.000000,800.000000,,79.687500,192.928125,7.075000,0.368390,Y
|
||||
40.015097,5,100.000000,800.000000,,79.687500,192.928125,6.737500,0.367243,Y
|
||||
40.109192,5,100.000000,800.000000,,102.628125,211.434375,6.412500,0.344474,Y
|
||||
40.202710,5,100.000000,800.000000,,102.637500,211.725000,6.037500,0.342956,Y
|
||||
40.311427,5,100.000000,800.000000,,123.703125,211.987500,5.800000,0.341911,Y
|
||||
40.406848,5,100.000000,800.000000,,123.703125,212.062500,5.550000,0.341031,Y
|
||||
40.514985,5,100.000000,800.000000,,139.359375,212.081250,5.350000,0.340373,Y
|
||||
40.609114,5,100.000000,800.000000,,139.359375,197.737500,5.212500,0.356238,Y
|
||||
40.702818,5,100.000000,800.000000,,150.534375,196.912500,5.175000,0.357098,Y
|
||||
40.813392,5,100.000000,800.000000,,150.553125,192.937500,5.050000,0.361497,Y
|
||||
40.908049,5,100.000000,800.000000,,158.887500,183.206250,4.975000,0.373597,Y
|
||||
41.001688,5,100.000000,800.000000,,165.281250,183.037500,4.937500,0.373687,Y
|
||||
41.111187,5,100.000000,800.000000,,170.081250,176.175000,4.875000,0.382703,Y
|
||||
41.205374,5,100.000000,800.000000,,170.081250,175.106250,4.850000,0.384092,Y
|
||||
41.312902,5,100.000000,800.000000,,174.562500,175.125000,4.837500,0.384021,Y
|
||||
41.406341,5,100.000000,800.000000,,174.562500,170.859375,4.850000,0.390085,Y
|
||||
41.501019,5,100.000000,800.000000,,178.078125,170.737500,4.775000,0.389984,Y
|
||||
41.609511,5,100.000000,800.000000,,178.078125,169.425000,4.775000,0.391874,Y
|
||||
41.703264,5,100.000000,800.000000,,180.581250,168.262500,4.737500,0.393425,Y
|
||||
41.812361,5,100.000000,800.000000,,180.581250,168.300000,4.750000,0.393417,Y
|
||||
41.907992,5,100.000000,800.000000,,182.400000,166.837500,4.712500,0.395423,Y
|
||||
42.002081,5,100.000000,800.000000,,182.400000,166.781250,4.712500,0.395506,Y
|
||||
42.114026,5,100.000000,800.000000,,183.562500,166.800000,4.700000,0.395431,Y
|
||||
42.207692,5,100.000000,800.000000,,183.553125,165.909375,4.700000,0.396749,Y
|
||||
42.316148,5,100.000000,800.000000,,184.828125,165.862500,4.675000,0.396725,Y
|
||||
42.408950,5,100.000000,800.000000,,184.828125,165.384375,4.700000,0.397530,Y
|
||||
42.502405,5,100.000000,800.000000,,185.634375,165.328125,4.675000,0.397520,Y
|
||||
42.610536,5,100.000000,800.000000,,185.634375,165.328125,4.650000,0.397426,Y
|
||||
42.702625,5,100.000000,800.000000,,186.459375,164.859375,4.700000,0.398314,Y
|
||||
42.810789,5,100.000000,800.000000,,186.459375,164.859375,4.650000,0.398126,Y
|
||||
42.903012,5,100.000000,800.000000,,186.825000,164.850000,4.650000,0.398140,Y
|
||||
43.011615,5,100.000000,800.000000,,186.825000,164.662500,4.712500,0.398656,Y
|
||||
43.105389,5,100.000000,800.000000,,187.003125,164.578125,4.700000,0.398735,Y
|
||||
43.215291,5,100.000000,800.000000,,187.012500,164.512500,4.650000,0.398646,Y
|
||||
43.308404,5,100.000000,800.000000,,187.387500,164.362500,4.675000,0.398965,Y
|
||||
43.415343,5,100.000000,800.000000,,187.378125,164.484375,4.675000,0.398782,Y
|
||||
43.508002,5,100.000000,800.000000,,187.575000,164.409375,4.650000,0.398800,Y
|
||||
43.602971,5,100.000000,800.000000,,187.753125,164.400000,4.675000,0.398909,Y
|
||||
43.712418,5,100.000000,800.000000,,187.781250,164.550000,4.700000,0.398778,Y
|
||||
43.805036,5,100.000000,800.000000,,187.950000,164.512500,4.700000,0.398834,Y
|
||||
43.914587,5,100.000000,800.000000,,188.053125,164.559375,4.675000,0.398670,Y
|
||||
44.006794,5,100.000000,800.000000,,188.053125,164.625000,4.675000,0.398571,Y
|
||||
44.114919,5,100.000000,800.000000,,188.156250,164.606250,4.700000,0.398693,Y
|
||||
44.208490,5,100.000000,800.000000,,188.137500,164.578125,4.675000,0.398641,Y
|
||||
44.302467,5,100.000000,800.000000,,188.156250,164.606250,4.700000,0.398693,Y
|
||||
44.411331,5,100.000000,800.000000,,188.137500,164.653125,4.650000,0.398435,Y
|
||||
44.505121,5,100.000000,800.000000,,188.334375,164.550000,4.700000,0.398778,Y
|
||||
44.614528,5,100.000000,800.000000,,188.306250,164.578125,4.700000,0.398735,Y
|
||||
44.705887,5,100.000000,800.000000,,188.334375,164.559375,4.675000,0.398670,Y
|
||||
44.800613,5,100.000000,800.000000,,188.325000,164.550000,4.700000,0.398778,Y
|
||||
44.910433,5,100.000000,800.000000,,188.334375,164.578125,4.737500,0.398876,Y
|
||||
45.003392,5,100.000000,800.000000,,188.334375,164.587500,4.675000,0.398627,Y
|
||||
45.114573,5,100.000000,800.000000,,188.437500,164.634375,4.675000,0.398557,Y
|
||||
45.206638,5,100.000000,800.000000,,188.428125,164.625000,4.700000,0.398665,Y
|
||||
45.314430,5,100.000000,800.000000,,188.428125,164.456250,4.675000,0.398824,Y
|
||||
45.407853,5,100.000000,800.000000,,188.428125,164.475000,4.700000,0.398890,Y
|
||||
45.501768,5,100.000000,800.000000,,188.512500,164.456250,4.675000,0.398824,Y
|
||||
45.612725,5,100.000000,800.000000,,188.512500,164.428125,4.700000,0.398961,Y
|
||||
45.705491,5,100.000000,800.000000,,188.428125,164.381250,4.700000,0.399031,Y
|
||||
45.814080,5,100.000000,800.000000,,188.428125,164.456250,4.675000,0.398824,Y
|
||||
45.907639,5,100.000000,800.000000,,188.428125,164.503125,4.650000,0.398660,Y
|
||||
46.015027,5,100.000000,800.000000,,188.428125,164.456250,4.650000,0.398730,Y
|
||||
46.107939,5,100.000000,800.000000,,188.512500,164.437500,4.675000,0.398852,Y
|
||||
46.202424,5,100.000000,800.000000,,188.531250,164.475000,4.650000,0.398702,Y
|
||||
46.313403,5,100.000000,800.000000,,188.334375,164.484375,4.675000,0.398782,Y
|
||||
46.407104,5,100.000000,800.000000,,188.334375,164.353125,4.700000,0.399073,Y
|
||||
46.500578,5,100.000000,800.000000,,188.409375,164.325000,4.700000,0.399115,Y
|
||||
46.610702,5,100.000000,800.000000,,188.428125,164.437500,4.675000,0.398852,Y
|
||||
46.703466,5,100.000000,800.000000,,188.531250,164.334375,4.675000,0.399007,Y
|
||||
46.813803,5,100.000000,800.000000,,188.531250,164.353125,4.700000,0.399073,Y
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
+34
-5
@@ -33,14 +33,42 @@
|
||||
2. 核对 PLC 地址、端口和站号。默认值为 192.168.1.88、502、1。
|
||||
3. 默认模型显示为“内置 valve_model_004.json(80 点)”。
|
||||
4. 点击“连接设备”。连接成功后程序先设置 100% 开度,并进入传感器监测。
|
||||
5. 输入 0~300 SLM 的目标值,点击“设置目标流量”或按回车。首次设置即启动
|
||||
10 Hz 闭环;运行中可以继续修改目标。
|
||||
5. 输入 0~300 SLM 的目标值,点击“设置目标流量”或按回车。闭环运行中仍可输入
|
||||
新目标并再次点击,以直接切换目标。独立的“停止流量控制”按钮位于“设置目标
|
||||
流量”和“当前目标”之间;点击后程序停止闭环并立即下发 100% 总开度。
|
||||
6. 观察当前流量、阀前压力、阀后压力、下发总开度、电机行程、前馈开度、
|
||||
PI 修正量和 FAR/NEAR 模式。
|
||||
7. 结束时点击“断开设备”,等待状态变为“设备已断开”后再关闭程序。
|
||||
|
||||
手动给定开度:在闭环和开环扫点均停止、设备处于监测状态时,在“手动开度”
|
||||
输入 0~100%,再点击“设置开度”。闭环运行中点击该按钮只会提示先停止流量
|
||||
控制;关闭提示后不会停止闭环,也不会下发开度。手动下发和仅监测状态下 PID
|
||||
均未参与控制,因此“PID 模式”统一显示为“未启用”。
|
||||
|
||||
四、选择其他 A_eff 模型
|
||||
|
||||
四、开环扫点
|
||||
|
||||
1. 先连接设备并确认传感器数据正常,再点击“开环扫点”。
|
||||
2. 在弹窗中设置起止开度、开度间隔、随机种子、采样周期、稳态窗口、标准差
|
||||
阈值、单点最长等待时间,并选择是否追加闭阀端精细行程点。
|
||||
3. 点击“开始扫点”。若闭环正在运行,程序会先要求确认并停止闭环,再直接按
|
||||
扫描序列下发阀门开度。
|
||||
4. 每个扫描点在阀后流量达到稳态或到达最长等待时间后自动切换。界面会持续
|
||||
显示当前点数、流量、压力、总开度和电机行程。
|
||||
5. 扫点启动后按钮变为“停止扫点”,点击可提前停止。完成或停止后,程序尝试恢复
|
||||
100% 开度,设备保持连接并回到监测状态。
|
||||
|
||||
结果保存在 FlowControl.exe 同目录的 open_loop_data 文件夹。CSV 在采样时逐行
|
||||
写入,因此中途停止或故障时仍保留已采数据;结束时还会生成包含压力、流量和
|
||||
绝对压力比的 PNG。通信或安全故障会按安全策略断开设备。
|
||||
|
||||
CSV 是原始实验数据,并不是前馈模型,所以扫点结束时不会自动出现 JSON。
|
||||
需要前馈表时点击设备区域的“拟合前馈表”,可按住 Ctrl 或 Shift 一次选择多份
|
||||
开环 CSV,再选择 JSON 保存位置。程序会合并数据,输出可通过“选择 JSON…”
|
||||
加载的阀模型,并在同目录生成同名诊断 PNG。闭环或开环扫点运行时不能启动拟合。
|
||||
|
||||
|
||||
五、选择其他 A_eff 模型
|
||||
|
||||
只能在未连接或故障已断开的状态下切换模型。点击“选择 JSON…”后选择由本工程
|
||||
辨识工具生成的模型。程序会检查 JSON 结构、有效数值、标定点数量以及行程范围
|
||||
@@ -50,10 +78,11 @@
|
||||
A_eff 存在多个反解行程时,沿用工程算法选择更接近关闭端的较大行程。
|
||||
|
||||
|
||||
五、状态与故障
|
||||
六、状态与故障
|
||||
|
||||
- 已连接 / 监测中:设备已连接,阀门保持 100% 开度,尚未启动闭环。
|
||||
- 闭环运行中:目标流量已经生效,程序以 10 Hz 读取传感器并下发总开度。
|
||||
- 开环扫点中:程序按扫描序列直接下发开度,闭环反馈此时不参与控制。
|
||||
- 故障 / 已断开:出现流量越界、压力超限、连续读取失败、写入失败、控制周期
|
||||
严重超时或通信异常;程序已停止闭环并执行安全断开。
|
||||
|
||||
@@ -61,7 +90,7 @@ A_eff 存在多个反解行程时,沿用工程算法选择更接近关闭端
|
||||
保留该日志。日志最多约 2 MB,自动保留 3 份历史文件。
|
||||
|
||||
|
||||
六、当前默认硬件参数
|
||||
七、当前默认硬件参数
|
||||
|
||||
- 阀后流量:D700,量程 0~300 SLM
|
||||
- 阀后压力:D710,量程 0~400 kPa
|
||||
|
||||
Reference in New Issue
Block a user