feat: 增加组合折叠姿态干涉扫描、力矩实测与结构强度复核数据

This commit is contained in:
cbb
2026-10-02 12:06:23 +08:00
parent 6fe97fc643
commit b593e47c76
21 changed files with 1587 additions and 1030 deletions
+32 -3
View File
@@ -69,8 +69,8 @@ python src/sweep_v2.py
| 构件编号 | 构件名称 | 关联驱动关节 | 设计说明 |
| :--- | :--- | :---: | :--- |
| **P01** | 胸板 / 安装板 | 根基固定 | 12mm 基础基座,预留 4x M4 紧固位与躯干走线窗 |
| **P02** | 底脚 / 支撑座 | 根基固定 | 200×320mm 重型防倾覆底盘,提供稳固调试基座 |
| **P01** | 立柱(三段 A/B/C) | 根基固定 | 66×66mm 三角截面立柱,分段榫接 + M4 自攻对锁孔防反装;顶端为 P03 安装台 |
| **P02** | 底盘(核心座 + 前/后防翻力臂) | 根基固定 | 195×190mm 核心承力座 + 48mm 套筒 + 前后三角加强筋;前后防翻力臂带 M5 预埋螺母 |
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2,后侧刚性抱紧 M3 |
@@ -78,6 +78,23 @@ python src/sweep_v2.py
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5 |
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5,立式抱紧 M6 腕旋转电机 |
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 40mm) |
| **M1~M6** | 关节电机模组 ×6 | 各关节驱动 | Ø60 方形标准减速电机模组(63.6mm 方形尾盖),210 g/台,整机 1260 g |
> ⚠️ **P01 / P02 是固定测试台架,不是机器人本体**。它在扫描中代表机械臂与躯干的连接
> 界面——**臂撞立柱 = 臂撞躯干,是真约束,碰撞计算必须包含它**。但台架的位置与截面
> 是一个假设,所以躯干侧的工作空间只和这个假设一样准。要改的是"真机躯干占位"这个
> 输入,而不是改台架(改台架是改测试工装)。`sweep_v2.py --no-stand` 是诊断开关:
> 排除台架重扫,给出不含躯干约束的自由空间行程,用来分辨某个限位是臂自身折叠产生的
> 还是躯干挡的。**默认包含台架。**
> ⚠️ **关节电机力矩现状**:按 `torque_balance.py` 的实测(零件真实质心 + 可达姿态搜索),
> **J1 肩抬举仅悬停自身臂身即需 3.84 N·m、J2 需 2.48 N·m**,已超出假定许用连续力矩
> 2.0 N·m;带 0.5 kg 末端负载时 J1 达 6.14 N·m(307%)。详见 `docs/torque_report.md`。
> 结论对电机额定参数敏感,该参数目前为工程假定(无数据手册),可用 `ARMT_CONT` 覆盖重算。
>
> 相反地,**结构本身不是瓶颈**:12 个剖面全部合格,最差的 P03 吊臂根座安全系数 8.93,
> 最大挠度 1.6mm。减重救不了电机——J1 的 distal 载荷里 5 台电机占 65%(1.05kg),
> 打印件只占 0.58kg,把 P07/P08/P09 全部减掉 J1 也只降到 2.54 N·m。
---
@@ -85,8 +102,20 @@ python src/sweep_v2.py
- 📘 **权威数据(工具自动生成)**:
- [各关节行程与硬限位 travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json)(sweep_v2.py 扫描生成,修改几何后必须重扫)
> 滑块范围 = **单关节**硬限位。组合折叠姿态下另有干涉约束,见 `travel_detail.json` 的
> `combos` 字段(目前全部是工具法兰 P09 撞躯干占位 P01_A,属真约束);查看器运行期
> 另有胶囊碰撞模型按姿态夹取。
- [零件明细与质量属性 bom.json](file:///g:/Robot_v2/机器人手臂/docs/bom.json)(构建自动生成)
- [静态干涉检测结果 collision.json](file:///g:/Robot_v2/机器人手臂/docs/collision.json)(构建自动生成)
- [截面刚度与应力 strength.json](file:///g:/Robot_v2/机器人手臂/docs/strength.json)(strength_check.py 生成)
- [关节力矩与负载能力 torque_report.md](file:///g:/Robot_v2/机器人手臂/docs/torque_report.md)(torque_balance.py 生成)
- 🔬 **测试与校验工具**(src/ 目录):
- sweep_v2.py — 权威动态碰撞检测与行程扫描
- strength_check.py / torque_balance.py — 刚度、应力与力矩平衡校核(按需重新生成报告)
- strength_check.py — 剖面刚度/应力校核(零件真实质心 + 可达姿态搜索,输出含挠度估算)
- torque_balance.py — 关节电机力矩、惯量与负载能力校核(拓扑取自 human_layout,不手抄)
- finalize_travel.py — 从 travel_detail 重建 travel.json(含组合卡死阈值提示)
> 三个校验脚本共用同一套零件表与运动学链(`strength_check.part_table / chain`,
> 关节轴来自 `human_layout.py`),改几何后不存在"报告与模型不同步"的问题。
> 环境变量:`ARMPAY`(末端负载 kg)、`ARMDYN`(动载系数)、`ARMINFILL`(实体率)、
> `ARMMAT`(材料)、`ARMT_CONT`/`ARMT_PEAK`/`ARMT_ALLOW`(电机力矩)、`ARMT_ALPHA`(角加速度)。
+6 -6
View File
@@ -74,20 +74,20 @@
"名称": "肩叉",
"类型": "3D打印",
"数量": 1,
"体积cm3": 347.4,
"质量g_PLA": 430.8,
"体积cm3": 361.4,
"质量g_PLA": 448.1,
"包络": "[-15.5 91.5 455.0 .. 54.5 164.5 646.5] (70.0x73.0x191.5)",
"说明": "齐平插槽版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"
"说明": "齐平插槽+ M2 前端压盖版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉;【2026-10-02 新增】M2 前端压盖(R35, Y155.5..161.0):电机前端盖外端面加 4xM3x8 沉孔(BCD55)夹紧,与后端安装板 4xM3x10 形成前后双向面压,消除大扭矩下电机在座孔里的倾斜"
},
{
"件号": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
"名称": "大臂旋转座",
"类型": "3D打印",
"数量": 1,
"体积cm3": 159.5,
"质量g_PLA": 197.8,
"体积cm3": 141.5,
"质量g_PLA": 175.5,
"包络": "[-15.5 93.0 389.0 .. 54.5 174.0 525.0] (70.0x81.0x136.0)",
"说明": "V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"
"说明": "V3圆角+ J2前端夹紧配合:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂);J2 从动法兰盘背面由 Y=157 后移至 Y=162(hub_gap=6.0,给 P04 新增的 M2 前端压盖让位,J2 直锁凸台同步加长到 Y=165.5,3xS01 输出螺钉咬合量不变);保证 J2 全角度 0 干涉"
},
{
"件号": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
+121 -111
View File
@@ -1,112 +1,122 @@
[
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
},
{
"id": "S04",
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°);座位为舱孔前段 4 个 Ø10 局部凸台(2026-10-02 平直舱孔方案),M3x10 旋入电机前端面咬合 3.5mm",
"头型": "沉头内六角",
"数量": 4,
"螺纹": "M3",
"规格": "M3 x 10",
"长度mm": 10
},
{
"id": "S05",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 8,
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
"备注": "统一 Ø3.9 直孔(母口壁与公榫壁同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从外面直接拧入自攻成型,螺纹吃在母口壁 3.6 + 公榫壁 6.9 共 10.5mm;孔位 x=-131/-93 非对称物理防反装"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
"备注": "统一 Ø3.9 直孔(鞍座与立柱顶环同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从上方直接拧入自攻成型,穿鞍座 9mm 后吃进立柱顶环 7mm;载荷走 2403mm² 承压面,螺钉只防上抬"
},
{
"id": "S11",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 1,
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
"备注": "与 S05/S09/S06 同为 M4x16(全机 M4x16 合计 17 颗);Ø3.9 直孔贯穿 11mm 后壁 + 榫头同径:从后方直接拧入自攻成型,榫头内咬合 4.7mm"
},
{
"id": "S10",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
}
]
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4x M3 螺纹径向槽(电机.STEP 实测:BCD55、相位 39.9°+90k,槽宽 2.5)"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔位 BCD55、相位 39.9°+90k(电机.STEP 实测螺纹径向槽),轴向锁死防拉脱;旧 35.1×40.9 矩形孔距对角两颗偏出槽位 ~4.7mm,2026-10-02 修正"
},
{
"id": "S04",
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°);座位为舱孔前段 4 个 Ø10 局部凸台(2026-10-02 平直舱孔方案),M3x10 旋入电机前端面咬合 3.5mm",
"头型": "沉头内六角",
"数量": 4,
"螺纹": "M3",
"规格": "M3 x 10",
"长度mm": 10
},
{
"id": "S05",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 8,
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
"备注": "统一 Ø3.9 直孔(母口壁与公榫壁同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从外面直接拧入自攻成型,螺纹吃在母口壁 3.6 + 公榫壁 6.9 共 10.5mm;孔位 x=-131/-93 非对称物理防反装"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
"备注": "统一 Ø3.9 直孔(鞍座与立柱顶环同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从上方直接拧入自攻成型,穿鞍座 9mm 后吃进立柱顶环 7mm;载荷走 2403mm² 承压面,螺钉只防上抬"
},
{
"id": "S11",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 1,
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
"备注": "与 S05/S09/S06 同为 M4x16(全机 M4x16 合计 17 颗);Ø3.9 直孔贯穿 11mm 后壁 + 榫头同径:从后方直接拧入自攻成型,榫头内咬合 4.7mm"
},
{
"id": "S10",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
},
{
"id": "S12",
"规格": "M3 x 8",
"螺纹": "M3",
"长度mm": 8,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P04 前端压盖 -> M2 电机前端盖(J2)",
"备注": "Ø3.4 通孔 + Ø6.0 沉孔(深 3.0,自压盖前端面 Y=161 沉到 Y=158),BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°,与 P03→M1 的 S04 同约定);螺钉头全沉入压盖,头面 Y=158 压孔口台阶,压盖背面 Y=156 与电机前端盖外端面0.0 间隙面压贴合 —— 与后端安装板 S02 的 4xM3x10 一起把 M2 前后双向夹死,消掉大扭矩下电机绕 J2 轴点头/翘起的倾向;M3x8 穿过 3.5mm 端盖后再自攻入壳体约 4.5mm"
}
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 132 KiB

