feat: 增加组合折叠姿态干涉扫描、力矩实测与结构强度复核数据
This commit is contained in:
+187
-279
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user