| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- #!/usr/bin/env python3
- """Run competition task 1 after a valid voice command."""
- import json
- import math
- import subprocess
- import sys
- import threading
- import time
- from pathlib import Path
- import actionlib
- import rospy
- import rospkg
- from actionlib_msgs.msg import GoalStatus
- from geometry_msgs.msg import PoseWithCovarianceStamped, Quaternion, Twist
- from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
- from std_msgs.msg import String
- from std_srvs.srv import SetBool, Trigger
- class TaskFailure(Exception):
- """A controlled task failure that must not use stale TTS output."""
- class CompetitionTask1Manager:
- WALLS = ("wall1", "wall2", "wall3")
- def __init__(self):
- self._lock = threading.Lock()
- self._status_pub = rospy.Publisher(
- "/competition_task1/status", String, queue_size=10, latch=True
- )
- self._orders_pub = rospy.Publisher(
- "/competition_task1/orders", String, queue_size=1, latch=True
- )
- self._initial_pose_pub = rospy.Publisher(
- "/initialpose", PoseWithCovarianceStamped, queue_size=1
- )
- self._client = actionlib.SimpleActionClient("move_base", MoveBaseAction)
- self._cmd_vel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1)
- self._latest_amcl_yaw = None
- self._latest_amcl_x = None
- self._latest_amcl_y = None
- self._latest_amcl_time = 0.0
- self._amcl_sub = rospy.Subscriber(
- "/amcl_pose", PoseWithCovarianceStamped, self._amcl_callback, queue_size=10
- )
- self._qr_sub = rospy.Subscriber(
- "/qr_scan_result", String, self._qr_callback, queue_size=1
- )
- self._active_wall = None
- self._products = {wall: None for wall in self.WALLS}
- self._seen_products = set()
- self._product_order = []
- self._scan_deadline = None
- self._config = self._load_config()
- self._frame_id = self._config["frame_id"]
- self._start_pose = self._config["start_pose"]
- self._initial_pose_settle = float(self._config["initial_pose_settle_seconds"])
- self._initial_pose_position_tolerance = float(
- self._config["initial_pose_position_tolerance_m"]
- )
- self._initial_pose_yaw_tolerance = float(
- self._config["initial_pose_yaw_tolerance_rad"]
- )
- self._points = self._config["points"]
- self._scan_window = float(self._config["scan_window_seconds"])
- self._settle = float(self._config["scan_settle_seconds"])
- self._center_rotation_speed = abs(float(
- self._config["center_rotation_speed_rad_s"]
- ))
- self._center_rotation_tolerance = float(
- self._config["center_rotation_yaw_tolerance_rad"]
- )
- self._center_rotation_timeout = float(
- self._config["center_rotation_timeout_seconds"]
- )
- self._scan_timeout = float(self._config["scan_total_timeout_seconds"])
- self._initial_navigation_timeout = float(
- self._config["initial_navigation_timeout_seconds"]
- )
- self._server_timeout = float(
- self._config["move_base_server_timeout_seconds"]
- )
- rospy.on_shutdown(self._shutdown)
- @staticmethod
- def _load_config():
- package_path = Path(rospkg.RosPack().get_path("ucar_nav"))
- config_path = package_path / "config" / "competition" / "task1_scan_points.json"
- with config_path.open("r", encoding="utf-8") as stream:
- config = json.load(stream)
- if config.get("frame_id") != "map":
- raise TaskFailure("任务1扫码点必须使用 map 坐标系")
- return config
- def _publish(self, state, detail):
- message = "%s: %s" % (state, detail)
- rospy.loginfo("competition task1 %s", message)
- self._status_pub.publish(String(data=message))
- def _amcl_callback(self, message):
- orientation = message.pose.pose.orientation
- yaw = math.atan2(
- 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y),
- 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z),
- )
- with self._lock:
- self._latest_amcl_x = message.pose.pose.position.x
- self._latest_amcl_y = message.pose.pose.position.y
- self._latest_amcl_yaw = yaw
- self._latest_amcl_time = time.monotonic()
- def _set_initial_pose(self):
- try:
- x = float(self._start_pose["x"])
- y = float(self._start_pose["y"])
- yaw = float(self._start_pose["yaw"])
- except (KeyError, TypeError, ValueError) as error:
- raise TaskFailure("初始位姿配置无效:%s" % error)
- if not all(math.isfinite(value) for value in (x, y, yaw)):
- raise TaskFailure("初始位姿包含非有限数值")
- message = PoseWithCovarianceStamped()
- message.header.frame_id = self._frame_id
- message.pose.pose.position.x = x
- message.pose.pose.position.y = y
- message.pose.pose.orientation.z = math.sin(yaw / 2.0)
- message.pose.pose.orientation.w = math.cos(yaw / 2.0)
- message.pose.covariance[0] = 0.25
- message.pose.covariance[7] = 0.25
- message.pose.covariance[35] = 0.0685
- self._publish("INITIALIZING", "设置AMCL初始位姿 x=%.3f y=%.3f yaw=%.3f" % (x, y, yaw))
- for _ in range(3):
- message.header.stamp = rospy.Time.now()
- self._initial_pose_pub.publish(message)
- rospy.sleep(0.1)
- deadline = time.monotonic() + self._initial_pose_settle
- while not rospy.is_shutdown() and time.monotonic() < deadline:
- with self._lock:
- current_x = self._latest_amcl_x
- current_y = self._latest_amcl_y
- current_yaw = self._latest_amcl_yaw
- if current_x is not None and current_y is not None and current_yaw is not None:
- position_error = math.hypot(current_x - x, current_y - y)
- yaw_error = abs(self._wrap_to_pi(current_yaw - yaw))
- if (position_error <= self._initial_pose_position_tolerance
- and yaw_error <= self._initial_pose_yaw_tolerance):
- self._publish("INITIALIZED", "AMCL已确认初始位姿")
- return
- rospy.sleep(0.05)
- raise TaskFailure("AMCL未确认自动设置的初始位姿")
- def _shutdown(self):
- self._client.cancel_all_goals()
- self._stop_robot()
- self._set_scanner_mode(False, False, required=False)
- def _qr_callback(self, message):
- product = message.data.strip()
- if not product:
- return
- with self._lock:
- if product in self._seen_products:
- rospy.logwarn("task1 ignored duplicate product: %s", product)
- return
- self._seen_products.add(product)
- self._product_order.append(product)
- wall = self._active_wall
- if wall is not None and self._products[wall] is None:
- self._products[wall] = product
- self._active_wall = None
- location = wall if wall is not None else "scan_area"
- self._publish("QR_FOUND", "%s识别到%s(第%d个商品)" % (
- location, product, len(self._product_order)
- ))
- def _reset_qr_scanner(self):
- try:
- rospy.wait_for_service("/qr_scan/reset", timeout=3.0)
- response = rospy.ServiceProxy("/qr_scan/reset", Trigger)()
- except (rospy.ROSException, rospy.ServiceException) as error:
- raise TaskFailure("二维码节点未就绪或不支持重置:%s" % error)
- if not response.success:
- raise TaskFailure("二维码节点重置失败:%s" % response.message)
- @staticmethod
- def _set_bool_service(name, value, required):
- try:
- rospy.wait_for_service(name, timeout=3.0)
- response = rospy.ServiceProxy(name, SetBool)(value)
- except (rospy.ROSException, rospy.ServiceException) as error:
- if required:
- raise TaskFailure("二维码服务%s不可用:%s" % (name, error))
- rospy.logwarn("二维码服务%s关闭失败:%s", name, error)
- return
- if not response.success:
- if required:
- raise TaskFailure("二维码服务%s切换失败:%s" % (name, response.message))
- rospy.logwarn("二维码服务%s关闭失败:%s", name, response.message)
- def _set_scanner_mode(self, npu_enabled, decode_enabled, required=True):
- # Decode is toggled first so idle mode never fetches QR URLs.
- self._set_bool_service("/qr_scan/set_decode_enabled", decode_enabled, required)
- self._set_bool_service("/qr_scan/set_npu_enabled", npu_enabled, required)
- def _remaining_scan_time(self):
- if self._scan_deadline is None:
- return None
- return self._scan_deadline - time.monotonic()
- def _require_scan_time(self):
- remaining = self._remaining_scan_time()
- if remaining is not None and remaining <= 0.0:
- raise TaskFailure("扫码总超时(%.0f 秒)" % self._scan_timeout)
- return remaining
- def _goal_from_point(self, name):
- try:
- point = self._points[name]
- x = float(point["x"])
- y = float(point["y"])
- yaw = float(point["yaw"])
- except (KeyError, TypeError, ValueError) as error:
- raise TaskFailure("扫码点%s配置无效:%s" % (name, error))
- if not all(math.isfinite(value) for value in (x, y, yaw)):
- raise TaskFailure("扫码点%s包含非有限数值" % name)
- goal = MoveBaseGoal()
- goal.target_pose.header.frame_id = self._frame_id
- goal.target_pose.header.stamp = rospy.Time.now()
- goal.target_pose.pose.position.x = x
- goal.target_pose.pose.position.y = y
- goal.target_pose.pose.orientation = Quaternion(
- z=math.sin(yaw / 2.0), w=math.cos(yaw / 2.0)
- )
- return goal
- def _navigate(self, point_name, initial=False):
- remaining = None if initial else self._require_scan_time()
- timeout = self._initial_navigation_timeout if initial else remaining
- goal = self._goal_from_point(point_name)
- self._publish(
- "NAVIGATING",
- "%s x=%.3f y=%.3f yaw=%.3f" % (
- point_name,
- goal.target_pose.pose.position.x,
- goal.target_pose.pose.position.y,
- 2.0 * math.atan2(
- goal.target_pose.pose.orientation.z,
- goal.target_pose.pose.orientation.w,
- ),
- ),
- )
- self._client.send_goal(goal)
- deadline = time.monotonic() + timeout
- terminal_states = {
- GoalStatus.PREEMPTED, GoalStatus.ABORTED, GoalStatus.REJECTED,
- GoalStatus.RECALLED, GoalStatus.LOST,
- }
- while not rospy.is_shutdown() and time.monotonic() < deadline:
- state = self._client.get_state()
- if state == GoalStatus.SUCCEEDED:
- return
- if state in terminal_states:
- raise TaskFailure("未到达%s,move_base状态码%d" % (point_name, state))
- rospy.sleep(0.05)
- self._client.cancel_goal()
- if initial:
- raise TaskFailure("前往领取区初始点超时")
- raise TaskFailure("前往%s时扫码总超时" % point_name)
- def _wrap_to_pi(self, angle):
- return math.atan2(math.sin(angle), math.cos(angle))
- def _stop_robot(self):
- command = Twist()
- for _ in range(3):
- self._cmd_vel_pub.publish(command)
- rospy.sleep(0.03)
- def _turn_in_place(self, target_point):
- target = float(self._points[target_point]["yaw"])
- remaining = self._require_scan_time()
- timeout = min(self._center_rotation_timeout, remaining)
- deadline = time.monotonic() + timeout
- self._client.cancel_all_goals()
- self._stop_robot()
- self._publish(
- "ROTATING",
- "%s slow in-place turn to %.3f rad at %.2f rad/s" % (
- target_point, target, self._center_rotation_speed
- ),
- )
- try:
- while not rospy.is_shutdown() and time.monotonic() < deadline:
- with self._lock:
- yaw = self._latest_amcl_yaw
- if yaw is None:
- rospy.sleep(0.05)
- continue
- error = self._wrap_to_pi(target - yaw)
- if abs(error) <= self._center_rotation_tolerance:
- return
- command = Twist()
- command.angular.z = (
- self._center_rotation_speed if error > 0.0
- else -self._center_rotation_speed
- )
- self._cmd_vel_pub.publish(command)
- rospy.sleep(0.05)
- finally:
- self._stop_robot()
- raise TaskFailure("中央原地转向%s超时" % target_point)
- def _scan_wall(self, wall, point_name):
- if self._all_walls_found():
- return True
- if self._products[wall] is not None:
- self._publish("SKIPPED", "%s已经识别,跳过%s" % (wall, point_name))
- return True
- self._require_scan_time()
- self._set_scanner_mode(True, True)
- try:
- rospy.sleep(min(self._settle, self._require_scan_time()))
- with self._lock:
- self._active_wall = wall
- self._publish("SCANNING", "%s在%s等待二维码" % (wall, point_name))
- deadline = time.monotonic() + min(
- self._scan_window, self._require_scan_time()
- )
- while not rospy.is_shutdown() and time.monotonic() < deadline:
- with self._lock:
- if self._products[wall] is not None or self._all_walls_found():
- return True
- rospy.sleep(0.05)
- with self._lock:
- return self._products[wall] is not None or self._all_walls_found()
- finally:
- with self._lock:
- if self._active_wall == wall:
- self._active_wall = None
- self._set_scanner_mode(False, False, required=False)
- def _all_walls_found(self):
- return len(self._product_order) >= len(self.WALLS)
- def _clear_previous_files(self):
- spark_dir = self._spark_dir()
- for filename in ("tts_output.txt", "llm_result.json", "qr_products_input.json"):
- path = spark_dir / "audio" / filename
- if path.exists():
- path.unlink()
- @staticmethod
- def _spark_dir():
- speech_path = Path(rospkg.RosPack().get_path("speech_command"))
- return speech_path.parent / "SparkTalk"
- def _write_products_for_spark(self):
- products = list(self._product_order)
- if len(products) != len(self.WALLS):
- raise TaskFailure("未收集到三个不同商品")
- output = self._spark_dir() / "audio" / "qr_products_input.json"
- payload = {"products": products, "wall_products": self._products}
- with output.open("w", encoding="utf-8") as stream:
- json.dump(payload, stream, ensure_ascii=False, indent=2)
- self._publish("QR_COMPLETE", "已收集%s" % "、".join(products))
- @staticmethod
- def _factory_category(category):
- return {"食品": "食品", "日用品": "日用品", "电子产品": "电子"}.get(str(category).strip())
- @staticmethod
- def _warehouse_for(category):
- return {"食品": "食品加工车间", "日用品": "日用品加工车间",
- "电子产品": "电子产品生产车间"}.get(category)
- def _publish_orders(self, spark_dir):
- path = spark_dir / "audio" / "llm_result.json"
- try:
- with path.open("r", encoding="utf-8") as stream:
- result = json.load(stream)
- raw_tasks = [("real_task", result["real_task"]),
- ("simulation_task", result["simulation_task"])]
- orders = []
- for order_id, task in raw_tasks:
- if task.get("success") is not True:
- raise ValueError("%s未成功分类" % order_id)
- product = str(task["matched_product"]).strip()
- category = str(task["category"]).strip()
- factory_category = self._factory_category(category)
- warehouse = self._warehouse_for(category)
- if not product or factory_category is None or warehouse is None:
- raise ValueError("%s字段或类别无效" % order_id)
- orders.append({"order_id": order_id, "product": product,
- "category": category, "factory_category": factory_category,
- "warehouse": warehouse})
- if orders[0]["factory_category"] == orders[1]["factory_category"]:
- raise ValueError("两份订单类别相同")
- except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError) as error:
- raise TaskFailure("星火订单结果无效:%s" % error)
- self._orders_pub.publish(String(data=json.dumps({"orders": orders}, ensure_ascii=False, sort_keys=True)))
- self._publish("ORDERS_READY", "已发布真实与仿真两份订单")
- def _run_spark(self):
- spark_dir = self._spark_dir()
- result = subprocess.run([sys.executable, "SparkMain.py"], cwd=str(spark_dir))
- if result.returncode != 0:
- raise TaskFailure("星火分类或JSON校验失败,退出码%d" % result.returncode)
- self._publish_orders(spark_dir)
- self._publish("COMPLETE", "任务1完成,等待语音播报")
- def run(self):
- self._clear_previous_files()
- self._publish("WAITING_FOR_NAV", "等待move_base")
- if not self._client.wait_for_server(rospy.Duration(self._server_timeout)):
- raise TaskFailure("%s秒内未连接到move_base" % self._server_timeout)
- self._set_initial_pose()
- self._reset_qr_scanner()
- self._navigate("wall1_p1", initial=True)
- self._scan_deadline = time.monotonic() + self._scan_timeout
- self._scan_wall("wall1", "wall1_p1")
- # 在中央C仅导航一次;其余两个墙面朝向由低速原地旋转完成。
- self._navigate("center_wall1")
- self._turn_in_place("center_wall1")
- self._scan_wall("wall1", "center_wall1")
- if not self._all_walls_found():
- self._turn_in_place("center_wall2")
- self._scan_wall("wall2", "center_wall2")
- if not self._all_walls_found():
- self._turn_in_place("center_wall3")
- self._scan_wall("wall3", "center_wall3")
- for wall, point in (
- ("wall3", "wall3_p1"),
- ("wall2", "wall2_p3"),
- ("wall2", "wall2_p1"),
- ("wall1", "wall1_p3"),
- ):
- if self._all_walls_found():
- break
- if self._products[wall] is None:
- self._navigate(point)
- self._scan_wall(wall, point)
- if not self._all_walls_found():
- missing = [wall for wall in self.WALLS if self._products[wall] is None]
- raise TaskFailure("扫码结束,未识别%s" % "、".join(missing))
- self._write_products_for_spark()
- self._run_spark()
- def main():
- rospy.init_node("competition_task1_manager")
- manager = CompetitionTask1Manager()
- try:
- manager.run()
- return 0
- except TaskFailure as error:
- manager._publish("FAILED", str(error))
- return 1
- except Exception as error:
- rospy.logerr("competition task1 crashed: %s", error)
- manager._publish("FAILED", "任务执行器异常:%s" % error)
- return 1
- if __name__ == "__main__":
- sys.exit(main())
|