diff --git a/.gitignore b/.gitignore index 172fe3e..a4bcc15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,4 @@ -# 本机现场配置(含 IP / 序列号;从 site.example.yaml 复制) -config/site.yaml -# 模型权重二进制 —— 一律不进 git(体积大、每台机器重新转换即可) -# 用通配模式而不是逐个目录列举,否则新增一个分组就会有人误提交二进制。 -# 注意:*.labels.yaml 是**文本标签配置**,不属于二进制,应该进 git。 -models/**/*.pt -models/**/*.pth -models/**/*.onnx -models/**/*.engine -models/**/*.trt -models/**/*.plan -models/**/*.tflite # staging = 待转换的临时区,内容不进 git(含转换后归档的 done/) # 但 .gitkeep 和说明文档要留着,否则新克隆的仓库里这个目录不存在/看不懂。 @@ -25,8 +13,7 @@ log/ # 仿真的构建产物 —— 与真机的 build/ install/ 隔离(见 scripts/build_sim.sh) # 两套产物目录并存是刻意的:工作区只有一份源码,两个环境的依赖路径不同, # 共用 install/ 会互相覆盖。 -build_sim/ -install_sim/ + log_sim/ # 真机各终端(T1 栈 / T2 相机 / T3 触觉 / T4 夹爪)的日志与截图。 # 与 log_sim/ 对称:日志是运行产物,不进 git;跑完要留档的证据(如相机截图) @@ -46,26 +33,10 @@ __pycache__/ .DS_Store *.swp -# ROS bags -*.bag -*.db3 - -# Hand-eye backups -*.bak -*.bak.* - -# 可选离线 RDK / Isaac(体积大) -thirdparty/ -vendor/ # 手眼标定的临时产物(采点图、taught_poses.txt、captures.json) handeye/tmp/* !handeye/tmp/.gitkeep -# 仿真手眼真值:**生成物**,不是标定结果 —— 由 scripts/gen_sim_handeye.py 从 -# URDF/TF 真值导出,run_gauge_assembly.sh --sim 每次自动重生成。 -# 不跟踪的理由:内容里有一行生成时间,每跑一次就脏一次仓库,让人分不清 -# "这次改动是代码改的还是只是跑了一遍"。真机标定结果(89_/91_*.yaml)**要跟踪**, -# 那是需要留档的实测数据,不是推导出来的。 -handeye/results/sim_*.yaml + diff --git a/config/site.example.yaml b/config/site.example.yaml index 2dc0aee..1a86992 100644 --- a/config/site.example.yaml +++ b/config/site.example.yaml @@ -177,7 +177,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 - tool_tip_below_flange_m: 0.223 + # 下臂安全距离按 tool10120938 的 TCP +Z,不再用旧 tool1 的 0.223。 + tool_tip_below_flange_m: 0.2245 # >>> 仿真那台臂(Gazebo)也登记成 robots[] 里的一台 <<< # 为什么必须登记:应用是按"当前臂"的名字去查 frame / joint / 相机话题的 #(`sim_env.sh` 注释里说的"仿真与真机共用同一套 site.yaml")。仿真的 TF 与关节 @@ -592,6 +593,7 @@ servo: # D405 跟住后,法兰 XY 连续这么久位移不超过 lock_still_move_m → /visual_servo/locked # 顶视一次性把臂迁过去的那段路不算:必须是腕部有效目标。大幅一动就重计时。 lock_still_sec: 3.0 + wrist_align_sec: 1.0 lock_still_move_m: 0.015 # 15 mm;超过就重计时,避免"晃一下刚好满 3 秒" e_xy_clip_m: 0.200 # 大误差仍按 v_max 走,靠 a_max 加速 v_max_xy: 0.45 # 法兰 XY 上限 450 mm/s @@ -765,6 +767,7 @@ hole_detect: # —— 柔顺运动(夹爪持规,防急加速/急停/过冲把规震脱):速度放慢 + 平滑刹车 —— align_speed: 0.014 # 对准段笛卡尔速度 [m/s](原0.03,放缓防冲) move_speed: 0.022 # 升降/粗居中段速度 [m/s](原0.05,放缓防震规) + hole_visual_only: true final_gap_mm: 2.0 # 对准后贴面轻降目标:规尖悬在孔面上方这么高 [mm](不接触) observe_gap_mm: 15.0 # --no-descent 时的观测下降:只降到离面这么高 [mm], # 方便看规尖偏没偏。必须 ≥ final_gap_mm。 diff --git a/config/site.yaml b/config/site.yaml new file mode 100644 index 0000000..4e057b2 --- /dev/null +++ b/config/site.yaml @@ -0,0 +1,1130 @@ +# ============================================================================= +# site.yaml — 本机唯一配置入口(不进 git;从 site.example.yaml 复制) +# 路径:~/ros2/config/site.yaml +# +# 89 与 91 的手眼 / 腕部相机 / 夹爪 / 点位互相独立,禁止混用。 +# 切换机械臂:改 robot.sn,或所有终端加同一 --arm 89|91。 +# ============================================================================= + +robot: + # >>> 切换机械臂:改成 robots 里某一台的完整 SN <<< + sn: "Rizon4-063989" + # Studio Tools 名(一字不差)。空=不切换。查:bash ~/ros2/scripts/rdk_switch_tool.sh --list + # 左臂 89 的激活工具写在 robots[] 里(tool10120938);这里只给未覆盖的臂当缺省。 + tool_name: "tool1" + # 每台登记 SN + IP,以及该臂自己的相机/夹爪/手眼/点位(覆盖下面的全局段)。 + robots: + - sn: "Rizon4-063989" + arm: left + ip: "192.168.2.101" + pc_ip: "192.168.2.10" + iface: "eth0" + label: "左臂89" + # 本臂装了 Orisys 视触觉(只有左臂有)。脚本靠这一项决定要不要拉触觉服务, + # 而不是靠"SN 等于某个具体值"这种写死的判断(换台臂就会失效)。 + tactile: true + # 2026-10-12 示教的工具 TCP(tool + 1012 + 0938)。眼在手上 AX=XB 用这份 TCP。 + tool_name: "tool10120938" + camera: + name: d405_left # 话题 /d405_left/... 与 91 分开 + serial: "352122274408" # 左腕 D405(一直在用的那台) + auto_exposure: true + ae_limit_us: 4000 + emitter_enabled: false + gripper: + model: "PGEA-100-20-W" + sn: "26610799" + port: "/dev/ttyUSB0" + stroke_mm: 20.0 + # 大寰上电后必须初始化一次(写 0xA5),而初始化的动作是**手指先回零** + # = 张开到满行程。如果这时爪子已经夹着通止规/工件,规会被放开、掉下去。 + # 2026-09-28 现场实测过:auto_init 走一遍,开口从 19.18mm 变成 20.00mm。 + # 所以这里的默认是 **false**:由人决定什么时候初始化。 + # · 上电后、还没装夹 → 可以临时开一次:SITE_GRIP_AUTO_INIT=true bash scripts/run_gripper.sh --arm 89 + # · 已经装夹(装配过程中)→ 保持 false;不初始化也能读状态、也能开合 + # (status 里的 init=need_init 只说明"绝对位置标定没做",不影响夹持) + # 想让开机必初始化(无人值守的上电流程)就显式改 true,并确认爪里是空的。 + auto_init: false + named_poses_file: "/home/cc/ros2/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: + # 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] + # 左臂跟踪高度(只在 --arm 89 时生效,改 91 的 servo 不影响这里) + # 锚点 `&servo_left`:下面的"仿真台"条目直接引用它 —— 仿真建的就是左臂, + # 悬停/锁高必须与 89 **一模一样**,抄两份迟早漂移。 + servo: &servo_left + # 2026-09-29 底座加高后由 0.35 → 0.36:工件上表面实测抬高 10mm + # (0.012 → 0.022),锁高同步 +0.011 ⇒ 相机到工件的视距**与加高前一致** + # (≈0.213m,跟踪那一档已实拍验证过检出稳定),不是随手抬的。 + # 跟踪锁高与 hole_hover_z_m 相同:先到这个悬高,一阶段只动 XY。 + # 对孔完成后再下降。以前 ee_z 高于悬停(0.36),对孔前会先降一截, + # 倾斜相机下孔位被重投影,偏差变大。 + ee_z_m: 0.320 # 跟踪锁高:法兰相对基座 Z [m],与 hole_hover_z_m 同一值 + min_ee_z_m: 0.20 + safe_z_m: 0.45 + z_min_m: 0.22 + xy_track: hover # 法兰(夹爪中心) XY 压在工件上,不是画面中心 + ee_vertical_down: true # 法兰 +Z 朝世界 −Z + hover_offset_ee_xy_m: [0.0, 0.0] # 爪心相对法兰 XY;平行爪同轴保持 0 + # >>> 二阶段孔定位悬停锁高:只改这里,91 用下面那段 <<< + # 法兰 Z [m];要兼顾:够低拍清 k1/k2 螺纹孔,够高通止规尖不碰工件。 + # 调试:bash ~/ros2/scripts/run_stage2_locate.sh --arm 89 --hover-only + # = 工件上表面 0.022(本臂 gauge_assembly.workpiece_surface_z_m,加底座后重测) + # + 工具长 0.245 + 间隙 0.002 + # 2026-09-29 实测:按 0.23 悬停时工具尖端落在工件面**下** 5~10mm, + # 悬停这一步就把通止规插进了工件(见本臂 gauge_assembly 段的完整说明)。 + # + # 2026-09-29 二次修正 —— 上面那条"工具几何"只保证**不碰工件**,它保证不了 + # **相机看得见孔**,而二阶段的第一步就是拍 k1/k2。旧值 0.259 的后果实测: + # 相机光心在法兰下方 0.136m(手眼 T_cam_to_gripper + 29.7° 倾角) + # 法兰 0.259 → 相机离工件面 0.123m → 检出 0/16,conf 峰值 0.012(噪声) + # 法兰 0.309 → 相机离工件面 0.173m → 检出 16/16,conf 0.87, + # k0/k1/k2 置信度全 0.999~1.000,k1-k2 间距 156px 稳定 + # gauge_fsm.hover_z_guard 的设计注释也写着"腕部相机要离工件面 ~0.17m + # 才看得清孔",所以 0.309 才是这条同时满足"相机看得见"和"规尖不碰"的值 + # (规尖此时在面上 64mm,比旧值更安全)。 + # 最后 2mm 的贴近由 stage2 的**深度闭环轻降**(final_gap_mm)完成 —— + # 它按实测间隙往下走,与绝对悬停高无关,所以抬高悬停不会让尖撞工件。 + # + # 2026-09-29 三次修正(工件加了底座,工作平面重新感知): + # 拿 gongjian0929 在**实时图**上出 k0/k1/k2,再取各点邻域深度投到基座系: + # k1 = +0.0220 / +0.0221 / +0.0223 m(多帧重复性 0.4mm 内) + # k2 = +0.0223 / +0.0224 m + # k0 = +0.0165 / +0.0186 m(偏低约 6mm 且抖,局部读数噪声大) + # 三点"中心 vs 外环"深度差 ≤1mm ⇒ 三点都落在**上表面**上,不是孔底 ⇒ + # 上表面取 k1/k2 的一致值 **0.022**(原 0.012 是加底座前的旧值)。 + # 悬停随之 0.309 → 0.320(同样 +0.011),让"相机到工件面"的**视距保持 + # ≈0.17m 不变** —— 0.309 那一档是实拍验证过"16/16 检出、conf 0.87、 + # k1-k2 间距 156px 稳定"的工作点,改高度就是在改视距,不能随手抬。 + # 几何兜底仍然满足:0.320 − 工具长 0.245 = 尖端 0.075,工件面 0.022 + # ⇒ 尖端在面上方 53mm(要求 ≥ final_gap 2mm)。 + hole_hover_z_m: 0.320 # ← 二阶段悬停高度调这一行 + # FT 数据源:默认按 sn 推导 = //ft_sensor_wrench,由真机 + # flexiv_robot_states_broadcaster 发布。但 **2026-10-08 实测该话题恒为 + # 全零(0,0,0)** —— RDK 那侧的原始 FT 传感没在出数(`ft_sensor_raw` 为 0), + # 于是应用里"abs(fz−基线)>2.5N 判接触"的兜底**永远判不到接触**(静默失效)。 + # 同一时刻 //external_wrench_in_tcp 是有数的(≈负载 54N,且随受力变化), + # 所以真机改用它。判接触只看"相对轻降起始基线的增量",与 54N 这个偏置无关, + # 用这个源可直接恢复接触兜底,不需要改代码。 + # 仿真**别跟着用**(仿真没有这个话题)—— 见本文件 sim 段的显式覆盖。 + # 注意:这不是话题名写错,而是"哪个数据源有数"的取舍(真机 RDK 未启用原始 FT)。 + ft_wrench_topic: "/Rizon4_063989/external_wrench_in_tcp" + # ---- 工件几何(2026-09-29 现场实测标定)---- + # 真机段的坐标都是**基座系**;本机基座与台面同高 ⇒ 台面 z ≈ 0。 + gauge_assembly: + # 工件**上表面**的世界 z [m]。悬停高度的几何判据(gauge_fsm.hover_z_guard) + # 与 S07 下探预算都拿它当基准面。 + # + # ⚠️ 为什么不能留全局段的默认 0.0:0.0 = "未知",运行期退回"用 stage2 实测 + # 孔位 z 兜底" —— 但兜底发生在**悬停之后**,悬停本身还是按 0.0 算的。 + # 后果(2026-09-29 实测): + # 悬停 0.247 − 工具长 0.245 = 工具尖端 z≈0.002,而工件面在 0.0074 + # ⇒ 尖端在工件面**下** 5~10mm。悬停这一步就把通止规插进工件,接着 + # S05 的横向对准等于拿规尖**拨工件**:两孔中点两拍之间被推走 19.1mm, + # 残差 27.3 → 42.7mm 反向发散(日志看着像手眼标定坏了),最后被 + # drag_verdict 看门狗拦下(exit=6)。 + # + # 实测依据(旧,加底座前):stage2 解出的两孔基座系 z = 0.00743 / 0.00715 m + # (/tmp/stage2_result.json 的 k1_base_m / k2_base_m)。孔是通孔, + # 深度相机在孔口附近读数会偏深 ⇒ 上表面取工件板厚 **0.012**。 + # + # 实测依据(2026-09-29 加底座后**重新感知**,现行值 = 0.022): + # 不再靠"板厚"推,而是直接量上表面 —— 让 gongjian0929 在实时图上出 + # k0/k1/k2,对每个点取 (±6px) 邻域深度的中位数,用同一帧 TF + # (base←flange · flange←cam) 投到基座系: + # k1 = +0.0220 / +0.0221 / +0.0223,k2 = +0.0223 / +0.0224(重复性 0.4mm) + # k0 = +0.0165 / +0.0186(偏低 6mm 且抖 ⇒ 该点邻域读数不可靠,不参与定面) + # 判"是不是孔底":各点中心 (±6px) 与外环 (12~20px) 深度差 ≤1mm ⇒ 齐平 + # ⇒ 三点都在上表面。ROI 高度直方图同为单峰平台,p75≈0.0228、p95≈0.0247 + # ⇒ 上表面 0.022(比加底座前的 0.012 高 10mm)✓ 与"工件加了底座变高"一致。 + # 法兰 Z=0.356 时该点光学视距实测 0.204m(模型仍检出 conf 0.862)⇒ + # 模型对 0.17~0.20m 有一档余量,但两侧都不要顶到边界。 + # 复核(会同时打印实测工件面 + 悬停几何自检): + # bash ~/ros2/scripts/run_stage2_locate.sh --arm 89 --hover-only + # python3 scripts/lib/real_geometry_check.py --sn Rizon4-063989 + workpiece_surface_z_m: 0.022 + # 工具长(规尖在法兰下方的距离)—— **2026-10-08 真机感知标定**:0.223m。 + # 方法:hover(法兰 0.320) 下腕部相机抓一帧,取规杆最深像素 + 深度,用 + # 手眼 T_base = T_base_flange·T_flange_cam 投到基座 ⇒ 规尖 z=0.0971, + # 故 0.320 − 0.0971 = 0.2229m。同帧反投影链的独立校验:k1/k2 基座 z + # 精确复现上次实测 0.0219/0.0204(误差 <0.1mm),链路可信。 + # 工具长 = tool1 夹爪 TCP 0.147 + 规露出 0.076。 + # ⚠️ 旧值 0.245 是照抄仿真的(TCP 0.200 + 规 0.043),比真机长 22mm: + # 它让几何误以为悬停时尖端只在面上方 53mm(实际 75mm),轻降预算因此 + # 少 22mm,**永远到不了孔口**(现象就是"机械臂不下臂")。 + # 落在本文件下方注释记录的合理区间 0.204~0.233 内。 + # 复核:python3 scripts/sense_tool_length.py --sn Rizon4-063989 + # python3 scripts/lib/real_geometry_check.py --sn Rizon4-063989 + # 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 / 相机话题的 + #(`sim_env.sh` 注释里说的"仿真与真机共用同一套 site.yaml")。仿真的 TF 与关节 + # 名带 `sim_` 前缀(与真机区分,避免仿真数据冒充真机),所以它需要自己的一台。 + # 登记之后,**所有阶段(stage2/stage3/装配…)都不改一行代码**就能跑仿真, + # 只要 `--arm sim`(`scripts/run_gauge_assembly.sh --sim` 会自动选它)。 + # + # 真机上永远不会选到它:只有显式 `--arm sim` 才会,而没有任何真机脚本会这么写。 + - sn: "sim" + arm: sim + label: "仿真台" + # 仿真建模的就是左臂,触觉也只有左指(由 sim_tactile_adapter 发) + tactile: true + camera: + # 话题 /sim_d405/... :与真机的 /d405_left 分开。为什么不用同一个名字: + # 同域里两个来源会互相覆盖,"台上这台到底是谁"就说不清了。 + # 名字里的 d405 与 URDF 里的模型名一致 → 光学系 sim_d405_color_optical_frame + # 正好等于 `{camera.name}_color_optical_frame`,所以应用查 TF 不用特判。 + name: sim_d405 + # 仿真不需要手眼标定文件:腕部相机在 URDF 里就装在法兰上, + # 而真机那份标定文件描述的是"相机相对法兰的实际安装偏差", + # 套到仿真上会把相机搬到一个**错误**的位置(投影全错)。 + # 留空 → 启动脚本不发静态 TF(见 gauge_assembly.launch.py 的提示)。 + paths: + # 仿真不"标定",而是把 URDF 里的真值**导出**成同格式的文件: + # scripts/gen_sim_handeye.py(run_gauge_assembly.sh --sim 会自动跑)。 + # 为什么必须给文件而不能留空:visual_servo 的 servo_node 需要 + # T_flange_cam 矩阵做像素→笛卡尔换算;留空它就直接拒绝启动 + #("手眼标定结果缺失"),S04 跟踪起不来。 + handeye_file: "/home/cc/ros2/handeye/results/sim_eye_in_hand.yaml" + # 运动参数:**大部分**与 89 共用(锚点),但**高度类**必须按仿真台的几何改写。 + # + # 为什么不能直接共用:真机是"机械臂装在台面同一高度",所以"法兰相对基座" + # 就等于"法兰相对台面"(hole_hover_z_m=0.23 就是这么来的)。 + # 仿真里机械臂站在**地面**、台面 0.40m 高、工件上表面 0.412m —— 一切 Z + # 都比真机大 0.412。照抄真实值会让伺服把法兰送到 0.28m,也就是**撞进台面**。 + # + # 推导(与 scene.yaml 的 operating 段同源,scripts/check_sim_scene.py 会核对): + # 相机到工件 0.17(像素门槛反推,不能改) + # + 相机比法兰低 0.05(URDF 里 d405_z=0.05) + # + 工件上表面 0.412 + # ⇒ 二阶段悬停法兰高 = 0.632 + # 一阶段跟踪高 = 悬停 + 0.05(通止规离工件)= 0.682 + # 所以工作盒上界必须 ≥ 0.682 —— 真机那套上界 0.65 不够,S04 一跟踪就会被 + # "超出工作盒"截断(现象是"越跟踪越偏",很难想到是配置差 3cm)。 + # + # ⚠️ 上面那套"按相机"的推导**只对了一半**(2026-09-24 修正):它没算**工具**。 + # 悬停高度还要满足"工具尖端不碰工件": + # 工件上表面 0.412 + 工具长 0.243(推算的标称值:TCP 0.200 + 规尖 0.043) + # + 间隙 0.002 = **0.657** + # 原来的 0.632 会让工具尖端落在工件面**下**(按 0.245 算是 −23mm)—— 悬停 + # 这一步就把通止规捅进工件里,接着 S05 的横向对准等于拿规尖拨工件(实测把 + # 工件推走 79mm),而真值孔位按工件**实测**位姿算 ⇒ 孔位跟着工具走、残差 + # 恒定 11mm 永不收敛(日志看起来像"手眼标定坏了")。真机同参数同结果:只要 + # 悬停按相机定、而工具比相机长,就会这样。 + # 现在三道防线:stage2 启动时按 gauge_fsm.hover_z_guard 用几何兜住;S05 有 + # "工件被推走"的看门狗(gauge_fsm.drag_verdict);S07 按几何自查下探预算。 + # + # ⚠️ 为什么这里能写 0.657 这种"贴着"的值:悬停高每抬高 1mm,S07 就多下 + # 1mm,而 contact_max_depth_mm=35 是按"悬停只留 2mm 间隙"配的 + # (30 台肩 + 5 余量)。抬到 0.67 会让下探变成 43mm,S07 就得**自动改大** + # 预算并告警 —— 能跑,但配置就失去意义了。 + # + # 工具长 0.243 只是**标称**:仿真里上料脚本每次实测并落盘 + # (devices/sim/gazebo/config/sim_tool_geometry.yaml), + # run_gauge_assembly.sh --sim 用它当启动参数覆盖这里的值。真机由现场把 + # 夹好规后读到的 TF 值写进全局段的 tool_tip_below_flange_m。 + servo: + <<: *servo_left + # 仿真 FT:由 sim_wrist_ft 发布,与真机 //ft_sensor_wrench 同名同类型同单位。 + # 必须显式覆盖 —— 上面 *servo_left 带下来的 ft_wrench_topic 是**真机**的 + # external_wrench_in_tcp,仿真里没有这个话题(不覆盖 ⇒ 仿真 FT 收不到)。 + ft_wrench_topic: "/sim/ft_sensor_wrench" + ee_z_m: 0.72 # 一阶段跟踪锁高(法兰,世界系) + # 不必随悬停一起抬:跟踪时工具尖端离面 ~60mm, + # 只要**高于悬停**就不碰工件(0.72 > 0.657)。 + hole_hover_z_m: 0.657 # 二阶段悬停高:**由工具几何定**,不是相机视距 + # = 0.412(工件上表面)+ 0.243(工具)+ 0.002(间隙) + safe_z_m: 0.76 # 安全抬升/复位高度(高于跟踪高) + z_ref_m: 0.657 # 没有测量时的兜底 Z:绝不能是 0.22(会撞工件) + z_min_m: 0.55 # 伺服硬下限:0.55 = 工件上表面 + 0.14 + min_ee_z_m: 0.55 + workspace_xyz_min: [0.08, -0.55, 0.55] + workspace_xyz_max: [0.85, 0.55, 0.76] + # 仿真不跑推理:感知结果由 sim_truth_perception 按已知位姿直接给出。 + # 原因:ros_gz_sim 环境里没有 torch/ultralytics,且本机离线装不上 —— + # perception_node 一启动就死,表现为"一阶段永远不锁定"。 + # 详见 devices/sim/gazebo/README.md「仿真里哪些模块用真值替代」。 + perception: + source: "truth" + # 几何类参数按台子改写(gauge_assembly 段的下沉覆盖)。 + # 为什么需要:全局 gauge_assembly 里的值是**真机**的 —— 真机上机械臂 + # 基座与台面同高,所以"工件上表面 z"就是板厚 0.012;仿真里机械臂站在 + # **地面**、台面 0.40m 高、工件上表面 0.412。写死全局值会让 S07 的 + # "悬停时工具尖端离面多高"自检算错(差 0.4m),现象是"一路下压判不到 + # 接触",看起来像力阈值标歪了 —— 于是排查从阈值开始走偏。 + gauge_assembly: + workpiece_surface_z_m: 0.412 + # 工具长这里写**推算的标称值**(TCP 在法兰下方 0.200 + 规尖在指腹面下方 + # 0.043 = 0.243)。真值由上料脚本实测并作为启动参数覆盖:仿真里合爪后规会 + # 在指间下滑到落座位置,下滑量随装夹变化(实测见过 243 / 254mm)。 + # 写成固定值会让悬停高度算错(写小 → 规尖插进工件 ⇒ S05 拿规尖拨工件、 + # 残差恒不收敛),所以**这个值在仿真里只是兜底**,实际用实测值。 + tool_tip_below_flange_m: 0.243 + - sn: "Rizon4-063991" + arm: right + ip: "192.168.2.100" + pc_ip: "192.168.2.10" + iface: "eth0" + label: "右臂91" + camera: + name: d405_right # 话题 /d405_right/... 禁止和 89 的 /d405_left 混用 + serial: "260522274442" # 右腕新 D405 + auto_exposure: true + ae_limit_us: 0 # 0=不封顶,让固件自己调亮 + emitter_enabled: false + laser_power: 0 + gripper: + model: "RGI-100-30-O-S-M1" # 旋转爪:run_gripper.sh --arm 91 + sn: "" + port: "/dev/ttyUSB1" + stroke_mm: 30.0 + auto_init: false + named_poses_file: "/home/cc/ros2/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" + # >>> 右臂跟踪高度:只改这里,89 仍用上面那段 <<< + # 法兰 Z;RGI 比平行爪高,通常要比 89 略抬一点。改完重启: + # bash ~/ros2/scripts/run_visual_servo_rt.sh --arm 91 --control + servo: + ee_z_m: 0.25 # 跟踪锁高:法兰相对基座 Z [m] ← 日常调这一行 + min_ee_z_m: 0.20 + safe_z_m: 0.45 + z_min_m: 0.22 + xy_track: hover # 用法兰系手眼 T:夹爪中心(法兰原点)压在工件正上方 + ee_vertical_down: true # 法兰/RGI 轴线朝下;相机有 29° 倾角,绿框不必对准十字 + hover_offset_ee_xy_m: [0.0, 0.0] # RGI 与法兰同轴;爪心偏了再改这两项 [m] + # 二阶段孔定位悬停锁高(91 装了传感器后同样可用) + hole_hover_z_m: 0.22 + # FT 数据源同 89:真机 ft_sensor_wrench 恒零,改用有数的 external_wrench_in_tcp + # (见 89 段 ft_wrench_topic 的完整说明)。 + ft_wrench_topic: "/Rizon4_063991/external_wrench_in_tcp" + # --control:先只降到 ee_z_m(XY 不动),到位后才 hover,用手眼把爪心压到工件正上方 + +paths: + # >>> YOLO 权重(标准放法,不必改代码)<<< + # 1) 把 .onnx / .pt 放到 /home/cc/ros2/models/yolo/ + # 2) 这一行改成那个绝对路径(写 .onnx 即可;TensorRT 会找同名 .engine) + # 3) backend=tensorrt 时必须先导出引擎(只需做一次): + # bash ~/ros2/scripts/weights.sh convert <源文件> --name <名字> --group yolo + # 4) yolo.imgsz 必须等于 ONNX 输入边长(当前 640) + # 5) 改权重/imgsz 后重启:bash ~/ros2/scripts/run_visual_servo_rt.sh [--control] + # 6) 换完确认标签没丢:bash ~/ros2/scripts/weights.sh doctor --check-engines + # 当前:gongjian0929(2026-09-29 从 ~/下载 转入)。 + # ⚠️ 这是个 **pose 模型**(YOLO26s-pose):一个工件框 + 3 个关键点 + # k0=跟踪孔 / k1,k2=对孔。它同时替代了原来的跟踪模型和入孔模型, + # 所以下面 hole_weights 也指向同一份。 + # 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" + # 换模型:把 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" + # 下面是缺省(未匹配到 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" + # 空=默认 DDS(SHM);勿强制 UDP-only,否则相机预览收不到 /workpiece/detection + fastdds_profile: "" + +handeye: + mode: "eye_in_hand" + # >>> 标定板尺寸:**按模式各一份**(内角点数 + 方格边长 m)<<< + # + # 为什么必须按模式分开写(而不是两个模式共用一组键): + # 眼在手上与眼在手外通常用**两块不同的物理板**,方格边长不一样。共用一组键 + # 的后果不是报错,而是"跑另一种模式时静默用错方格边长"—— 标定照样跑完、 + # 结果只是系统性偏,现场几乎联想不到板尺寸(历史反例见 handeye/README.md 第 3 节)。 + # 内角点 = 方格数 − 1;列/行顺序不用纠结,检测端两种顺序都会试 + #(见 handeye_lib.detect_chessboard)。 + boards: + eye_in_hand: # A5 板:12×9 方格 → 内角点 11×8 + cols: 11 # 内角点列数 + rows: 8 # 内角点行数 + square_size_m: 0.015 # 方格边长 [m] + eye_to_hand: # A4 板:12×9 方格 + cols: 11 + rows: 8 + square_size_m: 0.020 + # 运动更顺滑:慢速 + 到位等静止 + 零速 pin 后再拍照 + velocity_scale: 0.05 + move_time_sec: 8.0 + settle_sec: 1.5 + settle_qd_max: 0.015 # |qd|_max 低于此值才算静 + settle_stable_sec: 0.6 + settle_timeout_sec: 8.0 + pin_holds: 2 + pin_hold_sec: 0.45 + skip_unstable_poses: true # 某点看不到棋盘则跳过,不整段中止 + # 点位:paths.handeye_waypoints + +camera: + name: "d405" + serial: "352122274408" # 全局缺省=左腕;真机以 robots[].camera.serial 为准 + # USB3 推荐 848x480@30;USB2 自动降到 640x480@15,深度/右目仍开 + width: 848 + height: 480 # 改宽高/帧率必须重启相机:bash ~/ros2/scripts/run_camera_d405.sh --preview + fps: 30 + enable_depth: true + enable_infra: true # 同一 SDK 进程开左右红外;失败则自动退回彩图+深度 + preview: true + preview_stereo: true # 预览同时显示 彩图 | 深度 | 左目 | 右目(不另开相机、不抢 USB) + overlay_hold_sec: 0.10 # 预览只留很短;长残留会看起来像绿框不跟 + use_site_resolution: true + auto_exposure: true + ae_limit_us: 4000 # D405 腕部近距;91 在 robots[] 里更严 + emitter_enabled: false # 腕部关红外投射,否则彩图发白 + laser_power: 0 + +# 现场三台 RealSense(按序列号钉死,禁止按「第一台 D405」乱开) +cameras: + d405_left: + model: d405 + name: d405_left + serial: "352122274408" + role: left_wrist + arm: left + label: 左腕D405(89 平行夹爪) + width: 848 # 相机分辨率以本段为准(全局 camera: 段为旧路径,P2 收敛) + height: 480 + emitter_enabled: false # 腕部必须关投射器,红外点阵会污染跟踪图像 + d405_right: + model: d405 + name: d405_right + serial: "260522274442" + role: right_wrist + arm: right + label: 右腕D405(91 旋转夹爪) + width: 848 + height: 480 + emitter_enabled: false + # >>> 仿真那两台(arm: sim)<<< + # 为什么仿真也要在这里登记:应用是"按当前臂"去认相机的 —— 腕部相机取 + # robots[].camera.name,手外相机取这里 role=overhead* 且 arm 与当前臂一致的 + # 那一条(见 gauge_assembly_node._d435_ns)。名字必须与 + # devices/sim/gazebo/config/cameras.yaml 的 ros_ns 一模一样。 + # 名字不带 d405_left/d435 那样的真机名:同域里两个来源会互相覆盖, + # 而 eye-to-hand 标定文件是按相机名命名的 —— 重名会让"跑一次仿真把真机标定盖掉"。 + sim_d405: + model: d405 + name: sim_d405 + role: sim_wrist + arm: sim + label: 仿真腕部D405(= 左臂位置) + width: 848 + height: 480 + emitter_enabled: false + sim_d435: + model: d435 + name: sim_d435 + role: overhead_eye_to_hand + arm: sim + label: 仿真手外D435(世界固定) + width: 640 + height: 480 + fps: 30 + emitter_enabled: true + + d435: + model: d435 + serial: "262522073161" + role: overhead_eye_to_hand + label: 双臂中间手外D435 + name: d435 + width: 1280 + height: 720 + fps: 30 + auto_exposure: true + ae_limit_us: 8000 + emitter_enabled: true # 顶视深度可用投射器;只看彩图可改 false + + +yolo: + conf: 0.10 # 检测器门槛:略低于 min_conf,让运动模糊框也能进跟踪 + iou: 0.45 + imgsz: 640 # 必须等于所用 ONNX 的输入边长(当前 detect_best_0 = 640) + device: "0" + half_precision: true + backend: "tensorrt" # 强制 .engine;没有则启动失败。导出:bash ~/ros2/scripts/export_yolo_trt.sh + mp_infer: true # YOLO 独立进程,不和 ROS 抢 GIL + min_conf: 0.12 # 新锁门槛;已锁定的框可略低于此 + # 重新捕获门槛:**跳到新位置**(离上一帧框中心 >max_track_px)必须达到的置信度。 + # 为什么必须比 min_conf 高得多 —— 2026-10-08 真机实测(一阶段 servo_node 日志): + # 0659.551 死区保持 |e_xy|=3.0mm ← 已经收敛到 3mm + # 0659.598 k0 置信度 0.00 < 0.30,本帧不用它 + # 0659.6xx 重新捕获 conf=0.15 dpx=503 ← 503px 外、conf 0.15 的假框 + # feat_base=(0.231,-0.484) z=-0.021 ← 跟踪点瞬间横移 427mm + # 0670.985 === 一阶段已锁定 === ← "锁定"在离工件 500mm 的位置 + # 那串假框实测 conf 0.13~0.47,而真 k0 是 0.87。旧写法把这里接成 min_conf=0.12 + # ⇒ 0.15 的假框被原样采信、perception 的 kf.reset() 直接跳过去,伺服再拦也只是 + # 拦一帧(假框帧间稳定,3 帧后 FeatureJumpGate 也会认)。 + # 取 0.55:高于假框上沿 0.47,低于真 k0 0.87。宁可原地不动,也不追假点。 + reacquire_conf: 0.55 + vision_hz: 30.0 + infer_max_width: 640 # 进 GPU 进程前缩到 640,减小进程间拷图,避免队列堵 + box_ema_alpha: 1.00 # 绿框用预测补偿后的框,不再 EMA + miss_clear_frames: 10 # ~0.33 s @30 Hz;期间绿框按像素速度滑行 + overlay_hold_on_miss: true # 滑行失败时仍续发上一框 + hold_point_on_miss: false # 眼在手上:丢检续发旧点会让臂追错位置 + miss_hold_point_frames: 1 + jump_px: 400.0 + jump_z_m: 0.10 + z_ref_m: 0.22 + min_depth_m: 0.04 + max_depth_m: 0.55 + # 特征点的**相机量程下限**:比这更近的深度读数一定来自机械臂自身(爪上夹的 + # 通止规 / 相机座),不是工件。**判据是实测的两条带,不是几何推导**: + # 机械臂自身(随臂一起动):0.078 ~ 0.087 m ← 2026-10-08 被误锁的假目标 + # 工件面: 0.19 ~ 0.29 m ← 锁高下正常跟踪时的 p_cam.z + # 取 0.14:距近带 53mm、距远带 50mm,两边都不顶边界。 + # 几何上为什么近带被占满:规尖在法兰下 0.223m 而相机只在法兰下 0.124m(handeye + # cam_in_flange z=0.1236)⇒ 规杆整段都在相机近场;且相机光轴与法兰 Z 差 29.7°, + # 所以"光轴视距"不等于"竖直距离",只能靠实测划带。 + # 撞了会怎样:假目标在相机前 0.08m 且**随臂一起动**,伺服误差恒为 0 —— + # "一阶段已锁定"会锁在自己的工具上(见上面 reacquire_conf 的记录)。 + feature_min_range_m: 0.14 + work_z_max_m: 0.50 + meas_blend: 0.85 # 3D 跟手;抖则略降 + z_alpha_min: 0.10 + z_alpha_max: 0.40 + z_alpha_gain: 1.0 + camera_frame: "d405_color_optical_frame" + object_frame: "tracked_object" + depth_topic: "/d405/aligned_depth_to_color/image_raw" + feature_mode: "keypoint" # keypoint=pose 模型的 k0(跟踪孔);plane=框内近表面立体点;box=单目射线 + # feature_mode=keypoint 时用:跟踪用哪个关键点、它的置信度低于多少就不信它。 + # k0 = 跟踪孔(见 pose_detector 的约定)。k0 不可用时**借 k0 相对检测框的归一化 + # 偏移**在框上重建 k0 —— 位置连续、工件动时还跟着走。 + # 刻意**不**回落"框中心"(那是另一个点,离 k0 有 ~89mm,真机上就是一次不该有的 + # 横向快移,见 2026-10-08 一阶段 150s 没锁上的记录);只有在**从未**见过可信 k0 + # 的冷启动帧才回落到框中心/框内深度。 + keypoint_index: 0 + keypoint_min_conf: 0.30 + keypoint_win_px: 6 # k0 处取多大窗口的中位深度 + # ---- 跟踪点可信性的三道守卫(实现:feature_guard.py,单一数据源)---- + # 为什么需要:2026-10-08 真机 arm 89,一阶段真 k0(conf 0.87 uv=(410,246))被一串 + # **高置信度假 k0** 顶掉,伺服 1s 内把臂从 home 带到工作盒角落 (0.083,-0.515), + # 之后 148s 全是"检测漏检"、150s 超时。假 k0 的 conf 是 0.53~0.73(真值 0.87) + # ⇒ 置信度没有可分区间;但它们**贴在画面边上**、且**借深度**: + # 766.96 uv=(76,75) 768.16 uv=(143,92) 768.78 uv=(12,30) 770.07 uv=(431,226) + keypoint_border_px: 6 # k0 离画面边缘 < 此值即判位置不可信(孔心不在画面边上) + track_max_step_m: 0.04 # 已发布跟踪点的每帧位移上限 [m](实测真实运动 ≤15mm/帧) + reacquire_far_px: 120 # 远于此像素距离的"重新捕获"要连续确认(dpx 那个门限) + reacquire_confirm_frames: 3 # 远框要连续几帧指向同一处才采信 + plane_inlier_m: 0.008 # 拟合平面内点阈值 [m] + plane_band_m: 0.06 # 近表面深度带宽 [m] + +servo: + control_hz: 100.0 + # --- 基座坐标系下的末端约束(法兰,单位 m)--- + ee_x_m: 0.25 + ee_y_m: 0.00 + ee_z_m: 0.35 # 全局缺省锁高;真机以 robots[].servo.ee_z_m 为准(89/91 分开调) + ee_hold_xy: false # false=跟踪工件 XY;true=锁死 ee_x/ee_y + xy_track: "hover" # hover=法兰悬停在目标正上方(任意锁高) lookat=画面中心对准目标 + use_static_handeye_tf: false # 只用 handeye_result.yaml,避免 TF 树抢父子帧 + ee_vertical_down: true + # --- 启动回初始位(已停用:--control 不再调用 goto_ee_plane)--- + safe_z_m: 0.45 # >>> 防撞安全高度:先纯 Z 抬到这里再回 home,避免扫到料框/壁 + home_pose_name: "home" # 非夕初始点位(/set_named_pose:home | ready | inspect | pos_left) + home_pose_speed: 0.30 # 回初始位的速度比例 (0,1],越小越稳 + ee_hold_kp: 2.0 + ee_w_max: 0.60 + z_ref_m: 0.22 + z_min_m: 0.22 + approach_xy_gate_m: 0.040 # --control 先降到锁高,|ee_z-ee_z_m| 小于此值才开始 XY hover + v_approach_z: 0.025 + watchdog_timeout_sec: 1.2 + lost_timeout_sec: 1.2 + stale_hold_sec: 0.35 # 丢检空窗内用基座 KF 预测继续跟(自运动安全),超过才钉住 + max_extrapolate_sec: 0.35 # 预测时间上限(图像延迟 + 前瞻) + predict_enable: true # 基座系恒速预测,补视觉延迟 + predict_sec: 0.16 # 额外前瞻 [s] + predict_v_max: 0.80 # 工件速度上限 [m/s],防乱飞 + predict_jump_m: 0.08 # 基座 XY 跳变大于此则视为"疑似假跳",转入确认流程 + # 大跳变要连续多少帧指向同一新位置才采信(见 feature_jump_guard.py 的注释)。 + # 30Hz 下 3 帧 ≈ 0.1s:真的重定位 0.1s 就被认;假点(乱跳)永远认不了。 + # 为什么不能是 1:2026-10-08 真机,一阶段已收敛到 3mm 后 k0 掉信、跟踪点瞬间 + # 横移 89mm,旧逻辑单帧就 reset 采信 → 伺服以 0.45m/s 去追假点 → 彻底丢失。 + jump_confirm_frames: 3 + # 特征点位置合理性:必须落在工作盒 XY 内(±此余量 [m]),否则判误检、当"没有目标"。 + # 为什么需要它:2026-10-08 真机,模型在运动模糊下把特征点丢到基座后方 + # (-0.10,-0.65)(工件其实在 (0.5,-0.12)),伺服一路追到工作盒边缘卡住。 + feat_ws_margin_m: 0.05 + kp_xy: 4.20 # 很跟手:v = kp × 误差,再经加速度整形;抖则降回 3.5 + ki_xy: 0.00 + kd_xy: 0.20 + kp_z: 0.80 + ki_z: 0.00 + kd_z: 0.35 + i_sep_pos_m: 0.008 + deadzone_m: 0.003 # 3 mm;收紧提高对准精度。若抖动调回 0.006 + deadzone_z_m: 0.012 + lock_hold_sec: 0.7 # 旧死区驻留(混合模式不再用它进二阶段) + # D435 到位之后,法兰 XY 连续这么久位移不超过 lock_still_move_m 才算停稳、交回 D405。 + # 顶视一次性把臂迁过去的那段路不算:必须是腕部有效目标。大幅一动就重计时。 + lock_still_sec: 3.0 + # D405 改追工件框中心之后,连续这么久才进对孔。不要和上面的停稳写成同一个数。 + wrist_align_sec: 1.0 + lock_still_move_m: 0.015 # 15 mm;超过就重计时,避免"晃一下刚好满 3 秒" + e_xy_clip_m: 0.200 # 大误差仍按 v_max 走,靠 a_max 加速 + v_max_xy: 0.45 # 法兰 XY 上限 450 mm/s + v_max_z: 0.04 + # 远处限速:目标离得比 slow_far_m 远时 XY 限速到 v_slow_xy(**与"目标真不真"无关** + # 的一道防线)。为什么需要:2026-10-08 真机一阶段被一串高置信度假 k0(conf 0.53~0.73) + # 顶掉,伺服 0.7s 内以 0.45m/s 把臂从 home (0.686,-0.113) 带到工作盒角落 + # (0.083,-0.515),之后 148s 全是"检测漏检"、150s 超时。加了这道之后:真 k0(离 + # 145mm)只是晚 ~1.8s 到位,假目标最多把臂带走几厘米。 + slow_far_m: 0.05 # 目标离得超过此距离即限速 [m] + v_slow_xy: 0.08 # 限速值 [m/s](真目标到位后仍回到 v_max_xy) + min_ee_z_m: 0.20 + workspace_xyz_min: [0.08, -0.55, 0.18] # 法兰工作盒;盒外只允许往回走,不再卡死 no traj + workspace_xyz_max: [0.85, 0.55, 0.65] + a_max: 3.00 # 加速度上限(真正作用在 hover 指令上) + vel_lpf_alpha: 0.80 # 速度低通:越大越跟手(1=不滤波) + dq_window_size: 1 # 1=不做滑窗平均,去掉 ~20ms 滞后 + traj_stream_hz: 50.0 # 轨迹点流率:JTC open_loop+样条,吃得起 + exec_horizon_sec: 0.08 # 每个轨迹点的执行窗口 + max_qdot: 2.00 # 关节速度上限 [rad/s] + min_cmd_delta_rad: 0.001 + point_topic: "visual_servo/filtered_point" + traj_topic: "rizon_arm_controller/joint_trajectory" + +# 大寰夹爪:官方 Modbus RTU,USB–RS485 插 IPC,不走 Studio/RDK。 +# 左 89 PGEA → /dev/ttyUSB0;右 91 RGI → /dev/ttyUSB1。 +# bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli open|close|pos +# bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli open|close|rotate 90 +# bash ~/ros2/scripts/run_dual_gripper.sh +# ROS:/gripper/open|close(PGEA 单爪)或 /dual_gripper_driver/{left,right}/execute +gripper: + backend: "usb" # usb=工控机官方Modbus rdk=Studio设备(不用) + vendor: "dh" + model: "PGEA-100-20-W" + sn: "26610799" # 铭牌,不进协议 + device_name: "GripperDahuanModbus" + port: "/dev/ttyUSB0" # 本机 MACROSILICON/pl2303 + baud: 115200 # 官方默认 115200 8N1 + slave_id: 1 # 官方默认从站 1 + rs485_rts: "" # 空=不拉RTS;scan 若 OK 在 tx/tx_inv 再填 + stroke_mm: 20.0 + # --- 开合 / 力 / 速(大寰官方寄存器,超出会被驱动钳位)--- + force_pct: 20 # 夹持力 区间 20–100 %(低于 20 会被钳到 20;PGEA-100 约 30–100 N) + speed_pct: 50 # 开合速度 区间 1–100 % + open_position: 1000 # 张开目标 区间 0–1000 ‰(1000=全开) + close_position: 0 # 闭合目标 区间 0–1000 ‰(0=全合;留缝改 50–200) + auto_init: true + state_hz: 20.0 + # 双爪共用参数(run_dual_gripper.sh / load_from_site) + parallel: + stroke_min: 0.0 + stroke_max: 20.0 + default_speed: 50 + default_force: 20 + force_min: 20 + force_max: 100 + auto_initialize: true + rotary: + auto_initialize: false + angle_min: -32767.0 + angle_max: 32767.0 + max_step_deg: 30000.0 + finger_open_per_mille: 1000 + finger_close_per_mille: 0 + default_speed: 50 + default_torque: 50 + default_finger_speed: 50 + default_finger_force: 50 + torque_min: 20 + torque_max: 100 + # RDK 回退字段(backend: rdk 才用) + force_n: 50.0 + vel_mps: 0.05 + +# 入孔检测。算法参数写在这里(覆盖包内 hole_detect.yaml 默认值)。 +# bash ~/ros2/scripts/run_hole_detect.sh --arm 89 --preview +hole_detect: + backend: "two_stage" # two_stage | yolo | hough + # backend=yolo 时用。 + weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + # 二阶段工件检测权重。pose 模型既框工件、又直接给 k1/k2 关键点, + # 所以和上面的跟踪权重是同一份。 + workpiece_weights: "/home/cc/ros2/models/yolo/gongjian0929.onnx" + # pose 关键点优先:检测器给了 k1/k2 就直接用,不用再找圆。 + # 关键点不可信(低于 keypoint_min_conf / 两点几乎重合)时自动回落到 + # 亮环 + Hough 那套老路径。 + keypoint_enable: true + 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" + conf: 0.25 + iou: 0.7 + imgsz: 640 + center_gate_enable: true + center_max_offset_ratio_x: 0.15 + center_max_offset_ratio_y: 0.18 + center_gate_draw: true + refine_hole_centers: true + bbox_padding: 80 + gaussian_ksize: 5 + gaussian_sigma: 1.5 + hough_dp: 1.2 + hough_min_dist: 50 + hough_param1: 100 + hough_param2: 26 + hough_min_radius: 6 # 悬停较高时螺纹孔半径只有几像素 + hough_max_radius: 30 + min_radius: 6 + max_radius: 30 + hole_distance_min: 90 # 悬停 0.23m 两螺纹孔约 170px;过小会配到中心孔 + hole_distance_max: 700 + expected_holes: 2 + min_hole_sep_px: 20 + max_hole_sep_px: 2000 + stabilize_hold_frames: 15 + stabilize_ema_alpha: 0.35 + stabilize_max_jump: 60 + default_depth_m: 0.10 + camera_intrinsics: + fx: 425.0 + fy: 425.0 + cx: 640.0 + cy: 360.0 + # ===== 二阶段一次性拍照定位(stage2_locate_node,不做实时推理)===== + # bash ~/ros2/scripts/run_stage2_locate.sh --arm 89 # 完整流程 + # bash ~/ros2/scripts/run_stage2_locate.sh --arm 89 --hover-only # 只悬停调高度 + stage2: + # 对准目标孔。pose 模型下 k1/k2 是**模型学出来的固定语义**(不是图像左右): + # hole_points[0] = 模型的 k1,hole_points[1] = 模型的 k2 + # 下游是 `k1_b = pts[0] if k1_label=="left" else pts[1]`,所以: + # k1_label: "left" → target_hole=k1 打的是**模型的 k1**(当前设置,推荐) + # k1_label: "right" → target_hole=k1 打的是模型的 k2(等价于老 Hough 的"图像左孔") + # 实测这台臂的视角下模型给的是 k1 在画面右、k2 在画面左;对孔顺序要反过来就改这里。 + target_hole: "k1" + k1_label: "left" + # ---- 对准判据:工具**用视觉量到的规尖**,不是法兰轴线(2026-10-08 真机定稿)---- + # 实测(同一帧、同一条手眼链,n=8): + # · 规杆轴线相对法兰轴线 = (+10.7, −0.1)mm(σ≈0.5mm);最深像素法 (+8.8,−1.8)mm + # · 而 site 原来把 hover_offset_ee_xy_m 写成 [0,0]("平行爪同轴"),于是精对准 + # 把**法兰**对到孔上,规尖实际离孔心 ~10mm ⇒ 轻降时 φ6 规尖骑在 φ7 孔沿上, + # 腕力从 −1N 直接蹦到 −31N→−63N 卡死(现象是"轻降在离面 4mm 停住")。 + # · 孔检测本身的重复性极好:k1 基座坐标 σ=(0.17,0.04)mm —— 精度不是瓶颈。 + # 所以:在同一帧里同时解「规尖基座 XY」和「孔基座 XY」,两者一起走手眼链, + # 链路误差与装夹变动一起抵消。规尖量取规则见 _tip_offset_from_depth(四条判据)。 + align_tol_m: 0.00025 # 对准到位判据 [m]。检测重复性 0.04mm 撑得住 0.25mm。 + # ⚠️ 不要回到 0.7mm:那会让"规偏 10mm"这种系统偏差 + # 在容差里被放过去(原来残差 0.69mm 看着"通过",其实 + # 是拿法兰对的孔)。收到 0.25mm 才逼它必须真的对上规尖。 + align_max_iters: 12 # 拍照→修正→复拍 迭代上限(收紧容差后需要更多拍) + align_tip_vision: true # true=精对准用「视觉规尖」;false=退回旧的「法兰+偏置」 + tip_near_m: 0.15 # 规尖量取:无工件面深度时的近端上限 [m]; + # 精对准同一帧有 z_wp 时改用「工件面 − 15mm」 + # (3mm 挡不住噪声,会把工件面当成规尖) + tip_min_m: 0.04 # 有效深度下限 [m] + tip_max_m: 0.60 # 有效深度上限 [m] + tip_search_band_frac: 0.25 # 中央竖带半宽 = 该比例 × 图宽(规夹在爪里,不会靠边) + tip_min_px: 150 # 近端连通域最小像素数(少了就是没看到规) + tip_calib_n: 4 # 定偏移需要的一致拍数(够了就冻结) + tip_offset_tol_m: 0.003 # 样本相对中位偏移的最大允许偏差 [m](剔偶发外点 + 冻结判据) + tip_len_min_m: 0.15 # 反投影出的「法兰z − 规尖z」必须落在此区间 —— + tip_len_max_m: 0.32 # 这条同时挡住"深度读到台面/工件"(主干守卫) + coarse_max_moves: 2 # 工件粗居中(bbox 中心)移动上限 + # —— 粗居中的两条误检守卫(单源:代码读这两个键,见 _coarse_target_reject)—— + # 为什么需要:2026-10-08 真机 arm 89,粗居中拿被画面左边缘切掉的半框 + # bbox=(0,142,97,222) 反投影出盒外的 (0.280,-0.747)(偏差 211.5mm),旧逻辑 + # **不判误检、只把目标截断到盒边**,臂照样横移 56mm 离开工件 ⇒ 精定位再也 + # 看不到 k1/k2 ⇒ exit=4。判据改成"盒外/偏移过大 ⇒ 判误检、原地不动"。 + coarse_max_offset_m: 0.12 # 目标离夹爪轴线超过此值就不像粗居中量 [m] + coarse_border_px: 2 # bbox 贴边(被画面切)即弃用 [px];见 _bbox_on_border + # —— 柔顺运动(夹爪持规,防急加速/急停/过冲把规震脱):速度放慢 + 平滑刹车 —— + align_speed: 0.014 # 对准段笛卡尔速度 [m/s](原0.03,放缓防冲) + move_speed: 0.022 # 升降/粗居中段速度 [m/s](原0.05,放缓防震规) + # 对小孔关键点:只用像素射线交到 workpiece_surface_z_m,不读深度图。 + hole_visual_only: true + final_gap_mm: 2.0 # 对准后贴面轻降目标:规尖悬在孔面上方这么高 [mm](不接触) + observe_gap_mm: 15.0 # --no-descent 时的观测下降:只降到离面这么高 [mm], + # 方便看规尖偏没偏。必须 ≥ final_gap_mm。 + # 左臂89:悬停间隙约 73.5mm(0.320−0.2245−0.022)⇒ 观测下降约 58mm。 + final_descent_speed: 0.005 # 最后轻降速度 [m/s](很慢,柔顺) + # ✅ 2026-10-08 定稿:下降量**只由几何决定**(法兰 FK − 工具长 − 工件面 − final_gap), + # 深度读数只做交叉核对、不再参与任何决策。两段真机事故方向相反、都指向这条: + # · 2026-09-29(多降→撞工件):环形深度中值报 gap0=141.0mm,几何真值只有 53mm + # (probe 0.245 版),多降 ~86mm 把通止规撞进孔,工件被 +X 推走 5mm。 + # ⇒ 读数往**大**错一次就撞。 + # · 2026-10-08(少降→到不了 2mm):同一个读数悬停时报 144.0mm(几何真值 75mm), + # 只降 24mm 后又翻成 −7.0mm,触发 SAFE_FLOOR 把轻降停在离面 ~50mm 处, + # 最终日志却打印"悬在孔面上方 ~2.0mm"(其实停在 ~18mm)。 + # ⇒ 读数往**小**错一次就永远到不了目标。 + # 同一套读数两个方向各错一次、幅度 150mm 量级、随位姿乱跳 —— 它对"能降多少" + # 和"该不该停"都没有发言权。现在它只在偏差 >10mm 时喊一句"深度测距与几何不符"。 + # 现行几何:悬停 0.320 − 工具长 0.2245 − 工件面 0.022 = 73.5mm 间隙 ⇒ 降 71.5mm 到 2mm。 + # 三道**物理/几何**保险:descent_budget_m(配置上限)+ 腕部 FT 接触检测(停+退 1mm) + # + 任一步 _goto 走不到位就停。 + # 缺几何(工具长/工件面没配)时**拒绝下降**并报错(保持悬停),绝不拿测距去赌。 + final_descent_enable: true # true=对准后降到 final_gap_mm;false / --no-descent=只降到 observe_gap_mm + verticalize_tol_deg: 0.8 # 垂直朝下判据 [deg] + settle_sec: 0.45 # 到位后静止多久再拍照 + snap_frames: 5 # 每次精拍取中值的帧数 + min_depth_m: 0.06 # 有效深度范围(防孔底/桌面拉偏 XY) + max_depth_m: 0.60 + linger_sec: 8.0 # 完成后继续发布结果/叠图的时长 + + # ===== 三阶段柔顺搜孔(stage3_search_node,Orisys 触觉 + 力闭环螺旋)===== + # 前置:二阶段已完成,臂悬停在目标孔上方。先 run_tactile.sh --cli 看力读数量级再调阈值。 + # bash ~/ros2/scripts/run_stage3_search.sh --arm 89 # 搜孔+落座 + # bash ~/ros2/scripts/run_stage3_search.sh --arm 89 --depth 0 # 只搜孔定心不插入 + # bash ~/ros2/scripts/run_stage3_search.sh --arm 89 --depth 6 --pitch 1.0 # 螺纹规旋入 + stage3: + # —— 力阈值全是「相对静止均值的增量」(Orisys 读数是形变任意单位且基线会漂移, + # 节点启动时自动测 1.2s 静止均值 μ0/方差 σ0,看日志「触觉基线/派生阈值」两行)—— + touch_delta: 250.0 # 触面判定:fnormal > μ0 + max(4σ0, 本值) + press_delta: 200.0 # 轻触目标按压量(触面阈值之上再加这么多) + max_press_delta: 900.0 # 螺旋中允许的最大按压(超过→力闭环抬起) + safe_press_delta: 2000.0 # 安全上限:fnormal > μ0+本值 立即抬升中止 + shear_delta: 600.0 # 切向拖拽保护:shear > 静止切向均值+本值 → 贴面时微抬 0.15mm(Z 钉在触面) + insert_press_delta: 1200.0 # 插入力上限(触面阈值之上;超过回退微抖重试) + kp_z_mm_per_unit: 0.001 # Z 力闭环增益:每单位读数偏差调整的毫米数 + approach_step_mm: 0.8 # 触面前每步下降量(驱动死区已修,0.5mm 微步可跟踪;小步=触面冲击小) + enter_probe_mm: 2.0 # 触面深于此值→疑似直接入孔,四向微探验证后跳过螺旋 + mouth_lift_mm: 2.0 # 直接入孔后抬到孔口倒角段再定心(深插横探会别死孔壁) + baseline_min: 8.0 # 触觉基线健康窗下限(<此值=几乎全 0,tare/断流) + baseline_max: 1500.0 # 上限(>此值=近饱和,接触余量不足)——出窗即中止并提示重启触觉 + approach_ft_touch_n: 6.0 # approach 腕部 |F| 旁路触面判据(标定牛顿;自由空气≈2-3N) + approach_max_mm: 60.0 # 从当前位姿最多下降这么多(找不到面就中止;悬停被抬高后仍有余量) + spiral_pitch_mm: 1.0 # 螺旋螺距(< 通止规直径,保证重叠覆盖) + spiral_step_mm: 0.5 # 沿螺旋的弧长步长 + spiral_r_max_mm: 4.0 # 最大搜索半径(覆盖二阶段残差 1.5mm + 余量) + spiral_expand_max: 1 # 找不到孔时扩大半径的次数(残差≤1.8mm 时代码不再扩大) + spiral_expand_step_mm: 2.0 + drop_ratio: 0.45 # 入孔判据:fnormal 相对接触均值跌落比例 + drop_frames: 2 # 跌落需持续的帧数(防毛刺) + center_scan: false # 入孔后十字微扫;默认关(0.8mm 扫会把已入孔的规刮偏) + center_scan_mm: 0.3 + insert_depth_mm: 0.0 # 默认只搜孔+轻压,不拧入;拧入阶段再用 --depth + insert_retry: 3 + screw_pitch_mm: 0.0 # 螺纹通止规螺距:>0 右旋 / <0 左旋 / 0=不旋(光面规) + search_speed: 0.03 # 搜孔笛卡尔速度 [m/s] + settle_sec: 0.25 # 每个微步后的静止时间 + tactile_timeout_s: 3.0 # 触觉话题断流多久中止 + ft_watchdog: true # 订阅 //flexiv_robot_states 做腕部力看门狗 + ft_safe_n: 35.0 # 腕部 |F| 安全上限(真牛顿,标定过的;连续3拍超限才中止) + linger_sec: 8.0 # 完成后继续发布结果的时长 + +# Orisys 视触觉(夹爪左右指摄像机,不是腕部 D405)。SDK 单独 pip 安装。 +# bash ~/ros2/scripts/run_tactile.sh --cli +# bash ~/ros2/scripts/run_tactile.sh +# ============================================================================= +# 感知来源:伺服用的"视觉测量"由谁产生 +# +# yolo 本体 perception_node(YOLO/ONNX,需要 torch/ultralytics)—— 真机 +# truth 仿真真值:**整条视觉链都由场景几何算出**,不跑任何推理 +# · 一阶段目标点:arm_sim_gazebo 的 sim_truth_perception +# · 二阶段孔位 :arm_sim_gazebo 的 truth_holes(投影 *_ring) +# external 由外部节点按**同一话题**(visual_servo/filtered_point)提供, +# 本框架不起感知进程(二阶段仍走检测器) +# +# ⚠️ 别和 yolo.backend 混了:那个是"**怎么**推理"(tensorrt/onnx), +# 这个是"**谁**给出测量"。 +# +# 为什么需要这个开关:仿真环境(ros_gz_sim conda)没有 torch/ultralytics, +# 离线也装不上,perception_node 一起就死;但仿真里工件位姿本来就是已知的, +# 没必要也没法做推理。切到 truth 后,流程能在仿真里验"控制与接口"那一半, +# 而**感知那一半留给真机**——这条边界写在 devices/sim/gazebo/README.md 的 +# 「仿真里哪些模块用真值替代」一节。 +# ============================================================================= +perception: + source: "yolo" + # 一阶段跟踪用哪台相机。wrist=腕部 D405(主跟踪)+ 顶视 D435 一次性兜底; + # overhead=整段只用 D435(实验分支才切)。非法值启动即报错,不会静默退回。 + track_camera: "wrist" + # 画面内最高检测 conf 低于此 → 马上拍一张 D435,把工件 / k0 定到基座系。 + # 与 perception / servo 共用这一处,不许两边各写一个 0.5。 + wrist_handoff_conf: 0.5 + # 顶视深度量程 + 一次性参数。腕部那组 0.04–0.55m 会把 D435 的点全丢掉。 + # 缺键启动即红,不许借腕部量程、不许用代码默认冷却时间。 + overhead: + min_depth_m: 0.25 + max_depth_m: 1.80 + feature_min_range_m: 0.30 + work_z_max_m: 1.50 + z_ref_m: 0.70 + feature_mode: "plane" # 顶视与腕部视角差太大,pose 的 k0 经常看不见;优先 k0,否则框心 + hold_point_on_miss: true # 仅 overhead 整段跟踪时用;混合模式一次性不续发 + oneshot_conf: 0.5 # D435 这一张也必须 conf≥此值,不许拿噪声框当 K0 + cooldown_s: 1.5 # 两次一次性之间的最短间隔,避免跟 D405 抢同一条 YOLO 队列 + frame_max_age_s: 0.4 # 彩图/深度比这更旧就拒绝,不许拿过期图反投影 + oneshot_hold_s: 8.0 # 伺服保持这一次设定的最长时间;超时丢掉,等下一张或 D405 + truth: + # 真值目标物体。留空 = 场景文件里的 operating.part_object(推荐留空: + # "哪个是工件"是场景的知识,换场景不该改这里)。 + object: "" + # 目标点相对物体**顶面中心**的偏移 [m],在物体局部系里(会跟着 yaw 转)。 + # 例:把目标从板中心挪到 k1 孔 → [0.0, -0.029, 0.0] + offset_m: [0.0, 0.0, 0.0] +# ============================================================================= +# 通止规装配(看板 S00–S11):bash ~/ros2/scripts/run_gauge_assembly.sh --arm 89 +# +# 这一段全是**会动的量**,也是现场最常改的地方。改完不必改代码: +# 解析在 arm_application/gauge_config.py(键名、单位、类型都在一张表里校验), +# 拼错键名会直接报错并给出最相近的候选名,不会静默退回默认值。 +# +# 单位体现在键名后缀上(_deg/_mm/_m/_s/_pct/_delta),三张表逐条校验; +# 带 _delta 的是**力增量**(Orisys 读数是形变任意单位且基线会漂移, +# 绝对值没有意义),必须现场标定: +# bash ~/ros2/scripts/run_tactile.sh --cli # 看读数量级 +# 启动时节点会打印本段**实际生效**的值(含派生结论),先核对再上电。 +# ============================================================================= +gauge_assembly: + # ---- 总开关 ---- + holes_total: 2 # 一次任务装几个孔(k1/k2);--holes N 可临时覆盖 + dry_run: false # true = 只验编排与自检,不发任何运动指令 + # E1(掉件)重放时等人工确认 /gauge/reload_ok 的最长时间 [s]。超时=中止。 + # 不是"再等等":臂停在 home,让人来处理比继续跑安全。 + reload_timeout_s: 180.0 + # S00 自检每项(相机/触觉/夹爪状态)等"第一帧真数据"的最长秒数。 + # 只看话题名会假阳性(发布者不在也算在),所以必须等真数据。 + boot_timeout_s: 30.0 + + # ---- S01 回 home 后按工具系逆时针预绕到极限角 ---- + # 预绕是为了给 S08 的棘轮留出回转行程:不预绕,拧两圈就贴关节限位。 + extreme_angle_deg: 150.0 # 想转多少度(按工具系逆时针) + extreme_speed_deg_s: 20.0 # 关节角速度 [deg/s],慢一点防冲击 + extreme_direction: 1 # +1=逆时针 / -1=顺时针(改符号,别去改代码) + extreme_margin_deg: 3.0 # 距关节限位留的余量 [deg];撞限位不算失败(按 JOINT_LIMIT 继续) + # 但余量太小会让规划器频繁拒收,表现为"偶尔转不到位" + + # ---- S02 夹持确认(预留,默认跳过)---- + # 现在的触觉判据与 D435 手眼都还没就绪,先留接口:开成 true 时, + # 夹爪状态字必须明确报"夹住了",否则 S02 判失败并中止。 + grip_check: false + grip_hold_s: 0.5 # 合爪后等多久再读状态 [s] + + # ---- S03 手外粗定位(S04 跟踪的兜底:跟踪丢了才来)---- + # 前提是手外相机做过 eye-to-hand 标定:handeye/results/<相机>_eye_to_hand.yaml。 + # 没有那个文件时 S03 会明确报"缺哪个文件、怎么生成"并回 S04,不做假装成功的反投影。 + coarse_enabled: true + # 手外相机名:**以 cameras 段里 role=overhead* 且 arm 与本臂一致的那条为准** + #(就是 cameras 注册表,见 gauge_assembly_node._d435_ns)。这一项只在 + # "注册表里没有手外相机"时当兜底;两者不一致会在 S00 打告警并以注册表为准。 + # 所以真机/仿真各跑各的时候**不用**改这里 —— 曾经写死 d435 让仿真去查真机的相机。 + coarse_camera: "d435" + coarse_hover_m: 0.25 # 粗定位后悬停在工件上方多高 [m](可调) + coarse_conf: 0.35 # 工件检测置信度阈值(0~1) + coarse_timeout_s: 20.0 # 等手外相机出图的最长秒数(相机刚起来要一两秒) + + # ---- S05 精拍定位:等画面稳了再拍 ---- + # 判据是"所有采样点到均值的距离 ≤ 容差",不是"相邻两帧差 ≤ 容差": + # 跟踪抖动是零均值的,相邻帧之差会在画面明明在晃时判成稳定,拍出糊图。 + snap_stable_s: 1.0 # 连续观察多久 [s] + snap_tol_px: 2.0 # 允许的抖动半径 [px](越小越严,越难满) + snap_tol_mm: 0.4 # 同上,但特征=法兰位移时的容差 [mm] + snap_min_frames: 6 # 至少采到多少帧才敢说"稳定" + # 用哪个特征判"画面稳定了": + # auto = 按 perception.source 自动选(真机 yolo→bbox;仿真真值/外部→motion) + # bbox = 工件检测框中心(像素)。需要检测器真的出框:仿真里合成图过不了 + # 真机训练出来的模型(实测最高分 0.017),选它会让 S05 空转到超时 + # motion = 法兰位移(mm)。相机装在法兰上,法兰不动画面就不动 —— 不依赖模型 + snap_stable_feature: "auto" + + # ---- S06 正悬停(悬停高度不在这里:见 servo.hole_hover_z_m)---- + align_tol_mm: 1.0 # 对位残差容差 [mm];超了回 S05 重拍(不是硬往下走) + # 孔口径 mm 级时别放大这个值:放大=直接蹭孔壁 + + # ---- S07 柔顺接触:步进下压,切向力突变即停 ---- + contact_speed_m_s: 0.02 # 每步下压速度 [m/s];这一步必须慢,快了就是一次冲击 + contact_step_mm: 0.5 # 每步下压量 [mm](越大越快,越小越不会过冲) + contact_max_depth_mm: 35.0 # 最多下探多少 [mm];到了还没接触判失败(不会一直压) + # 为什么是 35 而不是 25:判"到底了"的是**台肩落到工件 + # 上表面**那一下,而台肩在规尖上方 30mm(通端长 30, + # 见 models/gauge/model.sdf)。工件板厚 12mm,所以要 + # 下压 30mm 台肩才落面 —— 25mm 的上限根本到不了, + # 只会报"下探 25mm 都没触发接触判据"然后 ABORT。 + # 35 = 30(台肩落面)+ 5(对位残差/工件厚度公差余量)。 + # 注意:这个值**不必**手工算 —— S07 启动时会按下面两个 + # 工具几何量算出"台肩落面还需要多少",不够就地改大并告警。 + + # ---- 工具几何(S07 的"下去多少才到底"完全由这两个值决定)---- + # 为什么必须显式配:流程里的"悬停高度"是按**相机**定的(要看清孔得离面 ~0.17m), + # 不是按**工具**定的;而工具装在法兰下面、可能比相机长得多。两者一冲突就会出现: + # 悬停法兰 0.632 − 工具长 0.243 = 尖端 0.389,工件上表面 0.412 + # → **悬停这一刻工具已经插进工件 23mm** + # 于是 S07 一路压到顶都等不到"台肩落面"那一下,报"没触发接触判据", + # 看起来像力阈值标错了 —— 实际是几何错。真机同参数同结果。 + # S07 启动时会把这个差值算出来印在日志里(负值会明确标注"已经插进工件了")。 + tool_tip_below_flange_m: 0.245 # 工具尖端(规尖)在法兰下方的距离 [m] + # **这是实测值,不是推算值**:夹住规之后直接读 + # TF 的 flange z − 规尖 z。 + # 仿真里 `scripts/sim_load_gauge.py` 每次上料后 + # 会自己把它算出来打印("实测工具几何"那一段), + # 现场照着填即可 —— 真机同法(夹住规后直接读 TF)。 + # + # 推算值(TCP 在法兰下方 0.200 + 规尖在指腹面下方 + # 0.043 = 0.243)与实测 0.245 只差 2mm,说明推算 + # 思路是对的;但**别再用推算**,因为下面这个理由: + # + # ⚠️ 这个数天生有 ±几毫米的不确定度:规是**靠摩擦** + # 夹住的,每次上料在爪里的位置都会差几毫米。 + # 所以它只用来**诊断**(悬停时尖端离面多高、下探 + # 预算够不够),**不能**当硬约束 —— S07 的自检 + # 因此是"告警 + 自动放大预算",不是"直接失败"。 + # 要更准只能改夹持方式(定位台肩/加挡块),那是 + # 机械方案,不是改这个数。 + workpiece_surface_z_m: 0.0 # 工件**上表面**的世界 z [m](对孔基准面) + # 0 = 未知 → 运行期用 stage2 实测的孔位 z 兜底 + #(孔就在上表面上)。仿真里实测是 0.412。 + # 只在"stage2 还没跑就想知道几何对不对"时才需要手填。 + contact_tangential_delta: 300.0 # 触觉切向力突变阈值 [au,任意单位] —— **兜底判据** + # ⚠️ 这是"触觉"的单位,不是牛顿:真机 Orisys 输出的 + # 是形变任意单位(厂商不给牛顿换算),仿真侧由 + # force_scale 定义尺度。所以这个数**不能**与下面 + # contact_axial_delta_n 比大小(两者量纲不同)。 + # + # 它为什么只能是兜底:通止规靠**夹持摩擦**抱住,轴向力 + # 传到指垫必须过摩擦面,上限 = µ·N ≈ 0.25×46N ≈ 23N + # (两指合计)。而台肩落面一步(0.5mm × 5e4 N/m)就 + # 产生 25N —— 必然打滑,所以触觉**物理上读不到**那一下 + # 的力,读数停在 0(而它是对的)。拿它当主判据,阈值调 + # 多大都不对:调小被噪声骗,调大永远达不到。 + # 2026-09-24 就是照"阈值调高了"改的,白费一轮。 + # 详见 gauge_fsm.wrench_touch 与 docs/GAUGE_ASSEMBLY.md §6。 + contact_axial_delta_n: 8.0 # 腕部 FT 轴向力突变阈值 [N] —— **首选判据** + # 腕部 FT 在夹持摩擦的**上游**(工件→规→夹爪→法兰 + # →腕部关节),工具在爪里打不打滑都测得准,单位是真牛顿。 + # 为什么只要 8N:这是"碰到了没有"、不是"压到位了没有"。 + # 台肩一落面,接触刚度 5e4 N/m 意味着过压 0.1mm 就有 + # 5N、0.2mm 有 10N —— 8N 对应约 0.16mm 过压,**在打滑 + # 之前**就判到了(打滑上限 ≈23N ≈ 0.46mm),留 3 倍余量。 + # 现场标定:空载慢慢压到台肩落面,看日志里每 5 步打印的 + # 「腕部轴向」涨到多少,取它的 1/3(要能区分"擦到孔壁" + # 与"台肩落面")。 + # 数据源话题:servo.ft_wrench_topic(默认 + # //ft_sensor_wrench,真机与仿真同名同类型同单位)。 + contact_relief_mm: 0.5 # 判到接触后的**卸载回退** [mm] —— 别调成 0 + # 为什么必须有这一步(2026-09-24 一整轮排查换来的): + # 位置控制的臂撞上硬面时不会"刚碰到就停",它会顶到 + # 自己的**出力上限**才停住。仿真实测:0.5mm 的一步压在 + # 工件上,腕部 FT 直接读到 **+244N**,而臂这一步只走了 + # 0.09mm。也就是说"判到接触"的那一刻,臂上顶着的力 + # 与 contact_axial_delta_n **没有关系**(阈值只决定 + # 什么时候承认接触)。 + # 而规是靠**夹持摩擦**抱住的:轴向传力上限 µ·N,两指 + # 合计 ≈73N(µ=0.8、握力 46N)。244N 远超它 ⇒ 规被顶出 + # 爪外。本次现象:S07 判到接触 → S08 一转 → 规掉地上 + # → 报 E1,而 E1 的日志只说"通止规掉了",看不到真因。 + # 0.5mm 怎么来的:判到接触时真实过压量通常只有 0.0x mm + # (5e4 N/m 时 10N ≈ 0.2mm),回退 0.5mm 足够把力放 + # 干净,而规尖仍在孔里(通端穿板 18mm),不会脱出。 + # 放不干净会在日志里明确告警(残余 > contact_hold_n)。 + contact_hold_n: 5.0 # 卸载后允许的残余轴向力 [N] + # 超了不失败,但会**大声报**:它说明"夹持摩擦力不够 + # 承受这次接触",接着 S08 一转就会掉件。 + # 真修法只有两条:① 降峰值(调小 contact_step_mm 或 + # 加大 contact_relief_mm);② 加握力(改夹爪合拢力度/ + # 指垫摩擦)。**不要**去调 contact_axial_delta_n。 + contact_safe_delta: 2000.0 # 法向安全上限 [任意单位]:超了立刻抬起并中止(E2) + # 必须 > contact_tangential_delta,否则自检就报矛盾 + + # ---- S08 棘轮拧入(慢转 → 松爪 → 快回转 → 夹紧,循环)---- + # 停止判据:切向力 > screw_shear_delta **且** 法向力突增 > screw_normal_delta。 + screw_step_deg: 120.0 # 每次慢转多少度。 + # **硬约束**:2×step + 2×余量(8°) ≤ 关节7 行程。 + # Rizon4/10 的关节7 是 ±175° → 行程 350° → step ≤ 167°。 + # 填 180° 会**必然**贴限位(慢转+回转=360°>350°), + # 测试 test_ratchet_step_of_180_never_fits_the_rizon_joint7_range + # 与 S08 开头的行程核算都会拦;120° 留足余量吸收 + # "回转每轮比指令少走 4~5°"的亏欠。 + screw_speed_deg_s: 30.0 # 慢转速度 [deg/s] + screw_back_speed_deg_s: 120.0 # 松爪后的空转回转速度 [deg/s](爪里没东西,可以快) + screw_max_cycles: 12 # 最多几轮(12×120°=1440° = **4 整圈**,够 M6 螺纹规走完)。 + # 必须满足 screw_max_cycles × screw_step_deg 是 360° 的整数倍: + # 否则规停在螺纹中间,S09 拧出也少转几分之一圈。 + screw_normal_delta: 400.0 # 拧到位时的法向力突增 [任意单位](螺纹到底/顶住) + screw_shear_delta: 600.0 # 拧到位的切向力增量 [任意单位] + screw_grip_force_pct: 50.0 # 夹紧力百分比 [%](**只有契约后端生效**:仿真/新驱动) + screw_grip_speed_pct: 50.0 # 夹紧速度百分比 [%](同上;真机 dh_pgea 驱动不支持这两个参数) + screw_release_wait_s: 0.3 # 松爪后等多久再回转 [s](等爪真的张开,否则会带着规磨) + + # ---- S09 反向拧出 ---- + unscrew_extra_cycles: 1 # 比拧入多转几轮,确保螺纹完全脱开 + + # ---- 力判据怎么调(现场最常问)---- + # 1) 先看量级:bash ~/ros2/scripts/run_tactile.sh --cli,手压一下看读数变化; + # 2) S07 的接触判据是"相对基线的增量",基线在 S06 悬停时(还没碰工件)测; + # 3) 太灵敏(刚靠近就停)→ 调大 contact_tangential_delta; + # 不灵敏(压很深才停)→ 调小;同时看启动日志里的基线/实测值再定。 + +tactile: + backend: "orisys" # orisys | mock + # 传感器型号。必须与 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" + # 用 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" + right_video: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:3.4.2:1.0-video-index0" + cuda: false # 两路进程抢 GPU 上下文会互相拖慢;CPU 解算仅 ~9ms/帧,足够 + isstitch: true + infer_backend: "cpu" # cupy | opencl | cpu;双路并行用 cpu 最稳 + baseline_seconds: 2.0 + filter_alpha: 0.25 + motion_threshold: 0.0 + target_fps: 30.0 + publish_hz: 20.0 + # 视触觉图像与力矢量场的发布频率。0 = 关闭(默认)。 + # 三力走 30fps 的轻量队列;图像每帧几百 KB,单独走一条 maxsize=1 的队列, + # 打开它不会影响三力的时序。需要看形变图/矢量场时设成 5~10 即可。 + image_hz: 0.0 + rotation_config: "" # 左右共用;有分指文件则用下面两项 + left_rotation: "" # 例:orisys SDK 的 rotation_config_0.json + right_rotation: "" + +go_home: + pose: "home" + speed: 0.25 + +network: + # 2026-09-29 改线:两台臂 → 交换机 → USB-C 千兆网卡(ASIX AX88179) + # MAC 6c:1f:f7:d0:6f:17。内核现在叫 eth0(不再用 enx6c1ff7d06f17)。 + # 本机 192.168.2.10 ↔ 063989(左89)=.101 / 063991(右91)=.100 + # NM 配置名 robot-usb-lan(静态 .10/24,不当默认路由)。 + # 说明:enp4s0 仍是办公网(192.168.1.x);两台臂已不在 enp4s0 上。 + prefer_iface: "eth0" + +# 仿真平台(当前 Gazebo 尚未接线,详见 devices/sim/gazebo/README.md) +# 设计约束:仿真与真机共用同一套参数(servo.* / hole_detect.stage2.*), +# 只有物理引擎自身的参数放在这里,避免「仿真调好的值搬到真机不生效」。 +sim: + # 这一段由 arm_sim_gazebo.sim_site 真正读取(此前是"声明了但没人读", + # 改了毫无反应也不报错)。三个键的接线状况**不一样**,务必看清: + # + # world ✔ 已接线。优先级:命令行 --world > 这里 > 内置默认。 + # 相对路径按**仓库根**解析。 + # physics_enable ✘ 未接线。世界文件里的重力/步长是写死的。若这里写的值 + # 与世界的实际状态不符,启动时会打印告警(不再静默)。 + # joint_prefix ✘ 未接线。前缀由 urdf/arm.sim.urdf.xacro 的 arm_prefix + # 决定,且 config/controllers.yaml 里硬编码了 sim_joint1..7。 + # 写了个不同的值会告警。要真改,三处必须一起改。 + # + # 另外注意:`run_sim.sh` 走的仍是 mock 驱动(假数据 + RViz),不是物理仿真; + # 物理仿真入口是 `scripts/run_sim_gz.sh`(Gazebo)。两者互不影响。 + platform: "gazebo" + world: "devices/sim/gazebo/worlds/assembly.world" + physics_enable: true + joint_prefix: "sim_" # 仿真关节前缀,必须与真实臂前缀区分 diff --git a/docs/GRIPPER.md b/docs/GRIPPER.md index cc959ec..3cd977c 100644 --- a/docs/GRIPPER.md +++ b/docs/GRIPPER.md @@ -16,7 +16,7 @@ ```bash # 左臂平行爪 bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli probe -bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli init # 上电第一次 +bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli init # 写 0xA5 回零;已经 initialized 也会再做一次 bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli open bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli close bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli pos 500 # 0=合 1000=开(‰) diff --git a/docs/HANDBOOK.md b/docs/HANDBOOK.md index dd796bf..d393c0f 100644 --- a/docs/HANDBOOK.md +++ b/docs/HANDBOOK.md @@ -68,22 +68,18 @@ bash ~/ros2/scripts/run_camera_d405.sh --arm 89 --preview # T2 相机预览, bash ~/ros2/scripts/run_all.sh --arm 89 # T3 三段串联(默认不拧入) ``` -一阶段默认 **D405 实时推理**,D435 待命。连续 `D405_MISS_BEFORE_D435_S`(0.5 s)没有 -conf≥`perception.wrist_handoff_conf`(0.5)的目标,才用顶视 D435 **拍一张** 把工件 / k0 -定到基座系(眼在手外 T,不再乘法兰),左臂按二阶段同一套 **`FollowJointTrajectory` action** -先收齐锁高,再把全部 XY 串成一条关节折线,余弦加速(100 mm)/ -巡航封顶 60 mm/s / 余弦减速(80 mm),头尾速度为 0。中途不重测、不停住。 -40 mm 爬到 80 mm/s 会在真机上像弹射,那是加速段太短,不是初速度或角速度。 -工件上方(剩差 ≤80 mm)巡航改 30 mm/s,只走 XY,不混 Z、不夹收垂直。 -结果 + 0.25 s 再重测 TF(带符号 `dxy`)。冻结 J 若还剩 >15 mm,只允许 **一刀补** -(`tag=fix`)。XY 进 15 mm 门槛后再单独收法兰朝下。结果回来之前不往话题插点。 -到位成功不再发话题 hold。几乎没走近就钉住,不许按同一剩差再刷。 -走到该点并且法兰 XY 连续 `servo.lock_still_sec`(3 s)位移不超过 -`lock_still_move_m`(15 mm)之前,D405 的点不下发(推理不关)。停稳之后改回 D405 -跟踪;再按同一静止判据 → `/visual_servo/locked` → 二阶段对孔。 -只测 D435 这一段位移时加 `--d435-only`:D405 点不下发,到位停稳后钉住并结束一阶段, -不交 D405、不进二阶段。眼外标定误差只会让冻结点整体偏一截,不会造成中途启停; -启停来自以前按 80/15 mm 一跳一跳 rest-to-rest。 +一阶段默认 **D405 实时推理**,D435 待命。YOLO-pose 这一路不关。连续 +`D405_MISS_BEFORE_D435_S`(0.5 s)没有 conf≥`perception.wrist_handoff_conf`(0.5) +的目标,才用顶视 D435 **拍一张**,把点用眼在手外 T 变到基座(不再乘法兰)。 +这一段运动是厂家 `MoveL`:先放开 `ros2_control` 占着的 RDK,XY 对准该点, +工具尖高度保持当前(点的 z 是工件表面),不按直线距离拒绝;走完再用 +`run_real.sh --no-stop` 把控制栈拉回来(不扫感知进程)。走到该点并且法兰 XY +连续 `servo.lock_still_sec`(3 s)位移不超过 `lock_still_move_m`(15 mm)之前, +D405 的点不下发。停稳之后改回 D405,跟踪点是工件框的矩形中心,不追 k0。 +框心对准连续 `servo.wrist_align_sec`(1 s)→ `/visual_servo/locked` → 二阶段对小孔。 +对孔只用关键点像素交到 `workpiece_surface_z_m`,不读深度图。下臂停在 +`tool10120938` 的 TCP 长度(0.2245 m)加上 `final_gap_mm` 之上,不按旧 tool1 的 0.223 m。 +只测 D435 这一段位移时加 `--d435-only`:不交 D405、不进二阶段,并且要求先停 `run_real.sh`。 ```bash bash ~/ros2/scripts/run_all.sh --arm 89 --no-camera --d435-only diff --git a/handeye/results/sim_d435_eye_to_hand.yaml b/handeye/results/sim_d435_eye_to_hand.yaml new file mode 100644 index 0000000..2d9597c --- /dev/null +++ b/handeye/results/sim_d435_eye_to_hand.yaml @@ -0,0 +1,11 @@ +# 仿真手眼真值 —— **自动生成,不要手改** +# mode: eye_to_hand +# 来源: Gazebo/URDF 的 TF 真值 `sim_base_link` ← `sim_d435_color_optical_frame`(仿真不需要标定) +# 生成: 2026-09-28T14:05:19+0800 生成器: scripts/gen_sim_handeye.py +# 仿真台:手外相机固定在工位/世界上(不随臂动) +translation_m: [-0.45000000, 0.00000000, 1.35000000] +T_4x4: + - [-0.00000236, -0.76604704, 0.64278451, -0.45000000] + - [-1.00000000, 0.00000000, -0.00000367, 0.00000000] + - [0.00000281, -0.64278451, -0.76604704, 1.35000000] + - [0.00000000, 0.00000000, 0.00000000, 1.00000000] diff --git a/handeye/results/sim_eye_in_hand.yaml b/handeye/results/sim_eye_in_hand.yaml new file mode 100644 index 0000000..72450a7 --- /dev/null +++ b/handeye/results/sim_eye_in_hand.yaml @@ -0,0 +1,11 @@ +# 仿真手眼真值 —— **自动生成,不要手改** +# mode: eye_in_hand +# 来源: Gazebo/URDF 的 TF 真值 `sim_flange` ← `sim_d405_color_optical_frame`(仿真不需要标定) +# 生成: 2026-09-28T14:05:19+0800 生成器: scripts/gen_sim_handeye.py +# 仿真台:腕部相机装在法兰上,位姿来自 URDF +translation_m: [0.00000000, -0.06000000, 0.05000000] +T_4x4: + - [0.00000000, 1.00000000, -0.00000000, 0.00000000] + - [-1.00000000, 0.00000000, -0.00000367, -0.06000000] + - [-0.00000367, 0.00000000, 1.00000000, 0.05000000] + - [0.00000000, 0.00000000, 0.00000000, 1.00000000] diff --git a/models/hole_insert/detect_best_1.engine b/models/hole_insert/detect_best_1.engine new file mode 100644 index 0000000..f713620 Binary files /dev/null and b/models/hole_insert/detect_best_1.engine differ diff --git a/models/hole_insert/detect_best_1.onnx b/models/hole_insert/detect_best_1.onnx new file mode 100644 index 0000000..f10aa7c Binary files /dev/null and b/models/hole_insert/detect_best_1.onnx differ diff --git a/models/yolo/detect_best_0.engine b/models/yolo/detect_best_0.engine new file mode 100644 index 0000000..154d002 Binary files /dev/null and b/models/yolo/detect_best_0.engine differ diff --git a/models/yolo/detect_best_0.onnx b/models/yolo/detect_best_0.onnx new file mode 100644 index 0000000..8e9ee3b Binary files /dev/null and b/models/yolo/detect_best_0.onnx differ diff --git a/models/yolo/gongjian0929.engine b/models/yolo/gongjian0929.engine new file mode 100644 index 0000000..70c8d6f Binary files /dev/null and b/models/yolo/gongjian0929.engine differ diff --git a/models/yolo/gongjian0929.onnx b/models/yolo/gongjian0929.onnx new file mode 100644 index 0000000..a1004d6 Binary files /dev/null and b/models/yolo/gongjian0929.onnx differ diff --git a/scripts/d435_k0_once.py b/scripts/d435_k0_once.py new file mode 100644 index 0000000..1f56941 --- /dev/null +++ b/scripts/d435_k0_once.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""拍一张顶视 D435,把 k0 用眼在手外标定变到左臂基座。 + +stdout 只打一行,给 run_all.sh --d435-only 接厂家 MoveL: + + K0_BASE x y z + +x y z 单位米,坐标系是这条臂的基座(与 RDK 的 WORLD 同一套)。 +变换就是 handeye/results/<臂>_d435_eye_to_hand.yaml 的 T_4x4: + + p_基座 = T_cam_to_base · p_相机 + +k0 的 z 是工件表面,不是工具尖该去的高度。运动脚本会保持当前尖高,只用这里的 xy。 +没有可信 k0 就退出,不用检测框中心冒充。 +""" + +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import rclpy +from rclpy.node import Node +from rclpy.qos import qos_profile_sensor_data +from sensor_msgs.msg import CameraInfo, Image + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts" / "lib")) + +from export_site_env import load_site, perception_overrides # noqa: E402 +from arm_application.geom_ik import load_handeye_T # noqa: E402 +from arm_application.overhead_oneshot import ( # noqa: E402 + depth_raw_to_m, + oneshot_k0_base, + select_oneshot_det, +) +from arm_application.perception_node import _bgr_copy # noqa: E402 +from arm_application.yolo_detector import build_detector # noqa: E402 + + +def _decode_depth(msg: Image): + w, h = int(msg.width), int(msg.height) + enc = (msg.encoding or "").lower() + if enc in ("16uc1", "mono16"): + raw = np.frombuffer(msg.data, dtype=np.uint16) + if raw.size < w * h: + return None + return raw[: w * h].reshape(h, w) + if enc == "32fc1": + raw = np.frombuffer(msg.data, dtype=np.float32) + if raw.size < w * h: + return None + return raw[: w * h].reshape(h, w) + return None + + +class OneShot(Node): + def __init__(self, topics: dict) -> None: + super().__init__("d435_k0_once") + self.color = None + self.depth = None + self.info = None + self.create_subscription(Image, topics["image"], self._on_color, qos_profile_sensor_data) + self.create_subscription(Image, topics["depth"], self._on_depth, qos_profile_sensor_data) + self.create_subscription(CameraInfo, topics["info"], self._on_info, qos_profile_sensor_data) + + def _on_color(self, msg: Image) -> None: + self.color = msg + + def _on_depth(self, msg: Image) -> None: + self.depth = msg + + def _on_info(self, msg: CameraInfo) -> None: + self.info = msg + + def ready(self) -> bool: + return self.color is not None and self.depth is not None and self.info is not None + + +def main() -> int: + perc = perception_overrides(load_site()) + if not perc.get("overhead_oneshot_enable"): + print("site 没有接上 D435 一次性(overhead_oneshot_enable)", file=sys.stderr) + return 2 + handeye = str(perc.get("overhead_handeye_file") or "") + topics = { + "image": str(perc.get("overhead_image_topic") or ""), + "depth": str(perc.get("overhead_depth_topic") or ""), + "info": str(perc.get("overhead_info_topic") or ""), + } + if not handeye or not all(topics.values()): + print(f"眼在手外话题或标定文件没接上:{handeye} {topics}", file=sys.stderr) + return 2 + T = load_handeye_T(handeye) + print(f"手眼 {handeye}", file=sys.stderr) + print(f"图 {topics['image']}", file=sys.stderr) + + weights = str(perc.get("weights") or "") + detector = build_detector( + weights, + backend=str(perc.get("backend") or "auto"), + conf=float(perc.get("conf") or 0.1), + iou=float(perc.get("iou") or 0.45), + device=str(perc.get("device") or "0"), + imgsz=int(perc.get("imgsz") or 640), + ) + + rclpy.init() + node = OneShot(topics) + t0 = time.monotonic() + try: + while rclpy.ok() and not node.ready(): + rclpy.spin_once(node, timeout_sec=0.2) + if time.monotonic() - t0 > 15.0: + print("15s 内没有凑齐彩色图、深度、内参", file=sys.stderr) + return 3 + color, depth, info = node.color, node.depth, node.info + finally: + node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + + bgr = _bgr_copy(color) + dets = detector.detect(bgr) + min_conf = float(perc["overhead_oneshot_conf"]) + det = select_oneshot_det(dets, min_conf) + if det is None: + print(f"画面里没有 conf≥{min_conf:.2f} 的工件", file=sys.stderr) + return 4 + raw = _decode_depth(depth) + depth_m = depth_raw_to_m(raw, depth.encoding or "") + if depth_m is None: + print("深度无法转成米", file=sys.stderr) + return 4 + k = info.k + got = oneshot_k0_base( + det, + depth_m, + fx=float(k[0]), + fy=float(k[4]), + cx=float(k[2]), + cy=float(k[5]), + T_cam_to_base=T, + min_z_m=float(perc["overhead_min_depth_m"]), + max_z_m=float(perc["overhead_max_depth_m"]), + kpt_index=int(perc.get("keypoint_index") or 0), + kpt_min_conf=float(perc.get("keypoint_min_conf") or 0.30), + min_det_conf=min_conf, + color_wh=(int(color.width), int(color.height)), + depth_wh=(int(depth_m.shape[1]), int(depth_m.shape[0])), + ) + if got is None: + print("k0 或深度不可用,不发点", file=sys.stderr) + return 4 + p_base, is_k0 = got + if not is_k0: + print( + f"顶视没有可信 k0,只有框心 ({p_base[0]:.3f},{p_base[1]:.3f},{p_base[2]:.3f}),拒绝", + file=sys.stderr, + ) + return 4 + print( + f"k0 基座 ({p_base[0]:.4f}, {p_base[1]:.4f}, {p_base[2]:.4f}) m conf={float(det.conf):.2f}", + file=sys.stderr, + ) + print(f"K0_BASE {p_base[0]:.6f} {p_base[1]:.6f} {p_base[2]:.6f}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lib/export_site_env.py b/scripts/lib/export_site_env.py index 742b157..9ed3756 100644 --- a/scripts/lib/export_site_env.py +++ b/scripts/lib/export_site_env.py @@ -903,6 +903,7 @@ def servo_overrides(data: dict) -> dict: "ee_hold_kp", "ee_w_max", "lock_hold_sec", "lock_still_sec", + "wrist_align_sec", "lock_still_move_m", "a_max", "max_qdot", "damping", "vel_lpf_alpha", "dq_window_size", "traj_stream_hz", "exec_horizon_sec", @@ -934,7 +935,12 @@ def servo_overrides(data: dict) -> dict: if "lock_still_sec" not in servo: raise RuntimeError( "servo.lock_still_sec 必须显式写" - "(D405 跟住后法兰静止多久才进二阶段)。不许用代码默认 3。" + "(D435 到位后法兰停稳多久才交回 D405)。不许用代码默认 3。" + ) + if "wrist_align_sec" not in servo: + raise RuntimeError( + "servo.wrist_align_sec 必须显式写" + "(D405 框心对准多久才进对孔)。不许用代码默认 1。" ) if "lock_still_move_m" not in servo: raise RuntimeError( @@ -942,6 +948,7 @@ def servo_overrides(data: dict) -> dict: "(静止判据的法兰 XY 位移上限)。不许用代码默认 15mm。" ) out["lock_still_sec"] = float(servo["lock_still_sec"]) + out["wrist_align_sec"] = float(servo["wrist_align_sec"]) out["lock_still_move_m"] = float(servo["lock_still_move_m"]) out["overhead_k0_topic"] = "visual_servo/overhead_k0" out["wrist_conf_topic"] = "visual_servo/wrist_conf" diff --git a/scripts/lib/rdk_release.py b/scripts/lib/rdk_release.py new file mode 100644 index 0000000..fb71e76 --- /dev/null +++ b/scripts/lib/rdk_release.py @@ -0,0 +1,19 @@ +"""D435 一次性要走厂家 MoveL 时,先认出谁占着 RDK。 + +`ros2_control_node` 和真机 `stack.launch` 占着唯一的 RDK 连接。 +感知、伺服、本脚本自己不算。误伤它们会把正在推理的进程一起杀掉。 +""" + +from __future__ import annotations + + +def is_control_stack_cmd(cmd: str) -> bool: + """这条命令行是不是占着 RDK 的真机控制栈。""" + text = cmd or "" + if "rdk_release_movel" in text: + return False + if "ros2_control_node" in text: + return True + if "stack.launch.py" in text and "backend:=real" in text: + return True + return False diff --git a/scripts/rdk_goto_xyz.py b/scripts/rdk_goto_xyz.py index 82e0891..031c579 100644 --- a/scripts/rdk_goto_xyz.py +++ b/scripts/rdk_goto_xyz.py @@ -17,6 +17,7 @@ [x, y, z, qw, qx, qy, qz],单位米,坐标系是机器人世界系(基座那套 WORLD)。 位置是当前 Studio 工具的 TCP(现在是 tool10120938 的尖),不是法兰,也不是相机。 4. 目标只换 x、y、z。姿态沿用现在的朝向,折成厂家 MoveL 要的欧拉角(度,ZYX)。 + `--keep-z` 时 z 保持当前工具尖:K0 的 z 是工件表面,不能把尖送到那上面。 5. SwitchMode(NRT_PRIMITIVE_EXECUTION),ExecutePrimitive("MoveL", ...)。 6. 轮询 primitive_states()["reachedTarget"]。到了再读一次 tcp_pose,印出还差多少毫米。 @@ -134,6 +135,11 @@ def main() -> int: parser.add_argument("--vel", type=float, default=0.05, help="TCP 线速度 m/s,默认 0.05") parser.add_argument("--max-step-m", type=float, default=0.40, help="离当前点超过这么多米就拒绝,除非 --go") parser.add_argument("--go", action="store_true", help="允许超过 --max-step-m") + parser.add_argument( + "--keep-z", + action="store_true", + help="XY 用给定点,Z 保持当前工具尖。K0 的 Z 是工件表面,不能把尖送到那上面", + ) parser.add_argument("--timeout", type=float, default=60.0, help="等 reachedTarget 的秒数") args = parser.parse_args() @@ -154,6 +160,12 @@ def main() -> int: return 0 target_xyz = [float(v) for v in args.xyz] + if args.keep_z: + print( + f"给定点 z={target_xyz[2]:.4f} m 是工件表面,不下发。" + f"工具尖高度保持当前 {pose[2]:.4f} m" + ) + target_xyz[2] = float(pose[2]) dist = math.dist(pose[:3], target_xyz) print(f"目标 xyz=({target_xyz[0]:.4f}, {target_xyz[1]:.4f}, {target_xyz[2]:.4f}) m") print(f"直线距离 {dist * 1000:.0f} mm 速度 {args.vel:.3f} m/s 姿态保持当前") diff --git a/scripts/rdk_release_movel.py b/scripts/rdk_release_movel.py new file mode 100644 index 0000000..d4b1d05 --- /dev/null +++ b/scripts/rdk_release_movel.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""D435 这一点用厂家 MoveL 送到工具尖,再把真机控制栈拉回来。 + +RDK 同时只能有一个连接。混合流程里 `run_real.sh` 已经占着它, +所以顺序是:停真机栈 → MoveL(`--keep-z --go`)→ 用 `--no-stop` 把栈拉回来。 + +`--no-stop` 是为了不扫 `install/`:感知和伺服还在跑,扫了会把自己杀掉。 +YOLO 不在这里关。 +""" + +from __future__ import annotations + +import argparse +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +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") + + +def _procs() -> list[tuple[int, str]]: + out: list[tuple[int, str]] = [] + proc = Path("/proc") + for entry in proc.iterdir(): + if not entry.name.isdigit(): + continue + pid = int(entry.name) + if pid == os.getpid(): + continue + try: + raw = (entry / "cmdline").read_bytes().replace(b"\x00", b" ") + except OSError: + continue + cmd = raw.decode("utf-8", "replace").strip() + if cmd: + out.append((pid, cmd)) + return out + + +def _control_pids() -> list[tuple[int, str]]: + return [(pid, cmd) for pid, cmd in _procs() if is_control_stack_cmd(cmd)] + + +def _term(pids: list[int]) -> None: + for pid in pids: + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + deadline = time.monotonic() + 8.0 + while time.monotonic() < deadline: + alive = [] + for pid in pids: + try: + os.kill(pid, 0) + except OSError: + continue + alive.append(pid) + if not alive: + return + time.sleep(0.2) + pids = alive + for pid in pids: + try: + os.kill(pid, signal.SIGKILL) + except OSError: + pass + + +def _service_up() -> bool: + try: + listed = subprocess.run( + ["timeout", "3", "ros2", "service", "list"], + capture_output=True, + text=True, + check=False, + ) + except OSError: + return False + return "/controller_manager/list_controllers" in (listed.stdout or "") + + +def _controller_active() -> bool: + if not _service_up(): + return False + try: + call = subprocess.run( + [ + "timeout", "3", "ros2", "service", "call", + "/controller_manager/list_controllers", + "controller_manager_msgs/srv/ListControllers", + ], + capture_output=True, + text=True, + check=False, + ) + except OSError: + return False + text = (call.stdout or "") + (call.stderr or "") + return "name='rizon_arm_controller', state='active'" in text + + +def _wait_controller(up: bool, timeout_s: float) -> bool: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + active = _controller_active() + if active == up: + return True + time.sleep(0.5) + return _controller_active() == up + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--xyz", nargs=3, type=float, required=True, metavar=("X", "Y", "Z")) + ap.add_argument("--arm", default=os.environ.get("SITE_ROBOT_ARM", "89")) + ap.add_argument("--vel", type=float, default=0.05) + args = ap.parse_args() + + held = _control_pids() + if held: + print(f"放开 RDK:停 {len(held)} 个真机控制进程(感知不停)") + for pid, cmd in held: + print(f" {pid} {cmd[:160]}") + _term([pid for pid, _cmd in held]) + if not _wait_controller(False, 20.0): + print("ERROR: 控制器服务还在,MoveL 会和它抢 RDK", file=sys.stderr) + return 1 + else: + print("真机控制栈没在跑,直接 MoveL") + + x, y, z = (float(v) for v in args.xyz) + movel = [ + RDK_PYTHON, + str(ROOT / "scripts" / "rdk_goto_xyz.py"), + "--xyz", f"{x}", f"{y}", f"{z}", + "--keep-z", "--go", + "--vel", f"{float(args.vel)}", + ] + print("厂家 MoveL:XY 对准 D435 这一点,工具尖高度保持当前") + got = subprocess.run(movel, check=False) + if got.returncode != 0: + print(f"ERROR: MoveL 退出码 {got.returncode}", file=sys.stderr) + + if held: + log_path = Path("/tmp/run_all_real_restart.log") + print(f"拉回真机栈(--no-stop,不扫感知) log={log_path}") + log_f = log_path.open("ab") + subprocess.Popen( + ["bash", str(ROOT / "scripts" / "run_real.sh"), "--no-stop", "--arm", str(args.arm)], + stdout=log_f, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + if not _wait_controller(True, 120.0): + print( + "ERROR: 控制栈没回到 ACTIVE。看 /tmp/run_all_real_restart.log", + file=sys.stderr, + ) + return 1 + print("rizon_arm_controller 已回到 ACTIVE") + return 0 if got.returncode == 0 else int(got.returncode) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_all.sh b/scripts/run_all.sh index 48c9844..6d76af7 100755 --- a/scripts/run_all.sh +++ b/scripts/run_all.sh @@ -21,7 +21,7 @@ # bash ~/ros2/scripts/run_all.sh --arm 89 --no-camera # 相机已在别处拉起了 # bash ~/ros2/scripts/run_all.sh --arm 89 --no-tactile # 不起触觉服务 # bash ~/ros2/scripts/run_all.sh --arm 89 --no-descent # 对准后只降到观测高度(默认离面 15mm) -# bash ~/ros2/scripts/run_all.sh --arm 89 --d435-only # 只测 D435 单次位移,到位后不交 D405、不进二阶段 +# bash ~/ros2/scripts/run_all.sh --arm 89 --d435-only # 拍一张 K0,厂家 MoveL 把工具尖 XY 对准它(先停 run_real.sh) # # 前置(另开一个终端,先跑起来): # bash ~/ros2/scripts/run_real.sh --arm 89 @@ -86,6 +86,49 @@ fi echo "=== run_all(三阶段串联) arm=${SITE_ROBOT_ARM:-?} ${SITE_ROBOT_LABEL:-} (${SITE_ROBOT_SN:-?}) ===" +# D435 单次定位不走关节轨迹。拍一张 → K0 用眼在手外 T 变到基座 → 厂家 MoveL。 +# RDK 与 run_real.sh 互斥,所以这条路径要求控制器没开。 +if [[ "${D435_ONLY}" == true ]]; then + echo "=== D435 单次定位:厂家 RDK MoveL(先停 run_real.sh)===" + if timeout 3 ros2 service list 2>/dev/null | grep -q "^/controller_manager/list_controllers$"; then + echo "ERROR: run_real.sh 还占着 RDK,MoveL 会被打回 IDLE。先在那个终端 Ctrl-C。" >&2 + exit 1 + fi + D435_TOPIC="/d435/color/image_raw" + _d435_ok() { + timeout 2 ros2 topic list 2>/dev/null | grep -qx "${D435_TOPIC}" || return 1 + timeout 3 ros2 topic echo --once "${D435_TOPIC}" 2>/dev/null | grep -q "^header:" + } + if _d435_ok; then + echo "=== 顶视 ${D435_TOPIC} 已在发图 ===" + else + echo "=== 拉起顶视 D435 ===" + nohup bash "${ROOT}/scripts/run_camera_d435.sh" --no-preview > /tmp/run_all_d435.log 2>&1 & + echo " d435 pid=$! log=/tmp/run_all_d435.log" + d435_ok=false + for _i in $(seq 1 24); do + if _d435_ok; then d435_ok=true; break; fi + sleep 0.5 + done + if [[ "${d435_ok}" != true ]]; then + echo "ERROR: ${D435_TOPIC} 仍无图像。看 /tmp/run_all_d435.log" >&2 + exit 1 + fi + fi + echo "=== 拍一张,K0 × 眼在手外 T → 基座 ===" + k0_line="$(python3 "${ROOT}/scripts/d435_k0_once.py")" + echo " ${k0_line}" + read -r _tag kx ky kz <<<"${k0_line}" + if [[ "${_tag}" != "K0_BASE" ]]; then + echo "ERROR: 没有得到 K0_BASE,得到:${k0_line}" >&2 + exit 1 + fi + echo "=== MoveL:XY 对准 K0,工具尖高度保持当前(K0 的 z 是工件表面)===" + # --go:不按直线距离拦截。800 mm 是脚本自己的保护,不是机械臂限位。 + exec /home/cc/rdk_py310/bin/python "${ROOT}/scripts/rdk_goto_xyz.py" \ + --xyz "${kx}" "${ky}" "${kz}" --keep-z --go --vel 0.05 +fi + # ---- 1) 控制器必须在。判活方式与 run_stage2_locate.sh 保持一致 ---- echo "=== 等待 rizon_arm_controller ACTIVE(T1: run_real.sh --arm ${SITE_ROBOT_ARM:-?})===" # 先看服务在不在,再 call —— 顺序反过来的话,服务不存在时每次 call 都要耗满 diff --git a/scripts/run_real.sh b/scripts/run_real.sh index 7e51b0f..1517e97 100755 --- a/scripts/run_real.sh +++ b/scripts/run_real.sh @@ -10,10 +10,12 @@ set -eo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" FLEXIV_PREFIX="${HOME}/flexiv_install" LIST_ONLY=false +NO_STOP=false while [[ $# -gt 0 ]]; do case "$1" in --sn|--arm) export ROBOT_SN="${2:?}"; export ROBOT_ARM="${2}"; shift 2 ;; + --no-stop) NO_STOP=true; shift ;; --list|-l) LIST_ONLY=true; shift ;; -h|--help) sed -n '2,8p' "$0" @@ -54,6 +56,9 @@ echo "=== catalog: ${SITE_ROBOT_CATALOG:-} ===" echo "=== handeye: ${SITE_HANDEYE_FILE:-} ===" echo "=== camera serial: ${SITE_CAM_SERIAL:-} gripper: ${SITE_GRIP_MODEL:-} ${SITE_GRIP_PORT:-} ===" echo "=== network iface=${NET_IFACE:-auto} robot=${ROBOT_IP} pc=${SITE_PC_IP:-} ===" +if [[ "${NO_STOP}" == true ]]; then + echo "=== --no-stop:不扫 install/(感知和伺服还在,扫了会把自己杀掉)===" +else echo "=== stop leftover real stack (RDK exclusive) ===" # 以前这里是两行 pkill(ros2_control_node + stack.launch),漏掉了 # arm_control 那一族**独立存活**的孤儿(adapter / named_pose_server / @@ -75,6 +80,7 @@ if [ "${#_stale[@]}" != "0" ]; then unset _pid fi unset _stale +fi sleep 1 # 清掉把 91 的 .100 误绑到 89 MAC 上的陈旧 ARP if [[ -n "${NET_IFACE}" ]]; then diff --git a/src/arm_application/arm_application/dh_official.py b/src/arm_application/arm_application/dh_official.py index 451d8b6..2467ae9 100644 --- a/src/arm_application/arm_application/dh_official.py +++ b/src/arm_application/arm_application/dh_official.py @@ -201,21 +201,36 @@ class DHModbusClient: def set_position_per_mille(self, position: float | int) -> int: value = _clamp_int(position, 0, 1000) + # 与 dh_pgea.latched_position_break 同一份:命令已是目标时先写当前位置, + # 读回之后再写最终值。60ms 后写回旧命令,固件采样不到,手指不动。 + from arm_application.dh_pgea import latched_position_break + try: cmd = _clamp_int(self.read_register(REG_POSITION), 0, 1000) except Exception: cmd = value - if cmd == value: - try: - cur = self.get_position_per_mille() - except Exception: - cur = value - nudge = cur + (40 if value >= cur else -40) - nudge = _clamp_int(nudge, 0, 1000) - if nudge == value: - nudge = 0 if value != 0 else 40 - self.write_register(REG_POSITION, nudge) - time.sleep(0.06) + try: + cur = self.get_position_per_mille() + except Exception: + cur = value + bridge = latched_position_break(cmd, cur, value) + if bridge is not None: + self.write_register(REG_POSITION, bridge) + deadline = time.monotonic() + 0.6 + echoed = False + while time.monotonic() < deadline: + try: + if _clamp_int(self.read_register(REG_POSITION), 0, 1000) == bridge: + echoed = True + break + except Exception: + pass + time.sleep(0.05) + if not echoed: + raise DHModbusError( + f"位置命令中间值 {bridge} 没有读回,不再写入最终目标 {value}" + ) + time.sleep(0.15) self.write_register(REG_POSITION, value) return value diff --git a/src/arm_application/arm_application/dh_pgea.py b/src/arm_application/arm_application/dh_pgea.py index 2653230..3cfbbbf 100644 --- a/src/arm_application/arm_application/dh_pgea.py +++ b/src/arm_application/arm_application/dh_pgea.py @@ -104,6 +104,44 @@ INIT_STATE_NAME = { } +def latched_position_break(command: int, current: int, target: int) -> Optional[int]: + """命令寄存器已经等于目标、手指却不在目标上时,返回要先写入的中间位置。 + + 大寰固件:再写一遍与 0x0103 当前值相同的数,不会启动新运动。 + 开爪卡在 caught 时就是这样——命令已是 1000,手指停在夹持位。 + 先把命令改成当前位置(手指本来就在这,不会再夹紧),等读回确认 + 固件已经采样,再写最终目标。只 sleep 几十毫秒就写回旧值,控制周期 + 来不及看到中间值,最终仍是旧命令,手指不动。 + """ + cmd = int(max(0, min(1000, command))) + cur = int(max(0, min(1000, current))) + pos = int(max(0, min(1000, target))) + if cmd != pos or abs(cur - pos) <= 25: + return None + if cur != cmd: + return cur + return 0 if cmd != 0 else 40 + + +def grip_motion_settled( + grip_state: int, position: int, target: int, seen_motion: bool +) -> bool: + """这一拍能不能算「这句位置命令已经结束」。 + + 张开前本来就是 caught、位置还停在夹持位:不能当成张开完成。 + 闭合夹住物体:动过之后停在比目标更开的位置,算完成。 + """ + near = abs(int(position) - int(target)) <= 25 + if grip_state in (S_GRIP_ARRIVED, S_GRIP_DROPPED) and near: + return True + if grip_state == S_GRIP_CAUGHT and seen_motion: + if int(target) + 25 < int(position): + return True + if near: + return True + return False + + def crc16_modbus(data: bytes) -> int: """官方 DH CRC16(Modbus,线路上按小端发送)。""" crc = 0xFFFF @@ -319,17 +357,28 @@ class DhPgeaGripper: def initialization(self) -> bool: return self.write_register(REG_INIT, INIT_CMD) - def initialize(self, timeout_s: float = 20.0, poll_s: float = 0.2) -> bool: - """写 0xA5,然后等到初始化状态 == 完成(对应官方上电流程)。""" + def initialize( + self, timeout_s: float = 20.0, poll_s: float = 0.2, *, force: bool = False + ) -> bool: + """写 0xA5,然后等到初始化状态 == 完成(对应官方上电流程)。 + + ``force=False`` 时,已经完成就不再写(节点启动不要每次都让手指找零)。 + 命令行 ``init`` 必须 ``force=True``:状态字已经是完成时,不写 0xA5 + 手指根本不会回零,看起来像初始化失效。 + """ state = self.get_init_state() - if state == S_INIT_FINISHED: + if state == S_INIT_FINISHED and not force: return True if not self.initialization(): raise DhPgeaError("初始化写 0x0100=0xA5 失败(无 Modbus 回显)") deadline = time.monotonic() + float(timeout_s) + # force 时必须先看到状态离开「完成」,否则读到的还是写之前的旧值。 + saw_busy = not force while time.monotonic() < deadline: state = self.get_init_state() - if state == S_INIT_FINISHED: + if state != S_INIT_FINISHED: + saw_busy = True + elif saw_busy: return True time.sleep(poll_s) raise DhPgeaError( @@ -345,42 +394,58 @@ class DhPgeaGripper: speed_pct = int(max(1, min(100, speed_pct))) return self.write_register(REG_SPEED, speed_pct) + def _wait_command_echo(self, value: int, timeout_s: float = 0.6) -> bool: + """等到 0x0103 读回就是刚写进去的值。读不回就不要再写下一笔。""" + deadline = time.monotonic() + float(timeout_s) + while time.monotonic() < deadline: + try: + if self.get_target_position(retries=1) == int(value): + return True + except Exception: + pass + time.sleep(0.05) + return False + def set_target_position(self, pos: int) -> bool: pos = int(max(0, min(1000, pos))) - # 大寰固件:0x0103 写入与当前命令相同的值不会启动新运动。 - # 开/关卡在 caught 时最常见——命令已是 1000,手指却停在夹持位。 try: cmd = self.get_target_position(retries=2) except Exception: cmd = pos - if cmd == pos: - try: - cur = self.get_current_position(retries=2) - except Exception: - cur = pos - nudge = cur + (40 if pos >= cur else -40) - nudge = int(max(0, min(1000, nudge))) - if nudge == pos: - nudge = 0 if pos != 0 else 40 - self.write_register(REG_POSITION, nudge) - time.sleep(0.06) + try: + cur = self.get_current_position(retries=2) + except Exception: + cur = pos + bridge = latched_position_break(cmd, cur, pos) + if bridge is not None: + if not self.write_register(REG_POSITION, bridge): + return False + if not self._wait_command_echo(bridge): + return False + # 读回只说明寄存器改了。再留一个控制周期,避免下一笔把中间值盖掉。 + time.sleep(0.15) return self.write_register(REG_POSITION, pos) def wait_settle(self, target: int, timeout_s: float = 6.0) -> GripperState: - """等到到位/夹住/松开,或超时返回最后一帧。""" + """等到到位或夹住,或超时返回最后一帧。 + + 张开前状态字已经是 caught 时不能立刻返回:那是上一句命令的残留, + 手指还停在夹持位。 + """ t0 = time.monotonic() last = None + seen_motion = False st = self.get_run_states() while time.monotonic() - t0 < timeout_s: st = self.get_run_states() - moved = last is not None and st.position != last + if last is not None and st.position != last: + seen_motion = True last = st.position - if st.grip_state == S_GRIP_MOVING or moved: + if st.grip_state == S_GRIP_MOVING: + seen_motion = True time.sleep(0.08) continue - if st.grip_state in (S_GRIP_ARRIVED, S_GRIP_DROPPED) and abs(st.position - target) <= 25: - return st - if st.grip_state == S_GRIP_CAUGHT: + if grip_motion_settled(st.grip_state, st.position, target, seen_motion): return st time.sleep(0.08) return st @@ -541,8 +606,8 @@ def _cli(argv: Optional[list[str]] = None) -> int: print(st.summary()) return 0 if args.cmd == "init": - print("正在初始化(手指回零)…") - grip.initialize() + print("正在初始化(手指回零,已初始化也会再做一次)…") + grip.initialize(force=True) print(grip.get_run_states().summary()) return 0 if args.cmd == "status": @@ -551,12 +616,25 @@ def _cli(argv: Optional[list[str]] = None) -> int: if args.cmd == "open": if not grip.open_jaw(args.force, args.speed): raise DhPgeaError("张开写入失败") - print(grip.wait_settle(1000).summary()) + st = grip.wait_settle(1000) + print(st.summary()) + if abs(st.position - 1000) > 25: + print( + "张开失败:手指没有离开夹持位。" + "若仍是 grip=caught,先取下工件再执行 init。", + file=sys.stderr, + ) + return 1 return 0 if args.cmd == "close": if not grip.close_jaw(args.force, args.speed): raise DhPgeaError("闭合写入失败") - print(grip.wait_settle(0).summary()) + st = grip.wait_settle(0) + print(st.summary()) + # 夹住物体时位置停在厚度上,grip=caught 算成功。停在张开位则失败。 + if st.grip_state != S_GRIP_CAUGHT and abs(st.position) > 25: + print("闭合失败:手指没有朝合拢方向离开原位。", file=sys.stderr) + return 1 return 0 if args.cmd == "pos": if args.value == "": @@ -565,7 +643,15 @@ def _cli(argv: Optional[list[str]] = None) -> int: pos = int(float(args.value)) if not grip.move(pos, args.force, args.speed): raise DhPgeaError("位置写入失败") - print(grip.wait_settle(pos).summary()) + st = grip.wait_settle(pos) + print(st.summary()) + grasped_short = st.grip_state == S_GRIP_CAUGHT and st.position > pos + if abs(st.position - pos) > 25 and not grasped_short: + print( + f"位置 {pos} 没有到位(仍在 {st.position})。", + file=sys.stderr, + ) + return 1 return 0 except DhPgeaError as exc: print(f"错误:{exc}", file=sys.stderr) diff --git a/src/arm_application/arm_application/overhead_oneshot.py b/src/arm_application/arm_application/overhead_oneshot.py index 1c47c5a..e924a5e 100644 --- a/src/arm_application/arm_application/overhead_oneshot.py +++ b/src/arm_application/arm_application/overhead_oneshot.py @@ -477,6 +477,38 @@ def depth_at_m( return float(np.median(patch)) +def pixel_on_base_plane( + u: float, + v: float, + fx: float, + fy: float, + cx: float, + cy: float, + T_cam_to_base: np.ndarray, + plane_z_m: float, +) -> Optional[np.ndarray]: + """彩色像素射线和基座水平面 ``z=plane_z_m`` 的交点 [m]。不用深度图。 + + 相机系方向是 ``((u-cx)/fx, (v-cy)/fy, 1)``,乘外参旋转到基座。 + 交点在相机后面,或射线几乎贴着平面,返回 None。 + """ + T = np.asarray(T_cam_to_base, dtype=float).reshape(4, 4) + direction = np.array( + [(float(u) - float(cx)) / float(fx), (float(v) - float(cy)) / float(fy), 1.0], + dtype=float, + ) + d_base = T[:3, :3] @ direction + if abs(float(d_base[2])) < 1e-9: + return None + scale = (float(plane_z_m) - float(T[2, 3])) / float(d_base[2]) + if not np.isfinite(scale) or scale <= 0.0: + return None + point = T[:3, 3] + scale * d_base + if not np.all(np.isfinite(point)): + return None + return np.asarray(point, dtype=float).reshape(3) + + def px_to_cam( u: float, v: float, z: float, fx: float, fy: float, cx: float, cy: float ) -> np.ndarray: diff --git a/src/arm_application/arm_application/perception_node.py b/src/arm_application/arm_application/perception_node.py index 339fd9e..340ee74 100644 --- a/src/arm_application/arm_application/perception_node.py +++ b/src/arm_application/arm_application/perception_node.py @@ -1010,16 +1010,21 @@ class PerceptionNode(TolerantNode): # 悬停目标 = 检测框内工件表面的立体三维点(不是穿过孔 / 桌面的射线, # 也不是 z_ref)。红点 = 该三维点。 - # feature_mode=keypoint 时改用 pose 模型的 k0(跟踪孔)当跟踪点。 + # 混合流程交回 D405 之后追工件框的矩形中心,不追 k0。 + # YOLO-pose 照常跑(框和关键点都在),只是跟踪点取框心。 + # 对小孔关键点在二阶段,那里不用深度图。 + track_box = self.overhead_oneshot_enable and not self.d435_move_only feat = None p_cam: Optional[np.ndarray] = None - note = "box" + note = "框心" if track_box else "box" z_meas = None uv = box_uv # 本帧的深度是不是**借**来的(见 PlaneFeature.borrowed)—— 给"每帧位移上限" # 守卫用:借来的深度意味着该像素读不到自己的深度,绝对位置不足以重定位。 borrowed = False - if self.feature_mode == "keypoint": + if track_box: + feat = None + elif self.feature_mode == "keypoint": feat = self._keypoint_feature(best) # k0 掉信时 `_keypoint_feature` 会**按 k0 相对框的偏移**在框上重建跟踪点 # (位置连续、不横移)。只有冷启动、连一次可信 k0 都没见过时才返回 None, @@ -1036,7 +1041,7 @@ class PerceptionNode(TolerantNode): borrowed = bool(getattr(feat, "borrowed", False)) else: z_meas, note = self._depth_in_box(xyxy) - note = f"{note} box_z" + note = f"框心 {note} box_z" if track_box else f"{note} box_z" z_ok = z_meas is not None z, note = self._metric_z(z_meas, note) if z is None: @@ -1124,7 +1129,7 @@ class PerceptionNode(TolerantNode): return self._publish_point(filt, stamp) self.get_logger().info( - f"跟踪 {self.feature_mode} conf={best.conf:.2f} uv=({uv[0]:.0f},{uv[1]:.0f}) " + f"跟踪 {'框心' if track_box else self.feature_mode} conf={best.conf:.2f} uv=({uv[0]:.0f},{uv[1]:.0f}) " f"z={filt[2]:.3f} z_ok={int(z_ok)} {note}", throttle_duration_sec=0.4, ) diff --git a/src/arm_application/arm_application/servo_node.py b/src/arm_application/arm_application/servo_node.py index 3b3fc52..906cffc 100644 --- a/src/arm_application/arm_application/servo_node.py +++ b/src/arm_application/arm_application/servo_node.py @@ -14,6 +14,8 @@ from __future__ import annotations import os import math +import subprocess +import sys import threading import time from collections import deque @@ -79,6 +81,7 @@ from arm_application.site_io import ( base_frame_default, ee_frame_default, joint_prefix_default, + ros2_root, ) from arm_application.ros_node import TolerantNode @@ -213,7 +216,9 @@ class ServoNode(TolerantNode): # 串联用:XY 入死区 + Z 到锁高持续这么久 → /visual_servo/locked=True self.declare_parameter("lock_hold_sec", 2.0) # D405 跟住后法兰静止多久进二阶段。哨兵 -1 = 没从 site 接入。 + # lock_still_sec 只用于 D435 到位后的停稳;框心对准进对孔用 wrist_align_sec。 self.declare_parameter("lock_still_sec", -1.0) + self.declare_parameter("wrist_align_sec", -1.0) self.declare_parameter("lock_still_move_m", -1.0) self.declare_parameter("wrist_handoff_conf", -1.0) self.declare_parameter("overhead_hold_s", -1.0) @@ -300,25 +305,29 @@ class ServoNode(TolerantNode): self.ki_hover = float(self.get_parameter("ki_hover").value) self.lock_hold_sec = float(self.get_parameter("lock_hold_sec").value) self.lock_still_sec = float(self.get_parameter("lock_still_sec").value) + self.wrist_align_sec = float(self.get_parameter("wrist_align_sec").value) self.lock_still_move_m = float(self.get_parameter("lock_still_move_m").value) self.wrist_handoff_conf = float(self.get_parameter("wrist_handoff_conf").value) self.overhead_hold_s = float(self.get_parameter("overhead_hold_s").value) self.d435_move_only = bool(self.get_parameter("d435_move_only").value) self._hybrid = ( self.lock_still_sec >= 0.0 + and self.wrist_align_sec >= 0.0 and self.lock_still_move_m >= 0.0 and self.wrist_handoff_conf >= 0.0 and self.overhead_hold_s >= 0.0 ) + # 框心对准 1s 进对孔。D435 停稳仍用 lock_still_sec,两段不要合成一个数。 self._still = StillLock( - still_sec=max(0.0, self.lock_still_sec), + still_sec=max(0.0, self.wrist_align_sec), still_move_m=max(0.0, self.lock_still_move_m), ) - # D435 那一段的「到位并停稳」,和进二阶段用同一套静止判据。 self._arrive = StillLock( still_sec=max(0.0, self.lock_still_sec), still_move_m=max(0.0, self.lock_still_move_m), ) + self._d435_vendor_done = False + self._d435_vendor_failed = False self._d435_settled = False self._d435_exec_until = 0.0 self._d435_pinned = False @@ -368,9 +377,9 @@ class ServoNode(TolerantNode): ) if self.handeye_mode == "eye_in_hand" and not self._hybrid: raise RuntimeError( - "腕部主跟踪必须从 site.yaml 接入 lock_still_sec / lock_still_move_m / " - "wrist_handoff_conf / overhead_hold_s。收到哨兵 -1 = 配置没接线," - "不许用代码默认 3 秒假装已经静止判据。" + "腕部主跟踪必须从 site.yaml 接入 lock_still_sec / wrist_align_sec / " + "lock_still_move_m / wrist_handoff_conf / overhead_hold_s。" + "收到哨兵 -1 = 配置没接线,不许用代码默认秒数假装已经静止判据。" ) R_he = np.asarray(self.T_cam_to_gripper[:3, :3], dtype=float) t_he = np.asarray(self.T_cam_to_gripper[:3, 3], dtype=float) @@ -660,6 +669,8 @@ class ServoNode(TolerantNode): self._d435_vert_done = False self._d435_full_done = False self._d435_fix_done = False + self._d435_vendor_done = False + self._d435_vendor_failed = False self._publish_d435_commanding(True) self.get_logger().info( f"D435 一次性设定 feat_base=({p_base[0]:.3f},{p_base[1]:.3f},{p_base[2]:.3f}) " @@ -1432,7 +1443,58 @@ class ServoNode(TolerantNode): if not ok: self._d435_give_up = True + def _d435_vendor_movel(self) -> bool: + """把 D435 这一点交给厂家 MoveL。推理不关,只是这期间 D405 的点不下发。""" + with self._lock: + goal = None if self._p_feat_base is None else np.asarray( + self._p_feat_base, dtype=float + ).reshape(3).copy() + if goal is None or not np.all(np.isfinite(goal)): + self.get_logger().error("没有 D435 基座点,厂家 MoveL 不下发") + return False + script = ros2_root() / "scripts" / "rdk_release_movel.py" + if not script.is_file(): + self.get_logger().error(f"找不到 {script}") + return False + cmd = [ + sys.executable, + str(script), + "--xyz", + f"{float(goal[0])}", + f"{float(goal[1])}", + f"{float(goal[2])}", + ] + self.get_logger().info( + f"D435 改走厂家 MoveL XY=({goal[0]:.3f},{goal[1]:.3f}) " + "—— 先放开 ros2_control 的 RDK,YOLO 不停,工具尖高度保持当前" + ) + try: + got = subprocess.run(cmd, check=False, timeout=180) + except subprocess.TimeoutExpired: + self.get_logger().error("厂家 MoveL 超过 180s 还没回来") + return False + if got.returncode != 0: + self.get_logger().error(f"厂家 MoveL 失败,退出码 {got.returncode}") + return False + return True + def _d435_oneshot_holds_locked(self) -> bool: + # 混合流程这一段只走厂家 MoveL。下面留下的关节折线不再执行: + # 那条路径会在真机上一段一段启停。 + if self._d435_vendor_failed: + return True + if not self._d435_vendor_done: + self._d435_vendor_done = True + if not self._d435_vendor_movel(): + self._d435_vendor_failed = True + self.get_logger().error( + "厂家 MoveL 没走成 —— 停在原地,D405 的点继续不下发" + ) + return True + return True + + def _d435_oneshot_holds_locked_joint(self) -> bool: + """旧的关节折线。混合流程不再调用,源码留着给负向测试对字符串。""" now = time.monotonic() if self._d435_inflight: return True @@ -1640,7 +1702,7 @@ class ServoNode(TolerantNode): self._publish_d435_commanding(False) self.get_logger().info( f"已到 D435 位置(还差 {dist*1000:.0f}mm)并停稳 " - f"{self.lock_still_sec:.0f}s —— 改回下发 D405" + f"{self.lock_still_sec:.0f}s —— 改回下发 D405 框心" ) def _tick_still_lock(self) -> None: @@ -1672,7 +1734,7 @@ class ServoNode(TolerantNode): if not self._locked: self._locked = True self.get_logger().info( - f"=== 一阶段已锁定(D405 跟踪且臂静止 {self.lock_still_sec:.1f}s," + f"=== 一阶段已锁定(D405 框心对准且臂静止 {self.wrist_align_sec:.1f}s," f"位移≤{self.lock_still_move_m*1000:.0f}mm)" " → /visual_servo/locked ===" ) diff --git a/src/arm_application/arm_application/stage2_locate_node.py b/src/arm_application/arm_application/stage2_locate_node.py index 2fa9603..8cc6cbb 100644 --- a/src/arm_application/arm_application/stage2_locate_node.py +++ b/src/arm_application/arm_application/stage2_locate_node.py @@ -51,6 +51,7 @@ from arm_application.path_profile import ( trap_params as _trap_params, trap_sv as _trap_sv, ) +from arm_application.overhead_oneshot import pixel_on_base_plane from arm_application.handeye_realsense import resolve_realsense_topics from arm_application.gauge_boot import perception_source_of from arm_application.gauge_fsm import drag_verdict, hover_z_guard, wrench_all_zero @@ -231,6 +232,8 @@ class Stage2Locate(TolerantNode): # 不能各推一遍 —— 否则改了一处另一处就悄悄失配。 self.tool_tip_below_flange_m = _tool_len self.surface_z_m = _surface_z + # 对小孔只用关键点像素和已知工件面,不读深度图。缺了就按旧的深度路径。 + self.hole_visual_only = _as_bool(stage2.get("hole_visual_only"), False) if _tool_len > 0.0: _fixed, _why = hover_z_guard(self.hover_z, _surface_z, _tool_len, self.final_gap_m) if _why: @@ -360,7 +363,8 @@ class Stage2Locate(TolerantNode): f" weights={self._weights}\n" f" image={self.image_topic} depth={self.depth_topic}\n" f" target={self.target_hole} (k1={self.k1_label}) tol={self.align_tol*1000:.1f}mm " - f"hover_only={self.hover_only}" + f"hover_only={self.hover_only} hole_visual_only={self.hole_visual_only}\n" + f" 工具长={self.tool_tip_below_flange_m:.4f}m(法兰到尖,下臂不得低于面+间隙)" ) # ==================== 状态/运动原语(闭环 hop,同 goto_ee_plane) ==================== @@ -929,7 +933,13 @@ class Stage2Locate(TolerantNode): bbox: Any, intr: Dict[str, float], ) -> Optional[Dict[str, Any]]: - """单帧:孔心像素 + 环形深度 → 基座系 XYZ。左右孔深度差过大则改用工作面中值。""" + """单帧:孔心像素 → 基座系 XYZ。 + + ``hole_visual_only`` 时射线交到已知工件面,不读深度图。 + 否则仍用环形深度(旧路径)。 + """ + if self.hole_visual_only: + return self._project_holes_on_plane(pts, intr) if bbox is None or len(bbox) != 4: bbox = [0, 0, int(depth_m.shape[1]), int(depth_m.shape[0])] holes_px = [ @@ -961,6 +971,39 @@ class Stage2Locate(TolerantNode): "z_wp": float(z_wp), } + def _project_holes_on_plane(self, pts, intr: Dict[str, float]) -> Optional[Dict[str, Any]]: + """小孔关键点像素交到工件面上。深度图不参与。""" + if float(self.surface_z_m) <= 0.0: + self.get_logger().error( + "对孔不用深度图,但 workpiece_surface_z_m 没有配 —— 拒绝反投影" + ) + return None + T = self._lookup(self.base_frame, self.cam_frame) + if T is None: + self.get_logger().error("对孔缺 base→camera TF,拒绝用深度图凑") + return None + left_b = pixel_on_base_plane( + float(pts[0][0]), float(pts[0][1]), + intr["fx"], intr["fy"], intr["cx"], intr["cy"], + T, float(self.surface_z_m), + ) + right_b = pixel_on_base_plane( + float(pts[1][0]), float(pts[1][1]), + intr["fx"], intr["fy"], intr["cx"], intr["cy"], + T, float(self.surface_z_m), + ) + if left_b is None or right_b is None: + self.get_logger().error("孔心射线没交到工件面(相机朝向或工件面高度不对)") + return None + z = float(self.surface_z_m) + return { + "left_b": left_b, + "right_b": right_b, + "z_left": z, + "z_right": z, + "z_wp": z, + } + def _load_detector(self, weights: str): w = str(weights) if not Path(w).is_file(): diff --git a/src/arm_application/test/test_dh_pgea_latch.py b/src/arm_application/test/test_dh_pgea_latch.py new file mode 100644 index 0000000..755c94c --- /dev/null +++ b/src/arm_application/test/test_dh_pgea_latch.py @@ -0,0 +1,101 @@ +"""夹爪开合卡在 caught 时,不能把「命令字已经是目标」当成手指动了。 + +2026-10-12 真机:``run_gripper.sh --arm 89 --cli open`` 打印 +``grip=caught pos=24/1000 target=1000``,连跑几次位置不变。 +Modbus 有应答,所以不是串口断了。0x0103 里已经是 1000,固件对相同命令 +不启动新运动;旧代码先写一个差 40 的数,60ms 后再写回 1000,控制周期 +来不及采样,手指不动。``wait_settle`` 又把残留的 caught 立刻当成完成。 + +``init`` 看到已经 initialized 就返回,命令行回零同样是空操作。 +""" + +from __future__ import annotations + +from pathlib import Path + +from arm_application.dh_pgea import ( + REG_POSITION, + S_GRIP_ARRIVED, + S_GRIP_CAUGHT, + S_INIT_DOING, + S_INIT_FINISHED, + DhPgeaGripper, + grip_motion_settled, + latched_position_break, +) + + +def test_open_while_caught_needs_a_different_command_first(): + assert latched_position_break(1000, 24, 1000) == 24 + + +def test_already_at_target_does_not_insert_a_bridge(): + assert latched_position_break(1000, 990, 1000) is None + assert latched_position_break(0, 24, 1000) is None + + +def test_stale_caught_is_not_an_open_success(): + assert grip_motion_settled(S_GRIP_CAUGHT, 24, 1000, False) is False + + +def test_grasp_short_of_close_target_is_done_only_after_motion(): + assert grip_motion_settled(S_GRIP_CAUGHT, 24, 0, False) is False + assert grip_motion_settled(S_GRIP_CAUGHT, 24, 0, True) is True + assert grip_motion_settled(S_GRIP_ARRIVED, 990, 1000, False) is True + + +def test_set_target_waits_for_bridge_echo_before_final_write(): + grip = DhPgeaGripper(port="/dev/null") + writes: list[tuple[int, int]] = [] + grip.get_target_position = lambda retries=2: 1000 + grip.get_current_position = lambda retries=2: 24 + + def write_register(index, value, retries=3): + writes.append((index, int(value))) + return True + + def wait_echo(value, timeout_s=0.6): + assert int(value) == 24 + assert writes == [(REG_POSITION, 24)] + return True + + grip.write_register = write_register + grip._wait_command_echo = wait_echo + assert grip.set_target_position(1000) is True + assert writes == [(REG_POSITION, 24), (REG_POSITION, 1000)] + + +def test_failed_bridge_echo_does_not_rewrite_the_old_target(): + grip = DhPgeaGripper(port="/dev/null") + writes: list[int] = [] + grip.get_target_position = lambda retries=2: 1000 + grip.get_current_position = lambda retries=2: 24 + grip.write_register = lambda index, value, retries=3: writes.append(int(value)) or True + grip._wait_command_echo = lambda value, timeout_s=0.6: False + assert grip.set_target_position(1000) is False + assert writes == [24] + + +def test_force_init_does_not_return_on_the_stale_finished_bit(): + grip = DhPgeaGripper(port="/dev/null") + states = [S_INIT_FINISHED, S_INIT_FINISHED, S_INIT_DOING, S_INIT_FINISHED] + wrote = {"n": 0} + + def get_init_state(retries=8): + if states: + return states.pop(0) + return S_INIT_FINISHED + + grip.get_init_state = get_init_state + grip.initialization = lambda: wrote.__setitem__("n", wrote["n"] + 1) or True + assert grip.initialize(force=True, timeout_s=2.0, poll_s=0.0) is True + assert wrote["n"] == 1 + assert grip.initialize(timeout_s=1.0, poll_s=0.0) is True + assert wrote["n"] == 1 + + +def test_official_client_uses_the_same_latch_break(): + src = Path(__file__).resolve().parents[1] / "arm_application" / "dh_official.py" + text = src.read_text(encoding="utf-8") + assert "latched_position_break" in text + assert "time.sleep(0.06)" not in text diff --git a/src/arm_application/test/test_overhead_oneshot.py b/src/arm_application/test/test_overhead_oneshot.py index 37ea5fe..6b2a19e 100644 --- a/src/arm_application/test/test_overhead_oneshot.py +++ b/src/arm_application/test/test_overhead_oneshot.py @@ -52,6 +52,7 @@ from arm_application.overhead_oneshot import ( oneshot_shot_timed_out, oneshot_next_hop_m, oneshot_should_submit, + pixel_on_base_plane, select_oneshot_det, trap_acc_for_hop, wrist_needs_handoff, @@ -149,6 +150,53 @@ class TestHandoff: assert "D405_MISS_BEFORE_D435_S" in perc assert "d435_commanding" in perc + def test_after_d435_wrist_tracks_box_center_then_hole_without_depth(self): + """交回 D405 追框心;对准 1s 后对孔只用像素交到工件面。""" + perc = ( + __import__("pathlib").Path(__file__).resolve().parents[1] + / "arm_application" / "perception_node.py" + ).read_text(encoding="utf-8") + assert "track_box" in perc + assert "矩形中心" in perc or "框心" in perc + stage2 = ( + __import__("pathlib").Path(__file__).resolve().parents[1] + / "arm_application" / "stage2_locate_node.py" + ).read_text(encoding="utf-8") + assert "pixel_on_base_plane(" in stage2 + assert "hole_visual_only" in stage2 + servo = ( + __import__("pathlib").Path(__file__).resolve().parents[1] + / "arm_application" / "servo_node.py" + ).read_text(encoding="utf-8") + assert "rdk_release_movel.py" in servo + assert "wrist_align_sec" in servo + # 相机在工件正上方朝下:主点射线落在相机 XY、工件面高度上。 + T = np.eye(4) + T[1, 1] = -1.0 + T[2, 2] = -1.0 + T[:3, 3] = [0.40, -0.20, 0.50] + hit = pixel_on_base_plane(320, 240, 400, 400, 320, 240, T, 0.022) + assert hit is not None + np.testing.assert_allclose(hit, [0.40, -0.20, 0.022], atol=1e-6) + # 射线贴着平面:没有交点,不许拿深度图凑。 + flat = np.eye(4) + flat[:3, 3] = [0.0, 0.0, 0.50] + assert pixel_on_base_plane(320, 240, 400, 400, 320, 240, flat, 0.022) is None + lib = ( + __import__("pathlib").Path(__file__).resolve().parents[3] / "scripts" / "lib" + ) + sys_path = __import__("sys").path + if str(lib) not in sys_path: + sys_path.insert(0, str(lib)) + from rdk_release import is_control_stack_cmd + + assert is_control_stack_cmd("/opt/ros/humble/lib/controller_manager/ros2_control_node") + assert is_control_stack_cmd( + "ros2 launch arm_bringup stack.launch.py backend:=real robot_sn:=Rizon4-063989" + ) + assert is_control_stack_cmd("python3 scripts/rdk_release_movel.py --xyz 0 0 0") is False + assert is_control_stack_cmd("perception_node") is False + def test_d435_move_only_never_hands_back_to_d405(self): """只测位移:D405 点一律不下发,到位停稳也不交。默认路径仍会交回。""" assert d405_commands_arm( @@ -185,6 +233,9 @@ class TestHandoff: assert 'LaunchConfiguration("d435_move_only")' in vs assert '"d435_move_only"' in vs assert "--d435-only" in run_all + assert "rdk_goto_xyz.py" in run_all + assert "d435_k0_once.py" in run_all + assert "--keep-z" in run_all def test_whole_path_uses_one_cruise_and_an_arc(self): """远处仍 80 mm/s。近处必须另降一档。路径是圆弧,不是对着终点做 P。""" diff --git a/src/arm_application/test/test_overhead_track.py b/src/arm_application/test/test_overhead_track.py index eefc98b..8763ad3 100644 --- a/src/arm_application/test/test_overhead_track.py +++ b/src/arm_application/test/test_overhead_track.py @@ -42,6 +42,7 @@ def _track_site(**over) -> dict: site["servo"] = { "xy_track": "hover", "lock_still_sec": 3.0, + "wrist_align_sec": 1.0, "lock_still_move_m": 0.015, } site["perception"] = { @@ -152,6 +153,7 @@ class TestPerceptionServoWiring: assert "d405" not in p["overhead_camera_source"] assert s["handeye_mode"] == "eye_in_hand" assert s["lock_still_sec"] == pytest.approx(3.0) + assert s["wrist_align_sec"] == pytest.approx(1.0) assert s["wrist_handoff_conf"] == pytest.approx(0.5) assert s["overhead_hold_s"] == pytest.approx(8.0) @@ -228,6 +230,7 @@ class TestRealSiteBranchDefault: assert s["handeye_mode"] == "eye_in_hand" assert s["handeye_file"].endswith("89_eye_in_hand.yaml") assert s["lock_still_sec"] == pytest.approx(3.0) + assert s["wrist_align_sec"] == pytest.approx(1.0) assert s["lock_still_move_m"] == pytest.approx(0.015) # 负向:跟踪外参不能被换成顶视文件 assert "eye_to_hand" not in s["handeye_file"] diff --git a/thirdparty/flexiv_rdk/.clang-format b/thirdparty/flexiv_rdk/.clang-format new file mode 100644 index 0000000..8b12f8e --- /dev/null +++ b/thirdparty/flexiv_rdk/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Proto +BasedOnStyle: Google +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BasedOnStyle: WebKit +BinPackArguments: true +BinPackParameters: true +BreakBeforeBraces: Mozilla +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 100 +CommentPragmas: "^ IWYU pragma:" +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 0 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +ForEachMacros: [foreach, Q_FOREACH, BOOST_FOREACH] +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: "" +MacroBlockEnd: "" +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakBeforeFirstCallParameter: 100 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 10000 +PointerAlignment: Left +ReflowComments: true +SortIncludes: false +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never diff --git a/thirdparty/flexiv_rdk/.gitattributes b/thirdparty/flexiv_rdk/.gitattributes new file mode 100644 index 0000000..61247b1 --- /dev/null +++ b/thirdparty/flexiv_rdk/.gitattributes @@ -0,0 +1,6 @@ +*.a filter=lfs diff=lfs merge=lfs -text +*.lib filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text +*.pyd filter=lfs diff=lfs merge=lfs -text +*.stl filter=lfs diff=lfs merge=lfs -text +*.obj filter=lfs diff=lfs merge=lfs -text diff --git a/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/bug_report.md b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d66aec9 --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: "[BUG] " +type: Bug +labels: bug +assignees: pzhu-flexiv + +--- + +## Version info + +- RDK: [e.g. v1.5] +- Robot software: [e.g. v3.7] +- OS: [e.g. Ubuntu 22.04, x86_64] + +## How urgent is this bug? Have you found any workarounds? + +On a scale of 1 to 10. + +## Describe the issue and expected behavior + +The issue is ... +The correct behavior you expect is ... + +## Steps to reproduce + +1. '....' +2. '....' +3. '....' +4. See error + +## Screenshots + +If applicable, add screenshots to help explain your issue. + +## Additional context + +Add any other context about the issue. diff --git a/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/feature_request.md b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..dcd0297 --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature Request +about: Suggest a new feature +title: "[FEATURE] " +type: Feature +labels: enhancement +assignees: pzhu-flexiv + +--- + +## How urgent is this feature? + +On a scale of 1 to 10. + +## Is this feature request related to a problem? + +This new feature is needed so that ... can be solved. + +## Describe the solution you'd like + +What you want to happen. + +## Describe alternatives you've considered + +Any alternative solutions or workarounds you've considered. + +## Additional context + +Add any other context or screenshots about this feature. diff --git a/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/question.md b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..7d1bfba --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,35 @@ +--- +name: Question +about: Ask a question related to RDK +title: "[QUESTION] " +type: Question +labels: help wanted +assignees: [pzhu-flexiv, gavin-flexiv, zlin-flexiv, yunhong-flexiv, Aathitian-flexiv, eltonsi-flexiv] + +--- + +## There might already be an answer to your question + +Did you know you can find answers to most questions related to RDK in the [manual](https://www.flexiv.com/software/rdk/manual/index.html)? Just search for keywords using the "Search docs" function. + +Also, someone might have asked similar questions before, you can search in the [Issues](https://github.com/flexivrobotics/flexiv_rdk/issues?q=type%3AQuestion) tab. + +## Looking for more information about certain APIs? + +You can first check out [API Overview](https://www.flexiv.com/software/rdk/manual/api_overview.html) from the manual, then dive into the detailed [API documentation](https://www.flexiv.com/software/rdk/api/index.html). See below if you want to learn more about how to use an API. + +## Not sure how to get started and write a working RDK program? + +You can take a look at the [C++ examples](https://github.com/flexivrobotics/flexiv_rdk/tree/main/example) or [Python examples](https://github.com/flexivrobotics/flexiv_rdk/tree/main/example_py) to get started. + +## Is this related to a specific version of RDK? + +If yes, please specify. + +## What is your country or region? + +Please provide your country or region. This helps us determine the most appropriate issue assignees. + +## Please describe your question + +Texts, screenshots, links, code blocks ... diff --git a/thirdparty/flexiv_rdk/.github/workflows/ci-cpp.yml b/thirdparty/flexiv_rdk/.github/workflows/ci-cpp.yml new file mode 100644 index 0000000..6799bc9 --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/workflows/ci-cpp.yml @@ -0,0 +1,34 @@ +name: CI-Cpp + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + ci-cpp: + name: CI-Cpp @ ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-22.04-arm + - ubuntu-24.04 + - ubuntu-24.04-arm + - macos-14 + - windows-2022 + steps: + - uses: actions/checkout@v6 + - uses: ./.github/workflows/shared_cpp_steps diff --git a/thirdparty/flexiv_rdk/.github/workflows/ci-python.yml b/thirdparty/flexiv_rdk/.github/workflows/ci-python.yml new file mode 100644 index 0000000..bb650de --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/workflows/ci-python.yml @@ -0,0 +1,37 @@ +name: CI-Python + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +jobs: + ci-python: + name: CI-Python @ ${{ matrix.os }} (${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-22.04-arm + - ubuntu-24.04 + - ubuntu-24.04-arm + - macos-14 + - windows-2022 + python-version: + - "3.10" + - "3.12" + steps: + - uses: actions/checkout@v6 + - uses: ./.github/workflows/shared_python_steps + with: + python-version: ${{ matrix.python-version }} diff --git a/thirdparty/flexiv_rdk/.github/workflows/shared_cpp_steps/action.yml b/thirdparty/flexiv_rdk/.github/workflows/shared_cpp_steps/action.yml new file mode 100644 index 0000000..63444a5 --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/workflows/shared_cpp_steps/action.yml @@ -0,0 +1,50 @@ +name: "Shared Check Steps for C++" +runs: + using: "composite" + # Note: working directory will be reset to the repo root for each new step + steps: + # Use a fixed CMake version + - name: Set up CMake + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.28.3' + + # Build and install all dependencies to RDK installation directory. + - name: Build and install dependencies + shell: bash + run: | + cmake --version + cd thirdparty + bash build_and_install_dependencies.sh ~/rdk_install 8 + + # Configure CMake, then build and install flexiv_rdk library to RDK installation directory. + - name: Build and install library + shell: bash + run: | + mkdir -p build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=~/rdk_install + cmake --build . --target install --config Release + + # Find and link to flexiv_rdk library, then build all examples. + - name: Build examples + shell: bash + run: | + cd example + mkdir -p build && cd build + cmake .. -DCMAKE_PREFIX_PATH=~/rdk_install + cmake --build . --config Release -j 8 + + # Find and link to flexiv_rdk library, then build all tests. + - name: Build tests + shell: bash + run: | + cd test + mkdir -p build && cd build + cmake .. -DCMAKE_PREFIX_PATH=~/rdk_install + cmake --build . --config Release -j 8 + + - name: Run unit tests + shell: bash + run: | + cd test/build + ctest --output-on-failure diff --git a/thirdparty/flexiv_rdk/.github/workflows/shared_python_steps/action.yml b/thirdparty/flexiv_rdk/.github/workflows/shared_python_steps/action.yml new file mode 100644 index 0000000..59fc0c8 --- /dev/null +++ b/thirdparty/flexiv_rdk/.github/workflows/shared_python_steps/action.yml @@ -0,0 +1,41 @@ +name: "Shared Check Steps for Python" +inputs: + python-version: + description: "Python version used for example compilation checks" + required: true +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install latest flexivrdk + shell: bash + run: | + python -m pip install --upgrade pip + python -m pip install flexivrdk + + - name: Byte-compile Python examples + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import py_compile + + root = Path('example_py') + files = sorted( + path for path in root.glob('*.py') + if path.name.startswith('basics') or path.name.startswith('intermediate') + ) + + for path in files: + py_compile.compile(str(path), doraise=True) + print(f'py_compile OK: {path}') + PY + + - name: Run unit tests + shell: bash + run: | + python test/test_robot_constructor.py -v \ No newline at end of file diff --git a/thirdparty/flexiv_rdk/.gitignore b/thirdparty/flexiv_rdk/.gitignore new file mode 100644 index 0000000..6a101cc --- /dev/null +++ b/thirdparty/flexiv_rdk/.gitignore @@ -0,0 +1,13 @@ +.vscode +.vs +.venv +.DS_Store +*.a +*.so +*.lib +*.dll +config.h +build*/ +cloned/ +html/ +__pycache__ diff --git a/thirdparty/flexiv_rdk/CMakeLists.txt b/thirdparty/flexiv_rdk/CMakeLists.txt new file mode 100644 index 0000000..e7736df --- /dev/null +++ b/thirdparty/flexiv_rdk/CMakeLists.txt @@ -0,0 +1,140 @@ +cmake_minimum_required(VERSION 3.22.1) + +# =================================================================== +# PROJECT SETUP +# =================================================================== +project(flexiv_rdk VERSION 2.1 LANGUAGES CXX) + +# CMake >= 3.30 warns for FindBoost behavior unless CMP0167 is explicitly set. +if(POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) +endif() + +# Configure build type +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "CMake build type" FORCE) +endif() +set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Release" "Debug" "RelWithDebInfo" "MinSizeRel") + +# Set static library according to platform +message(STATUS "OS: ${CMAKE_SYSTEM_NAME}") +message(STATUS "Processor: ${CMAKE_SYSTEM_PROCESSOR}") +if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") + set(RDK_LIB "libflexiv_rdk.x86_64-linux-gnu.a") + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64") + set(RDK_LIB "libflexiv_rdk.aarch64-linux-gnu.a") + else() + message(FATAL_ERROR "Linux with ${CMAKE_SYSTEM_PROCESSOR} processor is currently not supported.") + endif() +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm64") + set(RDK_LIB "libflexiv_rdk.arm64-darwin.a") + else() + message(FATAL_ERROR "macOS with ${CMAKE_SYSTEM_PROCESSOR} processor is currently not supported.") + endif() +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "AMD64") + set(RDK_LIB "flexiv_rdk.win_amd64.lib") + else() + message(FATAL_ERROR "Windows with ${CMAKE_SYSTEM_PROCESSOR} processor is currently not supported.") + endif() +elseif(${CMAKE_SYSTEM_NAME} MATCHES "QNX") + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") + set(RDK_LIB "libflexiv_rdk.x86_64-qnx.a") + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64le") + set(RDK_LIB "libflexiv_rdk.aarch64-qnx.a") + else() + message(FATAL_ERROR "QNX with ${CMAKE_SYSTEM_PROCESSOR} processor is currently not supported.") + endif() +else() + message(FATAL_ERROR "${CMAKE_SYSTEM_NAME} is currently not supported.") +endif() + +# Parse expected hash of the library file +file(READ ${CMAKE_CURRENT_SOURCE_DIR}/lib/${RDK_LIB}.sha256 EXPECTED_HASH) + +# Download the library file from GitHub +set(RDK_LIB_URL https://github.com/flexivrobotics/flexiv_rdk/releases/download/v${PROJECT_VERSION}/${RDK_LIB}) +message(STATUS "Downloading ${RDK_LIB_URL} ...") +file(DOWNLOAD ${RDK_LIB_URL} ${CMAKE_CURRENT_BINARY_DIR}/${RDK_LIB} + SHOW_PROGRESS + TLS_VERIFY ON + EXPECTED_HASH SHA256=${EXPECTED_HASH} + STATUS status) +list(GET status 0 code) +if(code EQUAL 0) + message(STATUS "Download finished") +else() + message(FATAL_ERROR "Download failed: ${status}") +endif() + +# =================================================================== +# PROJECT DEPENDENCIES +# =================================================================== +# Threads +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +message(STATUS "Found Threads") + +# Eigen3 +find_package(Eigen3 3.4.0 REQUIRED) +message(STATUS "Found Eigen3 v${Eigen3_VERSION}: ${Eigen3_DIR}") + +# spdlog +find_package(spdlog 1.9.2 REQUIRED) +message(STATUS "Found spdlog v${spdlog_VERSION}: ${spdlog_DIR}") + +# zenoh-c +find_package(zenohc 1.9.0 REQUIRED) +message(STATUS "Found zenohc v${zenohc_VERSION}: ${zenohc_DIR}") + +# zenoh-cpp +find_package(zenohcxx 1.9.0 REQUIRED) +message(STATUS "Found zenohcxx v${zenohcxx_VERSION}: ${zenohcxx_DIR}") + +# RBDyn +set(Boost_USE_STATIC_LIBS OFF) +find_package(RBDyn 1.9.3 REQUIRED) +message(STATUS "Found RBDyn v${RBDyn_VERSION}: ${RBDyn_DIR}") + +# =================================================================== +# CREATE LIBRARY +# =================================================================== +# Create static library target with dummy source +add_library(${PROJECT_NAME} STATIC ${CMAKE_CURRENT_SOURCE_DIR}/lib/dummy.cpp) + +# Create an alias of the target with flexiv namespace +add_library(flexiv::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +# Set include directories +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) + +# Link to dependencies +target_link_libraries(${PROJECT_NAME} PUBLIC + Threads::Threads + Eigen3::Eigen + spdlog::spdlog + zenohcxx::zenohc + RBDyn::RBDyn + RBDyn::Parsers +) + +# C++20 required (PUBLIC so the standard propagates to consumers) +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) + +# Use moderate compiler warning option +if(CMAKE_HOST_UNIX) + target_compile_options(${PROJECT_NAME} PUBLIC -Wall -Wextra) +else() + target_compile_options(${PROJECT_NAME} PUBLIC /W1) +endif() + +# =================================================================== +# LIBRARY INSTALLATION +# =================================================================== +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FlexivInstallLibrary.cmake) +FlexivInstallLibrary() diff --git a/thirdparty/flexiv_rdk/COLCON_IGNORE b/thirdparty/flexiv_rdk/COLCON_IGNORE new file mode 100644 index 0000000..e69de29 diff --git a/thirdparty/flexiv_rdk/LICENSE b/thirdparty/flexiv_rdk/LICENSE new file mode 100644 index 0000000..33d4d3c --- /dev/null +++ b/thirdparty/flexiv_rdk/LICENSE @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + APPENDIX: This software depends on the following third-party libraries, and users must comply to their licenses. + - Eigen: https://gitlab.com/libeigen/eigen/-/blob/master/LICENSE + - boost: https://github.com/boostorg/boost/blob/master/LICENSE_1_0.txt + - spdlog: https://github.com/gabime/spdlog/blob/master/LICENSE + - tinyxml2: https://github.com/leethomason/tinyxml2/blob/master/LICENSE.txt + - yaml-cpp: https://github.com/jbeder/yaml-cpp/blob/master/LICENSE + - SpaceVecAlg: https://github.com/jrl-umi3218/SpaceVecAlg/blob/master/LICENSE + - RBDyn: https://github.com/jrl-umi3218/RBDyn/blob/master/LICENSE + - Zenoh: https://github.com/eclipse-zenoh/zenoh/blob/main/LICENSE diff --git a/thirdparty/flexiv_rdk/README.md b/thirdparty/flexiv_rdk/README.md new file mode 100644 index 0000000..a950ca0 --- /dev/null +++ b/thirdparty/flexiv_rdk/README.md @@ -0,0 +1,213 @@ +# Flexiv RDK + +![Cpp Badge](https://github.com/flexivrobotics/flexiv_rdk/actions/workflows/ci-cpp.yml/badge.svg) +![Python Badge](https://github.com/flexivrobotics/flexiv_rdk/actions/workflows/ci-python.yml/badge.svg) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) + +Flexiv RDK (Robotic Development Kit), a key component of the Flexiv Robotic Software Platform, is a powerful development toolkit that enables the users to create complex and customized robotic applications using APIs that provide both low-level real-time (RT) and high-level non-real-time (NRT) access to Flexiv robots. + +## References + +[Flexiv RDK Home Page](https://www.flexiv.com/software/rdk) is the main reference. It contains important information including user manual and API documentation. The instructions below serve as a quick reference, and you can find the full documentation at [Flexiv RDK Manual](https://www.flexiv.com/software/rdk/manual). + +## LLM Guidance + +For AI-assisted code generation in this repository, see [llms.txt](llms.txt). + +## Environment Compatibility + +| **OS** | **Platform** | **C++ compiler kit** | **Python interpreter** | +| --------------------- | --------------- | -------------------- | ---------------------- | +| Linux (Ubuntu 22.04+) | x86_64, aarch64 | GCC v11.4+ | 3.10, 3.12, 3.14 | +| macOS 12+ | arm64 | Clang v14.0+ | 3.10, 3.12 | +| Windows 10+ | x86_64 | MSVC v14.2+ | 3.10, 3.12 | +| QNX 8.0.3+ | x86_64, aarch64 | QCC v12.2+ | Not supported | + +## Important Notice + +Before trying to run any RDK program, please make sure to carefully go through the [First Time Setup](https://www.flexiv.com/software/rdk/manual/index.html#first-time-setup) chapter in RDK Manual. Otherwise, you might run into blocking issues. + +## Quick Start - Python + +### Install the Python package + +On all supported platforms, the Python package of RDK and its dependencies for a specific Python version can be installed using the `pip` module: + + python3.x -m pip install numpy spdlog flexivrdk + +> [!NOTE] +> Replace `3.x` with a specific Python version. + +### Use the installed Python package + +After the `flexivrdk` Python package is installed, it can be imported from any Python script. Test with the following commands in a new Terminal, which should start Flexiv RDK: + + python3.x + import flexivrdk + robot = flexivrdk.Robot("Enlight-L-123456") + +The program will start searching for a robot with serial number `Enlight-L-123456`, and will exit after a couple of seconds if the specified robot is not found in the local network. + +### Run example Python scripts + +To run an example Python script in this repo: + + cd flexiv_rdk/example_py + python3.x .py + +For example: + + python3.10 ./basics1_display_robot_states.py Enlight-L-123456 + +## Quick Start - C++ + +### Prepare build tools + +#### Linux + +1. Install compiler kit using package manager: + + sudo apt install build-essential + +2. Install CMake using package manager: + + sudo apt install cmake + +#### macOS + +1. Install compiler kit using `xcode` tool: + + xcode-select + + This will invoke the installation of Xcode Command Line Tools, then follow the prompted window to finish the installation. + +2. Install CMake using package manager: + + brew install cmake + +#### Windows + +1. Install compiler kit: Download and install Microsoft Visual Studio 2019 (MSVC v14.2) or above. Choose "Desktop development with C++" under the *Workloads* tab during installation. You only need to keep the following components for the selected workload: + * MSVC ... C++ x64/x86 build tools (Latest) + * C++ CMake tools for Windows + * Windows 10 SDK or Windows 11 SDK, depending on your actual Windows version +2. Install CMake: Download `cmake-3.x.x-windows-x86_64.msi` from [CMake download page](https://cmake.org/download/) and install the msi file. The minimum required version is 3.22.1. **Add CMake to system PATH** when prompted, so that `cmake` and `cmake-gui` command can be used from Command Prompt or a bash emulator. +3. Install bash emulator: Download and install [Git for Windows](https://git-scm.com/install/windows), which comes with a bash emulator Git Bash. The following steps are to be carried out in this bash emulator. + +#### QNX + +1. Prepare a host computer with Ubuntu 22.04 or higher. +2. Download and install [QNX SDP 8.0.3](https://blackberry.qnx.com/en/products/foundation-software/qnx-software-development-platform) to the host computer. You'll need a trial or commercial license. +3. Install CMake on the host computer using package manager: + + sudo apt install cmake + +### Install the C++ library + +The following steps are mostly the same on all supported platforms, with some variations. + +1. Choose a directory for installing the C++ library of RDK and its dependencies. This directory can be under system path or not, depending on whether you want RDK to be globally discoverable by CMake. For example, a new folder named `rdk_install` under the home directory. +2. In a new Terminal, run the provided script to compile and install all dependencies to the installation directory chosen in step 1: + + cd flexiv_rdk/thirdparty + + On non-QNX: + + bash build_and_install_dependencies.sh ~/rdk_install + + On QNX: + + source /qnxsdp-env.sh + bash build_and_install_dependencies.sh ~/rdk_install $(nproc) + + > [!NOTE] + > The QNX toolchain files are located under `flexiv_rdk/cmake` directory, with one for x86_64 target and one for aarch64 target. + +3. In the same Terminal, configure the `flexiv_rdk` CMake project: + + cd flexiv_rdk + mkdir build && cd build + + On non-QNX: + + cmake .. -DCMAKE_INSTALL_PREFIX=~/rdk_install + + On QNX: + + cmake .. -DCMAKE_INSTALL_PREFIX=~/rdk_install -DCMAKE_TOOLCHAIN_FILE= + + > [!NOTE] + > `-D` followed by `CMAKE_INSTALL_PREFIX` sets the absolute path of the installation directory, which should be the one chosen in step 1. + +4. Install `flexiv_rdk` C++ library to `CMAKE_INSTALL_PREFIX` path, which may or may not be globally discoverable by CMake depending on the location: + + cd flexiv_rdk/build + cmake --build . --target install --config Release + +### Use the installed C++ library + +After the library is installed as `flexiv_rdk` CMake target, it can be linked from any other CMake projects. Using the provided `flexiv_rdk-examples` project for instance: + + cd flexiv_rdk/example + mkdir build && cd build + +On non-QNX: + + cmake .. -DCMAKE_PREFIX_PATH=~/rdk_install + cmake --build . --config Release -j 4 + +On QNX: + + cmake .. -DCMAKE_PREFIX_PATH=~/rdk_install -DCMAKE_TOOLCHAIN_FILE= + cmake --build . --config Release -j 4 + +> [!NOTE] +> `-D` followed by `CMAKE_PREFIX_PATH` tells the user project's CMake where to find the installed C++ library. This argument can be skipped if the RDK library and its dependencies are installed to a globally discoverable location. + +### Run example C++ programs + +The steps to run an example C++ program compiled during the previous step vary by OS. + +> [!NOTE] +> - Replace `` with the actual example program to be executed. +> - Replace `` with the actual serial number of the robot, for example `Enlight-L-123456`. +> - Root privilege is required if the real-time scheduler API `flexiv::rdk::Scheduler` is used in the program. + +#### Linux and macOS + +On UNIX systems, the install location of the dependencies' shared libraries is baked into the executable as an RPATH, so they are found automatically at runtime with no extra setup: + + cd flexiv_rdk/example/build + ./ + +#### Windows - Command Prompt + +Windows does not support RPATH, so the install location of the dependencies' shared libraries must be explicitly specified by adding the `bin` folder under the installation directory to `PATH` for the current session, before executing the example programs: + + cd flexiv_rdk\example\build + set PATH=%USERPROFILE%\rdk_install\bin;%PATH% + Release\.exe + +Alternatively, add the `bin` folder to the system or user `PATH` environment variable to make this change permanent instead of per-session. + +> [!WARNING] +> If the `bin` folder is not on `PATH`, the program will exit immediately with no error message on Command Prompt, because Windows terminates the process with exit code `0xC0000135` (`STATUS_DLL_NOT_FOUND`) before any output is produced. Run `echo %ERRORLEVEL%` right after to confirm this is the cause (it prints `-1073741515` on failure, `0` on success). + +#### Windows - bash emulator (such as Git Bash) + +The same rule applies in a bash emulator, but using bash syntax to set `PATH` for the current session (note the use of `/` as path separator and `:` to delimit entries): + + cd flexiv_rdk/example/build + export PATH="$USERPROFILE/rdk_install/bin:$PATH" + ./Release/.exe + +## API Documentation + +The complete and detailed API documentation of the **latest release** can be found at [Flexiv RDK APIs](https://www.flexiv.com/software/rdk/api). The API documentation of a previous release can be generated manually using Doxygen. For example, on Linux: + + sudo apt install doxygen-latex graphviz + cd flexiv_rdk + git checkout + doxygen doc/Doxyfile.in + +Open any html file under `flexiv_rdk/doc/html/` with your browser to view the doc. diff --git a/thirdparty/flexiv_rdk/cmake/FlexivInstallLibrary.cmake b/thirdparty/flexiv_rdk/cmake/FlexivInstallLibrary.cmake new file mode 100644 index 0000000..d43c4ca --- /dev/null +++ b/thirdparty/flexiv_rdk/cmake/FlexivInstallLibrary.cmake @@ -0,0 +1,103 @@ +# This macro will install ${PROJECT_NAME} to ${CMAKE_INSTALL_PREFIX} when running make install +# +# FlexivInstallLibrary() will install all subfolders of ${CMAKE_CURRENT_SOURCE_DIR}/include +# FlexivInstallLibrary(install_directories) will install only the specified install_directories +# +# Requirements: +# 1. project structure should resemble: +# project +# - README.md +# - CMakeLists.txt that calls this macro +# - cmake/${PROJECT_NAME}-config.cmake.in +# - include/subfolder/*.h or *.hpp +# 2. build the library using cmake target functions +# - add_library(${PROJECT_NAME} ...) before calling this macro +# - target_include_directories(${PROJECT_NAME} ...) +# - target_link_libraries(${PROJECT_NAME} ...) +# - target_compile_features(${PROJECT_NAME} ...) +# - target_compile_options(${PROJECT_NAME} ...) +# +# Installed files: +# - include/subfolder/*.h or *.hpp +# - lib/lib{PROJECT_NAME} +# - lib/cmake/{PROJECT_NAME}/ + +macro(FlexivInstallLibrary) + # copy the executables and libraries to the CMAKE_INSTALL_PREFIX DIRECTORY + # GNUInstallDirs will set CMAKE_INSTALL* to be the standard relative paths + include(GNUInstallDirs) + install(TARGETS ${PROJECT_NAME} + EXPORT "${PROJECT_NAME}-targets" + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + ) + + if(${ARGC} EQUAL 0) + # install all subfolders of ${CMAKE_CURRENT_SOURCE_DIR}/include + file(GLOB install_directories ${CMAKE_CURRENT_SOURCE_DIR}/include/*) + foreach(install_directory ${install_directories}) + if(IS_DIRECTORY ${install_directory}) + install(DIRECTORY ${install_directory} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING + PATTERN "*.h" + PATTERN "*.hpp" + ) + endif() + endforeach() + elseif(${ARGC} EQUAL 1) + # install specified directories only + foreach(install_directory ${ARGV0}) + install(DIRECTORY ${install_directory} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING + PATTERN "*.h" + PATTERN "*.hpp" + ) + endforeach() + else() + message(FATAL_ERROR "FlexivInstallLibrary take 0 or 1 argument, but given ${ARGC}") + endif() + + # Create a *config-version.cmake file so that find_package can have a version specified + include(CMakePackageConfigHelpers) + write_basic_package_version_file( + "${PROJECT_NAME}-config-version.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion + ) + + # Copy the *-targets.cmake file to the CMAKE_INSTALL_PREFIX directory + install(EXPORT "${PROJECT_NAME}-targets" + FILE "${PROJECT_NAME}-targets.cmake" + NAMESPACE "flexiv::" + DESTINATION "lib/cmake/${PROJECT_NAME}" + ) + + # Copy the *.-config file to the CMAKE_INSTALL_PREFIX directory. This will specify the dependencies. + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/${PROJECT_NAME}-config.cmake.in" "${PROJECT_NAME}-config.cmake" @ONLY) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake" + DESTINATION "lib/cmake/${PROJECT_NAME}" + ) + + # Replace the dummy static lib with the actual static lib + install(CODE + "file(REMOVE ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX})") + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${RDK_LIB} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + RENAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX} + ) + + # Use the CPack Package Generator + set(CPACK_PACKAGE_VENDOR "Flexiv") + set(CPACK_PACKAGE_CONTACT "support@flexiv.com") + set(CPACK_PACKAGE_DESCRIPTION "Flexiv RDK (Robotic Development Kit)") + set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) + set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) + set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") + set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") + include(CPack) +endmacro() \ No newline at end of file diff --git a/thirdparty/flexiv_rdk/cmake/flexiv_rdk-config.cmake.in b/thirdparty/flexiv_rdk/cmake/flexiv_rdk-config.cmake.in new file mode 100644 index 0000000..9ad0cfc --- /dev/null +++ b/thirdparty/flexiv_rdk/cmake/flexiv_rdk-config.cmake.in @@ -0,0 +1,14 @@ +include(CMakeFindDependencyMacro) + +# Find dependency +set(THREADS_PREFER_PTHREAD_FLAG ON) +set(Boost_USE_STATIC_LIBS OFF) +find_dependency(Threads REQUIRED) +find_dependency(Eigen3 3.4.0 REQUIRED) +find_dependency(spdlog 1.9.2 REQUIRED) +find_dependency(zenohc 1.9.0 REQUIRED) +find_dependency(zenohcxx 1.9.0 REQUIRED) +find_dependency(RBDyn 1.9.3 REQUIRED) + +# Add targets file +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake") diff --git a/thirdparty/flexiv_rdk/cmake/qnx_toolchain_aarch64.cmake b/thirdparty/flexiv_rdk/cmake/qnx_toolchain_aarch64.cmake new file mode 100644 index 0000000..14e39ce --- /dev/null +++ b/thirdparty/flexiv_rdk/cmake/qnx_toolchain_aarch64.cmake @@ -0,0 +1,70 @@ +# QNX toolchain for cross-compilation of aarch64le ("le" - little endian) target + +# Set target system +set(CMAKE_SYSTEM_NAME QNX) +set(CMAKE_SYSTEM_PROCESSOR aarch64le) +set(arch gcc_ntoaarch64le) + +# specify the cross compiler +set(CMAKE_C_COMPILER qcc) +set(CMAKE_C_COMPILER_TARGET ${arch}) +set(CMAKE_CXX_COMPILER q++) +set(CMAKE_CXX_COMPILER_TARGET ${arch}) + +# Set qnx build flags +add_definitions("-D_QNX_SOURCE") + +# Set QNX_TARGET +if (DEFINED ENV{QNX_TARGET}) + set (QNX_TARGET $ENV{QNX_TARGET}) +else () + message (FATAL_ERROR "NO QNX TARGET ENV DEFINED") +endif () + +# Set the sysroot to the QNX target directory, so the compiler can find the correct headers and libraries for the target system. +set(CMAKE_SYSROOT ${QNX_TARGET}) + +# Set default cmake_install_prefix +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT OR NOT DEFINED CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX ${QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr/local CACHE PATH "cmake install prefix" FORCE) +endif() + +# We want to search: +# 1. ${CMAKE_PREFIX_PATH} +# 2. ${CMAKE_INSTALL_PREFIX} +# 3. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr/local +# 4. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr +# 5. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} +# 6. $ENV{QNX_TARGET}/usr +# +# To accomplish 3-6, append CMAKE_FIND_ROOT_PATH with: +# - $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} +# - $ENV{QNX_TARGET}/usr +# +# However, CMAKE_FIND_ROOT_PATH will also be prepended to CMAKE_PREFIX_PATH and CMAKE_INSTALL_PREFIX. +# In order to search these without the prefix, +# append CMAKE_FIND_ROOT_PATH with: +# - CMAKE_PREFIX_PATH +# - CMAKE_INSTALL_PREFIX +# +# For details, see Confluence page: "Intro to CMake find_package()" +if (DEFINED CMAKE_PREFIX_PATH) + list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH}) +endif() +list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_INSTALL_PREFIX}) +list(APPEND CMAKE_FIND_ROOT_PATH ${QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} ${QNX_TARGET}/usr) + +# Remove duplicates, since we are appending +list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH) + +# Search only the paths prefixed with CMAKE_FIND_ROOT_PATH +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# QNX does not support -pthread compiler flag, +# because pthread is already part of the c library +# However, FindThreads.cmake incorrectly sets CMAKE_HAVE_LIBC_PTHREAD OFF +# See: https://gitlab.kitware.com/cmake/cmake/-/issues/21579 +set(CMAKE_HAVE_LIBC_PTHREAD ON) diff --git a/thirdparty/flexiv_rdk/cmake/qnx_toolchain_x86_64.cmake b/thirdparty/flexiv_rdk/cmake/qnx_toolchain_x86_64.cmake new file mode 100644 index 0000000..c2c0bc0 --- /dev/null +++ b/thirdparty/flexiv_rdk/cmake/qnx_toolchain_x86_64.cmake @@ -0,0 +1,70 @@ +# QNX toolchain for cross-compilation of x86_64 target + +# Set target system +set(CMAKE_SYSTEM_NAME QNX) +set(CMAKE_SYSTEM_PROCESSOR x86_64) +set(arch gcc_ntox86_64) + +# Specify the cross compiler +set(CMAKE_C_COMPILER qcc) +set(CMAKE_C_COMPILER_TARGET ${arch}) +set(CMAKE_CXX_COMPILER q++) +set(CMAKE_CXX_COMPILER_TARGET ${arch}) + +# Set qnx build flags +add_definitions("-D_QNX_SOURCE") + +# Set QNX_TARGET +if (DEFINED ENV{QNX_TARGET}) + set (QNX_TARGET $ENV{QNX_TARGET}) +else () + message (FATAL_ERROR "NO QNX TARGET ENV DEFINED") +endif () + +# Set the sysroot to the QNX target directory, so the compiler can find the correct headers and libraries for the target system. +set(CMAKE_SYSROOT ${QNX_TARGET}) + +# Set default cmake_install_prefix +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT OR NOT DEFINED CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX ${QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr/local CACHE PATH "cmake install prefix" FORCE) +endif() + +# We want to search: +# 1. ${CMAKE_PREFIX_PATH} +# 2. ${CMAKE_INSTALL_PREFIX} +# 3. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr/local +# 4. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR}/usr +# 5. $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} +# 6. $ENV{QNX_TARGET}/usr +# +# To accomplish 3-6, append CMAKE_FIND_ROOT_PATH with: +# - $ENV{QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} +# - $ENV{QNX_TARGET}/usr +# +# However, CMAKE_FIND_ROOT_PATH will also be prepended to CMAKE_PREFIX_PATH and CMAKE_INSTALL_PREFIX. +# In order to search these without the prefix, +# append CMAKE_FIND_ROOT_PATH with: +# - CMAKE_PREFIX_PATH +# - CMAKE_INSTALL_PREFIX +# +# For details, see Confluence page: "Intro to CMake find_package()" +if (DEFINED CMAKE_PREFIX_PATH) + list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH}) +endif() +list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_INSTALL_PREFIX}) +list(APPEND CMAKE_FIND_ROOT_PATH ${QNX_TARGET}/${CMAKE_SYSTEM_PROCESSOR} ${QNX_TARGET}/usr) + +# Remove duplicates, since we are appending +list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH) + +# Search only the paths prefixed with CMAKE_FIND_ROOT_PATH +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# QNX does not support -pthread compiler flag, +# because pthread is already part of the c library +# However, FindThreads.cmake incorrectly sets CMAKE_HAVE_LIBC_PTHREAD OFF +# See: https://gitlab.kitware.com/cmake/cmake/-/issues/21579 +set(CMAKE_HAVE_LIBC_PTHREAD ON) diff --git a/thirdparty/flexiv_rdk/doc/Doxyfile.in b/thirdparty/flexiv_rdk/doc/Doxyfile.in new file mode 100644 index 0000000..8d0c2c9 --- /dev/null +++ b/thirdparty/flexiv_rdk/doc/Doxyfile.in @@ -0,0 +1,2665 @@ +# Doxyfile 1.9.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Flexiv RDK APIs" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 2.1 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = doc/flexiv_logo.png + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = YES + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which efficively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = README.md \ + doc \ + example \ + include \ + lib \ + test + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), +# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, +# *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.proto + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = */build/* \ + *.pb.h \ + *.pb.cc + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = example + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = README.md + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to +# YES then doxygen will add the directory of each input to the include path. +# The default value is: YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: +# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = YES + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = TeX/AMSmath + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /