diff --git a/experiment_monitor.py b/experiment_monitor.py index 74e0f21..1d05847 100644 --- a/experiment_monitor.py +++ b/experiment_monitor.py @@ -115,45 +115,71 @@ class ExperimentStateMonitor: """监控循环 - 定时查询并检测状态变化""" try: logger.info(f"[监控循环] 开始监控实验{self.experiment_id}") + logger.info( + f"[监控循环] 配置信息: status_field={self.query_config.get('status_field')}, " + f"status_values={self.query_config.get('status_values')}" + ) while not self._stop_event.is_set(): try: # 查询当前状态 - logger.debug(f"[监控循环] 实验{self.experiment_id}开始查询状态...") + logger.info(f"[监控循环] 实验{self.experiment_id}开始查询状态...") current_state = self._query_current_state() logger.info( - f"[监控循环] 实验{self.experiment_id}查询结果: 当前状态={current_state}, " - f"上一状态={self._last_state}" + f"[监控循环] 实验{self.experiment_id}查询结果: 当前状态={current_state} (type={type(current_state).__name__ if current_state else 'None'}), " + f"上一状态={self._last_state} (type={type(self._last_state).__name__ if self._last_state else 'None'})" ) if current_state is not None: + # 数值比较函数:支持字符串和数值的灵活比较 + def states_equal(state1: str, state2: str) -> bool: + """比较两个状态是否相等,支持数值比较""" + if state1 == state2: + return True + try: + return float(state1) == float(state2) + except (ValueError, TypeError): + return False + # 检测状态变化 if self._last_state is None: - # 第一次查询,记录初始状态 + # 第一次查询,记录初始状态并检查是否为开始状态 self._last_state = current_state logger.info( f"[监控循环] 实验{self.experiment_id}初始状态: {current_state}" ) - elif current_state != self._last_state: - # 状态发生变化 - old_state = self._last_state - self._last_state = current_state - - logger.info( - f"[监控循环] 🔄 实验{self.experiment_id}状态变化: {old_state} -> {current_state}" - ) - - # 处理状态变化 - self._handle_state_change(old_state, current_state) + # 检查初始状态是否为开始值,如果是则触发开始事件 + self._check_initial_state(current_state) else: - logger.debug( - f"[监控循环] 实验{self.experiment_id}状态无变化: {current_state}" + # 比较状态是否变化 + is_equal = states_equal(current_state, self._last_state) + logger.info( + f"[监控循环-比较] 实验{self.experiment_id}: " + f"current_state='{current_state}', _last_state='{self._last_state}', " + f"states_equal={is_equal}, 字符串比较={(current_state == self._last_state)}" ) + + if not is_equal: + # 状态发生变化(使用数值比较) + old_state = self._last_state + self._last_state = current_state + + logger.info( + f"[监控循环] 🔄 实验{self.experiment_id}状态变化: {old_state} -> {current_state}" + ) + + # 处理状态变化 + self._handle_state_change(old_state, current_state) + else: + logger.info( + f"[监控循环] 实验{self.experiment_id}状态无变化: {current_state}" + ) else: - logger.debug(f"[监控循环] 实验{self.experiment_id}暂无数据") + logger.info(f"[监控循环] 实验{self.experiment_id}暂无数据") # 等待下一个轮询周期 + logger.info(f"[监控循环] 实验{self.experiment_id}等待{self.poll_interval}秒后继续...") self._stop_event.wait(self.poll_interval) except Exception as e: @@ -237,9 +263,10 @@ class ExperimentStateMonitor: # 构建时间范围:查询最近1小时的数据 time_range = "-1h" - logger.debug( + logger.info( f"[查询] 实验{self.experiment_id}查询InfluxDB: " - f"bucket={bucket}, measurement={measurement}, fields={fields}" + f"bucket={bucket}, measurement={measurement}, fields={fields}, " + f"filters={filters}, status_field={status_field}" ) # 执行查询 @@ -252,23 +279,52 @@ class ExperimentStateMonitor: ) if df.empty: - logger.debug(f"[查询] 实验{self.experiment_id}查询结果为空") + logger.info(f"[查询] 实验{self.experiment_id}查询结果为空(DataFrame为空)") return None + logger.info( + f"[查询] 实验{self.experiment_id}查询到 {len(df)} 条记录, " + f"列: {list(df.columns)}" + ) + # 获取最新的状态值 if '_value' in df.columns and '_field' in df.columns: + # 显示所有字段 + unique_fields = df['_field'].unique() + logger.info( + f"[查询] 实验{self.experiment_id}查询结果包含字段: {list(unique_fields)}" + ) + # 筛选出指定字段的数据 status_data = df[df['_field'] == status_field] + logger.info( + f"[查询] 实验{self.experiment_id}筛选字段'{status_field}'后有 {len(status_data)} 条记录" + ) + if not status_data.empty: # 按时间排序,取最后一条记录 if '_time' in status_data.columns: status_data = status_data.sort_values('_time') + logger.info( + f"[查询] 实验{self.experiment_id}时间范围: " + f"{status_data['_time'].iloc[0]} 到 {status_data['_time'].iloc[-1]}" + ) + + # 显示最近几条记录 + if len(status_data) > 0: + recent_count = min(5, len(status_data)) + logger.info( + f"[查询] 实验{self.experiment_id}最近{recent_count}条记录的值: " + f"{list(status_data['_value'].tail(recent_count))}" + ) latest_value = status_data['_value'].iloc[-1] - logger.debug( + latest_time = status_data['_time'].iloc[-1] if '_time' in status_data.columns else 'N/A' + + logger.info( f"[查询] 实验{self.experiment_id}最新状态值: {latest_value} " - f"(字段: {status_field})" + f"(类型: {type(latest_value).__name__}, 字段: {status_field}, 时间: {latest_time})" ) return str(latest_value) else: @@ -350,6 +406,24 @@ class ExperimentStateMonitor: f"(连续失败{self._consecutive_failures}/{self.max_consecutive_failures}次)" ) + def _try_float_compare(self, val1: str, val2: str) -> str: + """ + 尝试浮点数比较,用于调试 + + Args: + val1: 值1 + val2: 值2 + + Returns: + 比较结果的字符串描述 + """ + try: + f1 = float(val1) + f2 = float(val2) + return f"float({val1})={f1}, float({val2})={f2}, 相等={f1 == f2}" + except (ValueError, TypeError) as e: + return f"转换失败: {e}" + def _handle_state_change(self, old_state: str, new_state: str) -> None: """ 处理状态变化 @@ -365,24 +439,126 @@ class ExperimentStateMonitor: logger.info( f"[状态变化] 实验{self.experiment_id}: {old_state} -> {new_state}, " - f"开始值={start_value}, 结束值={end_value}" + f"开始值配置={start_value} (type={type(start_value).__name__}), " + f"结束值配置={end_value} (type={type(end_value).__name__})" ) + # 类型转换和比较日志 + old_state_str = str(old_state) + new_state_str = str(new_state) + start_value_str = str(start_value) + end_value_str = str(end_value) + + logger.info( + f"[状态变化-详细] 实验{self.experiment_id}: " + f"old_state_str='{old_state_str}' (type={type(old_state).__name__}), " + f"new_state_str='{new_state_str}' (type={type(new_state).__name__}), " + f"start_value_str='{start_value_str}', " + f"end_value_str='{end_value_str}'" + ) + + logger.info( + f"[状态变化-标志] 实验{self.experiment_id}: " + f"_experiment_started={self._experiment_started}, " + f"_experiment_ended={self._experiment_ended}" + ) + + # 数值比较函数:支持字符串和数值的灵活比较 + def values_equal(val1: str, val2: str) -> bool: + """比较两个值是否相等,支持数值比较""" + # 先尝试字符串直接比较 + if val1 == val2: + return True + + # 尝试作为浮点数比较(处理 '1' vs '1.0' 的情况) + try: + return float(val1) == float(val2) + except (ValueError, TypeError): + return False + # 检测实验开始:状态从非开始值变为开始值 - if not self._experiment_started and str(new_state) == str(start_value): + is_start_match = values_equal(new_state_str, start_value_str) + is_start_condition = not self._experiment_started and is_start_match + + logger.info( + f"[状态变化-开始检测] 实验{self.experiment_id}: " + f"not _experiment_started={not self._experiment_started}, " + f"values_equal(new_state_str, start_value_str)={is_start_match}, " + f"触发开始={is_start_condition}" + ) + + if is_start_condition: + logger.info(f"[状态变化] ✅ 实验{self.experiment_id}满足开始条件,调用 _on_experiment_started()") self._on_experiment_started() - # 检测实验结束:状态从开始值变回非开始值 - elif self._experiment_started and str(new_state) == str(end_value): + # 检测实验结束:状态从开始值变回结束值 + is_end_match = values_equal(new_state_str, end_value_str) + is_end_condition = self._experiment_started and is_end_match + + logger.info( + f"[状态变化-结束检测] 实验{self.experiment_id}: " + f"_experiment_started={self._experiment_started}, " + f"new_state_str='{new_state_str}', end_value_str='{end_value_str}', " + f"values_equal(new_state_str, end_value_str)={is_end_match}, " + f"字符串比较=(new_state_str == end_value_str)={(new_state_str == end_value_str)}, " + f"浮点数比较尝试={self._try_float_compare(new_state_str, end_value_str)}, " + f"触发结束={is_end_condition}" + ) + + if is_end_condition: + logger.info(f"[状态变化] ✅ 实验{self.experiment_id}满足结束条件,调用 _on_experiment_ended()") self._on_experiment_ended() + elif self._experiment_started and not is_end_match and not is_start_match: + # 只在状态既不是开始值也不是结束值时才警告(意外的中间状态) + logger.warning( + f"[状态变化] ⚠️ 实验{self.experiment_id}检测到意外状态: " + f"new_state_str='{new_state_str}' (期望: start='{start_value_str}' 或 end='{end_value_str}')" + ) # 触发回调 if self.on_state_changed: + logger.debug(f"[状态变化] 实验{self.experiment_id}触发状态变化回调") self.on_state_changed(old_state, new_state) except Exception as e: logger.error(f"[状态变化] 实验{self.experiment_id}处理失败: {e}", exc_info=True) + def _check_initial_state(self, initial_state: str) -> None: + """ + 检查初始状态,如果是开始值则触发开始事件 + + Args: + initial_state: 初始状态值 + """ + try: + status_values = self.query_config.get('status_values', {}) + start_value = str(status_values.get('start', '')) + initial_state_str = str(initial_state) + + # 数值比较函数 + def values_equal(val1: str, val2: str) -> bool: + if val1 == val2: + return True + try: + return float(val1) == float(val2) + except (ValueError, TypeError): + return False + + is_start_match = values_equal(initial_state_str, start_value) + + logger.info( + f"[初始状态检查] 实验{self.experiment_id}: " + f"initial_state='{initial_state_str}', start_value='{start_value}', " + f"匹配={is_start_match}" + ) + + if is_start_match and not self._experiment_started: + logger.info(f"[初始状态检查] ✅ 实验{self.experiment_id}初始状态即为开始状态,触发开始事件") + self._on_experiment_started() + + except Exception as e: + logger.error(f"[初始状态检查] 实验{self.experiment_id}检查失败: {e}", exc_info=True) + def _on_experiment_started(self) -> None: """实验开始事件处理""" try: @@ -421,6 +597,19 @@ class ExperimentStateMonitor: f"记录时间: {self._end_time_recorded}" ) + # 验证实验时长 + if not self._validate_experiment_duration(): + logger.warning( + f"[实验结束] ⚠️ 实验{self.experiment_id}时长不足3.5小时," + f"标记为异常数据,不入库" + ) + # 标记为作废状态 + self._mark_experiment_as_invalid() + # 设置停止标志 + self._stop_event.set() + logger.info(f"[实验结束] 🛑 异常实验已标记作废,监控退出") + return + # 更新数据库:设置end_ts self._update_experiment_end_time(self._end_time_recorded) @@ -465,6 +654,82 @@ class ExperimentStateMonitor: ) return False + def _validate_experiment_duration(self) -> bool: + """验证实验时长是否满足最小要求(3.5小时)""" + try: + if not self._start_time_recorded or not self._end_time_recorded: + logger.warning(f"[时长验证] 实验{self.experiment_id}缺少开始或结束时间") + return False + + # 解析时间 + start_dt = datetime.datetime.fromisoformat(self._start_time_recorded) + end_dt = datetime.datetime.fromisoformat(self._end_time_recorded) + + # 计算时长(小时) + duration = (end_dt - start_dt).total_seconds() / 3600.0 + + logger.info( + f"[时长验证] 实验{self.experiment_id}持续时长: {duration:.2f}小时 " + f"(开始: {self._start_time_recorded}, 结束: {self._end_time_recorded})" + ) + + # 最小时长要求:3.5小时 + MIN_DURATION_HOURS = 3.5 + + if duration < MIN_DURATION_HOURS: + logger.warning( + f"[时长验证] ❌ 实验{self.experiment_id}时长{duration:.2f}小时 < " + f"{MIN_DURATION_HOURS}小时,不满足要求" + ) + return False + + logger.info( + f"[时长验证] ✅ 实验{self.experiment_id}时长{duration:.2f}小时 >= " + f"{MIN_DURATION_HOURS}小时,满足要求" + ) + return True + + except Exception as e: + logger.error( + f"[时长验证] 实验{self.experiment_id}验证失败: {e}", + exc_info=True + ) + return False + + def _mark_experiment_as_invalid(self) -> None: + """标记实验为作废状态(异常数据)""" + try: + from pathlib import Path + db_path = Path(__file__).parent / "experiments.db" + + db = sqlite3.connect(str(db_path)) + cur = db.cursor() + + # 更新实验状态为作废,设置 is_terminated=1,并记录结束时间 + cur.execute( + """UPDATE experiments + SET end_ts=?, + is_terminated=1, + remark=CASE + WHEN remark IS NULL OR remark='' THEN '作废-时长不足3.5小时' + ELSE remark || ' [作废-时长不足3.5小时]' + END + WHERE id=?""", + (self._end_time_recorded, self.experiment_id) + ) + db.commit() + db.close() + + logger.info( + f"[作废标记] ✅ 实验{self.experiment_id}已标记为作废状态 (is_terminated=1)" + ) + + except Exception as e: + logger.error( + f"[作废标记] 实验{self.experiment_id}标记失败: {e}", + exc_info=True + ) + def _update_experiment_start_time(self, start_time: str) -> None: """更新实验记录的开始时间""" try: diff --git a/logger.py b/logger.py index 3e0ff60..41f14ae 100644 --- a/logger.py +++ b/logger.py @@ -26,9 +26,9 @@ def get_logger(name: str = "docx_creator") -> logging.Logger: fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") handler.setFormatter(fmt) logger.addHandler(handler) - # also log warnings+ to stderr for debugging + # also log INFO+ to stderr for debugging stream = logging.StreamHandler() - stream.setLevel(logging.WARNING) + stream.setLevel(logging.INFO) # 改为 INFO 级别 stream.setFormatter(fmt) logger.addHandler(stream) _LOGGER = logger diff --git a/pcm-influxdb-debug.py b/pcm-influxdb-debug.py index 83514ee..2aef7f2 100644 --- a/pcm-influxdb-debug.py +++ b/pcm-influxdb-debug.py @@ -887,21 +887,24 @@ class Breaker: self.optFlag = 3 def alarming(self): + """报警时:红灯亮+蜂鸣器响,绿灯灭""" if not self.alarm and self.closed & 0xFF == 0xF0: self.exeCmd('turnOffGreen') self.exeCmd('turnOnRed') self.exeCmd('turnOnAlarm') + self.alarm = 1 # 设置报警标志 def unalarming(self): + """解除报警:根据合闸状态控制指示灯""" if self.alarm: + self.exeCmd('turnOffRed') + self.exeCmd('turnOffAlarm') + # 如果是合闸状态,恢复绿灯 if self.closed & 0xFF == 0xF0: self.exeCmd('turnOnGreen') - self.exeCmd('turnOffRed') - self.exeCmd('turnOffAlarm') else: self.exeCmd('turnOffGreen') - self.exeCmd('turnOffRed') - self.exeCmd('turnOffAlarm') + self.alarm = 0 # 清除报警标志 def open(self): """打开串口连接""" @@ -926,6 +929,10 @@ class Breaker: case 0: if self.open() == 0: ret0 = self.exeCmd('openBreaker') + # 初始化时关闭所有指示灯 + self.exeCmd('turnOffGreen') + self.exeCmd('turnOffRed') + self.exeCmd('turnOffAlarm') # self.logger.info(f"setOverLimitValues ret: {ret0}") ret1 = self.exeCmd('setOverLimitValues') # self.logger.info(f"setOverLimitValues ret: {ret1}") @@ -952,6 +959,11 @@ class Breaker: case 2: ret = self.exeCmd('openBreaker') if ret[0]: + # 分闸成功后,关闭所有指示灯 + self.exeCmd('turnOffGreen') + self.exeCmd('turnOffRed') + self.exeCmd('turnOffAlarm') + self.alarm = 0 # 清除报警标志 self.optFlag = 1 continue self.optFlag = -1 @@ -959,6 +971,9 @@ class Breaker: case 3: ret = self.exeCmd('closeBreaker') if ret[0]: + # 合闸成功后,点亮绿灯(如果没有报警) + # if not self.alarm: + self.exeCmd('turnOnGreen') self.optFlag = 1 continue self.optFlag = -1 diff --git a/ui_main.py b/ui_main.py index 4127258..48380ed 100644 --- a/ui_main.py +++ b/ui_main.py @@ -5039,16 +5039,18 @@ class MainWindow(QMainWindow): if row: start_ts, end_ts = row - # 如果实验已完成(有结束时间),退出等待状态 + # 只有实验已完成(有结束时间)时,才退出等待状态 if end_ts is not None: self.logger.info(f"[等待状态] ✅ 实验 {self._waiting_experiment_id} 已完成,自动退出等待状态") self.statusBar().showMessage("✅ 实验已完成", 3000) self._exit_waiting_state() elif start_ts is not None: - # 实验已开始(进行中),也退出等待状态 - self.logger.info(f"[等待状态] 🔄 实验 {self._waiting_experiment_id} 已开始,自动退出等待状态") - self.statusBar().showMessage("🔄 实验已开始", 3000) - self._exit_waiting_state() + # 实验已开始但未结束,更新提示为"等待实验结束" + self.logger.info(f"[等待状态] 🔄 实验 {self._waiting_experiment_id} 已开始,继续监控等待结束") + # 更新等待标签文本 + work_order_no = self.waiting_label.text().split("工单号: ")[-1] if "工单号:" in self.waiting_label.text() else "未知" + self.waiting_label.setText(f"⏳ 等待实验结束 - 工单号: {work_order_no}") + self.statusBar().showMessage("🔄 实验进行中,等待结束...", 3000) else: self.logger.info(f"[等待状态] ⏳ 实验 {self._waiting_experiment_id} 仍在等待开始") else: @@ -5207,7 +5209,7 @@ class MainWindow(QMainWindow): query_config=query_config, on_state_changed=self._on_monitor_state_changed, on_connection_changed=self._on_monitor_connection_changed, - poll_interval=5 # 每5秒查询一次 + poll_interval=1 # 每1秒查询一次(避免错过快速状态变化) ) # 启动监控器