From 3c86441fb022d48dd58086c45fa2b02fc723d0c9 Mon Sep 17 00:00:00 2001 From: arm_dev Date: Thu, 15 Oct 2026 10:51:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8D=A2=E5=8F=B0=E7=94=B5=E8=84=91=E5=85=8B?= =?UTF-8?q?=E9=9A=86=E5=90=8E=E5=90=AF=E5=8A=A8=E5=A4=B1=E8=B4=A5=EF=BC=8C?= =?UTF-8?q?=E6=98=AF=E5=9B=A0=E4=B8=BA=20site=20=E5=92=8C=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E8=84=9A=E6=9C=AC=E5=86=99=E6=AD=BB=E4=BA=86=20/home/?= =?UTF-8?q?cc/ros2=E3=80=82=E4=BB=93=E5=BA=93=E5=86=85=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=94=B9=E6=88=90=E7=9B=B8=E5=AF=B9=E4=BB=93=E5=BA=93=E6=A0=B9?= =?UTF-8?q?=EF=BC=8C=E5=8A=A0=E8=BD=BD=E6=97=B6=E5=86=8D=E5=B1=95=E5=BC=80?= =?UTF-8?q?=EF=BC=9B=E7=BC=BA=20install=20=E6=97=B6=E5=85=88=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E8=A6=81=E7=BC=96=E8=AF=91=EF=BC=8C=E8=80=8C=E4=B8=8D?= =?UTF-8?q?=E6=98=AF=20source=20=E4=B8=80=E4=B8=AA=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84=E6=96=87=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- config/site.example.yaml | 66 +++++++++------- config/site.yaml | 40 +++++----- handeye/waypoints.yaml | 2 +- scripts/env.sh | 8 +- scripts/gen_site_example.py | 4 +- scripts/install_rdk_offline.sh | 2 +- scripts/lib/dev_tools.sh | 31 ++++++++ scripts/lib/export_site_env.py | 8 +- scripts/lib/real_preflight.py | 10 ++- scripts/rdk_release_movel.py | 4 +- scripts/rdk_switch_tool.sh | 4 +- scripts/run_all.sh | 4 +- scripts/run_mock_demo.sh | 9 ++- scripts/run_real.sh | 10 ++- scripts/show_site_config.sh | 2 +- scripts/test_rdk_python.sh | 4 +- .../arm_application/handeye_calibrate.py | 4 +- .../arm_application/handeye_teach.py | 14 ++-- .../arm_application/perception_node.py | 6 +- .../arm_application/realsense_color_pub.py | 12 +-- .../arm_application/servo_node.py | 11 +-- .../arm_application/site_io.py | 76 ++++++++++++++++++- src/arm_application/config/perception.yaml | 2 +- src/arm_application/config/servo.yaml | 2 +- .../arm_driver_flexiv/rdk_client.py | 10 ++- 25 files changed, 254 insertions(+), 91 deletions(-) create mode 100644 scripts/lib/dev_tools.sh diff --git a/config/site.example.yaml b/config/site.example.yaml index 34db6c4..d0a65d3 100644 --- a/config/site.example.yaml +++ b/config/site.example.yaml @@ -10,8 +10,8 @@ # 用法:复制本文件为 config/site.yaml,然后把所有 <占位符> 换成现场实际值。 # 现场值包括:机械臂 SN / IP、相机序列号、夹爪串口、传感器 USB 路径。 # -# 说明:site.yaml 里的路径出现 /home/USER 是因为生成时匿名化了用户名; -# 部署时请改成实际用户的 home 目录。 +# 说明:仓库里的文件用相对仓库根的路径(例如 models/yolo/...)。 +# 不要改回某个用户的绝对家目录,换台电脑就会找不到文件。 # ============================================================================= # ============================================================================= @@ -61,13 +61,15 @@ robot: # (status 里的 init=need_init 只说明"绝对位置标定没做",不影响夹持) # 想让开机必初始化(无人值守的上电流程)就显式改 true,并确认爪里是空的。 auto_init: false - named_poses_file: "/home/USER/ros2/src/arm_control/config/named_poses_89.yaml" + named_poses_file: "src/arm_control/config/named_poses_89.yaml" paths: - handeye_file: "/home/USER/ros2/handeye/results/89_eye_in_hand.yaml" - handeye_waypoints: "/home/USER/ros2/handeye/poses/89_eye_in_hand.yaml" - handeye_workdir: "/home/USER/ros2/handeye/tmp/89" + handeye_file: "handeye/results/89_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/89_eye_in_hand.yaml" + handeye_workdir: "handeye/tmp/89" handeye: # Studio tcp_location(法兰系,米 + qwxyz)。与 rdk Tool.params 必须一致。 + # 眼在手上:AX=XB 的手是这份 TCP,再折成 cam→法兰写 T_4x4。 + # 眼在手外:AX=XB 的手同样是这份 TCP(板夹在工具上);T_4x4 仍是 cam→基座,不折。 gripper_tool: "tool10120938" gripper_tcp_xyz: [-0.0005, 0.0067, 0.2245] gripper_tcp_qwxyz: [1.0, 0.0, 0.0, 0.0] @@ -177,7 +179,8 @@ robot: # 落在本文件下方注释记录的合理区间 0.204~0.233 内。 # 复核:python3 scripts/sense_tool_length.py --sn Rizon4-XXXXXX # python3 scripts/lib/real_geometry_check.py --sn Rizon4-XXXXXX - # 下臂安全距离按 tool10120938 的 TCP +Z,不再用旧 tool1 的 0.223。 + # 2026-10-12:下臂安全距离改按 Studio 工具 tool10120938 的 TCP +Z。 + # 上面 0.223 是旧 tool1(夹爪 TCP 0.147 + 规露出 0.076),这把工具不再用。 tool_tip_below_flange_m: 0.2245 # >>> 仿真那台臂(Gazebo)也登记成 robots[] 里的一台 <<< # 为什么必须登记:应用是按"当前臂"的名字去查 frame / joint / 相机话题的 @@ -208,7 +211,7 @@ robot: # 为什么必须给文件而不能留空:visual_servo 的 servo_node 需要 # T_flange_cam 矩阵做像素→笛卡尔换算;留空它就直接拒绝启动 #("手眼标定结果缺失"),S04 跟踪起不来。 - handeye_file: "/home/USER/ros2/handeye/results/sim_eye_in_hand.yaml" + handeye_file: "handeye/results/sim_eye_in_hand.yaml" # 运动参数:**大部分**与 89 共用(锚点),但**高度类**必须按仿真台的几何改写。 # # 为什么不能直接共用:真机是"机械臂装在台面同一高度",所以"法兰相对基座" @@ -302,11 +305,11 @@ robot: port: "/dev/ttyUSB1" stroke_mm: 30.0 auto_init: false - named_poses_file: "/home/USER/ros2/src/arm_control/config/named_poses_91.yaml" + named_poses_file: "src/arm_control/config/named_poses_91.yaml" paths: - handeye_file: "/home/USER/ros2/handeye/results/91_eye_in_hand.yaml" - handeye_waypoints: "/home/USER/ros2/handeye/poses/91_eye_in_hand.yaml" - handeye_workdir: "/home/USER/ros2/handeye/tmp/91" + handeye_file: "handeye/results/91_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/91_eye_in_hand.yaml" + handeye_workdir: "handeye/tmp/91" # >>> 右臂跟踪高度:只改这里,89 仍用上面那段 <<< # 法兰 Z;RGI 比平行爪高,通常要比 89 略抬一点。改完重启: # bash ~/ros2/scripts/run_visual_servo_rt.sh --arm 91 --control @@ -327,7 +330,7 @@ robot: paths: # >>> YOLO 权重(标准放法,不必改代码)<<< - # 1) 把 .onnx / .pt 放到 /home/USER/ros2/models/yolo/ + # 1) 把 .onnx / .pt 放到 models/yolo/ # 2) 这一行改成那个绝对路径(写 .onnx 即可;TensorRT 会找同名 .engine) # 3) backend=tensorrt 时必须先导出引擎(只需做一次): # bash ~/ros2/scripts/weights.sh convert <源文件> --name <名字> --group yolo @@ -341,18 +344,18 @@ paths: # ultralytics 解不了 pose 的 TensorRT engine,运行时走 # arm_application/pose_detector.py 的自解码(见该模块头)。 # 仍然用 .engine(实测 ~2.6 ms/帧;onnx 只有 CPU provider,约 27 ms)。 - yolo_weights: "/home/USER/ros2/models/yolo/gongjian0929.onnx" + yolo_weights: "models/yolo/gongjian0929.onnx" # 换模型:把 models/yolo/ 下别的 .onnx / .pt 绝对路径填进来即可。 # 未在用的模型已移出仓库(体积大且干扰 `weights.sh list`),要恢复见 models/README.md。 # 入孔模型(backend=yolo 时把孔当框)。two_stage 用下面的工件权重。 # pose 模型两处共用同一份 —— 它既能框工件,又直接给 k1/k2 关键点。 # 查悬空引用:bash ~/ros2/scripts/weights.sh doctor - hole_weights: "/home/USER/ros2/models/yolo/gongjian0929.onnx" + hole_weights: "models/yolo/gongjian0929.onnx" # 下面是缺省(未匹配到 robots[] 时)。真机以 robots[].paths 为准:89 / 91 各一份。 - handeye_file: "/home/USER/ros2/handeye/results/89_eye_in_hand.yaml" - handeye_waypoints: "/home/USER/ros2/handeye/poses/89_eye_in_hand.yaml" - handeye_calibrate: "/home/USER/ros2/handeye/calibrate.yaml" - handeye_workdir: "/home/USER/ros2/handeye/tmp/89" + handeye_file: "handeye/results/89_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/89_eye_in_hand.yaml" + handeye_calibrate: "handeye/calibrate.yaml" + handeye_workdir: "handeye/tmp/89" # 空=默认 DDS(SHM);勿强制 UDP-only,否则相机预览收不到 /workpiece/detection fastdds_profile: "" @@ -593,6 +596,7 @@ servo: # 混合模式必填。交回 D405 的等待不再用它,见 d435_handback_sec。 lock_still_sec: 3.0 # 进程内 MoveL 到位后,法兰 XY 连续停稳这么久才交回 D405。 + # 旧 3s 是杀栈冷启动的垫时间;不杀栈后只留机械停稳。晃了把数加大,不许再杀栈。 d435_handback_sec: 0.5 # 框心对准并且法兰停稳这么久,才精拍一次小孔关键点、进对孔。实时点到此为止。 wrist_align_sec: 0.5 @@ -675,10 +679,10 @@ gripper: hole_detect: backend: "two_stage" # two_stage | yolo | hough # backend=yolo 时用。 - weights: "/home/USER/ros2/models/yolo/gongjian0929.onnx" + weights: "models/yolo/gongjian0929.onnx" # 二阶段工件检测权重。pose 模型既框工件、又直接给 k1/k2 关键点, # 所以和上面的跟踪权重是同一份。 - workpiece_weights: "/home/USER/ros2/models/yolo/gongjian0929.onnx" + workpiece_weights: "models/yolo/gongjian0929.onnx" # pose 关键点优先:检测器给了 k1/k2 就直接用,不用再找圆。 # 关键点不可信(低于 keypoint_min_conf / 两点几乎重合)时自动回落到 # 亮环 + Hough 那套老路径。 @@ -686,7 +690,7 @@ hole_detect: keypoint_min_conf: 0.30 keypoint_min_dist_px: 15.0 keypoint_hole_radius_px: 12.0 # 关键点没有半径;反投影时按这个半径取孔周深度环 - config: "/home/USER/ros2/src/arm_application/config/hole_detect.yaml" + config: "src/arm_application/config/hole_detect.yaml" conf: 0.25 iou: 0.7 imgsz: 640 @@ -769,17 +773,21 @@ hole_detect: # —— 柔顺运动(夹爪持规,防急加速/急停/过冲把规震脱):速度放慢 + 平滑刹车 —— align_speed: 0.014 # 对准段笛卡尔速度 [m/s](原0.03,放缓防冲) move_speed: 0.022 # 升降/粗居中段速度 [m/s](原0.05,放缓防震规) + # 对小孔关键点:只用像素射线交到 workpiece_surface_z_m,不读深度图。 hole_visual_only: true - # 本分支实验:true=悬停拍一次并走完到孔;main 保存点应保持 false。 + # 本分支实验(exp/hole-keypoint-once):true=悬停拍一次,框心→k1 一次走完 + # (40mm 上限)。main 保存点是 false(多走多拍)。 hole_keypoint_once: true - # true=悬停走完后一条厂家 MoveL 贴面。对照观察高补交时改 false。 + # true=悬停走完后一条厂家 MoveL 贴面(不补拍)。对照 37b85aa 改回 false。 hole_once_lock_xy_descend: true - # 0=笛卡尔锁 XY 只降 Z。k>0 才沿 -look 加前馈。 + # 贴面相对悬停:沿 -look 走 k×剩余下降 [m/m]。0=笛卡尔锁 XY 只降 Z。 + # 0.018 在框心改平面交点后两条链路都 +1.3mm X 过补,插不进。 once_ff_look_per_m: 0.0 - final_gap_mm: 2.0 # 对准后贴面目标:规尖离面 [mm]。CLI --final-gap 覆盖。 + final_gap_mm: 2.0 # 对准后贴面目标:规尖离面 [mm](不接触)。笛卡尔锁 XY + # 时法兰 Z = 工具长 + 工件面 + 本值。CLI --final-gap 覆盖。 observe_gap_mm: 15.0 # --no-descent 时的观测下降:只降到离面这么高 [mm], # 方便看规尖偏没偏。必须 ≥ final_gap_mm。 - # 左臂89:悬停间隙约 75mm(0.320−0.223−0.022)⇒ 大约下降 60mm。 + # 左臂89:悬停间隙约 73.5mm(0.320−0.2245−0.022)⇒ 观测下降约 58mm。 final_descent_speed: 0.005 # 最后轻降速度 [m/s](很慢,柔顺) # ✅ 2026-10-08 定稿:下降量**只由几何决定**(法兰 FK − 工具长 − 工件面 − final_gap), # 深度读数只做交叉核对、不再参与任何决策。两段真机事故方向相反、都指向这条: @@ -792,7 +800,7 @@ hole_detect: # ⇒ 读数往**小**错一次就永远到不了目标。 # 同一套读数两个方向各错一次、幅度 150mm 量级、随位姿乱跳 —— 它对"能降多少" # 和"该不该停"都没有发言权。现在它只在偏差 >10mm 时喊一句"深度测距与几何不符"。 - # 现行几何:悬停 0.320 − 工具长 0.223 − 工件面 0.022 = 75mm 间隙 ⇒ 降 73mm 到 2mm。 + # 现行几何:悬停 0.320 − 工具长 0.2245 − 工件面 0.022 = 73.5mm 间隙 ⇒ 降 71.5mm 到 2mm。 # 三道**物理/几何**保险:descent_budget_m(配置上限)+ 腕部 FT 接触检测(停+退 1mm) # + 任一步 _goto 走不到位就停。 # 缺几何(工具长/工件面没配)时**拒绝下降**并报错(保持悬停),绝不拿测距去赌。 @@ -1093,7 +1101,7 @@ tactile: # 传感器型号。必须与 devices/sensors/orisys/models/ 下的子目录名一致。 model: "dd03" # 型号配置随 SDK 一起放:devices/sensors/orisys/models/<型号>/config.json - config_file: "/home/USER/ros2/devices/sensors/orisys/models/dd03/config.json" + config_file: "devices/sensors/orisys/models/dd03/config.json" # 用 by-path 稳定路径(插哪个 USB 口就认哪个口,不受 video 编号漂移影响)。 # 当前:3.4.1=左指 3.4.2=右指;若左右反了,互换两行即可。 left_video: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:X.Y.Z:1.0-video-index0" diff --git a/config/site.yaml b/config/site.yaml index 2be5d83..25dc048 100644 --- a/config/site.yaml +++ b/config/site.yaml @@ -45,11 +45,11 @@ robot: # (status 里的 init=need_init 只说明"绝对位置标定没做",不影响夹持) # 想让开机必初始化(无人值守的上电流程)就显式改 true,并确认爪里是空的。 auto_init: false - named_poses_file: "/home/cc/ros2/src/arm_control/config/named_poses_89.yaml" + named_poses_file: "src/arm_control/config/named_poses_89.yaml" paths: - handeye_file: "/home/cc/ros2/handeye/results/89_eye_in_hand.yaml" - handeye_waypoints: "/home/cc/ros2/handeye/poses/89_eye_in_hand.yaml" - handeye_workdir: "/home/cc/ros2/handeye/tmp/89" + handeye_file: "handeye/results/89_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/89_eye_in_hand.yaml" + handeye_workdir: "handeye/tmp/89" handeye: # Studio tcp_location(法兰系,米 + qwxyz)。与 rdk Tool.params 必须一致。 # 眼在手上:AX=XB 的手是这份 TCP,再折成 cam→法兰写 T_4x4。 @@ -195,7 +195,7 @@ robot: # 为什么必须给文件而不能留空:visual_servo 的 servo_node 需要 # T_flange_cam 矩阵做像素→笛卡尔换算;留空它就直接拒绝启动 #("手眼标定结果缺失"),S04 跟踪起不来。 - handeye_file: "/home/cc/ros2/handeye/results/sim_eye_in_hand.yaml" + handeye_file: "handeye/results/sim_eye_in_hand.yaml" # 运动参数:**大部分**与 89 共用(锚点),但**高度类**必须按仿真台的几何改写。 # # 为什么不能直接共用:真机是"机械臂装在台面同一高度",所以"法兰相对基座" @@ -289,11 +289,11 @@ robot: port: "/dev/ttyUSB1" stroke_mm: 30.0 auto_init: false - named_poses_file: "/home/cc/ros2/src/arm_control/config/named_poses_91.yaml" + named_poses_file: "src/arm_control/config/named_poses_91.yaml" paths: - handeye_file: "/home/cc/ros2/handeye/results/91_eye_in_hand.yaml" - handeye_waypoints: "/home/cc/ros2/handeye/poses/91_eye_in_hand.yaml" - handeye_workdir: "/home/cc/ros2/handeye/tmp/91" + handeye_file: "handeye/results/91_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/91_eye_in_hand.yaml" + handeye_workdir: "handeye/tmp/91" # >>> 右臂跟踪高度:只改这里,89 仍用上面那段 <<< # 法兰 Z;RGI 比平行爪高,通常要比 89 略抬一点。改完重启: # bash ~/ros2/scripts/run_visual_servo_rt.sh --arm 91 --control @@ -314,7 +314,7 @@ robot: paths: # >>> YOLO 权重(标准放法,不必改代码)<<< - # 1) 把 .onnx / .pt 放到 /home/cc/ros2/models/yolo/ + # 1) 把 .onnx / .pt 放到 models/yolo/ # 2) 这一行改成那个绝对路径(写 .onnx 即可;TensorRT 会找同名 .engine) # 3) backend=tensorrt 时必须先导出引擎(只需做一次): # bash ~/ros2/scripts/weights.sh convert <源文件> --name <名字> --group yolo @@ -328,18 +328,18 @@ paths: # ultralytics 解不了 pose 的 TensorRT engine,运行时走 # arm_application/pose_detector.py 的自解码(见该模块头)。 # 仍然用 .engine(实测 ~2.6 ms/帧;onnx 只有 CPU provider,约 27 ms)。 - yolo_weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + yolo_weights: "models/yolo/gongjian0929.onnx" # 换模型:把 models/yolo/ 下别的 .onnx / .pt 绝对路径填进来即可。 # 未在用的模型已移出仓库(体积大且干扰 `weights.sh list`),要恢复见 models/README.md。 # 入孔模型(backend=yolo 时把孔当框)。two_stage 用下面的工件权重。 # pose 模型两处共用同一份 —— 它既能框工件,又直接给 k1/k2 关键点。 # 查悬空引用:bash ~/ros2/scripts/weights.sh doctor - hole_weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + hole_weights: "models/yolo/gongjian0929.onnx" # 下面是缺省(未匹配到 robots[] 时)。真机以 robots[].paths 为准:89 / 91 各一份。 - handeye_file: "/home/cc/ros2/handeye/results/89_eye_in_hand.yaml" - handeye_waypoints: "/home/cc/ros2/handeye/poses/89_eye_in_hand.yaml" - handeye_calibrate: "/home/cc/ros2/handeye/calibrate.yaml" - handeye_workdir: "/home/cc/ros2/handeye/tmp/89" + handeye_file: "handeye/results/89_eye_in_hand.yaml" + handeye_waypoints: "handeye/poses/89_eye_in_hand.yaml" + handeye_calibrate: "handeye/calibrate.yaml" + handeye_workdir: "handeye/tmp/89" # 空=默认 DDS(SHM);勿强制 UDP-only,否则相机预览收不到 /workpiece/detection fastdds_profile: "" @@ -663,10 +663,10 @@ gripper: hole_detect: backend: "two_stage" # two_stage | yolo | hough # backend=yolo 时用。 - weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + weights: "models/yolo/gongjian0929.onnx" # 二阶段工件检测权重。pose 模型既框工件、又直接给 k1/k2 关键点, # 所以和上面的跟踪权重是同一份。 - workpiece_weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + workpiece_weights: "models/yolo/gongjian0929.onnx" # pose 关键点优先:检测器给了 k1/k2 就直接用,不用再找圆。 # 关键点不可信(低于 keypoint_min_conf / 两点几乎重合)时自动回落到 # 亮环 + Hough 那套老路径。 @@ -674,7 +674,7 @@ hole_detect: keypoint_min_conf: 0.30 keypoint_min_dist_px: 15.0 keypoint_hole_radius_px: 12.0 # 关键点没有半径;反投影时按这个半径取孔周深度环 - config: "/home/cc/ros2/src/arm_application/config/hole_detect.yaml" + config: "src/arm_application/config/hole_detect.yaml" conf: 0.25 iou: 0.7 imgsz: 640 @@ -1085,7 +1085,7 @@ tactile: # 传感器型号。必须与 devices/sensors/orisys/models/ 下的子目录名一致。 model: "dd03" # 型号配置随 SDK 一起放:devices/sensors/orisys/models/<型号>/config.json - config_file: "/home/cc/ros2/devices/sensors/orisys/models/dd03/config.json" + config_file: "devices/sensors/orisys/models/dd03/config.json" # 用 by-path 稳定路径(插哪个 USB 口就认哪个口,不受 video 编号漂移影响)。 # 当前:3.4.1=左指 3.4.2=右指;若左右反了,互换两行即可。 left_video: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:3.4.1:1.0-video-index0" diff --git a/handeye/waypoints.yaml b/handeye/waypoints.yaml index 0ac49d0..bc27002 100644 --- a/handeye/waypoints.yaml +++ b/handeye/waypoints.yaml @@ -20,6 +20,6 @@ handeye_waypoints: camera_namespace: "" image_topic: "" preview_window: true - append_poses_file: "/home/cc/ros2/handeye/tmp/taught_poses.txt" + append_poses_file: "handeye/tmp/taught_poses.txt" waypoint_times: [4.0, 4.0, 4.0, 4.0, 4.0, 4.5, 4.5, 4.5, 4.5, 4.5, 4.5, 4.0, 4.0] diff --git a/scripts/env.sh b/scripts/env.sh index 119f2ea..47bcdca 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -30,8 +30,12 @@ if [[ -f "${HOME}/anaconda3/etc/profile.d/conda.sh" ]]; then unset _env_helper fi -# shellcheck disable=SC1091 -source "${ROOT}/install/setup.bash" +if [[ -f "${ROOT}/install/setup.bash" ]]; then + # shellcheck disable=SC1091 + source "${ROOT}/install/setup.bash" +else + echo "警告:${ROOT}/install 还没编译。工位窗口可以先开;真机栈要先在仓库根目录 colcon build(不要在中文路径下编)。" >&2 +fi export LD_LIBRARY_PATH="${HOME}/flexiv_install/lib:${LD_LIBRARY_PATH:-}" # pip 版 TensorRT(tensorrt-cu12)—— 加载 YOLO `.engine` 需要它 _TRT_SITE="${CONDA_PREFIX:-${HOME}/anaconda3/envs/ros_flexiv}/lib/python3.11/site-packages" diff --git a/scripts/gen_site_example.py b/scripts/gen_site_example.py index 6852ebc..980682a 100644 --- a/scripts/gen_site_example.py +++ b/scripts/gen_site_example.py @@ -51,8 +51,8 @@ HEADER = """\ # 用法:复制本文件为 config/site.yaml,然后把所有 <占位符> 换成现场实际值。 # 现场值包括:机械臂 SN / IP、相机序列号、夹爪串口、传感器 USB 路径。 # -# 说明:site.yaml 里的路径出现 /home/USER 是因为生成时匿名化了用户名; -# 部署时请改成实际用户的 home 目录。 +# 说明:仓库里的文件用相对仓库根的路径(例如 models/yolo/...)。 +# 不要改回某个用户的绝对家目录,换台电脑就会找不到文件。 # ============================================================================= """ diff --git a/scripts/install_rdk_offline.sh b/scripts/install_rdk_offline.sh index 0a4b2ee..50603ef 100755 --- a/scripts/install_rdk_offline.sh +++ b/scripts/install_rdk_offline.sh @@ -99,6 +99,6 @@ Next: --packages-up-to flexiv_bringup flexiv_hardware arm_bringup Python (already ready, no GitHub): - /home/cc/rdk_py310/bin/python + \${HOME}/rdk_py310/bin/python bash ${ROOT}/scripts/test_rdk_python.sh EOF diff --git a/scripts/lib/dev_tools.sh b/scripts/lib/dev_tools.sh new file mode 100644 index 0000000..95dca43 --- /dev/null +++ b/scripts/lib/dev_tools.sh @@ -0,0 +1,31 @@ +# 本机工具链。仓库位置用调用方已经算好的 ROOT,不写死某个用户的家目录。 +# 用法:source "${ROOT}/scripts/lib/dev_tools.sh" + +dev_source_conda() { + local cand + for cand in \ + "${HOME}/anaconda3/etc/profile.d/conda.sh" \ + "${HOME}/miniconda3/etc/profile.d/conda.sh" + do + if [[ -f "${cand}" ]]; then + # shellcheck disable=SC1090 + source "${cand}" + return 0 + fi + done + echo "ERROR: 找不到 conda.sh。试过 \$HOME/anaconda3 和 \$HOME/miniconda3。" >&2 + return 1 +} + +dev_rdk_python() { + if [[ -n "${RDK_PYTHON:-}" && -x "${RDK_PYTHON}" ]]; then + printf '%s\n' "${RDK_PYTHON}" + return 0 + fi + if [[ -x "${HOME}/rdk_py310/bin/python" ]]; then + printf '%s\n' "${HOME}/rdk_py310/bin/python" + return 0 + fi + echo "ERROR: 找不到 RDK 的 Python。设置 RDK_PYTHON,或装到 \$HOME/rdk_py310。" >&2 + return 1 +} diff --git a/scripts/lib/export_site_env.py b/scripts/lib/export_site_env.py index 70dc5ab..e0485a1 100644 --- a/scripts/lib/export_site_env.py +++ b/scripts/lib/export_site_env.py @@ -42,7 +42,13 @@ def load_site(path: Path | None = None) -> dict: data = yaml.safe_load(text) or {} if not isinstance(data, dict): raise ValueError(f"无效的 site.yaml: {path}") - return data + # 相对路径按仓库根展开。site.yaml 里不再写 /home/某用户/ros2。 + src = str(_root() / "src" / "arm_application") + if src not in sys.path: + sys.path.insert(0, src) + from arm_application.site_io import absolutize_site_paths + + return absolutize_site_paths(data, _root()) def list_robots(data: dict) -> list[dict]: diff --git a/scripts/lib/real_preflight.py b/scripts/lib/real_preflight.py index d434e5c..d6a3d20 100644 --- a/scripts/lib/real_preflight.py +++ b/scripts/lib/real_preflight.py @@ -55,7 +55,15 @@ LEVEL_FAIL = "fail" #: 级别排序:fail 最先报 _LEVEL_RANK = {LEVEL_FAIL: 0, LEVEL_WARN: 1, LEVEL_OK: 2} -DEFAULT_RDK_PYTHON = os.environ.get("RDK_PYTHON") or "/home/cc/rdk_py310/bin/python" +def default_rdk_python() -> str: + """RDK 解释器。环境变量优先,否则用当前用户家目录下的 rdk_py310。""" + env = os.environ.get("RDK_PYTHON", "").strip() + if env: + return env + return str(Path.home() / "rdk_py310" / "bin" / "python") + + +DEFAULT_RDK_PYTHON = default_rdk_python() #: 上一轮没退干净的进程特征。留着它们会让这一轮的错误指向别处: #: · ros2_control_node 占着 RDK(同一个 SN 是独占访问) diff --git a/scripts/rdk_release_movel.py b/scripts/rdk_release_movel.py index fb8ea56..5e5e46b 100644 --- a/scripts/rdk_release_movel.py +++ b/scripts/rdk_release_movel.py @@ -23,7 +23,9 @@ sys.path.insert(0, str(ROOT / "scripts" / "lib")) from rdk_release import is_control_stack_cmd # noqa: E402 -RDK_PYTHON = os.environ.get("RDK_PYTHON", "/home/cc/rdk_py310/bin/python") +RDK_PYTHON = os.environ.get("RDK_PYTHON", "").strip() or str( + Path.home() / "rdk_py310" / "bin" / "python" +) def _procs() -> list[tuple[int, str]]: diff --git a/scripts/rdk_switch_tool.sh b/scripts/rdk_switch_tool.sh index d441d7c..69fcfa1 100755 --- a/scripts/rdk_switch_tool.sh +++ b/scripts/rdk_switch_tool.sh @@ -12,7 +12,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck disable=SC1091 source "${ROOT}/scripts/lib/load_site_config.sh" -RDK_PYTHON="${RDK_PYTHON:-/home/cc/rdk_py310/bin/python}" +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/dev_tools.sh" +RDK_PYTHON="$(dev_rdk_python)" SN="${ROBOT_SN:-${SITE_ROBOT_SN:-}}" NAME="${SITE_ROBOT_TOOL_NAME:-}" IFACE="${SITE_NET_IFACE:-}" diff --git a/scripts/run_all.sh b/scripts/run_all.sh index 647484f..9e20e89 100755 --- a/scripts/run_all.sh +++ b/scripts/run_all.sh @@ -139,7 +139,9 @@ if [[ "${D435_ONLY}" == true ]]; then fi echo "=== MoveL:XY 对准 K0,工具尖高度保持当前(K0 的 z 是工件表面)===" # --go:不按直线距离拦截。800 mm 是脚本自己的保护,不是机械臂限位。 - exec /home/cc/rdk_py310/bin/python "${ROOT}/scripts/rdk_goto_xyz.py" \ + # shellcheck disable=SC1091 + source "${ROOT}/scripts/lib/dev_tools.sh" + exec "$(dev_rdk_python)" "${ROOT}/scripts/rdk_goto_xyz.py" \ --xyz "${kx}" "${ky}" "${kz}" --keep-z --go --vel 0.05 fi diff --git a/scripts/run_mock_demo.sh b/scripts/run_mock_demo.sh index cea37c8..2a827ea 100755 --- a/scripts/run_mock_demo.sh +++ b/scripts/run_mock_demo.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -source /home/cc/anaconda3/etc/profile.d/conda.sh +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/dev_tools.sh" +dev_source_conda conda activate ros_flexiv +if [[ ! -f "${ROOT}/install/setup.bash" ]]; then + echo "ERROR: ${ROOT}/install/setup.bash 不存在。先在仓库根目录 colcon build。" >&2 + exit 1 +fi +# shellcheck disable=SC1091 source "${ROOT}/install/setup.bash" ros2 run arm_application demo_named_poses --ros-args \ -p "poses:=['home','ready','inspect','pos_left','home']" \ diff --git a/scripts/run_real.sh b/scripts/run_real.sh index 1517e97..e5f7fe1 100755 --- a/scripts/run_real.sh +++ b/scripts/run_real.sh @@ -26,8 +26,14 @@ while [[ $# -gt 0 ]]; do esac done -source /home/cc/anaconda3/etc/profile.d/conda.sh +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/dev_tools.sh" +dev_source_conda conda activate ros_flexiv +if [[ ! -f "${ROOT}/install/setup.bash" ]]; then + echo "ERROR: ${ROOT}/install/setup.bash 不存在。先在仓库根目录 colcon build,不要在中文路径下编。" >&2 + exit 1 +fi # shellcheck disable=SC1091 source "${ROOT}/install/setup.bash" # shellcheck disable=SC1091 @@ -44,7 +50,7 @@ export LD_LIBRARY_PATH="${FLEXIV_PREFIX}/lib:${LD_LIBRARY_PATH:-}" ROBOT_SN="${SITE_ROBOT_SN:?run_real.sh 需要已配置的机械臂 SN —— 见 scripts/lib/load_site_config.sh 的报错}" ROBOT_IP="${ROBOT_IP:-${SITE_ROBOT_IP:-192.168.100.1}}" NET_IFACE="${SITE_NET_IFACE:-}" -RDK_PYTHON="${RDK_PYTHON:-/home/cc/rdk_py310/bin/python}" +RDK_PYTHON="$(dev_rdk_python)" if [[ "${LIST_ONLY}" == true ]]; then python3 "${ROOT}/scripts/lib/export_site_env.py" --print diff --git a/scripts/show_site_config.sh b/scripts/show_site_config.sh index 5b5e624..51f77d7 100755 --- a/scripts/show_site_config.sh +++ b/scripts/show_site_config.sh @@ -14,7 +14,7 @@ python3 "${ROOT}/scripts/lib/export_site_env.py" --print ${ROBOT_SN:+--sn "$ROBO echo echo "Edit: ${SITE_CONFIG:-$ROOT/config/site.yaml}" echo "Switch weights:" -echo " paths.yolo_weights: \"/home/cc/ros2/models/yolo/detect_best_0.onnx\" # 写 onnx 或 pt 均可" +echo " paths.yolo_weights: \"models/yolo/detect_best_0.onnx\" # 相对仓库根,写 onnx 或 pt 均可" echo " 改完确认标签和 engine 都对:" echo " bash ~/ros2/scripts/weights.sh list" echo " bash ~/ros2/scripts/weights.sh doctor --check-engines" diff --git a/scripts/test_rdk_python.sh b/scripts/test_rdk_python.sh index a41ae48..d5eb406 100755 --- a/scripts/test_rdk_python.sh +++ b/scripts/test_rdk_python.sh @@ -8,7 +8,9 @@ source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true SN="${1:-${ROBOT_SN:-${SITE_ROBOT_SN:-}}}" ROBOT_IP="${ROBOT_IP:-${SITE_ROBOT_IP:-192.168.100.1}}" IFACE="${SITE_NET_IFACE:-}" -PY="${RDK_PYTHON:-/home/cc/rdk_py310/bin/python}" +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/dev_tools.sh" +PY="$(dev_rdk_python)" echo "Using: ${PY}" echo "Robot SN: ${SN} IP: ${ROBOT_IP} iface: ${IFACE:-auto}" diff --git a/src/arm_application/arm_application/handeye_calibrate.py b/src/arm_application/arm_application/handeye_calibrate.py index e7e827d..a04c653 100644 --- a/src/arm_application/arm_application/handeye_calibrate.py +++ b/src/arm_application/arm_application/handeye_calibrate.py @@ -137,7 +137,9 @@ class HandeyeCalibrate(TolerantNode): self.declare_parameter('pin_hold_sec', 0.45) # 每拍多长 self.declare_parameter('skip_unstable_poses', True) - self.declare_parameter('output_dir', '/home/cc/ros2/handeye/tmp') + from arm_application.site_io import ros2_root + + self.declare_parameter('output_dir', str(ros2_root() / 'handeye' / 'tmp')) self.declare_parameter('result_file', '') self.declare_parameter('cleanup_images', True) # 眼在手上:AX=XB 的 gripper 用 Studio 工具 TCP,再折成 cam→法兰写 T_4x4。 diff --git a/src/arm_application/arm_application/handeye_teach.py b/src/arm_application/arm_application/handeye_teach.py index e2f5b43..c657a18 100644 --- a/src/arm_application/arm_application/handeye_teach.py +++ b/src/arm_application/arm_application/handeye_teach.py @@ -108,10 +108,12 @@ class RdkBridge: self.robot_sn = robot_sn self._last_q: Optional[List[float]] = None self._io = threading.Lock() - py = os.environ.get('RDK_PYTHON', '/home/cc/rdk_py310/bin/python') - bridge = Path.home() / 'ros2' / 'scripts' / 'rdk_drag_bridge.py' - if not bridge.is_file(): - bridge = Path('/home/cc/ros2/scripts/rdk_drag_bridge.py') + from arm_application.site_io import ros2_root + + py = os.environ.get('RDK_PYTHON', '').strip() or str( + Path.home() / 'rdk_py310' / 'bin' / 'python' + ) + bridge = ros2_root() / 'scripts' / 'rdk_drag_bridge.py' if not bridge.is_file(): raise FileNotFoundError(f'找不到 rdk_drag_bridge.py:{bridge}') @@ -246,7 +248,9 @@ class RdkBridge: class HandeyeTeach(TolerantNode): def __init__(self) -> None: super().__init__('handeye_teach') - self.declare_parameter('output_dir', '/home/cc/ros2/handeye/tmp') + from arm_application.site_io import ros2_root + + self.declare_parameter('output_dir', str(ros2_root() / 'handeye' / 'tmp')) self.declare_parameter('width', 1280) self.declare_parameter('height', 720) self.declare_parameter('fps', 15) diff --git a/src/arm_application/arm_application/perception_node.py b/src/arm_application/arm_application/perception_node.py index 262365f..cb94271 100644 --- a/src/arm_application/arm_application/perception_node.py +++ b/src/arm_application/arm_application/perception_node.py @@ -105,7 +105,7 @@ class PerceptionNode(TolerantNode): self.declare_parameter("image_topic", "") self.declare_parameter("camera_info_topic", "") self.declare_parameter("depth_topic", "/d405/aligned_depth_to_color/image_raw") - self.declare_parameter("weights", "/home/cc/ros2/models/yolo/gongjian0929.onnx") + self.declare_parameter("weights", "models/yolo/gongjian0929.onnx") self.declare_parameter("conf", 0.10) self.declare_parameter("iou", 0.45) self.declare_parameter("imgsz", 640) @@ -336,7 +336,9 @@ class PerceptionNode(TolerantNode): r_pos=float(self.get_parameter("kf_r_pos").value), ) - weights_raw = str(self.get_parameter("weights").value) + from arm_application.site_io import resolve_repo_path + + weights_raw = resolve_repo_path(str(self.get_parameter("weights").value)) weights = str(resolve_weights(weights_raw, self.backend_name)) yolo_cfg = { "weights": weights, diff --git a/src/arm_application/arm_application/realsense_color_pub.py b/src/arm_application/arm_application/realsense_color_pub.py index cda88d9..1d8464a 100644 --- a/src/arm_application/arm_application/realsense_color_pub.py +++ b/src/arm_application/arm_application/realsense_color_pub.py @@ -73,15 +73,9 @@ def _fill_camera_info( def _site_yaml_path() -> Path: - env = str(os.environ.get("SITE_CONFIG") or "").strip() - if env: - p = Path(env).expanduser() - if p.is_file(): - return p - home = Path.home() / "ros2" / "config" / "site.yaml" - if home.is_file(): - return home - return Path("/home/cc/ros2/config/site.yaml") + from arm_application.site_io import site_path + + return site_path() class RealsenseColorPub(TolerantNode): diff --git a/src/arm_application/arm_application/servo_node.py b/src/arm_application/arm_application/servo_node.py index 4e49b88..848dbcd 100644 --- a/src/arm_application/arm_application/servo_node.py +++ b/src/arm_application/arm_application/servo_node.py @@ -126,7 +126,7 @@ class ServoNode(TolerantNode): self.declare_parameter("point_topic", "visual_servo/filtered_point") # 默认值只作兜底:真机运行一定由 launch 传入 site.yaml 的 paths.handeye_file。 # 指向 /tmp 是历史遗留(标定临时产物),会让人误以为"标定结果在那里"。 - self.declare_parameter("handeye_file", "/home/cc/ros2/handeye/results/89_eye_in_hand.yaml") + self.declare_parameter("handeye_file", "handeye/results/89_eye_in_hand.yaml") # eye_in_hand = cam→法兰;eye_to_hand = cam→基座。写错会把固定相机当腕部用。 self.declare_parameter("handeye_mode", "eye_in_hand") self.declare_parameter("use_static_handeye_tf", False) @@ -374,9 +374,10 @@ class ServoNode(TolerantNode): self.ws_min = np.array([float(v) for v in ws_min[:3]], dtype=float) self.ws_max = np.array([float(v) for v in ws_max[:3]], dtype=float) - self.T_cam_to_gripper = load_handeye_T( - str(self.get_parameter("handeye_file").value) - ) + from arm_application.site_io import resolve_repo_path + + handeye_path = resolve_repo_path(str(self.get_parameter("handeye_file").value)) + self.T_cam_to_gripper = load_handeye_T(handeye_path) self.handeye_mode = str( self.get_parameter("handeye_mode").value or "eye_in_hand" ).strip().lower() @@ -399,7 +400,7 @@ class ServoNode(TolerantNode): t_he = np.asarray(self.T_cam_to_gripper[:3, 3], dtype=float) axis_z = float(np.clip(R_he[2, 2], -1.0, 1.0)) self._cam_tilt_deg = float(np.degrees(np.arccos(axis_z))) - he_path = str(self.get_parameter("handeye_file").value) + he_path = handeye_path if self.handeye_mode == "eye_to_hand": self.get_logger().warn( f"HANDEYE yaml={he_path} mode=eye_to_hand " diff --git a/src/arm_application/arm_application/site_io.py b/src/arm_application/arm_application/site_io.py index 9243123..8ddc507 100644 --- a/src/arm_application/arm_application/site_io.py +++ b/src/arm_application/arm_application/site_io.py @@ -10,13 +10,83 @@ import yaml def ros2_root() -> Path: + """仓库根目录。先看环境变量,再从本文件向上找 ``config/site.yaml``。 + + 不能默认 ``~/ros2``:别人把仓库克隆到别的目录时,写死的家目录路径是空的, + 启动会报文件不存在。 + """ for key in ("SITE_ROS2_ROOT", "ROS2_WS"): v = os.environ.get(key, "").strip() if v: return Path(v).expanduser() + here = Path(__file__).resolve() + for parent in here.parents: + if (parent / "config" / "site.yaml").is_file() and (parent / "scripts" / "env.sh").is_file(): + return parent return Path.home() / "ros2" +_PATH_KEYS = frozenset({ + "named_poses_file", + "handeye_file", + "handeye_waypoints", + "handeye_workdir", + "handeye_calibrate", + "yolo_weights", + "hole_weights", + "weights", + "workpiece_weights", + "config_file", + "append_poses_file", +}) + + +def resolve_repo_path(value: str, root: Path | None = None) -> str: + """仓库内的相对路径展开成绝对路径。已经是绝对路径的原样保留。""" + text = str(value or "").strip() + if not text: + return "" + path = Path(text).expanduser() + if path.is_absolute(): + return str(path) + base = root or ros2_root() + return str((base / path).resolve()) + + +def _is_path_key(key: str) -> bool: + return ( + key in _PATH_KEYS + or key.startswith("handeye_file") + or key.startswith("handeye_waypoints") + or key.endswith("_weights") + or key.endswith("_file") + or key.endswith("_workdir") + ) + + +def absolutize_site_paths(data: Any, root: Path | None = None) -> Any: + """把 site 里的仓库相对路径展开。只动路径字段,不动别的字符串。""" + base = root or ros2_root() + if isinstance(data, dict): + out: dict[Any, Any] = {} + for key, val in data.items(): + name = str(key) + if isinstance(val, str) and _is_path_key(name): + out[key] = resolve_repo_path(val, base) + elif ( + name == "config" + and isinstance(val, str) + and (val.endswith((".yaml", ".yml", ".json")) or "/" in val) + ): + out[key] = resolve_repo_path(val, base) + else: + out[key] = absolutize_site_paths(val, base) + return out + if isinstance(data, list): + return [absolutize_site_paths(item, base) for item in data] + return data + + def site_path() -> Path: env = os.environ.get("SITE_CONFIG", "").strip() if env: @@ -29,7 +99,11 @@ def load_site_dict(path: str | Path | None = None) -> dict[str, Any]: if not p.is_file(): return {} data = yaml.safe_load(p.read_text(encoding="utf-8")) or {} - return data if isinstance(data, dict) else {} + if not isinstance(data, dict): + return {} + resolved = p.resolve() + root = resolved.parent.parent if resolved.parent.name == "config" else ros2_root() + return absolutize_site_paths(data, root) def deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]: diff --git a/src/arm_application/config/perception.yaml b/src/arm_application/config/perception.yaml index 5685bf9..c2411e9 100644 --- a/src/arm_application/config/perception.yaml +++ b/src/arm_application/config/perception.yaml @@ -12,7 +12,7 @@ perception_node: image_topic: "" camera_info_topic: "" depth_topic: "/d405/aligned_depth_to_color/image_raw" - weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + weights: "models/yolo/gongjian0929.onnx" conf: 0.10 iou: 0.45 imgsz: 640 diff --git a/src/arm_application/config/servo.yaml b/src/arm_application/config/servo.yaml index 62dd669..ad71745 100644 --- a/src/arm_application/config/servo.yaml +++ b/src/arm_application/config/servo.yaml @@ -10,7 +10,7 @@ servo_node: camera_frame: "d405_color_optical_frame" traj_topic: "rizon_arm_controller/joint_trajectory" point_topic: "visual_servo/filtered_point" - handeye_file: "/home/cc/ros2/src/arm_application/config/handeye_result.yaml" + handeye_file: "src/arm_application/config/handeye_result.yaml" use_static_handeye_tf: false handeye_child_frame: "d405_color_optical_frame" diff --git a/src/arm_driver_flexiv/arm_driver_flexiv/rdk_client.py b/src/arm_driver_flexiv/arm_driver_flexiv/rdk_client.py index 1837760..572cfca 100644 --- a/src/arm_driver_flexiv/arm_driver_flexiv/rdk_client.py +++ b/src/arm_driver_flexiv/arm_driver_flexiv/rdk_client.py @@ -27,8 +27,16 @@ import time from pathlib import Path from typing import Any, Dict, List, Optional +def default_rdk_python() -> str: + """RDK 解释器在当前用户家目录,不写死某一台机器的用户名。""" + env = os.environ.get("RDK_PYTHON", "").strip() + if env: + return env + return str(Path.home() / "rdk_py310" / "bin" / "python") + + #: 桥进程必须用这个解释器 —— RDK 绑定只在这里有。 -DEFAULT_RDK_PYTHON = "/home/cc/rdk_py310/bin/python" +DEFAULT_RDK_PYTHON = default_rdk_python() #: 桥脚本位置(相对工作区根)。 BRIDGE_SCRIPT_REL = "scripts/rdk_drag_bridge.py"