214 lines
6.8 KiB
Python
214 lines
6.8 KiB
Python
"""气体流量闭环控制程序入口。
|
|
|
|
示例:
|
|
python main.py --target 50
|
|
|
|
程序启动后会先下发关闭位置,再开始闭环。Ctrl+C、控制故障或其他异常都会
|
|
再次尝试关阀并断开 MT2-AM8。
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
from pathlib import Path
|
|
import sys
|
|
import time
|
|
|
|
import config
|
|
from controllers import IncrementalPID
|
|
from flow_control import FlowControlFault, FlowControlLoop
|
|
|
|
|
|
def build_logger():
|
|
log_dir = Path(__file__).resolve().parent / config.LOG_DIRECTORY
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
log_path = log_dir / config.LOG_FILE_NAME
|
|
|
|
logger = logging.getLogger("flow_control")
|
|
logger.setLevel(logging.INFO)
|
|
logger.handlers.clear()
|
|
|
|
formatter = logging.Formatter(
|
|
"%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
file_handler = logging.FileHandler(log_path, encoding="utf-8")
|
|
file_handler.setFormatter(formatter)
|
|
logger.addHandler(file_handler)
|
|
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setFormatter(formatter)
|
|
logger.addHandler(console_handler)
|
|
return logger
|
|
|
|
|
|
def build_control_loop(hardware, logger):
|
|
pid = IncrementalPID(
|
|
kp=config.PID_KP,
|
|
ki=config.PID_KI,
|
|
kd=config.PID_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_PCT_S,
|
|
)
|
|
return FlowControlLoop(
|
|
hardware=hardware,
|
|
pid=pid,
|
|
period_s=config.CONTROL_PERIOD_S,
|
|
flow_channel=config.FLOW_INPUT_CHANNEL,
|
|
pressure_channel=config.PRESSURE_INPUT_CHANNEL,
|
|
motor_channel=config.MOTOR_OUTPUT_CHANNEL,
|
|
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
|
|
),
|
|
logger=logger,
|
|
)
|
|
|
|
|
|
def parse_args(argv=None):
|
|
parser = argparse.ArgumentParser(description="MT2-AM8 气体流量 PID 控制")
|
|
parser.add_argument(
|
|
"--target",
|
|
type=float,
|
|
required=True,
|
|
help=(
|
|
"目标流量 SLM,允许范围 "
|
|
f"{config.TARGET_FLOW_MIN_SLM}~{config.TARGET_FLOW_MAX_SLM}"
|
|
),
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def format_status(result):
|
|
flow_text = (
|
|
"--" if result.measured_flow_slm is None
|
|
else f"{result.measured_flow_slm:.2f}"
|
|
)
|
|
pressure_text = (
|
|
"--" if result.pressure_kpa is None
|
|
else f"{result.pressure_kpa:.2f}"
|
|
)
|
|
return (
|
|
f"状态={result.status} 目标={result.target_flow_slm:.2f} SLM "
|
|
f"实际={flow_text} SLM 压力={pressure_text} kPa "
|
|
f"开度={result.opening_pct:.2f}% "
|
|
f"行程={result.motor_position:.1f} dt={result.actual_dt_s:.3f}s"
|
|
)
|
|
|
|
|
|
def run(target_flow_slm):
|
|
config.validate_config()
|
|
logger = build_logger()
|
|
try:
|
|
from PcControl import MT2AM8Client
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name and exc.name.startswith("pymodbus"):
|
|
logger.critical(
|
|
"缺少 PcControl.py 所需的 pymodbus;请在运行本工程的 Python "
|
|
"环境中安装与现有硬件代码兼容的 pymodbus 版本"
|
|
)
|
|
return 6
|
|
raise
|
|
|
|
hardware = MT2AM8Client(
|
|
host=config.MT2AM8_HOST,
|
|
port=config.MT2AM8_PORT,
|
|
slave_id=config.MT2AM8_SLAVE_ID,
|
|
pressure_range=config.PRESSURE_RANGE_KPA,
|
|
flow_range=config.FLOW_METER_RANGE_SLM,
|
|
)
|
|
controller = build_control_loop(hardware, logger)
|
|
controller.set_target_flow(target_flow_slm)
|
|
|
|
connected = False
|
|
exit_code = 0
|
|
try:
|
|
logger.info(
|
|
"正在连接 MT2-AM8 %s:%s,目标流量=%.3f SLM",
|
|
config.MT2AM8_HOST,
|
|
config.MT2AM8_PORT,
|
|
target_flow_slm,
|
|
)
|
|
connected = bool(hardware.connect())
|
|
if not connected:
|
|
raise FlowControlFault(
|
|
"DEVICE_CONNECT_FAILED",
|
|
"STARTUP",
|
|
"无法连接 MT2-AM8",
|
|
{
|
|
"host": config.MT2AM8_HOST,
|
|
"port": config.MT2AM8_PORT,
|
|
"slave_id": config.MT2AM8_SLAVE_ID,
|
|
},
|
|
)
|
|
|
|
if not controller.safe_close("STARTUP"):
|
|
raise FlowControlFault(
|
|
"SAFE_CLOSE_FAILED",
|
|
"STARTUP",
|
|
"启动前无法确认阀门关闭命令已成功写入",
|
|
{"motor_closed_position": config.MOTOR_CLOSED_POSITION},
|
|
)
|
|
|
|
controller.start(initial_opening=0.0)
|
|
logger.info("闭环控制已启动;按 Ctrl+C 停止")
|
|
|
|
next_deadline = time.perf_counter()
|
|
next_status_time = next_deadline
|
|
while True:
|
|
now = time.perf_counter()
|
|
result = controller.step(now=now)
|
|
|
|
if now >= next_status_time or result.status != "OK":
|
|
logger.info(format_status(result))
|
|
next_status_time = now + config.STATUS_PRINT_PERIOD_S
|
|
|
|
next_deadline += config.CONTROL_PERIOD_S
|
|
sleep_s = next_deadline - time.perf_counter()
|
|
if sleep_s > 0:
|
|
time.sleep(sleep_s)
|
|
else:
|
|
# 丢弃已经错过的节拍,避免连续快速补跑 PID。
|
|
next_deadline = time.perf_counter()
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("收到 Ctrl+C,正在停止控制")
|
|
except FlowControlFault as exc:
|
|
exit_code = 2
|
|
logger.critical("控制故障:%s", exc)
|
|
except Exception:
|
|
exit_code = 3
|
|
logger.exception("未处理异常,系统将进入安全关闭")
|
|
finally:
|
|
if connected:
|
|
close_ok = controller.safe_close("PROGRAM_EXIT")
|
|
if not close_ok:
|
|
exit_code = max(exit_code, 4)
|
|
logger.critical("程序退出时安全关阀失败,请立即人工检查")
|
|
try:
|
|
hardware.disconnect()
|
|
except Exception:
|
|
exit_code = max(exit_code, 5)
|
|
logger.exception("断开 MT2-AM8 时发生异常")
|
|
logger.info("程序结束,退出码=%d", exit_code)
|
|
return exit_code
|
|
|
|
|
|
def main(argv=None):
|
|
args = parse_args(argv)
|
|
return run(args.target)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|