API source snapshot ยท 2026-09-24
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | #!/usr/bin/env python3 """Sequence robot motion and Zivid captures for Octomap mapping. Requires robot/camera bringup and octomap_mapping.launch.py running separately. Use --self-check to verify task sequencing without ROS or hardware. """ import argparse import json import subprocess import sys # Keep ROS imports local so --self-check works without ROS. def plan_capture_steps(run: dict, capture_prefix: str): """Return single-task goals and capture flags for matching moveto targets.""" base = {k: run[k] for k in ("start_gripper", "poses") if k in run} run_name = run.get("run_name", "capture_run") steps = [] for task in run.get("tasks", []): goal = dict(base) goal["tasks"] = [task] goal["run_name"] = run_name target = task.get("target", "") do_capture = ( task.get("task_type") == "moveto" and target.startswith(capture_prefix) ) steps.append((json.dumps(goal), do_capture)) return steps def _send_goal(node, action_client, full_json: str): """Wait for one MTCExecution goal; return (success, message).""" import rclpy from action_msgs.msg import GoalStatus from beambot_interfaces.action import MTCExecution goal = MTCExecution.Goal() goal.full_json = full_json send_future = action_client.send_goal_async(goal) rclpy.spin_until_future_complete(node, send_future) handle = send_future.result() if handle is None or not handle.accepted: return False, "goal rejected" result_future = handle.get_result_async() rclpy.spin_until_future_complete(node, result_future) result = result_future.result() ok = result.status == GoalStatus.STATUS_SUCCEEDED return ok, result.result.error_message def _capture(node, trigger_client, timeout: float): """Request a Zivid 3D capture; return (success, message).""" import rclpy from std_srvs.srv import Trigger future = trigger_client.call_async(Trigger.Request()) rclpy.spin_until_future_complete(node, future, timeout_sec=timeout) if not future.done(): return False, f"capture timed out after {timeout}s" res = future.result() return res.success, res.message def _save_map(path: str): """Save the current map with octomap_saver_node.""" cmd = [ "ros2", "run", "octomap_server", "octomap_saver_node", "--ros-args", "-p", f"octomap_path:={path}", ] print(f"Saving octomap -> {path}") try: subprocess.run(cmd, timeout=30, check=True) print(f"Saved {path}") except Exception as exc: # noqa: BLE001 - save is best-effort print( f"Auto-save failed ({exc}). Save manually with:\n {' '.join(cmd)}", file=sys.stderr, ) def run(args) -> int: import rclpy from rclpy.node import Node from rclpy.action import ActionClient from std_srvs.srv import Trigger from beambot_interfaces.action import MTCExecution with open(args.run_json) as f: run_data = json.load(f) steps = plan_capture_steps(run_data, args.capture_prefix) if not steps: print(f"No tasks in {args.run_json}", file=sys.stderr) return 1 rclpy.init() node = None try: node = Node("capture_run") action_client = ActionClient(node, MTCExecution, "beambot_execution") trigger_client = node.create_client(Trigger, args.capture_service) if not action_client.wait_for_server(timeout_sec=15.0): node.get_logger().error("beambot_execution action server not available") return 1 if not trigger_client.wait_for_service(timeout_sec=15.0): node.get_logger().error(f"capture service {args.capture_service} not available") return 1 n_capture = sum(1 for _, c in steps if c) node.get_logger().info( f"Capture run: {len(steps)} moves, {n_capture} captures " f"(prefix '{args.capture_prefix}')" ) captures = 0 rc = 0 for i, (full_json, do_capture) in enumerate(steps, 1): node.get_logger().info(f"[{i}/{len(steps)}] moving") ok, msg = _send_goal(node, action_client, full_json) if not ok: node.get_logger().error(f"move {i} failed: {msg} โ aborting") rc = 1 break if do_capture: node.get_logger().info(f"[{i}/{len(steps)}] capture -> {args.capture_service}") cok, cmsg = _capture(node, trigger_client, args.capture_timeout) if cok: captures += 1 else: # Continue after capture failures; the map may be incomplete. node.get_logger().warning(f"capture {i} failed: {cmsg}") _sleep(node, args.settle) node.get_logger().info(f"Done: {captures}/{n_capture} captures succeeded") finally: if node is not None: node.destroy_node() rclpy.try_shutdown() if rc == 0 and not args.no_save and args.save: _save_map(args.save) return rc def _sleep(node, seconds: float): """Wait on the node clock while processing callbacks.""" import rclpy end = node.get_clock().now().nanoseconds + int(seconds * 1e9) while rclpy.ok() and node.get_clock().now().nanoseconds < end: rclpy.spin_once(node, timeout_sec=0.05) def _self_check(): """Check task sequencing without ROS.""" run_data = { "start_gripper": "hande", "poses": {"safe_sample_transport": [0] * 6, "oscan1": [1] * 6, "oscan2": [2] * 6}, "tasks": [ {"task_type": "moveto", "target": "safe_sample_transport", "planning_type": "joint"}, {"task_type": "moveto", "target": "oscan1", "planning_type": "joint"}, {"task_type": "moveto", "target": "oscan2", "planning_type": "joint"}, ], "run_name": "oscan", } steps = plan_capture_steps(run_data, "oscan") assert [c for _, c in steps] == [False, True, True], "capture only at oscan* poses" for goal_json, _ in steps: g = json.loads(goal_json) assert g["start_gripper"] == "hande" assert len(g["tasks"]) == 1, "one task per goal" assert "oscan1" in g["poses"], "full poses dict carried on every goal" assert g["run_name"] == "oscan" assert plan_capture_steps({"tasks": []}, "oscan") == [] print("self-check OK") def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("run_json", nargs="?", help="Path to the run JSON (poses + moveto tasks)") parser.add_argument("--capture-prefix", default="oscan") parser.add_argument("--capture-service", default="/capture") parser.add_argument("--capture-timeout", type=float, default=30.0) parser.add_argument("--settle", type=float, default=2.0) parser.add_argument("--save", default="") parser.add_argument("--no-save", action="store_true") parser.add_argument("--self-check", action="store_true", help="Run logic self-check and exit") args, _ = parser.parse_known_args() if args.self_check: _self_check() return 0 if not args.run_json: parser.error("run_json is required (or pass --self-check)") return run(args) if __name__ == "__main__": sys.exit(main()) |