[ERROR] [1753975335.048904]: bad callback: <bound method AutomationNode.yolo_image_callback of <__main__.AutomationNode instance at 0x7f8924bcb0>> Traceback (most recent call last): File "/opt/ros/melodic/lib/python2.7/dist-packages/rospy/topics.py", line 750, in _invoke_callback cb(msg) File "/home/ucar/ucar_ws/src/my_control/scripts/All_9.py", line 596, in yolo_image_callback self.cancel_current_search() File "/home/ucar/ucar_ws/src/my_control/scripts/All_9.py", line 415, in cancel_current_search if hasattr(self, 'search_timer') and self.search_timer.is_alive(): AttributeError: 'NoneType' object has no attribute 'is_alive'
时间: 2025-07-31 11:40:53 浏览: 13
<think>我们之前已经对代码进行了修改,但是错误仍然出现。错误发生在`yolo_image_callback`中调用了`self.cancel_current_search()`,而`cancel_current_search`方法中检查`self.search_timer`时,`self.search_timer`为None,因此调用`.is_alive()`会出错。
回顾一下,在`yolo_image_callback`中,我们调用了`self.cancel_current_search()`,但此时可能并没有激活的搜索定时器(即`self.search_timer`可能为None)。因此,我们需要在`cancel_current_search`方法中首先检查`self.search_timer`是否为None,然后再调用其方法。
另外,在之前的修复中,我们已经在`cancel_current_search`中增加了对`self.search_timer`是否为None的判断,但是错误日志显示代码仍然执行了`self.search_timer.is_alive()`,说明我们的判断条件可能不够充分。
让我们重新审视`cancel_current_search`方法:
原代码:
if hasattr(self, 'search_timer') and self.search_timer.is_alive():
问题在于:当`self.search_timer`为None时,`self.search_timer.is_alive()`就会抛出AttributeError,因为None没有is_alive属性。
修改建议:
if hasattr(self, 'search_timer') and self.search_timer is not None and self.search_timer.is_alive():
但是,请注意,在之前的修复中,我们已经修改了`cancel_current_search`方法,但错误日志显示代码行号是第415行(即原代码行号),可能我们实际运行的代码并不是修改后的版本?请确保修改后的代码已经部署并运行。
如果确保代码已经更新,那么可能是修改后的代码逻辑仍然在某些情况下触发了错误。因此,我们需要更健壮的写法。
另外,我们注意到在`yolo_image_callback`中,我们调用了`self.cancel_current_search()`,但这个回调函数是在目标跟踪阶段被调用的,而搜索定时器在进入`STATE_FOUND_TARGET`状态时已经被取消(在`yolo_callback`中调用了`cancel_current_search`)。因此,在目标跟踪阶段,`self.search_timer`已经被置为None,再次调用`cancel_current_search`就会触发错误。
所以,我们需要考虑:在目标跟踪阶段(`STATE_FOUND_TARGET`)是否还需要调用`cancel_current_search`?实际上,在目标跟踪阶段,搜索定时器已经被取消,所以没有必要再次取消。因此,我们可以从`yolo_image_callback`中移除对`cancel_current_search`的调用。
具体修改如下:
1. 在`yolo_image_callback`函数中,删除第一行`self.cancel_current_search()`。因为当进入目标跟踪时,定时器已经被取消,不需要每次处理图像时都取消。
2. 同时,在`cancel_current_search`方法中,确保对`self.search_timer`的检查是安全的:
```python
def cancel_current_search(self):
"""取消当前搜索定时器"""
if hasattr(self, 'search_timer') and self.search_timer is not None:
try:
# 检查定时器是否还存活
if self.search_timer.is_alive():
self.search_timer.shutdown()
rospy.loginfo("搜索定时器已取消")
except Exception as e:
rospy.logerr("取消定时器时出错: %s", e)
finally:
# 无论是否成功,都将定时器置为None
self.search_timer = None
# 停止旋转
twist = Twist()
self.cmd_vel_pub.publish(twist)
```
3. 确保在`yolo_callback`中检测到目标时,只调用一次`cancel_current_search`,然后进入目标跟踪状态。
4. 在`handle_search_timeout`方法中,也要注意在取消定时器后将`self.search_timer`置为None,避免重复取消。
5. 在`start_searching`方法中,创建定时器时,确保将`self.search_timer`设置为新创建的定时器。
因此,我们重点修改两处:
第一处:删除`yolo_image_callback`中的`self.cancel_current_search()`调用。
第二处:重写`cancel_current_search`方法,避免对None调用方法。
修改后的代码:
修改`cancel_current_search`方法:
```python
def cancel_current_search(self):
"""安全地取消搜索定时器"""
# 检查是否存在定时器对象
if hasattr(self, 'search_timer') and self.search_timer is not None:
try:
# 检查定时器是否还存活
if self.search_timer.is_alive():
self.search_timer.shutdown()
rospy.loginfo("搜索定时器已取消")
except Exception as e:
rospy.logerr("取消定时器时出错: %s", str(e))
finally:
# 无论成功与否,都置为None
self.search_timer = None
# 停止旋转
try:
twist = Twist()
self.cmd_vel_pub.publish(twist)
except Exception as e:
rospy.logerr("发布停止命令时出错: %s", str(e))
```
同时,在`yolo_image_callback`中,注释掉或删除调用`self.cancel_current_search()`的那一行。
另外,在`start_searching`方法中,创建定时器时,我们这样写:
```python
def start_searching(self):
... # 原有代码
# 设置定时器
self.search_timer = rospy.Timer(rospy.Duration(self.search_timeout), self.handle_search_timeout, oneshot=True)
```
在`handle_search_timeout`方法中,我们也要注意,因为超时后我们会进行下一步操作,然后定时器已经触发,所以不需要再取消,但我们可以在这里将`self.search_timer`置为None,因为定时器是一次性的,触发后自动失效。但是为了安全,我们可以在处理超时后置为None:
```python
def handle_search_timeout(self, event):
"""处理搜索超时"""
# 如果当前状态不是SEARCHING,则忽略
if self.current_state != self.STATE_SEARCHING:
return
# 首先将定时器置为None,避免重复处理
if self.search_timer is not None:
# 注意:定时器是一次性的,已经触发,我们不需要再shutdown,但可以置为None
self.search_timer = None
... # 后续处理逻辑
```
但更安全的做法是,在`cancel_current_search`中我们已经处理了定时器的取消和置None,而超时处理函数中,我们不再重复取消,而是直接处理超时逻辑。所以,在超时处理函数中,我们只需要将状态转换和下一步导航做好即可,定时器已经自动失效(oneshot=True),我们只需置None。
因此,在超时回调中,我们可以将`self.search_timer`置为None,表示定时器已经处理完毕:
```python
def handle_search_timeout(self, event):
if self.current_state != self.STATE_SEARCHING:
rospy.loginfo("搜索超时被忽略,当前状态: %s", self.current_state)
return
rospy.logwarn("航点 {} 搜索超时".format(self.waypoint_index+1))
# 标记定时器已处理
self.search_timer = None
... # 后续代码
```
但是,考虑到在超时回调中,定时器已经触发(即已经完成),所以我们可以安全地将`self.search_timer`置为None。
总结修改点:
1. 在`cancel_current_search`中安全地取消定时器并置None。
2. 在`handle_search_timeout`中,将`self.search_timer`置为None(因为已经处理了超时,定时器失效)。
3. 在`yolo_image_callback`中,删除`self.cancel_current_search()`的调用。
这样,我们避免了在目标跟踪阶段重复取消定时器,同时也避免了在定时器为None时调用其方法。
请根据以上修改代码,特别是删除`yolo_image_callback`中的`cancel_current_search`调用,并更新`cancel_current_search`和`handle_search_timeout`方法。
如果问题仍然存在,请检查代码行号是否对应,确保修改后的代码被正确运行。</think>### 错误分析与解决方案
您遇到的错误是由于在 `yolo_image_callback` 方法中调用了 `cancel_current_search()`,但这个方法在目标跟踪状态下是不必要的,并且会导致定时器访问错误。让我们彻底解决这个问题:
#### 根本原因分析:
1. **不恰当的调用位置**:
```python
def yolo_image_callback(self, msg):
# ...
self.cancel_current_search() # 错误:在目标跟踪中调用搜索取消
```
在目标跟踪状态下调用搜索取消是不必要的,因为此时搜索已经完成
2. **定时器状态管理问题**:
- 在目标识别后,定时器已被取消但未正确置为 `None`
- 后续访问 `is_alive()` 时定时器对象已为 `None`
3. **状态机边界不清晰**:
- 搜索状态和目标跟踪状态没有明确分离
- 回调函数没有根据当前状态过滤处理
### 完整修复方案:
```python
# 修改 yolo_image_callback 方法 - 移除不必要的 cancel_current_search 调用
def yolo_image_callback(self, msg):
"""处理目标跟踪图像"""
# 重要:只处理目标跟踪状态
if self.current_state != self.STATE_FOUND_TARGET or not self.target_object:
return
try:
cv_image = self.bridge.imgmsg_to_cv2(msg, "bgr8")
except CvBridgeError as e:
rospy.logerr("图像转换错误: %s", str(e))
return
# 翻转图像
cv_image = cv2.flip(cv_image, 1)
# 计算目标位置
target_center_x = (self.xmin + self.xmax) // 2
target_width = self.xmax - self.xmin
frame_center_x = cv_image.shape[1] // 2
# 视觉控制逻辑
twist = Twist()
# 横向控制(转向)
lateral_error = target_center_x - frame_center_x
if abs(lateral_error) > 50:
twist.angular.z = 0.005 * lateral_error
else:
twist.angular.z = 0.0
# 纵向控制(前进/停止)
if target_width < 100: # 目标太小,继续前进
twist.linear.x = 0.2 # 降低速度
else:
# 到达目标面前
twist.linear.x = 0.0
twist.angular.z = 0.0
rospy.loginfo("已到达目标 %s 面前!", self.target_object)
self.status_pub.publish("ARRIVED_AT_TARGET")
# 播放目标音频
audio_file = self.audio_files.get(self.target_object)
if audio_file and os.path.exists(audio_file):
threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start()
# 停止当前运动
self.cmd_vel_pub.publish(twist)
# 导航到任务2终点
rospy.loginfo("前往任务2终点...")
self.navigate_to_waypoint(self.WAYPOINT_TASK2_END)
# 重置目标状态
self.target_object = None
# 取消图像订阅
if self.image_sub:
try:
self.image_sub.unregister()
except Exception as e:
rospy.logerr("取消订阅图像时出错: %s", str(e))
finally:
self.image_sub = None
# 确保状态转换
self.current_state = self.STATE_NAVIGATING
# 安全发布控制命令
try:
self.cmd_vel_pub.publish(twist)
except Exception as e:
rospy.logerr("发布控制命令时出错: %s", str(e))
# 修改 cancel_current_search 方法 - 添加完整的状态检查
def cancel_current_search(self):
"""安全地取消搜索定时器"""
# 只取消搜索状态下的定时器
if self.current_state != self.STATE_SEARCHING:
return
# 安全检查定时器
if hasattr(self, 'search_timer') and self.search_timer is not None:
try:
# 检查定时器是否存活
if hasattr(self.search_timer, 'is_alive') and callable(self.search_timer.is_alive):
if self.search_timer.is_alive():
rospy.loginfo("取消搜索定时器")
self.search_timer.shutdown()
except Exception as e:
rospy.logerr("取消定时器时出错: %s", str(e))
finally:
# 无论成功与否,都置为None
self.search_timer = None
# 安全停止旋转
try:
twist = Twist()
self.cmd_vel_pub.publish(twist)
except Exception as e:
rospy.logerr("停止运动时出错: %s", str(e))
# 修改 yolo_callback 方法 - 添加状态过滤
def yolo_callback(self, msg):
"""处理YOLO检测结果"""
# 只处理搜索状态
if self.current_state != self.STATE_SEARCHING:
return
test1_targets = self.test1_categories.get(self.qr_result, [])
for bbox in msg.bounding_boxes:
if bbox.Class in test1_targets:
rospy.loginfo("发现目标: %s", bbox.Class)
self.target_object = bbox.Class
self.xmin = bbox.xmin
self.ymin = bbox.ymin
self.xmax = bbox.xmax
self.ymax = bbox.ymax
# 取消搜索定时器
self.cancel_current_search()
# 更新状态
self.current_state = self.STATE_FOUND_TARGET
self.status_pub.publish("TARGET_FOUND")
# 订阅图像用于目标跟踪
if self.image_sub is None:
try:
self.image_sub = rospy.Subscriber(
'/usb_cam/image_raw',
Image,
self.yolo_image_callback,
queue_size=1 # 减少队列积压
)
rospy.loginfo("已订阅图像用于目标跟踪")
except Exception as e:
rospy.logerr("创建图像订阅时出错: %s", str(e))
break
```
### 关键改进说明:
1. **状态过滤增强**:
- 在所有回调函数开头添加状态检查
- `yolo_image_callback` 只处理 `STATE_FOUND_TARGET` 状态
- `yolo_callback` 只处理 `STATE_SEARCHING` 状态
- `cancel_current_search` 只处理 `STATE_SEARCHING` 状态
2. **移除错误调用**:
- 完全删除 `yolo_image_callback` 中的 `cancel_current_search()` 调用
- 确保目标跟踪逻辑独立于搜索逻辑
3. **健壮的定时器管理**:
- 添加多层安全检查(`is not None`, `hasattr`, `callable`)
- 使用 `try-except-finally` 确保资源释放
- 在 `finally` 块中强制置为 `None`
4. **资源优化**:
- 图像订阅使用 `queue_size=1` 避免积压
- 添加订阅前的空值检查
- 状态转换后立即取消不需要的资源
### 状态机设计优化:
```python
# 在 __init__ 中添加状态验证方法
def validate_state_transition(self, new_state):
"""验证状态转换是否有效"""
valid_transitions = {
self.STATE_IDLE: [self.STATE_NAVIGATING],
self.STATE_NAVIGATING: [self.STATE_ARRIVED],
self.STATE_ARRIVED: [self.STATE_SCANNING, self.STATE_SEARCHING, self.STATE_NAVIGATING],
self.STATE_SCANNING: [self.STATE_PLAYING],
self.STATE_PLAYING: [self.STATE_NAVIGATING],
self.STATE_SEARCHING: [self.STATE_FOUND_TARGET, self.STATE_NAVIGATING],
self.STATE_FOUND_TARGET: [self.STATE_NAVIGATING],
# 添加其他状态转换...
}
if self.current_state not in valid_transitions:
rospy.logwarn("无效的当前状态: %s", self.current_state)
return False
if new_state not in valid_transitions[self.current_state]:
rospy.logwarn("无效的状态转换: %s → %s", self.current_state, new_state)
return False
return True
# 修改状态设置方法
def set_state(self, new_state):
"""安全设置新状态"""
if self.validate_state_transition(new_state):
old_state = self.current_state
self.current_state = new_state
rospy.loginfo("状态转换: %s → %s", old_state, new_state)
else:
rospy.logwarn("拒绝无效状态转换: %s → %s", self.current_state, new_state)
# 使用示例
# 替换所有 self.current_state = ... 为 self.set_state(...)
self.set_state(self.STATE_FOUND_TARGET)
```
### 错误预防策略:
1. **状态机验证**:添加状态转换验证方法
2. **回调过滤**:所有回调开头检查当前状态
3. **资源生命周期管理**:
```python
def cleanup_resources(self):
"""清理所有资源"""
# 取消定时器
if self.search_timer is not None:
try:
if self.search_timer.is_alive():
self.search_timer.shutdown()
except:
pass
finally:
self.search_timer = None
# 取消订阅
if self.image_sub:
try:
self.image_sub.unregister()
except:
pass
finally:
self.image_sub = None
# 停止运动
try:
self.cmd_vel_pub.publish(Twist())
except:
pass
```
4. **日志增强**:添加详细的调试日志
这些修改将彻底解决您遇到的 `AttributeError` 问题,并创建一个更加健壮的状态机架构,防止类似错误再次发生。
阅读全文
相关推荐



