+139 -90
View File
@@ -3,145 +3,169 @@
"截面": "吊臂根座 @z=480",
"零件": "P03_肩吊臂_螺旋体35%_墙5底5_竖打",
"关节": "J1",
"A": 975.4800000000001,
"I": 391731.4096400291,
"c": 37.23299062774025,
"A": 957.2387999999999,
"I": 401134.9375110902,
"c": 37.922833863111535,
"bend": 1,
"centroid": [
-110.76700947225983,
128.00000000000003
-110.07716623688886,
128.0
],
"M": 23529.618031505226,
"F": 41.71918090692416,
"sigma": 2.2364253314442926,
"tau": 0.06415177283018231,
"SF": 5.365705633574787
"M": 14218.240146201913,
"F": 41.72937644104729,
"sigma": 1.3441760077937133,
"tau": 0.06539022933626484,
"L": 526.087122895359,
"defl": 1.6350063694519446,
"bend_axis": 1,
"SF": 8.927402312213866
},
{
"截面": "吊臂颊板 @z=560",
"零件": "P03_肩吊臂_螺旋体35%_墙5底5_竖打",
"关节": "J1",
"A": 1102.4399999999998,
"I": 605072.9254967812,
"c": 43.53697862036803,
"A": 1130.88,
"I": 667937.0157562625,
"c": 44.90221547001558,
"bend": 1,
"centroid": [
-96.71968814629368,
128.0
-98.0257258064516,
127.99999999999997
],
"M": 23529.618031505226,
"F": 41.71918090692416,
"sigma": 1.6930330775286337,
"tau": 0.05676387954027997,
"SF": 7.087870969134711
"M": 13715.34105182334,
"F": 41.72937644104729,
"sigma": 0.922016873786273,
"tau": 0.05534987325054023,
"L": 518.8210523704677,
"defl": 0.9212023341880543,
"bend_axis": 1,
"SF": 13.014946191518016
},
{
"截面": "肩叉腹板 @z=560",
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
"关节": "J2",
"A": 2217.3650000000002,
"I": 207369.8226041668,
"c": 16.75000010000293,
"I": 207369.82260416728,
"c": 16.750000100002925,
"bend": 1,
"centroid": [
27.25,
28.249999999999996,
124.95500000000001
],
"M": 15271.482458030512,
"F": 34.395230761329984,
"sigma": 1.2335320997379497,
"tau": 0.023267638003664246,
"SF": 9.728161920187782
"M": 7818.411904486269,
"F": 34.39607710754928,
"sigma": 0.6315210117722179,
"tau": 0.023268210538780903,
"L": 384.4650327126411,
"defl": 0.9288282882988673,
"bend_axis": 1,
"SF": 19.001743055745322
},
{
"截面": "肩叉腹板 @z=600",
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
"关节": "J2",
"A": 2368.7748,
"I": 276311.8560102971,
"c": 21.62027641094759,
"A": 2393.7248,
"I": 289729.609479537,
"c": 22.374104171783745,
"bend": 1,
"centroid": [
25.562276310943524,
126.94624799284419
26.31610407177968,
126.97437823679647
],
"M": 15271.482458030512,
"F": 34.395230761329984,
"sigma": 1.1949312516479667,
"tau": 0.02178039302933946,
"SF": 10.042418744551561
"M": 7884.930337951309,
"F": 34.39607710754928,
"sigma": 0.6089065355988086,
"tau": 0.02155390446776669,
"L": 391.71974764776274,
"defl": 0.6959926630802069,
"bend_axis": 1,
"SF": 19.707458039022367
},
{
"截面": "大臂旋转座横梁 @z=480",
"零件": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
"关节": "J2",
"A": 1250.9348,
"I": 490359.7093282266,
"c": 35.000000100000086,
"I": 490359.70932822645,
"c": 35.00000010000009,
"bend": 1,
"centroid": [
18.5,
19.500000000000004,
165.03578024690017
],
"M": 15271.482458030512,
"F": 34.395230761329984,
"sigma": 1.090019994282287,
"tau": 0.04124343342434392,
"SF": 11.008972370182331
"M": 8644.341221650102,
"F": 34.39607710754928,
"sigma": 0.6170000060499929,
"tau": 0.041244448280856785,
"L": 420.96884917144854,
"defl": 0.5206738812710487,
"bend_axis": 1,
"SF": 19.448946324690457
},
{
"截面": "大臂旋转座套筒 @z=440",
"零件": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
"关节": "J3",
"A": 923.6637500000008,
"I": 460541.5056708324,
"I": 460541.5056708321,
"c": 35.00071932511497,
"bend": 1,
"centroid": [
18.499280774885154,
19.49928077488516,
162.1529414131134
],
"M": 10944.224641918447,
"F": 28.80059116294328,
"sigma": 0.831750733009024,
"tau": 0.046771226806740965,
"SF": 14.42739936830306
"M": 7224.027167183286,
"F": 28.801398145740663,
"sigma": 0.5490192396606857,
"tau": 0.04677253732065479,
"L": 384.25524483485555,
"defl": 0.38601035205736267,
"bend_axis": 1,
"SF": 21.85715751494692
},
{
"截面": "大臂骨架中段 @z=350",
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
"关节": "J3",
"A": 1516.920000000001,
"I": 1131945.0546959997,
"I": 1131945.0546960002,
"c": 35.00000010000099,
"bend": 1,
"centroid": [
18.499999999999996,
19.499999999999993,
119.46110407931855
],
"M": 10944.224641918447,
"F": 28.80059116294328,
"sigma": 0.33839793015788383,
"tau": 0.028479344160809335,
"SF": 35.46120980823154
"M": 5824.4734611263175,
"F": 28.801398145740663,
"sigma": 0.1800940521592921,
"tau": 0.028480142142374657,
"L": 287.98631358672816,
"defl": 0.07112524430753915,
"bend_axis": 1,
"SF": 66.63185072534252
},
{
"截面": "大臂骨架抱箍 @z=325",
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
"关节": "J4",
"A": 562.2200000000005,
"I": 255126.52178866684,
"I": 255126.52178866687,
"c": 34.31311712541806,
"bend": 1,
"centroid": [
18.5,
19.499999999999996,
100.98098075486462
],
"M": 6102.354966361885,
"F": 23.11498093318896,
"sigma": 0.820733255145853,
"tau": 0.06167064743300382,
"SF": 14.621071980161778
"M": 4674.1717086407425,
"F": 23.115225549656877,
"sigma": 0.6286504444087546,
"tau": 0.06167130006845235,
"L": 259.0371452768405,
"defl": 0.20489053402473298,
"bend_axis": 1,
"SF": 19.088509531375568
},
{
"截面": "小臂骨架中段 @z=245",
@@ -152,54 +176,79 @@
"c": 35.00000010000015,
"bend": 1,
"centroid": [
18.5,
19.5,
120.60880570566229
],
"M": 6102.354966361885,
"F": 23.11498093318896,
"sigma": 0.17433637444141997,
"tau": 0.020535356793291413,
"SF": 68.83245127959344
"M": 3519.6618136307306,
"F": 23.115225549656877,
"sigma": 0.10055217751682426,
"tau": 0.020535574110643336,
"L": 199.5734012002831,
"defl": 0.019071177205131774,
"bend_axis": 1,
"SF": 119.34102568780449
},
{
"截面": "小臂骨架抱箍 @z=195",
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
"关节": "J5",
"A": 381.97200000000026,
"I": 75885.39226040001,
"I": 75885.39226040008,
"c": 22.39173964832456,
"bend": 1,
"centroid": [
18.5,
19.5,
103.25743876514508
],
"M": 2105.5758538864893,
"F": 16.322293440980538,
"sigma": 0.6212988419199141,
"tau": 0.06409747353594188,
"SF": 19.31437689939684
"M": 2244.921562213841,
"F": 16.321781071344514,
"sigma": 0.6624160151865485,
"tau": 0.06409546146580575,
"L": 159.53235542447365,
"defl": 0.1254842711067732,
"bend_axis": 1,
"SF": 18.11550404109807
},
{
"截面": "腕偏转座根部 @z=150",
"零件": "P08_腕偏转座_螺旋体25%_墙4底5_竖打",
"关节": "J5",
"A": 877.5228499999998,
"I": 452897.5474791441,
"c": 35.000019545458414,
"I": 452897.5474791437,
"c": 35.00001954545841,
"bend": 1,
"centroid": [
18.49998055454244,
19.499980554542446,
153.23437984777257
],
"M": 2105.5758538864893,
"F": 16.322293440980538,
"sigma": 0.1627193533077502,
"tau": 0.02790062978014853,
"SF": 73.74660577285154
"M": 2445.425561814797,
"F": 16.321781071344514,
"sigma": 0.18898301158149458,
"tau": 0.02789975395742319,
"L": 164.6512204903413,
"defl": 0.02439681521795845,
"bend_axis": 1,
"SF": 63.497771040786255
},
{
"截面": "工具法兰 @z=55",
"截面": "工具法兰 @z=107",
"零件": "P09_工具法兰_螺旋体100%_墙4底5_竖打",
"关节": "J6"
"关节": "J6",
"A": 3734.962150000003,
"I": 1169654.3875857776,
"c": 35.0000685148389,
"bend": 1,
"centroid": [
19.499931585161203,
127.9986725177389
],
"M": 1390.8472530369402,
"F": 10.722602788827801,
"sigma": 0.04161891723455653,
"tau": 0.004306309819830889,
"L": 130.23020962176682,
"defl": 0.0033611981177080657,
"bend_axis": 1,
"SF": 288.3304227346958
}
]
+121
View File
@@ -0,0 +1,121 @@
{
"assumptions": {
"motor": "60mm 方形标准关节电机模组,2 级行星减速",
"T_cont_Nm": 2.5,
"T_peak_Nm": 8.0,
"T_allow_Nm": 2.0,
"alpha_rad_s2": 5.0,
"payload_kg": 0.5,
"note": "电机参数为工程假定,仓库内无数据手册;结论对此敏感,见报告第 4 节"
},
"joints": {
"J1": {
"cn": "肩抬举",
"axis": "roll(绕X)",
"distal_mass_kg": 2.127,
"arm_mass_kg": 1.627,
"weight_N": 20.9,
"reach_tip_mm": 512.0,
"tau_gravity_Nm": 6.137,
"tau_gravity_arm_only_Nm": 3.84,
"I_distal_kgm2": 0.28468,
"tau_dynamic_Nm": 1.423,
"tau_required_static_Nm": 6.137,
"tau_required_peak_Nm": 7.561,
"duty_vs_Tcont_pct": 245.5,
"duty_vs_Tallow_pct": 306.9,
"peak_margin_x": 1.06,
"status": "OVERLOAD"
},
"J2": {
"cn": "肩俯仰",
"axis": "pitch(绕Y)",
"distal_mass_kg": 1.753,
"arm_mass_kg": 1.253,
"weight_N": 17.2,
"reach_tip_mm": 394.0,
"tau_gravity_Nm": 4.395,
"tau_gravity_arm_only_Nm": 2.478,
"I_distal_kgm2": 0.14623,
"tau_dynamic_Nm": 0.731,
"tau_required_static_Nm": 4.395,
"tau_required_peak_Nm": 5.126,
"duty_vs_Tcont_pct": 175.8,
"duty_vs_Tallow_pct": 219.8,
"peak_margin_x": 1.56,
"status": "OVERLOAD"
},
"J3": {
"cn": "大臂自转",
"axis": "roll(绕Z)",
"distal_mass_kg": 1.468,
"arm_mass_kg": 0.968,
"weight_N": 14.4,
"reach_tip_mm": 250.0,
"tau_gravity_Nm": 1.712,
"tau_gravity_arm_only_Nm": 0.742,
"I_distal_kgm2": 0.03871,
"tau_dynamic_Nm": 0.194,
"tau_required_static_Nm": 1.712,
"tau_required_peak_Nm": 1.906,
"duty_vs_Tcont_pct": 68.5,
"duty_vs_Tallow_pct": 85.6,
"peak_margin_x": 4.2,
"status": "OK"
},
"J4": {
"cn": "肘部俯仰",
"axis": "pitch(绕Y)",
"distal_mass_kg": 1.178,
"arm_mass_kg": 0.678,
"weight_N": 11.6,
"reach_tip_mm": 241.0,
"tau_gravity_Nm": 2.062,
"tau_gravity_arm_only_Nm": 0.89,
"I_distal_kgm2": 0.04258,
"tau_dynamic_Nm": 0.213,
"tau_required_static_Nm": 2.062,
"tau_required_peak_Nm": 2.275,
"duty_vs_Tcont_pct": 82.5,
"duty_vs_Tallow_pct": 103.1,
"peak_margin_x": 3.52,
"status": "OVERLOAD"
},
"J5": {
"cn": "腕部俯仰",
"axis": "pitch(绕Y)",
"distal_mass_kg": 0.832,
"arm_mass_kg": 0.332,
"weight_N": 8.2,
"reach_tip_mm": 132.0,
"tau_gravity_Nm": 0.863,
"tau_gravity_arm_only_Nm": 0.23,
"I_distal_kgm2": 0.01018,
"tau_dynamic_Nm": 0.051,
"tau_required_static_Nm": 0.863,
"tau_required_peak_Nm": 0.914,
"duty_vs_Tcont_pct": 34.5,
"duty_vs_Tallow_pct": 43.1,
"peak_margin_x": 8.75,
"status": "OK"
},
"J6": {
"cn": "腕自转",
"axis": "roll(绕Z)",
"distal_mass_kg": 0.547,
"arm_mass_kg": 0.047,
"weight_N": 5.4,
"reach_tip_mm": 83.0,
"tau_gravity_Nm": 0.0,
"tau_gravity_arm_only_Nm": 0.0,
"I_distal_kgm2": 0.0,
"tau_dynamic_Nm": 0.0,
"tau_required_static_Nm": 0.0,
"tau_required_peak_Nm": 0.0,
"duty_vs_Tcont_pct": 0.0,
"duty_vs_Tallow_pct": 0.0,
"peak_margin_x": null,
"status": "OK"
}
}
}
+46
View File
@@ -0,0 +1,46 @@
# 6 自由度机械臂 · 关节电机力矩与负载能力校核
## 1. 前提
- 驱动:60mm 方形标准关节电机模组,2 级行星减速
- 连续额定输出力矩 `T_cont` = 2.50 N·m;峰值堵转 `T_peak` = 8.00 N·m
- 封闭打印外壳热衰减后许用连续力矩 `T_allow` = 2.00 N·m;末端负载 0.50 kg;角加速度 5.0 rad/s²
- **电机参数为工程假定,仓库内无数据手册;结论对此敏感,见报告第 4 节**
载荷模型:每个零件按网格体积加权质心定位,随姿态做刚体变换,在可达姿态网格(俯仰轴取行程端点/中值,自转轴取 0/±60/±120/180°)上取重力力矩最大值。力矩为绕**关节自身轴**的分量——这才是电机要出的力。
## 2. 各关节校核结果
| 关节 | 名称 | 轴 | 以下质量(kg) | TCP力臂(mm) | 重力力矩(N·m) | 惯性力矩(N·m) | 峰值需求(N·m) | 负荷率(对T_allow) | 峰值裕度 | 判定 |
|:---:|:---|:---|---:|---:|---:|---:|---:|---:|---:|:---:|
| J1 | 肩抬举 | roll(绕X) | 2.13 | 512 | **6.14** | 1.42 | 7.56 | **307%** | 1.06× | OVERLOAD |
| J2 | 肩俯仰 | pitch(绕Y) | 1.75 | 394 | **4.39** | 0.73 | 5.13 | **220%** | 1.56× | OVERLOAD |
| J3 | 大臂自转 | roll(绕Z) | 1.47 | 250 | **1.71** | 0.19 | 1.91 | **86%** | 4.20× | OK |
| J4 | 肘部俯仰 | pitch(绕Y) | 1.18 | 241 | **2.06** | 0.21 | 2.27 | **103%** | 3.52× | OVERLOAD |
| J5 | 腕部俯仰 | pitch(绕Y) | 0.83 | 132 | **0.86** | 0.05 | 0.91 | **43%** | 8.75× | OK |
| J6 | 腕自转 | roll(绕Z) | 0.55 | 83 | **0.00** | 0.00 | 0.00 | **0%** | — | OK |
## 3. 许用连续工况下的末端负载能力
去掉末端负载后各关节仍需 `tau_gravity_arm_only` 的重力力矩,余额除以该关节实测最大 TCP 力臂,即为还能挂的负载:
| 关节 | 仅臂身重力力矩(N·m) | 余额(N·m) | 可挂负载(kg) |
|:---:|---:|---:|---:|
| J1 | 3.84 | -1.84 | 0.00 |
| J2 | 2.48 | -0.48 | 0.00 |
| J3 | 0.74 | +1.26 | 0.51 |
| J4 | 0.89 | +1.11 | 0.47 |
| J5 | 0.23 | +1.77 | 1.37 |
| J6 | 0.00 | +2.00 | 2.46 |
**整臂瓶颈:J1,可挂末端负载 0.00 kg。**
## 4. 结论要点
- **超载关节:J1、J2、J4**。
- J1(肩抬举)重力 6.14 N·m,超 `T_allow` 207%;即使按峰值堵转算,裕度也只有 1.06×。
- J2(肩俯仰)重力 4.39 N·m,超 `T_allow` 120%;即使按峰值堵转算,裕度也只有 1.56×。
- J4(肘部俯仰)重力 2.06 N·m,超 `T_allow` 3%;即使按峰值堵转算,裕度也只有 3.52×。
- 结论对电机参数高度敏感:若实际连续额定高于假定值,请用 `ARMT_CONT` / `ARMT_ALLOW` 覆盖后重跑本脚本。
- 惯量按 Σm·d² 估,**未计入零件自身转动惯量**,对肩/肘是大臂量级的关节约低估一成,惯性力矩一栏应视为下限。
- 本版不含被动弹簧平衡。旧版报告提出的 J2 拉簧预补偿(2.48 N·m)在几何上**尚未实现**;若要保留该方案,需要在 P04/P05 之间增加拉簧连杆机构。
+101 -100
View File
@@ -1,9 +1,10 @@
{
"generated": "2026-09-30 00:06:07",
"generated": "2026-10-02 12:06:22",
"vol_min": 2.0,
"step": 2.0,
"combo_step": 6.0,
"workers": 12,
"fixture_included": true,
"singles": {
"J1": {
"lo": -180.0,
@@ -12,16 +13,16 @@
},
"J2": {
"lo": -107.5,
"hi": 10.25,
"hi": 10.38,
"jams": {
"12.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
3160.7,
2557.1,
[
-53.9,
137.2,
-52.9,
137.7,
149.2
]
]
@@ -30,9 +31,9 @@
[
"P04_肩叉_螺旋体30%_墙5底5_竖打",
"P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
16.7,
16.5,
[
45.8,
46.8,
136.3,
534.6
]
@@ -47,17 +48,17 @@
},
"J4": {
"lo": -112.5,
"hi": 17.5,
"hi": 17.88,
"jams": {
"18.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
29.2,
6.2,
[
-82.0,
128.0,
117.5
116.1
]
]
],
@@ -67,7 +68,7 @@
"P07_小臂骨架_螺旋体30%_墙5底5_竖打",
56.3,
[
18.5,
19.5,
128.0,
383.1
]
@@ -77,17 +78,17 @@
},
"J5": {
"lo": -96.25,
"hi": 35.38,
"hi": 35.75,
"jams": {
"36.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
45.9,
11.4,
[
-82.4,
-82.2,
128.0,
139.5
140.7
]
]
],
@@ -97,7 +98,7 @@
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
328.2,
[
58.4,
59.4,
139.3,
227.9
]
@@ -118,7 +119,7 @@
"hold_at": -180.0,
"swept": "J2",
"direction": "hi",
"old": 10.25,
"old": 10.375,
"new": 5.0,
"jams": {}
},
@@ -148,7 +149,7 @@
"hold_at": -180.0,
"swept": "J4",
"direction": "hi",
"old": 17.5,
"old": 17.875,
"new": 11.0,
"jams": {}
},
@@ -168,7 +169,7 @@
"hold_at": -180.0,
"swept": "J5",
"direction": "hi",
"old": 35.375,
"old": 35.75,
"new": 29.0,
"jams": {}
},
@@ -315,11 +316,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
29.2,
6.2,
[
-82.0,
128.0,
117.5
116.1
]
]
]
@@ -332,17 +333,17 @@
"swept": "J4",
"direction": "lo",
"old": -109.0,
"new": -18.25,
"new": -18.625,
"jams": {
"-18.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
29.2,
6.2,
[
-82.0,
128.0,
117.5
116.1
]
]
]
@@ -361,11 +362,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
45.9,
11.4,
[
-82.4,
-82.2,
128.0,
139.4
140.6
]
]
]
@@ -378,17 +379,17 @@
"swept": "J5",
"direction": "lo",
"old": -96.25,
"new": -36.25,
"new": -36.625,
"jams": {
"-36.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
45.9,
11.4,
[
-82.4,
-82.2,
128.0,
139.4
140.6
]
]
]
@@ -437,11 +438,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
329.3,
186.7,
[
-83.8,
104.3,
122.2
-83.0,
104.7,
120.8
]
]
]
@@ -453,18 +454,18 @@
"hold_at": 167.0,
"swept": "J4",
"direction": "lo",
"old": -18.25,
"old": -18.625,
"new": -17.875,
"jams": {
"-18.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
329.3,
186.7,
[
-83.8,
104.3,
122.2
-83.0,
104.7,
120.8
]
]
]
@@ -483,11 +484,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
18.2,
2.5,
[
-82.5,
108.7,
138.8
-82.0,
108.0,
138.4
]
]
]
@@ -530,17 +531,17 @@
"swept": "J3",
"direction": "hi",
"old": 167.0,
"new": 155.75,
"new": 157.625,
"jams": {
"162.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
142.2,
52.0,
[
-82.8,
104.4,
118.7
-82.4,
104.8,
117.4
]
]
],
@@ -548,11 +549,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
142.2,
52.0,
[
-82.8,
151.6,
118.7
-82.4,
151.2,
117.4
]
]
]
@@ -565,17 +566,17 @@
"swept": "J3",
"direction": "lo",
"old": -180.0,
"new": -157.75,
"new": -159.625,
"jams": {
"162.0": [
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
142.2,
52.0,
[
-82.8,
104.4,
118.7
-82.4,
104.8,
117.4
]
]
],
@@ -583,11 +584,11 @@
[
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
142.2,
52.0,
[
-82.8,
151.6,
118.7
-82.4,
151.2,
117.4
]
]
]
@@ -616,7 +617,7 @@
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J1",
"direction": "hi",
"old": 161.0,
@@ -626,7 +627,7 @@
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J2",
"direction": "hi",
"old": 0.0,
@@ -636,27 +637,27 @@
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J3",
"direction": "hi",
"old": 155.75,
"new": 149.0,
"old": 157.625,
"new": 155.0,
"jams": {}
},
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J3",
"direction": "lo",
"old": -157.75,
"old": -159.625,
"new": -157.0,
"jams": {}
},
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J4",
"direction": "hi",
"old": 0.0,
@@ -666,7 +667,7 @@
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J4",
"direction": "lo",
"old": -17.875,
@@ -676,7 +677,7 @@
{
"pass_": 0,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J6",
"direction": "hi",
"old": 149.0,
@@ -709,8 +710,8 @@
"hold_at": -180.0,
"swept": "J3",
"direction": "hi",
"old": 149.0,
"new": 143.0,
"old": 155.0,
"new": 149.0,
"jams": {}
},
{
@@ -759,8 +760,8 @@
"hold_at": 143.0,
"swept": "J3",
"direction": "hi",
"old": 143.0,
"new": 137.0,
"old": 149.0,
"new": 143.0,
"jams": {}
},
{
@@ -799,8 +800,8 @@
"hold_at": -180.0,
"swept": "J3",
"direction": "hi",
"old": 137.0,
"new": 131.0,
"old": 143.0,
"new": 137.0,
"jams": {}
},
{
@@ -849,8 +850,8 @@
"hold_at": 143.0,
"swept": "J3",
"direction": "hi",
"old": 131.0,
"new": 125.0,
"old": 137.0,
"new": 131.0,
"jams": {}
},
{
@@ -899,8 +900,8 @@
"hold_at": -103.0,
"swept": "J3",
"direction": "hi",
"old": 125.0,
"new": 119.0,
"old": 131.0,
"new": 125.0,
"jams": {}
},
{
@@ -986,7 +987,7 @@
{
"pass_": 1,
"hold": "J3",
"hold_at": 119.0,
"hold_at": 125.0,
"swept": "J1",
"direction": "hi",
"old": 131.0,
@@ -996,7 +997,7 @@
{
"pass_": 1,
"hold": "J3",
"hold_at": 119.0,
"hold_at": 125.0,
"swept": "J2",
"direction": "hi",
"old": 0.0,
@@ -1006,7 +1007,7 @@
{
"pass_": 1,
"hold": "J3",
"hold_at": 119.0,
"hold_at": 125.0,
"swept": "J4",
"direction": "hi",
"old": 0.0,
@@ -1016,7 +1017,7 @@
{
"pass_": 1,
"hold": "J3",
"hold_at": 119.0,
"hold_at": 125.0,
"swept": "J5",
"direction": "hi",
"old": 0.0,
@@ -1026,7 +1027,7 @@
{
"pass_": 1,
"hold": "J3",
"hold_at": 119.0,
"hold_at": 125.0,
"swept": "J6",
"direction": "hi",
"old": 119.0,
@@ -1059,8 +1060,8 @@
"hold_at": -13.0,
"swept": "J3",
"direction": "hi",
"old": 119.0,
"new": 113.0,
"old": 125.0,
"new": 119.0,
"jams": {}
},
{
@@ -1086,7 +1087,7 @@
{
"pass_": 1,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J1",
"direction": "hi",
"old": 119.0,
@@ -1096,7 +1097,7 @@
{
"pass_": 1,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J2",
"direction": "hi",
"old": 0.0,
@@ -1106,17 +1107,17 @@
{
"pass_": 1,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J3",
"direction": "hi",
"old": 113.0,
"new": 107.0,
"old": 119.0,
"new": 113.0,
"jams": {}
},
{
"pass_": 1,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J4",
"direction": "hi",
"old": 0.0,
@@ -1126,7 +1127,7 @@
{
"pass_": 1,
"hold": "J5",
"hold_at": -36.25,
"hold_at": -36.625,
"swept": "J6",
"direction": "hi",
"old": 107.0,
@@ -1159,8 +1160,8 @@
"hold_at": -180.0,
"swept": "J3",
"direction": "hi",
"old": 107.0,
"new": 101.0,
"old": 113.0,
"new": 107.0,
"jams": {}
},
{
@@ -1209,8 +1210,8 @@
"hold_at": 101.0,
"swept": "J3",
"direction": "hi",
"old": 101.0,
"new": 95.0,
"old": 107.0,
"new": 101.0,
"jams": {}
},
{
+13 -9
View File
@@ -211,17 +211,21 @@ class Joint:
def rear_mount_holes(self, shape, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0):
"""Cut the 4x rear mounting bolt holes into a proximal link body.
Matches the 4x tapped holes on the motor rear metal endplate:
(±17.55, ±20.46) on the rear face at motor-local z = 0.
Matches the 4x M3 tapped radial slots on the motor rear endplate
(电机.STEP 实测):BCD55、相位 39.9°+90k,槽宽 2.5、径向可调约 ±1.25mm。
旧版误用 35.1x40.9 矩形孔距:对角两颗偏出螺纹槽 ~4.7mm 无处咬合
(2026-10-02 用户指出"螺钉没有对齐",按 STEP 实测槽位修正)。
"""
cuts = []
for sx in (-17.55, 17.55):
for sy in (-20.46, 20.46):
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
cuts.append(self.L(h))
if cb_d > 0 and cb_depth > 0:
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
cuts.append(self.L(cb))
for k in range(4):
a = math.radians(39.9 + 90.0 * k)
sx = 27.5 * math.cos(a)
sy = 27.5 * math.sin(a)
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
cuts.append(self.L(h))
if cb_d > 0 and cb_depth > 0:
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
cuts.append(self.L(cb))
return shape.cut(*cuts)
def front_collar_pocket(self, shape, r_bore=30.25, z0=50.0, z1=56.5):
+62 -15
View File
@@ -794,13 +794,15 @@ def p_girdle(j1):
s = s.cut(Xcyl(1.95, X_COL - TENON / 2.0 - 1.0, X_COL - TENON / 2.0 + 14.0, Y_COL, Z_LOCK))
# 4x M3 连接螺孔(螺丝刀通道直接走两竖板之间的开放腔体,无需再钻 Ø8.5 直通隧道)
for dy in (-17.55, 17.55):
for dz in (-20.46, 20.46):
py = Y_ARM + dy
pz = Z_ARM + dz
th = Xcyl(1.7, X_BACK - 0.5, X_MOUNT + 4.0, py, pz)
cb = Xcyl(3.25, X_BACK - 0.5, X_BACK + 4.5, py, pz)
s = s.cut(th).cut(cb)
# 孔位 = 电机尾端 4x M3 螺纹径向槽实测位(电机.STEP):BCD55、相位 39.9°+90k。
# 旧 35.1×40.9 矩形孔距与实际槽位差 ~4.7mm(对角两颗螺钉无处咬合),
# 2026-10-02 用户指出"螺钉没有对齐"后按 STEP 实测修正。
for dy, dz in ((21.06, 17.65), (-17.65, 21.06), (-21.06, -17.65), (17.65, -21.06)):
py = Y_ARM + dy
pz = Z_ARM + dz
th = Xcyl(1.7, X_BACK - 0.5, X_MOUNT + 4.0, py, pz)
cb = Xcyl(3.25, X_BACK - 0.5, X_BACK + 4.5, py, pz)
s = s.cut(th).cut(cb)
# 中央走线孔 (Ø26mm)
wire_hole = Xcyl(13.0, X_COL - 40.0, X_MOUNT + 2.0, Y_ARM, Z_ARM)
@@ -883,6 +885,11 @@ def p_fork(j1, j2):
2. J1 从动法兰圆盘已删除(盘面在 column 前缘形成鼓楞):J1 由 boss(Ø35.6)贯通 P03 前盖中心孔直连电机输出盘;column 顶边保持 Z=646.5 与 P03 环齐高,R=18mm 大圆弧顺滑过渡;
3. 头部转接凸台加长贯通 P03 前盖中心孔 (R=19.5mm),直连 J1 旋转法兰盘;
4. M2 保留后端安装板(R=35mm, 带 rear_mount_holes 螺钉孔),前端定位环鼓出圆盘已删除(前端定位由 column 端面 pocket 承担),中间镂空散热;
4b. 【2026-10-02 新增】M2 前端压盖:原方案电机只被后端安装板(S02 4xM3x10)单面锁死,
大扭矩下电机绕 Y 轴在 Ø60.3 座孔里点头/翘起,固定不牢。现在前端盖外端面
(Y=156.0) 再加一枚同径 R35 压盖,用 4x M3x8 在电机前端盖 BCD55 上把电机
从前端夹一次 —— 电机被前后两块同心 Ø60.3 端面双向夹紧,倾斜自由度归零;
螺钉沉在压盖沉孔内不外凸,不占用 J2 轴向间隙(P05 法兰盘已相应后移);
5. 严格满足 single-solid 流形拓扑与 360° 全范围零干涉 (COLLISION pairs = 0)。
"""
# 1. M2 后端安装法兰盘 (Y: 92..100, R=35mm)
@@ -892,6 +899,28 @@ def p_fork(j1, j2):
front_collar = Ycyl(35.0, Y_COVER - 6.5, Y_COVER - 0.5, X_ARM, Z_SH)
s = rear_plate.fuse(front_collar)
# 2b. 【2026-10-02 新增】M2 前端压盖 (Y: 155.5..161.0, R=35,与后端板同径)
# 根部与 front_collar 前端面 (Y_COVER-0.5=155.5) 共面熔接成一体,无缝隙、无悬浮面。
# 压盖背面 (Y_COVER=156.0) 与电机前端盖外端面 0.0 间隙面压贴合;Ø37 中央让位孔
# 让出 P05 的 J2 直锁凸台 (R17.8) 与 3xS01 输出螺钉头;Ø60.3 让位槽把压盖背面
# 精确落在 Y=156.0,不让根部那 0.5mm 啃到电机前端盖 R30.15 的外圆。
FP_Y0 = Y_COVER - 0.5 # 155.5 压盖根部(= front_collar 前端面,保证熔接)
FP_Y1 = Y_COVER + 5.0 # 161.0 压盖前端面(与 P05 法兰盘 162.0 留 1.0mm 动间隙)
FP_BORE = 27.5 # 4xM3 分布圆半径 (BCD55,与 P03→M1 的 S04 同约定)
fp = Ycyl(35.0, FP_Y0, FP_Y1, X_ARM, Z_SH)
fp = fp.cut(Ycyl(18.5, FP_Y0 - 0.5, FP_Y1 + 0.5, X_ARM, Z_SH)) # Ø37 中央让位孔
fp = fp.cut(Ycyl(30.15, FP_Y0 - 0.5, Y_COVER, X_ARM, Z_SH)) # Ø60.3 让位槽
for deg in (17.8, 107.8, 197.8, 287.8):
rad = math.radians(deg)
fu, fz = X_ARM + FP_BORE * math.cos(rad), Z_SH + FP_BORE * math.sin(rad)
# Ø6.0 沉孔深 3.0 (自前端面 FP_Y1 往 -Y 沉到 Y=158.0):M3 圆柱头(Ø5.5x3.0)
# 全沉入压盖,头面 Y=158.0 压在 Ø3.4 孔口台阶上;压盖背面(156.0)压电机前端盖,
# 中间 2.0mm 压盖本体受压 —— 夹紧力全部走面压,螺钉只提供压紧力不提供定位。
fp = fp.cut(Ycyl(3.0, Y_COVER + 2.0, FP_Y1 + 0.5, fu, fz))
fp = fp.cut(Ycyl(1.7, Y_COVER - 0.5, Y_COVER + 2.0, fu, fz))
# 压盖在第 7 步 M2 接口成型(座孔/让位槽)之后才熔接,避免被那些 R30.15~30.5 的
# 圆柱刀具蹭掉根部一圈 0.1mm 薄边。
# 3. J1 从动法兰圆盘已删除:原 R=36.5 圆盘盘面在 column 前缘形成圆盘鼓楞,
# J1 与 P04 的连接由 boss(Ø35.6 贯通 P03 前盖中心孔 R=19.5)直连电机输出盘
# 承担,螺钉走 boss 端面 4xB 孔,圆盘无传力功能已去除。
@@ -960,6 +989,10 @@ def p_fork(j1, j2):
c_m2 = [cyl(BORE_R, -0.6, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + 0.6)]
s = s.cut(*[j2.L(item) for item in c_m2])
# 7b. M2 前端压盖熔接(见 2b):必须在 6/7 步所有 R30.15~30.5 圆柱刀具走完之后,
# 否则压盖根部会被 front_collar_pocket / c_m2 啃掉一圈 0.1mm 薄边。
s = s.fuse(fp)
# 8. 头部转接凸台加长(贯通 P03 前盖中心孔,跨越装配缝直连 J1 输出盘)
boss = cyl(17.8, 54.942, 61.5 + X_FORK_GAP)
s = s.fuse(j1.L(boss))
@@ -977,17 +1010,22 @@ def p_fork(j1, j2):
def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket"):
def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hub_gap=1.0):
"""Half-wrapped (半包) 双曲面有机流线型 Pitch-to-Roll 结构座 (P05 & P08):
1. 顶部从动法兰与前侧 Loft 自然相切顺接;
2. 严格清除 z_plate 顶面以上与上级旋转件贴面的多余材料 (cut_top_solid),保证关节大范围摆动动态 0 干涉;
3. 下方电机实行半包散热设计,-Y侧敞开,+Y侧背脊与圆柱一体化融合;
4. 严格满足 single-solid 流形拓扑与零干涉 (COLLISION pairs = 0)。
hub_gap = 从动法兰盘背面与上级电机前端盖之间的轴向让位间隙。
默认 1.0(贴合防磨)。P05 传 6.0:J2 处 P04 新增了 M2 前端压盖
(Y 155.5..161.0),必须让出这 6.0mm 才能装下;见 p_shoulder_roll_base。
"""
z_bot = z_r - 56.0 # roll 电机底端 (front collar)
z_plate = z_r + 7.0 # roll 电机顶端安装板 (rear plate top)
y_out = HL.Y_HUB + HL.PLATE_T # 174.0 (俯仰直连法兰外侧面)
y_hub_in = HL.Y_COVER + 1.0 # 157.0 (从动法兰内侧面,预留 1.0mm 动间隙)
y_hub_in = HL.Y_COVER + hub_gap # 157.0 / (P05) 162.0 从动法兰内侧面
z_hub_in = y_hub_in - HL.Y_REAR # 法兰盘背面的电机局部 z(凸台长度跟着它走)
y_cyl_max = HL.Y_AXIS + 35.0 # 163.0 (下方电机外轮廓极值)
# 1. 基础立柱:Ø70mm 实心圆柱 (Z: z_bot .. z_plate, R=35mm)
@@ -1065,8 +1103,10 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket"):
# 9. 电机定子配合空腔
s = s.cut(cyl(30.5, z_r - 49.5, z_r - 0.5, HL.X_ARM, HL.Y_AXIS))
# 10. 俯仰轴直连法兰结构(凸台延伸至 59.5 跨过间隙与法兰盘牢固熔接,保持单实体)
boss = cyl(17.8, 54.942, 59.5)
# 10. 俯仰轴直连法兰结构(凸台延伸至法兰盘背面之后 2.5mm,跨过间隙与法兰盘牢固熔接,
# 保持单实体)。凸台长度跟着 y_hub_in 走:法兰盘后移时凸台同步加长,
# 3xS01 输出螺钉的头面台阶(局部 z=58)、杆长、咬合量与改前完全一致。
boss = cyl(17.8, 54.942, z_hub_in + 2.5)
s = s.fuse(j_pitch.L(boss))
s = s.cut(j_pitch.L(cyl(3.5, 54.0, 74.0 + 30.0)))
for a in AB.HUB_M2_CLOCK:
@@ -1089,8 +1129,15 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket"):
def p_shoulder_roll_base(j2, j3, half=None):
"""P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版)"""
return make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3, name="P05_大臂旋转座_螺旋体30%_墙4底5_竖打")
"""P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版)
hub_gap=6.0:J2 处 P04 新增了 M2 前端压盖(前端夹紧电机,消除大扭矩下的倾斜),
压盖占 Y 155.5..161.0,所以从动法兰盘背面必须从 Y=157 后移到 Y=162(留 1.0mm 动间隙)。
P08 保持默认 1.0 —— J5 处 P07 没有前端压盖,不需要这段轴向空间。
"""
return make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3,
name="P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
hub_gap=6.0)
def p_humerus_bone(j3, j4):
@@ -1394,9 +1441,9 @@ def build():
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
"轻量化板式桁架版:主干由 66mm 实心块改为双侧 6mm 桁架竖板+帐篷减重窗(斜面≥52°自支撑)+R4内角,外轮廓与旧版并集完全一致,实心 728->286cm³;底部 9mm 承力鞍座(Ø3.9 安装孔)+12mm 中位龙骨筋(自鞍座斜拉至法兰下缘)+40~45°外扩斜撑;保留 10mm 后置法兰、侧开式电机托架、Ø60.7 直通电机舱(平孔无台阶)(M1 从+Y滑入,后端 4xM3x16 锁死)、大散热窗与对流槽;无封闭空腔、免支撑"),
("P04_肩叉_螺旋体30%_墙5底5_竖打", "肩叉", lambda: p_fork(j1, j2), 0x2563eb,
"齐平插槽版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"),
"齐平插槽+ M2 前端压盖版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉;【2026-10-02 新增】M2 前端压盖(R35, Y155.5..161.0):电机前端盖外端面加 4xM3x8 沉孔(BCD55)夹紧,与后端安装板 4xM3x10 形成前后双向面压,消除大扭矩下电机在座孔里的倾斜"),
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
"V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"),
"V3圆角+ J2前端夹紧配合:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂);J2 从动法兰盘背面由 Y=157 后移至 Y=162(hub_gap=6.0,给 P04 新增的 M2 前端压盖让位,J2 直锁凸台同步加长到 Y=165.5,3xS01 输出螺钉咬合量不变);保证 J2 全角度 0 干涉"),
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
"Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体,单实体0干涉"),
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "小臂骨架", lambda: p_forearm_bone(j4, j5, None), 0x7c3aed,
+32 -4
View File
@@ -3,6 +3,11 @@
滑块范围 = 单关节硬限位(任意单关节推满零干涉,含 1° 安全余量);
组合折叠约束保留在 travel_detail.json 的 combos 字段里,供报告引用。
注意 note 的措辞:单轴整周无干涉 **不等于** 任意组合姿态无干涉。sweep_v2 的组合扫描
已经在 J1/J3/J6 上找到了真实卡死(工具法兰 P09 撞固定立柱 P01_A),例如 J3≈±162°。
所以连续轴的 note 必须写明"单轴"并指向 combos,否则这份文件会误导人——查看器运行期
另有胶囊碰撞模型按姿态夹取(ik-glue.js 的 dynLimit),但静态滑块范围按设计不压缩。
"""
import json
import math
@@ -13,20 +18,43 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sweep_v2 import CHAIN, DOCS
d = json.load(open(DOCS + "/travel_detail.json", encoding="utf-8"))
# 组合扫描中,每个被扫轴真实出现卡死的最小绝对角
combo_warn = {}
for c in d.get("combos", []):
if not c.get("jams"):
continue
ax = c["swept"]
for a in c["jams"]:
try:
v = abs(float(a))
except (TypeError, ValueError):
continue
combo_warn[ax] = min(combo_warn.get(ax, 1e9), v)
out = []
for t in CHAIN[1:]:
s = d["singles"][t]
lo, hi = s["lo"], s["hi"]
warn = combo_warn.get(t)
warn_txt = (";组合折叠姿态下(其余关节同时处于各自极限)约 ±%.0f° 起与躯干占位"
"(台架 P01 立柱)干涉,见 travel_detail.combos,"
"运行期由查看器碰撞模型按姿态夹取" % warn) if warn is not None else ""
if lo <= -179.9 and hi >= 179.9:
out.append(dict(axis=t, lo=-180.0, hi=180.0, continuous=True, zero=0.0,
note="整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"))
combo_warn_deg=warn,
note="单轴整周无干涉(滑块按单轴限位给出)%s" % (warn_txt or
";组合扫描未发现该轴卡死")))
else:
lo_out = float(math.ceil(lo + 1.0)) if lo < -0.5 else 0.0
hi_out = float(math.floor(hi - 1.0)) if hi > 0.5 else 0.0
out.append(dict(axis=t, lo=lo_out, hi=hi_out, continuous=False, zero=0.0,
note="正向 +%d° / 反向 %d°,总行程 %d°(全对全动态扫描验证,含相邻件与电机)"
% (hi_out, lo_out, hi_out - lo_out)))
print("%-3s %7.1f .. %7.1f -> %s .. %s" % (t, lo, hi, out[-1]["lo"], out[-1]["hi"]))
combo_warn_deg=warn,
note="单轴正向 +%d° / 反向 %d°,总行程 %d°(单关节硬限位,含相邻件与电机)%s"
% (hi_out, lo_out, hi_out - lo_out, warn_txt)))
print("%-3s %7.1f .. %7.1f -> %s .. %s%s"
% (t, lo, hi, out[-1]["lo"], out[-1]["hi"],
(" 组合卡死阈值 ±%.0f°" % warn) if warn is not None else ""))
json.dump(out, open(DOCS + "/travel.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=1)
+20 -1
View File
@@ -79,7 +79,26 @@ Y_M6 = Y_AXIS
Y_CHEST = 62.0 # mast half width
X_CHEST = 6.0 # chest plate half thickness
# ------------------------------------------------------------------ joint frames
# ------------------------------------------------------------------ 台架(非常规构件)
# P01 立柱 / P02 底盘 是【固定测试台架】,不是机器人本体的一部分(用户 2026-10-02
# 确认);它在扫描中代表机械臂与**躯干/机身连接界面**的那块实体。因此:
#
# - 臂撞立柱 = 臂撞躯干,是真约束,不能忽略。sweep_v2 报出的组合卡死(J3 ±162°、
# J4 ±18°、J5 ±36° 等,全部是工具法兰 P09 撞立柱 P01_A)就是这类真约束。
# - 但台架的**位置与截面是一个假设**。躯干侧工作空间只和这个假设一样准:真机躯干
# 挪后一点或外形不同,这些限位就变。所以台架本身不要为了放宽行程去改(那是改测试
# 工装,不是改设计);要改的是"真机躯干占位"这个输入。
# - `--no-stand` 仅用于诊断:给出不含躯干约束的自由空间行程,用来分辨某个限位到底是
# 臂自身折叠产生的、还是躯干挡的。默认不排除。
FIXTURE_PREFIX = ("P01_", "P02_")
def is_fixture(name):
"""该零件是否属于固定测试台架(代表躯干/安装界面,非机器人本体)。"""
return name.startswith(FIXTURE_PREFIX)
# ------------------------------------------------------------------ 关节表
# tag, cn, rear, d, xdir, holder of the motor, driven part
# J1 roll +X fore-aft, in the shoulder base -> drives P04 肩叉
# J2 pitch +Y outboard in the fork -> drives P05 大臂旋转座
+28 -14
View File
@@ -13,7 +13,7 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
S03 P03 后法兰 4xM3 :J1 局部 (±17.55, ±20.46, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 62.5)
(沉孔 Ø6.0 在 Ø10 凸台端面 x=11.5,M3x10 旋入电机前端面咬合 3.5mm)
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
@@ -61,6 +61,12 @@ PROX = {"J2": "P04 肩叉", "J3": "P05 大臂旋转座", "J4": "P06 大臂骨架
COLOR = 0xc3cad6 # 不锈钢冷色
LMAX = {"S08": 17.0, "S10": 22.0} # 可见段截断:螺杆伸到底座底面(z=0)即止(台面/预埋螺母未建模)
# 电机尾端 4x M3 螺纹径向槽的实测孔位(电机.STEP):BCD55、相位 39.9°+90k,
# 槽宽 2.5、径向可调约 ±1.25mm。S02(P04~P08 后端板)与 S03(P03 后法兰)共用;
# 旧 35.1×40.9 矩形孔距对角两颗偏出螺纹槽 ~4.7mm(2026-10-02 用户指出"螺钉没有对齐")。
REAR_SLOTS = tuple((27.5 * math.cos(math.radians(39.9 + 90.0 * k)),
27.5 * math.sin(math.radians(39.9 + 90.0 * k))) for k in range(4))
# ---------------------------------------------------------------- 局部坐标系工具
def _norm(v):
@@ -127,8 +133,9 @@ def _geom(spec):
nom = float(str(spec["螺纹"]).lstrip("Mm"))
rs, L = nom / 2.0, float(spec["长度mm"])
head = spec.get("头型", "")
if "沉头" in head: # 沉头内六角:90° 锥,最宽处在承载面
return dict(kind="csk", rs=rs, L=L, rh=2.0 * nom)
if "沉头" in head: # 沉头内六角:90° 锥,最宽处(刃口)在承载面
rh = {3.0: 3.15, 4.0: 3.5, 6.0: 5.5}.get(nom, 1.05 * nom) # 头径≈沉孔座位径(GB dk)
return dict(kind="csk", rs=rs, L=L, rh=rh)
if "外六角" in head: # 外六角:M6 对边 10,高 4
af = {3.0: 5.5, 4.0: 7.0, 5.0: 8.0, 6.0: 10.0}.get(nom, 1.7 * nom)
hh = max(3.0, 0.7 * nom)
@@ -142,9 +149,10 @@ def _mesh(g):
"""局部网格:头部 z ∈ [-hh,0](0 = 承载头面),杆部 z ∈ [0, L]。"""
rs, L = g["rs"], g["L"]
if g["kind"] == "csk":
hh = g["rh"] - rs
head = _merge(_side(rs, g["rh"], -hh, 0.0, 14), _cap(rs, -hh, 14, up=False),
_cap(g["rh"], 0.0, 14, up=True))
hh = g["rh"] - rs # 90° 锥:头高 = (dk-d2)/2
head = _merge(_side(g["rh"], rs, 0.0, hh, 14), # 锥面自承载面收进材料(沉入沉孔座位)
_cap(g["rh"], 0.0, 14, up=False), # 承载端外露环形面
_cap(rs, hh, 14, up=True))
elif g["kind"] == "hex":
hh, rh = g["hh"], 0.98 * g["rh"]
head = _merge(_side(rh, rh, -hh, 0.0, 6), _cap(rh, 0.0, 6, up=True),
@@ -186,10 +194,9 @@ def instances(m2_bcd, m2_clock):
for tag, tp in (("J2", 8.0), ("J3", 7.0), ("J4", 8.0), ("J5", 8.0), ("J6", 7.0)):
d = frame(tag)[1]
zt = -tp + 4.0 # 沉孔台阶 = -t_plate + cb_depth(4)
for u in (-17.55, 17.55):
for v in (-20.46, 20.46):
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
for u, v in REAR_SLOTS:
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
d1 = frame("J1")[1]
for deg in (17.8, 107.8, 197.8, 287.8):
@@ -197,10 +204,17 @@ def instances(m2_bcd, m2_clock):
v = 27.5 * math.sin(math.radians(deg))
out.append(("S04", to_global("J1", u, v, 62.5), d1, "root",
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
for u in (-17.55, 17.55):
for v in (-20.46, 20.46):
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
"P03 后置法兰", "35.1×40.9 孔距 -> M1 尾端面"))
for u, v in REAR_SLOTS:
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
"P03 后置法兰", "BCD55 @39.9° 螺纹槽 -> M1 尾端面"))
# S12 P04 前端压盖 -> M2 前端盖(J2):沉孔台阶 Y=158(局部 z=58),与 S04 同 BCD55 相位
d2 = frame("J2")[1]
for deg in (17.8, 107.8, 197.8, 287.8):
u = 27.5 * math.cos(math.radians(deg))
v = 27.5 * math.sin(math.radians(deg))
out.append(("S12", to_global("J2", u, v, 58.0), d2, "J1",
"P04 前端压盖", "BCD55 @%.1f° -> M2 前端面(前端夹紧)" % deg))
for x in (X_COL - 16.0, X_COL + 22.0):
for z in (180.0, 329.0):
+253 -32
View File
@@ -17,6 +17,7 @@ import io
import json
import math
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -40,6 +41,8 @@ RHO = {"PLA": 1.24e-3, "motor_g": 210.0} # g/mm3 for printed, g per module
INFILL = float(os.environ.get("ARMINFILL", "0.38"))
# allowable bending stress across the print layers, after notch + creep derating
ALLOW = {"PLA": 12.0, "PETG": 20.0, "PA_CF": 30.0} # MPa
# Young's modulus of the printed lattice, for the deflection estimate only
EMOD = {"PLA": 3000.0, "PETG": 2100.0, "PA_CF": 8000.0} # MPa
MAT = os.environ.get("ARMMAT", "PLA")
PAYLOAD = float(os.environ.get("ARMPAY", "0.5")) # kg held at the fingertip
DYN = float(os.environ.get("ARMDYN", "2.0")) # dynamic factor on the moving mass
@@ -48,6 +51,7 @@ G981 = 9.81
GRIP_T = 12.0 # N*mm per motor is far too small to matter; the grip force
# case is carried separately below as a normal force at the pad
_cache = {}
GRP = {"root": 0, "J1": 1, "J2": 2, "J3": 3, "J4": 4, "J5": 5, "J6": 6}
def load(name):
@@ -173,6 +177,210 @@ def arm_masses():
return out, tot
# ------------------------------------------------------------------ 零件表:质量 + 零位世界质心
def _mesh_vol_cg(v, t):
"""四面体法求闭合三角网格的体积与体积加权质心。
网格顶点已在零位世界系(mesh_arm.json 由 arm_model 直接落盘),所以质心就是零件
在装配中的真实位置。实测体积与 STEP/BOM 差 <0.1%(电机包络 1.7%),可作载荷输入。
"""
vol = mx = my = mz = 0.0
for a, b, c in t:
x1, y1, z1 = v[a]
x2, y2, z2 = v[b]
x3, y3, z3 = v[c]
det = x1 * (y2 * z3 - y3 * z2) - x2 * (y1 * z3 - y3 * z1) + x3 * (y1 * z2 - y2 * z1)
vol += det
mx += (x1 + x2 + x3) * det
my += (y1 + y2 + y3) * det
mz += (z1 + z2 + z3) * det
if vol <= 0.0:
return 0.0, None
V = vol / 6.0
return V, (mx / (24.0 * V), my / (24.0 * V), mz / (24.0 * V))
def part_table(payload_kg=None):
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
meta = json.load(io.open(OUT.replace("/out", "/web") + "/mesh_arm.json", encoding="utf-8"))
rows = []
for p in meta["parts"]:
nm = p["name"]
if nm.startswith("ENV_") or not p.get("v") or not p.get("t") or p.get("vol", 0) <= 0:
continue # 可达包络网格 / 无实体螺钉实例,不承载
V, cg = _mesh_vol_cg(p["v"], p["t"])
if V <= 0.0 or cg is None:
continue
if p.get("printed"):
m = V * RHO["PLA"] * INFILL
elif re.match(r"^M\d", nm):
m = RHO["motor_g"]
else:
continue
rows.append([nm, m, cg, GRP.get(p["grp"], 0)])
if payload_kg:
rows.append(["PAYLOAD", payload_kg * 1000.0,
(HL.X_ARM, HL.Y_AXIS, HL.Z_TIP), 6])
return rows
# ------------------------------------------------------------------ 运动学:产品外参链
def _rodrigues(deg, d, p):
"""绕世界轴 d 过点 p 转 deg -> (R, t),使 x -> R x + t。"""
n = math.sqrt(sum(c * c for c in d)) or 1.0
x, y, z = d[0] / n, d[1] / n, d[2] / n
a = math.radians(deg)
c, s = math.cos(a), math.sin(a)
t1 = 1.0 - c
R = [[t1 * x * x + c, t1 * x * y - s * z, t1 * x * z + s * y],
[t1 * x * y + s * z, t1 * y * y + c, t1 * y * z - s * x],
[t1 * x * z - s * y, t1 * y * z + s * x, t1 * z * z + c]]
t = [p[i] - (R[i][0] * p[0] + R[i][1] * p[1] + R[i][2] * p[2]) for i in range(3)]
return R, t
def chain(ang):
"""acc[g] = (R, t):第 g 组零件的刚体变换。
acc[g] = acc[g-1] o R_g(产品外参,右侧先作用),与 sweep_v2.accum_trsf、
查看器嵌套 pivot 三者一致。
"""
R = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
t = [0.0, 0.0, 0.0]
out = [(R, t)]
for j in range(1, 7):
ax = HL.AXIS["J%d" % j]
Rj, tj = _rodrigues(ang.get(j, 0.0), ax["d"], ax["rear"])
Rn = [[sum(R[i][k] * Rj[k][j2] for k in range(3)) for j2 in range(3)] for i in range(3)]
tn = [sum(R[i][k] * tj[k] for k in range(3)) + t[i] for i in range(3)]
R, t = Rn, tn
out.append((R, t))
return out
def _xf(T, c):
R, t = T
return [sum(R[i][k] * c[k] for k in range(3)) + t[i] for i in range(3)]
def _dir(T, v):
"""只取旋转部分——方向向量绝不能带平移,否则投影出的"力矩"会被平移量放大。"""
R = T[0]
return [sum(R[i][k] * v[k] for k in range(3)) for i in range(3)]
def _cross(a, b):
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
def joint_loads(parts, payload_kg=None):
"""在可达姿态网格上搜索,返回每个关节的:
F 该关节以下的总重力 (N)(与姿态无关)
Mxyz 重力对关节轴点取矩后,三个全局分量各自的最大绝对值 (N*mm)
tau 重力绕该关节自身轴的最大力矩 (N*mm) —— 电机真正要出的力
Lp 出现 tau 最大值时,TCP 到该关节轴点的距离 (mm)
为什么搜索姿态:力臂不是常数。J1 是绕 X 的自转轴,手臂在轴正下方时重力对它
只产生绕 X 的扭矩(不产生弯曲),手臂摆到侧方时扭矩最大。旧算法把全部 distal
质量当作集中在指尖,量级完全错掉。
"""
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
def picks(j):
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
if t.get("continuous"):
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
lo, hi = t["lo"], t["hi"]
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
grid = [picks(j) for j in range(1, 7)]
res = {}
for j in range(1, 7):
res["J%d" % j] = dict(F=0.0, Mxyz=[0.0, 0.0, 0.0], tau=0.0, Lp=0.0, I=0.0, Iarm=0.0)
import itertools
for combo in itertools.product(*grid):
ang = {k + 1: v for k, v in enumerate(combo)}
acc = chain(ang)
pos = [(nm, m, _xf(acc[g], cg), g) for nm, m, cg, g in parts]
tcp = None
for nm, m, c, g in pos:
if nm == "PAYLOAD":
tcp = c
for j in range(1, 7):
ax = HL.AXIS["J%d" % j]
pj = _xf(acc[j - 1], ax["rear"])
dj = _dir(acc[j - 1], ax["d"])
F = M = [0.0, 0.0, 0.0]
wsum = 0.0
I = Iarm = 0.0
for _nm, m, c, g in pos:
if g < j:
continue
w = m * G981 / 1000.0
wsum += w
r = [c[i] - pj[i] for i in range(3)]
proj = sum(r[i] * dj[i] for i in range(3))
perp2 = sum(r[i] * r[i] for i in range(3)) - proj * proj
I += m * perp2 / 1e9 # g*mm^2 -> kg*m^2
if _nm != "PAYLOAD":
Iarm += m * perp2 / 1e9
rc = _cross(r, [0.0, 0.0, -w])
for i in range(3):
M[i] += rc[i]
r = res["J%d" % j]
r["F"] = max(r["F"], wsum)
for i in range(3):
r["Mxyz"][i] = max(r["Mxyz"][i], abs(M[i]))
tau = abs(M[0] * dj[0] + M[1] * dj[1] + M[2] * dj[2])
if tau > r["tau"]:
r["tau"] = tau
r["Lp"] = math.dist(tcp, pj) if tcp else 0.0
r["I"] = max(r["I"], I)
r["Iarm"] = max(r["Iarm"], Iarm)
return res
def section_moment(jid, bend_axis, ref, refgrp, parts):
"""截面绕自身形心、沿 bend_axis 的弯矩最大值 (N*mm),以及该姿态下 TCP 到该
截面的距离 (mm)(挠度估算用)。
参考点取【剖面形心】而不是关节轴:这两个参考点差得很远。P03 在 z=480 的剖面
形心 x≈-111,而臂身所有零件质心 x≈+19.5,真实力臂 130mm;若误用关节轴点
(J1 在 x=-51)只会得到 70mm,把弯矩低估 1.85 倍。
"""
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
jj = int(jid[1])
def picks(j):
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
if t.get("continuous"):
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
lo, hi = t["lo"], t["hi"]
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
import itertools
best = 0.0
best_L = 0.0
for combo in itertools.product(*[picks(j) for j in range(1, 7)]):
acc = chain({k + 1: v for k, v in enumerate(combo)})
rw = _xf(acc[refgrp], ref)
s = 0.0
tcp = rw
for nm, m, cg, g in parts:
if g < jj:
continue
c = _xf(acc[g], cg)
if nm == "PAYLOAD":
tcp = c
s += m * (c[bend_axis] - rw[bend_axis])
if abs(s) > best:
best = abs(s)
best_L = math.dist(tcp, rw)
return best * G981 / 1000.0, best_L
# critical cuts: (label, part, plane normal, plane origin, which load case owns it)
def cuts():
"""(label, part, cut-plane normal, point, joint whose load it carries, bend axis)
@@ -190,7 +398,9 @@ def cuts():
("小臂骨架中段 @z=245", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
("小臂骨架抱箍 @z=195", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
("腕偏转座根部 @z=150", "P08_腕偏转座_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
("工具法兰 @z=55", "P09_工具法兰_螺旋体100%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 55.0), "J6", 0),
# P09 包络 z=93..121,剖面取 107(法兰中截面)。原 -55 落在零件之外,
# 剖切返回空,工具法兰其实一直没被校核过。
("工具法兰 @z=107", "P09_工具法兰_螺旋体100%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
]
@@ -200,16 +410,12 @@ MOMENT = { # worst reachable bending moment per joint, N*mm
def main():
masses, per = arm_masses()
# Moment arms DERIVED from the swept travel (docs/travel.json), not hand-typed: they were
# stale (J4 said 24 deg while the arm now sweeps 360, and J1 was missing entirely), which
# silently under-loaded those sections. A continuous or >90 deg axis is capped at 90 deg
# because that is where a gravity load sits furthest off the axis.
TRV = json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))
dev = {t["axis"]: min(max(abs(t["hi"]), abs(t["lo"])), 90.0) for t in TRV}
# distance from each joint axis out to the fingertip load, mm
dist = {"J1": 564.0, "J2": 444.0, "J3": 380.0, "J4": 264.0, "J5": 129.0, "J6": 60.0}
reach = {k: dist[k] * math.sin(math.radians(dev.get(k, 90.0))) for k in dist}
parts = part_table(PAYLOAD)
grpof = {nm: g for nm, _, _, g in parts}
jl = joint_loads(parts)
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
masses["all"] = sum(m for _, m, _, _ in parts)
from concurrent.futures import ThreadPoolExecutor
def compute_cut(c_item):
@@ -218,46 +424,61 @@ def main():
sp = section_props(sh, n, o, bend)
if not sp:
return (lab, part, None, jid)
m_g = masses[jid] * DYN + PAYLOAD * 1000.0 * DYN
F = m_g * G981 / 1000.0 # N
# the section carries everything distal to its own joint, at that joint's worst
# reachable moment arm, plus the grip reaction on the finger root
M = F * reach[jid] # N*mm
if jid == "J6":
M = max(M, GRIP_T * 60.0)
I = sp["I"] # already the real bend axis, not min(Iu,Iv)
sigma = M * sp["c"] / max(I, 1e-6)
# 剖面形心的三维重建 + 弯矩所绕的轴
nvec = [float(x) for x in n]
p_ax, q_ax = [k for k in (0, 1, 2) if abs(nvec[k]) < 0.5]
bend_ax = sp["bend"]
far_ax = p_ax if bend_ax == q_ax else q_ax # I 绕 bend_ax、c 沿 far_ax
ref = list(o)
ref[p_ax] += sp["centroid"][0]
ref[q_ax] += sp["centroid"][1]
M, L = section_moment(jid, far_ax, ref, grpof.get(part, 0), parts)
M *= DYN
F = jl[jid]["F"] * DYN
I, c = sp["I"], sp["c"]
sigma = M * c / max(I, 1e-6)
tau = 1.5 * F / sp["A"]
return (lab, part, dict(sp, M=M, F=F, sigma=sigma, tau=tau,
SF=ALLOW[MAT] / sigma if sigma > 0 else 99.0), jid)
defl = M * L * L / (2.0 * EMOD[MAT] * I) if I > 1e-6 and L > 0 else 0.0
return (lab, part, dict(sp, M=M, F=F, sigma=sigma, tau=tau, L=L, defl=defl,
bend_axis=bend_ax, SF=ALLOW[MAT] / sigma if sigma > 0 else 99.0), jid)
with ThreadPoolExecutor(max_workers=min(13, os.cpu_count() or 4)) as executor:
rows = list(executor.map(compute_cut, cuts()))
print("材料 %s 许用弯曲 %g MPa(跨层、含缺口与蠕变折减) 末端负载 %g kg 动载系数 %g\n"
% (MAT, ALLOW[MAT], PAYLOAD, DYN))
print("材料 %s 许用弯曲 %g MPa(跨层、含缺口与蠕变折减) 弹性模量 %g MPa 末端负载 %g kg 动载系数 %g"
% (MAT, ALLOW[MAT], EMOD[MAT], PAYLOAD, DYN))
print("载荷模型:每个零件按真实质心(网格体积加权)随姿态变换,在可达姿态网格上取弯矩最大值;"
"参考点为剖面形心\n")
print("打印件按 %d%% 实体率计质量(壁+填充),电机按 %.0f g/台" % (INFILL * 100, RHO["motor_g"]))
print("%-26s %-10s %7s %9s %6s %8s %7s %5s %9s" %
("截面", "零件", "面积mm2", "Imin mm4", "c mm", "M N*mm", "sigma", "SF", "需Z mm3"))
print("%-24s %-9s %7s %10s %6s %9s %7s %6s %7s %7s"
% ("截面", "零件", "面积mm2", "I mm4", "c mm", "M N*mm", "sigma", "SF", "挠度mm", "需Z mm3"))
bad = []
for lab, part, d, jid in rows:
if not d:
print("%-26s %-10s 剖切失败" % (lab, part))
print("%-24s %-9s 剖切失败" % (lab, part))
continue
Zreq = d["M"] / ALLOW[MAT] * 1.5 # section modulus x1.5 SF
print("%-26s %-10s %7.0f %9.0f %6.1f %8.0f %7.1f %5.2f %9.0f%s" %
(lab, part, d["A"], d["I"], d["c"], d["M"], d["sigma"], d["SF"], Zreq,
" <<< 不足" if d["SF"] < 1.5 else ""))
print("%-24s %-9s %7.0f %10.0f %6.1f %9.0f %7.2f %6.2f %7.1f %7.0f%s"
% (lab, part[:9], d["A"], d["I"], d["c"], d["M"], d["sigma"], d["SF"], d["defl"],
Zreq, " <<< 不足" if d["SF"] < 1.5 else ""))
if d["SF"] < 1.5:
bad.append((lab, part, d))
print("\n各关节以下质量(g, 含电机): " + " ".join("%s=%.0f" % (k, v) for k, v in masses.items() if k != "all"))
print("整机质量(含 6 电机) %.0f g" % masses["all"])
print("\n各关节以下质量(g, 含电机与末端负载): "
+ " ".join("%s=%.0f" % (k, v) for k, v in masses.items() if k != "all"))
print("合计 %.0f g (其中末端负载 %.0f g)" % (masses["all"], PAYLOAD * 1000))
print("\n各关节电机需出的重力力矩(绕自身轴,姿态搜索最大值):")
for j in range(1, 7):
r = jl["J%d" % j]
print(" J%d %6.2f N·m (该关节以下总重 %.1f N,力臂 %.0f mm)"
% (j, r["tau"] / 1000.0, r["F"], r["Lp"]))
io.open(DOCS + "/strength.json", "w", encoding="utf-8").write(
json.dumps([dict(截面=a, 零件=b, 关节=d, **(c or {})) for a, b, c, d in rows],
ensure_ascii=False, indent=1, default=float))
return bad
if __name__ == "__main__":
b = main()
print("\n不合格截面 %d 处" % len(b))
sys.stdout.flush() # os._exit 不刷缓冲,重定向到文件时否则输出全丢
os._exit(0)
+23 -8
View File
@@ -23,8 +23,13 @@
- 预剪枝:按关节轴计算每对零件的轴向/径向区间,区间不相交的零件对在该关节的
任何角度都不可能碰撞,直接跳过(对单关节扫描是精确安全的)。
台架(P01 立柱 / P02 底盘)代表躯干与机身的连接界面,默认【参与】碰撞计算——臂撞立柱
就是臂撞躯干,是真约束。--no-stand 仅用于诊断:排除台架后重扫,给出"不含躯干约束"的
自由空间行程,用来分辨某个限位是臂自身折叠产生的还是躯干挡的(台架位置本身是假设,
改它等于改测试工装,不等于改设计)。
运行:python src/sweep_v2.py [--workers 20] [--step 2] [--combo-step 6]
[--no-combos] [--joints J2,J4] [--no-gjk]
[--no-combos] [--joints J2,J4] [--no-gjk] [--no-stand]
"""
import json
import math
@@ -44,6 +49,7 @@ import multiprocessing as mp
import arm_build as AB
import cadquery as cq
import human_layout as HL
from armcore import place, bb, DOCS
VOL_MIN = 2.0 # mm3,超过即计为干涉
@@ -61,6 +67,7 @@ MOUNT_PAIRS = {
MESH_TOL = 0.5 # mm,零件网格化容差
MESH_MARGIN = 0.8 # mm,凸包安全外扩(> MESH_TOL 保证网格内缩不致漏判)
USE_GJK = True # 进程内全局;工作进程由 initializer 传入
INCLUDE_FIXTURE = True # True=把 P01 立柱/P02 底盘(代表躯干)算进碰撞;--no-stand 置 False
# ---------------------------------------------------------------- 网格化 + GJK
@@ -165,11 +172,15 @@ def _do_simplex(s):
# ---------------------------------------------------------------- 模型与上下文
def load_model():
def load_model(include_fixture=True):
"""include_fixture=False 时把 P01 立柱 / P02 底盘(固定测试台架,非本体)排除在
碰撞配对之外——用来得到真机工作空间而不是台架工作空间。见 human_layout.FIXTURE_PREFIX。"""
import arm_model as AM
JS = AM.build()
parts = []
for p in AB.PARTS:
if not include_fixture and HL.is_fixture(p["name"]):
continue
g = CHAIN.index(AB.GRP.get(p["name"], "root"))
lo, hi = bb(p["shape"])
corners = [(x, y, z) for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])]
@@ -212,8 +223,8 @@ def axis_intervals(parts, rear, d):
return out
def make_ctx():
parts, joints = load_model()
def make_ctx(include_fixture=True):
parts, joints = load_model(include_fixture)
impossible = {}
for k in range(1, len(CHAIN)):
rear, d = joints[CHAIN[k]]
@@ -395,10 +406,10 @@ _POOL = None
_WORKERS = 4
def _init_worker(use_gjk=True):
def _init_worker(use_gjk=True, include_fixture=True):
global _CTX, USE_GJK
USE_GJK = bool(use_gjk)
_CTX = make_ctx()
_CTX = make_ctx(include_fixture)
def _task(task):
@@ -410,7 +421,7 @@ def get_pool(workers):
global _POOL, _WORKERS
_WORKERS = workers
if _POOL is None and workers > 1:
_POOL = mp.Pool(workers, initializer=_init_worker, initargs=(USE_GJK,))
_POOL = mp.Pool(workers, initializer=_init_worker, initargs=(USE_GJK, INCLUDE_FIXTURE))
return _POOL
@@ -448,7 +459,7 @@ def eval_angles(angles_list, workers, chunk=None, label="姿态"):
def _ctx_single():
global _CTX
if _CTX is None:
_CTX = make_ctx()
_CTX = make_ctx(INCLUDE_FIXTURE)
return _CTX
@@ -555,6 +566,9 @@ def main():
if "--no-gjk" in sys.argv:
global USE_GJK
USE_GJK = False
if "--no-stand" in sys.argv:
global INCLUDE_FIXTURE
INCLUDE_FIXTURE = False
# 内存感知的默认进程数:每个工作进程预留 ~2.5GB 峰值(建模+布尔瞬时开销)
avail_gb = 16.0
try:
@@ -691,6 +705,7 @@ def main():
open(DOCS + "/travel.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
detail = dict(generated=time.strftime("%Y-%m-%d %H:%M:%S"),
vol_min=VOL_MIN, step=step, combo_step=combo_step, workers=workers,
fixture_included=INCLUDE_FIXTURE,
singles={t: dict(lo=round(singles[t]["lo"], 2), hi=round(singles[t]["hi"], 2),
jams={str(a): v for a, v in singles[t]["jams"].items()})
for t in singles},
+187 -279
View File
@@ -1,27 +1,28 @@
# -*- coding: utf-8 -*-
"""Motor Torque Balance and Dynamic Capacity Analysis.
"""关节电机力矩与负载能力校核。
This module evaluates static and dynamic torque demand across all 6 joints (J1 to J6)
for the 6-DOF compact robotic arm driven by the standardized 60mm gearmotor module.
与旧版的区别(这些是旧版实际出错的地方):
Motor Module Parameters (measured from 电机.STEP):
- Reduction: 2-stage planetary 25:1
- Mass: 210g per unit
- Continuous Rated Torque: T_cont = 2.5 N·m
- Peak Stall Torque: T_peak = 8.0 N·m
- Safe Continuous Allowable (in 3D printed housing): T_allow = 2.0 N·m
1. 拓扑。旧版把关节表手抄成 "J3 肘俯仰 / J4 前臂自转 / J5 腕俯仰 / J6 夹爪",
并据此虚构了一个夹爪机构和 135mm 大臂——与 human_layout.py 的实际拓扑
(J3 大臂自转 / J4 肘俯仰 / J6 腕自转)完全不符,整份报告的结论都是错的。
本版不再复制拓扑:零件表、关节轴、运动学链全部复用 strength_check,
而 strength_check 的轴又直接来自 human_layout,单一事实源。
2. 力臂。旧版把每个关节的质量与力臂写成硬编码常数(J2 = 1.75kg @ 464mm),
几何一改就失效且不会报错。本版按每个零件的真实质心、在可达姿态网格上
搜索最大重力力矩。
3. 惯量。旧版用一组手填的 kg·m²。本版按 Σm·d²(对关节轴的平行轴距离)
在同一姿态网格上取最大值。零件自身转动惯量未计入,对大臂量级的关节
是小修正,已在报告中标注。
Load Cases Evaluated:
1. Horizontal Outstretched (Worst-Case Static Overhang):
theta_2 = 90°, theta_3 = 0°, theta_5 = 0°, payload = 0.5 kg (tip)
2. Standard Working Posture (90° Elbow Flexion):
theta_2 = 45°, theta_3 = 90°, payload = 0.5 kg
3. Dynamic Acceleration (worst-case sweep @ alpha = 5 rad/s^2)
4. Spring Gravity Compensation (Passive Assist at J2)
电机参数:仓库内没有原始数据手册,以下为工程假定,可用环境变量覆盖。
ARMT_CONT 连续额定输出力矩 (N·m)
ARMT_PEAK 峰值堵转输出力矩 (N·m)
ARMT_ALLOW 封闭塑料外壳热衰减后的许用连续力矩 (N·m)
ARMT_ALPHA 角加速度 (rad/s²)
ARMPAY 末端负载 (kg)
Outputs:
- docs/torque_balance.json
- docs/torque_report.md
输出:docs/torque_balance.json + docs/torque_report.md
"""
import io
import json
@@ -30,277 +31,184 @@ import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from armcore import DOCS, WEB, OUT
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
import human_layout as HL
from armcore import DOCS
import strength_check as SC
G = 9.81
T_CONT = 2.5 # N·m
T_PEAK = 8.0 # N·m
T_ALLOW = 2.0 # N·m derated thermal continuous for enclosed plastic housing
PAYLOAD_KG = 0.5 # 500g rated tip payload
T_CONT = float(os.environ.get("ARMT_CONT", "2.5"))
T_PEAK = float(os.environ.get("ARMT_PEAK", "8.0"))
T_ALLOW = float(os.environ.get("ARMT_ALLOW", "2.0"))
ALPHA = float(os.environ.get("ARMT_ALPHA", "5.0"))
PAYLOAD = float(os.environ.get("ARMPAY", "0.5"))
# Link geometric parameters (measured from human_layout / arm_model)
# Joint stations (mm):
# J1: Z=652 (Torso roll)
# J2: Z=560 (Shoulder pitch, offset from J1 by 92mm in Z)
# J3: Z=425 (Elbow pitch, L_upper = 135mm)
# J4: Z=300 (Forearm roll, internal coaxial)
# J5: Z=267 (Wrist pitch, L_fore = 158mm)
# J6: Z=171 (Gripper tip, L_hand = 96mm, total wrist-to-tip = 171mm)
# 关节名与轴向,全部取自 human_layout(不手抄)
JOINT_CN = {t[0]: t[1] for t in HL.JOINTS}
JOINT_D = {t[0]: t[3] for t in HL.JOINTS}
def analyze_torque(masses=None):
if masses is None:
# Default mass distribution (in kg) based on 38% infill PLA + 210g motor per joint
# Distal mass carried by each joint (including its driven link + all distal motors/links)
masses = {
"J1": 2.15, # carries entire arm (J2..J6 motors + links)
"J2": 1.75, # carries humerus + forearm + hand + payload
"J3": 1.20, # carries forearm + wrist + hand + payload
"J4": 0.85, # carries internal forearm rotor + wrist + hand + payload
"J5": 0.65, # carries palm + gripper + payload
"J6": 0.25 # gripper jaws + payload
def _axis_kind(tag):
d = JOINT_D[tag]
if abs(d[2]) == 1:
return "roll(绕Z)"
if abs(d[0]) == 1:
return "roll(绕X)"
return "pitch(绕Y)"
def analyze(payload_kg=PAYLOAD):
parts = SC.part_table(payload_kg)
jl = SC.joint_loads(parts)
jl0 = SC.joint_loads(SC.part_table(0.0)) # 去掉末端负载,用于反推负载能力
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
arm_mass = {k: v - payload_kg * 1000.0 for k, v in masses.items()}
res = {"assumptions": {
"motor": "60mm 方形标准关节电机模组,2 级行星减速",
"T_cont_Nm": T_CONT, "T_peak_Nm": T_PEAK, "T_allow_Nm": T_ALLOW,
"alpha_rad_s2": ALPHA, "payload_kg": payload_kg,
"note": "电机参数为工程假定,仓库内无数据手册;结论对此敏感,见报告第 4 节",
}, "joints": {}}
for j in range(1, 7):
tag = "J%d" % j
r = jl[tag]
tau_g = r["tau"] / 1000.0 # N*m,重力
tau_dyn = r["I"] * ALPHA # N*m,惯性
tau_g_arm = jl0[tag]["tau"] / 1000.0 # 去掉负载后的重力力矩
req_static = tau_g
req_peak = tau_g + tau_dyn
res["joints"][tag] = {
"cn": JOINT_CN[tag], "axis": _axis_kind(tag),
"distal_mass_kg": round(masses[tag] / 1000.0, 3),
"arm_mass_kg": round(arm_mass[tag] / 1000.0, 3),
"weight_N": round(r["F"], 1),
"reach_tip_mm": round(r["Lp"], 0),
"tau_gravity_Nm": round(tau_g, 3),
"tau_gravity_arm_only_Nm": round(tau_g_arm, 3),
"I_distal_kgm2": round(r["I"], 5),
"tau_dynamic_Nm": round(tau_dyn, 3),
"tau_required_static_Nm": round(req_static, 3),
"tau_required_peak_Nm": round(req_peak, 3),
"duty_vs_Tcont_pct": round(100.0 * req_static / T_CONT, 1),
"duty_vs_Tallow_pct": round(100.0 * req_static / T_ALLOW, 1),
"peak_margin_x": round(T_PEAK / req_peak, 2) if req_peak > 1e-3 else None,
"status": "OK" if (req_static <= T_ALLOW and req_peak <= T_PEAK) else "OVERLOAD",
}
# Moment arms to distal center of mass and payload in horizontal outstretched pose (meters)
# L_cg: distance from joint axis to combined distal link CG
# L_tip: distance from joint axis to tip payload (0.5 kg)
dist_tip = {
"J1": 0.547,
"J2": 0.464,
"J3": 0.329,
"J4": 0.171,
"J5": 0.171,
"J6": 0.060
}
# Distance to the distal mass CG (excluding payload)
dist_cg = {
"J1": 0.280,
"J2": 0.235,
"J3": 0.165,
"J4": 0.085,
"J5": 0.085,
"J6": 0.030
}
# --- Case 1: Worst-Case Horizontal Outstretched ---
# In horizontal pose:
# J2 (pitch) and J3 (pitch) and J5 (pitch) bear full gravity moment.
# J1 (roll) sees zero gravity bending torque if arm is purely along pitch plane,
# but if arm swings 90° sideways, J1 sees the full horizontal moment.
# J4 (roll) has axis aligned with forearm, so gravity torque on J4 is near 0.
# Masses excluding payload
m_arm_only = {k: masses[k] - PAYLOAD_KG for k in masses}
horiz_static = {}
for j in ["J1", "J2", "J3", "J4", "J5", "J6"]:
if j in ["J1", "J2", "J3", "J5"]:
# Arm gravity moment + Payload gravity moment
m_arm = m_arm_only[j]
t_arm = m_arm * G * dist_cg[j]
t_pay = PAYLOAD_KG * G * dist_tip[j]
t_tot = t_arm + t_pay
elif j == "J4":
# Forearm roll: minimal eccentricity (< 10mm)
t_tot = (m_arm_only[j] + PAYLOAD_KG) * G * 0.008
else: # J6 gripper
# Grip clamping force is normal, axial reaction torque:
t_tot = 0.08
horiz_static[j] = t_tot
# --- Passive Spring Gravity Assist at J2 ---
# Mounting lugs placed on shoulder fork and elbow
# Designed counter-moment: Ts(theta) = T_spring_max * sin(theta_2)
# Target: compensate ~65% of horizontal gravity torque (approx 2.48 N·m)
t_spring_j2 = 2.48
horiz_net_j2 = horiz_static["J2"] - t_spring_j2
# --- Case 2: Natural Working Posture (Elbow 90° flexed, shoulder 45°) ---
# Shoulder angle = 45°, elbow angle = 90°
# The effective moment arm to payload is reduced from 464mm to ~210mm
# The effective moment arm of humerus is 135mm * sin(45°) = 95mm
work_static = {}
# J2 in working pose:
# Humerus mass (~0.55kg) at 48mm: 0.55 * 9.81 * 0.048 = 0.26 N·m
# Forearm + Hand + Payload (~1.20kg) hanging vertically at L_upper*sin(45°) = 95mm:
# 1.20 * 9.81 * 0.095 = 1.12 N·m
# Total J2 static working torque: ~1.38 N·m (without spring), with spring (~1.75 N·m counter): net is ~0.37 N·m
work_static["J1"] = 0.65
work_static["J2"] = 1.38
work_static["J3"] = 0.88
work_static["J4"] = 0.05
work_static["J5"] = 0.52
work_static["J6"] = 0.08
work_net_j2 = abs(work_static["J2"] - t_spring_j2 * math.sin(math.radians(45)))
# --- Dynamic Acceleration Load (alpha = 5 rad/s^2) ---
# Approximate moment of inertia I_distal = sum(m_i * r_i^2)
inertia = {
"J1": 0.085, # kg·m^2
"J2": 0.072, # kg·m^2
"J3": 0.031, # kg·m^2
"J4": 0.003, # kg·m^2
"J5": 0.004, # kg·m^2
"J6": 0.001 # kg·m^2
}
alpha = 5.0 # rad/s^2 (corresponds to high speed reach)
dyn_torque = {k: inertia[k] * alpha for k in inertia}
# Total peak combined torque during fast horizontal move
peak_combined = {}
for k in masses:
if k == "J2":
peak_combined[k] = horiz_net_j2 + dyn_torque[k]
else:
peak_combined[k] = horiz_static[k] + dyn_torque[k]
results = {
"motor_specs": {
"module": "电机.STEP (60mm x 56mm)",
"ratio": "25:1 2-stage planetary",
"rated_continuous_Nm": T_CONT,
"peak_stall_Nm": T_PEAK,
"allowable_thermal_Nm": T_ALLOW
},
"payload_kg": PAYLOAD_KG,
"joints": {}
}
for j in ["J1", "J2", "J3", "J4", "J5", "J6"]:
t_raw = horiz_static[j]
t_work = work_static[j]
t_dyn = dyn_torque[j]
t_opt = horiz_net_j2 if j == "J2" else t_raw
duty_raw = (t_raw / T_CONT) * 100.0
duty_opt = (t_opt / T_CONT) * 100.0
margin_peak = T_PEAK / max(peak_combined[j], 0.01)
results["joints"][j] = {
"distal_mass_kg": round(masses[j], 3),
"reach_tip_mm": int(dist_tip[j] * 1000),
"static_horizontal_raw_Nm": round(t_raw, 2),
"static_horizontal_opt_Nm": round(t_opt, 2),
"static_working_pose_Nm": round(work_static[j], 2),
"dynamic_accel_Nm": round(t_dyn, 2),
"peak_combined_Nm": round(peak_combined[j], 2),
"duty_cycle_raw_pct": round(duty_raw, 1),
"duty_cycle_opt_pct": round(duty_opt, 1),
"peak_stall_safety_factor": round(margin_peak, 2),
"status": "PASS" if duty_opt <= 100.0 and margin_peak >= 1.5 else "WARN"
}
return results
def generate_report(res):
lines = []
lines.append("# 6自由度机械臂关节电机力矩平衡校核与优化分析报告\n")
lines.append("## 1. 电机性能基准与工况参数")
lines.append("- **驱动电机规格**: 标准化一体化行星减速关节电机 (`电机.STEP`)")
lines.append("- **外形尺寸**: 直径 Ø60.0 mm × 长度 56.0 mm,方形端盖 63.6 mm")
lines.append("- **传动形式**: 2级行星减速器 (减速比 25:1)")
lines.append("- **连续额定扭矩 ($T_{rated}$)**: 2.50 N·m")
lines.append("- **峰值堵转扭矩 ($T_{peak}$)**: 8.00 N·m")
lines.append("- **封闭塑料外壳热衰减后容许连续扭矩 ($T_{allow}$)**: 2.00 N·m")
lines.append("- **末端负载 ($m_{load}$)**: 0.50 kg (500g 全行程工况)")
lines.append("- **角加速度 ($\alpha$)**: 5.0 rad/s²\n")
lines.append("## 2. 各关节力矩校核与平衡计算表")
lines.append("| 关节编号 | 运动自由度 | 下属质量(kg) | 极限量臂(mm) | 极限水平静力矩(N·m) | 弹簧平衡后力矩(N·m) | 工作姿态静力矩(N·m) | 额定负荷率 | 峰值安全裕度 | 状态 |")
lines.append("|:---|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|")
labels = {
"J1": "肩抬举 (Roll)",
"J2": "肩俯仰 (Pitch)",
"J3": "肘俯仰 (Pitch)",
"J4": "前臂自转 (Roll)",
"J5": "腕俯仰 (Pitch)",
"J6": "手爪开合 (Grip)"
}
for jid, d in res["joints"].items():
spring_note = f"**{d['static_horizontal_opt_Nm']:.2f}**" if jid == "J2" else "-"
lines.append(f"| {jid} | {labels[jid]} | {d['distal_mass_kg']:.2f} | {d['reach_tip_mm']} | "
f"{d['static_horizontal_raw_Nm']:.2f} | {spring_note} | {d['static_working_pose_Nm']:.2f} | "
f"**{d['duty_cycle_opt_pct']:.1f}%** | {d['peak_stall_safety_factor']:.2f}x | {d['status']} |")
lines.append("\n## 3. 关键瓶颈分析与优化措施落地")
lines.append("### 3.1 瓶颈定位:J2 肩俯仰关节点")
lines.append("- **原始状态**: 在全臂水平伸直极限工况下,下属总质量达 1.75 kg(含大臂、小臂、腕部电机及0.5kg末端负载),质心距离达 464 mm。由此产生的重力力矩高达 **3.78 N·m**。")
lines.append("- **风险**: 3.78 N·m 超过电机连续额定值 2.5 N·m(负荷率 151%)。长时间悬停会导致电机过热保护甚至失步;同时 3D 打印结构受长期持续弯矩容易产生塑性蠕变下垂。")
lines.append("\n### 3.2 优化方案一:被动式拉簧重力预补偿 (Spring Gravity Balance)")
lines.append("- **设计**: 在 J2 肩叉 (P04) 与大臂 (P05) 之间布置平行拉伸弹簧连杆机构。")
lines.append("- **力矩对冲**: 水平展开时拉簧产生反向补偿力矩 $T_s = -2.48\\text{ N}\\cdot\\text{m}$。")
lines.append("- **效果**: J2 电机实际所需静扭矩骤降至 **1.30 N·m**,额定负荷率从 **151% 降至 52.0%**,安全落入连续工况安全区(小于 $T_{allow} = 2.0\\text{ N}\\cdot\\text{m}$)。")
lines.append("\n### 3.3 优化方案二:四点角接触薄壁轴承解耦 (QJ204 Moment Decoupling)")
lines.append("- **设计**: J2 驱动侧配置薄壁四点接触轴承 (Ø20ר47×14mm)。")
lines.append("- **效果**: 臂身产生的 38 N·m 侧向倾覆弯矩完全由外圈刚性轴承与肩叉支架承担,电机行星减速箱输出轴端面齿仅承受纯扭矩载荷,彻底杜绝行星齿轮偏磨卡死。")
lines.append("\n### 3.4 优化方案三:闭口箱涵与内腔纵向加强筋 (Closed-Box Beam Stiffness)")
lines.append("- **设计**: 大臂与小臂由原始开缝受弯槽钢结构升级为**带中肋闭口箱型梁**,壁厚 3.0mm,外廓 66mm。")
lines.append("- **效果**: 截面抗弯惯性矩 $I_{min}$ 从 $8.25\\times 10^4\\text{ mm}^4$ 提升至 $3.12\\times 10^5\\text{ mm}^4$(**刚度提升 3.78 倍**),彻底消除悬臂颤振与末端抖动。")
lines.append("\n### 3.5 优化方案四:典型作业姿态规划 (Elbow Flexion Workload)")
lines.append("- 在日常搬运抓取姿态下(肘部弯曲 90°),末端质心力臂由 464 mm 缩短至 210 mm。")
lines.append("- 各关节电机工作负荷均在 0.37~1.38 N·m 之间(负荷率 15%~55%),发热温升处于环境温标以内,保证长时间稳定运行。")
return "\n".join(lines)
return res, jl0, jl
def get_actual_arm_masses():
json_path = WEB + "/mesh_arm.json"
if not os.path.exists(json_path):
return None
d = json.load(io.open(json_path, encoding="utf-8"))
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
tot = {}
for p in d.get("parts", []):
nm = p["name"]
if p.get("printed", False):
v_cm3 = p.get("vol", 0.0) / 1000.0
g = v_cm3 * 1.24 * 0.38
elif nm.startswith("M"):
g = 210.0 # motor module
elif "半联轴器" in nm:
g = 14.0 # aluminum face spline half-coupling
elif "抱箍" in nm:
g = 8.0 # aluminum clamp band
else:
g = 20.0
tot[nm] = (g, p.get("grp", "root"))
m_calc = {}
for k, jid in enumerate(TREE[1:], 1):
down = set(TREE[k:])
m_calc[jid] = (sum(g for g, grp in tot.values() if grp in down) + PAYLOAD_KG * 1000.0) / 1000.0
return m_calc
def max_payload(res):
"""在许用连续力矩下,末端还能带多少 kg(按各关节取最小)。
线性化:tau_g(p) ≈ tau_g_arm + p·g·L。用的是该关节实测的最大 TCP 力臂,
即最不利姿态下的力臂。
"""
out = {}
for tag, d in res["joints"].items():
L = d["reach_tip_mm"] / 1000.0
if L <= 0:
out[tag] = None
continue
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
out[tag] = round(max(head, 0.0) / (G * L), 3) if head > 0 else 0.0
return out
def generate_report(res, mp):
L = []
L.append("# 6 自由度机械臂 · 关节电机力矩与负载能力校核\n")
a = res["assumptions"]
L.append("## 1. 前提\n")
L.append("- 驱动:%s" % a["motor"])
L.append("- 连续额定输出力矩 `T_cont` = %.2f N·m;峰值堵转 `T_peak` = %.2f N·m"
% (a["T_cont_Nm"], a["T_peak_Nm"]))
L.append("- 封闭打印外壳热衰减后许用连续力矩 `T_allow` = %.2f N·m;末端负载 %.2f kg;"
"角加速度 %.1f rad/s²" % (a["T_allow_Nm"], a["payload_kg"], a["alpha_rad_s2"]))
L.append("- **%s**\n" % a["note"])
L.append("载荷模型:每个零件按网格体积加权质心定位,随姿态做刚体变换,在可达姿态"
"网格(俯仰轴取行程端点/中值,自转轴取 0/±60/±120/180°)上取重力力矩最大值。"
"力矩为绕**关节自身轴**的分量——这才是电机要出的力。\n")
L.append("## 2. 各关节校核结果\n")
L.append("| 关节 | 名称 | 轴 | 以下质量(kg) | TCP力臂(mm) | 重力力矩(N·m) | 惯性力矩(N·m) | 峰值需求(N·m) | 负荷率(对T_allow) | 峰值裕度 | 判定 |")
L.append("|:---:|:---|:---|---:|---:|---:|---:|---:|---:|---:|:---:|")
for tag, d in res["joints"].items():
L.append("| %s | %s | %s | %.2f | %.0f | **%.2f** | %.2f | %.2f | **%.0f%%** | %s | %s |"
% (tag, d["cn"], d["axis"], d["distal_mass_kg"], d["reach_tip_mm"],
d["tau_gravity_Nm"], d["tau_dynamic_Nm"], d["tau_required_peak_Nm"],
d["duty_vs_Tallow_pct"],
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—", d["status"]))
L.append("")
L.append("## 3. 许用连续工况下的末端负载能力\n")
L.append("去掉末端负载后各关节仍需 `tau_gravity_arm_only` 的重力力矩,余额除以该关节"
"实测最大 TCP 力臂,即为还能挂的负载:\n")
L.append("| 关节 | 仅臂身重力力矩(N·m) | 余额(N·m) | 可挂负载(kg) |")
L.append("|:---:|---:|---:|---:|")
for tag, d in res["joints"].items():
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
L.append("| %s | %.2f | %+.2f | %s |"
% (tag, d["tau_gravity_arm_only_Nm"], head,
("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
ok = [t for t, v in mp.items() if v is not None]
worst = min(ok, key=lambda t: mp[t]) if ok else None
if worst:
L.append("")
L.append("**整臂瓶颈:%s,可挂末端负载 %.2f kg。**\n" % (worst, mp[worst]))
L.append("## 4. 结论要点\n")
over = [t for t, d in res["joints"].items() if d["status"] == "OVERLOAD"]
if over:
L.append("- **超载关节:%s**。" % "、".join(over))
for t in over:
d = res["joints"][t]
L.append(" - %s(%s)重力 %.2f N·m,超 `T_allow` %.0f%%;即使按峰值堵转算,"
"裕度也只有 %s。" % (t, d["cn"], d["tau_gravity_Nm"],
d["duty_vs_Tallow_pct"] - 100.0,
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—"))
L.append("- 结论对电机参数高度敏感:若实际连续额定高于假定值,"
"请用 `ARMT_CONT` / `ARMT_ALLOW` 覆盖后重跑本脚本。")
else:
L.append("- 全部关节在许用连续力矩内。")
L.append("- 惯量按 Σm·d² 估,**未计入零件自身转动惯量**,对肩/肘是大臂量级的关节"
"约低估一成,惯性力矩一栏应视为下限。")
L.append("- 本版不含被动弹簧平衡。旧版报告提出的 J2 拉簧预补偿(2.48 N·m)在几何上"
"**尚未实现**;若要保留该方案,需要在 P04/P05 之间增加拉簧连杆机构。")
return "\n".join(L)
def main():
masses = get_actual_arm_masses()
if masses:
print("Accurate physical mass distribution (including 0.5kg payload):")
for k, v in masses.items():
print(f" {k}: {v:.2f} kg (arm: {v - PAYLOAD_KG:.2f} kg + payload: {PAYLOAD_KG:.2f} kg)")
res = analyze_torque(masses)
# Save JSON
out_json = DOCS + "/torque_balance.json"
io.open(out_json, "w", encoding="utf-8").write(json.dumps(res, ensure_ascii=False, indent=2))
print(f"Torque balance JSON written to: {out_json}")
res, jl0, jl = analyze(PAYLOAD)
mp = max_payload(res)
io.open(DOCS + "/torque_balance.json", "w", encoding="utf-8").write(
json.dumps(res, ensure_ascii=False, indent=2))
io.open(DOCS + "/torque_report.md", "w", encoding="utf-8").write(generate_report(res, mp))
# Save Markdown report
rep = generate_report(res)
out_md = DOCS + "/torque_report.md"
io.open(out_md, "w", encoding="utf-8").write(rep)
print(f"Torque report MD written to: {out_md}")
print("末端负载 %.2f kg T_cont=%.2f T_allow=%.2f T_peak=%.2f alpha=%.1f\n"
% (PAYLOAD, T_CONT, T_ALLOW, T_PEAK, ALPHA))
print("%-4s %-8s %-12s %8s %8s %8s %9s %8s %s"
% ("关节", "名称", "轴", "重力N·m", "惯性N·m", "峰值N·m", "负荷率", "峰值裕度", "判定"))
for tag, d in res["joints"].items():
print("%-4s %-8s %-12s %8.2f %8.2f %8.2f %8.0f%% %8s %s"
% (tag, d["cn"], d["axis"], d["tau_gravity_Nm"], d["tau_dynamic_Nm"],
d["tau_required_peak_Nm"], d["duty_vs_Tallow_pct"],
("%.2fx" % d["peak_margin_x"]) if d["peak_margin_x"] else "-",
d["status"]))
print("\n许用连续力矩下可挂末端负载 (kg):")
for tag in res["joints"]:
print(" %-4s %s" % (tag, ("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
ok = [t for t, v in mp.items() if v is not None]
if ok:
w = min(ok, key=lambda t: mp[t])
print(" 整臂瓶颈 %s -> %.2f kg" % (w, mp[w]))
print("\n已写出 docs/torque_balance.json + docs/torque_report.md")
sys.stdout.flush()
print("\n" + "="*80)
print("TORQUE BALANCE SUMMARY (WITH PASSIVE SPRING & STIFFNESS OPTIMIZATION)")
print("="*80)
for jid, d in res["joints"].items():
print(f"[{jid}] Horiz Raw: {d['static_horizontal_raw_Nm']:5.2f} N·m | "
f"Opt: {d['static_horizontal_opt_Nm']:5.2f} N·m | "
f"Duty: {d['duty_cycle_opt_pct']:5.1f}% | "
f"Peak Margin: {d['peak_stall_safety_factor']:4.2f}x | Status: {d['status']}")
print("="*80)
os._exit(0)
if __name__ == "__main__":
main()
os._exit(0)
+134 -119
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long