first commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
"""气体流量闭环控制程序入口。
|
||||
|
||||
示例:
|
||||
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())
|
||||
Reference in New Issue
Block a user