自动进样、自动连接与断点续传功能架构说明

本文档记录了色谱工作站中关于“开机自动连接”、“自动进样循环”以及“意外关机断点续传”功能的核心设计逻辑、触发时机以及关键防坑经验。

一、 功能概述

  1. 开机自动连接主板:软件启动后,底层通讯建立时自动选中对应的主板设备并进入分析视角,无需用户手动双击。
  2. 自动进样循环控制:根据用户设置的“循环间隔”和“循环次数”,在单次分析指令成功下发后,精准倒计时并自动触发下一次分析。
  3. 意外关机断点续传:当自动进样任务未完成时如果软件被关闭(意外断电或手动关闭),下次打开软件并连接上仪器后,系统会自动恢复剩余的进样任务,无缝衔接。

二、 核心逻辑与触发时机(极其重要)

1. 自动连接的触发时机

错误做法:在 Form_Load 时用定时器盲等,或在底层 TcpServerSocket 收到第一个数据包时立即触发。 正确做法:在主界面(如 FormMain.cs)的 UpdateChromDevice 方法中,检查 UI 层对应的设备图标状态。只有当 ListViewImageIndex 明确变为 20(绿灯,已连接)或 22(蓝灯,选中)时,才代表底层对象和 UI 状态已完全初始化同步,此时再触发自动连接。

2. 自动循环定时器的触发时机

错误做法:将循环计时器绑定在“分析停止/完成(Answer150/Command 22)”的回传上。这会导致单针分析结束后定时器被重置,使得实际间隔时间变为(单针时间+设定间隔),并清空进样计数。 正确做法:自动进样循环逻辑(OnInstrumentStarted只能绑定在底层接收到分析开始指令响应(Answer146 / Command 18)时触发。

3. 断点续传的延时与 UI 同步

错误做法:设备刚变绿就立刻发送恢复指令(会导致硬件处理不过来被丢弃);恢复状态时只修改后台变量不刷新 UI(会导致用户手动操作时,从空 UI 读取的值覆盖掉正确的后台变量)。 正确做法

  • 安全延时缓冲:检测到需要恢复任务时,必须引入适当的延时(目前设置为 Task.Delay(8000) 即 8 秒),等待硬件初始化彻底完成、状态稳定后再下发指令。抛弃不可靠的 StateYiqi == 3 状态轮询,避免死锁。
  • UI 强同步:在从持久化文件恢复循环时间(RestoreCycleMin)时,必须通过 Invoke 同步刷新右侧控制面板的输入框(如 maskedTextBox20.Text)。

三、 状态持久化机制

为了跨进程(软件重启)保留自动进样状态,系统引入了本地文件持久化机制:

  • 存储文件:程序根目录下的 AutoInjState.ini
  • 存储格式[是否运行中(bool)],[已完成组数(int)],[最大组数(int)],[循环间隔(float)]
  • 写入时机:每次开始新一轮循环、完成一组进样、手动停止循环时,调用 Instrument.SaveAutoInjState 更新该文件。
  • 读取时机:设备自动连接变绿时,通过 Instrument.LoadAutoInjState 读取,并判断是否需要恢复。

四、 关键文件与方法映射

文件名关键方法/逻辑说明
Instrument.csglobalAutoInjRunning
SaveAutoInjState
LoadAutoInjState
RestoreCycleMin
自动进样的核心大脑,负责计时器控制、状态读写、防并发冲突锁。
TcpServerSocket.csAnswer146
Answer150
底层通讯。负责在收到 146(开始响应)时向第一台设备广播 OnInstrumentStarted;剔除了 150(停止响应)的错误广播。
FormMain.cs (及其他主界面)UpdateChromDevice监听设备变绿事件。包含 8 秒安全延时的断点续传指令下发逻辑。
InsDeviceCtrl.csbutton14_Click右侧控制面板逻辑。负责下发手动停止指令并调用 CancelAutoInjectorCycle() 终止后台自动循环。

五、 防坑指南与维护注意事项

  1. 文本匹配陷阱:在多语言或不同 UI 版本中,控件的 Text 可能存在空格差异(如 "StartAll" 和 "Start All")。在判断按钮状态时,必须使用 .Contains 进行宽容匹配,切忌使用绝对等于 ==
  2. 多通道并发爆炸:软件支持多通道(4台设备),但在广播 OnInstrumentStarted 时,切忌使用 foreach 遍历所有通道并触发,这会导致启动 4 个并发的定时器。只应向 pageInstrus[0] 发送主控信号。
  3. 线程访问 UI:后台定时器或 TCP 接收线程需要修改 UI 时,必须使用 Invoke,且由于部分子窗体可能随时被销毁,推荐获取最顶层的 MainForm 接口来进行跨线程调度。