新增气体流量控制 GUI 与 Windows 可执行程序
新增 Tkinter 单窗口操作界面和后台设备状态机,支持连接与断开 PLC、在线设置目标流量、实时显示传感器数据与下发总开度,并在监测及闭环阶段统一执行安全保护。 默认将 valve_model_004.json 内置到单文件 EXE,同时支持校验并选择外部阀模型;整理标定 JSON/PNG 到 valve_model 目录并同步配置路径。补充运行层离线测试、PyInstaller 构建配置、交付文件和完整的 README 使用及重新打包说明。
@@ -1,21 +1,23 @@
|
||||
# 气体流量控制与阀门开环标定
|
||||
|
||||
本项目基于信捷 **Easy521 PLC**(Modbus TCP)提供三个可独立运行的在线入口和两个离线工具:
|
||||
本项目基于信捷 **Easy521 PLC**(Modbus TCP)提供一个图形闭环入口、三个可独立运行的命令行在线入口和两个离线工具:
|
||||
|
||||
- [gui_main.py](gui_main.py):Windows 单窗口操作界面,支持连接/断开设备、设置目标流量、实时监视三路传感器与下发总开度,以及选择外部 `A_eff` 模型;
|
||||
- [main.py](main.py):以 10 Hz 周期运行单回路气体流量闭环控制,支持增量式 PID,并可选“前馈反解打底 + PI 修残差”与按阀前压力缩放增益的增益调度;
|
||||
- [open_loop.py](open_loop.py):随机扫描阀门开度,并在末尾追加闭阀端精细行程扫描;采集阀前/阀后的压力与阀后流量,用于开环标定、动态特性观察和临界压力比分析。
|
||||
- [test_valve.py](test_valve.py):在 Windows 终端中手动切换阀门开度,同时观察并记录阀后流量、阀前/阀后压力和电机行程。
|
||||
- [identify_valve.py](identify_valve.py):离线从开环扫点 CSV 拟合阀特性表,产出供前馈使用的 `valve_model.json`。
|
||||
- [test_valve_model.py](test_valve_model.py):阀特性模型与前馈反解的离线自测,不接触硬件。
|
||||
|
||||
三个在线入口都会持续保存 CSV,并在运行结束时为有效采样数据生成 PNG 曲线图。闭环程序还会持续进行滑动窗口稳态判定,并在首次达到稳态时输出一次控制性能报告。
|
||||
三个命令行在线入口都会持续保存 CSV,并在运行结束时为有效采样数据生成 PNG 曲线图。命令行闭环程序还会持续进行滑动窗口稳态判定,并在首次达到稳态时输出一次控制性能报告。GUI 保持轻量,只写轮转运行日志,不生成 CSV、PNG 或性能报告。
|
||||
|
||||
## 重要安全说明
|
||||
|
||||
> 本项目会直接控制真实阀门。首次运行前必须核对 IP、站号、寄存器地址、传感器量程、阀门行程方向、压力上限和执行机构的断电/失驱位置,并准备独立的机械或电气急停手段。
|
||||
|
||||
三个入口当前的阀门初始化与退出策略如下:
|
||||
四个在线入口当前的阀门初始化与退出策略如下:
|
||||
|
||||
- **图形界面 `gui_main.py` / `FlowControl.exe`**:启动后不会自动连接;连接成功后先主动设置 `100%` 开度,再进入传感器监测,首次设置目标流量时启动闭环。点击断开、关闭窗口或发生故障时,会再次尝试设置 `100%` 开度,然后断开 PLC;
|
||||
- **闭环 `main.py`**:连接成功后先主动设置 `100%` 开度,再开始初始采样、性能报告和控制计时;故障、正常退出或 `Ctrl+C` 时再次尝试设置 `100%` 开度,然后断开 PLC。
|
||||
- **开环 `open_loop.py`**:连接成功后先主动设置 `100%` 开度,再创建记录器并开始实验计时;实验结束、异常或 `Ctrl+C` 时再次尝试设置 `100%` 开度,然后断开设备。
|
||||
- **手动测试 `test_valve.py`**:连接成功后先主动设置 `100%` 开度,再创建记录器并开始采样;退出时再次尝试设置 `100%` 开度,然后断开设备。该脚本不执行流量/压力越限和连续读取失败保护,只适合在具备独立安全措施的受控测试中使用。
|
||||
@@ -57,18 +59,48 @@ opening = 100% -> motor_position = 800(当前打开端/机械死区边界)
|
||||
项目使用 Python 3,直接依赖:
|
||||
|
||||
- `pymodbus`:Modbus TCP/RTU 通讯;
|
||||
- `matplotlib`:使用 `Agg` 后端生成 PNG,无显示器环境也可运行。
|
||||
- `matplotlib`:命令行入口使用 `Agg` 后端生成 PNG,无显示器环境也可运行;
|
||||
- `tkinter`:GUI 使用的 Python 标准库组件;制作 EXE 时另需 `PyInstaller`。
|
||||
|
||||
示例安装:
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
python -m pip install pymodbus matplotlib
|
||||
python -m pip install pymodbus matplotlib pyinstaller
|
||||
```
|
||||
|
||||
仓库当前没有锁定依赖版本。若 `PcControl.py` 导入 `pymodbus.payload` 或调用 `slave=` 参数时报错,请使用与现有 [PcControl.py](PcControl.py) API 兼容的 `pymodbus` 版本。
|
||||
|
||||
## 图形界面与 Windows 可执行程序
|
||||
|
||||
交付目录为 `可执行程序/`,包含以下三个文件:
|
||||
|
||||
- `FlowControl.exe`:Windows 64 位单文件程序,无需安装 Python,默认内置 `valve_model_004.json`;
|
||||
- `valve_model_004.json`:内置模型的外置备份和自定义模型格式参考,删除该外置副本不会影响内置模型;
|
||||
- `使用说明.txt`:现场操作、安全策略和故障说明。
|
||||
|
||||
双击 `FlowControl.exe` 后,核对 PLC 地址、端口和站号,再点击“连接设备”。连接成功后程序保持 `100%` 开度并以 `0.25 s` 周期监测阀后流量、阀前压力和阀后压力;首次设置 `0~300 SLM` 目标时启动 10 Hz 闭环,运行中可继续修改目标。界面显示的“下发总开度”为最终命令:
|
||||
|
||||
```text
|
||||
总开度 = clamp(前馈开度 + PI 修正量, 0, 100)
|
||||
```
|
||||
|
||||
GUI 的 Modbus 读写与控制周期全部在单一后台线程中串行执行,避免通信超时阻塞界面。监测和闭环阶段都会执行流量/压力越限保护;连续读取失败、写入失败、控制周期严重超时或其他通信异常会停止闭环,尝试恢复 `100%` 开度并断开设备。运行日志写到 EXE 同目录的 `logs/flow_control.log`,单文件最大约 2 MB,保留 3 份历史文件。
|
||||
|
||||
默认模型只能在未连接状态下切换。点击“选择 JSON…”可加载其他模型,程序会在连接前检查 JSON 结构、有限数值、标定点数量、`800~1000` 行程范围以及 `motor_open` / `motor_closed` 与当前配置是否一致;无效模型不会进入控制。点击“使用内置”可恢复 `valve_model_004.json`。
|
||||
|
||||
源码运行与重新打包命令为:
|
||||
|
||||
```powershell
|
||||
python gui_main.py
|
||||
python gui_main.py --smoke-test
|
||||
python -m PyInstaller --noconfirm --clean --distpath "可执行程序" --workpath "build/flow_control_gui" flow_control_gui.spec
|
||||
Copy-Item valve_model/valve_model_004.json 可执行程序/valve_model_004.json -Force
|
||||
```
|
||||
|
||||
`--smoke-test` 只检查内置模型和关键依赖,不打开界面,也不会连接真实设备。
|
||||
|
||||
## 闭环控制
|
||||
|
||||
### 运行
|
||||
@@ -129,13 +161,13 @@ opening_to_motor_position()
|
||||
| 模式 | 用途 | Kp / Ki / Kd | 最大开度速度 |
|
||||
| --- | --- | --- | ---: |
|
||||
| `FAR` | 大误差时快速接近目标 | `0.5 / 0.5 / 0.0` | `200%/s` |
|
||||
| `NEAR` | 小误差时抑制振荡和稳态误差 | `0.05 / 0.01 / 0.0` | `2%/s` |
|
||||
| `NEAR` | 小误差时抑制振荡和稳态误差 | `0.1 / 0.1 / 0.0` | `2%/s` |
|
||||
|
||||
切换阈值同时考虑绝对误差下限和目标流量比例:
|
||||
|
||||
```text
|
||||
near_threshold = min(3 SLM, max(1 SLM, 5% * target))
|
||||
far_threshold = min(12 SLM, max(4 SLM, 20% * target))
|
||||
near_threshold = min(2 SLM, max(1 SLM, 5% * target))
|
||||
far_threshold = min(8 SLM, max(4 SLM, 20% * target))
|
||||
|
||||
FAR -> NEAR:|error| <= near_threshold,连续满足 10 个采样周期
|
||||
NEAR -> FAR :|error| >= far_threshold,连续满足 10 个采样周期
|
||||
@@ -147,7 +179,7 @@ NEAR -> FAR :|error| >= far_threshold,连续满足 10 个采样周期
|
||||
|
||||
### 前馈 + 增益调度(MFC 结构)
|
||||
|
||||
闭环当前默认启用“前馈反解打底 + PI 修残差”的 MFC 结构,并加载仓库根目录的 `valve_model.json`;按阀前压力缩放 Kp/Ki 的增益调度仍默认关闭。投用前必须确认阀前/阀后压力接线和阀特性表与当前硬件一致;不满足时应关闭前馈或重新辨识。
|
||||
闭环当前默认启用“前馈反解打底 + PI 修残差”的 MFC 结构,并加载 `valve_model/valve_model_004.json`;按阀前压力缩放 Kp/Ki 的增益调度仍默认关闭。投用前必须确认阀前/阀后压力接线和阀特性表与当前硬件一致;不满足时应关闭前馈或重新辨识。
|
||||
|
||||
静态阀特性建模为:
|
||||
|
||||
@@ -170,7 +202,7 @@ PI 修正量 correction = PI(目标流量 - 实测流量) # 输出限幅改为
|
||||
|
||||
即前馈把开度送到稳态工作点附近,PI 只负责修正模型残差和动态偏差;PI 输出不再是 `0~100%` 的全行程,而是围绕前馈的修正量(限幅由 `FEEDFORWARD_CORRECTION_BAND_PCT` 控制,当前默认 ±50%)。
|
||||
|
||||
为避免接近目标后压力噪声持续改写前馈,同时又能响应下游负载变化,控制器维护一个 `FEEDFORWARD_UPDATE_WINDOW_S=3 s` 的流量绝对误差滑动窗口。窗口首次积满后,程序在每个控制周期都重新计算最近 3 秒的平均绝对误差,并与 `FEEDFORWARD_UPDATE_ERROR_THRESHOLD_SLM=5 SLM` 比较:均值不大于阈值时冻结上一次前馈开度,只让 PI correction 逐周期更新;均值超过阈值时重新根据当前目标流量和阀前/阀后压力反解前馈。因此,下游扰动持续影响流量并使 3 秒误差均值越过阈值后,前馈会自动解除冻结。
|
||||
为避免接近目标后压力噪声持续改写前馈,同时又能响应下游负载变化,控制器维护一个 `FEEDFORWARD_UPDATE_WINDOW_S=3 s` 的流量绝对误差滑动窗口。窗口首次积满后,程序在每个控制周期都重新计算最近 3 秒的平均绝对误差,并与 `FEEDFORWARD_UPDATE_ERROR_THRESHOLD_SLM=2 SLM` 比较:均值不大于阈值时冻结上一次前馈开度,只让 PI correction 逐周期更新;均值超过阈值时重新根据当前目标流量和阀前/阀后压力反解前馈。因此,下游扰动持续影响流量并使 3 秒误差均值越过阈值后,前馈会自动解除冻结。
|
||||
|
||||
窗口未首次积满或尚无缓存前馈时,每个周期都尝试反解前馈。窗口一旦积满,其“已就绪”状态不会因采样抖动和旧样本淘汰而反复失效;但滑动均值仍会持续更新,冻结状态并不会锁死。目标改变、控制器重启或进入零流量分支会清空窗口并重新累计 3 秒。当前周期反解失败时继续复用上次有效前馈;如果从未得到有效前馈,则以 `0%` 为基值并由 correction 调整。
|
||||
|
||||
@@ -189,7 +221,7 @@ Kp_eff = scale · Kp_base,Ki_eff = scale · Ki_base # Kd 不调度
|
||||
| --- | --- | --- |
|
||||
| `FEEDFORWARD_ENABLED` | `True` | 启用前馈反解打底 + PI 修残差 |
|
||||
| `GAIN_SCHEDULE_ENABLED` | `False` | 启用按阀前压力缩放 Kp/Ki |
|
||||
| `VALVE_MODEL_PATH` | `"valve_model.json"` | 阀特性表 JSON 路径(空串 = 关闭前馈) |
|
||||
| `VALVE_MODEL_PATH` | `"valve_model/valve_model_004.json"` | 阀特性表 JSON 路径(空串 = 关闭前馈) |
|
||||
| `PRESSURE_BEFORE_ADDR` | `720` | 阀前压力 P1 的寄存器地址(D720) |
|
||||
|
||||
前馈生效需同时满足:`FEEDFORWARD_ENABLED=True`、`VALVE_MODEL_PATH` 指向有效 JSON、`PRESSURE_BEFORE_ADDR` 与 `PRESSURE_AFTER_ADDR` 均已配置。任一条件缺失时自动退化为纯 PID,并在启动日志中提示。
|
||||
@@ -298,9 +330,9 @@ python test_valve.py
|
||||
前馈需要一张 `A_eff(x)` 阀特性表。先用 [open_loop.py](open_loop.py) 扫点得到开环 CSV(时间戳命名、从不覆盖),再用 [identify_valve.py](identify_valve.py) 离线拟合。`--csv` 支持多个路径与 glob 模式,可把多次实验合并重拟合,数据越多表越稳:
|
||||
|
||||
```powershell
|
||||
python identify_valve.py --csv open_loop_data/open_loop_YYYYMMDD_HHMMSS.csv --out valve_model.json
|
||||
python identify_valve.py --csv "open_loop_data/*.csv" --out valve_model.json --plot
|
||||
python identify_valve.py --plot # 省略 --csv 时默认合并 open_loop_data/ 下全部 CSV
|
||||
python identify_valve.py --csv open_loop_data/open_loop_YYYYMMDD_HHMMSS.csv --out valve_model/valve_model.json
|
||||
python identify_valve.py --csv "open_loop_data/*.csv" --out valve_model/valve_model.json --plot
|
||||
python identify_valve.py --plot --out valve_model/valve_model.json # 默认合并 open_loop_data/ 下全部 CSV
|
||||
```
|
||||
|
||||
- 每个 CSV 按 `step_index` 分组,每组取末尾 `--tail` 个采样点平均,得到稳态 `(行程 x, 流量 Q, 阀前压 P1, 阀后压 P2)`,并按开度访问顺序标记逼近方向(上升 / 下降 / 该文件起始步);
|
||||
@@ -310,7 +342,7 @@ python identify_valve.py --plot # 省略 --csv 时默认合并 open_loop_da
|
||||
|
||||
非单调表可能让同一个有效面积对应多个行程,反解时选择数值更大的行程,即更靠近关闭端的交点。目标有效面积小于表中最小实测值时直接返回机械全关行程 `MOTOR_CLOSED_POSITION`;恰好等于最小值时仍返回对应实测行程。
|
||||
|
||||
将 `config.VALVE_MODEL_PATH` 指到该 JSON、确认 `PRESSURE_BEFORE_ADDR` 接线无误、并把 `FEEDFORWARD_ENABLED` 设为 `True` 后,闭环即可启用前馈。当前配置已指向 `valve_model.json` 并默认启用,重新标定后应覆盖该文件或同步修改配置路径。
|
||||
将 `config.VALVE_MODEL_PATH` 指到该 JSON、确认 `PRESSURE_BEFORE_ADDR` 接线无误、并把 `FEEDFORWARD_ENABLED` 设为 `True` 后,闭环即可启用前馈。当前配置已指向 `valve_model/valve_model_004.json` 并默认启用;重新标定后应覆盖该文件或同步修改配置路径。
|
||||
|
||||
[test_valve_model.py](test_valve_model.py) 是纯计算离线自测,不接触硬件,可在任意环境运行:
|
||||
|
||||
@@ -364,19 +396,19 @@ python -m pytest -q
|
||||
| `TARGET_SWITCH_KEY` | `s` | 运行中按此键进入"输入新目标流量"模式 |
|
||||
| `CONTROL_PERIOD_S` / `MAX_CONTROL_DT_S` | `0.1` / `0.3` | 目标周期 / 超时阈值 |
|
||||
| `PID_FAR_KP` / `PID_FAR_KI` / `PID_FAR_KD` | `0.5` / `0.5` / `0.0` | 大误差快速模式 PID 参数 |
|
||||
| `PID_NEAR_KP` / `PID_NEAR_KI` / `PID_NEAR_KD` | `0.05` / `0.01` / `0.0` | 小误差稳定模式 PID 参数 |
|
||||
| `PID_NEAR_KP` / `PID_NEAR_KI` / `PID_NEAR_KD` | `0.1` / `0.1` / `0.0` | 小误差稳定模式 PID 参数 |
|
||||
| `OPENING_MIN_PCT` / `OPENING_MAX_PCT` | `0` / `100` | PID 输出开度范围 |
|
||||
| `MAX_OPENING_RATE_FAR_PCT_S` / `MAX_OPENING_RATE_NEAR_PCT_S` | `200` / `2` | FAR/NEAR 每秒最大开度变化百分比 |
|
||||
| `PID_NEAR_ERROR_MIN_SLM` / `PID_NEAR_ERROR_MAX_SLM` / `PID_NEAR_ERROR_TARGET_RATIO` | `1` / `3` / `0.05` | FAR 进入 NEAR 的误差阈值下限、上限和目标比例 |
|
||||
| `PID_FAR_ERROR_MIN_SLM` / `PID_FAR_ERROR_MAX_SLM` / `PID_FAR_ERROR_TARGET_RATIO` | `4` / `12` / `0.20` | NEAR 返回 FAR 的误差阈值下限、上限和目标比例 |
|
||||
| `PID_NEAR_ERROR_MIN_SLM` / `PID_NEAR_ERROR_MAX_SLM` / `PID_NEAR_ERROR_TARGET_RATIO` | `1` / `2` / `0.05` | FAR 进入 NEAR 的误差阈值下限、上限和目标比例 |
|
||||
| `PID_FAR_ERROR_MIN_SLM` / `PID_FAR_ERROR_MAX_SLM` / `PID_FAR_ERROR_TARGET_RATIO` | `4` / `8` / `0.20` | NEAR 返回 FAR 的误差阈值下限、上限和目标比例 |
|
||||
| `PID_MODE_SWITCH_CONFIRM_CYCLES` | `10` | 模式切换条件需连续满足的采样周期数(当前约 1 秒) |
|
||||
| `MOTOR_CLOSED_POSITION` / `MOTOR_OPEN_POSITION` | `1000` / `800` | 当前反向控制区间;打开端为机械死区边界 |
|
||||
| `INITIAL_OPENING_PCT` | `100` | 闭环 PID 内部初始输出 |
|
||||
| `PRESSURE_BEFORE_ADDR` | `720` | 阀前压力 P1 的寄存器地址(前馈/增益调度/开环共用) |
|
||||
| `VALVE_MODEL_PATH` | `"valve_model.json"` | 阀特性表 JSON 路径;空串关闭前馈 |
|
||||
| `VALVE_MODEL_PATH` | `"valve_model/valve_model_004.json"` | 阀特性表 JSON 路径;空串关闭前馈 |
|
||||
| `FEEDFORWARD_ENABLED` | `True` | 是否启用前馈反解打底 + PI 修残差 |
|
||||
| `FEEDFORWARD_CORRECTION_BAND_PCT` | `50` | 前馈开启时 PI 修正量的对称限幅(±%) |
|
||||
| `FEEDFORWARD_UPDATE_ERROR_THRESHOLD_SLM` / `FEEDFORWARD_UPDATE_WINDOW_S` | `5` / `3` | 前馈冻结/更新的流量绝对误差均值阈值与窗口长度 |
|
||||
| `FEEDFORWARD_UPDATE_ERROR_THRESHOLD_SLM` / `FEEDFORWARD_UPDATE_WINDOW_S` | `2` / `3` | 前馈冻结/更新的流量绝对误差均值阈值与窗口长度 |
|
||||
| `GAIN_SCHEDULE_ENABLED` | `False` | 是否启用按阀前压力缩放 Kp/Ki |
|
||||
| `GAIN_SCHEDULE_REF_ABS_KPA` | `501.325` | 满压调好基准增益对应的绝对压力(400 + 101.325) |
|
||||
| `GAIN_SCHEDULE_FLOOR_ABS_KPA` | `60` | 绝对压力下限,防止 P1 很低时增益爆炸 |
|
||||
@@ -394,6 +426,8 @@ python -m pytest -q
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| [gui_main.py](gui_main.py) | Tkinter 单窗口界面、用户输入校验、状态与实时数据展示 |
|
||||
| [control_runtime.py](control_runtime.py) | GUI 后台工作线程、模型校验、设备状态机与安全连接/断开 |
|
||||
| [main.py](main.py) | 闭环入口:参数解析、日志、控制循环、记录器、性能报告与收尾 |
|
||||
| [flow_control.py](flow_control.py) | 闭环控制层、传感器读取、开度/行程映射、前馈与增益调度、故障处理 |
|
||||
| [controllers.py](controllers.py) | 通用增量式 PID,含输出限幅和输出变化率限幅 |
|
||||
@@ -403,16 +437,21 @@ python -m pytest -q
|
||||
| [open_loop.py](open_loop.py) | 独立开环扫点、传感器记录和压力比分析 |
|
||||
| [test_valve.py](test_valve.py) | Windows 终端手动开度测试、每秒采样、CSV 与三联图 |
|
||||
| [valve_model.py](valve_model.py) | 静态阀特性模型 `Q_ss = A_eff(x)·P1_abs·F(r)` 与前馈反解(纯计算) |
|
||||
| [identify_valve.py](identify_valve.py) | 从一份或多份开环扫点 CSV(支持 glob 合并)拟合 `A_eff(x)` 表,输出 `valve_model.json` |
|
||||
| [identify_valve.py](identify_valve.py) | 从一份或多份开环扫点 CSV(支持 glob 合并)拟合 `A_eff(x)` 表,输出阀模型 JSON |
|
||||
| [test_valve_model.py](test_valve_model.py) | 阀特性模型与前馈反解的离线自测 |
|
||||
| [test_flow_control.py](test_flow_control.py) | 前馈误差滑窗、采样抖动与复位逻辑的离线测试 |
|
||||
| [test_performance_reporter.py](test_performance_reporter.py) | 闭环性能报告器与压力带判稳的离线测试 |
|
||||
| [PcControl.py](PcControl.py) | Modbus 硬件客户端;本项目三个入口实际使用 `Easy521ModbusClient` |
|
||||
| [test_control_runtime.py](test_control_runtime.py) | GUI 运行层、模型加载、连接、控制与安全断开的离线测试 |
|
||||
| [flow_control_gui.spec](flow_control_gui.spec) | PyInstaller 单文件 EXE 构建配置和内置模型资源声明 |
|
||||
| [PcControl.py](PcControl.py) | Modbus 硬件客户端;GUI 和三个命令行入口使用 `Easy521ModbusClient` |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
flow_control/
|
||||
├── gui_main.py # 图形界面入口
|
||||
├── control_runtime.py # GUI 后台设备与闭环运行层
|
||||
├── flow_control_gui.spec # Windows 单文件打包配置
|
||||
├── main.py
|
||||
├── open_loop.py
|
||||
├── test_valve.py
|
||||
@@ -426,11 +465,19 @@ flow_control/
|
||||
├── test_valve_model.py # 阀特性模型离线自测
|
||||
├── test_flow_control.py # 前馈误差滑窗离线测试
|
||||
├── test_performance_reporter.py # 闭环判稳逻辑离线测试
|
||||
├── test_control_runtime.py # GUI 运行层离线测试
|
||||
├── PcControl.py
|
||||
├── README.md
|
||||
├── valve_model/ # 标定 JSON 与诊断 PNG
|
||||
│ ├── valve_model.json
|
||||
│ ├── valve_model_001.json ... valve_model_004.json
|
||||
│ └── valve_model.png ... valve_model_004.png
|
||||
├── 可执行程序/
|
||||
│ ├── FlowControl.exe
|
||||
│ ├── valve_model_004.json
|
||||
│ └── 使用说明.txt
|
||||
├── data/ # 闭环 CSV / PNG;历史数据可能已人工分组
|
||||
├── open_loop_data/ # 开环 CSV / PNG(首次运行时创建)
|
||||
├── test_valve_data/ # 手动阀门测试 CSV / PNG
|
||||
├── valve_model.json # identify_valve.py 产出的阀特性表(供前馈使用)
|
||||
└── logs/ # flow_control.log / open_loop.log
|
||||
```
|
||||
|
||||
@@ -0,0 +1,829 @@
|
||||
('D:\\flow_control\\build\\flow_control_gui\\flow_control_gui\\PYZ-00.pyz',
|
||||
[('PcControl', 'D:\\flow_control\\PcControl.py', 'PYMODULE'),
|
||||
('__future__', 'D:\\Anaconda\\Lib\\__future__.py', 'PYMODULE'),
|
||||
('_colorize', 'D:\\Anaconda\\Lib\\_colorize.py', 'PYMODULE'),
|
||||
('_compat_pickle', 'D:\\Anaconda\\Lib\\_compat_pickle.py', 'PYMODULE'),
|
||||
('_compression', 'D:\\Anaconda\\Lib\\_compression.py', 'PYMODULE'),
|
||||
('_opcode_metadata', 'D:\\Anaconda\\Lib\\_opcode_metadata.py', 'PYMODULE'),
|
||||
('_py_abc', 'D:\\Anaconda\\Lib\\_py_abc.py', 'PYMODULE'),
|
||||
('_pydatetime', 'D:\\Anaconda\\Lib\\_pydatetime.py', 'PYMODULE'),
|
||||
('_pydecimal', 'D:\\Anaconda\\Lib\\_pydecimal.py', 'PYMODULE'),
|
||||
('_strptime', 'D:\\Anaconda\\Lib\\_strptime.py', 'PYMODULE'),
|
||||
('_threading_local', 'D:\\Anaconda\\Lib\\_threading_local.py', 'PYMODULE'),
|
||||
('aiohappyeyeballs',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohappyeyeballs\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('aiohappyeyeballs._staggered',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohappyeyeballs\\_staggered.py',
|
||||
'PYMODULE'),
|
||||
('aiohappyeyeballs.impl',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohappyeyeballs\\impl.py',
|
||||
'PYMODULE'),
|
||||
('aiohappyeyeballs.types',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohappyeyeballs\\types.py',
|
||||
'PYMODULE'),
|
||||
('aiohappyeyeballs.utils',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohappyeyeballs\\utils.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket.helpers',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\helpers.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket.models',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\models.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket.reader',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\reader.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket.reader_py',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\reader_py.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp._websocket.writer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\_websocket\\writer.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.abc',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\abc.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.base_protocol',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\base_protocol.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.client',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\client.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.client_exceptions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\client_exceptions.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.client_proto',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\client_proto.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.client_reqrep',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\client_reqrep.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.client_ws',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\client_ws.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.compression_utils',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\compression_utils.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.connector',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\connector.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.cookiejar',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\cookiejar.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.formdata',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\formdata.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.hdrs',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\hdrs.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.helpers',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\helpers.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.http',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\http.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.http_exceptions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\http_exceptions.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.http_parser',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\http_parser.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.http_websocket',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\http_websocket.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.http_writer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\http_writer.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.log',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\log.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.multipart',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\multipart.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.payload',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\payload.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.payload_streamer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\payload_streamer.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.resolver',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\resolver.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.streams',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\streams.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.tcp_helpers',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\tcp_helpers.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.tracing',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\tracing.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.typedefs',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\typedefs.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_app',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_app.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_exceptions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_exceptions.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_fileresponse',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_fileresponse.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_log',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_log.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_middlewares',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_middlewares.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_protocol',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_protocol.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_request',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_request.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_response',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_response.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_routedef',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_routedef.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_runner',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_runner.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_server',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_server.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_urldispatcher',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_urldispatcher.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.web_ws',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\web_ws.py',
|
||||
'PYMODULE'),
|
||||
('aiohttp.worker',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiohttp\\worker.py',
|
||||
'PYMODULE'),
|
||||
('aiosignal',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\aiosignal\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('argparse', 'D:\\Anaconda\\Lib\\argparse.py', 'PYMODULE'),
|
||||
('ast', 'D:\\Anaconda\\Lib\\ast.py', 'PYMODULE'),
|
||||
('asyncio', 'D:\\Anaconda\\Lib\\asyncio\\__init__.py', 'PYMODULE'),
|
||||
('asyncio.base_events',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\base_events.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.base_futures',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\base_futures.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.base_subprocess',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\base_subprocess.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.base_tasks',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\base_tasks.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.constants', 'D:\\Anaconda\\Lib\\asyncio\\constants.py', 'PYMODULE'),
|
||||
('asyncio.coroutines',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\coroutines.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.events', 'D:\\Anaconda\\Lib\\asyncio\\events.py', 'PYMODULE'),
|
||||
('asyncio.exceptions',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\exceptions.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.format_helpers',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\format_helpers.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.futures', 'D:\\Anaconda\\Lib\\asyncio\\futures.py', 'PYMODULE'),
|
||||
('asyncio.locks', 'D:\\Anaconda\\Lib\\asyncio\\locks.py', 'PYMODULE'),
|
||||
('asyncio.log', 'D:\\Anaconda\\Lib\\asyncio\\log.py', 'PYMODULE'),
|
||||
('asyncio.mixins', 'D:\\Anaconda\\Lib\\asyncio\\mixins.py', 'PYMODULE'),
|
||||
('asyncio.proactor_events',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\proactor_events.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.protocols', 'D:\\Anaconda\\Lib\\asyncio\\protocols.py', 'PYMODULE'),
|
||||
('asyncio.queues', 'D:\\Anaconda\\Lib\\asyncio\\queues.py', 'PYMODULE'),
|
||||
('asyncio.runners', 'D:\\Anaconda\\Lib\\asyncio\\runners.py', 'PYMODULE'),
|
||||
('asyncio.selector_events',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\selector_events.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.sslproto', 'D:\\Anaconda\\Lib\\asyncio\\sslproto.py', 'PYMODULE'),
|
||||
('asyncio.staggered', 'D:\\Anaconda\\Lib\\asyncio\\staggered.py', 'PYMODULE'),
|
||||
('asyncio.streams', 'D:\\Anaconda\\Lib\\asyncio\\streams.py', 'PYMODULE'),
|
||||
('asyncio.subprocess',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\subprocess.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.taskgroups',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\taskgroups.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.tasks', 'D:\\Anaconda\\Lib\\asyncio\\tasks.py', 'PYMODULE'),
|
||||
('asyncio.threads', 'D:\\Anaconda\\Lib\\asyncio\\threads.py', 'PYMODULE'),
|
||||
('asyncio.timeouts', 'D:\\Anaconda\\Lib\\asyncio\\timeouts.py', 'PYMODULE'),
|
||||
('asyncio.transports',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\transports.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.trsock', 'D:\\Anaconda\\Lib\\asyncio\\trsock.py', 'PYMODULE'),
|
||||
('asyncio.unix_events',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\unix_events.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.windows_events',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\windows_events.py',
|
||||
'PYMODULE'),
|
||||
('asyncio.windows_utils',
|
||||
'D:\\Anaconda\\Lib\\asyncio\\windows_utils.py',
|
||||
'PYMODULE'),
|
||||
('attr', 'D:\\Anaconda\\Lib\\site-packages\\attr\\__init__.py', 'PYMODULE'),
|
||||
('attr._cmp', 'D:\\Anaconda\\Lib\\site-packages\\attr\\_cmp.py', 'PYMODULE'),
|
||||
('attr._compat',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_compat.py',
|
||||
'PYMODULE'),
|
||||
('attr._config',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_config.py',
|
||||
'PYMODULE'),
|
||||
('attr._funcs',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_funcs.py',
|
||||
'PYMODULE'),
|
||||
('attr._make',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_make.py',
|
||||
'PYMODULE'),
|
||||
('attr._next_gen',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_next_gen.py',
|
||||
'PYMODULE'),
|
||||
('attr._version_info',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\_version_info.py',
|
||||
'PYMODULE'),
|
||||
('attr.converters',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\converters.py',
|
||||
'PYMODULE'),
|
||||
('attr.exceptions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\exceptions.py',
|
||||
'PYMODULE'),
|
||||
('attr.filters',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\filters.py',
|
||||
'PYMODULE'),
|
||||
('attr.setters',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\setters.py',
|
||||
'PYMODULE'),
|
||||
('attr.validators',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\attr\\validators.py',
|
||||
'PYMODULE'),
|
||||
('base64', 'D:\\Anaconda\\Lib\\base64.py', 'PYMODULE'),
|
||||
('bisect', 'D:\\Anaconda\\Lib\\bisect.py', 'PYMODULE'),
|
||||
('brotli', 'D:\\Anaconda\\Lib\\site-packages\\brotli.py', 'PYMODULE'),
|
||||
('bz2', 'D:\\Anaconda\\Lib\\bz2.py', 'PYMODULE'),
|
||||
('calendar', 'D:\\Anaconda\\Lib\\calendar.py', 'PYMODULE'),
|
||||
('concurrent', 'D:\\Anaconda\\Lib\\concurrent\\__init__.py', 'PYMODULE'),
|
||||
('concurrent.futures',
|
||||
'D:\\Anaconda\\Lib\\concurrent\\futures\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('concurrent.futures._base',
|
||||
'D:\\Anaconda\\Lib\\concurrent\\futures\\_base.py',
|
||||
'PYMODULE'),
|
||||
('concurrent.futures.process',
|
||||
'D:\\Anaconda\\Lib\\concurrent\\futures\\process.py',
|
||||
'PYMODULE'),
|
||||
('concurrent.futures.thread',
|
||||
'D:\\Anaconda\\Lib\\concurrent\\futures\\thread.py',
|
||||
'PYMODULE'),
|
||||
('config', 'D:\\flow_control\\config.py', 'PYMODULE'),
|
||||
('contextlib', 'D:\\Anaconda\\Lib\\contextlib.py', 'PYMODULE'),
|
||||
('contextvars', 'D:\\Anaconda\\Lib\\contextvars.py', 'PYMODULE'),
|
||||
('control_runtime', 'D:\\flow_control\\control_runtime.py', 'PYMODULE'),
|
||||
('controllers', 'D:\\flow_control\\controllers.py', 'PYMODULE'),
|
||||
('copy', 'D:\\Anaconda\\Lib\\copy.py', 'PYMODULE'),
|
||||
('csv', 'D:\\Anaconda\\Lib\\csv.py', 'PYMODULE'),
|
||||
('ctypes', 'D:\\Anaconda\\Lib\\ctypes\\__init__.py', 'PYMODULE'),
|
||||
('ctypes._endian', 'D:\\Anaconda\\Lib\\ctypes\\_endian.py', 'PYMODULE'),
|
||||
('ctypes.wintypes', 'D:\\Anaconda\\Lib\\ctypes\\wintypes.py', 'PYMODULE'),
|
||||
('dataclasses', 'D:\\Anaconda\\Lib\\dataclasses.py', 'PYMODULE'),
|
||||
('datetime', 'D:\\Anaconda\\Lib\\datetime.py', 'PYMODULE'),
|
||||
('decimal', 'D:\\Anaconda\\Lib\\decimal.py', 'PYMODULE'),
|
||||
('dis', 'D:\\Anaconda\\Lib\\dis.py', 'PYMODULE'),
|
||||
('email', 'D:\\Anaconda\\Lib\\email\\__init__.py', 'PYMODULE'),
|
||||
('email._encoded_words',
|
||||
'D:\\Anaconda\\Lib\\email\\_encoded_words.py',
|
||||
'PYMODULE'),
|
||||
('email._header_value_parser',
|
||||
'D:\\Anaconda\\Lib\\email\\_header_value_parser.py',
|
||||
'PYMODULE'),
|
||||
('email._parseaddr', 'D:\\Anaconda\\Lib\\email\\_parseaddr.py', 'PYMODULE'),
|
||||
('email._policybase', 'D:\\Anaconda\\Lib\\email\\_policybase.py', 'PYMODULE'),
|
||||
('email.base64mime', 'D:\\Anaconda\\Lib\\email\\base64mime.py', 'PYMODULE'),
|
||||
('email.charset', 'D:\\Anaconda\\Lib\\email\\charset.py', 'PYMODULE'),
|
||||
('email.contentmanager',
|
||||
'D:\\Anaconda\\Lib\\email\\contentmanager.py',
|
||||
'PYMODULE'),
|
||||
('email.encoders', 'D:\\Anaconda\\Lib\\email\\encoders.py', 'PYMODULE'),
|
||||
('email.errors', 'D:\\Anaconda\\Lib\\email\\errors.py', 'PYMODULE'),
|
||||
('email.feedparser', 'D:\\Anaconda\\Lib\\email\\feedparser.py', 'PYMODULE'),
|
||||
('email.generator', 'D:\\Anaconda\\Lib\\email\\generator.py', 'PYMODULE'),
|
||||
('email.header', 'D:\\Anaconda\\Lib\\email\\header.py', 'PYMODULE'),
|
||||
('email.headerregistry',
|
||||
'D:\\Anaconda\\Lib\\email\\headerregistry.py',
|
||||
'PYMODULE'),
|
||||
('email.iterators', 'D:\\Anaconda\\Lib\\email\\iterators.py', 'PYMODULE'),
|
||||
('email.message', 'D:\\Anaconda\\Lib\\email\\message.py', 'PYMODULE'),
|
||||
('email.parser', 'D:\\Anaconda\\Lib\\email\\parser.py', 'PYMODULE'),
|
||||
('email.policy', 'D:\\Anaconda\\Lib\\email\\policy.py', 'PYMODULE'),
|
||||
('email.quoprimime', 'D:\\Anaconda\\Lib\\email\\quoprimime.py', 'PYMODULE'),
|
||||
('email.utils', 'D:\\Anaconda\\Lib\\email\\utils.py', 'PYMODULE'),
|
||||
('flow_control', 'D:\\flow_control\\flow_control.py', 'PYMODULE'),
|
||||
('fnmatch', 'D:\\Anaconda\\Lib\\fnmatch.py', 'PYMODULE'),
|
||||
('fractions', 'D:\\Anaconda\\Lib\\fractions.py', 'PYMODULE'),
|
||||
('frozenlist',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\frozenlist\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('ftplib', 'D:\\Anaconda\\Lib\\ftplib.py', 'PYMODULE'),
|
||||
('getopt', 'D:\\Anaconda\\Lib\\getopt.py', 'PYMODULE'),
|
||||
('getpass', 'D:\\Anaconda\\Lib\\getpass.py', 'PYMODULE'),
|
||||
('gettext', 'D:\\Anaconda\\Lib\\gettext.py', 'PYMODULE'),
|
||||
('glob', 'D:\\Anaconda\\Lib\\glob.py', 'PYMODULE'),
|
||||
('gzip', 'D:\\Anaconda\\Lib\\gzip.py', 'PYMODULE'),
|
||||
('hashlib', 'D:\\Anaconda\\Lib\\hashlib.py', 'PYMODULE'),
|
||||
('hmac', 'D:\\Anaconda\\Lib\\hmac.py', 'PYMODULE'),
|
||||
('html', 'D:\\Anaconda\\Lib\\html\\__init__.py', 'PYMODULE'),
|
||||
('html.entities', 'D:\\Anaconda\\Lib\\html\\entities.py', 'PYMODULE'),
|
||||
('http', 'D:\\Anaconda\\Lib\\http\\__init__.py', 'PYMODULE'),
|
||||
('http.client', 'D:\\Anaconda\\Lib\\http\\client.py', 'PYMODULE'),
|
||||
('http.cookiejar', 'D:\\Anaconda\\Lib\\http\\cookiejar.py', 'PYMODULE'),
|
||||
('http.cookies', 'D:\\Anaconda\\Lib\\http\\cookies.py', 'PYMODULE'),
|
||||
('idna', 'D:\\Anaconda\\Lib\\site-packages\\idna\\__init__.py', 'PYMODULE'),
|
||||
('idna.core', 'D:\\Anaconda\\Lib\\site-packages\\idna\\core.py', 'PYMODULE'),
|
||||
('idna.idnadata',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\idna\\idnadata.py',
|
||||
'PYMODULE'),
|
||||
('idna.intranges',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\idna\\intranges.py',
|
||||
'PYMODULE'),
|
||||
('idna.package_data',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\idna\\package_data.py',
|
||||
'PYMODULE'),
|
||||
('idna.uts46data',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\idna\\uts46data.py',
|
||||
'PYMODULE'),
|
||||
('importlib', 'D:\\Anaconda\\Lib\\importlib\\__init__.py', 'PYMODULE'),
|
||||
('importlib._abc', 'D:\\Anaconda\\Lib\\importlib\\_abc.py', 'PYMODULE'),
|
||||
('importlib._bootstrap',
|
||||
'D:\\Anaconda\\Lib\\importlib\\_bootstrap.py',
|
||||
'PYMODULE'),
|
||||
('importlib._bootstrap_external',
|
||||
'D:\\Anaconda\\Lib\\importlib\\_bootstrap_external.py',
|
||||
'PYMODULE'),
|
||||
('importlib.abc', 'D:\\Anaconda\\Lib\\importlib\\abc.py', 'PYMODULE'),
|
||||
('importlib.machinery',
|
||||
'D:\\Anaconda\\Lib\\importlib\\machinery.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._adapters',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_adapters.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._collections',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_collections.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._functools',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_functools.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._itertools',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_itertools.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._meta',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_meta.py',
|
||||
'PYMODULE'),
|
||||
('importlib.metadata._text',
|
||||
'D:\\Anaconda\\Lib\\importlib\\metadata\\_text.py',
|
||||
'PYMODULE'),
|
||||
('importlib.readers', 'D:\\Anaconda\\Lib\\importlib\\readers.py', 'PYMODULE'),
|
||||
('importlib.resources',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources._adapters',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\_adapters.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources._common',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\_common.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources._functional',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\_functional.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources._itertools',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\_itertools.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources.abc',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\abc.py',
|
||||
'PYMODULE'),
|
||||
('importlib.resources.readers',
|
||||
'D:\\Anaconda\\Lib\\importlib\\resources\\readers.py',
|
||||
'PYMODULE'),
|
||||
('importlib.util', 'D:\\Anaconda\\Lib\\importlib\\util.py', 'PYMODULE'),
|
||||
('inspect', 'D:\\Anaconda\\Lib\\inspect.py', 'PYMODULE'),
|
||||
('ipaddress', 'D:\\Anaconda\\Lib\\ipaddress.py', 'PYMODULE'),
|
||||
('json', 'D:\\Anaconda\\Lib\\json\\__init__.py', 'PYMODULE'),
|
||||
('json.decoder', 'D:\\Anaconda\\Lib\\json\\decoder.py', 'PYMODULE'),
|
||||
('json.encoder', 'D:\\Anaconda\\Lib\\json\\encoder.py', 'PYMODULE'),
|
||||
('json.scanner', 'D:\\Anaconda\\Lib\\json\\scanner.py', 'PYMODULE'),
|
||||
('logging', 'D:\\Anaconda\\Lib\\logging\\__init__.py', 'PYMODULE'),
|
||||
('logging.handlers', 'D:\\Anaconda\\Lib\\logging\\handlers.py', 'PYMODULE'),
|
||||
('lzma', 'D:\\Anaconda\\Lib\\lzma.py', 'PYMODULE'),
|
||||
('mimetypes', 'D:\\Anaconda\\Lib\\mimetypes.py', 'PYMODULE'),
|
||||
('multidict',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\multidict\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('multidict._abc',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\multidict\\_abc.py',
|
||||
'PYMODULE'),
|
||||
('multidict._compat',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\multidict\\_compat.py',
|
||||
'PYMODULE'),
|
||||
('multidict._multidict_py',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\multidict\\_multidict_py.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.connection',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\connection.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.context',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\context.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.dummy',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\dummy\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.dummy.connection',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\dummy\\connection.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.forkserver',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\forkserver.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.heap',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\heap.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.managers',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\managers.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.pool',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\pool.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.popen_fork',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\popen_fork.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.popen_forkserver',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\popen_forkserver.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.popen_spawn_posix',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\popen_spawn_posix.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.popen_spawn_win32',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\popen_spawn_win32.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.process',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\process.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.queues',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\queues.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.reduction',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\reduction.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.resource_sharer',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\resource_sharer.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.resource_tracker',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\resource_tracker.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.shared_memory',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\shared_memory.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.sharedctypes',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\sharedctypes.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.spawn',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\spawn.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.synchronize',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\synchronize.py',
|
||||
'PYMODULE'),
|
||||
('multiprocessing.util',
|
||||
'D:\\Anaconda\\Lib\\multiprocessing\\util.py',
|
||||
'PYMODULE'),
|
||||
('netrc', 'D:\\Anaconda\\Lib\\netrc.py', 'PYMODULE'),
|
||||
('nturl2path', 'D:\\Anaconda\\Lib\\nturl2path.py', 'PYMODULE'),
|
||||
('numbers', 'D:\\Anaconda\\Lib\\numbers.py', 'PYMODULE'),
|
||||
('opcode', 'D:\\Anaconda\\Lib\\opcode.py', 'PYMODULE'),
|
||||
('pathlib', 'D:\\Anaconda\\Lib\\pathlib\\__init__.py', 'PYMODULE'),
|
||||
('pathlib._abc', 'D:\\Anaconda\\Lib\\pathlib\\_abc.py', 'PYMODULE'),
|
||||
('pathlib._local', 'D:\\Anaconda\\Lib\\pathlib\\_local.py', 'PYMODULE'),
|
||||
('pickle', 'D:\\Anaconda\\Lib\\pickle.py', 'PYMODULE'),
|
||||
('pkgutil', 'D:\\Anaconda\\Lib\\pkgutil.py', 'PYMODULE'),
|
||||
('platform', 'D:\\Anaconda\\Lib\\platform.py', 'PYMODULE'),
|
||||
('pprint', 'D:\\Anaconda\\Lib\\pprint.py', 'PYMODULE'),
|
||||
('propcache',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\propcache\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('propcache._helpers',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\propcache\\_helpers.py',
|
||||
'PYMODULE'),
|
||||
('propcache._helpers_py',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\propcache\\_helpers_py.py',
|
||||
'PYMODULE'),
|
||||
('propcache.api',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\propcache\\api.py',
|
||||
'PYMODULE'),
|
||||
('py_compile', 'D:\\Anaconda\\Lib\\py_compile.py', 'PYMODULE'),
|
||||
('pymodbus',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.bit_read_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\bit_read_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.bit_write_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\bit_write_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.base',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\base.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.mixin',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\mixin.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.serial',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\serial.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.tcp',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\tcp.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.tls',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\tls.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.client.udp',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\client\\udp.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.constants',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\constants.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.datastore',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\datastore\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.datastore.context',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\datastore\\context.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.datastore.remote',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\datastore\\remote.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.datastore.simulator',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\datastore\\simulator.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.datastore.store',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\datastore\\store.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.device',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\device.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.diag_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\diag_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.events',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\events.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.exceptions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\exceptions.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.factory',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\factory.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.file_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\file_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.ascii_framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\ascii_framer.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.base',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\base.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.binary_framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\binary_framer.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.rtu_framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\rtu_framer.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.socket_framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\socket_framer.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.framer.tls_framer',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\framer\\tls_framer.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.logging',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\logging.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.mei_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\mei_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.ascii',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\ascii.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.base',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\base.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.raw',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\raw.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.rtu',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\rtu.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.socket',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\socket.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.message.tls',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\message\\tls.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.other_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\other_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.payload',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\payload.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.pdu',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\pdu.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.register_read_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\register_read_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.register_write_message',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\register_write_message.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server.async_io',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\async_io.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server.simulator',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\simulator\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server.simulator.custom_actions',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\simulator\\custom_actions.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server.simulator.http_server',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\simulator\\http_server.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.server.simulator.main',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\server\\simulator\\main.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.transaction',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\transaction.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.transport',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\transport\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.transport.serialtransport',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\transport\\serialtransport.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.transport.transport',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\transport\\transport.py',
|
||||
'PYMODULE'),
|
||||
('pymodbus.utilities',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\pymodbus\\utilities.py',
|
||||
'PYMODULE'),
|
||||
('queue', 'D:\\Anaconda\\Lib\\queue.py', 'PYMODULE'),
|
||||
('quopri', 'D:\\Anaconda\\Lib\\quopri.py', 'PYMODULE'),
|
||||
('random', 'D:\\Anaconda\\Lib\\random.py', 'PYMODULE'),
|
||||
('runpy', 'D:\\Anaconda\\Lib\\runpy.py', 'PYMODULE'),
|
||||
('secrets', 'D:\\Anaconda\\Lib\\secrets.py', 'PYMODULE'),
|
||||
('selectors', 'D:\\Anaconda\\Lib\\selectors.py', 'PYMODULE'),
|
||||
('serial',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('serial.serialcli',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\serialcli.py',
|
||||
'PYMODULE'),
|
||||
('serial.serialjava',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\serialjava.py',
|
||||
'PYMODULE'),
|
||||
('serial.serialposix',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\serialposix.py',
|
||||
'PYMODULE'),
|
||||
('serial.serialutil',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\serialutil.py',
|
||||
'PYMODULE'),
|
||||
('serial.serialwin32',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\serialwin32.py',
|
||||
'PYMODULE'),
|
||||
('serial.win32',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\serial\\win32.py',
|
||||
'PYMODULE'),
|
||||
('shutil', 'D:\\Anaconda\\Lib\\shutil.py', 'PYMODULE'),
|
||||
('signal', 'D:\\Anaconda\\Lib\\signal.py', 'PYMODULE'),
|
||||
('smtplib', 'D:\\Anaconda\\Lib\\smtplib.py', 'PYMODULE'),
|
||||
('socket', 'D:\\Anaconda\\Lib\\socket.py', 'PYMODULE'),
|
||||
('ssl', 'D:\\Anaconda\\Lib\\ssl.py', 'PYMODULE'),
|
||||
('statistics', 'D:\\Anaconda\\Lib\\statistics.py', 'PYMODULE'),
|
||||
('string', 'D:\\Anaconda\\Lib\\string.py', 'PYMODULE'),
|
||||
('stringprep', 'D:\\Anaconda\\Lib\\stringprep.py', 'PYMODULE'),
|
||||
('subprocess', 'D:\\Anaconda\\Lib\\subprocess.py', 'PYMODULE'),
|
||||
('tarfile', 'D:\\Anaconda\\Lib\\tarfile.py', 'PYMODULE'),
|
||||
('tempfile', 'D:\\Anaconda\\Lib\\tempfile.py', 'PYMODULE'),
|
||||
('textwrap', 'D:\\Anaconda\\Lib\\textwrap.py', 'PYMODULE'),
|
||||
('threading', 'D:\\Anaconda\\Lib\\threading.py', 'PYMODULE'),
|
||||
('tkinter', 'D:\\Anaconda\\Lib\\tkinter\\__init__.py', 'PYMODULE'),
|
||||
('tkinter.commondialog',
|
||||
'D:\\Anaconda\\Lib\\tkinter\\commondialog.py',
|
||||
'PYMODULE'),
|
||||
('tkinter.constants', 'D:\\Anaconda\\Lib\\tkinter\\constants.py', 'PYMODULE'),
|
||||
('tkinter.dialog', 'D:\\Anaconda\\Lib\\tkinter\\dialog.py', 'PYMODULE'),
|
||||
('tkinter.filedialog',
|
||||
'D:\\Anaconda\\Lib\\tkinter\\filedialog.py',
|
||||
'PYMODULE'),
|
||||
('tkinter.messagebox',
|
||||
'D:\\Anaconda\\Lib\\tkinter\\messagebox.py',
|
||||
'PYMODULE'),
|
||||
('tkinter.simpledialog',
|
||||
'D:\\Anaconda\\Lib\\tkinter\\simpledialog.py',
|
||||
'PYMODULE'),
|
||||
('tkinter.ttk', 'D:\\Anaconda\\Lib\\tkinter\\ttk.py', 'PYMODULE'),
|
||||
('token', 'D:\\Anaconda\\Lib\\token.py', 'PYMODULE'),
|
||||
('tokenize', 'D:\\Anaconda\\Lib\\tokenize.py', 'PYMODULE'),
|
||||
('tracemalloc', 'D:\\Anaconda\\Lib\\tracemalloc.py', 'PYMODULE'),
|
||||
('typing', 'D:\\Anaconda\\Lib\\typing.py', 'PYMODULE'),
|
||||
('urllib', 'D:\\Anaconda\\Lib\\urllib\\__init__.py', 'PYMODULE'),
|
||||
('urllib.error', 'D:\\Anaconda\\Lib\\urllib\\error.py', 'PYMODULE'),
|
||||
('urllib.parse', 'D:\\Anaconda\\Lib\\urllib\\parse.py', 'PYMODULE'),
|
||||
('urllib.request', 'D:\\Anaconda\\Lib\\urllib\\request.py', 'PYMODULE'),
|
||||
('urllib.response', 'D:\\Anaconda\\Lib\\urllib\\response.py', 'PYMODULE'),
|
||||
('uuid', 'D:\\Anaconda\\Lib\\uuid.py', 'PYMODULE'),
|
||||
('valve_model', 'D:\\flow_control\\valve_model.py', 'PYMODULE'),
|
||||
('win32con',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\win32\\lib\\win32con.py',
|
||||
'PYMODULE'),
|
||||
('win32evtlogutil',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\win32\\lib\\win32evtlogutil.py',
|
||||
'PYMODULE'),
|
||||
('winerror',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\win32\\lib\\winerror.py',
|
||||
'PYMODULE'),
|
||||
('xml', 'D:\\Anaconda\\Lib\\xml\\__init__.py', 'PYMODULE'),
|
||||
('xml.parsers', 'D:\\Anaconda\\Lib\\xml\\parsers\\__init__.py', 'PYMODULE'),
|
||||
('xml.parsers.expat',
|
||||
'D:\\Anaconda\\Lib\\xml\\parsers\\expat.py',
|
||||
'PYMODULE'),
|
||||
('xml.sax', 'D:\\Anaconda\\Lib\\xml\\sax\\__init__.py', 'PYMODULE'),
|
||||
('xml.sax._exceptions',
|
||||
'D:\\Anaconda\\Lib\\xml\\sax\\_exceptions.py',
|
||||
'PYMODULE'),
|
||||
('xml.sax.expatreader',
|
||||
'D:\\Anaconda\\Lib\\xml\\sax\\expatreader.py',
|
||||
'PYMODULE'),
|
||||
('xml.sax.handler', 'D:\\Anaconda\\Lib\\xml\\sax\\handler.py', 'PYMODULE'),
|
||||
('xml.sax.saxutils', 'D:\\Anaconda\\Lib\\xml\\sax\\saxutils.py', 'PYMODULE'),
|
||||
('xml.sax.xmlreader',
|
||||
'D:\\Anaconda\\Lib\\xml\\sax\\xmlreader.py',
|
||||
'PYMODULE'),
|
||||
('xmlrpc', 'D:\\Anaconda\\Lib\\xmlrpc\\__init__.py', 'PYMODULE'),
|
||||
('xmlrpc.client', 'D:\\Anaconda\\Lib\\xmlrpc\\client.py', 'PYMODULE'),
|
||||
('yarl', 'D:\\Anaconda\\Lib\\site-packages\\yarl\\__init__.py', 'PYMODULE'),
|
||||
('yarl._parse',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_parse.py',
|
||||
'PYMODULE'),
|
||||
('yarl._path',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_path.py',
|
||||
'PYMODULE'),
|
||||
('yarl._query',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_query.py',
|
||||
'PYMODULE'),
|
||||
('yarl._quoters',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_quoters.py',
|
||||
'PYMODULE'),
|
||||
('yarl._quoting',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_quoting.py',
|
||||
'PYMODULE'),
|
||||
('yarl._quoting_py',
|
||||
'D:\\Anaconda\\Lib\\site-packages\\yarl\\_quoting_py.py',
|
||||
'PYMODULE'),
|
||||
('yarl._url', 'D:\\Anaconda\\Lib\\site-packages\\yarl\\_url.py', 'PYMODULE'),
|
||||
('zipfile', 'D:\\Anaconda\\Lib\\zipfile\\__init__.py', 'PYMODULE'),
|
||||
('zipfile._path',
|
||||
'D:\\Anaconda\\Lib\\zipfile\\_path\\__init__.py',
|
||||
'PYMODULE'),
|
||||
('zipfile._path.glob',
|
||||
'D:\\Anaconda\\Lib\\zipfile\\_path\\glob.py',
|
||||
'PYMODULE'),
|
||||
('zipimport', 'D:\\Anaconda\\Lib\\zipimport.py', 'PYMODULE')])
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
This file lists modules PyInstaller was not able to find. This does not
|
||||
necessarily mean these modules are required for running your program. Both
|
||||
Python's standard library and 3rd-party Python packages often conditionally
|
||||
import optional modules, some of which may be available only on certain
|
||||
platforms.
|
||||
|
||||
Types of import:
|
||||
* top-level: imported at the top-level - look at these first
|
||||
* conditional: imported within an if-statement
|
||||
* delayed: imported within a function
|
||||
* optional: imported within a try-except-statement
|
||||
|
||||
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 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 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)
|
||||
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named pyimod02_importers - imported by D:\Anaconda\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
|
||||
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
|
||||
missing module named 'System.IO' - imported by serial.serialcli (top-level)
|
||||
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)
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -122,7 +122,7 @@ INITIAL_OPENING_PCT = 100.0
|
||||
# 阀前压力 P1(前馈缩放 / 判阻塞 / 增益调度用),地址见上 PLC 段 PRESSURE_BEFORE_ADDR。
|
||||
|
||||
# 阀特性表 JSON(identify_valve.py 产出)。空字符串表示关闭前馈。
|
||||
VALVE_MODEL_PATH = "valve_model_004.json"
|
||||
VALVE_MODEL_PATH = "valve_model/valve_model_004.json"
|
||||
|
||||
# 前馈反解打底 + PI 修残差(市面 MFC 结构)。
|
||||
FEEDFORWARD_ENABLED = True
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
"""桌面 GUI 使用的设备与闭环运行层。
|
||||
|
||||
所有 Modbus 访问都在 :class:`ControlWorker` 的后台线程中执行。GUI 主线程只
|
||||
发送命令并消费事件,避免网络超时阻塞界面。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import math
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import config
|
||||
from controllers import IncrementalPID
|
||||
from flow_control import FlowControlFault, FlowControlLoop
|
||||
from valve_model import ValveModel
|
||||
|
||||
|
||||
APP_NAME = "FlowControl"
|
||||
BUILTIN_MODEL_NAME = "valve_model_004.json"
|
||||
MONITOR_PERIOD_S = 0.25
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceSettings:
|
||||
"""PLC 连接参数。"""
|
||||
|
||||
host: str
|
||||
port: int
|
||||
slave_id: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModelInfo:
|
||||
"""已经校验并加载的阀模型。"""
|
||||
|
||||
model: ValveModel
|
||||
path: Path
|
||||
display_name: str
|
||||
point_count: int
|
||||
is_builtin: bool
|
||||
is_monotonic: bool
|
||||
|
||||
|
||||
def application_directory() -> Path:
|
||||
"""返回日志等可写文件的程序目录。"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return Path(sys.executable).resolve().parent
|
||||
return Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def resource_path(name: str) -> Path:
|
||||
"""同时支持源码运行和 PyInstaller 单文件资源。"""
|
||||
bundle_root = getattr(sys, "_MEIPASS", None)
|
||||
base = Path(bundle_root) if bundle_root else Path(__file__).resolve().parent
|
||||
direct = base / name
|
||||
if direct.exists():
|
||||
return direct
|
||||
return base / "valve_model" / name
|
||||
|
||||
|
||||
def build_runtime_logger() -> logging.Logger:
|
||||
"""创建 GUI 运行日志,默认写到 EXE 旁的 logs 目录。"""
|
||||
log_dir = application_directory() / config.LOG_DIRECTORY
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_path = log_dir / config.LOG_FILE_NAME
|
||||
|
||||
logger = logging.getLogger("flow_control_gui")
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
logger.handlers.clear()
|
||||
|
||||
handler = RotatingFileHandler(
|
||||
log_path,
|
||||
maxBytes=2 * 1024 * 1024,
|
||||
backupCount=3,
|
||||
encoding="utf-8",
|
||||
)
|
||||
handler.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
|
||||
def validate_device_settings(host, port, slave_id) -> DeviceSettings:
|
||||
"""解析并校验 UI 输入的 PLC 连接参数。"""
|
||||
host_text = str(host).strip()
|
||||
if not host_text or any(char.isspace() for char in host_text):
|
||||
raise ValueError("PLC 地址不能为空或包含空格")
|
||||
try:
|
||||
port_value = int(str(port).strip())
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("端口必须是整数") from exc
|
||||
try:
|
||||
slave_value = int(str(slave_id).strip())
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("站号必须是整数") from exc
|
||||
if not 1 <= port_value <= 65535:
|
||||
raise ValueError("端口必须位于 1~65535")
|
||||
if not 0 <= slave_value <= 247:
|
||||
raise ValueError("站号必须位于 0~247")
|
||||
return DeviceSettings(host_text, port_value, slave_value)
|
||||
|
||||
|
||||
def validate_target(target) -> float:
|
||||
"""解析并校验目标流量。"""
|
||||
try:
|
||||
value = float(str(target).strip())
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("目标流量必须是数字") from exc
|
||||
if not math.isfinite(value):
|
||||
raise ValueError("目标流量必须是有限数值")
|
||||
if not config.TARGET_FLOW_MIN_SLM <= value <= config.TARGET_FLOW_MAX_SLM:
|
||||
raise ValueError(
|
||||
f"目标流量必须位于 {config.TARGET_FLOW_MIN_SLM:g}~"
|
||||
f"{config.TARGET_FLOW_MAX_SLM:g} SLM"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def load_model(model_path: str | Path | None = None) -> ModelInfo:
|
||||
"""加载内置或外部阀模型,并检查其是否匹配当前行程配置。"""
|
||||
is_builtin = model_path is None
|
||||
path = resource_path(BUILTIN_MODEL_NAME) if is_builtin else Path(model_path)
|
||||
path = path.expanduser().resolve()
|
||||
if not path.is_file():
|
||||
raise ValueError(f"阀模型文件不存在:{path}")
|
||||
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
raw = json.load(file)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"无法读取阀模型:{exc}") from exc
|
||||
|
||||
try:
|
||||
declared_open = float(raw["motor_open"])
|
||||
declared_closed = float(raw["motor_closed"])
|
||||
area_table = raw["area_table"]
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise ValueError("阀模型缺少有效的 motor_open、motor_closed 或 area_table") from exc
|
||||
|
||||
if not math.isclose(declared_open, config.MOTOR_OPEN_POSITION, abs_tol=1e-9):
|
||||
raise ValueError(
|
||||
f"模型打开端行程为 {declared_open:g},当前配置要求 "
|
||||
f"{config.MOTOR_OPEN_POSITION:g}"
|
||||
)
|
||||
if not math.isclose(declared_closed, config.MOTOR_CLOSED_POSITION, abs_tol=1e-9):
|
||||
raise ValueError(
|
||||
f"模型关闭端行程为 {declared_closed:g},当前配置要求 "
|
||||
f"{config.MOTOR_CLOSED_POSITION:g}"
|
||||
)
|
||||
if not isinstance(area_table, list) or len(area_table) < 2:
|
||||
raise ValueError("阀模型 area_table 至少需要两个标定点")
|
||||
|
||||
try:
|
||||
model = ValveModel.load(
|
||||
str(path),
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_CLOSED_POSITION,
|
||||
)
|
||||
except (OSError, KeyError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"阀模型内容无效:{exc}") from exc
|
||||
|
||||
for stroke, _area in model.area_table:
|
||||
if not config.MOTOR_OPEN_POSITION <= stroke <= config.MOTOR_CLOSED_POSITION:
|
||||
raise ValueError(
|
||||
f"模型行程 {stroke:g} 超出当前有效范围 "
|
||||
f"{config.MOTOR_OPEN_POSITION:g}~{config.MOTOR_CLOSED_POSITION:g}"
|
||||
)
|
||||
|
||||
prefix = "内置" if is_builtin else "外部"
|
||||
return ModelInfo(
|
||||
model=model,
|
||||
path=path,
|
||||
display_name=f"{prefix} {path.name}({len(model.area_table)} 点)",
|
||||
point_count=len(model.area_table),
|
||||
is_builtin=is_builtin,
|
||||
is_monotonic=model.is_monotonic,
|
||||
)
|
||||
|
||||
|
||||
def build_control_loop(hardware, valve_model, logger) -> FlowControlLoop:
|
||||
"""按 config 构造闭环控制器,不引入命令行记录和绘图依赖。"""
|
||||
pid = IncrementalPID(
|
||||
kp=config.PID_FAR_KP,
|
||||
ki=config.PID_FAR_KI,
|
||||
kd=config.PID_FAR_KD,
|
||||
dt=config.CONTROL_PERIOD_S,
|
||||
out_min=config.OPENING_MIN_PCT,
|
||||
out_max=config.OPENING_MAX_PCT,
|
||||
output_rate_limit=config.MAX_OPENING_RATE_FAR_PCT_S,
|
||||
)
|
||||
return FlowControlLoop(
|
||||
hardware=hardware,
|
||||
pid=pid,
|
||||
period_s=config.CONTROL_PERIOD_S,
|
||||
flow_channel=config.FLOW_AFTER_ADDR,
|
||||
pressure_channel=config.PRESSURE_AFTER_ADDR,
|
||||
pressure_before_channel=config.PRESSURE_BEFORE_ADDR,
|
||||
motor_channel=config.MOTOR_OUTPUT_ADDR,
|
||||
motor_open_position=config.MOTOR_OPEN_POSITION,
|
||||
motor_closed_position=config.MOTOR_CLOSED_POSITION,
|
||||
target_min_slm=config.TARGET_FLOW_MIN_SLM,
|
||||
target_max_slm=config.TARGET_FLOW_MAX_SLM,
|
||||
zero_flow_threshold_slm=config.ZERO_FLOW_THRESHOLD_SLM,
|
||||
flow_valid_min_slm=config.FLOW_VALID_MIN_SLM,
|
||||
flow_valid_max_slm=config.FLOW_VALID_MAX_SLM,
|
||||
max_pressure_kpa=config.MAX_PRESSURE_KPA,
|
||||
max_control_dt_s=config.MAX_CONTROL_DT_S,
|
||||
max_consecutive_flow_failures=config.MAX_CONSECUTIVE_FLOW_FAILURES,
|
||||
max_consecutive_pressure_failures=config.MAX_CONSECUTIVE_PRESSURE_FAILURES,
|
||||
pid_far_gains=(config.PID_FAR_KP, config.PID_FAR_KI, config.PID_FAR_KD),
|
||||
pid_near_gains=(
|
||||
config.PID_NEAR_KP,
|
||||
config.PID_NEAR_KI,
|
||||
config.PID_NEAR_KD,
|
||||
),
|
||||
opening_rate_far_pct_s=config.MAX_OPENING_RATE_FAR_PCT_S,
|
||||
opening_rate_near_pct_s=config.MAX_OPENING_RATE_NEAR_PCT_S,
|
||||
pid_near_error_min_slm=config.PID_NEAR_ERROR_MIN_SLM,
|
||||
pid_near_error_max_slm=config.PID_NEAR_ERROR_MAX_SLM,
|
||||
pid_near_error_target_ratio=config.PID_NEAR_ERROR_TARGET_RATIO,
|
||||
pid_far_error_min_slm=config.PID_FAR_ERROR_MIN_SLM,
|
||||
pid_far_error_max_slm=config.PID_FAR_ERROR_MAX_SLM,
|
||||
pid_far_error_target_ratio=config.PID_FAR_ERROR_TARGET_RATIO,
|
||||
pid_mode_switch_confirm_cycles=config.PID_MODE_SWITCH_CONFIRM_CYCLES,
|
||||
valve_model=valve_model,
|
||||
feedforward_enabled=config.FEEDFORWARD_ENABLED,
|
||||
feedforward_correction_band_pct=config.FEEDFORWARD_CORRECTION_BAND_PCT,
|
||||
feedforward_update_error_threshold_slm=(
|
||||
config.FEEDFORWARD_UPDATE_ERROR_THRESHOLD_SLM
|
||||
),
|
||||
feedforward_update_window_s=config.FEEDFORWARD_UPDATE_WINDOW_S,
|
||||
gain_schedule_enabled=config.GAIN_SCHEDULE_ENABLED,
|
||||
gain_schedule_ref_abs_kpa=config.GAIN_SCHEDULE_REF_ABS_KPA,
|
||||
gain_schedule_floor_abs_kpa=config.GAIN_SCHEDULE_FLOOR_ABS_KPA,
|
||||
gain_schedule_scale_change_threshold=(
|
||||
config.GAIN_SCHEDULE_SCALE_CHANGE_THRESHOLD
|
||||
),
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
|
||||
def create_hardware(settings: DeviceSettings):
|
||||
"""延迟导入硬件模块,便于离线测试 GUI 运行层。"""
|
||||
from PcControl import Easy521ModbusClient
|
||||
|
||||
return Easy521ModbusClient(
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
slave_id=settings.slave_id,
|
||||
flow_addr=config.FLOW_AFTER_ADDR,
|
||||
flow_range=config.FLOW_AFTER_RANGE_SLM,
|
||||
pressure_before_addr=config.PRESSURE_BEFORE_ADDR,
|
||||
pressure_before_range=config.PRESSURE_BEFORE_RANGE_KPA,
|
||||
pressure_after_addr=config.PRESSURE_AFTER_ADDR,
|
||||
pressure_after_range=config.PRESSURE_AFTER_RANGE_KPA,
|
||||
motor_output_addr=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
|
||||
|
||||
class ControlWorker:
|
||||
"""拥有设备连接的后台线程,通过命令/事件队列与 GUI 通信。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logger,
|
||||
*,
|
||||
hardware_factory=create_hardware,
|
||||
model_loader=load_model,
|
||||
controller_factory=build_control_loop,
|
||||
):
|
||||
self.logger = logger
|
||||
self.hardware_factory = hardware_factory
|
||||
self.model_loader = model_loader
|
||||
self.controller_factory = controller_factory
|
||||
self.commands = Queue()
|
||||
self.events = Queue()
|
||||
self.thread = threading.Thread(
|
||||
target=self._run,
|
||||
name="flow-control-worker",
|
||||
daemon=False,
|
||||
)
|
||||
self._shutdown_requested = False
|
||||
self._state = "DISCONNECTED"
|
||||
self._hardware = None
|
||||
self._controller = None
|
||||
self._next_cycle = 0.0
|
||||
self._monitor_failures = {
|
||||
"FLOW": 0,
|
||||
"PRESSURE_BEFORE": 0,
|
||||
"PRESSURE_AFTER": 0,
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
||||
|
||||
def is_alive(self):
|
||||
return self.thread.is_alive()
|
||||
|
||||
def request_connect(self, settings: DeviceSettings, model_path=None):
|
||||
self.commands.put(("CONNECT", {"settings": settings, "model_path": model_path}))
|
||||
|
||||
def request_target(self, target):
|
||||
self.commands.put(("SET_TARGET", {"target": target}))
|
||||
|
||||
def request_disconnect(self):
|
||||
self.commands.put(("DISCONNECT", {}))
|
||||
|
||||
def request_shutdown(self):
|
||||
self.commands.put(("SHUTDOWN", {}))
|
||||
|
||||
def _emit(self, kind, **payload):
|
||||
payload["kind"] = kind
|
||||
self.events.put(payload)
|
||||
|
||||
def _set_state(self, state, message):
|
||||
self._state = state
|
||||
self._emit("state", state=state, message=message)
|
||||
|
||||
def _run(self):
|
||||
self._set_state("DISCONNECTED", "请选择模型并连接设备")
|
||||
try:
|
||||
while not self._shutdown_requested:
|
||||
timeout = self._command_timeout()
|
||||
command = None
|
||||
try:
|
||||
command = self.commands.get(timeout=timeout)
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
if command is not None:
|
||||
self._handle_command(*command)
|
||||
if self._shutdown_requested:
|
||||
break
|
||||
|
||||
now = time.perf_counter()
|
||||
if self._state == "MONITORING" and now >= self._next_cycle:
|
||||
self._monitor_once()
|
||||
self._advance_deadline(now, MONITOR_PERIOD_S)
|
||||
elif self._state == "CONTROLLING" and now >= self._next_cycle:
|
||||
self._control_once(now)
|
||||
self._advance_deadline(now, config.CONTROL_PERIOD_S)
|
||||
finally:
|
||||
errors = self._safe_disconnect()
|
||||
if errors:
|
||||
self.logger.error("程序退出时安全断开存在问题:%s", "; ".join(errors))
|
||||
self._emit("shutdown_complete", errors=errors)
|
||||
|
||||
def _command_timeout(self):
|
||||
if self._state in {"MONITORING", "CONTROLLING"}:
|
||||
return max(0.0, min(0.05, self._next_cycle - time.perf_counter()))
|
||||
return 0.1
|
||||
|
||||
def _advance_deadline(self, now, period):
|
||||
self._next_cycle += period
|
||||
if self._next_cycle <= now:
|
||||
self._next_cycle = time.perf_counter() + period
|
||||
|
||||
def _handle_command(self, command, payload):
|
||||
if command == "CONNECT":
|
||||
self._connect(payload["settings"], payload.get("model_path"))
|
||||
elif command == "SET_TARGET":
|
||||
self._set_target(payload["target"])
|
||||
elif command == "DISCONNECT":
|
||||
self._disconnect()
|
||||
elif command == "SHUTDOWN":
|
||||
self._set_state("STOPPING", "正在安全退出…")
|
||||
self._shutdown_requested = True
|
||||
|
||||
def _connect(self, settings, model_path):
|
||||
if self._state not in {"DISCONNECTED", "FAULT"}:
|
||||
self._emit("warning", message="当前状态不能重复连接")
|
||||
return
|
||||
|
||||
self._set_state("CONNECTING", f"正在连接 {settings.host}:{settings.port}…")
|
||||
try:
|
||||
config.validate_config()
|
||||
model_info = self.model_loader(model_path)
|
||||
hardware = self.hardware_factory(settings)
|
||||
self._hardware = hardware
|
||||
if not bool(hardware.connect()):
|
||||
raise RuntimeError("无法连接 PLC")
|
||||
if not bool(
|
||||
hardware.set_motor_position(
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
):
|
||||
raise RuntimeError("连接后设置阀门 100% 开度失败")
|
||||
|
||||
self._controller = self.controller_factory(
|
||||
hardware,
|
||||
model_info.model,
|
||||
self.logger,
|
||||
)
|
||||
for sensor in self._monitor_failures:
|
||||
self._monitor_failures[sensor] = 0
|
||||
self._next_cycle = time.perf_counter()
|
||||
self.logger.info(
|
||||
"PLC 已连接 %s:%d,站号=%d;模型=%s",
|
||||
settings.host,
|
||||
settings.port,
|
||||
settings.slave_id,
|
||||
model_info.display_name,
|
||||
)
|
||||
self._set_state("MONITORING", "设备已连接,正在监测;请设置目标流量")
|
||||
self._emit(
|
||||
"model_active",
|
||||
display_name=model_info.display_name,
|
||||
point_count=model_info.point_count,
|
||||
is_monotonic=model_info.is_monotonic,
|
||||
)
|
||||
except Exception as exc:
|
||||
errors = self._safe_disconnect()
|
||||
detail = str(exc)
|
||||
if errors:
|
||||
detail += ";安全断开:" + ";".join(errors)
|
||||
self.logger.exception("连接设备失败:%s", detail)
|
||||
self._state = "FAULT"
|
||||
self._emit("fault", message=detail, state="FAULT")
|
||||
|
||||
def _set_target(self, target):
|
||||
if self._state not in {"MONITORING", "CONTROLLING"} or self._controller is None:
|
||||
self._emit("warning", message="设备尚未连接,无法设置目标流量")
|
||||
return
|
||||
try:
|
||||
value = validate_target(target)
|
||||
self._controller.set_target_flow(value)
|
||||
if not self._controller.running:
|
||||
self._controller.start(initial_opening=config.INITIAL_OPENING_PCT)
|
||||
self._next_cycle = time.perf_counter()
|
||||
self.logger.info("目标流量设置为 %.3f SLM", value)
|
||||
self._set_state("CONTROLLING", f"闭环运行中,目标 {value:g} SLM")
|
||||
self._emit("target_applied", target=value)
|
||||
except Exception as exc:
|
||||
self._emit("warning", message=f"目标流量设置失败:{exc}")
|
||||
|
||||
def _disconnect(self):
|
||||
if self._state == "DISCONNECTED":
|
||||
return
|
||||
self._set_state("DISCONNECTING", "正在恢复 100% 开度并断开设备…")
|
||||
errors = self._safe_disconnect()
|
||||
if errors:
|
||||
message = ";".join(errors)
|
||||
self.logger.error("设备已断开,但安全收尾存在问题:%s", message)
|
||||
self._emit("warning", message=message)
|
||||
self._set_state("DISCONNECTED", "设备已断开(收尾存在警告)")
|
||||
else:
|
||||
self.logger.info("设备已安全断开")
|
||||
self._set_state("DISCONNECTED", "设备已断开")
|
||||
|
||||
def _monitor_once(self):
|
||||
try:
|
||||
flow = self._read_monitor_value(
|
||||
"FLOW", "get_flow", config.FLOW_AFTER_ADDR
|
||||
)
|
||||
pressure_before = self._read_monitor_value(
|
||||
"PRESSURE_BEFORE", "get_pressure", config.PRESSURE_BEFORE_ADDR
|
||||
)
|
||||
pressure_after = self._read_monitor_value(
|
||||
"PRESSURE_AFTER", "get_pressure", config.PRESSURE_AFTER_ADDR
|
||||
)
|
||||
if flow is not None and not (
|
||||
config.FLOW_VALID_MIN_SLM <= flow <= config.FLOW_VALID_MAX_SLM
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"FLOW_OUT_OF_RANGE",
|
||||
"MONITORING",
|
||||
f"流量读数 {flow:.3f} SLM 超出允许范围",
|
||||
{"measured_flow_slm": flow},
|
||||
)
|
||||
for name, value in (
|
||||
("阀前压力", pressure_before),
|
||||
("阀后压力", pressure_after),
|
||||
):
|
||||
if (
|
||||
value is not None
|
||||
and config.MAX_PRESSURE_KPA is not None
|
||||
and value > config.MAX_PRESSURE_KPA
|
||||
):
|
||||
raise FlowControlFault(
|
||||
"PRESSURE_OVER_LIMIT",
|
||||
"MONITORING",
|
||||
f"{name} {value:.3f} kPa 超过上限 "
|
||||
f"{config.MAX_PRESSURE_KPA:.3f} kPa",
|
||||
{"pressure_kpa": value, "sensor": name},
|
||||
)
|
||||
status = (
|
||||
"MONITORING"
|
||||
if all(
|
||||
value is not None
|
||||
for value in (flow, pressure_before, pressure_after)
|
||||
)
|
||||
else "SENSOR_READ_FAILED"
|
||||
)
|
||||
self._emit(
|
||||
"telemetry",
|
||||
measured_flow_slm=flow,
|
||||
pressure_before_kpa=pressure_before,
|
||||
pressure_kpa=pressure_after,
|
||||
target_flow_slm=None,
|
||||
opening_pct=100.0,
|
||||
motor_position=config.MOTOR_OPEN_POSITION,
|
||||
feedforward_pct=None,
|
||||
correction_pct=None,
|
||||
pid_mode="--",
|
||||
status=status,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _read_monitor_value(self, sensor, method_name, address):
|
||||
try:
|
||||
method = getattr(self._hardware, method_name)
|
||||
value = method(address)
|
||||
if value is not None:
|
||||
value = float(value)
|
||||
if value is None or not math.isfinite(value):
|
||||
raise ValueError(f"无效读数:{value!r}")
|
||||
except Exception as exc:
|
||||
self._monitor_failures[sensor] += 1
|
||||
count = self._monitor_failures[sensor]
|
||||
limit = (
|
||||
config.MAX_CONSECUTIVE_FLOW_FAILURES
|
||||
if sensor == "FLOW"
|
||||
else config.MAX_CONSECUTIVE_PRESSURE_FAILURES
|
||||
)
|
||||
self.logger.error(
|
||||
"监测阶段 %s 读取失败 %d/%d:%r",
|
||||
sensor,
|
||||
count,
|
||||
limit,
|
||||
exc,
|
||||
)
|
||||
if count >= limit:
|
||||
raise FlowControlFault(
|
||||
f"{sensor}_READ_FAILED",
|
||||
"MONITORING",
|
||||
f"{sensor} 连续读取失败 {count} 次",
|
||||
{"failure_detail": repr(exc)},
|
||||
) from exc
|
||||
return None
|
||||
self._monitor_failures[sensor] = 0
|
||||
return value
|
||||
|
||||
def _control_once(self, now):
|
||||
try:
|
||||
result = self._controller.step(now=now)
|
||||
self._emit(
|
||||
"telemetry",
|
||||
**result.to_dict(),
|
||||
pid_mode=self._controller.pid_mode,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._handle_runtime_fault(exc)
|
||||
|
||||
def _handle_runtime_fault(self, exc):
|
||||
if isinstance(exc, FlowControlFault):
|
||||
message = str(exc)
|
||||
else:
|
||||
message = f"未处理的控制异常:{exc}"
|
||||
self.logger.exception("控制故障:%s", message)
|
||||
errors = self._safe_disconnect()
|
||||
if errors:
|
||||
message += ";安全断开:" + ";".join(errors)
|
||||
self._state = "FAULT"
|
||||
self._emit("fault", message=message, state="FAULT")
|
||||
|
||||
def _safe_disconnect(self):
|
||||
errors = []
|
||||
controller = self._controller
|
||||
hardware = self._hardware
|
||||
self._controller = None
|
||||
self._hardware = None
|
||||
|
||||
if controller is not None:
|
||||
try:
|
||||
controller.stop()
|
||||
except Exception as exc:
|
||||
errors.append(f"停止控制失败:{exc}")
|
||||
|
||||
if hardware is not None:
|
||||
if bool(getattr(hardware, "connected", False)):
|
||||
try:
|
||||
success = hardware.set_motor_position(
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
channel=config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
if not success:
|
||||
errors.append("断开前设置阀门 100% 开度失败")
|
||||
except Exception as exc:
|
||||
errors.append(f"断开前设置阀门 100% 开度异常:{exc}")
|
||||
try:
|
||||
hardware.disconnect()
|
||||
except Exception as exc:
|
||||
errors.append(f"断开 PLC 失败:{exc}")
|
||||
return errors
|
||||
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 274 KiB |
@@ -0,0 +1,42 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
|
||||
|
||||
hiddenimports = collect_submodules("pymodbus")
|
||||
|
||||
a = Analysis(
|
||||
["gui_main.py"],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[("valve_model/valve_model_004.json", ".")],
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=["matplotlib", "numpy", "pytest"],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name="FlowControl",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@@ -0,0 +1,568 @@
|
||||
"""气体流量控制桌面程序。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
from datetime import datetime
|
||||
from queue import Empty
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
import config
|
||||
from control_runtime import (
|
||||
BUILTIN_MODEL_NAME,
|
||||
ControlWorker,
|
||||
build_runtime_logger,
|
||||
load_model,
|
||||
validate_device_settings,
|
||||
validate_target,
|
||||
)
|
||||
|
||||
|
||||
WINDOW_TITLE = "气体流量控制"
|
||||
|
||||
|
||||
class FlowControlApp:
|
||||
"""单窗口操作界面。"""
|
||||
|
||||
STATE_TEXT = {
|
||||
"DISCONNECTED": "未连接",
|
||||
"CONNECTING": "正在连接",
|
||||
"MONITORING": "已连接 / 监测中",
|
||||
"CONTROLLING": "闭环运行中",
|
||||
"DISCONNECTING": "正在断开",
|
||||
"STOPPING": "正在退出",
|
||||
"FAULT": "故障 / 已断开",
|
||||
}
|
||||
|
||||
STATE_COLORS = {
|
||||
"DISCONNECTED": ("#5f6368", "#eef0f2"),
|
||||
"CONNECTING": ("#875a00", "#fff3cd"),
|
||||
"MONITORING": ("#0b6b3a", "#dff4e8"),
|
||||
"CONTROLLING": ("#075cad", "#deefff"),
|
||||
"DISCONNECTING": ("#875a00", "#fff3cd"),
|
||||
"STOPPING": ("#875a00", "#fff3cd"),
|
||||
"FAULT": ("#a61b1b", "#fde5e5"),
|
||||
}
|
||||
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title(WINDOW_TITLE)
|
||||
self.root.geometry("860x720")
|
||||
self.root.minsize(780, 640)
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
self.logger = build_runtime_logger()
|
||||
self.worker = ControlWorker(self.logger)
|
||||
self.current_state = "DISCONNECTED"
|
||||
self.closing = False
|
||||
self.selected_model_path = None
|
||||
self.model_info = load_model()
|
||||
|
||||
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.active_target_var = tk.StringVar(value="--")
|
||||
self.status_var = tk.StringVar(value="未连接")
|
||||
self.detail_var = tk.StringVar(value="请选择模型并连接设备")
|
||||
|
||||
self.value_vars = {
|
||||
"flow": tk.StringVar(value="--"),
|
||||
"pressure_before": tk.StringVar(value="--"),
|
||||
"pressure_after": tk.StringVar(value="--"),
|
||||
"opening": tk.StringVar(value="--"),
|
||||
"motor": tk.StringVar(value="--"),
|
||||
"feedforward": tk.StringVar(value="--"),
|
||||
"correction": tk.StringVar(value="--"),
|
||||
"mode": tk.StringVar(value="--"),
|
||||
}
|
||||
|
||||
self._configure_style()
|
||||
self._build_ui()
|
||||
self._apply_state("DISCONNECTED", "未连接")
|
||||
self._append_log(
|
||||
f"已加载 {self.model_info.display_name};双击程序不会自动连接设备"
|
||||
)
|
||||
self.worker.start()
|
||||
self.root.after(80, self._poll_worker_events)
|
||||
|
||||
def _configure_style(self):
|
||||
self.root.option_add("*Font", ("Microsoft YaHei UI", 10))
|
||||
style = ttk.Style(self.root)
|
||||
try:
|
||||
style.theme_use("vista")
|
||||
except tk.TclError:
|
||||
pass
|
||||
style.configure("Title.TLabel", font=("Microsoft YaHei UI", 20, "bold"))
|
||||
style.configure("Section.TLabelframe.Label", font=("Microsoft YaHei UI", 11, "bold"))
|
||||
style.configure("Primary.TButton", font=("Microsoft YaHei UI", 10, "bold"))
|
||||
style.configure("Danger.TButton", font=("Microsoft YaHei UI", 10, "bold"))
|
||||
|
||||
def _build_ui(self):
|
||||
outer = ttk.Frame(self.root, padding=(18, 14, 18, 14))
|
||||
outer.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(outer, text=WINDOW_TITLE, style="Title.TLabel").pack(anchor="w")
|
||||
ttk.Label(
|
||||
outer,
|
||||
text="Easy521 PLC · Modbus TCP · 10 Hz 闭环控制",
|
||||
foreground="#5f6368",
|
||||
).pack(anchor="w", pady=(0, 10))
|
||||
|
||||
warning = tk.Label(
|
||||
outer,
|
||||
text=(
|
||||
"安全提示:连接成功后及断开设备前,程序会命令阀门至 100% 开度"
|
||||
f"(行程 {config.MOTOR_OPEN_POSITION:g})。"
|
||||
),
|
||||
bg="#fff4e5",
|
||||
fg="#8a4b00",
|
||||
padx=10,
|
||||
pady=7,
|
||||
anchor="w",
|
||||
)
|
||||
warning.pack(fill="x", pady=(0, 10))
|
||||
|
||||
connection = ttk.LabelFrame(
|
||||
outer,
|
||||
text="设备与模型",
|
||||
padding=10,
|
||||
style="Section.TLabelframe",
|
||||
)
|
||||
connection.pack(fill="x")
|
||||
connection.columnconfigure(1, weight=1)
|
||||
connection.columnconfigure(3, weight=1)
|
||||
|
||||
ttk.Label(connection, text="PLC 地址").grid(row=0, column=0, sticky="w")
|
||||
self.host_entry = ttk.Entry(connection, textvariable=self.host_var, width=20)
|
||||
self.host_entry.grid(row=0, column=1, sticky="ew", padx=(7, 14))
|
||||
ttk.Label(connection, text="端口").grid(row=0, column=2, sticky="w")
|
||||
self.port_entry = ttk.Entry(connection, textvariable=self.port_var, width=9)
|
||||
self.port_entry.grid(row=0, column=3, sticky="w", padx=(7, 14))
|
||||
ttk.Label(connection, text="站号").grid(row=0, column=4, sticky="w")
|
||||
self.slave_entry = ttk.Entry(connection, textvariable=self.slave_var, width=7)
|
||||
self.slave_entry.grid(row=0, column=5, sticky="w", padx=(7, 0))
|
||||
|
||||
ttk.Label(connection, text="A_eff 模型").grid(
|
||||
row=1, column=0, sticky="w", pady=(10, 0)
|
||||
)
|
||||
self.model_label = ttk.Label(
|
||||
connection,
|
||||
textvariable=self.model_var,
|
||||
foreground="#174f85",
|
||||
)
|
||||
self.model_label.grid(
|
||||
row=1, column=1, columnspan=3, sticky="w", padx=(7, 10), pady=(10, 0)
|
||||
)
|
||||
self.model_browse_button = ttk.Button(
|
||||
connection, text="选择 JSON…", command=self._choose_model
|
||||
)
|
||||
self.model_browse_button.grid(row=1, column=4, pady=(10, 0), padx=(0, 7))
|
||||
self.model_builtin_button = ttk.Button(
|
||||
connection, text="使用内置", command=self._use_builtin_model
|
||||
)
|
||||
self.model_builtin_button.grid(row=1, column=5, pady=(10, 0))
|
||||
|
||||
button_row = ttk.Frame(connection)
|
||||
button_row.grid(row=2, column=0, columnspan=6, sticky="ew", pady=(12, 0))
|
||||
self.connect_button = ttk.Button(
|
||||
button_row,
|
||||
text="连接设备",
|
||||
command=self._connect,
|
||||
style="Primary.TButton",
|
||||
width=16,
|
||||
)
|
||||
self.connect_button.pack(side="left")
|
||||
self.disconnect_button = ttk.Button(
|
||||
button_row,
|
||||
text="断开设备",
|
||||
command=self._disconnect,
|
||||
style="Danger.TButton",
|
||||
width=16,
|
||||
)
|
||||
self.disconnect_button.pack(side="left", padx=(10, 0))
|
||||
|
||||
target_frame = ttk.LabelFrame(
|
||||
outer,
|
||||
text="目标流量",
|
||||
padding=10,
|
||||
style="Section.TLabelframe",
|
||||
)
|
||||
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")
|
||||
self.target_entry = ttk.Entry(
|
||||
target_frame,
|
||||
textvariable=self.target_var,
|
||||
width=18,
|
||||
font=("Microsoft YaHei UI", 12),
|
||||
)
|
||||
self.target_entry.grid(row=0, column=1, sticky="w", padx=(8, 5))
|
||||
self.target_entry.bind("<Return>", lambda _event: self._set_target())
|
||||
ttk.Label(target_frame, text="SLM").grid(row=0, column=2, sticky="w")
|
||||
self.target_button = ttk.Button(
|
||||
target_frame,
|
||||
text="设置目标流量",
|
||||
command=self._set_target,
|
||||
style="Primary.TButton",
|
||||
width=17,
|
||||
)
|
||||
self.target_button.grid(row=0, column=3, padx=(18, 0))
|
||||
ttk.Label(target_frame, text="当前目标:").grid(
|
||||
row=0, column=4, padx=(24, 0)
|
||||
)
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
textvariable=self.active_target_var,
|
||||
font=("Microsoft YaHei UI", 11, "bold"),
|
||||
foreground="#075cad",
|
||||
).grid(row=0, column=5, sticky="w")
|
||||
ttk.Label(
|
||||
target_frame,
|
||||
text=(
|
||||
f"允许范围 {config.TARGET_FLOW_MIN_SLM:g}~"
|
||||
f"{config.TARGET_FLOW_MAX_SLM:g} SLM;首次设置即启动闭环"
|
||||
),
|
||||
foreground="#5f6368",
|
||||
).grid(row=1, column=0, columnspan=6, sticky="w", pady=(7, 0))
|
||||
|
||||
telemetry = ttk.LabelFrame(
|
||||
outer,
|
||||
text="实时监视",
|
||||
padding=10,
|
||||
style="Section.TLabelframe",
|
||||
)
|
||||
telemetry.pack(fill="x", pady=(10, 0))
|
||||
for column in range(4):
|
||||
telemetry.columnconfigure(column, weight=1, uniform="card")
|
||||
|
||||
self._add_value_card(telemetry, 0, 0, "当前流量", "flow", "SLM")
|
||||
self._add_value_card(telemetry, 0, 1, "阀前压力", "pressure_before", "kPa")
|
||||
self._add_value_card(telemetry, 0, 2, "阀后压力", "pressure_after", "kPa")
|
||||
self._add_value_card(
|
||||
telemetry, 0, 3, "下发总开度", "opening", "%", prominent=True
|
||||
)
|
||||
self._add_value_card(telemetry, 1, 0, "电机行程", "motor", "")
|
||||
self._add_value_card(telemetry, 1, 1, "前馈开度", "feedforward", "%")
|
||||
self._add_value_card(telemetry, 1, 2, "PI 修正", "correction", "%")
|
||||
self._add_value_card(telemetry, 1, 3, "PID 模式", "mode", "")
|
||||
|
||||
status_frame = ttk.LabelFrame(
|
||||
outer,
|
||||
text="运行状态",
|
||||
padding=10,
|
||||
style="Section.TLabelframe",
|
||||
)
|
||||
status_frame.pack(fill="both", expand=True, pady=(10, 0))
|
||||
status_header = ttk.Frame(status_frame)
|
||||
status_header.pack(fill="x")
|
||||
self.status_badge = tk.Label(
|
||||
status_header,
|
||||
textvariable=self.status_var,
|
||||
padx=10,
|
||||
pady=4,
|
||||
font=("Microsoft YaHei UI", 10, "bold"),
|
||||
)
|
||||
self.status_badge.pack(side="left")
|
||||
ttk.Label(status_header, textvariable=self.detail_var).pack(
|
||||
side="left", padx=(10, 0)
|
||||
)
|
||||
|
||||
self.log_text = tk.Text(
|
||||
status_frame,
|
||||
height=3,
|
||||
wrap="word",
|
||||
state="disabled",
|
||||
bg="#fafafa",
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
font=("Microsoft YaHei UI", 9),
|
||||
)
|
||||
self.log_text.pack(fill="both", expand=True, pady=(8, 0))
|
||||
|
||||
def _add_value_card(self, parent, row, column, title, key, unit, prominent=False):
|
||||
frame = tk.Frame(
|
||||
parent,
|
||||
bg="#edf5ff" if prominent else "#f6f7f8",
|
||||
highlightbackground="#b8c6d5" if prominent else "#d9dde1",
|
||||
highlightthickness=1,
|
||||
padx=10,
|
||||
pady=8,
|
||||
)
|
||||
frame.grid(
|
||||
row=row,
|
||||
column=column,
|
||||
sticky="nsew",
|
||||
padx=(0 if column == 0 else 5, 0),
|
||||
pady=(0 if row == 0 else 7, 0),
|
||||
)
|
||||
tk.Label(
|
||||
frame,
|
||||
text=title,
|
||||
bg=frame["bg"],
|
||||
fg="#4c545c",
|
||||
font=("Microsoft YaHei UI", 9),
|
||||
).pack(anchor="w")
|
||||
value_row = tk.Frame(frame, bg=frame["bg"])
|
||||
value_row.pack(anchor="w", pady=(3, 0))
|
||||
tk.Label(
|
||||
value_row,
|
||||
textvariable=self.value_vars[key],
|
||||
bg=frame["bg"],
|
||||
fg="#075cad" if prominent else "#202124",
|
||||
font=("Microsoft YaHei UI", 16 if prominent else 14, "bold"),
|
||||
).pack(side="left")
|
||||
if unit:
|
||||
tk.Label(
|
||||
value_row,
|
||||
text=f" {unit}",
|
||||
bg=frame["bg"],
|
||||
fg="#5f6368",
|
||||
font=("Microsoft YaHei UI", 9),
|
||||
).pack(side="left", anchor="s", pady=(0, 2))
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
settings = validate_device_settings(
|
||||
self.host_var.get(), self.port_var.get(), self.slave_var.get()
|
||||
)
|
||||
# 在主线程先校验一次,避免无效模型进入连接流程。
|
||||
self.model_info = load_model(self.selected_model_path)
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("无法连接", str(exc), parent=self.root)
|
||||
return
|
||||
|
||||
self._apply_state("CONNECTING", f"正在连接 {settings.host}:{settings.port}…")
|
||||
self._append_log(f"请求连接 {settings.host}:{settings.port},站号 {settings.slave_id}")
|
||||
self.worker.request_connect(settings, self.selected_model_path)
|
||||
|
||||
def _disconnect(self):
|
||||
self._apply_state("DISCONNECTING", "正在安全断开设备…")
|
||||
self._append_log("请求停止控制并安全断开设备")
|
||||
self.worker.request_disconnect()
|
||||
|
||||
def _set_target(self):
|
||||
try:
|
||||
value = validate_target(self.target_var.get())
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("目标流量无效", str(exc), parent=self.root)
|
||||
return
|
||||
self.detail_var.set(f"正在设置目标流量 {value:g} SLM…")
|
||||
self.worker.request_target(value)
|
||||
|
||||
def _choose_model(self):
|
||||
path = filedialog.askopenfilename(
|
||||
parent=self.root,
|
||||
title="选择 A_eff 阀模型",
|
||||
filetypes=(("JSON 阀模型", "*.json"), ("所有文件", "*.*")),
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
info = load_model(path)
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("模型无效", str(exc), parent=self.root)
|
||||
return
|
||||
self.selected_model_path = str(info.path)
|
||||
self.model_info = info
|
||||
self.model_var.set(info.display_name)
|
||||
monotonic_text = "单调" if info.is_monotonic else "非单调(按关闭端优先反解)"
|
||||
self._append_log(f"已选择 {info.display_name},{monotonic_text}")
|
||||
|
||||
def _use_builtin_model(self):
|
||||
try:
|
||||
info = load_model()
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("内置模型不可用", str(exc), parent=self.root)
|
||||
return
|
||||
self.selected_model_path = None
|
||||
self.model_info = info
|
||||
self.model_var.set(info.display_name)
|
||||
self._append_log(f"已切换为 {info.display_name}")
|
||||
|
||||
def _poll_worker_events(self):
|
||||
try:
|
||||
while True:
|
||||
event = self.worker.events.get_nowait()
|
||||
self._handle_worker_event(event)
|
||||
except Empty:
|
||||
pass
|
||||
if not self.closing:
|
||||
self.root.after(80, self._poll_worker_events)
|
||||
elif self.worker.is_alive():
|
||||
self.root.after(80, self._poll_worker_events)
|
||||
else:
|
||||
self.root.destroy()
|
||||
|
||||
def _handle_worker_event(self, event):
|
||||
kind = event["kind"]
|
||||
if kind == "state":
|
||||
state = event["state"]
|
||||
message = event.get("message", "")
|
||||
self._apply_state(state, message)
|
||||
if state != "DISCONNECTED" or message != "未连接":
|
||||
self._append_log(message)
|
||||
elif kind == "telemetry":
|
||||
self._update_telemetry(event)
|
||||
elif kind == "target_applied":
|
||||
target = event["target"]
|
||||
self.active_target_var.set(f"{target:g} SLM")
|
||||
self._append_log(f"目标流量已生效:{target:g} SLM")
|
||||
elif kind == "model_active":
|
||||
self._append_log(f"当前控制模型:{event['display_name']}")
|
||||
elif kind == "warning":
|
||||
message = event.get("message", "未知警告")
|
||||
self.detail_var.set(message)
|
||||
self._append_log("警告:" + message)
|
||||
elif kind == "fault":
|
||||
message = event.get("message", "未知故障")
|
||||
self._apply_state("FAULT", message)
|
||||
self._append_log("故障:" + message)
|
||||
if not self.closing:
|
||||
messagebox.showerror("设备或控制故障", message, parent=self.root)
|
||||
elif kind == "shutdown_complete" and self.closing:
|
||||
errors = event.get("errors") or []
|
||||
if errors:
|
||||
self._append_log("退出警告:" + ";".join(errors))
|
||||
|
||||
def _update_telemetry(self, event):
|
||||
self.value_vars["flow"].set(self._format_number(event.get("measured_flow_slm")))
|
||||
self.value_vars["pressure_before"].set(
|
||||
self._format_number(event.get("pressure_before_kpa"))
|
||||
)
|
||||
self.value_vars["pressure_after"].set(
|
||||
self._format_number(event.get("pressure_kpa"))
|
||||
)
|
||||
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._format_number(event.get("feedforward_pct"), digits=2)
|
||||
)
|
||||
correction = event.get("correction_pct")
|
||||
self.value_vars["correction"].set(
|
||||
"--" if correction is None else f"{float(correction):+.2f}"
|
||||
)
|
||||
self.value_vars["mode"].set(str(event.get("pid_mode") or "--"))
|
||||
|
||||
status = event.get("status", "")
|
||||
if status and status not in {"OK", "MONITORING"}:
|
||||
self.detail_var.set(f"采样状态:{status}")
|
||||
|
||||
@staticmethod
|
||||
def _format_number(value, digits=2):
|
||||
if value is None:
|
||||
return "--"
|
||||
return f"{float(value):.{digits}f}"
|
||||
|
||||
def _apply_state(self, state, detail):
|
||||
self.current_state = state
|
||||
self.status_var.set(self.STATE_TEXT.get(state, state))
|
||||
self.detail_var.set(detail)
|
||||
foreground, background = self.STATE_COLORS.get(
|
||||
state, ("#202124", "#eef0f2")
|
||||
)
|
||||
if hasattr(self, "status_badge"):
|
||||
self.status_badge.configure(fg=foreground, bg=background)
|
||||
|
||||
editable = state in {"DISCONNECTED", "FAULT"}
|
||||
connected = state in {"MONITORING", "CONTROLLING"}
|
||||
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")
|
||||
self._set_state(
|
||||
self.model_browse_button, "normal" if editable else "disabled"
|
||||
)
|
||||
self._set_state(
|
||||
self.model_builtin_button, "normal" if editable 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")
|
||||
if state in {"DISCONNECTED", "FAULT"}:
|
||||
self.active_target_var.set("--")
|
||||
|
||||
@staticmethod
|
||||
def _set_state(widget, state):
|
||||
widget.configure(state=state)
|
||||
|
||||
def _append_log(self, message):
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.insert("end", f"[{timestamp}] {message}\n")
|
||||
self.log_text.see("end-1c")
|
||||
self.log_text.configure(state="disabled")
|
||||
|
||||
def _on_close(self):
|
||||
if self.closing:
|
||||
return
|
||||
self.closing = True
|
||||
self._apply_state("STOPPING", "正在安全退出,请稍候…")
|
||||
self._append_log("正在停止控制、恢复 100% 开度并关闭连接")
|
||||
self.worker.request_shutdown()
|
||||
|
||||
|
||||
def enable_dpi_awareness():
|
||||
"""让 Windows 高 DPI 显示更清晰;失败时安全忽略。"""
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except (AttributeError, OSError):
|
||||
try:
|
||||
ctypes.windll.user32.SetProcessDPIAware()
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
def smoke_test():
|
||||
"""供打包后自动检查资源和关键依赖,不连接设备。"""
|
||||
config.validate_config()
|
||||
info = load_model()
|
||||
if info.point_count < 2 or info.path.name != BUILTIN_MODEL_NAME:
|
||||
raise RuntimeError("内置阀模型校验失败")
|
||||
from PcControl import Easy521ModbusClient # noqa: F401
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(add_help=True)
|
||||
parser.add_argument(
|
||||
"--smoke-test",
|
||||
action="store_true",
|
||||
help="仅检查内置资源和依赖,不打开界面、不连接设备",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
if args.smoke_test:
|
||||
return smoke_test()
|
||||
|
||||
enable_dpi_awareness()
|
||||
root = tk.Tk()
|
||||
try:
|
||||
FlowControlApp(root)
|
||||
except Exception as exc:
|
||||
root.withdraw()
|
||||
messagebox.showerror("程序启动失败", str(exc), parent=root)
|
||||
root.destroy()
|
||||
return 1
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,172 @@
|
||||
import logging
|
||||
from queue import Empty
|
||||
import time
|
||||
|
||||
import config
|
||||
from control_runtime import (
|
||||
ControlWorker,
|
||||
load_model,
|
||||
validate_device_settings,
|
||||
validate_target,
|
||||
)
|
||||
|
||||
|
||||
class FakeHardware:
|
||||
def __init__(self, *, connect_ok=True):
|
||||
self.connect_ok = connect_ok
|
||||
self.connected = False
|
||||
self.disconnect_count = 0
|
||||
self.motor_commands = []
|
||||
|
||||
def connect(self):
|
||||
self.connected = self.connect_ok
|
||||
return self.connect_ok
|
||||
|
||||
def disconnect(self):
|
||||
self.connected = False
|
||||
self.disconnect_count += 1
|
||||
|
||||
def get_flow(self, _address):
|
||||
return 20.0
|
||||
|
||||
def get_pressure(self, address):
|
||||
if address == config.PRESSURE_BEFORE_ADDR:
|
||||
return 200.0
|
||||
return 100.0
|
||||
|
||||
def set_motor_position(self, position, channel=None):
|
||||
self.motor_commands.append((float(position), channel))
|
||||
return True
|
||||
|
||||
|
||||
class OverpressureHardware(FakeHardware):
|
||||
def get_pressure(self, address):
|
||||
if address == config.PRESSURE_BEFORE_ADDR:
|
||||
return config.MAX_PRESSURE_KPA + 1.0
|
||||
return 100.0
|
||||
|
||||
|
||||
def wait_for_event(worker, predicate, timeout=2.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
event = worker.events.get(timeout=0.1)
|
||||
except Empty:
|
||||
continue
|
||||
if predicate(event):
|
||||
return event
|
||||
raise AssertionError("等待后台线程事件超时")
|
||||
|
||||
|
||||
def make_logger():
|
||||
logger = logging.getLogger(f"test-control-runtime-{time.monotonic_ns()}")
|
||||
logger.addHandler(logging.NullHandler())
|
||||
return logger
|
||||
|
||||
|
||||
def test_validate_ui_values():
|
||||
settings = validate_device_settings(" 192.168.1.88 ", "502", "1")
|
||||
assert settings.host == "192.168.1.88"
|
||||
assert settings.port == 502
|
||||
assert settings.slave_id == 1
|
||||
assert validate_target("50.5") == 50.5
|
||||
|
||||
|
||||
def test_builtin_model_is_valve_model_004():
|
||||
info = load_model()
|
||||
assert info.is_builtin is True
|
||||
assert info.path.name == "valve_model_004.json"
|
||||
assert info.point_count == 80
|
||||
assert info.model.motor_open == config.MOTOR_OPEN_POSITION
|
||||
assert info.model.motor_closed == config.MOTOR_CLOSED_POSITION
|
||||
|
||||
|
||||
def test_worker_connect_control_and_safe_disconnect():
|
||||
hardware = FakeHardware()
|
||||
worker = ControlWorker(make_logger(), hardware_factory=lambda _settings: hardware)
|
||||
worker.start()
|
||||
wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "state"
|
||||
and event.get("state") == "DISCONNECTED",
|
||||
)
|
||||
|
||||
settings = validate_device_settings("192.168.1.88", 502, 1)
|
||||
worker.request_connect(settings)
|
||||
wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "state"
|
||||
and event.get("state") == "MONITORING",
|
||||
)
|
||||
assert hardware.connected is True
|
||||
assert hardware.motor_commands[0] == (
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
|
||||
worker.request_target(50.0)
|
||||
wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "target_applied",
|
||||
)
|
||||
telemetry = wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "telemetry"
|
||||
and event.get("target_flow_slm") == 50.0,
|
||||
)
|
||||
assert telemetry["measured_flow_slm"] == 20.0
|
||||
assert 0.0 <= telemetry["opening_pct"] <= 100.0
|
||||
assert telemetry["pid_mode"] in {"FAR", "NEAR"}
|
||||
|
||||
worker.request_disconnect()
|
||||
wait_for_event(
|
||||
worker,
|
||||
lambda event: event.get("kind") == "state"
|
||||
and event.get("state") == "DISCONNECTED",
|
||||
)
|
||||
assert hardware.connected is False
|
||||
assert hardware.motor_commands[-1] == (
|
||||
config.MOTOR_OPEN_POSITION,
|
||||
config.MOTOR_OUTPUT_ADDR,
|
||||
)
|
||||
assert hardware.disconnect_count == 1
|
||||
|
||||
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)
|
||||
worker.start()
|
||||
wait_for_event(worker, lambda event: event.get("state") == "DISCONNECTED")
|
||||
worker.request_connect(validate_device_settings("192.168.1.88", 502, 1))
|
||||
fault = wait_for_event(worker, lambda event: event.get("kind") == "fault")
|
||||
assert "无法连接 PLC" in fault["message"]
|
||||
assert hardware.motor_commands == []
|
||||
assert hardware.disconnect_count == 1
|
||||
|
||||
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_monitoring_stage_overpressure_triggers_safe_disconnect():
|
||||
hardware = OverpressureHardware()
|
||||
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")
|
||||
fault = wait_for_event(worker, lambda event: event.get("kind") == "fault")
|
||||
assert "超过上限" in fault["message"]
|
||||
assert hardware.connected is False
|
||||
assert hardware.motor_commands[-1][0] == config.MOTOR_OPEN_POSITION
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,25 @@
|
||||
time_s,timestamp,flow_after_slm,opening_pct,pressure_before_kpa,pressure_after_kpa,motor_position
|
||||
0.000000,2026-09-03T14:33:33.002,180.825000,100.000000,201.375000,121.100000,800.000000
|
||||
1.009349,2026-09-03T14:33:34.012,180.834375,100.000000,201.581250,121.450000,800.000000
|
||||
2.019912,2026-09-03T14:33:35.022,180.459375,100.000000,201.909375,121.237500,800.000000
|
||||
3.029416,2026-09-03T14:33:36.031,181.378125,100.000000,202.031250,121.375000,800.000000
|
||||
4.038972,2026-09-03T14:33:37.041,174.637500,0.000000,212.353125,39.475000,1000.000000
|
||||
5.048765,2026-09-03T14:33:38.051,34.912500,0.000000,306.000000,-0.237500,1000.000000
|
||||
6.059989,2026-09-03T14:33:39.062,8.325000,0.000000,306.000000,-0.275000,1000.000000
|
||||
7.069332,2026-09-03T14:33:40.071,3.675000,0.000000,306.000000,-0.300000,1000.000000
|
||||
8.078579,2026-09-03T14:33:41.081,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
9.089545,2026-09-03T14:33:42.092,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
10.100123,2026-09-03T14:33:43.102,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
11.110366,2026-09-03T14:33:44.113,0.881250,0.000000,306.000000,-0.337500,1000.000000
|
||||
12.122532,2026-09-03T14:33:45.125,0.881250,0.000000,306.000000,-0.337500,1000.000000
|
||||
13.134354,2026-09-03T14:33:46.136,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
14.144802,2026-09-03T14:33:47.147,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
15.154118,2026-09-03T14:33:48.156,0.881250,0.000000,306.000000,-0.337500,1000.000000
|
||||
16.164254,2026-09-03T14:33:49.166,0.862500,0.000000,306.000000,-0.312500,1000.000000
|
||||
17.174010,2026-09-03T14:33:50.176,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
18.182420,2026-09-03T14:33:51.184,0.862500,0.000000,306.000000,-0.337500,1000.000000
|
||||
19.191267,2026-09-03T14:33:52.193,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
20.201921,2026-09-03T14:33:53.204,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
21.211323,2026-09-03T14:33:54.214,0.862500,0.000000,306.000000,-0.312500,1000.000000
|
||||
22.222314,2026-09-03T14:33:55.224,0.862500,0.000000,306.000000,-0.337500,1000.000000
|
||||
23.232360,2026-09-03T14:33:56.235,0.862500,0.000000,306.000000,-0.350000,1000.000000
|
||||
|
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,27 @@
|
||||
time_s,timestamp,flow_after_slm,opening_pct,pressure_before_kpa,pressure_after_kpa,motor_position
|
||||
0.000000,2026-09-03T14:36:53.577,186.300000,100.000000,206.175000,124.550000,800.000000
|
||||
1.009534,2026-09-03T14:36:54.587,186.206250,100.000000,206.559375,124.400000,800.000000
|
||||
2.018970,2026-09-03T14:36:55.597,184.837500,100.000000,207.750000,124.375000,800.000000
|
||||
3.029327,2026-09-03T14:36:56.607,185.653125,100.000000,206.878125,124.550000,800.000000
|
||||
4.039865,2026-09-03T14:36:57.618,186.187500,100.000000,206.850000,124.875000,800.000000
|
||||
5.049509,2026-09-03T14:36:58.627,185.925000,100.000000,206.812500,124.775000,800.000000
|
||||
6.059955,2026-09-03T14:36:59.638,185.559375,100.000000,206.831250,124.937500,800.000000
|
||||
7.068847,2026-09-03T14:37:00.647,185.756250,100.000000,206.653125,124.812500,800.000000
|
||||
8.079927,2026-09-03T14:37:01.658,186.459375,100.000000,206.709375,124.812500,800.000000
|
||||
9.090449,2026-09-03T14:37:02.668,76.059375,0.000000,306.000000,-0.175000,1000.000000
|
||||
10.099598,2026-09-03T14:37:03.677,17.353125,0.000000,306.000000,-0.250000,1000.000000
|
||||
11.109955,2026-09-03T14:37:04.688,6.412500,0.000000,306.000000,-0.275000,1000.000000
|
||||
12.120598,2026-09-03T14:37:05.698,3.131250,0.000000,306.000000,-0.312500,1000.000000
|
||||
13.130787,2026-09-03T14:37:06.709,0.909375,0.000000,306.000000,-0.312500,1000.000000
|
||||
14.141024,2026-09-03T14:37:07.719,0.909375,0.000000,306.000000,-0.337500,1000.000000
|
||||
15.151936,2026-09-03T14:37:08.730,0.909375,0.000000,306.000000,-0.312500,1000.000000
|
||||
16.161696,2026-09-03T14:37:09.739,0.881250,0.000000,306.000000,-0.337500,1000.000000
|
||||
17.170813,2026-09-03T14:37:10.749,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
18.181591,2026-09-03T14:37:11.759,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
19.192289,2026-09-03T14:37:12.770,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
20.201665,2026-09-03T14:37:13.779,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
21.210939,2026-09-03T14:37:14.789,0.881250,0.000000,306.000000,-0.350000,1000.000000
|
||||
22.221416,2026-09-03T14:37:15.799,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
23.231089,2026-09-03T14:37:16.809,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
24.242616,2026-09-03T14:37:17.820,0.881250,0.000000,306.000000,-0.312500,1000.000000
|
||||
25.251933,2026-09-03T14:37:18.830,0.881250,0.000000,306.000000,-0.337500,1000.000000
|
||||
|
|
After Width: | Height: | Size: 138 KiB |
@@ -0,0 +1,76 @@
|
||||
time_s,timestamp,flow_after_slm,opening_pct,pressure_before_kpa,pressure_after_kpa,motor_position
|
||||
0.000000,2026-09-03T14:37:29.223,220.031250,100.000000,254.634375,155.712500,800.000000
|
||||
1.008507,2026-09-03T14:37:30.231,222.900000,100.000000,254.259375,156.812500,800.000000
|
||||
2.017654,2026-09-03T14:37:31.241,223.359375,100.000000,254.587500,155.775000,800.000000
|
||||
3.029588,2026-09-03T14:37:32.253,203.062500,0.000000,279.431250,33.137500,1000.000000
|
||||
4.037959,2026-09-03T14:37:33.261,36.056250,0.000000,306.000000,-0.175000,1000.000000
|
||||
5.048918,2026-09-03T14:37:34.272,10.575000,0.000000,306.000000,-0.212500,1000.000000
|
||||
6.059525,2026-09-03T14:37:35.283,4.659375,0.000000,306.000000,-0.212500,1000.000000
|
||||
7.070708,2026-09-03T14:37:36.294,0.900000,0.000000,306.000000,-0.250000,1000.000000
|
||||
8.081269,2026-09-03T14:37:37.304,0.881250,0.000000,306.000000,-0.275000,1000.000000
|
||||
9.092312,2026-09-03T14:37:38.316,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
10.104664,2026-09-03T14:37:39.328,0.900000,0.000000,306.000000,-0.312500,1000.000000
|
||||
11.114669,2026-09-03T14:37:40.339,0.900000,0.000000,306.000000,-0.350000,1000.000000
|
||||
12.125869,2026-09-03T14:37:41.349,0.900000,0.000000,306.000000,-0.337500,1000.000000
|
||||
13.136267,2026-09-03T14:37:42.360,0.881250,0.000000,293.025000,-0.350000,1000.000000
|
||||
14.146329,2026-09-03T14:37:43.369,0.881250,0.000000,276.253125,-0.375000,1000.000000
|
||||
15.157293,2026-09-03T14:37:44.380,0.881250,0.000000,258.778125,-0.350000,1000.000000
|
||||
16.167387,2026-09-03T14:37:45.390,0.881250,0.000000,242.231250,-0.375000,1000.000000
|
||||
17.177553,2026-09-03T14:37:46.401,0.900000,0.000000,226.481250,-0.375000,1000.000000
|
||||
18.189422,2026-09-03T14:37:47.412,0.881250,0.000000,211.875000,-0.400000,1000.000000
|
||||
19.200252,2026-09-03T14:37:48.423,0.881250,0.000000,198.150000,-0.350000,1000.000000
|
||||
20.209384,2026-09-03T14:37:49.432,0.881250,0.000000,186.337500,-0.400000,1000.000000
|
||||
21.219451,2026-09-03T14:37:50.443,0.881250,0.000000,175.237500,-0.400000,1000.000000
|
||||
22.228531,2026-09-03T14:37:51.451,0.862500,0.000000,163.237500,-0.400000,1000.000000
|
||||
23.238794,2026-09-03T14:37:52.462,0.862500,0.000000,151.903125,-0.400000,1000.000000
|
||||
24.249379,2026-09-03T14:37:53.472,0.862500,0.000000,171.750000,-0.350000,1000.000000
|
||||
25.259381,2026-09-03T14:37:54.482,0.862500,0.000000,215.634375,-0.350000,1000.000000
|
||||
26.269218,2026-09-03T14:37:55.492,0.862500,0.000000,242.606250,-0.350000,1000.000000
|
||||
27.279854,2026-09-03T14:37:56.503,0.862500,0.000000,265.800000,-0.350000,1000.000000
|
||||
28.290244,2026-09-03T14:37:57.513,0.853125,0.000000,273.759375,-0.375000,1000.000000
|
||||
29.301363,2026-09-03T14:37:58.525,0.862500,0.000000,275.081250,-0.375000,1000.000000
|
||||
30.311467,2026-09-03T14:37:59.535,0.853125,0.000000,275.325000,-0.375000,1000.000000
|
||||
31.322894,2026-09-03T14:38:00.546,0.862500,0.000000,275.437500,-0.375000,1000.000000
|
||||
32.331937,2026-09-03T14:38:01.555,0.853125,0.000000,276.712500,-0.350000,1000.000000
|
||||
33.343316,2026-09-03T14:38:02.566,0.853125,0.000000,280.950000,-0.350000,1000.000000
|
||||
34.352015,2026-09-03T14:38:03.575,0.853125,0.000000,283.903125,-0.375000,1000.000000
|
||||
35.362602,2026-09-03T14:38:04.586,0.853125,0.000000,284.100000,-0.337500,1000.000000
|
||||
36.370819,2026-09-03T14:38:05.594,0.853125,0.000000,284.353125,-0.350000,1000.000000
|
||||
37.380717,2026-09-03T14:38:06.604,0.862500,0.000000,285.450000,-0.350000,1000.000000
|
||||
38.391094,2026-09-03T14:38:07.614,0.853125,0.000000,285.862500,-0.350000,1000.000000
|
||||
39.402279,2026-09-03T14:38:08.626,0.853125,0.000000,286.903125,-0.337500,1000.000000
|
||||
40.413082,2026-09-03T14:38:09.636,0.862500,0.000000,287.137500,-0.350000,1000.000000
|
||||
41.423721,2026-09-03T14:38:10.647,0.862500,0.000000,288.609375,-0.337500,1000.000000
|
||||
42.433300,2026-09-03T14:38:11.656,0.862500,0.000000,289.481250,-0.375000,1000.000000
|
||||
43.443357,2026-09-03T14:38:12.666,0.853125,0.000000,289.331250,-0.350000,1000.000000
|
||||
44.453689,2026-09-03T14:38:13.677,0.853125,0.000000,288.750000,-0.350000,1000.000000
|
||||
45.463437,2026-09-03T14:38:14.686,0.853125,0.000000,288.834375,-0.375000,1000.000000
|
||||
46.473149,2026-09-03T14:38:15.696,0.834375,0.000000,288.909375,-0.350000,1000.000000
|
||||
47.484513,2026-09-03T14:38:16.708,0.853125,0.000000,289.228125,-0.350000,1000.000000
|
||||
48.494721,2026-09-03T14:38:17.718,0.853125,0.000000,289.884375,-0.337500,1000.000000
|
||||
49.505305,2026-09-03T14:38:18.728,0.853125,0.000000,290.550000,-0.375000,1000.000000
|
||||
50.514115,2026-09-03T14:38:19.737,0.862500,0.000000,291.056250,-0.350000,1000.000000
|
||||
51.524633,2026-09-03T14:38:20.748,0.862500,0.000000,291.187500,-0.350000,1000.000000
|
||||
52.534418,2026-09-03T14:38:21.758,0.853125,0.000000,291.206250,-0.337500,1000.000000
|
||||
53.545409,2026-09-03T14:38:22.769,0.853125,0.000000,290.812500,-0.337500,1000.000000
|
||||
54.555086,2026-09-03T14:38:23.778,0.834375,0.000000,290.625000,-0.375000,1000.000000
|
||||
55.565609,2026-09-03T14:38:24.789,0.853125,0.000000,290.175000,-0.375000,1000.000000
|
||||
56.576575,2026-09-03T14:38:25.800,0.834375,0.000000,289.884375,-0.350000,1000.000000
|
||||
57.586980,2026-09-03T14:38:26.810,0.834375,0.000000,289.509375,-0.350000,1000.000000
|
||||
58.596223,2026-09-03T14:38:27.819,0.834375,0.000000,289.725000,-0.375000,1000.000000
|
||||
59.606634,2026-09-03T14:38:28.830,0.853125,0.000000,290.278125,-0.375000,1000.000000
|
||||
60.616196,2026-09-03T14:38:29.839,0.853125,0.000000,290.259375,-0.375000,1000.000000
|
||||
61.625949,2026-09-03T14:38:30.849,0.853125,0.000000,290.212500,-0.375000,1000.000000
|
||||
62.635355,2026-09-03T14:38:31.859,0.853125,0.000000,289.931250,-0.375000,1000.000000
|
||||
63.646221,2026-09-03T14:38:32.869,0.853125,0.000000,289.556250,-0.375000,1000.000000
|
||||
64.655401,2026-09-03T14:38:33.878,0.853125,0.000000,288.984375,-0.375000,1000.000000
|
||||
65.665560,2026-09-03T14:38:34.888,0.853125,0.000000,288.412500,-0.350000,1000.000000
|
||||
66.675832,2026-09-03T14:38:35.899,0.834375,0.000000,287.981250,-0.350000,1000.000000
|
||||
67.685689,2026-09-03T14:38:36.909,0.834375,0.000000,287.634375,-0.337500,1000.000000
|
||||
68.695668,2026-09-03T14:38:37.919,0.834375,0.000000,287.325000,-0.350000,1000.000000
|
||||
69.705494,2026-09-03T14:38:38.928,0.834375,0.000000,286.959375,-0.350000,1000.000000
|
||||
70.716592,2026-09-03T14:38:39.940,0.853125,0.000000,286.884375,-0.375000,1000.000000
|
||||
71.726608,2026-09-03T14:38:40.950,0.834375,0.000000,286.612500,-0.337500,1000.000000
|
||||
72.737259,2026-09-03T14:38:41.960,0.853125,0.000000,286.228125,-0.337500,1000.000000
|
||||
73.747217,2026-09-03T14:38:42.970,0.834375,0.000000,286.125000,-0.337500,1000.000000
|
||||
74.757698,2026-09-03T14:38:43.981,0.834375,0.000000,285.834375,-0.337500,1000.000000
|
||||
|
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,22 @@
|
||||
time_s,timestamp,flow_after_slm,opening_pct,pressure_before_kpa,pressure_after_kpa,motor_position
|
||||
0.000000,2026-09-03T14:41:56.141,158.231250,100.000000,170.634375,100.837500,800.000000
|
||||
1.008806,2026-09-03T14:41:57.150,157.809375,100.000000,170.728125,100.437500,800.000000
|
||||
2.018271,2026-09-03T14:41:58.159,157.378125,100.000000,170.803125,100.512500,800.000000
|
||||
3.028556,2026-09-03T14:41:59.170,157.003125,100.000000,170.559375,100.375000,800.000000
|
||||
4.038681,2026-09-03T14:42:00.180,157.003125,100.000000,170.503125,100.400000,800.000000
|
||||
5.049503,2026-09-03T14:42:01.191,157.753125,100.000000,170.850000,100.600000,800.000000
|
||||
6.060871,2026-09-03T14:42:02.202,157.837500,100.000000,171.075000,100.537500,800.000000
|
||||
7.072065,2026-09-03T14:42:03.213,157.950000,100.000000,171.009375,100.575000,800.000000
|
||||
8.083669,2026-09-03T14:42:04.225,157.950000,100.000000,170.906250,100.600000,800.000000
|
||||
9.094138,2026-09-03T14:42:05.235,84.112500,0.000000,261.731250,-0.212500,1000.000000
|
||||
10.103063,2026-09-03T14:42:06.244,18.553125,0.000000,306.000000,-0.300000,1000.000000
|
||||
11.112780,2026-09-03T14:42:07.254,6.487500,0.000000,306.000000,-0.312500,1000.000000
|
||||
12.122113,2026-09-03T14:42:08.263,1.303125,0.000000,306.000000,-0.337500,1000.000000
|
||||
13.132136,2026-09-03T14:42:09.273,0.900000,0.000000,305.325000,-0.350000,1000.000000
|
||||
14.142424,2026-09-03T14:42:10.284,0.900000,0.000000,304.078125,-0.350000,1000.000000
|
||||
15.151374,2026-09-03T14:42:11.293,0.900000,0.000000,303.281250,-0.337500,1000.000000
|
||||
16.161177,2026-09-03T14:42:12.302,0.881250,0.000000,302.550000,-0.337500,1000.000000
|
||||
17.172047,2026-09-03T14:42:13.314,0.881250,0.000000,302.184375,-0.350000,1000.000000
|
||||
18.182562,2026-09-03T14:42:14.324,0.881250,0.000000,301.809375,-0.337500,1000.000000
|
||||
19.193113,2026-09-03T14:42:15.334,0.881250,0.000000,301.612500,-0.337500,1000.000000
|
||||
20.203147,2026-09-03T14:42:16.344,0.881250,0.000000,301.275000,-0.350000,1000.000000
|
||||
|
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
@@ -0,0 +1,4 @@
|
||||
2026-09-03 16:10:35.432 INFO PLC 已连接 192.168.1.88:502,站号=1;模型=内置 valve_model_004.json(80 点)
|
||||
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 设备已安全断开
|
||||
@@ -0,0 +1,327 @@
|
||||
{
|
||||
"motor_open": 800.0,
|
||||
"motor_closed": 1000.0,
|
||||
"enforce_monotonic": false,
|
||||
"area_table": [
|
||||
[
|
||||
800.0,
|
||||
0.7209916054057561
|
||||
],
|
||||
[
|
||||
802.0,
|
||||
0.7125354555381634
|
||||
],
|
||||
[
|
||||
804.0,
|
||||
0.6561316030427767
|
||||
],
|
||||
[
|
||||
808.0,
|
||||
0.6598630623935171
|
||||
],
|
||||
[
|
||||
810.0,
|
||||
0.6753272742288868
|
||||
],
|
||||
[
|
||||
812.0,
|
||||
0.621104587742021
|
||||
],
|
||||
[
|
||||
814.0,
|
||||
0.6240506162781531
|
||||
],
|
||||
[
|
||||
816.0,
|
||||
0.595488483660327
|
||||
],
|
||||
[
|
||||
820.0,
|
||||
0.5924200631363882
|
||||
],
|
||||
[
|
||||
824.0,
|
||||
0.5416291218881875
|
||||
],
|
||||
[
|
||||
826.0,
|
||||
0.5394252385668694
|
||||
],
|
||||
[
|
||||
828.0,
|
||||
0.5163395000335028
|
||||
],
|
||||
[
|
||||
830.0,
|
||||
0.5386794264154983
|
||||
],
|
||||
[
|
||||
832.0,
|
||||
0.5087205083171691
|
||||
],
|
||||
[
|
||||
836.0,
|
||||
0.46458618805923835
|
||||
],
|
||||
[
|
||||
838.0,
|
||||
0.47364585752172367
|
||||
],
|
||||
[
|
||||
840.0,
|
||||
0.46827932318875193
|
||||
],
|
||||
[
|
||||
844.0,
|
||||
0.43243164500883
|
||||
],
|
||||
[
|
||||
848.0,
|
||||
0.3984505111170211
|
||||
],
|
||||
[
|
||||
850.0,
|
||||
0.413907876348738
|
||||
],
|
||||
[
|
||||
852.0,
|
||||
0.3813184002409792
|
||||
],
|
||||
[
|
||||
856.0,
|
||||
0.3620510591370111
|
||||
],
|
||||
[
|
||||
860.0,
|
||||
0.35385962516692854
|
||||
],
|
||||
[
|
||||
862.0,
|
||||
0.3447135247536444
|
||||
],
|
||||
[
|
||||
864.0,
|
||||
0.3156364126476435
|
||||
],
|
||||
[
|
||||
868.0,
|
||||
0.29675945681605137
|
||||
],
|
||||
[
|
||||
870.0,
|
||||
0.306501316808223
|
||||
],
|
||||
[
|
||||
872.0,
|
||||
0.27050112885120703
|
||||
],
|
||||
[
|
||||
874.0,
|
||||
0.27959628186642943
|
||||
],
|
||||
[
|
||||
876.0,
|
||||
0.25531630079673473
|
||||
],
|
||||
[
|
||||
880.0,
|
||||
0.2535114488327249
|
||||
],
|
||||
[
|
||||
884.0,
|
||||
0.225092732619715
|
||||
],
|
||||
[
|
||||
886.0,
|
||||
0.22841807147673635
|
||||
],
|
||||
[
|
||||
888.0,
|
||||
0.2086511681620107
|
||||
],
|
||||
[
|
||||
890.0,
|
||||
0.2218855517263943
|
||||
],
|
||||
[
|
||||
892.0,
|
||||
0.20075819923793375
|
||||
],
|
||||
[
|
||||
896.0,
|
||||
0.18918524712906334
|
||||
],
|
||||
[
|
||||
898.0,
|
||||
0.19215080166026982
|
||||
],
|
||||
[
|
||||
900.0,
|
||||
0.18807050761212185
|
||||
],
|
||||
[
|
||||
904.0,
|
||||
0.17245083700454789
|
||||
],
|
||||
[
|
||||
908.0,
|
||||
0.16153923372120163
|
||||
],
|
||||
[
|
||||
910.0,
|
||||
0.16985605651603905
|
||||
],
|
||||
[
|
||||
912.0,
|
||||
0.15429304713019132
|
||||
],
|
||||
[
|
||||
916.0,
|
||||
0.151574549449086
|
||||
],
|
||||
[
|
||||
920.0,
|
||||
0.1530920502767755
|
||||
],
|
||||
[
|
||||
922.0,
|
||||
0.14979075485074195
|
||||
],
|
||||
[
|
||||
924.0,
|
||||
0.1428763734264717
|
||||
],
|
||||
[
|
||||
928.0,
|
||||
0.14159844506284675
|
||||
],
|
||||
[
|
||||
930.0,
|
||||
0.1481317007247288
|
||||
],
|
||||
[
|
||||
932.0,
|
||||
0.13731846151118046
|
||||
],
|
||||
[
|
||||
934.0,
|
||||
0.13994004337287919
|
||||
],
|
||||
[
|
||||
936.0,
|
||||
0.1344586476808378
|
||||
],
|
||||
[
|
||||
940.0,
|
||||
0.13892390786199926
|
||||
],
|
||||
[
|
||||
944.0,
|
||||
0.13143952256792465
|
||||
],
|
||||
[
|
||||
946.0,
|
||||
0.13338917913072393
|
||||
],
|
||||
[
|
||||
948.0,
|
||||
0.12960342436338804
|
||||
],
|
||||
[
|
||||
950.0,
|
||||
0.14035651644159308
|
||||
],
|
||||
[
|
||||
952.0,
|
||||
0.13085895406830145
|
||||
],
|
||||
[
|
||||
956.0,
|
||||
0.12884698464592212
|
||||
],
|
||||
[
|
||||
958.0,
|
||||
0.1340123387779701
|
||||
],
|
||||
[
|
||||
960.0,
|
||||
0.1353515916092655
|
||||
],
|
||||
[
|
||||
964.0,
|
||||
0.12802772183117378
|
||||
],
|
||||
[
|
||||
968.0,
|
||||
0.12244258908969449
|
||||
],
|
||||
[
|
||||
970.0,
|
||||
0.12014602858505499
|
||||
],
|
||||
[
|
||||
972.0,
|
||||
0.09119982507825447
|
||||
],
|
||||
[
|
||||
973.0,
|
||||
0.09502834500740251
|
||||
],
|
||||
[
|
||||
976.0,
|
||||
0.06089048620458142
|
||||
],
|
||||
[
|
||||
979.0,
|
||||
0.010700147302522556
|
||||
],
|
||||
[
|
||||
980.0,
|
||||
0.002096375744184619
|
||||
],
|
||||
[
|
||||
982.0,
|
||||
0.0020885117831348173
|
||||
],
|
||||
[
|
||||
984.0,
|
||||
0.0020668385196096484
|
||||
],
|
||||
[
|
||||
985.0,
|
||||
0.002069140121524581
|
||||
],
|
||||
[
|
||||
988.0,
|
||||
0.002072106769105725
|
||||
],
|
||||
[
|
||||
990.0,
|
||||
0.0021514223899834284
|
||||
],
|
||||
[
|
||||
991.0,
|
||||
0.002063386116737249
|
||||
],
|
||||
[
|
||||
992.0,
|
||||
0.002091005339716443
|
||||
],
|
||||
[
|
||||
994.0,
|
||||
0.00207971813405268
|
||||
],
|
||||
[
|
||||
996.0,
|
||||
0.0020990609464187075
|
||||
],
|
||||
[
|
||||
997.0,
|
||||
0.0020639615172159823
|
||||
],
|
||||
[
|
||||
1000.0,
|
||||
0.0020914299290714883
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
气体流量控制程序使用说明
|
||||
========================
|
||||
|
||||
一、交付文件
|
||||
|
||||
1. FlowControl.exe
|
||||
双击运行的 Windows 单文件程序,不需要安装 Python。
|
||||
|
||||
2. valve_model_004.json
|
||||
内置模型的外置备份和自定义模型格式参考。程序默认使用 EXE 内部自带的
|
||||
valve_model_004.json,外置文件被移动或删除不会影响默认模型。
|
||||
|
||||
3. 使用说明.txt
|
||||
本文件。
|
||||
|
||||
|
||||
二、重要安全说明
|
||||
|
||||
本程序会直接控制真实阀门。首次连接前必须确认 PLC IP、端口、站号、寄存器
|
||||
地址、传感器量程、阀门动作方向和管路允许压力,并准备独立的机械或电气急停。
|
||||
|
||||
根据当前工程的安全策略:
|
||||
|
||||
- 连接 PLC 成功后,程序会先命令阀门至 100% 开度(电机行程 800);
|
||||
- 点击“断开设备”、关闭窗口或发生控制故障时,程序会再次尝试命令阀门至
|
||||
100% 开度,然后断开 PLC;
|
||||
- “100% 开度”是当前软件定义的逻辑位置,现场必须再次确认真实动作方向。
|
||||
|
||||
|
||||
三、正常操作
|
||||
|
||||
1. 双击 FlowControl.exe。程序启动后不会自动连接设备。
|
||||
2. 核对 PLC 地址、端口和站号。默认值为 192.168.1.88、502、1。
|
||||
3. 默认模型显示为“内置 valve_model_004.json(80 点)”。
|
||||
4. 点击“连接设备”。连接成功后程序先设置 100% 开度,并进入传感器监测。
|
||||
5. 输入 0~300 SLM 的目标值,点击“设置目标流量”或按回车。首次设置即启动
|
||||
10 Hz 闭环;运行中可以继续修改目标。
|
||||
6. 观察当前流量、阀前压力、阀后压力、下发总开度、电机行程、前馈开度、
|
||||
PI 修正量和 FAR/NEAR 模式。
|
||||
7. 结束时点击“断开设备”,等待状态变为“设备已断开”后再关闭程序。
|
||||
|
||||
|
||||
四、选择其他 A_eff 模型
|
||||
|
||||
只能在未连接或故障已断开的状态下切换模型。点击“选择 JSON…”后选择由本工程
|
||||
辨识工具生成的模型。程序会检查 JSON 结构、有效数值、标定点数量以及行程范围
|
||||
是否与当前 800~1000 配置一致。无效模型不会用于控制。
|
||||
|
||||
点击“使用内置”可随时恢复 valve_model_004.json。该模型允许非单调特性;同一
|
||||
A_eff 存在多个反解行程时,沿用工程算法选择更接近关闭端的较大行程。
|
||||
|
||||
|
||||
五、状态与故障
|
||||
|
||||
- 已连接 / 监测中:设备已连接,阀门保持 100% 开度,尚未启动闭环。
|
||||
- 闭环运行中:目标流量已经生效,程序以 10 Hz 读取传感器并下发总开度。
|
||||
- 故障 / 已断开:出现流量越界、压力超限、连续读取失败、写入失败、控制周期
|
||||
严重超时或通信异常;程序已停止闭环并执行安全断开。
|
||||
|
||||
运行日志保存在 FlowControl.exe 同目录下的 logs\flow_control.log。遇到问题时请
|
||||
保留该日志。日志最多约 2 MB,自动保留 3 份历史文件。
|
||||
|
||||
|
||||
六、当前默认硬件参数
|
||||
|
||||
- 阀后流量:D700,量程 0~300 SLM
|
||||
- 阀后压力:D710,量程 0~400 kPa
|
||||
- 阀前压力:D720,量程 0~300 kPa
|
||||
- 电机命令:D730,行程 0~1000 映射到模拟量 0~32000
|
||||
- 有效控制行程:800(100% 开度)~1000(0% 开度)
|
||||
- 压力保护上限:500 kPa
|
||||
|
||||
首次真实设备投用必须由现场人员确认上述参数。
|
||||