12697 lines
497 KiB
Python
12697 lines
497 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
GLCanon 五轴刀具路径解析器 - 完整集成版
|
||
包含:RTCP运动学 + 完整G代码解析 + 梯形速度规划 + 虚拟HAL + 完整状态机 + JSON配置支持
|
||
集成 LinuxCNC 运动学算法:trtfuncs.c, 5axiskins.c, genhexkins.c, genserfuncs.c,
|
||
pumakins.c, scarakins.c, tripodkins.c, pentakins.c, rotarydeltakins.c, rotatekins.c,
|
||
corexykins.c, scorbot-kins.c, rosekins.c
|
||
集成 LinuxCNC 完整参数表系统 (linuxcnc_parameter_table)
|
||
|
||
版本: 5.0.0 - 完整集成所有 LinuxCNC 运动学算法和参数系统
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
import math
|
||
import time
|
||
import threading
|
||
import tempfile
|
||
import json
|
||
import traceback
|
||
from typing import List, Dict, Optional, Tuple, Any, Callable, Union, Set
|
||
from enum import IntEnum, Enum
|
||
from dataclasses import dataclass, field
|
||
from collections import deque, OrderedDict
|
||
from abc import ABC, abstractmethod
|
||
import dataclasses
|
||
|
||
# ==================== 导入 LinuxCNC 参数表系统 ====================
|
||
# ==================== 导入 LinuxCNC 参数表系统 ====================
|
||
try:
|
||
import linuxcnc_parameter_table as lcnc_param
|
||
from linuxcnc_parameter_table import (
|
||
LinuxCNCParameterTable,
|
||
InterpParameterIndex,
|
||
ParameterAttribute,
|
||
ParameterValue,
|
||
ExpressionEvaluator,
|
||
HALVariableManager,
|
||
SystemVariableQuery,
|
||
Context,
|
||
StateTag,
|
||
Block,
|
||
# G代码常量
|
||
G_0, G_1, G_2, G_3, G_4, G_5, G_5_1, G_5_2, G_5_3,
|
||
G_6, G_6_1, G_6_2, G_6_3, G_7, G_8, G_10,
|
||
G_17, G_17_1, G_18, G_18_1, G_19, G_19_1,
|
||
G_20, G_21, G_28, G_28_1, G_30, G_30_1,
|
||
G_33, G_33_1, G_38_2, G_38_3, G_38_4, G_38_5,
|
||
G_40, G_41, G_41_1, G_42, G_42_1,
|
||
G_43, G_43_1, G_43_2, G_49, G_52, G_53,
|
||
G_54, G_55, G_56, G_57, G_58, G_59,
|
||
G_59_1, G_59_2, G_59_3, G_61, G_61_1, G_64,
|
||
G_70, G_71, G_71_1, G_71_2, G_72, G_72_1, G_72_2,
|
||
G_73, G_74, G_76, G_80, G_81, G_82, G_83, G_84,
|
||
G_85, G_86, G_87, G_88, G_89, G_90, G_90_1,
|
||
G_91, G_91_1, G_92, G_92_1, G_92_2, G_92_3,
|
||
G_93, G_94, G_95, G_96, G_97, G_98, G_99,
|
||
# 常量
|
||
RS274NGC_MAX_PARAMETERS, READONLY_PARAMETERS, REQUIRED_PARAMETERS,
|
||
INTERP_SUB_PARAMS, INTERP_SUB_ROUTINE_LEVELS,
|
||
TOLERANCE_EQUAL, TOLERANCE_CONCAVE_CORNER,
|
||
EMCMOT_MAX_AXIS, EMCMOT_MAX_JOINTS, EMCMOT_MAX_SPINDLES,
|
||
)
|
||
except ImportError:
|
||
print("错误: 无法导入 linuxcnc_parameter_table 模块")
|
||
print("请确保 linuxcnc_parameter_table.py 在同一目录下")
|
||
sys.exit(1)
|
||
|
||
# ==================== 常量定义 ====================
|
||
VERSION = "5.0.0"
|
||
|
||
# 数学常量
|
||
CART_FUZZ = 1e-5
|
||
TO_RAD = math.pi / 180.0
|
||
TO_DEG = 180.0 / math.pi
|
||
GO_PI_2 = math.pi / 2.0
|
||
GO_PI = math.pi
|
||
PM_PI = math.pi
|
||
PM_2_PI = 2.0 * math.pi
|
||
TOLERANCE_EQUAL = 1e-6
|
||
TOLERANCE_CONCAVE_CORNER = 1e-5
|
||
TINY = 1e-7
|
||
|
||
# 单位转换
|
||
INCH_PER_MM = 0.03937007874016
|
||
MM_PER_INCH = 25.4
|
||
|
||
# 最大轴数
|
||
EMCMOT_MAX_AXIS = 9
|
||
EMCMOT_MAX_JOINTS = 9
|
||
EMCMOT_MAX_SPINDLES = 4
|
||
|
||
# 刀具表大小
|
||
CANON_POCKETS_MAX = 56
|
||
|
||
# 栈深度
|
||
STACK_LEN = 50
|
||
STACK_ENTRY_LEN = 80
|
||
|
||
# 行长度
|
||
LINELEN = 256
|
||
|
||
# 从 linuxcnc_parameter_table 导入常量
|
||
RS274NGC_MAX_PARAMETERS = lcnc_param.RS274NGC_MAX_PARAMETERS
|
||
READONLY_PARAMETERS = lcnc_param.READONLY_PARAMETERS
|
||
REQUIRED_PARAMETERS = lcnc_param.REQUIRED_PARAMETERS
|
||
INTERP_SUB_PARAMS = lcnc_param.INTERP_SUB_PARAMS
|
||
INTERP_SUB_ROUTINE_LEVELS = lcnc_param.INTERP_SUB_ROUTINE_LEVELS
|
||
|
||
# G代码常量(从lcnc_param导入)
|
||
G_0 = lcnc_param.G_0
|
||
G_1 = lcnc_param.G_1
|
||
G_2 = lcnc_param.G_2
|
||
G_3 = lcnc_param.G_3
|
||
G_4 = lcnc_param.G_4
|
||
G_5 = lcnc_param.G_5
|
||
G_5_1 = lcnc_param.G_5_1
|
||
G_5_2 = lcnc_param.G_5_2
|
||
G_5_3 = lcnc_param.G_5_3
|
||
G_6 = lcnc_param.G_6
|
||
G_6_1 = lcnc_param.G_6_1
|
||
G_6_2 = lcnc_param.G_6_2
|
||
G_6_3 = lcnc_param.G_6_3
|
||
G_7 = lcnc_param.G_7
|
||
G_8 = lcnc_param.G_8
|
||
G_10 = lcnc_param.G_10
|
||
G_17 = lcnc_param.G_17
|
||
G_17_1 = lcnc_param.G_17_1
|
||
G_18 = lcnc_param.G_18
|
||
G_18_1 = lcnc_param.G_18_1
|
||
G_19 = lcnc_param.G_19
|
||
G_19_1 = lcnc_param.G_19_1
|
||
G_20 = lcnc_param.G_20
|
||
G_21 = lcnc_param.G_21
|
||
G_28 = lcnc_param.G_28
|
||
G_28_1 = lcnc_param.G_28_1
|
||
G_30 = lcnc_param.G_30
|
||
G_30_1 = lcnc_param.G_30_1
|
||
G_33 = lcnc_param.G_33
|
||
G_33_1 = lcnc_param.G_33_1
|
||
G_38_2 = lcnc_param.G_38_2
|
||
G_38_3 = lcnc_param.G_38_3
|
||
G_38_4 = lcnc_param.G_38_4
|
||
G_38_5 = lcnc_param.G_38_5
|
||
G_40 = lcnc_param.G_40
|
||
G_41 = lcnc_param.G_41
|
||
G_41_1 = lcnc_param.G_41_1
|
||
G_42 = lcnc_param.G_42
|
||
G_42_1 = lcnc_param.G_42_1
|
||
G_43 = lcnc_param.G_43
|
||
G_43_1 = lcnc_param.G_43_1
|
||
G_43_2 = lcnc_param.G_43_2
|
||
G_49 = lcnc_param.G_49
|
||
G_52 = lcnc_param.G_52
|
||
G_53 = lcnc_param.G_53
|
||
G_54 = lcnc_param.G_54
|
||
G_55 = lcnc_param.G_55
|
||
G_56 = lcnc_param.G_56
|
||
G_57 = lcnc_param.G_57
|
||
G_58 = lcnc_param.G_58
|
||
G_59 = lcnc_param.G_59
|
||
G_59_1 = lcnc_param.G_59_1
|
||
G_59_2 = lcnc_param.G_59_2
|
||
G_59_3 = lcnc_param.G_59_3
|
||
G_61 = lcnc_param.G_61
|
||
G_61_1 = lcnc_param.G_61_1
|
||
G_64 = lcnc_param.G_64
|
||
G_70 = lcnc_param.G_70
|
||
G_71 = lcnc_param.G_71
|
||
G_71_1 = lcnc_param.G_71_1
|
||
G_71_2 = lcnc_param.G_71_2
|
||
G_72 = lcnc_param.G_72
|
||
G_72_1 = lcnc_param.G_72_1
|
||
G_72_2 = lcnc_param.G_72_2
|
||
G_73 = lcnc_param.G_73
|
||
G_74 = lcnc_param.G_74
|
||
G_76 = lcnc_param.G_76
|
||
G_80 = lcnc_param.G_80
|
||
G_81 = lcnc_param.G_81
|
||
G_82 = lcnc_param.G_82
|
||
G_83 = lcnc_param.G_83
|
||
G_84 = lcnc_param.G_84
|
||
G_85 = lcnc_param.G_85
|
||
G_86 = lcnc_param.G_86
|
||
G_87 = lcnc_param.G_87
|
||
G_88 = lcnc_param.G_88
|
||
G_89 = lcnc_param.G_89
|
||
G_90 = lcnc_param.G_90
|
||
G_90_1 = lcnc_param.G_90_1
|
||
G_91 = lcnc_param.G_91
|
||
G_91_1 = lcnc_param.G_91_1
|
||
G_92 = lcnc_param.G_92
|
||
G_92_1 = lcnc_param.G_92_1
|
||
G_92_2 = lcnc_param.G_92_2
|
||
G_92_3 = lcnc_param.G_92_3
|
||
G_93 = lcnc_param.G_93
|
||
G_94 = lcnc_param.G_94
|
||
G_95 = lcnc_param.G_95
|
||
G_96 = lcnc_param.G_96
|
||
G_97 = lcnc_param.G_97
|
||
G_98 = lcnc_param.G_98
|
||
G_99 = lcnc_param.G_99
|
||
|
||
# 默认颜色字典
|
||
DEFAULT_COLORS = {
|
||
'back': '#000000',
|
||
'grid': '#404040',
|
||
'axis': '#c0c0c0',
|
||
'rapid': '#00ff00',
|
||
'feed': '#ffff00',
|
||
'arc_cw': '#00ffff',
|
||
'arc_ccw': '#ff00ff',
|
||
'dwell': '#ffffff',
|
||
'probe': '#ff8000',
|
||
'selected': '#ff0000',
|
||
'traverse': '#00ff00',
|
||
'straight_feed': '#ffff00',
|
||
'arc_feed': '#00ffff',
|
||
'm1xx': '#ff8000',
|
||
'rtcp_on': '#00ff88',
|
||
'rtcp_off': '#ff6600',
|
||
'program_end': '#ff0000',
|
||
'g0': '#00ff00',
|
||
'g1': '#ffff00',
|
||
'g2': '#00ffff',
|
||
'g3': '#ff00ff',
|
||
'g38': '#ff8000',
|
||
'g80': '#808080',
|
||
'm6': '#ff0000',
|
||
'm3': '#00ff00',
|
||
'm4': '#ff0000',
|
||
'm5': '#ffff00',
|
||
'm7': '#0088ff',
|
||
'm8': '#0088ff',
|
||
'm9': '#ff0000',
|
||
}
|
||
|
||
|
||
# ==================== 纯 Python 实现的包围盒计算 ====================
|
||
def calc_extents_python(*lists):
|
||
"""
|
||
计算刀具路径的包围盒(纯 Python 实现,替代 gcode.calc_extents)
|
||
"""
|
||
INF = 9e99
|
||
|
||
min_x = min_y = min_z = INF
|
||
max_x = max_y = max_z = -INF
|
||
|
||
min_xt = min_yt = min_zt = INF
|
||
max_xt = max_yt = max_zt = -INF
|
||
|
||
for seq in lists:
|
||
if not seq:
|
||
continue
|
||
|
||
for item in seq:
|
||
if len(item) == 5:
|
||
linenum, start, end, feed, tooloffset = item
|
||
elif len(item) == 4:
|
||
linenum, start, end, tooloffset = item
|
||
else:
|
||
continue
|
||
|
||
xs, ys, zs = start[0], start[1], start[2]
|
||
xe, ye, ze = end[0], end[1], end[2]
|
||
|
||
if len(tooloffset) >= 3:
|
||
xt, yt, zt = tooloffset[0], tooloffset[1], tooloffset[2]
|
||
else:
|
||
xt = yt = zt = 0.0
|
||
|
||
for (x, y, z) in [(xs, ys, zs), (xe, ye, ze)]:
|
||
if x < min_x: min_x = x
|
||
if x > max_x: max_x = x
|
||
if y < min_y: min_y = y
|
||
if y > max_y: max_y = y
|
||
if z < min_z: min_z = z
|
||
if z > max_z: max_z = z
|
||
|
||
for (x, y, z) in [(xs + xt, ys + yt, zs + zt), (xe + xt, ye + yt, ze + zt)]:
|
||
if x < min_xt: min_xt = x
|
||
if x > max_xt: max_xt = x
|
||
if y < min_yt: min_yt = y
|
||
if y > max_yt: max_yt = y
|
||
if z < min_zt: min_zt = z
|
||
if z > max_zt: max_zt = z
|
||
|
||
if min_x == INF:
|
||
min_x = min_y = min_z = max_x = max_y = max_z = 0
|
||
if min_xt == INF:
|
||
min_xt = min_yt = min_zt = max_xt = max_yt = max_zt = 0
|
||
|
||
return (
|
||
[min_x, min_y, min_z], [max_x, max_y, max_z],
|
||
[min_xt, min_yt, min_zt], [max_xt, max_yt, max_zt]
|
||
)
|
||
|
||
|
||
# ==================== 纯 Python 实现的圆弧转线段 ====================
|
||
def arcs_to_segments_python(
|
||
canon_obj: Any,
|
||
x1: float, y1: float,
|
||
cx: float, cy: float,
|
||
rot: int,
|
||
z1: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float,
|
||
max_segments: int = 128
|
||
) -> List[Tuple[float, ...]]:
|
||
"""
|
||
圆弧转线段 - 纯 Python 实现,替代 gcode.arc_to_segments
|
||
"""
|
||
if hasattr(canon_obj, 'lo'):
|
||
o = list(canon_obj.lo)
|
||
else:
|
||
o = [0.0] * 9
|
||
|
||
while len(o) < 9:
|
||
o.append(0.0)
|
||
|
||
plane = getattr(canon_obj, 'plane', 17)
|
||
|
||
rotation_cos = getattr(canon_obj, 'rotation_cos', 1.0)
|
||
rotation_sin = getattr(canon_obj, 'rotation_sin', 0.0)
|
||
|
||
g5x_offset = [
|
||
getattr(canon_obj, 'g5x_offset_x', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_y', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_z', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_a', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_b', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_c', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_u', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_v', 0.0),
|
||
getattr(canon_obj, 'g5x_offset_w', 0.0)
|
||
]
|
||
|
||
g92_offset = [
|
||
getattr(canon_obj, 'g92_offset_x', 0.0),
|
||
getattr(canon_obj, 'g92_offset_y', 0.0),
|
||
getattr(canon_obj, 'g92_offset_z', 0.0),
|
||
getattr(canon_obj, 'g92_offset_a', 0.0),
|
||
getattr(canon_obj, 'g92_offset_b', 0.0),
|
||
getattr(canon_obj, 'g92_offset_c', 0.0),
|
||
getattr(canon_obj, 'g92_offset_u', 0.0),
|
||
getattr(canon_obj, 'g92_offset_v', 0.0),
|
||
getattr(canon_obj, 'g92_offset_w', 0.0)
|
||
]
|
||
|
||
if plane == 17:
|
||
X, Y, Z = 0, 1, 2
|
||
elif plane == 19:
|
||
X, Y, Z = 2, 0, 1
|
||
else:
|
||
X, Y, Z = 1, 2, 0
|
||
|
||
n = [0.0] * 9
|
||
n[X] = x1
|
||
n[Y] = y1
|
||
n[Z] = z1
|
||
n[3] = a
|
||
n[4] = b
|
||
n[5] = c
|
||
n[6] = u
|
||
n[7] = v
|
||
n[8] = w
|
||
|
||
for ax in range(9):
|
||
o[ax] -= g5x_offset[ax]
|
||
|
||
tx = o[X]
|
||
ty = o[Y]
|
||
o[X] = tx * rotation_cos + ty * rotation_sin
|
||
o[Y] = -tx * rotation_sin + ty * rotation_cos
|
||
|
||
for ax in range(9):
|
||
o[ax] -= g92_offset[ax]
|
||
|
||
theta1 = math.atan2(o[Y] - cy, o[X] - cx)
|
||
theta2 = math.atan2(n[Y] - cy, n[X] - cx)
|
||
len_arc = math.hypot(o[X] - n[X], o[Y] - n[Y])
|
||
|
||
if rot < 0:
|
||
if theta1 < theta2:
|
||
theta2 -= 2.0 * math.pi
|
||
if len_arc < CART_FUZZ:
|
||
theta2 -= 2.0 * math.pi
|
||
else:
|
||
if theta1 > theta2:
|
||
theta2 += 2.0 * math.pi
|
||
if len_arc < CART_FUZZ:
|
||
theta2 += 2.0 * math.pi
|
||
|
||
if rot < -1:
|
||
theta2 += 2.0 * math.pi * (rot + 1)
|
||
if rot > 1:
|
||
theta2 += 2.0 * math.pi * (rot - 1)
|
||
|
||
delta_theta = theta2 - theta1
|
||
steps = max(3, int(max_segments * abs(delta_theta) / math.pi))
|
||
rsteps = 1.0 / steps
|
||
|
||
d = [0.0] * 9
|
||
d[Z] = n[Z] - o[Z]
|
||
d[3] = n[3] - o[3]
|
||
d[4] = n[4] - o[4]
|
||
d[5] = n[5] - o[5]
|
||
d[6] = n[6] - o[6]
|
||
d[7] = n[7] - o[7]
|
||
d[8] = n[8] - o[8]
|
||
|
||
segs = []
|
||
tx = o[X] - cx
|
||
ty = o[Y] - cy
|
||
dc = math.cos(delta_theta * rsteps)
|
||
ds = math.sin(delta_theta * rsteps)
|
||
|
||
for i in range(steps - 1):
|
||
f = (i + 1) * rsteps
|
||
new_tx = tx * dc - ty * ds
|
||
new_ty = tx * ds + ty * dc
|
||
tx = new_tx
|
||
ty = new_ty
|
||
|
||
p = [0.0] * 9
|
||
p[X] = tx + cx
|
||
p[Y] = ty + cy
|
||
p[Z] = o[Z] + d[Z] * f
|
||
p[3] = o[3] + d[3] * f
|
||
p[4] = o[4] + d[4] * f
|
||
p[5] = o[5] + d[5] * f
|
||
p[6] = o[6] + d[6] * f
|
||
p[7] = o[7] + d[7] * f
|
||
p[8] = o[8] + d[8] * f
|
||
|
||
for ax in range(9):
|
||
p[ax] += g92_offset[ax]
|
||
|
||
px = p[X]
|
||
py = p[Y]
|
||
p[X] = px * rotation_cos - py * rotation_sin
|
||
p[Y] = px * rotation_sin + py * rotation_cos
|
||
|
||
for ax in range(9):
|
||
p[ax] += g5x_offset[ax]
|
||
|
||
segs.append(tuple(p))
|
||
|
||
for ax in range(9):
|
||
n[ax] += g92_offset[ax]
|
||
|
||
nx = n[X]
|
||
ny = n[Y]
|
||
n[X] = nx * rotation_cos - ny * rotation_sin
|
||
n[Y] = nx * rotation_sin + ny * rotation_cos
|
||
|
||
for ax in range(9):
|
||
n[ax] += g5x_offset[ax]
|
||
|
||
segs.append(tuple(n))
|
||
|
||
return segs
|
||
|
||
|
||
# ==================== 单位转换函数 ====================
|
||
def convert_units(value: float, from_units: int, to_units: int) -> float:
|
||
"""单位转换"""
|
||
if from_units == to_units:
|
||
return value
|
||
if from_units == 20 and to_units == 21: # 英寸 -> 毫米
|
||
return value * MM_PER_INCH
|
||
if from_units == 21 and to_units == 20: # 毫米 -> 英寸
|
||
return value * INCH_PER_MM
|
||
return value
|
||
|
||
|
||
def program_to_user_len(value: float, units: int) -> float:
|
||
"""程序单位 -> 用户单位(长度)"""
|
||
return value
|
||
|
||
|
||
def user_to_program_len(value: float, units: int) -> float:
|
||
"""用户单位 -> 程序单位(长度)"""
|
||
return value
|
||
|
||
|
||
def program_to_user_ang(value: float) -> float:
|
||
"""程序单位 -> 用户单位(角度)"""
|
||
return value
|
||
|
||
|
||
def user_to_program_ang(value: float) -> float:
|
||
"""用户单位 -> 程序单位(角度)"""
|
||
return value
|
||
|
||
|
||
# ==================== JSON配置数据类 ====================
|
||
@dataclass
|
||
class ProgramConfig:
|
||
"""程序配置"""
|
||
name: str = ""
|
||
description: str = ""
|
||
content: str = ""
|
||
|
||
|
||
@dataclass
|
||
class SubroutineConfig:
|
||
"""子程序配置"""
|
||
name: str = ""
|
||
description: str = ""
|
||
content: str = ""
|
||
parameters: List[Dict[str, Any]] = field(default_factory=list)
|
||
|
||
|
||
@dataclass
|
||
class MachineConfig:
|
||
"""机床配置数据类"""
|
||
name: str = ""
|
||
type: str = "5axis_mill"
|
||
kinematics_type: str = "TRT_BC"
|
||
kinematics_params: Dict[str, Any] = field(default_factory=dict)
|
||
axes: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||
spindle: Dict[str, Any] = field(default_factory=dict)
|
||
tool_table: List[Dict[str, Any]] = field(default_factory=list)
|
||
work_offsets: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||
|
||
|
||
@dataclass
|
||
class SimulationConfig:
|
||
"""仿真配置"""
|
||
acceleration: float = 500.0
|
||
deceleration: float = 500.0
|
||
max_rapid_rate: float = 10000.0
|
||
max_feed_rate: float = 5000.0
|
||
arc_division: int = 64
|
||
enable_rtcp_debug: bool = False
|
||
max_points: int = 50000
|
||
colors: Dict[str, str] = field(default_factory=dict)
|
||
rapid_override: float = 1.0
|
||
feed_override: float = 1.0
|
||
spindle_override: float = 1.0
|
||
|
||
|
||
@dataclass
|
||
class HALPinConfig:
|
||
"""HAL引脚配置"""
|
||
name: str = ""
|
||
pin_type: str = "float"
|
||
direction: str = "in"
|
||
default: Any = 0.0
|
||
min_val: Optional[float] = None
|
||
max_val: Optional[float] = None
|
||
description: str = ""
|
||
|
||
|
||
@dataclass
|
||
class HALConfig:
|
||
"""HAL配置数据类"""
|
||
version: str = "1.0"
|
||
description: str = ""
|
||
pins: Dict[str, HALPinConfig] = field(default_factory=dict)
|
||
connections: List[Dict[str, str]] = field(default_factory=list)
|
||
|
||
|
||
@dataclass
|
||
class VisualizationConfig:
|
||
"""可视化配置"""
|
||
camera: Dict[str, Any] = field(default_factory=dict)
|
||
grid: Dict[str, Any] = field(default_factory=dict)
|
||
show_tool: bool = True
|
||
show_path: bool = True
|
||
show_coord_systems: bool = True
|
||
show_workpiece: Dict[str, Any] = field(default_factory=dict)
|
||
show_grid: bool = True
|
||
show_axes: bool = True
|
||
|
||
|
||
@dataclass
|
||
class ProjectConfig:
|
||
"""项目完整配置"""
|
||
project: Dict[str, str] = field(default_factory=dict)
|
||
machine: MachineConfig = field(default_factory=MachineConfig)
|
||
hal_config: HALConfig = field(default_factory=HALConfig)
|
||
main_program: ProgramConfig = field(default_factory=ProgramConfig)
|
||
subroutines: List[SubroutineConfig] = field(default_factory=list)
|
||
simulation_config: SimulationConfig = field(default_factory=SimulationConfig)
|
||
visualization: VisualizationConfig = field(default_factory=VisualizationConfig)
|
||
execution_order: List[Dict[str, str]] = field(default_factory=list)
|
||
|
||
|
||
# ==================== 运动学抽象基类 ====================
|
||
|
||
@dataclass
|
||
class KinematicsParams:
|
||
"""运动学参数基类"""
|
||
pass
|
||
|
||
|
||
class BaseKinematics(ABC):
|
||
"""运动学抽象基类"""
|
||
|
||
def __init__(self, params: KinematicsParams = None):
|
||
self.params = params
|
||
self.rtcp_enabled = True
|
||
self.debug = False
|
||
self._tool_length = 0.0
|
||
|
||
@abstractmethod
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""
|
||
正运动学:关节坐标 -> 世界坐标 (刀尖点)
|
||
参数: joints = [轴1, 轴2, ...]
|
||
返回: (X, Y, Z, A, B, C)
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
"""
|
||
逆运动学:世界坐标 (刀尖点) -> 关节坐标
|
||
参数: world = (X, Y, Z, A, B, C)
|
||
返回: [轴1, 轴2, ...]
|
||
"""
|
||
pass
|
||
|
||
def set_tool_length(self, length: float):
|
||
"""设置刀具长度 (用于 RTCP)"""
|
||
self._tool_length = length
|
||
|
||
def get_tool_length(self) -> float:
|
||
"""获取刀具长度"""
|
||
return self._tool_length
|
||
|
||
def enable_rtcp(self, enable: bool = True):
|
||
"""启用/禁用 RTCP"""
|
||
self.rtcp_enabled = enable
|
||
|
||
def set_params(self, **kwargs):
|
||
"""更新运动学参数"""
|
||
if self.params:
|
||
for key, value in kwargs.items():
|
||
if hasattr(self.params, key):
|
||
setattr(self.params, key, value)
|
||
|
||
def joints_to_world(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""关节坐标转世界坐标 (别名)"""
|
||
if not self.rtcp_enabled:
|
||
if len(joints) >= 6:
|
||
return (joints[0], joints[1], joints[2], joints[3], joints[4], joints[5])
|
||
return (joints[0], joints[1], joints[2], 0.0, 0.0, 0.0)
|
||
return self.forward(joints)
|
||
|
||
def world_to_joints(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
"""世界坐标转关节坐标 (别名)"""
|
||
if not self.rtcp_enabled:
|
||
result = [world[0], world[1], world[2], world[3], world[4], world[5]]
|
||
while len(result) < EMCMOT_MAX_JOINTS:
|
||
result.append(0.0)
|
||
return result
|
||
result = self.inverse(world)
|
||
while len(result) < EMCMOT_MAX_JOINTS:
|
||
result.append(0.0)
|
||
return result
|
||
|
||
|
||
# ==================== 运动学类型枚举 ====================
|
||
|
||
class KinematicsType(Enum):
|
||
"""五轴运动学类型 - 基于LinuxCNC实现"""
|
||
IDENTITY = 0
|
||
TRT_AC = 1
|
||
TRT_BC = 2
|
||
MAXKINS_BC = 3
|
||
FIVEAXIS_BC = 4
|
||
HEXAPOD = 5
|
||
SERIAL_DH = 6
|
||
PUMA = 7
|
||
SCARA = 8
|
||
SCORBOT = 9
|
||
LINEAR_DELTA = 10
|
||
ROTARY_DELTA = 11
|
||
TRIPOD = 12
|
||
PENTAPOD = 13
|
||
ROTATE = 14
|
||
COREXY = 15
|
||
ROSE = 16
|
||
|
||
|
||
# 运动学关节配置
|
||
KINEMATICS_JOINT_CONFIG = {
|
||
KinematicsType.IDENTITY: {
|
||
'num_joints': 6,
|
||
'joint_names': ['X', 'Y', 'Z', 'A', 'B', 'C'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': 3, 'b_idx': 4, 'c_idx': 5,
|
||
},
|
||
KinematicsType.TRT_AC: {
|
||
'num_joints': 5,
|
||
'joint_names': ['X', 'Y', 'Z', 'A', 'C'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': 3, 'b_idx': -1, 'c_idx': 4,
|
||
},
|
||
KinematicsType.TRT_BC: {
|
||
'num_joints': 5,
|
||
'joint_names': ['X', 'Y', 'Z', 'B', 'C'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': -1, 'b_idx': 3, 'c_idx': 4,
|
||
},
|
||
KinematicsType.MAXKINS_BC: {
|
||
'num_joints': 9,
|
||
'joint_names': ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': 3, 'b_idx': 4, 'c_idx': 5,
|
||
},
|
||
KinematicsType.FIVEAXIS_BC: {
|
||
'num_joints': 6,
|
||
'joint_names': ['X', 'Y', 'Z', 'B', 'C', 'W'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': -1, 'b_idx': 3, 'c_idx': 4,
|
||
},
|
||
KinematicsType.HEXAPOD: {
|
||
'num_joints': 6,
|
||
'joint_names': ['S1', 'S2', 'S3', 'S4', 'S5', 'S6'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.PUMA: {
|
||
'num_joints': 6,
|
||
'joint_names': ['J1', 'J2', 'J3', 'J4', 'J5', 'J6'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.SCARA: {
|
||
'num_joints': 4,
|
||
'joint_names': ['J1', 'J2', 'Z', 'J4'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': 2,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': 3,
|
||
},
|
||
KinematicsType.SCORBOT: {
|
||
'num_joints': 5,
|
||
'joint_names': ['J0', 'J1', 'J2', 'J3', 'J4'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': 3, 'b_idx': 4, 'c_idx': -1,
|
||
},
|
||
KinematicsType.LINEAR_DELTA: {
|
||
'num_joints': 3,
|
||
'joint_names': ['Z1', 'Z2', 'Z3'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.ROTARY_DELTA: {
|
||
'num_joints': 3,
|
||
'joint_names': ['T1', 'T2', 'T3'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.TRIPOD: {
|
||
'num_joints': 3,
|
||
'joint_names': ['L1', 'L2', 'L3'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.PENTAPOD: {
|
||
'num_joints': 5,
|
||
'joint_names': ['L1', 'L2', 'L3', 'L4', 'L5'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': -1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': -1,
|
||
},
|
||
KinematicsType.ROTATE: {
|
||
'num_joints': 9,
|
||
'joint_names': ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': 3, 'b_idx': 4, 'c_idx': 5,
|
||
},
|
||
KinematicsType.COREXY: {
|
||
'num_joints': 9,
|
||
'joint_names': ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'],
|
||
'x_idx': 0, 'y_idx': 1, 'z_idx': 2,
|
||
'a_idx': 3, 'b_idx': 4, 'c_idx': 5,
|
||
},
|
||
KinematicsType.ROSE: {
|
||
'num_joints': 3,
|
||
'joint_names': ['R', 'Z', 'Theta'],
|
||
'x_idx': -1, 'y_idx': -1, 'z_idx': 1,
|
||
'a_idx': -1, 'b_idx': -1, 'c_idx': 2,
|
||
},
|
||
}
|
||
|
||
|
||
class MoveType(IntEnum):
|
||
"""运动类型枚举"""
|
||
RAPID = 0
|
||
FEED = 1
|
||
ARC_CW = 2
|
||
ARC_CCW = 3
|
||
DWELL = 4
|
||
PROBE = 5
|
||
RTCP_ON = 6
|
||
RTCP_OFF = 7
|
||
PROGRAM_END = 8
|
||
TOOL_CHANGE = 9
|
||
SPINDLE_ON = 10
|
||
SPINDLE_OFF = 11
|
||
COOLANT_ON = 12
|
||
COOLANT_OFF = 13
|
||
COMMENT = 14
|
||
MESSAGE = 15
|
||
USER_DEFINED = 16
|
||
WAIT = 17
|
||
|
||
|
||
class CompType(IntEnum):
|
||
"""刀具补偿类型"""
|
||
OFF = 0
|
||
LEFT = 1
|
||
RIGHT = 2
|
||
DYNAMIC = 3
|
||
|
||
|
||
class MachineState(Enum):
|
||
"""机床状态枚举"""
|
||
OFF = "OFF"
|
||
RESET = "RESET"
|
||
ESTOP = "ESTOP"
|
||
IDLE = "IDLE"
|
||
RUNNING = "RUNNING"
|
||
PAUSED = "PAUSED"
|
||
HOLDING = "HOLDING"
|
||
JOGGING = "JOGGING"
|
||
HOMING = "HOMING"
|
||
ALARM = "ALARM"
|
||
MDI = "MDI"
|
||
PROBING = "PROBING"
|
||
TOOL_CHANGING = "TOOL_CHANGING"
|
||
|
||
|
||
class SpindleState(Enum):
|
||
"""主轴状态"""
|
||
OFF = "OFF"
|
||
CW = "CW"
|
||
CCW = "CCW"
|
||
|
||
|
||
class CoolantState(Enum):
|
||
"""冷却液状态"""
|
||
OFF = "OFF"
|
||
FLOOD = "FLOOD"
|
||
MIST = "MIST"
|
||
BOTH = "BOTH"
|
||
|
||
|
||
class FeedMode(Enum):
|
||
"""进给模式"""
|
||
UNITS_PER_MINUTE = 94
|
||
UNITS_PER_REVOLUTION = 95
|
||
INVERSE_TIME = 93
|
||
|
||
|
||
class DistanceMode(Enum):
|
||
"""距离模式"""
|
||
ABSOLUTE = 90
|
||
INCREMENTAL = 91
|
||
|
||
|
||
class PlaneMode(Enum):
|
||
"""平面模式"""
|
||
XY = 17
|
||
XZ = 18
|
||
YZ = 19
|
||
UV = 171
|
||
UW = 181
|
||
VW = 191
|
||
|
||
|
||
class UnitsMode(Enum):
|
||
"""单位模式"""
|
||
INCHES = 20
|
||
MM = 21
|
||
|
||
|
||
@dataclass
|
||
class Point6D:
|
||
"""六维点 (X, Y, Z, A, B, C)"""
|
||
x: float = 0.0
|
||
y: float = 0.0
|
||
z: float = 0.0
|
||
a: float = 0.0
|
||
b: float = 0.0
|
||
c: float = 0.0
|
||
u: float = 0.0
|
||
v: float = 0.0
|
||
w: float = 0.0
|
||
|
||
def to_list(self) -> List[float]:
|
||
return [self.x, self.y, self.z, self.a, self.b, self.c, self.u, self.v, self.w]
|
||
|
||
def to_tuple(self) -> Tuple[float, ...]:
|
||
return (self.x, self.y, self.z, self.a, self.b, self.c, self.u, self.v, self.w)
|
||
|
||
def to_xyz_tuple(self) -> Tuple[float, float, float]:
|
||
return (self.x, self.y, self.z)
|
||
|
||
def to_xyzabc_tuple(self) -> Tuple[float, float, float, float, float, float]:
|
||
return (self.x, self.y, self.z, self.a, self.b, self.c)
|
||
|
||
def distance_to(self, other: 'Point6D') -> float:
|
||
dx = self.x - other.x
|
||
dy = self.y - other.y
|
||
dz = self.z - other.z
|
||
return math.sqrt(dx*dx + dy*dy + dz*dz)
|
||
|
||
def distance_to_6d(self, other: 'Point6D') -> float:
|
||
"""计算六维距离(包含旋转轴)"""
|
||
dx = self.x - other.x
|
||
dy = self.y - other.y
|
||
dz = self.z - other.z
|
||
da = self.a - other.a
|
||
db = self.b - other.b
|
||
dc = self.c - other.c
|
||
return math.sqrt(dx*dx + dy*dy + dz*dz + da*da + db*db + dc*dc)
|
||
|
||
def copy(self) -> 'Point6D':
|
||
return Point6D(self.x, self.y, self.z, self.a, self.b, self.c, self.u, self.v, self.w)
|
||
|
||
def to_dict(self) -> Dict[str, float]:
|
||
return {'x': self.x, 'y': self.y, 'z': self.z,
|
||
'a': self.a, 'b': self.b, 'c': self.c,
|
||
'u': self.u, 'v': self.v, 'w': self.w}
|
||
|
||
@classmethod
|
||
def from_list(cls, lst: List[float]) -> 'Point6D':
|
||
if len(lst) >= 9:
|
||
return cls(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5], lst[6], lst[7], lst[8])
|
||
elif len(lst) >= 6:
|
||
return cls(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5], 0, 0, 0)
|
||
elif len(lst) >= 3:
|
||
return cls(lst[0], lst[1], lst[2], 0, 0, 0, 0, 0, 0)
|
||
return cls()
|
||
|
||
@classmethod
|
||
def from_xyzabc(cls, x: float, y: float, z: float, a: float, b: float, c: float) -> 'Point6D':
|
||
return cls(x, y, z, a, b, c, 0, 0, 0)
|
||
|
||
def __add__(self, other: 'Point6D') -> 'Point6D':
|
||
return Point6D(
|
||
self.x + other.x, self.y + other.y, self.z + other.z,
|
||
self.a + other.a, self.b + other.b, self.c + other.c,
|
||
self.u + other.u, self.v + other.v, self.w + other.w
|
||
)
|
||
|
||
def __sub__(self, other: 'Point6D') -> 'Point6D':
|
||
return Point6D(
|
||
self.x - other.x, self.y - other.y, self.z - other.z,
|
||
self.a - other.a, self.b - other.b, self.c - other.c,
|
||
self.u - other.u, self.v - other.v, self.w - other.w
|
||
)
|
||
|
||
def __mul__(self, scalar: float) -> 'Point6D':
|
||
return Point6D(
|
||
self.x * scalar, self.y * scalar, self.z * scalar,
|
||
self.a * scalar, self.b * scalar, self.c * scalar,
|
||
self.u * scalar, self.v * scalar, self.w * scalar
|
||
)
|
||
|
||
def __truediv__(self, scalar: float) -> 'Point6D':
|
||
if scalar == 0:
|
||
return self.copy()
|
||
return Point6D(
|
||
self.x / scalar, self.y / scalar, self.z / scalar,
|
||
self.a / scalar, self.b / scalar, self.c / scalar,
|
||
self.u / scalar, self.v / scalar, self.w / scalar
|
||
)
|
||
|
||
def __eq__(self, other: 'Point6D') -> bool:
|
||
if not isinstance(other, Point6D):
|
||
return False
|
||
return (abs(self.x - other.x) < TOLERANCE_EQUAL and
|
||
abs(self.y - other.y) < TOLERANCE_EQUAL and
|
||
abs(self.z - other.z) < TOLERANCE_EQUAL and
|
||
abs(self.a - other.a) < TOLERANCE_EQUAL and
|
||
abs(self.b - other.b) < TOLERANCE_EQUAL and
|
||
abs(self.c - other.c) < TOLERANCE_EQUAL)
|
||
|
||
def __str__(self) -> str:
|
||
return f"Point6D(x={self.x:.3f}, y={self.y:.3f}, z={self.z:.3f}, a={self.a:.3f}, b={self.b:.3f}, c={self.c:.3f})"
|
||
|
||
def __repr__(self) -> str:
|
||
return self.__str__()
|
||
|
||
|
||
@dataclass
|
||
class ToolData:
|
||
"""刀具数据 - 完整版"""
|
||
tool_number: int = 0
|
||
pocket: int = 0
|
||
diameter: float = 0.0
|
||
radius: float = 0.0
|
||
length_offset: float = 0.0
|
||
offset: Point6D = field(default_factory=Point6D)
|
||
front_angle: float = 0.0
|
||
back_angle: float = 0.0
|
||
orientation: int = 0
|
||
name: str = ""
|
||
comment: str = ""
|
||
|
||
def __post_init__(self):
|
||
if self.diameter > 0 and self.radius == 0:
|
||
self.radius = self.diameter / 2.0
|
||
elif self.radius > 0 and self.diameter == 0:
|
||
self.diameter = self.radius * 2.0
|
||
|
||
|
||
@dataclass
|
||
class MoveSegment:
|
||
"""运动段数据"""
|
||
type: MoveType
|
||
start: Point6D
|
||
end: Point6D
|
||
line_number: int = 0
|
||
feedrate: float = 0.0
|
||
center_x: float = 0.0
|
||
center_y: float = 0.0
|
||
center_z: float = 0.0
|
||
radius: float = 0.0
|
||
turn: int = 0
|
||
dwell_time: float = 0.0
|
||
comp_type: CompType = CompType.OFF
|
||
comp_radius: float = 0.0
|
||
d_word: float = 0.0
|
||
duration: float = 0.0
|
||
max_velocity: float = 0.0
|
||
acceleration_time: float = 0.0
|
||
constant_time: float = 0.0
|
||
deceleration_time: float = 0.0
|
||
profile_type: str = ""
|
||
is_rtcp: bool = False
|
||
world_start: Optional[Point6D] = None
|
||
world_end: Optional[Point6D] = None
|
||
gcode: str = ""
|
||
machine_start: Optional[Point6D] = None # 绝对坐标起点
|
||
machine_end: Optional[Point6D] = None # 绝对坐标终点
|
||
tool_number: int = 0
|
||
spindle_speed: float = 0.0
|
||
spindle_state: str = ""
|
||
coolant_state: str = ""
|
||
comment: str = ""
|
||
message: str = ""
|
||
probe_tripped: bool = False
|
||
|
||
# ===== 新增字段 =====
|
||
coord_system: str = "WORLD" # "WORLD" | "ABSOLUTE" | "MACHINE"
|
||
active_csys: int = 1 # 当前激活的坐标系 (1=G54, 2=G55, ...)
|
||
g92_active: bool = False # G92 是否激活
|
||
|
||
def __post_init__(self):
|
||
"""初始化后处理:确保坐标完整性"""
|
||
# 如果 world_start 为空,使用 start 作为默认值
|
||
if self.world_start is None:
|
||
self.world_start = self.start.copy() if self.start else None
|
||
|
||
# 如果 world_end 为空,使用 end 作为默认值
|
||
if self.world_end is None:
|
||
self.world_end = self.end.copy() if self.end else None
|
||
|
||
# 确保圆弧段有正确的圆心信息
|
||
if self.type in [MoveType.ARC_CW, MoveType.ARC_CCW]:
|
||
if self.radius == 0 and self.world_start and self.center_x:
|
||
self.radius = math.hypot(
|
||
self.world_start.x - self.center_x,
|
||
self.world_start.y - self.center_y
|
||
)
|
||
|
||
def get_color(self, colors: Dict[str, str] = None) -> str:
|
||
"""获取运动段的显示颜色"""
|
||
if colors is None:
|
||
colors = DEFAULT_COLORS
|
||
|
||
if self.type == MoveType.RAPID:
|
||
return colors.get('rapid', '#00ff00')
|
||
elif self.type == MoveType.FEED:
|
||
return colors.get('feed', '#ffff00')
|
||
elif self.type == MoveType.ARC_CW:
|
||
return colors.get('arc_cw', '#00ffff')
|
||
elif self.type == MoveType.ARC_CCW:
|
||
return colors.get('arc_ccw', '#ff00ff')
|
||
elif self.type == MoveType.DWELL:
|
||
return colors.get('dwell', '#ffffff')
|
||
elif self.type == MoveType.PROBE:
|
||
return colors.get('probe', '#ff8000')
|
||
elif self.type == MoveType.RTCP_ON:
|
||
return colors.get('rtcp_on', '#00ff88')
|
||
elif self.type == MoveType.RTCP_OFF:
|
||
return colors.get('rtcp_off', '#ff6600')
|
||
elif self.type == MoveType.PROGRAM_END:
|
||
return colors.get('program_end', '#ff0000')
|
||
else:
|
||
return '#ffffff'
|
||
|
||
|
||
|
||
# ===== 新增:坐标系兼容属性 =====
|
||
|
||
@property
|
||
def effective_start(self) -> Point6D:
|
||
"""获取有效的起点(始终返回工件坐标)"""
|
||
if self.world_start is not None:
|
||
return self.world_start
|
||
return self.start
|
||
|
||
@property
|
||
def effective_end(self) -> Point6D:
|
||
"""获取有效的终点(始终返回工件坐标)"""
|
||
if self.world_end is not None:
|
||
return self.world_end
|
||
return self.end
|
||
|
||
def get_start_in_world(self) -> Point6D:
|
||
"""获取工件坐标系下的起点(兼容方法)"""
|
||
if self.world_start is not None:
|
||
return self.world_start
|
||
# 向后兼容:如果没有 world 坐标,假定 start 就是工件坐标
|
||
return self.start
|
||
|
||
def get_end_in_world(self) -> Point6D:
|
||
"""获取工件坐标系下的终点(兼容方法)"""
|
||
if self.world_end is not None:
|
||
return self.world_end
|
||
return self.end
|
||
|
||
def get_start_in_machine(self) -> Point6D:
|
||
"""获取绝对坐标系下的起点"""
|
||
if self.machine_start is not None:
|
||
return self.machine_start
|
||
return self.start
|
||
|
||
def get_end_in_machine(self) -> Point6D:
|
||
"""获取绝对坐标系下的终点"""
|
||
if self.machine_end is not None:
|
||
return self.machine_end
|
||
return self.end
|
||
|
||
def has_world_coords(self) -> bool:
|
||
"""是否有工件坐标"""
|
||
return self.world_start is not None and self.world_end is not None
|
||
|
||
def has_machine_coords(self) -> bool:
|
||
"""是否有绝对坐标"""
|
||
return self.machine_start is not None and self.machine_end is not None
|
||
|
||
def get_arc_info_in_world(self) -> Dict[str, float]:
|
||
"""
|
||
获取工件坐标系下的圆弧信息
|
||
用于几何验证和可视化
|
||
"""
|
||
start_pt = self.get_start_in_world()
|
||
end_pt = self.get_end_in_world()
|
||
return {
|
||
'start_x': start_pt.x,
|
||
'start_y': start_pt.y,
|
||
'start_z': start_pt.z,
|
||
'end_x': end_pt.x,
|
||
'end_y': end_pt.y,
|
||
'end_z': end_pt.z,
|
||
'center_x': self.center_x,
|
||
'center_y': self.center_y,
|
||
'center_z': self.center_z,
|
||
'radius': self.radius,
|
||
'turn': self.turn,
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@dataclass
|
||
class Subroutine:
|
||
"""子程序定义"""
|
||
name: str
|
||
lines: List[str] = field(default_factory=list)
|
||
line_numbers: List[int] = field(default_factory=list)
|
||
start_line: int = 0
|
||
parameters: List[str] = field(default_factory=list)
|
||
description: str = ""
|
||
source_file: str = ""
|
||
|
||
|
||
@dataclass
|
||
class ToolpathData:
|
||
"""刀具路径数据"""
|
||
segments: List[MoveSegment] = field(default_factory=list)
|
||
bounds: Dict[str, float] = field(default_factory=dict)
|
||
total_length: float = 0.0
|
||
total_time: float = 0.0
|
||
point_count: int = 0
|
||
generation_time_ms: float = 0.0
|
||
filename: str = ""
|
||
tool_changes: List[Dict] = field(default_factory=list)
|
||
tool_table: Dict[int, ToolData] = field(default_factory=dict)
|
||
subroutines: Dict[str, Subroutine] = field(default_factory=dict)
|
||
variables: Dict[int, float] = field(default_factory=dict)
|
||
named_variables: Dict[str, float] = field(default_factory=dict)
|
||
rtcp_enabled: bool = False
|
||
kinematics_type: str = ""
|
||
program_comments: List[str] = field(default_factory=list)
|
||
program_messages: List[str] = field(default_factory=list)
|
||
errors: List[str] = field(default_factory=list)
|
||
warnings: List[str] = field(default_factory=list)
|
||
|
||
def calculate_bounds(self):
|
||
if not self.segments:
|
||
return
|
||
|
||
min_x = min_y = min_z = min_a = min_b = min_c = float('inf')
|
||
max_x = max_y = max_z = max_a = max_b = max_c = float('-inf')
|
||
|
||
for seg in self.segments:
|
||
for point in [seg.start, seg.end]:
|
||
min_x = min(min_x, point.x)
|
||
max_x = max(max_x, point.x)
|
||
min_y = min(min_y, point.y)
|
||
max_y = max(max_y, point.y)
|
||
min_z = min(min_z, point.z)
|
||
max_z = max(max_z, point.z)
|
||
min_a = min(min_a, point.a)
|
||
max_a = max(max_a, point.a)
|
||
min_b = min(min_b, point.b)
|
||
max_b = max(max_b, point.b)
|
||
min_c = min(min_c, point.c)
|
||
max_c = max(max_c, point.c)
|
||
|
||
self.bounds = {
|
||
'minX': min_x if min_x != float('inf') else 0,
|
||
'maxX': max_x if max_x != float('-inf') else 0,
|
||
'minY': min_y if min_y != float('inf') else 0,
|
||
'maxY': max_y if max_y != float('-inf') else 0,
|
||
'minZ': min_z if min_z != float('inf') else 0,
|
||
'maxZ': max_z if max_z != float('-inf') else 0,
|
||
'minA': min_a if min_a != float('inf') else 0,
|
||
'maxA': max_a if max_a != float('-inf') else 0,
|
||
'minB': min_b if min_b != float('inf') else 0,
|
||
'maxB': max_b if max_b != float('-inf') else 0,
|
||
'minC': min_c if min_c != float('inf') else 0,
|
||
'maxC': max_c if max_c != float('-inf') else 0,
|
||
'width': (max_x - min_x) if max_x != float('-inf') else 0,
|
||
'height': (max_y - min_y) if max_y != float('-inf') else 0,
|
||
'depth': (max_z - min_z) if max_z != float('-inf') else 0,
|
||
}
|
||
|
||
def calculate_length_and_time(self) -> Tuple[float, float]:
|
||
total = 0.0
|
||
total_time = 0.0
|
||
for seg in self.segments:
|
||
if seg.type in [MoveType.RAPID, MoveType.FEED]:
|
||
total += seg.start.distance_to(seg.end)
|
||
elif seg.type in [MoveType.ARC_CW, MoveType.ARC_CCW]:
|
||
total += abs(seg.turn) * seg.radius * math.pi
|
||
total_time += seg.duration
|
||
self.total_length = total
|
||
self.total_time = total_time
|
||
return total, total_time
|
||
|
||
def get_statistics(self) -> Dict[str, Any]:
|
||
return {
|
||
'total_segments': len(self.segments),
|
||
'rapid_moves': sum(1 for s in self.segments if s.type == MoveType.RAPID),
|
||
'feed_moves': sum(1 for s in self.segments if s.type == MoveType.FEED),
|
||
'arc_moves': sum(1 for s in self.segments if s.type in [MoveType.ARC_CW, MoveType.ARC_CCW]),
|
||
'dwells': sum(1 for s in self.segments if s.type == MoveType.DWELL),
|
||
'probes': sum(1 for s in self.segments if s.type == MoveType.PROBE),
|
||
'rtcp_on_count': sum(1 for s in self.segments if s.type == MoveType.RTCP_ON),
|
||
'rtcp_off_count': sum(1 for s in self.segments if s.type == MoveType.RTCP_OFF),
|
||
'compensated_segments': sum(1 for s in self.segments if s.comp_type != CompType.OFF),
|
||
'tool_changes': len(self.tool_changes),
|
||
'subroutines': len(self.subroutines),
|
||
'variables': len(self.variables),
|
||
'named_variables': len(self.named_variables),
|
||
'rtcp_enabled': self.rtcp_enabled,
|
||
'kinematics_type': self.kinematics_type,
|
||
'errors': len(self.errors),
|
||
'warnings': len(self.warnings),
|
||
}
|
||
|
||
def get_segments_by_type(self, move_type: MoveType) -> List[MoveSegment]:
|
||
"""获取指定类型的运动段"""
|
||
return [s for s in self.segments if s.type == move_type]
|
||
|
||
def get_segments_by_tool(self, tool_number: int) -> List[MoveSegment]:
|
||
"""获取指定刀具的运动段"""
|
||
return [s for s in self.segments if s.tool_number == tool_number]
|
||
|
||
def get_segments_by_line(self, line_number: int) -> List[MoveSegment]:
|
||
"""获取指定行号的运动段"""
|
||
return [s for s in self.segments if s.line_number == line_number]
|
||
|
||
def add_error(self, error: str):
|
||
"""添加错误"""
|
||
self.errors.append(error)
|
||
|
||
def add_warning(self, warning: str):
|
||
"""添加警告"""
|
||
self.warnings.append(warning)
|
||
|
||
def add_comment(self, comment: str):
|
||
"""添加注释"""
|
||
self.program_comments.append(comment)
|
||
|
||
def add_message(self, message: str):
|
||
"""添加消息"""
|
||
self.program_messages.append(message)
|
||
|
||
|
||
|
||
# ==================== 关节坐标映射工具 ====================
|
||
|
||
def joints_to_point6d(joints: List[float], kinematics_type: KinematicsType) -> Point6D:
|
||
"""
|
||
根据运动学类型的关节配置,将关节数组转换为 Point6D
|
||
|
||
参数:
|
||
joints: 关节坐标数组
|
||
kinematics_type: 运动学类型
|
||
|
||
返回:
|
||
Point6D,其中 x/y/z/a/b/c 根据运动学配置正确映射
|
||
"""
|
||
config = KINEMATICS_JOINT_CONFIG.get(kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY])
|
||
|
||
def safe_get(idx: int, default: float = 0.0) -> float:
|
||
if idx >= 0 and idx < len(joints):
|
||
return joints[idx]
|
||
return default
|
||
|
||
return Point6D(
|
||
safe_get(config.get('x_idx', 0), 0.0),
|
||
safe_get(config.get('y_idx', 1), 0.0),
|
||
safe_get(config.get('z_idx', 2), 0.0),
|
||
safe_get(config.get('a_idx', 3), 0.0),
|
||
safe_get(config.get('b_idx', 4), 0.0),
|
||
safe_get(config.get('c_idx', 5), 0.0),
|
||
)
|
||
|
||
|
||
def point6d_to_joints(pos: Point6D, kinematics_type: KinematicsType) -> List[float]:
|
||
"""
|
||
根据运动学类型的关节配置,将 Point6D 转换为关节数组
|
||
|
||
参数:
|
||
pos: Point6D 坐标
|
||
kinematics_type: 运动学类型
|
||
|
||
返回:
|
||
关节坐标数组
|
||
"""
|
||
config = KINEMATICS_JOINT_CONFIG.get(kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY])
|
||
num_joints = config.get('num_joints', 6)
|
||
joints = [0.0] * num_joints
|
||
|
||
# 根据配置将 Point6D 的轴值放到正确位置
|
||
axes_map = {
|
||
'x': ('x_idx', pos.x),
|
||
'y': ('y_idx', pos.y),
|
||
'z': ('z_idx', pos.z),
|
||
'a': ('a_idx', pos.a),
|
||
'b': ('b_idx', pos.b),
|
||
'c': ('c_idx', pos.c),
|
||
}
|
||
|
||
for key_name, (config_key, value) in axes_map.items():
|
||
idx = config.get(config_key, -1)
|
||
if idx >= 0 and idx < num_joints:
|
||
joints[idx] = value
|
||
|
||
return joints
|
||
|
||
|
||
|
||
# ==================== TRT 双转台运动学参数 ====================
|
||
|
||
@dataclass
|
||
class TRTParams(KinematicsParams):
|
||
"""双转台/摆头式五轴参数 (对应 trtfuncs.c 的 haldata)"""
|
||
rot_center_x: float = 0.0
|
||
rot_center_y: float = 0.0
|
||
rot_center_z: float = 0.0
|
||
axis_offset_x: float = 0.0
|
||
axis_offset_y: float = 0.0
|
||
axis_offset_z: float = 0.0
|
||
tool_length: float = 0.0
|
||
conventional_directions: bool = False
|
||
pivot_length: float = 250.0
|
||
|
||
|
||
# ==================== XYZAC_TRTKinematics 类 ====================
|
||
|
||
class XYZAC_TRTKinematics(BaseKinematics):
|
||
"""
|
||
XYZAC 双转台运动学 - 完全参照 LinuxCNC trtfuncs.c
|
||
A 轴绕 X 轴倾斜,C 轴绕 Z 轴旋转。
|
||
|
||
参数映射 (LinuxCNC -> JSON):
|
||
rot_center_x/y/z = 旋转台回转中心在机床坐标系中的位置
|
||
axis_offset_x/y/z = 直线轴相对于旋转台的安装偏置
|
||
tool_length = 刀具长度补偿值
|
||
"""
|
||
|
||
def __init__(self, params: TRTParams = None, debug: bool = False):
|
||
super().__init__(params or TRTParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
|
||
# 同步刀具长度
|
||
if hasattr(self.p, 'tool_length') and self.p.tool_length > 0:
|
||
self._tool_length = self.p.tool_length
|
||
|
||
def _get_con(self) -> float:
|
||
"""获取旋转方向因子"""
|
||
return 1.0 if self.p.conventional_directions else -1.0
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""
|
||
正运动学:关节坐标 -> 世界坐标
|
||
|
||
参照 LinuxCNC xyzacKinematicsForward():
|
||
1. 从关节坐标中减去旋转中心坐标,把坐标系原点移到旋转中心
|
||
2. 再减去轴安装偏置(axis_offset)
|
||
3. 乘上旋转矩阵(A 轴绕 X,C 轴绕 Z)
|
||
4. 把旋转中心加回来,还原为机床坐标系下的刀尖位置
|
||
|
||
关节顺序: [X, Y, Z, A, C]
|
||
返回: (X, Y, Z, A, B, C) 刀尖点世界坐标
|
||
"""
|
||
if len(joints) < 5:
|
||
joints = list(joints) + [0.0] * (5 - len(joints))
|
||
|
||
x = joints[0] # 关节 X
|
||
y = joints[1] # 关节 Y
|
||
z = joints[2] # 关节 Z(主轴参考点)
|
||
a = joints[3] # A 轴角度(度)
|
||
c = joints[4] # C 轴角度(度)
|
||
|
||
con = self._get_con()
|
||
a_rad = a * TO_RAD
|
||
c_rad = c * TO_RAD
|
||
|
||
# 旋转中心 - 对应 LinuxCNC 的 x_rot_point, y_rot_point, z_rot_point
|
||
rx = self.p.rot_center_x
|
||
ry = self.p.rot_center_y
|
||
rz = self.p.rot_center_z
|
||
|
||
# 轴偏置 - 对应 LinuxCNC 的 x_offset, y_offset, z_offset
|
||
dx = self.p.axis_offset_x
|
||
dy = self.p.axis_offset_y
|
||
dz = self.p.axis_offset_z + self._tool_length # z_offset + tool_offset
|
||
|
||
# ===== 正解计算(完全参照 LinuxCNC 官方代码)=====
|
||
|
||
# 注意:dx (axis_offset_x) 在 AC 结构中用于 X 轴方向
|
||
# 这里的关键是:从关节坐标中减去旋转中心和轴偏置,
|
||
# 然后进行旋转变换,最后再加回旋转中心
|
||
|
||
pos_x = (
|
||
+ math.cos(c_rad) * (x - dx - rx)
|
||
- con * math.sin(c_rad) * math.cos(a_rad) * (y - dy - ry)
|
||
+ math.sin(c_rad) * math.sin(a_rad) * (z - dz - rz)
|
||
- con * math.sin(c_rad) * dy
|
||
+ rx
|
||
)
|
||
|
||
pos_y = (
|
||
+ con * math.sin(c_rad) * (x - dx - rx)
|
||
+ math.cos(c_rad) * math.cos(a_rad) * (y - dy - ry)
|
||
- con * math.cos(c_rad) * math.sin(a_rad) * (z - dz - rz)
|
||
+ math.cos(c_rad) * dy
|
||
+ ry
|
||
)
|
||
|
||
pos_z = (
|
||
+ con * math.sin(a_rad) * (y - dy - ry)
|
||
+ math.cos(a_rad) * (z - dz - rz)
|
||
+ dz
|
||
+ rz
|
||
)
|
||
|
||
if self.debug:
|
||
print(f"[XYZAC FWD] joints(x={x:.3f},y={y:.3f},z={z:.3f},a={a:.3f},c={c:.3f}) "
|
||
f"-> world({pos_x:.3f},{pos_y:.3f},{pos_z:.3f})")
|
||
print(f"[XYZAC FWD] rot_center=({rx:.1f},{ry:.1f},{rz:.1f}) "
|
||
f"axis_offset=({dx:.1f},{dy:.1f},{dz:.1f})")
|
||
|
||
return (pos_x, pos_y, pos_z, a, 0.0, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
"""
|
||
逆运动学:世界坐标 -> 关节坐标
|
||
|
||
参照 LinuxCNC xyzacKinematicsInverse():
|
||
1. 把世界坐标减去旋转中心,把坐标系移到旋转中心
|
||
2. 乘上逆旋转矩阵
|
||
3. 加上轴偏置和旋转中心,还原成关节坐标
|
||
|
||
输入: (X, Y, Z, A, B, C) 世界坐标
|
||
返回: [X, Y, Z, A, C] 关节坐标
|
||
"""
|
||
if len(world) < 6:
|
||
world = list(world) + [0.0] * (6 - len(world))
|
||
|
||
wx = world[0] # 世界 X
|
||
wy = world[1] # 世界 Y
|
||
wz = world[2] # 世界 Z
|
||
wa = world[3] # A 轴角度
|
||
wb = world[4] # B 轴(不使用)
|
||
wc = world[5] # C 轴角度
|
||
|
||
con = self._get_con()
|
||
a_rad = wa * TO_RAD
|
||
c_rad = wc * TO_RAD
|
||
|
||
# 旋转中心
|
||
rx = self.p.rot_center_x
|
||
ry = self.p.rot_center_y
|
||
rz = self.p.rot_center_z
|
||
|
||
# 轴偏置
|
||
dx = self.p.axis_offset_x
|
||
dy = self.p.axis_offset_y
|
||
dz = self.p.axis_offset_z + self._tool_length
|
||
|
||
# ===== 逆解计算(完全参照 LinuxCNC 官方代码)=====
|
||
|
||
# 第一步:先把世界坐标减去旋转中心,计算 P 向量
|
||
Px = (
|
||
+ math.cos(c_rad) * (wx - rx)
|
||
+ con * math.sin(c_rad) * (wy - ry)
|
||
+ rx
|
||
)
|
||
|
||
Py = (
|
||
- con * math.sin(c_rad) * math.cos(a_rad) * (wx - rx)
|
||
+ math.cos(c_rad) * math.cos(a_rad) * (wy - ry)
|
||
+ con * math.sin(a_rad) * (wz - rz)
|
||
- math.cos(a_rad) * dy
|
||
- con * math.sin(a_rad) * dz
|
||
+ dy
|
||
+ ry
|
||
)
|
||
|
||
Pz = (
|
||
+ math.sin(c_rad) * math.sin(a_rad) * (wx - rx)
|
||
- con * math.cos(c_rad) * math.sin(a_rad) * (wy - ry)
|
||
+ math.cos(a_rad) * (wz - rz)
|
||
+ con * math.sin(a_rad) * dy
|
||
- math.cos(a_rad) * dz
|
||
+ dz
|
||
+ rz
|
||
)
|
||
|
||
if self.debug:
|
||
print(f"[XYZAC INV] world({wx:.3f},{wy:.3f},{wz:.3f},a={wa:.1f},c={wc:.1f}) "
|
||
f"-> joints({Px:.3f},{Py:.3f},{Pz:.3f})")
|
||
print(f"[XYZAC INV] rot_center=({rx:.1f},{ry:.1f},{rz:.1f}) "
|
||
f"axis_offset=({dx:.1f},{dy:.1f},{dz:.1f})")
|
||
|
||
return [Px, Py, Pz, wa, wc]
|
||
|
||
def set_tool_length(self, length: float):
|
||
"""设置刀具长度 - 确保参数同步"""
|
||
self._tool_length = length
|
||
if hasattr(self, 'p') and self.p is not None:
|
||
self.p.tool_length = length
|
||
if self.debug:
|
||
print(f"[XYZAC TLO] set_tool_length({length}) -> "
|
||
f"_tool_length={self._tool_length}, "
|
||
f"p.tool_length={self.p.tool_length if hasattr(self.p, 'tool_length') else 'N/A'}")
|
||
|
||
|
||
# ==================== XYZBC_TRTKinematics 类 ====================
|
||
|
||
class XYZBC_TRTKinematics(BaseKinematics):
|
||
"""
|
||
XYZBC 双转台运动学 (基于 trtfuncs.c 的 xyzbcKinematicsForward/Inverse)
|
||
B 轴绕 Y 轴倾斜,C 轴绕 Z 轴旋转。
|
||
"""
|
||
|
||
def __init__(self, params: TRTParams = None, debug: bool = False):
|
||
super().__init__(params or TRTParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
# ★ 同样添加刀具长度同步 ★
|
||
if hasattr(self.p, 'tool_length') and self.p.tool_length > 0:
|
||
self._tool_length = self.p.tool_length
|
||
|
||
def _get_con(self) -> float:
|
||
"""获取方向因子"""
|
||
return 1.0 if self.p.conventional_directions else -1.0
|
||
|
||
def set_params(self, **kwargs):
|
||
"""更新运动学参数"""
|
||
supported = {
|
||
'rot_center_x', 'rot_center_y', 'rot_center_z',
|
||
'axis_offset_x', 'axis_offset_y', 'axis_offset_z',
|
||
'tool_length', 'conventional_directions', 'pivot_length'
|
||
}
|
||
for key, value in kwargs.items():
|
||
if key in supported and hasattr(self.p, key):
|
||
setattr(self.p, key, value)
|
||
|
||
if self.debug:
|
||
print(f"[XYZBC_TRT] 参数更新: {kwargs}")
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""
|
||
正运动学:关节坐标 -> 世界坐标 (刀尖点)
|
||
关节顺序: [X, Y, Z, B, C]
|
||
返回: (X, Y, Z, A, B, C)
|
||
"""
|
||
if len(joints) < 5:
|
||
joints = list(joints) + [0.0] * (5 - len(joints))
|
||
|
||
x, y, z, b, c = joints[0], joints[1], joints[2], joints[3], joints[4]
|
||
|
||
con = self._get_con()
|
||
b_rad = b * TO_RAD
|
||
c_rad = c * TO_RAD
|
||
|
||
rx = self.p.rot_center_x
|
||
ry = self.p.rot_center_y
|
||
rz = self.p.rot_center_z
|
||
|
||
dx = self.p.axis_offset_x
|
||
dz = self.p.axis_offset_z + self._tool_length
|
||
|
||
pos_x = (
|
||
+ math.cos(c_rad) * math.cos(b_rad) * (x - dx - rx)
|
||
- con * math.sin(c_rad) * (y - ry)
|
||
+ con * math.cos(c_rad) * math.sin(b_rad) * (z - dz - rz)
|
||
+ math.cos(c_rad) * dx
|
||
+ rx
|
||
)
|
||
|
||
pos_y = (
|
||
+ con * math.sin(c_rad) * math.cos(b_rad) * (x - dx - rx)
|
||
+ math.cos(c_rad) * (y - ry)
|
||
+ math.sin(c_rad) * math.sin(b_rad) * (z - dz - rz)
|
||
+ con * math.sin(c_rad) * dx
|
||
+ ry
|
||
)
|
||
|
||
pos_z = (
|
||
- con * math.sin(b_rad) * (x - dx - rx)
|
||
+ math.cos(b_rad) * (z - dz - rz)
|
||
+ dz
|
||
+ rz
|
||
)
|
||
|
||
if self.debug:
|
||
print(f"[XYZBC] FWD: joints({x:.3f},{y:.3f},{z:.3f},{b:.3f},{c:.3f}) -> world({pos_x:.3f},{pos_y:.3f},{pos_z:.3f})")
|
||
|
||
return (pos_x, pos_y, pos_z, 0.0, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
"""
|
||
逆运动学:世界坐标 (刀尖点) -> 关节坐标
|
||
输入: (X, Y, Z, A, B, C)
|
||
返回: [X, Y, Z, B, C]
|
||
"""
|
||
if len(world) < 6:
|
||
world = list(world) + [0.0] * (6 - len(world))
|
||
|
||
wx, wy, wz, wa, wb, wc = world[0], world[1], world[2], world[3], world[4], world[5]
|
||
|
||
con = self._get_con()
|
||
b_rad = wb * TO_RAD
|
||
c_rad = wc * TO_RAD
|
||
|
||
rx = self.p.rot_center_x
|
||
ry = self.p.rot_center_y
|
||
rz = self.p.rot_center_z
|
||
|
||
dx = self.p.axis_offset_x
|
||
dz = self.p.axis_offset_z + self._tool_length
|
||
|
||
dpx = -math.cos(b_rad) * dx + math.sin(b_rad) * dz + dx
|
||
dpz = -math.sin(b_rad) * dx - math.cos(b_rad) * dz + dz
|
||
|
||
Px = (
|
||
+ math.cos(c_rad) * math.cos(b_rad) * (wx - rx)
|
||
+ con * math.sin(c_rad) * math.cos(b_rad) * (wy - ry)
|
||
- con * math.sin(b_rad) * (wz - rz)
|
||
+ dpx
|
||
+ rx
|
||
)
|
||
|
||
Py = (
|
||
- con * math.sin(c_rad) * (wx - rx)
|
||
+ math.cos(c_rad) * (wy - ry)
|
||
+ ry
|
||
)
|
||
|
||
Pz = (
|
||
+ con * math.cos(c_rad) * math.sin(b_rad) * (wx - rx)
|
||
+ math.sin(c_rad) * math.sin(b_rad) * (wy - ry)
|
||
+ math.cos(b_rad) * (wz - rz)
|
||
+ dpz
|
||
+ rz
|
||
)
|
||
|
||
if self.debug:
|
||
print(f"[XYZBC] INV: world({wx:.3f},{wy:.3f},{wz:.3f},{wb:.3f},{wc:.3f}) -> joints({Px:.3f},{Py:.3f},{Pz:.3f})")
|
||
|
||
return [Px, Py, Pz, wb, wc]
|
||
|
||
def set_tool_length(self, length: float):
|
||
self._tool_length = length
|
||
self.p.tool_length = length
|
||
if self.debug:
|
||
print(f"[XYZBC] 刀具长度设置为: {length:.3f} mm")
|
||
|
||
def set_pivot_length(self, length: float):
|
||
"""设置枢轴长度(为兼容性保留)"""
|
||
self.p.pivot_length = length
|
||
if self.debug:
|
||
print(f"[XYZBC] 枢轴长度设置为: {length:.3f} mm")
|
||
|
||
|
||
# ==================== 五轴桥式铣床运动学 (5axiskins) ====================
|
||
|
||
class FiveAxisBCKinematics(BaseKinematics):
|
||
"""
|
||
五轴桥式铣床运动学 (基于 5axiskins.c)
|
||
采用球坐标描述刀具指向。
|
||
"""
|
||
|
||
def __init__(self, pivot_length: float = 250.0, tool_length: float = 0.0, debug: bool = False):
|
||
super().__init__()
|
||
self.pivot_length = pivot_length
|
||
self._tool_length = tool_length
|
||
self.debug = debug
|
||
|
||
def _s2r(self, r: float, t: float, p: float) -> Tuple[float, float, float]:
|
||
"""
|
||
球坐标转直角坐标 (来自 5axiskins.c 的 s2r 函数)
|
||
r = 向量长度
|
||
p = phi = 向量与 Z 轴夹角 (度)
|
||
t = theta = 向量在 XY 平面投影与 X 轴夹角 (度)
|
||
"""
|
||
t_rad = t * TO_RAD
|
||
p_rad = p * TO_RAD
|
||
x = r * math.sin(p_rad) * math.cos(t_rad)
|
||
y = r * math.sin(p_rad) * math.sin(t_rad)
|
||
z = r * math.cos(p_rad)
|
||
return (x, y, z)
|
||
|
||
def _r2s(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||
"""直角坐标转球坐标"""
|
||
r = math.sqrt(x*x + y*y + z*z)
|
||
if r < CART_FUZZ:
|
||
return (0.0, 0.0, 0.0)
|
||
p = math.acos(z / r) * TO_DEG
|
||
t = math.atan2(y, x) * TO_DEG
|
||
return (r, t, p)
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 6:
|
||
joints = list(joints) + [0.0] * (6 - len(joints))
|
||
|
||
x, y, z, b, c, w = joints[0], joints[1], joints[2], joints[3], joints[4], joints[5]
|
||
|
||
rx, ry, rz = self._s2r(self.pivot_length + w, c, 180.0 - b)
|
||
|
||
pos_x = x + rx
|
||
pos_y = y + ry
|
||
pos_z = z + self.pivot_length + rz
|
||
|
||
if self.debug:
|
||
print(f"[5AXISBC] FWD: joints({x:.3f},{y:.3f},{z:.3f},{b:.3f},{c:.3f},{w:.3f}) -> world({pos_x:.3f},{pos_y:.3f},{pos_z:.3f})")
|
||
|
||
return (pos_x, pos_y, pos_z, 0.0, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
if len(world) < 6:
|
||
world = list(world) + [0.0] * (6 - len(world))
|
||
|
||
wx, wy, wz, _, wb, wc = world[0], world[1], world[2], world[3], world[4], world[5]
|
||
|
||
rx, ry, rz = self._s2r(self.pivot_length, wc, 180.0 - wb)
|
||
|
||
Px = wx - rx
|
||
Py = wy - ry
|
||
Pz = wz - self.pivot_length - rz
|
||
|
||
if self.debug:
|
||
print(f"[5AXISBC] INV: world({wx:.3f},{wy:.3f},{wz:.3f},{wb:.3f},{wc:.3f}) -> joints({Px:.3f},{Py:.3f},{Pz:.3f})")
|
||
|
||
return [Px, Py, Pz, wb, wc, 0.0]
|
||
|
||
def set_tool_length(self, length: float):
|
||
self._tool_length = length
|
||
if self.debug:
|
||
print(f"[5AXISBC] 刀具长度设置为: {length:.3f} mm")
|
||
|
||
|
||
# ==================== Maxkins BC 运动学 ====================
|
||
|
||
class MaxkinsBCKinematics(BaseKinematics):
|
||
"""
|
||
Max 五轴铣床运动学 (基于 maxkins.c)
|
||
摆头 (B轴) + 转台 (C轴) 结构
|
||
"""
|
||
|
||
def __init__(self, pivot_length: float = 0.666, tool_length: float = 0.0,
|
||
conventional_directions: bool = False, debug: bool = False):
|
||
super().__init__()
|
||
self.pivot_length = pivot_length
|
||
self._tool_length = tool_length
|
||
self.conventional_directions = conventional_directions
|
||
self.debug = debug
|
||
|
||
def _get_con(self) -> float:
|
||
return 1.0 if self.conventional_directions else -1.0
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 9:
|
||
joints = list(joints) + [0.0] * (9 - len(joints))
|
||
|
||
x, y, z, a, b, c, u, v, w = joints[0], joints[1], joints[2], joints[3], joints[4], joints[5], joints[6], joints[7], joints[8]
|
||
|
||
con = self._get_con()
|
||
b_rad = b * TO_RAD
|
||
c_rad = c * TO_RAD
|
||
|
||
zb = (self.pivot_length + w) * math.cos(b_rad)
|
||
xb = (self.pivot_length + w) * math.sin(b_rad)
|
||
|
||
xyr = math.hypot(x, y)
|
||
xytheta = math.atan2(y, x) + c_rad
|
||
|
||
zv = u * math.sin(b_rad)
|
||
xv = u * math.cos(b_rad)
|
||
|
||
pos_x = xyr * math.cos(xytheta) - con * xb - xv
|
||
pos_y = xyr * math.sin(xytheta) - v
|
||
pos_z = z - zb - con * zv + self.pivot_length
|
||
|
||
if self.debug:
|
||
print(f"[MAXKINS] FWD: joints -> world({pos_x:.3f},{pos_y:.3f},{pos_z:.3f})")
|
||
|
||
return (pos_x, pos_y, pos_z, a, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
if len(world) < 9:
|
||
world = list(world) + [0.0] * (9 - len(world))
|
||
|
||
wx, wy, wz, wa, wb, wc, wu, wv, ww = world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], world[8]
|
||
|
||
con = self._get_con()
|
||
b_rad = wb * TO_RAD
|
||
c_rad = wc * TO_RAD
|
||
|
||
zb = (self.pivot_length + ww) * math.cos(b_rad)
|
||
xb = (self.pivot_length + ww) * math.sin(b_rad)
|
||
|
||
xyr = math.hypot(wx, wy)
|
||
xytheta = math.atan2(wy, wx) - c_rad
|
||
|
||
xv = wu * math.cos(b_rad)
|
||
zv = wu * math.sin(b_rad)
|
||
|
||
Px = xyr * math.cos(xytheta) + con * xb + xv
|
||
Py = xyr * math.sin(xytheta) + wv
|
||
Pz = wz + zb - con * zv - self.pivot_length
|
||
|
||
if self.debug:
|
||
print(f"[MAXKINS] INV: world -> joints({Px:.3f},{Py:.3f},{Pz:.3f})")
|
||
|
||
return [Px, Py, Pz, wa, wb, wc, wu, wv, ww]
|
||
|
||
def set_tool_length(self, length: float):
|
||
self._tool_length = length
|
||
if self.debug:
|
||
print(f"[MAXKINS] 刀具长度设置为: {length:.3f} mm")
|
||
|
||
|
||
# ==================== Hexapod 运动学参数 ====================
|
||
|
||
@dataclass
|
||
class HexapodParams(KinematicsParams):
|
||
"""Hexapod 参数 (对应 genhexkins.c 的 haldata)"""
|
||
base_joints: List[Tuple[float, float, float]] = field(default_factory=lambda: [
|
||
(-22.95, 13.25, 0), (22.95, 13.25, 0), (22.95, 13.25, 0),
|
||
(0, -26.5, 0), (0, -26.5, 0), (-22.95, 13.25, 0)
|
||
])
|
||
platform_joints: List[Tuple[float, float, float]] = field(default_factory=lambda: [
|
||
(-1.0, 11.5, 0), (1.0, 11.5, 0), (10.459, -4.884, 0),
|
||
(9.459, -6.616, 0), (-9.459, -6.616, 0), (-10.459, -4.884, 0)
|
||
])
|
||
base_joint_axes: List[Tuple[float, float, float]] = field(default_factory=lambda: [
|
||
(0.707107, 0.0, 0.707107), (0.0, -0.707107, 0.707107), (-0.707107, 0.0, 0.707107),
|
||
(-0.707107, 0.0, 0.707107), (0.0, 0.707107, 0.707107), (0.707107, 0.0, 0.707107)
|
||
])
|
||
platform_joint_axes: List[Tuple[float, float, float]] = field(default_factory=lambda: [
|
||
(-1.0, 0.0, 0.0), (0.866025, 0.5, 0.0), (0.866025, 0.5, 0.0),
|
||
(0.866025, -0.5, 0.0), (0.866025, -0.5, 0.0), (-1.0, 0.0, 0.0)
|
||
])
|
||
spindle_offset: float = 0.0
|
||
tool_offset: float = 0.0
|
||
screw_lead: float = 0.0
|
||
convergence_criterion: float = 1e-9
|
||
max_iterations: int = 120
|
||
max_error: float = 500.0
|
||
|
||
|
||
# ==================== Hexapod 运动学 ====================
|
||
|
||
class HexapodKinematics(BaseKinematics):
|
||
"""
|
||
通用 Hexapod (Stewart 平台) 运动学 - 完整版
|
||
基于 genhexkins.c 完整实现
|
||
"""
|
||
|
||
NUM_STRUTS = 6
|
||
|
||
def __init__(self, params: HexapodParams = None, debug: bool = False):
|
||
super().__init__(params or HexapodParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
self._update_offsets()
|
||
self.last_iterations = 0
|
||
self.max_iterations_used = 0
|
||
self._forward_fail = False
|
||
self._conv_err = 0.0
|
||
|
||
def _update_offsets(self):
|
||
"""更新带有主轴偏移和刀具偏移的坐标"""
|
||
offset_z = self.p.spindle_offset + self._tool_length
|
||
|
||
self.b = []
|
||
for bx, by, bz in self.p.base_joints:
|
||
self.b.append([bx, by, bz + offset_z])
|
||
|
||
self.a = []
|
||
for ax, ay, az in self.p.platform_joints:
|
||
self.a.append([ax, ay, az + offset_z])
|
||
|
||
self.nb1 = [list(n) for n in self.p.base_joint_axes]
|
||
self.na0 = [list(n) for n in self.p.platform_joint_axes]
|
||
|
||
def set_tool_length(self, length: float):
|
||
self._tool_length = length
|
||
self.p.tool_offset = length
|
||
self._update_offsets()
|
||
if self.debug:
|
||
print(f"[HEXAPOD] 刀具长度设置为: {length:.3f} mm")
|
||
|
||
def _rotation_matrix(self, a: float, b: float, c: float) -> List[List[float]]:
|
||
"""从 RPY 角 (A, B, C) 构建旋转矩阵"""
|
||
a_rad, b_rad, c_rad = a * TO_RAD, b * TO_RAD, c * TO_RAD
|
||
|
||
ca, sa = math.cos(a_rad), math.sin(a_rad)
|
||
cb, sb = math.cos(b_rad), math.sin(b_rad)
|
||
cc, sc = math.cos(c_rad), math.sin(c_rad)
|
||
|
||
return [
|
||
[cc*cb, cc*sb*sa - sc*ca, cc*sb*ca + sc*sa],
|
||
[sc*cb, sc*sb*sa + cc*ca, sc*sb*ca - cc*sa],
|
||
[ -sb, cb*sa, cb*ca]
|
||
]
|
||
|
||
def _cross_product(self, a: List[float], b: List[float]) -> List[float]:
|
||
"""三维向量叉积"""
|
||
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 _dot_product(self, a: List[float], b: List[float]) -> float:
|
||
"""三维向量点积"""
|
||
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
||
|
||
def _vector_magnitude(self, v: List[float]) -> float:
|
||
"""向量长度"""
|
||
return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
|
||
|
||
def _normalize(self, v: List[float]) -> List[float]:
|
||
"""向量归一化"""
|
||
mag = self._vector_magnitude(v)
|
||
if mag < 1e-12:
|
||
return [0.0, 0.0, 1.0]
|
||
return [v[0]/mag, v[1]/mag, v[2]/mag]
|
||
|
||
def _mat_vec_mult(self, M: List[List[float]], v: List[float]) -> List[float]:
|
||
"""矩阵乘向量"""
|
||
return [
|
||
M[0][0]*v[0] + M[0][1]*v[1] + M[0][2]*v[2],
|
||
M[1][0]*v[0] + M[1][1]*v[1] + M[1][2]*v[2],
|
||
M[2][0]*v[0] + M[2][1]*v[1] + M[2][2]*v[2]
|
||
]
|
||
|
||
def _strut_length_correction(self, strut_unit: List[float], R: List[List[float]],
|
||
strut_idx: int) -> float:
|
||
"""支腿长度修正 (对应 StrutLengthCorrection)"""
|
||
if self.p.screw_lead == 0.0:
|
||
return 0.0
|
||
|
||
nb2 = self._cross_product(self.nb1[strut_idx], strut_unit)
|
||
nb3 = self._cross_product(strut_unit, nb2)
|
||
nb3 = self._normalize(nb3)
|
||
|
||
na1 = self._mat_vec_mult(R, self.na0[strut_idx])
|
||
na2 = self._cross_product(na1, strut_unit)
|
||
na2 = self._normalize(na2)
|
||
|
||
dotprod = self._dot_product(nb3, na2)
|
||
dotprod = max(-1.0, min(1.0, dotprod))
|
||
|
||
return self.p.screw_lead * math.asin(dotprod) / (2 * math.pi)
|
||
|
||
def _mat_invert_6x6(self, J: List[List[float]]) -> List[List[float]]:
|
||
"""6x6 矩阵求逆 (高斯-约当消元法)"""
|
||
n = 6
|
||
aug = [[0.0] * (2 * n) for _ in range(n)]
|
||
|
||
for i in range(n):
|
||
for j in range(n):
|
||
aug[i][j] = J[i][j]
|
||
aug[i][i + n] = 1.0
|
||
|
||
for k in range(n):
|
||
if abs(aug[k][k]) < 0.01:
|
||
for j in range(k + 1, n):
|
||
if abs(aug[j][k]) > 0.01:
|
||
aug[k], aug[j] = aug[j], aug[k]
|
||
break
|
||
|
||
pivot = aug[k][k]
|
||
if abs(pivot) < 1e-12:
|
||
raise ValueError(f"Matrix is singular at row {k}")
|
||
|
||
for j in range(2 * n):
|
||
aug[k][j] /= pivot
|
||
|
||
for i in range(n):
|
||
if i != k:
|
||
factor = aug[i][k]
|
||
for j in range(2 * n):
|
||
aug[i][j] -= factor * aug[k][j]
|
||
|
||
inv = [[aug[i][j + n] for j in range(n)] for i in range(n)]
|
||
return inv
|
||
|
||
def _mat_mult_6x6_vec(self, J: List[List[float]], x: List[float]) -> List[float]:
|
||
"""6x6 矩阵乘 6x1 向量"""
|
||
result = [0.0] * 6
|
||
for i in range(6):
|
||
for j in range(6):
|
||
result[i] += J[i][j] * x[j]
|
||
return result
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""正运动学:牛顿-拉夫逊迭代法"""
|
||
if len(joints) < 6:
|
||
raise ValueError("Hexapod requires 6 strut lengths")
|
||
|
||
if any(j <= 0 for j in joints):
|
||
self._forward_fail = True
|
||
return (-1, -1, -1, 0, 0, 0)
|
||
|
||
x, y, z = 0.0, 0.0, 0.0
|
||
a, b, c = 0.0, 0.0, 0.0
|
||
|
||
iteration = 0
|
||
converge = False
|
||
self._forward_fail = False
|
||
|
||
while iteration < self.p.max_iterations:
|
||
iteration += 1
|
||
|
||
if abs(self._conv_err) > self.p.max_error:
|
||
self._forward_fail = True
|
||
return (-1, -1, -1, 0, 0, 0)
|
||
|
||
R = self._rotation_matrix(a, b, c)
|
||
|
||
strut_diff = []
|
||
jacobian_inv = [[0.0] * 6 for _ in range(6)]
|
||
|
||
for i in range(self.NUM_STRUTS):
|
||
ax, ay, az = self.a[i]
|
||
bx, by, bz = self.b[i]
|
||
|
||
aw_x = R[0][0]*ax + R[0][1]*ay + R[0][2]*az + x
|
||
aw_y = R[1][0]*ax + R[1][1]*ay + R[1][2]*az + y
|
||
aw_z = R[2][0]*ax + R[2][1]*ay + R[2][2]*az + z
|
||
|
||
vx = aw_x - bx
|
||
vy = aw_y - by
|
||
vz = aw_z - bz
|
||
|
||
length = math.sqrt(vx*vx + vy*vy + vz*vz)
|
||
|
||
if length > 1e-12:
|
||
ux, uy, uz = vx/length, vy/length, vz/length
|
||
else:
|
||
ux, uy, uz = 0.0, 0.0, 1.0
|
||
|
||
if self.p.screw_lead != 0.0:
|
||
corr = self._strut_length_correction([ux, uy, uz], R, i)
|
||
length += corr
|
||
|
||
strut_diff.append(length - joints[i])
|
||
|
||
rx = R[0][0]*ax + R[0][1]*ay + R[0][2]*az
|
||
ry = R[1][0]*ax + R[1][1]*ay + R[1][2]*az
|
||
rz = R[2][0]*ax + R[2][1]*ay + R[2][2]*az
|
||
|
||
cross_x = ry * uz - rz * uy
|
||
cross_y = rz * ux - rx * uz
|
||
cross_z = rx * uy - ry * ux
|
||
|
||
jacobian_inv[i] = [ux, uy, uz, cross_x, cross_y, cross_z]
|
||
|
||
self._conv_err = sum(abs(d) for d in strut_diff)
|
||
max_diff = max(abs(d) for d in strut_diff)
|
||
|
||
if max_diff < self.p.convergence_criterion:
|
||
converge = True
|
||
break
|
||
|
||
try:
|
||
jacobian = self._mat_invert_6x6(jacobian_inv)
|
||
delta = self._mat_mult_6x6_vec(jacobian, strut_diff)
|
||
|
||
x -= delta[0]
|
||
y -= delta[1]
|
||
z -= delta[2]
|
||
a -= delta[3] * TO_DEG
|
||
b -= delta[4] * TO_DEG
|
||
c -= delta[5] * TO_DEG
|
||
except ValueError:
|
||
self._forward_fail = True
|
||
return (-1, -1, -1, 0, 0, 0)
|
||
|
||
self.last_iterations = iteration
|
||
if iteration > self.max_iterations_used:
|
||
self.max_iterations_used = iteration
|
||
|
||
if not converge:
|
||
return (-1, -1, -1, 0, 0, 0)
|
||
|
||
if self.debug:
|
||
print(f"[HEXAPOD] FWD: converged after {iteration} iterations")
|
||
|
||
return (x, y, z, a, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
"""逆运动学:闭式解"""
|
||
if len(world) < 6:
|
||
raise ValueError("Hexapod requires 6 coordinates: X, Y, Z, A, B, C")
|
||
|
||
x, y, z, a, b, c = world[0], world[1], world[2], world[3], world[4], world[5]
|
||
|
||
R = self._rotation_matrix(a, b, c)
|
||
|
||
struts = []
|
||
for i in range(self.NUM_STRUTS):
|
||
ax, ay, az = self.a[i]
|
||
bx, by, bz = self.b[i]
|
||
|
||
aw_x = R[0][0]*ax + R[0][1]*ay + R[0][2]*az + x
|
||
aw_y = R[1][0]*ax + R[1][1]*ay + R[1][2]*az + y
|
||
aw_z = R[2][0]*ax + R[2][1]*ay + R[2][2]*az + z
|
||
|
||
vx = aw_x - bx
|
||
vy = aw_y - by
|
||
vz = aw_z - bz
|
||
|
||
length = math.sqrt(vx*vx + vy*vy + vz*vz)
|
||
|
||
if self.p.screw_lead != 0.0 and length > 1e-12:
|
||
u = [vx/length, vy/length, vz/length]
|
||
corr = self._strut_length_correction(u, R, i)
|
||
length += corr
|
||
|
||
struts.append(length)
|
||
|
||
if self.debug:
|
||
print(f"[HEXAPOD] INV: world -> {len(struts)} struts")
|
||
|
||
return struts
|
||
|
||
|
||
# ==================== PUMA 运动学参数 ====================
|
||
|
||
@dataclass
|
||
class PUMAParams(KinematicsParams):
|
||
"""PUMA 560 参数"""
|
||
a2: float = 300.0
|
||
a3: float = 50.0
|
||
d3: float = 70.0
|
||
d4: float = 400.0
|
||
d6: float = 70.0
|
||
|
||
|
||
# ==================== PUMA 运动学 ====================
|
||
|
||
class PUMAKinematics(BaseKinematics):
|
||
"""
|
||
PUMA 560 运动学 - 完整版
|
||
基于 pumakins.c 完整实现
|
||
"""
|
||
|
||
PUMA_SHOULDER_RIGHT = 0x01
|
||
PUMA_ELBOW_DOWN = 0x02
|
||
PUMA_WRIST_FLIP = 0x04
|
||
PUMA_SINGULAR = 0x08
|
||
PUMA_REACH = 0x01
|
||
|
||
def __init__(self, params: PUMAParams = None, debug: bool = False):
|
||
super().__init__(params or PUMAParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
self.inverse_flags = 0
|
||
self.SINGULAR_FUZZ = 0.000001
|
||
self.FLAG_FUZZ = 0.000001
|
||
self._last_joints = [0.0] * 6
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""正运动学 - 完整实现"""
|
||
if len(joints) < 6:
|
||
raise ValueError("PUMA requires 6 joints")
|
||
|
||
s1, s2, s3, s4, s5, s6 = [math.sin(j * TO_RAD) for j in joints[:6]]
|
||
c1, c2, c3, c4, c5, c6 = [math.cos(j * TO_RAD) for j in joints[:6]]
|
||
|
||
s23 = c2 * s3 + s2 * c3
|
||
c23 = c2 * c3 - s2 * s3
|
||
|
||
t1 = c4 * c5 * c6 - s4 * s6
|
||
t2 = s23 * s5 * c6
|
||
t3 = s4 * c5 * c6 + c4 * s6
|
||
t4 = c23 * t1 - t2
|
||
|
||
r11 = c1 * t4 + s1 * t3
|
||
r21 = s1 * t4 - c1 * t3
|
||
r31 = -s23 * t1 - c23 * s5 * c6
|
||
|
||
t1 = -c4 * c5 * s6 - s4 * c6
|
||
t2 = s23 * s5 * s6
|
||
t3 = c4 * c6 - s4 * c5 * s6
|
||
t4 = c23 * t1 + t2
|
||
|
||
r12 = c1 * t4 + s1 * t3
|
||
r22 = s1 * t4 - c1 * t3
|
||
r32 = -s23 * t1 + c23 * s5 * s6
|
||
|
||
t1 = c23 * c4 * s5 + s23 * c5
|
||
r13 = -c1 * t1 - s1 * s4 * s5
|
||
r23 = -s1 * t1 + c1 * s4 * s5
|
||
r33 = s23 * c4 * s5 - c23 * c5
|
||
|
||
t1_pos = self.p.a2 * c2 + self.p.a3 * c23 - self.p.d4 * s23
|
||
px = c1 * t1_pos - self.p.d3 * s1
|
||
py = s1 * t1_pos + self.p.d3 * c1
|
||
pz = -self.p.a3 * s23 - self.p.a2 * s2 - self.p.d4 * c23
|
||
|
||
px += r13 * self.p.d6
|
||
py += r23 * self.p.d6
|
||
pz += r33 * self.p.d6
|
||
|
||
if abs(r31) < 0.99999:
|
||
b = math.atan2(-r31, math.sqrt(r11**2 + r21**2))
|
||
c_angle = math.atan2(r21/math.cos(b), r11/math.cos(b))
|
||
a_angle = math.atan2(r32/math.cos(b), r33/math.cos(b))
|
||
else:
|
||
c_angle = 0
|
||
if r31 <= -1:
|
||
b = math.pi / 2
|
||
a_angle = math.atan2(r12, r13)
|
||
else:
|
||
b = -math.pi / 2
|
||
a_angle = math.atan2(-r12, -r13)
|
||
|
||
if self.debug:
|
||
print(f"[PUMA] FWD: joints -> world({px:.3f},{py:.3f},{pz:.3f})")
|
||
|
||
return (px, py, pz, a_angle * TO_DEG, b * TO_DEG, c_angle * TO_DEG)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float],
|
||
flags: int = 0) -> List[float]:
|
||
"""逆运动学 - 完整实现"""
|
||
if len(world) < 6:
|
||
raise ValueError("PUMA requires 6 coordinates")
|
||
|
||
px, py, pz, a_deg, b_deg, c_deg = world[:6]
|
||
|
||
a_rad, b_rad, c_rad = a_deg * TO_RAD, b_deg * TO_RAD, c_deg * TO_RAD
|
||
|
||
ca, sa = math.cos(a_rad), math.sin(a_rad)
|
||
cb, sb = math.cos(b_rad), math.sin(b_rad)
|
||
cc, sc = math.cos(c_rad), math.sin(c_rad)
|
||
|
||
R = [
|
||
[cc*cb, cc*sb*sa - sc*ca, cc*sb*ca + sc*sa],
|
||
[sc*cb, sc*sb*sa + cc*ca, sc*sb*ca - cc*sa],
|
||
[ -sb, cb*sa, cb*ca]
|
||
]
|
||
|
||
px = px - self.p.d6 * R[0][2]
|
||
py = py - self.p.d6 * R[1][2]
|
||
pz = pz - self.p.d6 * R[2][2]
|
||
|
||
sumSq = px*px + py*py - self.p.d3*self.p.d3
|
||
if sumSq < 0:
|
||
sumSq = 0
|
||
|
||
if flags & self.PUMA_SHOULDER_RIGHT:
|
||
th1 = math.atan2(py, px) - math.atan2(self.p.d3, -math.sqrt(sumSq))
|
||
else:
|
||
th1 = math.atan2(py, px) - math.atan2(self.p.d3, math.sqrt(sumSq))
|
||
|
||
s1 = math.sin(th1)
|
||
c1 = math.cos(th1)
|
||
|
||
k = (sumSq + pz*pz - self.p.a2*self.p.a2 - self.p.a3*self.p.a3 - self.p.d4*self.p.d4) / (2.0 * self.p.a2)
|
||
k_limit = math.sqrt(self.p.a3*self.p.a3 + self.p.d4*self.p.d4)
|
||
if abs(k) > k_limit:
|
||
k = k_limit if k > 0 else -k_limit
|
||
|
||
if flags & self.PUMA_ELBOW_DOWN:
|
||
th3 = math.atan2(self.p.a3, self.p.d4) - math.atan2(k, -math.sqrt(self.p.a3*self.p.a3 + self.p.d4*self.p.d4 - k*k))
|
||
else:
|
||
th3 = math.atan2(self.p.a3, self.p.d4) - math.atan2(k, math.sqrt(self.p.a3*self.p.a3 + self.p.d4*self.p.d4 - k*k))
|
||
|
||
s3 = math.sin(th3)
|
||
c3 = math.cos(th3)
|
||
|
||
t1 = (-self.p.a3 - self.p.a2 * c3) * pz + (c1*px + s1*py) * (self.p.a2*s3 - self.p.d4)
|
||
t2 = (self.p.a2*s3 - self.p.d4) * pz + (self.p.a3 + self.p.a2*c3) * (c1*px + s1*py)
|
||
t3 = pz*pz + (c1*px + s1*py)*(c1*px + s1*py)
|
||
|
||
th23 = math.atan2(t1, t2)
|
||
th2 = th23 - th3
|
||
|
||
s23 = t1 / t3 if t3 != 0 else 0
|
||
c23 = t2 / t3 if t3 != 0 else 0
|
||
|
||
t1_j4 = -R[2][0] * s1 + R[2][1] * c1
|
||
t2_j4 = -R[2][0] * c1 * c23 - R[2][1] * s1 * c23 + R[2][2] * s23
|
||
|
||
if abs(t1_j4) < self.SINGULAR_FUZZ and abs(t2_j4) < self.SINGULAR_FUZZ:
|
||
th4 = self._last_joints[3] * TO_RAD if len(self._last_joints) > 3 else 0
|
||
else:
|
||
th4 = math.atan2(t1_j4, t2_j4)
|
||
|
||
s4 = math.sin(th4)
|
||
c4 = math.cos(th4)
|
||
|
||
s5 = R[2][2] * (s23*c4) - R[2][0] * (c1*c23*c4 + s1*s4) - R[2][1] * (s1*c23*c4 - c1*s4)
|
||
c5 = -R[2][0] * (c1*s23) - R[2][1] * (s1*s23) - R[2][2] * c23
|
||
th5 = math.atan2(s5, c5)
|
||
|
||
s6 = R[0][2] * (s23*s4) - R[0][0] * (c1*c23*s4 - s1*c4) - R[0][1] * (s1*c23*s4 + c1*c4)
|
||
c6 = R[0][0] * ((c1*c23*c4 + s1*s4)*c5 - c1*s23*s5) + \
|
||
R[0][1] * ((s1*c23*c4 - c1*s4)*c5 - s1*s23*s5) - \
|
||
R[0][2] * (s23*c4*c5 + c23*s5)
|
||
th6 = math.atan2(s6, c6)
|
||
|
||
if flags & self.PUMA_WRIST_FLIP:
|
||
th4 = th4 + math.pi
|
||
th5 = -th5
|
||
th6 = th6 + math.pi
|
||
|
||
joints = [
|
||
th1 * TO_DEG, th2 * TO_DEG, th3 * TO_DEG,
|
||
th4 * TO_DEG, th5 * TO_DEG, th6 * TO_DEG
|
||
]
|
||
|
||
joints = [(j + 180) % 360 - 180 for j in joints]
|
||
self._last_joints = joints.copy()
|
||
|
||
if self.debug:
|
||
print(f"[PUMA] INV: world -> joints({joints[0]:.3f}, {joints[1]:.3f}, ...)")
|
||
|
||
return joints
|
||
|
||
|
||
# ==================== SCARA 运动学参数 ====================
|
||
|
||
@dataclass
|
||
class SCARAParams(KinematicsParams):
|
||
"""SCARA 参数"""
|
||
d1: float = 490.0
|
||
d2: float = 340.0
|
||
d3: float = 50.0
|
||
d4: float = 250.0
|
||
d5: float = 50.0
|
||
d6: float = 50.0
|
||
|
||
|
||
# ==================== SCARA 运动学 ====================
|
||
|
||
class SCARAKinematics(BaseKinematics):
|
||
"""
|
||
SCARA 运动学 (基于 scarakins.c)
|
||
"""
|
||
|
||
def __init__(self, params: SCARAParams = None, debug: bool = False):
|
||
super().__init__(params or SCARAParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
self._inverse_flags = 0
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 4:
|
||
raise ValueError("SCARA requires at least 4 joints")
|
||
|
||
a0 = joints[0] * TO_RAD
|
||
a1 = joints[1] * TO_RAD
|
||
a3 = joints[3] * TO_RAD if len(joints) > 3 else 0
|
||
|
||
a1_total = a1 + a0
|
||
a3_total = a3 + a1_total
|
||
|
||
x = self.p.d2 * math.cos(a0) + self.p.d4 * math.cos(a1_total) + self.p.d6 * math.cos(a3_total)
|
||
y = self.p.d2 * math.sin(a0) + self.p.d4 * math.sin(a1_total) + self.p.d6 * math.sin(a3_total)
|
||
z = self.p.d1 + self.p.d3 - joints[2] - self.p.d5
|
||
c = a3_total * TO_DEG
|
||
|
||
if self.debug:
|
||
print(f"[SCARA] FWD: joints -> world({x:.3f},{y:.3f},{z:.3f})")
|
||
|
||
return (x, y, z, 0.0, 0.0, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
if len(world) < 6:
|
||
raise ValueError("SCARA requires 6 coordinates")
|
||
|
||
x, y, z, _, _, c = world[:6]
|
||
a3 = c * TO_RAD
|
||
|
||
xt = x - self.p.d6 * math.cos(a3)
|
||
yt = y - self.p.d6 * math.sin(a3)
|
||
|
||
rsq = xt*xt + yt*yt
|
||
cc = (rsq - self.p.d2*self.p.d2 - self.p.d4*self.p.d4) / (2 * self.p.d2 * self.p.d4)
|
||
cc = max(-1.0, min(1.0, cc))
|
||
|
||
q1 = math.acos(cc)
|
||
if self._inverse_flags & 1:
|
||
q1 = -q1
|
||
|
||
q0 = math.atan2(yt, xt) - math.atan2(self.p.d4 * math.sin(q1), self.p.d2 + self.p.d4 * math.cos(q1))
|
||
|
||
joints = [
|
||
q0 * TO_DEG,
|
||
q1 * TO_DEG,
|
||
self.p.d1 + self.p.d3 - self.p.d5 - z,
|
||
c - (q0 + q1) * TO_DEG,
|
||
0.0, 0.0
|
||
]
|
||
|
||
if self.debug:
|
||
print(f"[SCARA] INV: world({x:.3f},{y:.3f},{z:.3f}) -> joints")
|
||
|
||
return joints
|
||
|
||
def set_flags(self, flags: int):
|
||
"""设置逆解标志"""
|
||
self._inverse_flags = flags
|
||
|
||
|
||
# ==================== 线性 Delta 运动学 ====================
|
||
|
||
class LinearDeltaKinematics(BaseKinematics):
|
||
"""
|
||
线性 Delta 机器人运动学 (基于 lineardeltakins-common.h)
|
||
Rostock 风格 Delta 机器人
|
||
"""
|
||
|
||
def __init__(self, radius: float = 165.25, rod_length: float = 269.0, debug: bool = False):
|
||
super().__init__()
|
||
self.R = radius
|
||
self.L = rod_length
|
||
self.debug = debug
|
||
self._update_geometry()
|
||
|
||
def _update_geometry(self):
|
||
self.L2 = self.L * self.L
|
||
SQ3 = math.sqrt(3)
|
||
SIN_60 = SQ3 / 2
|
||
COS_60 = 0.5
|
||
|
||
self.Ax = 0.0
|
||
self.Ay = self.R
|
||
|
||
self.Bx = -SIN_60 * self.R
|
||
self.By = -COS_60 * self.R
|
||
|
||
self.Cx = SIN_60 * self.R
|
||
self.Cy = -COS_60 * self.R
|
||
|
||
def set_geometry(self, radius: float, rod_length: float):
|
||
self.R = radius
|
||
self.L = rod_length
|
||
self._update_geometry()
|
||
if self.debug:
|
||
print(f"[LINEAR_DELTA] 几何更新: R={radius:.3f}, L={rod_length:.3f}")
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 3:
|
||
raise ValueError("Linear Delta requires 3 joints")
|
||
|
||
q1, q2, q3 = joints[0], joints[1], joints[2]
|
||
|
||
den = (self.By - self.Ay) * self.Cx - (self.Cy - self.Ay) * self.Bx
|
||
|
||
if abs(den) < 1e-12:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
w1 = self.Ay * self.Ay + q1 * q1
|
||
w2 = self.Bx * self.Bx + self.By * self.By + q2 * q2
|
||
w3 = self.Cx * self.Cx + self.Cy * self.Cy + q3 * q3
|
||
|
||
a1 = (q2 - q1) * (self.Cy - self.Ay) - (q3 - q1) * (self.By - self.Ay)
|
||
b1 = -((w2 - w1) * (self.Cy - self.Ay) - (w3 - w1) * (self.By - self.Ay)) / 2.0
|
||
|
||
a2 = -(q2 - q1) * self.Cx + (q3 - q1) * self.Bx
|
||
b2 = ((w2 - w1) * self.Cx - (w3 - w1) * self.Bx) / 2.0
|
||
|
||
a = a1 * a1 + a2 * a2 + den * den
|
||
b = 2 * (a1 * b1 + a2 * (b2 - self.Ay * den) - q1 * den * den)
|
||
c = (b2 - self.Ay * den) * (b2 - self.Ay * den) + b1 * b1 + den * den * (q1 * q1 - self.L2)
|
||
|
||
discr = b * b - 4.0 * a * c
|
||
if discr < 0:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
z = -0.5 * (b + math.sqrt(discr)) / a
|
||
x = (a1 * z + b1) / den
|
||
y = (a2 * z + b2) / den
|
||
|
||
if self.debug:
|
||
print(f"[LINEAR_DELTA] FWD: joints({q1:.3f},{q2:.3f},{q3:.3f}) -> world({x:.3f},{y:.3f},{z:.3f})")
|
||
|
||
return (x, y, z, 0.0, 0.0, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
|
||
def sq(v): return v * v
|
||
|
||
q1 = z + math.sqrt(max(0, self.L2 - sq(self.Ax - x) - sq(self.Ay - y)))
|
||
q2 = z + math.sqrt(max(0, self.L2 - sq(self.Bx - x) - sq(self.By - y)))
|
||
q3 = z + math.sqrt(max(0, self.L2 - sq(self.Cx - x) - sq(self.Cy - y)))
|
||
|
||
if self.debug:
|
||
print(f"[LINEAR_DELTA] INV: world({x:.3f},{y:.3f},{z:.3f}) -> joints({q1:.3f},{q2:.3f},{q3:.3f})")
|
||
|
||
return [q1, q2, q3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||
|
||
|
||
# ==================== DH 参数串联运动学 ====================
|
||
|
||
@dataclass
|
||
class DHLink:
|
||
"""Denavit-Hartenberg 参数"""
|
||
a: float = 0.0
|
||
alpha: float = 0.0
|
||
d: float = 0.0
|
||
theta: float = 0.0
|
||
is_revolute: bool = True
|
||
min_limit: float = -180.0
|
||
max_limit: float = 180.0
|
||
|
||
|
||
@dataclass
|
||
class SerialDHParams(KinematicsParams):
|
||
"""串联 DH 参数"""
|
||
links: List[DHLink] = field(default_factory=list)
|
||
max_iterations: int = 100
|
||
convergence_epsilon: float = 1e-6
|
||
use_kinematic_decoupling: bool = False
|
||
|
||
|
||
class SerialDHKinematics(BaseKinematics):
|
||
"""
|
||
通用串联机器人运动学 - 完整版
|
||
基于 genserfuncs.c 完整实现
|
||
"""
|
||
|
||
def __init__(self, params: SerialDHParams = None, debug: bool = False):
|
||
super().__init__(params or SerialDHParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
self.iterations = 0
|
||
self.GO_REAL_EPSILON = 1e-6
|
||
|
||
def _dh_transform(self, link: DHLink, joint_value: float) -> List[List[float]]:
|
||
"""计算单个连杆的变换矩阵"""
|
||
if link.is_revolute:
|
||
theta = (link.theta + joint_value) * TO_RAD
|
||
d = link.d
|
||
else:
|
||
theta = link.theta * TO_RAD
|
||
d = link.d + joint_value
|
||
|
||
alpha = link.alpha * TO_RAD
|
||
a = link.a
|
||
|
||
ct = math.cos(theta)
|
||
st = math.sin(theta)
|
||
ca = math.cos(alpha)
|
||
sa = math.sin(alpha)
|
||
|
||
return [
|
||
[ct, -st*ca, st*sa, a*ct],
|
||
[st, ct*ca, -ct*sa, a*st],
|
||
[ 0, sa, ca, d],
|
||
[ 0, 0, 0, 1]
|
||
]
|
||
|
||
def _mat_mult_4x4(self, A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
|
||
"""4x4 矩阵乘法"""
|
||
result = [[0.0]*4 for _ in range(4)]
|
||
for i in range(4):
|
||
for j in range(4):
|
||
for k in range(4):
|
||
result[i][j] += A[i][k] * B[k][j]
|
||
return result
|
||
|
||
def _mat_mult_3x3_vec(self, M: List[List[float]], v: List[float]) -> List[float]:
|
||
"""3x3 矩阵乘 3x1 向量"""
|
||
return [
|
||
M[0][0]*v[0] + M[0][1]*v[1] + M[0][2]*v[2],
|
||
M[1][0]*v[0] + M[1][1]*v[1] + M[1][2]*v[2],
|
||
M[2][0]*v[0] + M[2][1]*v[1] + M[2][2]*v[2]
|
||
]
|
||
|
||
def _cross_product_matrix(self, v: List[float]) -> List[List[float]]:
|
||
"""向量的叉积矩阵 (用于雅可比计算)"""
|
||
return [
|
||
[ 0, -v[2], v[1]],
|
||
[ v[2], 0, -v[0]],
|
||
[-v[1], v[0], 0]
|
||
]
|
||
|
||
def _mat_mult_3x3(self, A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
|
||
"""3x3 矩阵乘法"""
|
||
result = [[0.0]*3 for _ in range(3)]
|
||
for i in range(3):
|
||
for j in range(3):
|
||
for k in range(3):
|
||
result[i][j] += A[i][k] * B[k][j]
|
||
return result
|
||
|
||
def _mat_transpose_3x3(self, M: List[List[float]]) -> List[List[float]]:
|
||
"""3x3 矩阵转置"""
|
||
return [[M[j][i] for j in range(3)] for i in range(3)]
|
||
|
||
def _rpy_to_rot_matrix(self, r: float, p: float, y: float) -> List[List[float]]:
|
||
"""RPY 角转旋转矩阵"""
|
||
cr, sr = math.cos(r), math.sin(r)
|
||
cp, sp = math.cos(p), math.sin(p)
|
||
cy, sy = math.cos(y), math.sin(y)
|
||
|
||
return [
|
||
[cy*cp, cy*sp*sr - sy*cr, cy*sp*cr + sy*sr],
|
||
[sy*cp, sy*sp*sr + cy*cr, sy*sp*cr - cy*sr],
|
||
[ -sp, cp*sr, cp*cr]
|
||
]
|
||
|
||
def _rot_matrix_to_rpy(self, R: List[List[float]]) -> Tuple[float, float, float]:
|
||
"""旋转矩阵转 RPY 角"""
|
||
if abs(R[2][0]) < 0.99999:
|
||
p = math.atan2(-R[2][0], math.sqrt(R[0][0]**2 + R[1][0]**2))
|
||
y = math.atan2(R[1][0]/math.cos(p), R[0][0]/math.cos(p))
|
||
r = math.atan2(R[2][1]/math.cos(p), R[2][2]/math.cos(p))
|
||
else:
|
||
y = 0
|
||
if R[2][0] <= -1:
|
||
p = math.pi / 2
|
||
r = math.atan2(R[0][1], R[0][2])
|
||
else:
|
||
p = -math.pi / 2
|
||
r = math.atan2(-R[0][1], -R[0][2])
|
||
return (r, p, y)
|
||
|
||
def _compute_jfwd(self, joints: List[float]) -> Tuple[List[List[float]], List[List[float]]]:
|
||
"""计算正向雅可比矩阵"""
|
||
n = len(joints)
|
||
|
||
Jv = [[0.0] * n for _ in range(3)]
|
||
Jw = [[0.0] * n for _ in range(3)]
|
||
|
||
if self.p.links[0].is_revolute:
|
||
Jw[2][0] = 1.0
|
||
else:
|
||
Jv[2][0] = 1.0
|
||
|
||
T_curr = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]]
|
||
poses = []
|
||
|
||
for i, link in enumerate(self.p.links):
|
||
T_link = self._dh_transform(link, joints[i])
|
||
T_curr = self._mat_mult_4x4(T_curr, T_link)
|
||
poses.append(T_curr)
|
||
|
||
R_end = [[T_curr[i][j] for j in range(3)] for i in range(3)]
|
||
|
||
for col in range(1, n):
|
||
T_prev = poses[col - 1]
|
||
R_prev = [[T_prev[i][j] for j in range(3)] for i in range(3)]
|
||
|
||
link = self.p.links[col]
|
||
if link.is_revolute:
|
||
theta = (link.theta + joints[col]) * TO_RAD
|
||
d = link.d
|
||
else:
|
||
theta = link.theta * TO_RAD
|
||
d = link.d + joints[col]
|
||
|
||
a = link.a
|
||
P_i_ip1 = [a * math.cos(theta), a * math.sin(theta), d]
|
||
P_ip1_0 = self._mat_mult_3x3_vec(R_prev, P_i_ip1)
|
||
|
||
Jw_col = [0.0, 0.0, 1.0] if link.is_revolute else [0.0, 0.0, 0.0]
|
||
cross = self._cross_product_matrix(P_ip1_0)
|
||
Jv_col = [cross[i][0]*Jw_col[0] + cross[i][1]*Jw_col[1] + cross[i][2]*Jw_col[2] for i in range(3)]
|
||
|
||
for i in range(3):
|
||
Jv[i][col] = Jv_col[i]
|
||
Jw[i][col] = Jw_col[i]
|
||
|
||
R_end_inv = self._mat_transpose_3x3(R_end)
|
||
Jv = [self._mat_mult_3x3_vec(R_end_inv, [Jv[i][j] for i in range(3)]) for j in range(n)]
|
||
Jv = [[Jv[j][i] for j in range(n)] for i in range(3)]
|
||
Jw = [self._mat_mult_3x3_vec(R_end_inv, [Jw[i][j] for i in range(3)]) for j in range(n)]
|
||
Jw = [[Jw[j][i] for j in range(n)] for i in range(3)]
|
||
|
||
Jfwd = [[0.0] * n for _ in range(6)]
|
||
for col in range(n):
|
||
for row in range(3):
|
||
Jfwd[row][col] = Jv[row][col]
|
||
Jfwd[row + 3][col] = Jw[row][col]
|
||
|
||
return Jfwd, R_end
|
||
|
||
def _mat_invert_nxn(self, M: List[List[float]], n: int) -> List[List[float]]:
|
||
"""nxn 矩阵求逆"""
|
||
aug = [[0.0] * (2 * n) for _ in range(n)]
|
||
|
||
for i in range(n):
|
||
for j in range(n):
|
||
aug[i][j] = M[i][j]
|
||
aug[i][i + n] = 1.0
|
||
|
||
for k in range(n):
|
||
pivot = aug[k][k]
|
||
if abs(pivot) < 1e-12:
|
||
raise ValueError(f"Matrix is singular at row {k}")
|
||
|
||
for j in range(2 * n):
|
||
aug[k][j] /= pivot
|
||
|
||
for i in range(n):
|
||
if i != k:
|
||
factor = aug[i][k]
|
||
for j in range(2 * n):
|
||
aug[i][j] -= factor * aug[k][j]
|
||
|
||
return [[aug[i][j + n] for j in range(n)] for i in range(n)]
|
||
|
||
def _compute_jinv(self, Jfwd: List[List[float]]) -> List[List[float]]:
|
||
"""计算雅可比矩阵的逆或伪逆"""
|
||
m = len(Jfwd)
|
||
n = len(Jfwd[0])
|
||
|
||
if m == n:
|
||
return self._mat_invert_nxn(Jfwd, m)
|
||
elif m < n:
|
||
JJT = [[0.0] * m for _ in range(m)]
|
||
for i in range(m):
|
||
for j in range(m):
|
||
for k in range(n):
|
||
JJT[i][j] += Jfwd[i][k] * Jfwd[j][k]
|
||
|
||
JJT_inv = self._mat_invert_nxn(JJT, m)
|
||
JT = [[Jfwd[j][i] for j in range(m)] for i in range(n)]
|
||
Jinv = [[0.0] * m for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(m):
|
||
for k in range(m):
|
||
Jinv[i][j] += JT[i][k] * JJT_inv[k][j]
|
||
return Jinv
|
||
else:
|
||
JTJ = [[0.0] * n for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(n):
|
||
for k in range(m):
|
||
JTJ[i][j] += Jfwd[k][i] * Jfwd[k][j]
|
||
|
||
JTJ_inv = self._mat_invert_nxn(JTJ, n)
|
||
JT = [[Jfwd[j][i] for j in range(m)] for i in range(n)]
|
||
Jinv = [[0.0] * m for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(m):
|
||
for k in range(n):
|
||
Jinv[i][j] += JTJ_inv[i][k] * JT[k][j]
|
||
return Jinv
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
"""正运动学"""
|
||
if len(joints) != len(self.p.links):
|
||
raise ValueError(f"Expected {len(self.p.links)} joints, got {len(joints)}")
|
||
|
||
T = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]]
|
||
|
||
for i, link in enumerate(self.p.links):
|
||
T_link = self._dh_transform(link, joints[i])
|
||
T = self._mat_mult_4x4(T, T_link)
|
||
|
||
x, y, z = T[0][3], T[1][3], T[2][3]
|
||
|
||
r, p, y_rpy = self._rot_matrix_to_rpy([[T[i][j] for j in range(3)] for i in range(3)])
|
||
|
||
if self.debug:
|
||
print(f"[SERIAL_DH] FWD: joints -> world({x:.3f},{y:.3f},{z:.3f})")
|
||
|
||
return (x, y, z, r * TO_DEG, p * TO_DEG, y_rpy * TO_DEG)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float],
|
||
initial_joints: List[float] = None) -> List[float]:
|
||
"""逆运动学:牛顿-拉夫逊迭代法"""
|
||
if len(world) < 6:
|
||
raise ValueError("Serial DH requires 6 coordinates")
|
||
|
||
n = len(self.p.links)
|
||
|
||
if initial_joints and len(initial_joints) == n:
|
||
jest = list(initial_joints)
|
||
else:
|
||
jest = [0.0] * n
|
||
|
||
x, y, z, a, b, c = world[:6]
|
||
R_target = self._rpy_to_rot_matrix(a * TO_RAD, b * TO_RAD, c * TO_RAD)
|
||
|
||
self.iterations = 0
|
||
|
||
for iteration in range(self.p.max_iterations):
|
||
self.iterations = iteration + 1
|
||
|
||
Jfwd, R_current = self._compute_jfwd(jest)
|
||
current_pose = self.forward(jest)
|
||
cx, cy, cz = current_pose[0], current_pose[1], current_pose[2]
|
||
|
||
dx = x - cx
|
||
dy = y - cy
|
||
dz = z - cz
|
||
|
||
R_diff = self._mat_mult_3x3(R_target, self._mat_transpose_3x3(R_current))
|
||
rx = 0.5 * (R_diff[2][1] - R_diff[1][2])
|
||
ry = 0.5 * (R_diff[0][2] - R_diff[2][0])
|
||
rz = 0.5 * (R_diff[1][0] - R_diff[0][1])
|
||
|
||
error = [dx, dy, dz, rx, ry, rz]
|
||
|
||
max_error = max(abs(e) for e in error)
|
||
if max_error < self.p.convergence_epsilon:
|
||
if self.debug:
|
||
print(f"[SERIAL_DH] INV: converged after {iteration + 1} iterations")
|
||
return jest
|
||
|
||
try:
|
||
Jinv = self._compute_jinv(Jfwd)
|
||
except ValueError:
|
||
break
|
||
|
||
dq = [0.0] * n
|
||
for i in range(n):
|
||
for j in range(6):
|
||
dq[i] += Jinv[i][j] * error[j]
|
||
|
||
for i in range(n):
|
||
jest[i] += dq[i]
|
||
|
||
if self.debug:
|
||
print(f"[SERIAL_DH] INV: did not converge after {self.p.max_iterations} iterations")
|
||
|
||
return jest
|
||
|
||
|
||
# ==================== 新增运动学类 ====================
|
||
|
||
@dataclass
|
||
class TripodParams(KinematicsParams):
|
||
"""Tripod 参数"""
|
||
Bx: float = 1.0
|
||
Cx: float = 1.0
|
||
Cy: float = 1.0
|
||
|
||
|
||
class TripodKinematics(BaseKinematics):
|
||
"""Tripod运动学 - 基于 tripodkins.c"""
|
||
|
||
def __init__(self, params: TripodParams = None, debug: bool = False):
|
||
super().__init__(params or TripodParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 3:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
P = joints[0] * joints[0]
|
||
Q = joints[1] * joints[1] - self.p.Bx * self.p.Bx
|
||
R = joints[2] * joints[2] - self.p.Cx * self.p.Cx - self.p.Cy * self.p.Cy
|
||
|
||
s = -2.0 * self.p.Bx
|
||
t = -2.0 * self.p.Cx
|
||
u = -2.0 * self.p.Cy
|
||
|
||
if abs(s) < 1e-12 or abs(u) < 1e-12:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
x = (Q - P) / s
|
||
y = (R - Q - (t - s) * x) / u
|
||
z = P - x*x - y*y
|
||
|
||
if z < 0:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
z = math.sqrt(z)
|
||
|
||
if self.debug:
|
||
print(f"[TRIPOD] FWD: joints -> world({x:.3f},{y:.3f},{z:.3f})")
|
||
return (x, y, z, 0.0, 0.0, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
|
||
L1 = math.sqrt(x*x + y*y + z*z)
|
||
L2 = math.sqrt((x - self.p.Bx)*(x - self.p.Bx) + y*y + z*z)
|
||
L3 = math.sqrt((x - self.p.Cx)*(x - self.p.Cx) + (y - self.p.Cy)*(y - self.p.Cy) + z*z)
|
||
|
||
if self.debug:
|
||
print(f"[TRIPOD] INV: world({x:.3f},{y:.3f},{z:.3f}) -> joints({L1:.3f},{L2:.3f},{L3:.3f})")
|
||
return [L1, L2, L3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||
|
||
|
||
@dataclass
|
||
class PentapodParams(KinematicsParams):
|
||
"""Pentapod 参数"""
|
||
base: List[Tuple[float, float, float]] = field(default_factory=lambda: [
|
||
(-418.03, 324.56, 895.56), (417.96, 324.56, 895.56),
|
||
(-418.03, -325.44, 895.56), (417.96, -325.44, 895.56),
|
||
(-0.06, -492.96, 895.56)
|
||
])
|
||
effector_r: List[float] = field(default_factory=lambda: [80.32] * 5)
|
||
effector_z: List[float] = field(default_factory=lambda: [-185.50, -159.50, -67.50, -41.50, -14.00])
|
||
tool_offset: float = 0.0
|
||
convergence_criterion: float = 1e-9
|
||
max_iterations: int = 120
|
||
max_error: float = 100.0
|
||
|
||
|
||
class PentapodKinematics(BaseKinematics):
|
||
"""Pentapod运动学 - 基于 pentakins.c"""
|
||
|
||
NUM_STRUTS = 5
|
||
|
||
def __init__(self, params: PentapodParams = None, debug: bool = False):
|
||
super().__init__(params or PentapodParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
self.last_iterations = 0
|
||
|
||
def _rotation_matrix(self, r: float, p: float, y: float) -> List[List[float]]:
|
||
cr, sr = math.cos(r), math.sin(r)
|
||
cp, sp = math.cos(p), math.sin(p)
|
||
return [
|
||
[cp, sp*sr, sp*cr],
|
||
[0, cr, -sr],
|
||
[-sp, cp*sr, cp*cr]
|
||
]
|
||
|
||
def _inv_kins(self, coord: List[float]) -> List[float]:
|
||
x, y, z = coord[0], coord[1], coord[2]
|
||
r, p = coord[3], coord[4]
|
||
|
||
R = self._rotation_matrix(r, p, 0)
|
||
R_inv = [[R[j][i] for j in range(3)] for i in range(3)]
|
||
|
||
struts = []
|
||
for i in range(self.NUM_STRUTS):
|
||
bx, by, bz = self.p.base[i]
|
||
bz += self.p.tool_offset
|
||
ra = self.p.effector_r[i]
|
||
za = self.p.effector_z[i] + self.p.tool_offset
|
||
|
||
dx = bx - x
|
||
dy = by - y
|
||
dz = bz - z
|
||
|
||
tx = R_inv[0][0]*dx + R_inv[0][1]*dy + R_inv[0][2]*dz
|
||
ty = R_inv[1][0]*dx + R_inv[1][1]*dy + R_inv[1][2]*dz
|
||
tz = R_inv[2][0]*dx + R_inv[2][1]*dy + R_inv[2][2]*dz
|
||
|
||
L = math.sqrt((tz - za)**2 + (math.sqrt(tx*tx + ty*ty) - ra)**2)
|
||
struts.append(L)
|
||
|
||
return struts
|
||
|
||
def _mat_invert_5x5(self, J: List[List[float]]) -> List[List[float]]:
|
||
n = 5
|
||
aug = [[0.0] * (2 * n) for _ in range(n)]
|
||
|
||
for i in range(n):
|
||
for j in range(n):
|
||
aug[i][j] = J[i][j]
|
||
aug[i][i + n] = 1.0
|
||
|
||
for k in range(n):
|
||
if abs(aug[k][k]) < 0.01:
|
||
for j in range(k + 1, n):
|
||
if abs(aug[j][k]) > 0.01:
|
||
aug[k], aug[j] = aug[j], aug[k]
|
||
break
|
||
|
||
pivot = aug[k][k]
|
||
if abs(pivot) < 1e-12:
|
||
continue
|
||
|
||
for j in range(2 * n):
|
||
aug[k][j] /= pivot
|
||
|
||
for i in range(n):
|
||
if i != k:
|
||
factor = aug[i][k]
|
||
for j in range(2 * n):
|
||
aug[i][j] -= factor * aug[k][j]
|
||
|
||
return [[aug[i][j + n] for j in range(n)] for i in range(n)]
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 5:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
coord = [0.0, 0.0, 0.0, 0.0, 0.0]
|
||
iteration = 0
|
||
|
||
while iteration < self.p.max_iterations:
|
||
iteration += 1
|
||
|
||
struts = self._inv_kins(coord)
|
||
diff = [struts[i] - joints[i] for i in range(5)]
|
||
|
||
max_diff = max(abs(d) for d in diff)
|
||
if max_diff < self.p.convergence_criterion:
|
||
break
|
||
|
||
J = [[0.0]*5 for _ in range(5)]
|
||
delta = 1e-4
|
||
for i in range(5):
|
||
orig = coord[i]
|
||
coord[i] += delta
|
||
struts_pert = self._inv_kins(coord)
|
||
coord[i] = orig
|
||
for j in range(5):
|
||
J[j][i] = (struts_pert[j] - struts[j]) / delta
|
||
|
||
try:
|
||
Jinv = self._mat_invert_5x5(J)
|
||
dq = [0.0] * 5
|
||
for i in range(5):
|
||
for j in range(5):
|
||
dq[i] += Jinv[i][j] * diff[j]
|
||
|
||
for i in range(5):
|
||
coord[i] -= dq[i]
|
||
except:
|
||
pass
|
||
|
||
self.last_iterations = iteration
|
||
|
||
if self.debug:
|
||
print(f"[PENTAPOD] FWD: converged after {iteration} iterations")
|
||
|
||
return (coord[0], coord[1], coord[2], coord[3] * TO_DEG, coord[4] * TO_DEG, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
a, b = world[3] * TO_RAD, world[4] * TO_RAD
|
||
|
||
coord = [x, y, z, a, b]
|
||
struts = self._inv_kins(coord)
|
||
|
||
if self.debug:
|
||
print(f"[PENTAPOD] INV: world -> {len(struts)} struts")
|
||
|
||
return struts + [0.0] * (9 - len(struts))
|
||
|
||
def set_tool_length(self, length: float):
|
||
self.p.tool_offset = length
|
||
if self.debug:
|
||
print(f"[PENTAPOD] 刀具长度设置为: {length:.3f} mm")
|
||
|
||
|
||
@dataclass
|
||
class RotaryDeltaParams(KinematicsParams):
|
||
"""Rotary Delta 参数"""
|
||
platform_radius: float = 10.0
|
||
thigh_length: float = 10.0
|
||
shin_length: float = 14.0
|
||
foot_radius: float = 6.0
|
||
|
||
|
||
class RotaryDeltaKinematics(BaseKinematics):
|
||
"""Rotary Delta运动学 - 基于 rotarydeltakins.c"""
|
||
|
||
def __init__(self, params: RotaryDeltaParams = None, debug: bool = False):
|
||
super().__init__(params or RotaryDeltaParams())
|
||
self.debug = debug
|
||
self.p = self.params
|
||
|
||
def _inverse_j0(self, x: float, y: float, z: float) -> Optional[float]:
|
||
pfr = self.p.platform_radius
|
||
fr = self.p.foot_radius
|
||
tl = self.p.thigh_length
|
||
sl = self.p.shin_length
|
||
|
||
a = 0.5 * (x*x + (y-fr)*(y-fr) + z*z + tl*tl - sl*sl - pfr*pfr) / z
|
||
b = (fr - pfr - y) / z
|
||
|
||
d = tl*tl * (b*b + 1) - (a - b*pfr)*(a - b*pfr)
|
||
if d < 0:
|
||
return None
|
||
|
||
knee_y = (pfr + a*b + math.sqrt(d)) / (b*b + 1)
|
||
knee_z = b * knee_y - a
|
||
theta = math.atan2(knee_z, knee_y - pfr) * TO_DEG
|
||
return theta
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 3:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
j0, j1, j2 = [j * TO_RAD for j in joints[:3]]
|
||
pfr = self.p.platform_radius
|
||
fr = self.p.foot_radius
|
||
tl = self.p.thigh_length
|
||
|
||
y1 = -(pfr - fr + tl * math.cos(j0))
|
||
z1 = -tl * math.sin(j0)
|
||
|
||
y2 = (pfr - fr + tl * math.cos(j1)) * 0.5
|
||
x2 = y2 * math.sqrt(3)
|
||
z2 = -tl * math.sin(j1)
|
||
|
||
y3 = (pfr - fr + tl * math.cos(j2)) * 0.5
|
||
x3 = -y3 * math.sqrt(3)
|
||
z3 = -tl * math.sin(j2)
|
||
|
||
denom = x3*(y2-y1) - x2*(y3-y1)
|
||
if abs(denom) < 1e-12:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
w1 = y1*y1 + z1*z1
|
||
w2 = x2*x2 + y2*y2 + z2*z2
|
||
w3 = x3*x3 + y3*y3 + z3*z3
|
||
|
||
a1 = (z2-z1)*(y3-y1) - (z3-z1)*(y2-y1)
|
||
b1 = -((w2-w1)*(y3-y1) - (w3-w1)*(y2-y1)) / 2.0
|
||
a2 = -(z2-z1)*x3 + (z3-z1)*x2
|
||
b2 = ((w2-w1)*x3 - (w3-w1)*x2) / 2.0
|
||
|
||
a = a1*a1 + a2*a2 + denom*denom
|
||
b = 2 * (a1*b1 + a2*(b2 - y1*denom) - z1*denom*denom)
|
||
c = (b2 - y1*denom)*(b2 - y1*denom) + b1*b1 + denom*denom*(z1*z1 - self.p.shin_length*self.p.shin_length)
|
||
|
||
d = b*b - 4*a*c
|
||
if d < 0:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
z = (-b - math.sqrt(d)) / (2*a)
|
||
x = (a1*z + b1) / denom
|
||
y = (a2*z + b2) / denom
|
||
|
||
if self.debug:
|
||
print(f"[ROTARY_DELTA] FWD: joints -> world({x:.3f},{y:.3f},{z:.3f})")
|
||
|
||
return (x, y, z, 0.0, 0.0, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
|
||
def rotate(x: float, y: float, theta: float) -> Tuple[float, float]:
|
||
c, s = math.cos(theta), math.sin(theta)
|
||
return (x*c - y*s, x*s + y*c)
|
||
|
||
j0 = self._inverse_j0(x, y, z)
|
||
if j0 is None:
|
||
return [0.0] * 9
|
||
|
||
xr, yr = rotate(x, y, -2*math.pi/3)
|
||
j1 = self._inverse_j0(xr, yr, z)
|
||
if j1 is None:
|
||
return [0.0] * 9
|
||
|
||
xr, yr = rotate(x, y, 2*math.pi/3)
|
||
j2 = self._inverse_j0(xr, yr, z)
|
||
if j2 is None:
|
||
return [0.0] * 9
|
||
|
||
if self.debug:
|
||
print(f"[ROTARY_DELTA] INV: world -> joints({j0:.3f},{j1:.3f},{j2:.3f})")
|
||
|
||
return [j0, j1, j2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||
|
||
|
||
class RotateKinematics(BaseKinematics):
|
||
"""Rotary Table运动学 - 基于 rotatekins.c"""
|
||
|
||
def __init__(self, params: KinematicsParams = None, debug: bool = False):
|
||
super().__init__(params)
|
||
self.debug = debug
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 6:
|
||
joints = list(joints) + [0.0] * (6 - len(joints))
|
||
|
||
x, y, z, a, b, c = joints[0], joints[1], joints[2], joints[3], joints[4], joints[5]
|
||
c_rad = -c * TO_RAD
|
||
|
||
pos_x = x * math.cos(c_rad) - y * math.sin(c_rad)
|
||
pos_y = x * math.sin(c_rad) + y * math.cos(c_rad)
|
||
|
||
return (pos_x, pos_y, z, a, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
if len(world) < 6:
|
||
world = list(world) + [0.0] * (6 - len(world))
|
||
|
||
wx, wy, wz, wa, wb, wc = world[0], world[1], world[2], world[3], world[4], world[5]
|
||
c_rad = wc * TO_RAD
|
||
|
||
x = wx * math.cos(c_rad) - wy * math.sin(c_rad)
|
||
y = wx * math.sin(c_rad) + wy * math.cos(c_rad)
|
||
|
||
u = world[6] if len(world) > 6 else 0.0
|
||
v = world[7] if len(world) > 7 else 0.0
|
||
w = world[8] if len(world) > 8 else 0.0
|
||
|
||
return [x, y, wz, wa, wb, wc, u, v, w]
|
||
|
||
|
||
class CoreXYKinematics(BaseKinematics):
|
||
"""CoreXY运动学 - 基于 corexykins.c"""
|
||
|
||
def __init__(self, params: KinematicsParams = None, debug: bool = False):
|
||
super().__init__(params)
|
||
self.debug = debug
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 3:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
x = 0.5 * (joints[0] + joints[1])
|
||
y = 0.5 * (joints[0] - joints[1])
|
||
z = joints[2] if len(joints) > 2 else 0.0
|
||
a = joints[3] if len(joints) > 3 else 0.0
|
||
b = joints[4] if len(joints) > 4 else 0.0
|
||
c = joints[5] if len(joints) > 5 else 0.0
|
||
|
||
return (x, y, z, a, b, c)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z, a, b, c = world[0], world[1], world[2], world[3], world[4], world[5]
|
||
|
||
j0 = x + y
|
||
j1 = x - y
|
||
j2 = z
|
||
j3 = a
|
||
j4 = b
|
||
j5 = c
|
||
|
||
u = world[6] if len(world) > 6 else 0.0
|
||
v = world[7] if len(world) > 7 else 0.0
|
||
w = world[8] if len(world) > 8 else 0.0
|
||
|
||
return [j0, j1, j2, j3, j4, j5, u, v, w]
|
||
|
||
|
||
class ScorbotKinematics(BaseKinematics):
|
||
"""Scorbot ER3运动学 - 基于 scorbot-kins.c"""
|
||
|
||
L0_HORIZONTAL = 16.0
|
||
L0_VERTICAL = 140.0
|
||
L1 = 221.0
|
||
L2 = 221.0
|
||
|
||
def __init__(self, params: KinematicsParams = None, debug: bool = False):
|
||
super().__init__(params)
|
||
self.debug = debug
|
||
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 4:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
j0, j1, j2 = joints[0], joints[1], joints[2]
|
||
j0_rad, j1_rad, j2_rad = j0 * TO_RAD, j1 * TO_RAD, j2 * TO_RAD
|
||
|
||
j1_x = self.L0_HORIZONTAL * math.cos(j0_rad)
|
||
j1_y = self.L0_HORIZONTAL * math.sin(j0_rad)
|
||
j1_z = self.L0_VERTICAL
|
||
|
||
r1 = self.L1 * math.cos(j1_rad)
|
||
j2_x = r1 * math.cos(j0_rad)
|
||
j2_y = r1 * math.sin(j0_rad)
|
||
j2_z = self.L1 * math.sin(j1_rad)
|
||
|
||
r2 = self.L2 * math.cos(j2_rad)
|
||
j3_x = r2 * math.cos(j0_rad)
|
||
j3_y = r2 * math.sin(j0_rad)
|
||
j3_z = self.L2 * math.sin(j2_rad)
|
||
|
||
x = j1_x + j2_x + j3_x
|
||
y = j1_y + j2_y + j3_y
|
||
z = j1_z + j2_z + j3_z
|
||
|
||
a = joints[3] if len(joints) > 3 else 0.0
|
||
b = joints[4] if len(joints) > 4 else 0.0
|
||
|
||
return (x, y, z, a, b, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
|
||
j0 = math.atan2(y, x) * TO_DEG
|
||
|
||
r_j1 = self.L0_HORIZONTAL
|
||
z_j1 = self.L0_VERTICAL
|
||
|
||
r_cp = math.sqrt(x*x + y*y) - r_j1
|
||
z_cp = z - z_j1
|
||
|
||
dist_to_cp = math.sqrt(r_cp*r_cp + z_cp*z_cp)
|
||
dist_to_center = dist_to_cp / 2.0
|
||
|
||
angle_to_cp = math.acos(r_cp / dist_to_cp) * TO_DEG if dist_to_cp > 0 else 0
|
||
if z_cp < 0:
|
||
angle_to_cp = -angle_to_cp
|
||
|
||
if dist_to_center > self.L1:
|
||
dist_to_center = self.L1
|
||
|
||
j1_angle = math.acos(dist_to_center / self.L1) * TO_DEG
|
||
j1 = angle_to_cp + j1_angle
|
||
|
||
z_j2 = self.L1 * math.sin(j1 * TO_RAD)
|
||
j2 = -math.asin(max(-1, min(1, (z_j2 - z_cp) / self.L2))) * TO_DEG
|
||
|
||
j3 = world[3] if len(world) > 3 else 0.0
|
||
j4 = world[4] if len(world) > 4 else 0.0
|
||
|
||
return [j0, j1, j2, j3, j4, 0.0, 0.0, 0.0, 0.0]
|
||
|
||
|
||
@dataclass
|
||
class RoseParams(KinematicsParams):
|
||
"""Rose Engine 参数"""
|
||
pass
|
||
|
||
|
||
class RoseKinematics(BaseKinematics):
|
||
"""Rose Engine运动学 - 基于 rosekins.c"""
|
||
|
||
def __init__(self, params: RoseParams = None, debug: bool = False):
|
||
super().__init__(params or RoseParams())
|
||
self.debug = debug
|
||
self._revolutions = 0
|
||
self._old_quad = 0
|
||
|
||
def forward(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if len(joints) < 3:
|
||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
radius, z, theta = joints[0], joints[1], joints[2]
|
||
theta_rad = theta * TO_RAD
|
||
|
||
x = radius * math.cos(theta_rad)
|
||
y = radius * math.sin(theta_rad)
|
||
|
||
return (x, y, z, 0.0, 0.0, 0.0)
|
||
|
||
def inverse(self, world: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
x, y, z = world[0], world[1], world[2]
|
||
|
||
radius = math.hypot(x, y)
|
||
theta = math.atan2(y, x) * TO_DEG
|
||
|
||
if x >= 0 and y >= 0:
|
||
now_quad = 1
|
||
elif x < 0 and y >= 0:
|
||
now_quad = 2
|
||
elif x < 0 and y < 0:
|
||
now_quad = 3
|
||
else:
|
||
now_quad = 4
|
||
|
||
if self._old_quad == 2 and now_quad == 3:
|
||
self._revolutions += 1
|
||
elif self._old_quad == 3 and now_quad == 2:
|
||
self._revolutions -= 1
|
||
|
||
big_theta = theta + 360.0 * self._revolutions
|
||
self._old_quad = now_quad
|
||
|
||
if self.debug:
|
||
print(f"[ROSE] INV: world({x:.3f},{y:.3f},{z:.3f}) -> joints(R={radius:.3f},Z={z:.3f},Theta={big_theta:.3f})")
|
||
|
||
return [radius, z, big_theta, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||
|
||
|
||
# ==================== 运动学工厂类 ====================
|
||
|
||
class KinematicsFactory:
|
||
"""运动学工厂类,根据类型创建对应的运动学实例"""
|
||
|
||
_default_params = {
|
||
KinematicsType.TRT_AC: {
|
||
'rot_center_x': 0.0, 'rot_center_y': 0.0, 'rot_center_z': 0.0,
|
||
'axis_offset_x': 0.0, 'axis_offset_y': 0.0, 'axis_offset_z': 0.0,
|
||
'tool_length': 100.0, 'conventional_directions': False
|
||
},
|
||
KinematicsType.TRT_BC: {
|
||
'rot_center_x': 0.0, 'rot_center_y': 0.0, 'rot_center_z': 0.0,
|
||
'axis_offset_x': 0.0, 'axis_offset_y': 0.0, 'axis_offset_z': 0.0,
|
||
'tool_length': 100.0, 'conventional_directions': False
|
||
},
|
||
KinematicsType.MAXKINS_BC: {
|
||
'pivot_length': 250.0, 'tool_length': 100.0, 'conventional_directions': False
|
||
},
|
||
KinematicsType.FIVEAXIS_BC: {
|
||
'pivot_length': 250.0, 'tool_length': 100.0
|
||
},
|
||
KinematicsType.HEXAPOD: {
|
||
'tool_offset': 100.0, 'spindle_offset': 0.0,
|
||
'convergence_criterion': 1e-9, 'max_iterations': 120
|
||
},
|
||
KinematicsType.PUMA: {
|
||
'a2': 300.0, 'a3': 50.0, 'd3': 70.0, 'd4': 400.0, 'd6': 70.0
|
||
},
|
||
KinematicsType.SCARA: {
|
||
'd1': 490.0, 'd2': 340.0, 'd3': 50.0,
|
||
'd4': 250.0, 'd5': 50.0, 'd6': 50.0
|
||
},
|
||
KinematicsType.SCORBOT: {},
|
||
KinematicsType.LINEAR_DELTA: {
|
||
'radius': 165.25, 'rod_length': 269.0
|
||
},
|
||
KinematicsType.ROTARY_DELTA: {
|
||
'platform_radius': 10.0, 'thigh_length': 10.0,
|
||
'shin_length': 14.0, 'foot_radius': 6.0
|
||
},
|
||
KinematicsType.TRIPOD: {
|
||
'Bx': 1.0, 'Cx': 1.0, 'Cy': 1.0
|
||
},
|
||
KinematicsType.PENTAPOD: {},
|
||
KinematicsType.ROTATE: {},
|
||
KinematicsType.COREXY: {},
|
||
KinematicsType.ROSE: {},
|
||
}
|
||
|
||
@classmethod
|
||
def create(cls, kin_type: KinematicsType, debug: bool = False, **params) -> Optional[BaseKinematics]:
|
||
if kin_type == KinematicsType.IDENTITY:
|
||
return None
|
||
|
||
merged_params = cls._default_params.get(kin_type, {}).copy()
|
||
merged_params.update(params)
|
||
|
||
if kin_type == KinematicsType.TRT_AC:
|
||
p = TRTParams(**{k: v for k, v in merged_params.items() if hasattr(TRTParams, k)})
|
||
return XYZAC_TRTKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.TRT_BC:
|
||
p = TRTParams(**{k: v for k, v in merged_params.items() if hasattr(TRTParams, k)})
|
||
return XYZBC_TRTKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.MAXKINS_BC:
|
||
return MaxkinsBCKinematics(debug=debug, **merged_params)
|
||
elif kin_type == KinematicsType.FIVEAXIS_BC:
|
||
return FiveAxisBCKinematics(debug=debug, **merged_params)
|
||
elif kin_type == KinematicsType.HEXAPOD:
|
||
p = HexapodParams(**{k: v for k, v in merged_params.items() if hasattr(HexapodParams, k)})
|
||
return HexapodKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.PUMA:
|
||
p = PUMAParams(**{k: v for k, v in merged_params.items() if hasattr(PUMAParams, k)})
|
||
return PUMAKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.SCARA:
|
||
p = SCARAParams(**{k: v for k, v in merged_params.items() if hasattr(SCARAParams, k)})
|
||
return SCARAKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.SCORBOT:
|
||
return ScorbotKinematics(debug=debug)
|
||
elif kin_type == KinematicsType.LINEAR_DELTA:
|
||
return LinearDeltaKinematics(debug=debug, **merged_params)
|
||
elif kin_type == KinematicsType.ROTARY_DELTA:
|
||
p = RotaryDeltaParams(**{k: v for k, v in merged_params.items() if hasattr(RotaryDeltaParams, k)})
|
||
return RotaryDeltaKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.TRIPOD:
|
||
p = TripodParams(**{k: v for k, v in merged_params.items() if hasattr(TripodParams, k)})
|
||
return TripodKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.PENTAPOD:
|
||
p = PentapodParams(**{k: v for k, v in merged_params.items() if hasattr(PentapodParams, k)})
|
||
return PentapodKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.ROTATE:
|
||
return RotateKinematics(debug=debug)
|
||
elif kin_type == KinematicsType.COREXY:
|
||
return CoreXYKinematics(debug=debug)
|
||
elif kin_type == KinematicsType.ROSE:
|
||
p = RoseParams()
|
||
return RoseKinematics(p, debug=debug)
|
||
elif kin_type == KinematicsType.SERIAL_DH:
|
||
p = SerialDHParams(**{k: v for k, v in merged_params.items() if hasattr(SerialDHParams, k)})
|
||
return SerialDHKinematics(p, debug=debug)
|
||
|
||
return None
|
||
|
||
@classmethod
|
||
def get_supported_types(cls) -> List[KinematicsType]:
|
||
return list(cls._default_params.keys())
|
||
|
||
@classmethod
|
||
def get_type_info(cls, kin_type: KinematicsType) -> Dict[str, Any]:
|
||
info = {
|
||
KinematicsType.IDENTITY: {'name': 'IDENTITY', 'description': '三轴恒等运动学', 'joints': 3, 'source': 'trivkins.c'},
|
||
KinematicsType.TRT_AC: {'name': 'TRT_AC', 'description': 'XYZAC 双转台五轴', 'joints': 5, 'source': 'trtfuncs.c'},
|
||
KinematicsType.TRT_BC: {'name': 'TRT_BC', 'description': 'XYZBC 双转台五轴', 'joints': 5, 'source': 'trtfuncs.c'},
|
||
KinematicsType.MAXKINS_BC: {'name': 'MAXKINS_BC', 'description': 'Max 五轴铣床', 'joints': 9, 'source': 'maxkins.c'},
|
||
KinematicsType.FIVEAXIS_BC: {'name': 'FIVEAXIS_BC', 'description': '五轴桥式铣床', 'joints': 6, 'source': '5axiskins.c'},
|
||
KinematicsType.HEXAPOD: {'name': 'HEXAPOD', 'description': 'Stewart 六足平台', 'joints': 6, 'source': 'genhexkins.c'},
|
||
KinematicsType.PUMA: {'name': 'PUMA', 'description': 'PUMA 560 机器人', 'joints': 6, 'source': 'pumakins.c'},
|
||
KinematicsType.SCARA: {'name': 'SCARA', 'description': 'SCARA 机器人', 'joints': 4, 'source': 'scarakins.c'},
|
||
KinematicsType.SCORBOT: {'name': 'SCORBOT', 'description': 'Scorbot ER3', 'joints': 5, 'source': 'scorbot-kins.c'},
|
||
KinematicsType.LINEAR_DELTA: {'name': 'LINEAR_DELTA', 'description': '线性Delta机器人', 'joints': 3, 'source': 'lineardeltakins.c'},
|
||
KinematicsType.ROTARY_DELTA: {'name': 'ROTARY_DELTA', 'description': '旋转Delta机器人', 'joints': 3, 'source': 'rotarydeltakins.c'},
|
||
KinematicsType.TRIPOD: {'name': 'TRIPOD', 'description': 'Tripod机器人', 'joints': 3, 'source': 'tripodkins.c'},
|
||
KinematicsType.PENTAPOD: {'name': 'PENTAPOD', 'description': 'Pentapod机器人', 'joints': 5, 'source': 'pentakins.c'},
|
||
KinematicsType.ROTATE: {'name': 'ROTATE', 'description': '旋转工作台', 'joints': 9, 'source': 'rotatekins.c'},
|
||
KinematicsType.COREXY: {'name': 'COREXY', 'description': 'CoreXY运动学', 'joints': 9, 'source': 'corexykins.c'},
|
||
KinematicsType.ROSE: {'name': 'ROSE', 'description': 'Rose Engine', 'joints': 3, 'source': 'rosekins.c'},
|
||
}
|
||
return info.get(kin_type, {'name': 'UNKNOWN', 'description': '未知类型'})
|
||
|
||
|
||
# ==================== 五轴运动学统一接口 (向后兼容) ====================
|
||
@dataclass
|
||
class KinematicsParamsLegacy:
|
||
rot_center_x: float = 0.0
|
||
rot_center_y: float = 0.0
|
||
rot_center_z: float = 0.0
|
||
axis_offset_x: float = 0.0
|
||
axis_offset_y: float = 0.0
|
||
axis_offset_z: float = 0.0
|
||
pivot_length: float = 250.0
|
||
tool_length: float = 0.0
|
||
conventional_directions: bool = False
|
||
|
||
|
||
class FiveAxisKinematics:
|
||
def __init__(self, kin_type: KinematicsType = KinematicsType.TRT_BC, debug: bool = False):
|
||
self.kin_type = kin_type
|
||
self.debug = debug
|
||
self.rtcp_enabled = False
|
||
self.params = KinematicsParamsLegacy()
|
||
self._kinematics = KinematicsFactory.create(kin_type, debug=debug)
|
||
|
||
def set_params(self, **kwargs):
|
||
for key, value in kwargs.items():
|
||
if hasattr(self.params, key):
|
||
setattr(self.params, key, value)
|
||
if self._kinematics:
|
||
param_dict = {
|
||
'rot_center_x': self.params.rot_center_x,
|
||
'rot_center_y': self.params.rot_center_y,
|
||
'rot_center_z': self.params.rot_center_z,
|
||
'axis_offset_x': self.params.axis_offset_x,
|
||
'axis_offset_y': self.params.axis_offset_y,
|
||
'axis_offset_z': self.params.axis_offset_z,
|
||
'tool_length': self.params.tool_length,
|
||
'conventional_directions': self.params.conventional_directions,
|
||
'pivot_length': self.params.pivot_length,
|
||
}
|
||
self._kinematics.set_params(**param_dict)
|
||
|
||
def set_tool_length(self, length: float):
|
||
self.params.tool_length = length
|
||
if self._kinematics:
|
||
self._kinematics.set_tool_length(length)
|
||
|
||
def enable_rtcp(self, enable: bool = True):
|
||
self.rtcp_enabled = enable
|
||
if self._kinematics:
|
||
self._kinematics.enable_rtcp(enable)
|
||
|
||
def forward_transform(self, joints: List[float]) -> Tuple[float, float, float, float, float, float]:
|
||
if not self.rtcp_enabled or self._kinematics is None:
|
||
if len(joints) >= 6:
|
||
return (joints[0], joints[1], joints[2], joints[3], joints[4], joints[5])
|
||
return (joints[0], joints[1], joints[2], 0, 0, 0)
|
||
return self._kinematics.forward(joints)
|
||
|
||
def inverse_transform(self, pos: Tuple[float, float, float, float, float, float]) -> List[float]:
|
||
if not self.rtcp_enabled or self._kinematics is None:
|
||
return [pos[0], pos[1], pos[2], pos[3], pos[4], pos[5]]
|
||
return self._kinematics.inverse(pos)
|
||
|
||
|
||
# ==================== LineCodeWrapper ====================
|
||
class LineCodeWrapper:
|
||
"""行代码包装器"""
|
||
def __init__(self, seq_num: int, plane: int = 17):
|
||
self.sequence_number = seq_num
|
||
self.plane = plane
|
||
self.x = 0.0
|
||
self.y = 0.0
|
||
self.z = 0.0
|
||
self.a = 0.0
|
||
self.b = 0.0
|
||
self.c = 0.0
|
||
self.u = 0.0
|
||
self.v = 0.0
|
||
self.w = 0.0
|
||
self.i = 0.0
|
||
self.j = 0.0
|
||
self.k = 0.0
|
||
self.r = 0.0
|
||
self.f = 0.0
|
||
self.s = 0.0
|
||
self.t = 0
|
||
self.h = -1
|
||
self.d = -1
|
||
self.p = -1.0
|
||
self.q = -1.0
|
||
self.l = -1
|
||
self.comment = ""
|
||
|
||
|
||
@dataclass
|
||
class CNCRuntimeStatus:
|
||
"""CNC运行时状态"""
|
||
position_x: float = 0.0
|
||
position_y: float = 0.0
|
||
position_z: float = 0.0
|
||
position_a: float = 0.0
|
||
position_b: float = 0.0
|
||
position_c: float = 0.0
|
||
position_u: float = 0.0
|
||
position_v: float = 0.0
|
||
position_w: float = 0.0
|
||
|
||
world_x: float = 0.0
|
||
world_y: float = 0.0
|
||
world_z: float = 0.0
|
||
|
||
machine_x: float = 0.0
|
||
machine_y: float = 0.0
|
||
machine_z: float = 0.0
|
||
machine_a: float = 0.0
|
||
machine_b: float = 0.0
|
||
machine_c: float = 0.0
|
||
|
||
spindle_speed: float = 0.0
|
||
spindle_state: str = "OFF"
|
||
spindle_mode: int = 0
|
||
spindle_override_enabled: bool = True
|
||
|
||
coolant_flood: bool = False
|
||
coolant_mist: bool = False
|
||
coolant_state: str = "OFF"
|
||
|
||
current_tool: int = 0
|
||
selected_tool: int = 1
|
||
tool_length: float = 0.0
|
||
tool_diameter: float = 0.0
|
||
tool_offset: Point6D = field(default_factory=Point6D)
|
||
|
||
feedrate: float = 0.0
|
||
feed_override: int = 100
|
||
rapid_override: int = 100
|
||
spindle_override: int = 100
|
||
feed_override_enabled: bool = True
|
||
adaptive_feed_enabled: bool = False
|
||
feed_hold_enabled: bool = True
|
||
|
||
state: str = MachineState.OFF.value
|
||
current_line: int = 0
|
||
total_lines: int = 0
|
||
progress_percent: float = 0.0
|
||
|
||
coordinate_mode: str = "G90"
|
||
plane_mode: str = "G17"
|
||
unit_mode: str = "G21"
|
||
cutter_comp: str = "G40"
|
||
current_offset: str = "G54"
|
||
distance_mode: int = 90
|
||
feed_mode: int = 94
|
||
ijk_distance_mode: int = 91
|
||
retract_mode: int = 98
|
||
control_mode: int = 610
|
||
lathe_diameter_mode: bool = False
|
||
|
||
alarm_code: int = 0
|
||
alarm_message: str = ""
|
||
|
||
rtcp_enabled: bool = False
|
||
kinematics_type: str = "TRT_BC"
|
||
|
||
program_name: str = ""
|
||
path_length: float = 0.0
|
||
total_time: float = 0.0
|
||
path_segments: int = 0
|
||
|
||
g92_applied: bool = False
|
||
g92_x: float = 0.0
|
||
g92_y: float = 0.0
|
||
g92_z: float = 0.0
|
||
g92_a: float = 0.0 # ★ 添加
|
||
g92_b: float = 0.0 # ★ 添加
|
||
g92_c: float = 0.0 # ★ 添加
|
||
|
||
g5x_index: int = 1
|
||
g5x_offset_x: float = 0.0
|
||
g5x_offset_y: float = 0.0
|
||
g5x_offset_z: float = 0.0
|
||
g5x_offset_a: float = 0.0 # ★ 添加
|
||
g5x_offset_b: float = 0.0 # ★ 添加
|
||
g5x_offset_c: float = 0.0 # ★ 添加
|
||
|
||
work_offset_x: float = 0.0
|
||
work_offset_y: float = 0.0
|
||
work_offset_z: float = 0.0
|
||
|
||
rotation_xy: float = 0.0
|
||
|
||
motion_mode: int = -1
|
||
current_pocket: int = 0
|
||
selected_pocket: int = 0
|
||
|
||
probe_tripped: bool = False
|
||
probe_x: float = 0.0
|
||
probe_y: float = 0.0
|
||
probe_z: float = 0.0
|
||
|
||
block_delete: bool = False
|
||
optional_stop: bool = True
|
||
|
||
cycle_active: bool = False
|
||
cycle_type: int = 0
|
||
cycle_r: float = 0.0
|
||
cycle_z: float = 0.0
|
||
cycle_q: float = 0.0
|
||
cycle_l: int = 1
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""转换为字典"""
|
||
return {
|
||
'position': {'x': self.position_x, 'y': self.position_y, 'z': self.position_z,
|
||
'a': self.position_a, 'b': self.position_b, 'c': self.position_c},
|
||
'world_position': {'x': self.world_x, 'y': self.world_y, 'z': self.world_z},
|
||
'machine_position': {'x': self.machine_x, 'y': self.machine_y, 'z': self.machine_z},
|
||
'spindle': {'speed': self.spindle_speed, 'state': self.spindle_state},
|
||
'coolant': {'flood': self.coolant_flood, 'mist': self.coolant_mist},
|
||
'tool': {'current': self.current_tool, 'selected': self.selected_tool,
|
||
'length': self.tool_length, 'diameter': self.tool_diameter},
|
||
'feedrate': self.feedrate,
|
||
'overrides': {'feed': self.feed_override, 'rapid': self.rapid_override,
|
||
'spindle': self.spindle_override},
|
||
'state': self.state,
|
||
'line': self.current_line,
|
||
'progress': self.progress_percent,
|
||
'modes': {'coordinate': self.coordinate_mode, 'plane': self.plane_mode,
|
||
'units': self.unit_mode, 'cutter_comp': self.cutter_comp,
|
||
'offset': self.current_offset},
|
||
'rtcp_enabled': self.rtcp_enabled,
|
||
'kinematics_type': self.kinematics_type,
|
||
'program_name': self.program_name,
|
||
}
|
||
|
||
|
||
# ==================== Translated 类(处理坐标系变换) ====================
|
||
class Translated:
|
||
"""处理 G5x 和 G92 坐标系偏移以及 XY 旋转"""
|
||
|
||
def __init__(self):
|
||
self.g92_offset_x = 0.0
|
||
self.g92_offset_y = 0.0
|
||
self.g92_offset_z = 0.0
|
||
self.g92_offset_a = 0.0
|
||
self.g92_offset_b = 0.0
|
||
self.g92_offset_c = 0.0
|
||
self.g92_offset_u = 0.0
|
||
self.g92_offset_v = 0.0
|
||
self.g92_offset_w = 0.0
|
||
|
||
self.g5x_offset_x = 0.0
|
||
self.g5x_offset_y = 0.0
|
||
self.g5x_offset_z = 0.0
|
||
self.g5x_offset_a = 0.0
|
||
self.g5x_offset_b = 0.0
|
||
self.g5x_offset_c = 0.0
|
||
self.g5x_offset_u = 0.0
|
||
self.g5x_offset_v = 0.0
|
||
self.g5x_offset_w = 0.0
|
||
|
||
self.g52_offset_x = 0.0
|
||
self.g52_offset_y = 0.0
|
||
self.g52_offset_z = 0.0
|
||
self.g52_offset_a = 0.0
|
||
self.g52_offset_b = 0.0
|
||
self.g52_offset_c = 0.0
|
||
self.g52_offset_u = 0.0
|
||
self.g52_offset_v = 0.0
|
||
self.g52_offset_w = 0.0
|
||
|
||
self.rotation_xy = 0.0
|
||
self.rotation_sin = 0.0
|
||
self.rotation_cos = 1.0
|
||
|
||
self.g5x_index = 1
|
||
|
||
self._g53_active = False
|
||
|
||
def rotate_and_translate(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float) -> Tuple[float, ...]:
|
||
"""应用 G5x、G92、G52 偏移和 XY 旋转"""
|
||
if self._g53_active:
|
||
# G53 模式下不应用任何偏移
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
# 应用 G92 偏移
|
||
x += self.g92_offset_x
|
||
y += self.g92_offset_y
|
||
z += self.g92_offset_z
|
||
a += self.g92_offset_a
|
||
b += self.g92_offset_b
|
||
c += self.g92_offset_c
|
||
u += self.g92_offset_u
|
||
v += self.g92_offset_v
|
||
w += self.g92_offset_w
|
||
|
||
# 应用 G52 偏移
|
||
x += self.g52_offset_x
|
||
y += self.g52_offset_y
|
||
z += self.g52_offset_z
|
||
a += self.g52_offset_a
|
||
b += self.g52_offset_b
|
||
c += self.g52_offset_c
|
||
|
||
# XY 旋转
|
||
if self.rotation_xy != 0:
|
||
rotx = x * self.rotation_cos - y * self.rotation_sin
|
||
roty = x * self.rotation_sin + y * self.rotation_cos
|
||
x, y = rotx, roty
|
||
|
||
# 应用 G5x 偏移
|
||
x += self.g5x_offset_x
|
||
y += self.g5x_offset_y
|
||
z += self.g5x_offset_z
|
||
a += self.g5x_offset_a
|
||
b += self.g5x_offset_b
|
||
c += self.g5x_offset_c
|
||
u += self.g5x_offset_u
|
||
v += self.g5x_offset_v
|
||
w += self.g5x_offset_w
|
||
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
def reverse_rotate_and_translate(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float) -> Tuple[float, ...]:
|
||
"""反向应用偏移(用于从机器坐标计算工件坐标)"""
|
||
if self._g53_active:
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
# 减去 G5x 偏移
|
||
x -= self.g5x_offset_x
|
||
y -= self.g5x_offset_y
|
||
z -= self.g5x_offset_z
|
||
a -= self.g5x_offset_a
|
||
b -= self.g5x_offset_b
|
||
c -= self.g5x_offset_c
|
||
u -= self.g5x_offset_u
|
||
v -= self.g5x_offset_v
|
||
w -= self.g5x_offset_w
|
||
|
||
# 反向 XY 旋转
|
||
if self.rotation_xy != 0:
|
||
rotx = x * self.rotation_cos + y * self.rotation_sin
|
||
roty = -x * self.rotation_sin + y * self.rotation_cos
|
||
x, y = rotx, roty
|
||
|
||
# 减去 G52 偏移
|
||
x -= self.g52_offset_x
|
||
y -= self.g52_offset_y
|
||
z -= self.g52_offset_z
|
||
a -= self.g52_offset_a
|
||
b -= self.g52_offset_b
|
||
c -= self.g52_offset_c
|
||
|
||
# 减去 G92 偏移
|
||
x -= self.g92_offset_x
|
||
y -= self.g92_offset_y
|
||
z -= self.g92_offset_z
|
||
a -= self.g92_offset_a
|
||
b -= self.g92_offset_b
|
||
c -= self.g92_offset_c
|
||
u -= self.g92_offset_u
|
||
v -= self.g92_offset_v
|
||
w -= self.g92_offset_w
|
||
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
def set_g5x_offset(self, csys: int, x: float = None, y: float = None, z: float = None,
|
||
a: float = 0.0, b: float = 0.0, c: float = 0.0,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""
|
||
G10 L2: 直接设置坐标系偏移
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::convert_setup (G10 L2 分支)
|
||
|
||
Args:
|
||
csys: 坐标系编号 (1-9)
|
||
x, y, z, a, b, c, u, v, w: 新的偏移值(程序单位)
|
||
"""
|
||
if csys < 1 or csys > 9:
|
||
if self.debug:
|
||
print(f"[G10 L2] 错误: 坐标系编号 {csys} 超出范围 (1-9)")
|
||
return
|
||
|
||
if csys not in self.work_offsets:
|
||
self.work_offsets[csys] = {}
|
||
|
||
# 获取旧偏移
|
||
old_offsets = self.work_offsets[csys].copy()
|
||
old_g5x_x = old_offsets.get('X', 0.0)
|
||
old_g5x_y = old_offsets.get('Y', 0.0)
|
||
old_g5x_z = old_offsets.get('Z', 0.0)
|
||
old_g5x_a = old_offsets.get('A', 0.0)
|
||
old_g5x_b = old_offsets.get('B', 0.0)
|
||
old_g5x_c = old_offsets.get('C', 0.0)
|
||
|
||
# 使用新值或保留旧值
|
||
new_x = x if x is not None else old_g5x_x
|
||
new_y = y if y is not None else old_g5x_y
|
||
new_z = z if z is not None else old_g5x_z
|
||
new_a = a if a is not None else old_g5x_a
|
||
new_b = b if b is not None else old_g5x_b
|
||
new_c = c if c is not None else old_g5x_c
|
||
new_u = u if u is not None else old_offsets.get('U', 0.0)
|
||
new_v = v if v is not None else old_offsets.get('V', 0.0)
|
||
new_w = w if w is not None else old_offsets.get('W', 0.0)
|
||
new_r = old_offsets.get('R', 0.0) # G10 L2 不改变旋转角度
|
||
|
||
if self.debug:
|
||
print(f"[G10 L2] ========== 设置坐标系 G{53 + csys} ==========")
|
||
print(f"[G10 L2] 旧偏移: X={old_g5x_x:.3f}, Y={old_g5x_y:.3f}, Z={old_g5x_z:.3f}")
|
||
print(f"[G10 L2] 新偏移: X={new_x:.3f}, Y={new_y:.3f}, Z={new_z:.3f}")
|
||
print(f"[G10 L2] 是否为当前坐标系: {csys == self.g5x_index}")
|
||
|
||
# ===== 如果是当前坐标系,需要调整当前位置 =====
|
||
if csys == self.g5x_index:
|
||
# 第1步:正向旋转当前坐标(去掉旧 XY 旋转)
|
||
if self.rotation_xy != 0:
|
||
rot_rad = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
rot_x = self.current_pos.x * cos_r - self.current_pos.y * sin_r
|
||
rot_y = self.current_pos.x * sin_r + self.current_pos.y * cos_r
|
||
self.current_pos.x = rot_x
|
||
self.current_pos.y = rot_y
|
||
|
||
# 第2步:加上旧 G5x 偏移
|
||
self.current_pos.x += old_g5x_x
|
||
self.current_pos.y += old_g5x_y
|
||
self.current_pos.z += old_g5x_z
|
||
self.current_pos.a += old_g5x_a
|
||
self.current_pos.b += old_g5x_b
|
||
self.current_pos.c += old_g5x_c
|
||
|
||
# 第3步:设置新 G5x 偏移
|
||
self.g5x_offset_x = new_x
|
||
self.g5x_offset_y = new_y
|
||
self.g5x_offset_z = new_z
|
||
self.g5x_offset_a = new_a
|
||
self.g5x_offset_b = new_b
|
||
self.g5x_offset_c = new_c
|
||
|
||
# 第4步:减去新 G5x 偏移
|
||
self.current_pos.x -= new_x
|
||
self.current_pos.y -= new_y
|
||
self.current_pos.z -= new_z
|
||
self.current_pos.a -= new_a
|
||
self.current_pos.b -= new_b
|
||
self.current_pos.c -= new_c
|
||
|
||
if self.debug:
|
||
print(f"[G10 L2] 调整后工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
|
||
# ===== 存储新偏移 =====
|
||
self.work_offsets[csys] = {
|
||
'X': new_x, 'Y': new_y, 'Z': new_z,
|
||
'A': new_a, 'B': new_b, 'C': new_c,
|
||
'U': new_u, 'V': new_v, 'W': new_w,
|
||
'R': new_r
|
||
}
|
||
|
||
# ===== 同步到参数表和机器坐标 =====
|
||
if hasattr(self, 'var_manager') and self.var_manager:
|
||
self.var_manager.set_csys_origin(csys, new_x, new_y, new_z, new_a, new_b, new_c)
|
||
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
if self.debug:
|
||
print(f"[G10 L2] ========== G10 L2 完成 ==========")
|
||
|
||
def set_g92_offset(self, x: float = 0.0, y: float = 0.0, z: float = 0.0,
|
||
a: float = 0.0, b: float = 0.0, c: float = 0.0,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""G92: 设置临时坐标系偏移"""
|
||
self.g92_offset_x = x
|
||
self.g92_offset_y = y
|
||
self.g92_offset_z = z
|
||
self.g92_offset_a = a
|
||
self.g92_offset_b = b
|
||
self.g92_offset_c = c
|
||
self.g92_offset_u = u
|
||
self.g92_offset_v = v
|
||
self.g92_offset_w = w
|
||
|
||
def apply_g92_from_current(self, target_x: float = None, target_y: float = None,
|
||
target_z: float = None, current_x: float = 0.0,
|
||
current_y: float = 0.0, current_z: float = 0.0):
|
||
"""根据当前坐标计算并应用 G92 偏移"""
|
||
if target_x is not None:
|
||
self.g92_offset_x = current_x + self.g92_offset_x - target_x
|
||
if target_y is not None:
|
||
self.g92_offset_y = current_y + self.g92_offset_y - target_y
|
||
if target_z is not None:
|
||
self.g92_offset_z = current_z + self.g92_offset_z - target_z
|
||
|
||
def clear_g92_offset(self):
|
||
"""G92.1: 清除 G92 偏移"""
|
||
self.g92_offset_x = 0.0
|
||
self.g92_offset_y = 0.0
|
||
self.g92_offset_z = 0.0
|
||
self.g92_offset_a = 0.0
|
||
self.g92_offset_b = 0.0
|
||
self.g92_offset_c = 0.0
|
||
self.g92_offset_u = 0.0
|
||
self.g92_offset_v = 0.0
|
||
self.g92_offset_w = 0.0
|
||
|
||
def set_g52_offset(self, x: float = 0.0, y: float = 0.0, z: float = 0.0,
|
||
a: float = 0.0, b: float = 0.0, c: float = 0.0):
|
||
"""G52: 设置临时工件坐标系偏移"""
|
||
self.g52_offset_x = x
|
||
self.g52_offset_y = y
|
||
self.g52_offset_z = z
|
||
self.g52_offset_a = a
|
||
self.g52_offset_b = b
|
||
self.g52_offset_c = c
|
||
|
||
def clear_g52_offset(self):
|
||
"""清除 G52 偏移"""
|
||
self.g52_offset_x = 0.0
|
||
self.g52_offset_y = 0.0
|
||
self.g52_offset_z = 0.0
|
||
self.g52_offset_a = 0.0
|
||
self.g52_offset_b = 0.0
|
||
self.g52_offset_c = 0.0
|
||
|
||
def set_xy_rotation(self, theta: float):
|
||
"""设置 XY 旋转角度"""
|
||
self.rotation_xy = theta
|
||
t = math.radians(theta)
|
||
self.rotation_sin = math.sin(t)
|
||
self.rotation_cos = math.cos(t)
|
||
|
||
# ========== 新增:同步到参数表 ==========
|
||
# 如果有 var_manager 属性则同步
|
||
if hasattr(self, 'var_manager') and self.var_manager is not None:
|
||
self.var_manager.set_xy_rotation(theta)
|
||
|
||
def set_g53_active(self, active: bool = True):
|
||
"""设置 G53 模式"""
|
||
self._g53_active = active
|
||
|
||
|
||
# ==================== ArcsToSegmentsMixin 类 ====================
|
||
class ArcsToSegmentsMixin:
|
||
"""圆弧转线段的混入类"""
|
||
|
||
plane = 17
|
||
arcdivision = 64
|
||
|
||
def set_plane(self, plane: int):
|
||
self.plane = plane
|
||
|
||
def set_arc_division(self, division: int):
|
||
self.arcdivision = division
|
||
|
||
def arc_feed_to_segments(self, x1: float, y1: float, cx: float, cy: float,
|
||
rot: int, z1: float, a: float, b: float, c: float,
|
||
u: float, v: float, w: float):
|
||
"""将圆弧转换为线段"""
|
||
segs = arcs_to_segments_python(
|
||
self, x1, y1, cx, cy, rot, z1, a, b, c, u, v, w, self.arcdivision
|
||
)
|
||
self.straight_arcsegments(segs)
|
||
|
||
def straight_arcsegments(self, segs: List[Tuple[float, ...]]):
|
||
"""处理圆弧线段(由子类实现)"""
|
||
raise NotImplementedError
|
||
|
||
|
||
# ==================== 纯 Python GLCanon 类 ====================
|
||
class GLCanonPure(Translated, ArcsToSegmentsMixin):
|
||
"""纯 Python 实现的 GLCanon 类"""
|
||
|
||
def __init__(self, colors=None, geometry="XYZ", is_foam=0):
|
||
super().__init__()
|
||
|
||
if colors is None:
|
||
colors = DEFAULT_COLORS.copy()
|
||
|
||
self.colors = colors
|
||
self.geometry = geometry
|
||
self.is_foam = is_foam
|
||
|
||
self.traverse: List[Tuple] = []
|
||
self.feed: List[Tuple] = []
|
||
self.arcfeed: List[Tuple] = []
|
||
self.dwells: List[Tuple] = []
|
||
self.tool_list: List[int] = []
|
||
|
||
self.preview_zero_rxy: List[Tuple] = []
|
||
|
||
self.lineno = -1
|
||
self.first_move = True
|
||
self.feedrate = 1.0
|
||
self.lo = [0.0] * 9
|
||
self.xo = self.yo = self.zo = self.ao = self.bo = self.co = self.uo = self.vo = self.wo = 0.0
|
||
|
||
self.min_extents = [9e99, 9e99, 9e99]
|
||
self.max_extents = [-9e99, -9e99, -9e99]
|
||
self.min_extents_notool = [9e99, 9e99, 9e99]
|
||
self.max_extents_notool = [-9e99, -9e99, -9e99]
|
||
self.min_extents_zero_rxy = [9e99, 9e99, 9e99]
|
||
self.max_extents_zero_rxy = [-9e99, -9e99, -9e99]
|
||
self.min_extents_notool_zero_rxy = [9e99, 9e99, 9e99]
|
||
self.max_extents_notool_zero_rxy = [-9e99, -9e99, -9e99]
|
||
|
||
self.foam_z = 0.0
|
||
self.foam_w = 1.5
|
||
|
||
self.in_arc = 0
|
||
self.suppress = 0
|
||
self.dwell_time = 0.0
|
||
self.notify = 0
|
||
self.notify_message = ""
|
||
self.highlight_line = None
|
||
|
||
self.state = type('State', (), {})()
|
||
self.state.plane = 17
|
||
self.state.feedrate = 1.0
|
||
self.state.spindle_speed = 0.0
|
||
self.state.spindle_mode = 0
|
||
self.state.tool = 0
|
||
self.state.units = 21
|
||
|
||
self.g92_offset_u = 0.0
|
||
self.g92_offset_v = 0.0
|
||
self.g92_offset_w = 0.0
|
||
|
||
self.g5x_offset_u = 0.0
|
||
self.g5x_offset_v = 0.0
|
||
self.g5x_offset_w = 0.0
|
||
|
||
def comment(self, arg: str):
|
||
"""处理注释"""
|
||
if arg.startswith("AXIS,") or arg.startswith("PREVIEW,"):
|
||
parts = arg.split(",")
|
||
command = parts[1] if len(parts) > 1 else ""
|
||
if command == "stop":
|
||
raise KeyboardInterrupt("Preview stop requested")
|
||
elif command == "hide":
|
||
self.suppress += 1
|
||
elif command == "show":
|
||
self.suppress -= 1
|
||
elif command == "XY_Z_POS" and len(parts) > 2:
|
||
try:
|
||
self.foam_z = float(parts[2])
|
||
except ValueError:
|
||
pass
|
||
elif command == "UV_Z_POS" and len(parts) > 2:
|
||
try:
|
||
self.foam_w = float(parts[2])
|
||
except ValueError:
|
||
pass
|
||
elif command == "notify":
|
||
self.notify += 1
|
||
self.notify_message = "(AXIS,notify):" + str(self.notify)
|
||
if len(parts) > 2 and parts[2]:
|
||
self.notify_message = parts[2]
|
||
|
||
def message(self, message: str):
|
||
"""处理消息"""
|
||
pass
|
||
|
||
def check_abort(self):
|
||
"""检查是否中止"""
|
||
pass
|
||
|
||
def next_line(self, st: LineCodeWrapper):
|
||
"""处理下一行"""
|
||
self.state = st
|
||
self.lineno = getattr(st, 'sequence_number', -1)
|
||
|
||
def calc_extents(self):
|
||
"""计算包围盒"""
|
||
if not self.arcfeed and not self.feed and not self.traverse:
|
||
self.min_extents = self.max_extents = [0, 0, 0]
|
||
self.min_extents_notool = self.max_extents_notool = [0, 0, 0]
|
||
self.min_extents_zero_rxy = self.max_extents_zero_rxy = [0, 0, 0]
|
||
self.min_extents_notool_zero_rxy = self.max_extents_notool_zero_rxy = [0, 0, 0]
|
||
return
|
||
|
||
(min_p, max_p, min_pt, max_pt) = calc_extents_python(self.arcfeed, self.feed, self.traverse)
|
||
|
||
self.min_extents = min_p
|
||
self.max_extents = max_p
|
||
self.min_extents_notool = min_pt
|
||
self.max_extents_notool = max_pt
|
||
|
||
self.unrotate_preview()
|
||
(min_pz, max_pz, min_ptz, max_ptz) = calc_extents_python(self.preview_zero_rxy)
|
||
|
||
self.min_extents_zero_rxy = min_pz
|
||
self.max_extents_zero_rxy = max_pz
|
||
self.min_extents_notool_zero_rxy = min_ptz
|
||
self.max_extents_notool_zero_rxy = max_ptz
|
||
|
||
if self.is_foam:
|
||
min_z = min(self.foam_z, self.foam_w)
|
||
max_z = max(self.foam_z, self.foam_w)
|
||
self.min_extents[2] = min_z
|
||
self.max_extents[2] = max_z
|
||
self.min_extents_notool[2] = min_z
|
||
self.max_extents_notool[2] = max_z
|
||
|
||
def unrotate_preview(self):
|
||
"""反向旋转预览数据"""
|
||
angle = math.radians(-self.rotation_xy)
|
||
cos = math.cos(angle)
|
||
sin = math.sin(angle)
|
||
g5x_x = self.g5x_offset_x
|
||
g5x_y = self.g5x_offset_y
|
||
|
||
self.preview_zero_rxy = []
|
||
|
||
for lst in [self.feed, self.arcfeed]:
|
||
for item in lst:
|
||
if len(item) == 5:
|
||
linenum, start, end, feed, tooloffset = item
|
||
else:
|
||
continue
|
||
|
||
tsx = start[0] - g5x_x
|
||
tsy = start[1] - g5x_y
|
||
tex = end[0] - g5x_x
|
||
tey = end[1] - g5x_y
|
||
|
||
rsx = tsx * cos - tsy * sin + g5x_x
|
||
rsy = tsx * sin + tsy * cos + g5x_y
|
||
rex = tex * cos - tey * sin + g5x_x
|
||
rey = tex * sin + tey * cos + g5x_y
|
||
|
||
self.preview_zero_rxy.append((
|
||
linenum, (rsx, rsy) + start[2:], (rex, rey) + end[2:], feed, tooloffset
|
||
))
|
||
|
||
for item in self.traverse:
|
||
if len(item) == 4:
|
||
linenum, start, end, tooloffset = item
|
||
else:
|
||
continue
|
||
|
||
tsx = start[0] - g5x_x
|
||
tsy = start[1] - g5x_y
|
||
tex = end[0] - g5x_x
|
||
tey = end[1] - g5x_y
|
||
|
||
rsx = tsx * cos - tsy * sin + g5x_x
|
||
rsy = tsx * sin + tsy * cos + g5x_y
|
||
rex = tex * cos - tey * sin + g5x_x
|
||
rey = tex * sin + tey * cos + g5x_y
|
||
|
||
self.preview_zero_rxy.append((
|
||
linenum, (rsx, rsy) + start[2:], (rex, rey) + end[2:], tooloffset
|
||
))
|
||
|
||
def tool_offset(self, xo: float, yo: float, zo: float,
|
||
ao: float, bo: float, co: float,
|
||
uo: float, vo: float, wo: float):
|
||
"""设置刀具偏移"""
|
||
self.first_move = True
|
||
x, y, z, a, b, c, u, v, w = self.lo
|
||
self.lo = (
|
||
x - xo + self.xo, y - yo + self.yo, z - zo + self.zo,
|
||
a - ao + self.ao, b - bo + self.bo, c - co + self.co,
|
||
u - uo + self.uo, v - vo + self.vo, w - wo + self.wo
|
||
)
|
||
self.xo, self.yo, self.zo = xo, yo, zo
|
||
self.ao, self.bo, self.co = ao, bo, co
|
||
self.uo, self.vo, self.wo = uo, vo, wo
|
||
|
||
def set_spindle_rate(self, arg: float):
|
||
"""设置主轴转速"""
|
||
pass
|
||
|
||
def set_feed_rate(self, arg: float):
|
||
"""设置进给率"""
|
||
self.feedrate = arg / 60.0
|
||
|
||
def select_plane(self, plane: int):
|
||
"""选择平面"""
|
||
pass
|
||
|
||
def change_tool(self, arg: int):
|
||
"""换刀"""
|
||
self.first_move = True
|
||
try:
|
||
self.tool_list.append(arg)
|
||
except Exception as e:
|
||
print(f"Error in change_tool: {e}")
|
||
|
||
def straight_traverse(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float):
|
||
"""快速移动"""
|
||
if self.suppress > 0:
|
||
return
|
||
l = self.rotate_and_translate(x, y, z, a, b, c, u, v, w)
|
||
if not self.first_move:
|
||
self.traverse.append((self.lineno, self.lo, l, (self.xo, self.yo, self.zo)))
|
||
self.lo = l
|
||
self.first_move = False
|
||
|
||
def straight_feed(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float):
|
||
"""直线进给"""
|
||
if self.suppress > 0:
|
||
return
|
||
self.first_move = False
|
||
l = self.rotate_and_translate(x, y, z, a, b, c, u, v, w)
|
||
self.feed.append((self.lineno, self.lo, l, self.feedrate, (self.xo, self.yo, self.zo)))
|
||
self.lo = l
|
||
|
||
straight_probe = straight_feed
|
||
|
||
def arc_feed(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
i: float, j: float, k: float, turn: int, feed: float,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""圆弧进给"""
|
||
if self.suppress > 0:
|
||
return
|
||
self.first_move = False
|
||
self.in_arc = True
|
||
try:
|
||
self.arc_feed_to_segments(x, y, i, j, turn, z, a, b, c, u, v, w)
|
||
finally:
|
||
self.in_arc = False
|
||
|
||
def straight_arcsegments(self, segs: List[Tuple[float, ...]]):
|
||
"""处理圆弧线段"""
|
||
self.first_move = False
|
||
lo = self.lo
|
||
lineno = self.lineno
|
||
feedrate = self.feedrate
|
||
to = (self.xo, self.yo, self.zo)
|
||
|
||
for l in segs:
|
||
self.arcfeed.append((lineno, lo, l, feedrate, to))
|
||
lo = l
|
||
self.lo = lo
|
||
|
||
def rigid_tap(self, x: float, y: float, z: float):
|
||
"""刚性攻丝"""
|
||
if self.suppress > 0:
|
||
return
|
||
self.first_move = False
|
||
l = self.rotate_and_translate(x, y, z, 0, 0, 0, 0, 0, 0)[:3]
|
||
l += (self.lo[3], self.lo[4], self.lo[5], self.lo[6], self.lo[7], self.lo[8])
|
||
self.feed.append((self.lineno, self.lo, l, self.feedrate, (self.xo, self.yo, self.zo)))
|
||
self.feed.append((self.lineno, l, self.lo, self.feedrate, (self.xo, self.yo, self.zo)))
|
||
|
||
def user_defined_function(self, i: int, p: float, q: float):
|
||
"""用户自定义函数"""
|
||
if self.suppress > 0:
|
||
return
|
||
color = self.colors.get('m1xx', '#ff8000')
|
||
plane = int(getattr(self.state, 'plane', 17) / 10 - 17)
|
||
self.dwells.append((self.lineno, color, self.lo[0], self.lo[1], self.lo[2], plane))
|
||
|
||
def dwell(self, arg: float):
|
||
"""暂停"""
|
||
if self.suppress > 0:
|
||
return
|
||
self.dwell_time += arg
|
||
color = self.colors.get('dwell', '#ffffff')
|
||
plane = int(getattr(self.state, 'plane', 17) / 10 - 17)
|
||
self.dwells.append((self.lineno, color, self.lo[0], self.lo[1], self.lo[2], plane))
|
||
|
||
def get_extents(self) -> Dict:
|
||
"""获取包围盒"""
|
||
self.calc_extents()
|
||
return {
|
||
'min': self.min_extents,
|
||
'max': self.max_extents,
|
||
'min_notool': self.min_extents_notool,
|
||
'max_notool': self.max_extents_notool,
|
||
'min_zero_rxy': self.min_extents_zero_rxy,
|
||
'max_zero_rxy': self.max_extents_zero_rxy,
|
||
}
|
||
|
||
def get_traverse(self) -> List[Tuple]:
|
||
return self.traverse
|
||
|
||
def get_feed(self) -> List[Tuple]:
|
||
return self.feed
|
||
|
||
def get_arcfeed(self) -> List[Tuple]:
|
||
return self.arcfeed
|
||
|
||
def get_dwells(self) -> List[Tuple]:
|
||
return self.dwells
|
||
|
||
|
||
# ==================== 梯形速度曲线运动规划器 ====================
|
||
|
||
# ==================== 梯形速度曲线运动规划器(完整版)====================
|
||
|
||
class MotionProfile:
|
||
"""速度曲线规划结果"""
|
||
def __init__(self):
|
||
self.duration: float = 0.0 # 总时间 (s)
|
||
self.v_max: float = 0.0 # 最大速度 (mm/s)
|
||
self.t_acc: float = 0.0 # 加速时间 (s)
|
||
self.t_const: float = 0.0 # 匀速时间 (s)
|
||
self.t_dec: float = 0.0 # 减速时间 (s)
|
||
self.s_acc: float = 0.0 # 加速距离 (mm)
|
||
self.s_const: float = 0.0 # 匀速距离 (mm)
|
||
self.s_dec: float = 0.0 # 减速距离 (mm)
|
||
self.total_distance: float = 0.0 # 总距离 (mm)
|
||
self.profile_type: str = "zero" # zero / trapezoidal / triangular
|
||
|
||
|
||
class TrapezoidalMotionPlanner:
|
||
"""梯形速度曲线运动规划器 - 支持实时位置查询"""
|
||
|
||
def __init__(self,
|
||
acceleration: float = 500.0,
|
||
deceleration: float = 500.0,
|
||
max_rapid_rate: float = 10000.0,
|
||
max_feed_rate: float = 5000.0,
|
||
rapid_override: float = 1.0,
|
||
feed_override: float = 1.0):
|
||
|
||
self.acc = acceleration # mm/s²
|
||
self.dec = deceleration # mm/s²
|
||
self.max_rapid = max_rapid_rate / 60.0 # mm/s
|
||
self.max_feed = max_feed_rate / 60.0
|
||
self.rapid_override = rapid_override
|
||
self.feed_override = feed_override
|
||
self._jerk = 10000.0 # 加加速度 (mm/s³) - 预留
|
||
|
||
def plan_motion(self, distance: float, commanded_feedrate: float = None,
|
||
is_rapid: bool = False) -> MotionProfile:
|
||
"""
|
||
规划运动速度曲线
|
||
|
||
参数:
|
||
distance: 运动距离 (mm)
|
||
commanded_feedrate: 编程进给率 (mm/min)
|
||
is_rapid: 是否为G0快速移动
|
||
|
||
返回: MotionProfile 包含完整速度曲线参数
|
||
"""
|
||
profile = MotionProfile()
|
||
profile.total_distance = distance
|
||
|
||
if distance < CART_FUZZ:
|
||
profile.profile_type = "zero"
|
||
return profile
|
||
|
||
# 确定目标速度 (mm/s)
|
||
if is_rapid:
|
||
v_target = self.max_rapid * self.rapid_override
|
||
else:
|
||
if commanded_feedrate is not None and commanded_feedrate > 0:
|
||
v_target = (commanded_feedrate / 60.0) * self.feed_override
|
||
else:
|
||
v_target = self.max_feed * self.feed_override
|
||
|
||
max_allowed = self.max_rapid if is_rapid else self.max_feed
|
||
v_target = min(v_target, max_allowed)
|
||
v_target = max(v_target, 0.001)
|
||
|
||
# 计算加速距离和减速距离
|
||
s_acc = (v_target * v_target) / (2.0 * self.acc)
|
||
s_dec = (v_target * v_target) / (2.0 * self.dec)
|
||
s_cruise = distance - s_acc - s_dec
|
||
|
||
if s_cruise >= 0:
|
||
# ===== 梯形速度曲线 =====
|
||
profile.profile_type = "trapezoidal"
|
||
profile.t_acc = v_target / self.acc
|
||
profile.t_dec = v_target / self.dec
|
||
profile.t_const = s_cruise / v_target
|
||
profile.v_max = v_target
|
||
profile.s_acc = s_acc
|
||
profile.s_const = s_cruise
|
||
profile.s_dec = s_dec
|
||
profile.duration = profile.t_acc + profile.t_const + profile.t_dec
|
||
else:
|
||
# ===== 三角形速度曲线 =====
|
||
# v_peak = sqrt(2 * s * a1 * a2 / (a1 + a2))
|
||
profile.profile_type = "triangular"
|
||
v_peak = math.sqrt(
|
||
2.0 * distance * self.acc * self.dec / (self.acc + self.dec)
|
||
)
|
||
profile.v_max = v_peak
|
||
profile.t_acc = v_peak / self.acc
|
||
profile.t_dec = v_peak / self.dec
|
||
profile.t_const = 0.0
|
||
profile.s_acc = 0.5 * self.acc * profile.t_acc * profile.t_acc
|
||
profile.s_const = 0.0
|
||
profile.s_dec = 0.5 * self.dec * profile.t_dec * profile.t_dec
|
||
profile.duration = profile.t_acc + profile.t_dec
|
||
|
||
return profile
|
||
|
||
def get_position_at_time(self, elapsed: float, profile: MotionProfile) -> float:
|
||
"""
|
||
根据速度曲线获取当前位置 (mm)
|
||
|
||
参数:
|
||
elapsed: 已用时间 (s)
|
||
profile: 速度曲线
|
||
|
||
返回: 当前位置 (mm)
|
||
"""
|
||
if elapsed <= 0:
|
||
return 0.0
|
||
if elapsed >= profile.duration:
|
||
return profile.total_distance
|
||
|
||
if profile.profile_type == "zero":
|
||
return 0.0
|
||
|
||
elif profile.profile_type == "trapezoidal":
|
||
if elapsed <= profile.t_acc:
|
||
# 加速段: s = 1/2 * a * t²
|
||
return 0.5 * self.acc * elapsed * elapsed
|
||
elif elapsed <= profile.t_acc + profile.t_const:
|
||
# 匀速段: s = s_acc + v * (t - t_acc)
|
||
return profile.s_acc + profile.v_max * (elapsed - profile.t_acc)
|
||
else:
|
||
# 减速段: s = s_acc + s_const + v*(Δt) - 1/2*dec*(Δt)²
|
||
dt = elapsed - profile.t_acc - profile.t_const
|
||
s = (profile.s_acc + profile.s_const +
|
||
profile.v_max * dt - 0.5 * self.dec * dt * dt)
|
||
return min(s, profile.total_distance)
|
||
|
||
elif profile.profile_type == "triangular":
|
||
if elapsed <= profile.t_acc:
|
||
return 0.5 * self.acc * elapsed * elapsed
|
||
else:
|
||
dt = elapsed - profile.t_acc
|
||
s = (profile.s_acc +
|
||
profile.v_max * dt - 0.5 * self.dec * dt * dt)
|
||
return min(s, profile.total_distance)
|
||
|
||
return 0.0
|
||
|
||
def get_position_ratio(self, elapsed: float, profile: MotionProfile) -> float:
|
||
"""获取当前位置比例 (0.0 ~ 1.0)"""
|
||
if profile.total_distance < CART_FUZZ:
|
||
return 1.0
|
||
pos = self.get_position_at_time(elapsed, profile)
|
||
return min(pos / profile.total_distance, 1.0)
|
||
|
||
def get_velocity_at_time(self, elapsed: float, profile: MotionProfile) -> float:
|
||
"""获取当前速度 (mm/s)"""
|
||
if elapsed <= 0 or elapsed >= profile.duration:
|
||
return 0.0
|
||
|
||
if profile.profile_type == "zero":
|
||
return 0.0
|
||
|
||
elif profile.profile_type == "trapezoidal":
|
||
if elapsed <= profile.t_acc:
|
||
return self.acc * elapsed
|
||
elif elapsed <= profile.t_acc + profile.t_const:
|
||
return profile.v_max
|
||
else:
|
||
dt = elapsed - profile.t_acc - profile.t_const
|
||
return max(0.0, profile.v_max - self.dec * dt)
|
||
|
||
elif profile.profile_type == "triangular":
|
||
if elapsed <= profile.t_acc:
|
||
return self.acc * elapsed
|
||
else:
|
||
dt = elapsed - profile.t_acc
|
||
return max(0.0, profile.v_max - self.dec * dt)
|
||
|
||
return 0.0
|
||
|
||
def calculate_time(self, distance: float, commanded_feedrate: float = None,
|
||
is_rapid: bool = False) -> Dict[str, float]:
|
||
"""向后兼容的 calculate_time 方法"""
|
||
profile = self.plan_motion(distance, commanded_feedrate, is_rapid)
|
||
return {
|
||
'duration': profile.duration,
|
||
'max_velocity': profile.v_max,
|
||
'acceleration_time': profile.t_acc,
|
||
'constant_time': profile.t_const,
|
||
'deceleration_time': profile.t_dec,
|
||
'profile_type': profile.profile_type
|
||
}
|
||
|
||
def get_position_at_time_legacy(self, distance: float, duration: float,
|
||
commanded_feedrate: float = None,
|
||
is_rapid: bool = False) -> Callable[[float], float]:
|
||
"""向后兼容的旧接口"""
|
||
profile = self.plan_motion(distance, commanded_feedrate, is_rapid)
|
||
|
||
def pos_func(t: float) -> float:
|
||
return self.get_position_at_time(t, profile)
|
||
|
||
return pos_func
|
||
|
||
def get_velocity_at_time_legacy(self, distance: float, duration: float,
|
||
commanded_feedrate: float = None,
|
||
is_rapid: bool = False) -> Callable[[float], float]:
|
||
"""向后兼容的旧接口"""
|
||
profile = self.plan_motion(distance, commanded_feedrate, is_rapid)
|
||
|
||
def vel_func(t: float) -> float:
|
||
return self.get_velocity_at_time(t, profile)
|
||
|
||
return vel_func
|
||
|
||
def get_profile_summary(self, profile: MotionProfile) -> str:
|
||
"""获取速度曲线摘要"""
|
||
if profile.profile_type == "zero":
|
||
return "静止"
|
||
|
||
v_max_mm_min = profile.v_max * 60.0
|
||
parts = []
|
||
if profile.t_acc > 0.001:
|
||
parts.append(f"加速{profile.t_acc*1000:.0f}ms({profile.s_acc:.1f}mm)")
|
||
if profile.t_const > 0.001:
|
||
parts.append(f"匀速{profile.t_const*1000:.0f}ms({profile.s_const:.1f}mm)")
|
||
if profile.t_dec > 0.001:
|
||
parts.append(f"减速{profile.t_dec*1000:.0f}ms({profile.s_dec:.1f}mm)")
|
||
|
||
return (f"Vmax={v_max_mm_min:.0f}mm/min "
|
||
f"耗时{profile.duration*1000:.0f}ms [" + " → ".join(parts) + "]")
|
||
|
||
def set_feed_override(self, value: float):
|
||
"""设置进给倍率 (0.0 ~ 2.0)"""
|
||
self.feed_override = max(0.0, min(2.0, value))
|
||
|
||
def set_rapid_override(self, value: float):
|
||
"""设置快速倍率 (0.0 ~ 1.0)"""
|
||
self.rapid_override = max(0.0, min(1.0, value))
|
||
|
||
def set_acceleration(self, value: float):
|
||
"""设置加速度"""
|
||
self.acc = max(1.0, value)
|
||
|
||
def set_deceleration(self, value: float):
|
||
"""设置减速度"""
|
||
self.dec = max(1.0, value)
|
||
|
||
# ==================== 控制结构处理器 ====================
|
||
|
||
class ControlStructureHandler:
|
||
"""O代码控制结构处理器"""
|
||
|
||
def __init__(self, var_manager: 'lcnc_param.LinuxCNCParameterTable', debug: bool = False):
|
||
self.var_manager = var_manager
|
||
self.debug = debug
|
||
|
||
self.while_stack: List[Dict] = []
|
||
self.repeat_stack: List[Dict] = []
|
||
self.if_stack: List[Dict] = []
|
||
self.subroutine_stack: List[Dict] = []
|
||
|
||
self.jump_targets: Dict[str, int] = {}
|
||
|
||
self._skip_mode = False
|
||
self._skip_depth = 0
|
||
self._skip_target = None
|
||
|
||
def evaluate_condition(self, condition: str, local_level: int = 0) -> bool:
|
||
"""评估条件表达式"""
|
||
try:
|
||
expanded = self.var_manager.expand_variables(condition, local_level)
|
||
expanded = re.sub(r'(?i)EQ', '==', expanded)
|
||
expanded = re.sub(r'(?i)NE', '!=', expanded)
|
||
expanded = re.sub(r'(?i)GT', '>', expanded)
|
||
expanded = re.sub(r'(?i)GE', '>=', expanded)
|
||
expanded = re.sub(r'(?i)LT', '<', expanded)
|
||
expanded = re.sub(r'(?i)LE', '<=', expanded)
|
||
expanded = re.sub(r'(?i)XOR', '!=', expanded)
|
||
expanded = re.sub(r'(?i)AND', 'and', expanded)
|
||
expanded = re.sub(r'(?i)OR', 'or', expanded)
|
||
expanded = re.sub(r'(?i)NOT', 'not', expanded)
|
||
result = eval(expanded, {"__builtins__": {}}, {"math": math})
|
||
return bool(result)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f" [DEBUG] 条件评估失败: {condition}, 错误: {e}")
|
||
return False
|
||
|
||
def parse_o_code(self, line: str, line_num: int, current_line_idx: int,
|
||
lines: List[str], local_level: int = 0) -> Optional[int]:
|
||
"""解析O代码控制结构"""
|
||
line_upper = line.upper().strip()
|
||
|
||
# 如果在跳过模式
|
||
if self._skip_mode:
|
||
if re.match(r'O<[^>]+>\s+IF\s+\[', line_upper, re.I):
|
||
self._skip_depth += 1
|
||
elif re.match(r'O<[^>]+>\s+ENDIF', line_upper, re.I):
|
||
self._skip_depth -= 1
|
||
if self._skip_depth == 0:
|
||
self._skip_mode = False
|
||
self._skip_target = None
|
||
elif re.match(r'O<[^>]+>\s+ELSEIF', line_upper, re.I) and self._skip_depth == 1:
|
||
# 在跳过模式中遇到 ELSEIF,检查条件
|
||
if_match = re.match(r'O<([^>]+)>\s+ELSEIF\s+\[(.+)\]', line_upper, re.I)
|
||
if if_match:
|
||
condition = if_match.group(2)
|
||
if self.evaluate_condition(condition, local_level):
|
||
self._skip_mode = False
|
||
self._skip_depth = 0
|
||
elif re.match(r'O<[^>]+>\s+ELSE', line_upper, re.I) and self._skip_depth == 1:
|
||
# 在跳过模式中遇到 ELSE,停止跳过
|
||
self._skip_mode = False
|
||
self._skip_depth = 0
|
||
return None
|
||
|
||
# SUB
|
||
if re.match(r'O<[^>]+>\s+SUB', line_upper, re.I):
|
||
return None
|
||
|
||
# ENDSUB
|
||
if re.match(r'O<[^>]+>\s+ENDSUB', line_upper, re.I):
|
||
if self.subroutine_stack:
|
||
call_info = self.subroutine_stack.pop()
|
||
return call_info['return_line']
|
||
return None
|
||
|
||
# CALL
|
||
if re.match(r'O<[^>]+>\s+CALL', line_upper, re.I):
|
||
return None
|
||
|
||
# IF
|
||
if_match = re.match(r'O<([^>]+)>\s+IF\s+\[(.+)\]', line_upper, re.I)
|
||
if if_match:
|
||
label = if_match.group(1)
|
||
condition = if_match.group(2)
|
||
result = self.evaluate_condition(condition, local_level)
|
||
self.if_stack.append({
|
||
'label': label,
|
||
'condition_result': result,
|
||
'line_num': line_num
|
||
})
|
||
if not result:
|
||
self._skip_mode = True
|
||
self._skip_depth = 1
|
||
self._skip_target = label
|
||
return self._skip_to_endif(current_line_idx, lines)
|
||
return None
|
||
|
||
# ELSEIF
|
||
elseif_match = re.match(r'O<([^>]+)>\s+ELSEIF\s+\[(.+)\]', line_upper, re.I)
|
||
if elseif_match:
|
||
label = elseif_match.group(1)
|
||
condition = elseif_match.group(2)
|
||
if self.if_stack:
|
||
top = self.if_stack[-1]
|
||
if top['condition_result']:
|
||
self._skip_mode = True
|
||
self._skip_depth = 1
|
||
return self._skip_to_endif(current_line_idx, lines)
|
||
else:
|
||
result = self.evaluate_condition(condition, local_level)
|
||
if result:
|
||
top['condition_result'] = True
|
||
else:
|
||
self._skip_mode = True
|
||
self._skip_depth = 1
|
||
return self._skip_to_endif(current_line_idx, lines)
|
||
return None
|
||
|
||
# ELSE
|
||
if re.match(r'O<[^>]+>\s+ELSE', line_upper, re.I):
|
||
if self.if_stack:
|
||
top = self.if_stack[-1]
|
||
if top['condition_result']:
|
||
self._skip_mode = True
|
||
self._skip_depth = 1
|
||
return self._skip_to_endif(current_line_idx, lines)
|
||
return None
|
||
|
||
# ENDIF
|
||
if re.match(r'O<[^>]+>\s+ENDIF', line_upper, re.I):
|
||
if self.if_stack:
|
||
self.if_stack.pop()
|
||
return None
|
||
|
||
# WHILE
|
||
while_match = re.match(r'O<([^>]+)>\s+WHILE\s+\[(.+)\]', line_upper, re.I)
|
||
if while_match:
|
||
label = while_match.group(1)
|
||
condition = while_match.group(2)
|
||
result = self.evaluate_condition(condition, local_level)
|
||
self.while_stack.append({
|
||
'label': label,
|
||
'condition': condition,
|
||
'start_line': current_line_idx,
|
||
'line_num': line_num
|
||
})
|
||
if not result:
|
||
return self._skip_to_endwhile(current_line_idx, lines)
|
||
return None
|
||
|
||
# ENDWHILE
|
||
if re.match(r'O<[^>]+>\s+ENDWHILE', line_upper, re.I):
|
||
if self.while_stack:
|
||
loop_info = self.while_stack[-1]
|
||
result = self.evaluate_condition(loop_info['condition'], local_level)
|
||
if result:
|
||
return loop_info['start_line']
|
||
else:
|
||
self.while_stack.pop()
|
||
return None
|
||
|
||
# REPEAT
|
||
repeat_match = re.match(r'O<([^>]+)>\s+REPEAT\s+\[(.+)\]', line_upper, re.I)
|
||
if repeat_match:
|
||
label = repeat_match.group(1)
|
||
count_expr = repeat_match.group(2)
|
||
try:
|
||
expanded = self.var_manager.expand_variables(count_expr, local_level)
|
||
count = int(eval(expanded, {"__builtins__": {}}, {"math": math}))
|
||
except:
|
||
count = 1
|
||
self.repeat_stack.append({
|
||
'label': label,
|
||
'count': count,
|
||
'remaining': count,
|
||
'start_line': current_line_idx
|
||
})
|
||
return None
|
||
|
||
# ENDREPEAT
|
||
if re.match(r'O<[^>]+>\s+ENDREPEAT', line_upper, re.I):
|
||
if self.repeat_stack:
|
||
loop_info = self.repeat_stack[-1]
|
||
loop_info['remaining'] -= 1
|
||
if loop_info['remaining'] > 0:
|
||
return loop_info['start_line']
|
||
else:
|
||
self.repeat_stack.pop()
|
||
return None
|
||
|
||
# BREAK
|
||
if re.match(r'O<[^>]+>\s+BREAK', line_upper, re.I):
|
||
if self.while_stack:
|
||
return self._skip_to_endwhile(current_line_idx, lines)
|
||
if self.repeat_stack:
|
||
self.repeat_stack.pop()
|
||
return None
|
||
|
||
# CONTINUE
|
||
if re.match(r'O<[^>]+>\s+CONTINUE', line_upper, re.I):
|
||
if self.while_stack:
|
||
return self.while_stack[-1]['start_line']
|
||
if self.repeat_stack:
|
||
return self.repeat_stack[-1]['start_line']
|
||
return None
|
||
|
||
# GOTO
|
||
goto_match = re.match(r'O<[^>]+>\s+GOTO\s+(\d+)', line_upper, re.I)
|
||
if goto_match:
|
||
target_line = int(goto_match.group(1))
|
||
for i, l in enumerate(lines):
|
||
n_match = re.search(r'N(\d+)', l, re.I)
|
||
if n_match and int(n_match.group(1)) == target_line:
|
||
return i
|
||
if target_line <= len(lines):
|
||
return target_line - 1
|
||
return None
|
||
|
||
# RETURN
|
||
if re.match(r'O<[^>]+>\s+RETURN', line_upper, re.I):
|
||
if self.subroutine_stack:
|
||
call_info = self.subroutine_stack.pop()
|
||
return call_info['return_line']
|
||
return None
|
||
|
||
return None
|
||
|
||
def _skip_to_endif(self, line_idx: int, lines: List[str]) -> int:
|
||
"""跳过到对应的 ENDIF"""
|
||
i = line_idx + 1
|
||
depth = 1
|
||
while i < len(lines):
|
||
line_upper = lines[i].upper().strip()
|
||
if re.match(r'O<[^>]+>\s+IF\s+\[', line_upper, re.I):
|
||
depth += 1
|
||
elif re.match(r'O<[^>]+>\s+ENDIF', line_upper, re.I):
|
||
depth -= 1
|
||
if depth == 0:
|
||
return i
|
||
i += 1
|
||
return len(lines) - 1
|
||
|
||
def _skip_to_endwhile(self, line_idx: int, lines: List[str]) -> int:
|
||
"""跳过到对应的 ENDWHILE"""
|
||
i = line_idx + 1
|
||
depth = 1
|
||
while i < len(lines):
|
||
line_upper = lines[i].upper().strip()
|
||
if re.match(r'O<[^>]+>\s+WHILE\s+\[', line_upper, re.I):
|
||
depth += 1
|
||
elif re.match(r'O<[^>]+>\s+ENDWHILE', line_upper, re.I):
|
||
depth -= 1
|
||
if depth == 0:
|
||
return i
|
||
i += 1
|
||
return len(lines) - 1
|
||
|
||
def reset(self):
|
||
"""重置状态"""
|
||
self.while_stack.clear()
|
||
self.repeat_stack.clear()
|
||
self.if_stack.clear()
|
||
self.subroutine_stack.clear()
|
||
self.jump_targets.clear()
|
||
self._skip_mode = False
|
||
self._skip_depth = 0
|
||
self._skip_target = None
|
||
|
||
|
||
# ==================== 虚拟HAL系统 ====================
|
||
|
||
class VirtualHAL:
|
||
"""虚拟HAL系统 - 模拟LinuxCNC的硬件抽象层"""
|
||
|
||
def __init__(self, debug: bool = False):
|
||
self.debug = debug
|
||
self.pins: Dict[str, Any] = {}
|
||
self.signals: Dict[str, Any] = {}
|
||
self.params: Dict[str, Any] = {}
|
||
self.watchers: Dict[str, List[Callable]] = {}
|
||
self._comp_id = 0
|
||
self._initialized = False
|
||
self._init_pins()
|
||
|
||
def _init_pins(self):
|
||
"""初始化默认引脚"""
|
||
# 关节
|
||
for i in range(EMCMOT_MAX_JOINTS):
|
||
self.pins[f'joint.{i}.position'] = 0.0
|
||
self.pins[f'joint.{i}.velocity'] = 0.0
|
||
self.pins[f'joint.{i}.acceleration'] = 0.0
|
||
self.pins[f'joint.{i}.home'] = 0
|
||
self.pins[f'joint.{i}.homing'] = 0
|
||
self.pins[f'joint.{i}.error'] = 0.0
|
||
self.pins[f'joint.{i}.f-error'] = 0.0
|
||
self.pins[f'joint.{i}.enabled'] = 1
|
||
self.pins[f'joint.{i}.max-limit'] = 1000.0
|
||
self.pins[f'joint.{i}.min-limit'] = -1000.0
|
||
|
||
# 轴
|
||
for axis in ['x', 'y', 'z', 'a', 'b', 'c', 'u', 'v', 'w']:
|
||
self.pins[f'axis.{axis}.position'] = 0.0
|
||
self.pins[f'axis.{axis}.velocity'] = 0.0
|
||
self.pins[f'axis.{axis}.home'] = 0
|
||
self.pins[f'axis.{axis}.homing'] = 0
|
||
self.pins[f'axis.{axis}.enabled'] = 1
|
||
self.pins[f'axis.{axis}.max-limit'] = 1000.0
|
||
self.pins[f'axis.{axis}.min-limit'] = -1000.0
|
||
|
||
# 主轴
|
||
for i in range(EMCMOT_MAX_SPINDLES):
|
||
self.pins[f'spindle.{i}.speed'] = 0.0
|
||
self.pins[f'spindle.{i}.direction'] = 0
|
||
self.pins[f'spindle.{i}.at-speed'] = 0
|
||
self.pins[f'spindle.{i}.brake'] = 0
|
||
self.pins[f'spindle.{i}.enable'] = 0
|
||
self.pins[f'spindle.{i}.speed-out'] = 0.0
|
||
self.pins[f'spindle.{i}.speed-out-abs'] = 0.0
|
||
|
||
# 冷却液
|
||
self.pins['coolant.flood'] = 0
|
||
self.pins['coolant.mist'] = 0
|
||
|
||
# 刀具
|
||
self.pins['tool.number'] = 0
|
||
self.pins['tool.pocket'] = 0
|
||
self.pins['tool.length'] = 0.0
|
||
self.pins['tool.diameter'] = 0.0
|
||
self.pins['tool.prep-number'] = 0
|
||
self.pins['tool.prep-pocket'] = 0
|
||
|
||
# 倍率
|
||
self.pins['feed-override'] = 1.0
|
||
self.pins['rapid-override'] = 1.0
|
||
self.pins['spindle-override'] = 1.0
|
||
self.pins['max-velocity'] = 10000.0
|
||
|
||
# 运动
|
||
self.pins['motion.kins-type'] = 2
|
||
self.pins['motion.rtcp-active'] = 0
|
||
self.pins['motion.switchkins-type'] = 0
|
||
self.pins['motion.pivot-length'] = 250.0
|
||
self.pins['motion.tool-length'] = 0.0
|
||
self.pins['motion.rot-center-x'] = 0.0
|
||
self.pins['motion.rot-center-y'] = 0.0
|
||
self.pins['motion.rot-center-z'] = 0.0
|
||
self.pins['motion.enabled'] = 0
|
||
self.pins['motion.in-position'] = 1
|
||
self.pins['motion.feed-hold'] = 0
|
||
self.pins['motion.adaptive-feed'] = 1.0
|
||
|
||
# 急停
|
||
self.pins['estop.active'] = 0
|
||
self.pins['estop.reset-request'] = 0
|
||
|
||
# 机床
|
||
self.pins['machine.power-on'] = 0
|
||
self.pins['machine.power-request'] = 0
|
||
self.pins['machine.reset'] = 0
|
||
self.pins['machine.reset-done'] = 1
|
||
self.pins['machine.state'] = 'OFF'
|
||
self.pins['machine.is-on'] = 0
|
||
|
||
# 回零
|
||
self.pins['home.all'] = 0
|
||
for axis in ['x', 'y', 'z', 'a', 'b', 'c']:
|
||
self.pins[f'home.{axis}'] = 0
|
||
self.pins[f'home.{axis}-done'] = 0
|
||
self.pins[f'home.{axis}-switch'] = 0
|
||
self.pins['home.all-done'] = 0
|
||
|
||
# 程序
|
||
self.pins['program.run'] = 0
|
||
self.pins['program.pause'] = 0
|
||
self.pins['program.resume'] = 0
|
||
self.pins['program.stop'] = 0
|
||
self.pins['program.status'] = 'IDLE'
|
||
self.pins['program.line'] = 0
|
||
self.pins['program.progress'] = 0.0
|
||
self.pins['program.block-delete'] = 0
|
||
self.pins['program.optional-stop'] = 1
|
||
|
||
# 告警
|
||
self.pins['alarm.active'] = 0
|
||
self.pins['alarm.code'] = 0
|
||
self.pins['alarm.message'] = ''
|
||
|
||
# 模拟量
|
||
for i in range(8):
|
||
self.pins[f'analog.out.{i}'] = 0.0
|
||
|
||
# 数字量
|
||
for i in range(32):
|
||
self.pins[f'digital.in.{i}'] = 0
|
||
self.pins[f'digital.out.{i}'] = 0
|
||
|
||
# 探测
|
||
self.pins['probe.input'] = 0
|
||
self.pins['probe.tripped'] = 0
|
||
self.pins['probe.position-x'] = 0.0
|
||
self.pins['probe.position-y'] = 0.0
|
||
self.pins['probe.position-z'] = 0.0
|
||
|
||
# 固定循环
|
||
self.pins['motion.canned-cycle'] = 0
|
||
self.pins['motion.canned-cycle-type'] = 0
|
||
self.pins['motion.canned-cycle-retract'] = 0.0
|
||
self.pins['motion.canned-cycle-clearance'] = 0.0
|
||
|
||
def hal_init(self, comp_name: str) -> int:
|
||
"""初始化HAL组件"""
|
||
self._comp_id = hash(comp_name) & 0x7FFFFFFF
|
||
self._initialized = True
|
||
return self._comp_id
|
||
|
||
def hal_ready(self, comp_id: int) -> int:
|
||
"""标记HAL组件就绪"""
|
||
return 0
|
||
|
||
def hal_exit(self, comp_id: int):
|
||
"""退出HAL"""
|
||
self._initialized = False
|
||
|
||
def set(self, pin: str, value: Any) -> bool:
|
||
"""设置引脚值"""
|
||
old_value = self.pins.get(pin)
|
||
self.pins[pin] = value
|
||
self._notify_watchers(pin, value, old_value)
|
||
return True
|
||
|
||
def get(self, pin: str, default: Any = 0) -> Any:
|
||
"""获取引脚值"""
|
||
return self.pins.get(pin, default)
|
||
|
||
def add_watcher(self, pin: str, callback: Callable):
|
||
"""添加监听器"""
|
||
if pin not in self.watchers:
|
||
self.watchers[pin] = []
|
||
self.watchers[pin].append(callback)
|
||
|
||
def remove_watcher(self, pin: str, callback: Callable):
|
||
"""移除监听器"""
|
||
if pin in self.watchers:
|
||
try:
|
||
self.watchers[pin].remove(callback)
|
||
except ValueError:
|
||
pass
|
||
|
||
def _notify_watchers(self, pin: str, value: Any, old_value: Any):
|
||
"""通知监听器"""
|
||
if pin in self.watchers:
|
||
for callback in self.watchers[pin]:
|
||
try:
|
||
callback(pin, value, old_value)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[HAL] 监听器回调错误: {e}")
|
||
|
||
def update_axis_position(self, axis: str, position: float):
|
||
"""更新轴位置"""
|
||
self.set(f'axis.{axis}.position', position)
|
||
axis_map = {'x': 0, 'y': 1, 'z': 2, 'a': 3, 'b': 4, 'c': 5, 'u': 6, 'v': 7, 'w': 8}
|
||
if axis.lower() in axis_map:
|
||
self.set(f'joint.{axis_map[axis.lower()]}.position', position)
|
||
|
||
def update_all_positions(self, positions: Dict[str, float]):
|
||
"""更新所有轴位置"""
|
||
for axis, pos in positions.items():
|
||
self.update_axis_position(axis, pos)
|
||
|
||
def dump_state(self) -> Dict[str, Any]:
|
||
"""导出状态"""
|
||
return self.pins.copy()
|
||
|
||
def load_state(self, state: Dict[str, Any]):
|
||
"""加载状态"""
|
||
for pin, value in state.items():
|
||
if pin in self.pins:
|
||
self.pins[pin] = value
|
||
|
||
def to_json(self) -> str:
|
||
"""导出为JSON"""
|
||
serializable = {}
|
||
for k, v in self.pins.items():
|
||
if isinstance(v, (str, int, float, bool, type(None))):
|
||
serializable[k] = v
|
||
else:
|
||
serializable[k] = str(v)
|
||
return json.dumps(serializable, indent=2)
|
||
|
||
def reset(self):
|
||
"""重置"""
|
||
self._init_pins()
|
||
if self.debug:
|
||
print("[HAL] 系统已重置")
|
||
|
||
|
||
# ==================== 机床状态机 ====================
|
||
|
||
class MachineStateMachine:
|
||
"""机床状态机 - 模拟真实CNC控制器的状态转换"""
|
||
|
||
STATE_OFF = "OFF"
|
||
STATE_RESET = "RESET"
|
||
STATE_ESTOP = "ESTOP"
|
||
STATE_IDLE = "IDLE"
|
||
STATE_RUNNING = "RUNNING"
|
||
STATE_PAUSED = "PAUSED"
|
||
STATE_HOMING = "HOMING"
|
||
STATE_JOGGING = "JOGGING"
|
||
STATE_ALARM = "ALARM"
|
||
STATE_PROBING = "PROBING"
|
||
STATE_MDI = "MDI"
|
||
STATE_TOOL_CHANGING = "TOOL_CHANGING"
|
||
|
||
TRANSITIONS = {
|
||
STATE_OFF: [STATE_RESET, STATE_ESTOP],
|
||
STATE_RESET: [STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_ESTOP: [STATE_RESET, STATE_OFF],
|
||
STATE_IDLE: [STATE_RUNNING, STATE_HOMING, STATE_JOGGING, STATE_MDI,
|
||
STATE_ESTOP, STATE_ALARM, STATE_OFF, STATE_TOOL_CHANGING, STATE_RESET],
|
||
STATE_RUNNING: [STATE_PAUSED, STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_PAUSED: [STATE_RUNNING, STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_HOMING: [STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_JOGGING: [STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_ALARM: [STATE_RESET, STATE_OFF],
|
||
STATE_PROBING: [STATE_IDLE, STATE_ESTOP],
|
||
STATE_MDI: [STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
STATE_TOOL_CHANGING: [STATE_IDLE, STATE_ESTOP, STATE_ALARM],
|
||
}
|
||
|
||
def __init__(self, hal: VirtualHAL, debug: bool = False):
|
||
self.hal = hal
|
||
self.debug = debug
|
||
self.current_state = self.STATE_OFF
|
||
self.previous_state = self.STATE_OFF
|
||
self.state_entry_time = time.time()
|
||
self.alarm_reason = ""
|
||
self.alarm_code = 0
|
||
|
||
self.transition_callbacks: List[Callable] = []
|
||
|
||
self._update_hal_pins()
|
||
|
||
if self.debug:
|
||
print(f"[状态机] 初始化: {self.current_state}")
|
||
|
||
def _update_hal_pins(self):
|
||
"""更新 HAL 引脚"""
|
||
self.hal.set('machine.state', self.current_state)
|
||
self.hal.set('estop.active', 1 if self.current_state == self.STATE_ESTOP else 0)
|
||
|
||
is_powered = self.current_state not in [self.STATE_OFF, self.STATE_ESTOP]
|
||
self.hal.set('machine.power-on', 1 if is_powered else 0)
|
||
self.hal.set('machine.is-on', 1 if is_powered else 0)
|
||
|
||
self.hal.set('alarm.active', 1 if self.current_state == self.STATE_ALARM else 0)
|
||
self.hal.set('machine.reset-done', 1 if self.current_state == self.STATE_IDLE else 0)
|
||
|
||
def add_transition_callback(self, callback: Callable):
|
||
"""添加状态转换回调"""
|
||
self.transition_callbacks.append(callback)
|
||
|
||
def _notify_transition(self, from_state: str, to_state: str):
|
||
"""通知状态转换"""
|
||
for callback in self.transition_callbacks:
|
||
try:
|
||
callback(from_state, to_state)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[状态机] 回调错误: {e}")
|
||
|
||
def _can_transition(self, target_state: str) -> bool:
|
||
"""检查是否可以转换"""
|
||
return target_state in self.TRANSITIONS.get(self.current_state, [])
|
||
|
||
def _log_transition(self, target_state: str):
|
||
"""记录状态转换"""
|
||
if self.debug:
|
||
print(f"[状态机] {self.current_state} -> {target_state}")
|
||
|
||
def transition_to(self, target_state: str) -> bool:
|
||
"""执行状态转换"""
|
||
if not self._can_transition(target_state):
|
||
if self.debug:
|
||
print(f"[状态机] 无效转换: {self.current_state} -> {target_state}")
|
||
return False
|
||
|
||
old_state = self.current_state
|
||
self.previous_state = old_state
|
||
self.current_state = target_state
|
||
self.state_entry_time = time.time()
|
||
|
||
self._log_transition(target_state)
|
||
self._update_hal_pins()
|
||
self._notify_transition(old_state, target_state)
|
||
|
||
return True
|
||
|
||
def request_power_on(self) -> bool:
|
||
"""请求上电"""
|
||
if self.current_state == self.STATE_OFF:
|
||
return self.transition_to(self.STATE_RESET)
|
||
return False
|
||
|
||
def request_power_off(self) -> bool:
|
||
"""请求断电"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
return self.transition_to(self.STATE_OFF)
|
||
return False
|
||
|
||
def emergency_stop(self) -> bool:
|
||
"""急停"""
|
||
if self.current_state != self.STATE_ESTOP:
|
||
self.hal.set('estop.active', 1)
|
||
return self.transition_to(self.STATE_ESTOP)
|
||
return False
|
||
|
||
def reset(self) -> bool:
|
||
"""复位"""
|
||
if self.current_state in [self.STATE_ESTOP, self.STATE_ALARM, self.STATE_IDLE, self.STATE_RESET]:
|
||
self.hal.set('machine.reset', 1)
|
||
result = self.transition_to(self.STATE_RESET)
|
||
self.hal.set('machine.reset', 0)
|
||
return result
|
||
return False
|
||
|
||
def home_all(self) -> bool:
|
||
"""回零"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
self.hal.set('home.all', 1)
|
||
result = self.transition_to(self.STATE_HOMING)
|
||
self.hal.set('home.all', 0)
|
||
return result
|
||
return False
|
||
|
||
def start_program(self) -> bool:
|
||
"""启动程序"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
return self.transition_to(self.STATE_RUNNING)
|
||
return False
|
||
|
||
def pause_program(self) -> bool:
|
||
"""暂停程序"""
|
||
if self.current_state == self.STATE_RUNNING:
|
||
return self.transition_to(self.STATE_PAUSED)
|
||
return False
|
||
|
||
def resume_program(self) -> bool:
|
||
"""恢复程序"""
|
||
if self.current_state == self.STATE_PAUSED:
|
||
return self.transition_to(self.STATE_RUNNING)
|
||
return False
|
||
|
||
def stop_program(self) -> bool:
|
||
"""停止程序"""
|
||
if self.current_state in [self.STATE_RUNNING, self.STATE_PAUSED]:
|
||
return self.transition_to(self.STATE_IDLE)
|
||
return False
|
||
|
||
def start_jogging(self) -> bool:
|
||
"""开始点动"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
return self.transition_to(self.STATE_JOGGING)
|
||
return False
|
||
|
||
def stop_jogging(self) -> bool:
|
||
"""停止点动"""
|
||
if self.current_state == self.STATE_JOGGING:
|
||
return self.transition_to(self.STATE_IDLE)
|
||
return False
|
||
|
||
def start_mdi(self) -> bool:
|
||
"""开始MDI"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
return self.transition_to(self.STATE_MDI)
|
||
return False
|
||
|
||
def stop_mdi(self) -> bool:
|
||
"""停止MDI"""
|
||
if self.current_state == self.STATE_MDI:
|
||
return self.transition_to(self.STATE_IDLE)
|
||
return False
|
||
|
||
def start_tool_change(self) -> bool:
|
||
"""开始换刀"""
|
||
if self.current_state == self.STATE_IDLE:
|
||
return self.transition_to(self.STATE_TOOL_CHANGING)
|
||
return False
|
||
|
||
def stop_tool_change(self) -> bool:
|
||
"""停止换刀"""
|
||
if self.current_state == self.STATE_TOOL_CHANGING:
|
||
return self.transition_to(self.STATE_IDLE)
|
||
return False
|
||
|
||
def set_alarm(self, code: int, message: str) -> bool:
|
||
"""设置告警"""
|
||
if self.current_state not in [self.STATE_ESTOP, self.STATE_OFF]:
|
||
self.alarm_reason = message
|
||
self.alarm_code = code
|
||
self.hal.set('alarm.code', code)
|
||
self.hal.set('alarm.message', message)
|
||
return self.transition_to(self.STATE_ALARM)
|
||
return False
|
||
|
||
def clear_alarm(self) -> bool:
|
||
"""清除告警"""
|
||
if self.current_state == self.STATE_ALARM:
|
||
self.alarm_reason = ""
|
||
self.alarm_code = 0
|
||
self.hal.set('alarm.code', 0)
|
||
self.hal.set('alarm.message', '')
|
||
return self.transition_to(self.STATE_RESET)
|
||
return False
|
||
|
||
def get_state(self) -> str:
|
||
"""获取当前状态"""
|
||
return self.current_state
|
||
|
||
def get_state_duration(self) -> float:
|
||
"""获取当前状态持续时间"""
|
||
return time.time() - self.state_entry_time
|
||
|
||
def is_operational(self) -> bool:
|
||
"""是否可操作"""
|
||
return self.current_state == self.STATE_IDLE
|
||
|
||
def is_running(self) -> bool:
|
||
"""是否正在运行"""
|
||
return self.current_state == self.STATE_RUNNING
|
||
|
||
def is_paused(self) -> bool:
|
||
"""是否已暂停"""
|
||
return self.current_state == self.STATE_PAUSED
|
||
|
||
def is_estop(self) -> bool:
|
||
"""是否急停"""
|
||
return self.current_state == self.STATE_ESTOP
|
||
|
||
def is_alarm(self) -> bool:
|
||
"""是否有告警"""
|
||
return self.current_state == self.STATE_ALARM
|
||
|
||
def update(self):
|
||
"""更新状态机"""
|
||
if self.hal.get('estop.reset-request'):
|
||
self.reset()
|
||
self.hal.set('estop.reset-request', 0)
|
||
|
||
if self.hal.get('machine.power-request'):
|
||
self.request_power_on()
|
||
self.hal.set('machine.power-request', 0)
|
||
|
||
if self.hal.get('program.run'):
|
||
self.start_program()
|
||
self.hal.set('program.run', 0)
|
||
|
||
if self.hal.get('program.pause'):
|
||
self.pause_program()
|
||
self.hal.set('program.pause', 0)
|
||
|
||
if self.hal.get('program.resume'):
|
||
self.resume_program()
|
||
self.hal.set('program.resume', 0)
|
||
|
||
if self.hal.get('program.stop'):
|
||
self.stop_program()
|
||
self.hal.set('program.stop', 0)
|
||
|
||
if self.current_state == self.STATE_RESET:
|
||
if self.get_state_duration() >= 0.5:
|
||
self.transition_to(self.STATE_IDLE)
|
||
|
||
if self.current_state == self.STATE_HOMING:
|
||
if self.hal.get('home.all-done'):
|
||
self.transition_to(self.STATE_IDLE)
|
||
self.hal.set('home.all-done', 0)
|
||
|
||
self.hal.set('program.status', self.current_state)
|
||
|
||
|
||
# ==================== GLCanonPathCollectorWithRTCP 类 ====================
|
||
|
||
|
||
|
||
# ==================== 内置G代码解析器 ====================
|
||
|
||
class FullGCodeParserWithRTCP:
|
||
"""内置G代码解析器 - 支持LinuxCNC宏指令和子程序"""
|
||
|
||
def __init__(self, max_points: int = 50000,
|
||
tool_radius_map: Dict[int, float] = None,
|
||
tool_length_map: Dict[int, float] = None,
|
||
acceleration: float = 500.0,
|
||
max_rapid_rate: float = 10000.0,
|
||
max_feed_rate: float = 5000.0,
|
||
kinematics_type: KinematicsType = KinematicsType.TRT_BC,
|
||
kinematics_params: Dict[str, Any] = None,
|
||
debug: bool = False):
|
||
|
||
self.max_points = max_points
|
||
self.tool_radius_map = tool_radius_map or {1: 2.0, 2: 3.0, 3: 4.0}
|
||
self.tool_length_map = tool_length_map or {1: 100.0, 2: 120.0, 3: 150.0}
|
||
self.acceleration = acceleration
|
||
self.max_rapid_rate = max_rapid_rate
|
||
self.max_feed_rate = max_feed_rate
|
||
self.kinematics_type = kinematics_type
|
||
self.kinematics_params = kinematics_params or {}
|
||
self.debug = debug
|
||
|
||
self.collector: Optional[GLCanonPathCollectorWithRTCP] = None
|
||
self.current_plane = 17
|
||
self.current_filename = ""
|
||
self.subroutine_path = ""
|
||
|
||
self.subroutines: Dict[str, List[Tuple[int, str]]] = {}
|
||
self.call_level = 0
|
||
|
||
self.saved_states: List[Dict] = []
|
||
|
||
self._line_number_map: Dict[int, int] = {}
|
||
|
||
def set_subroutine_path(self, path: str):
|
||
self.subroutine_path = path
|
||
|
||
def load_subroutine_file(self, filename: str) -> bool:
|
||
if self.debug:
|
||
print(f"[DEBUG] load_subroutine_file: 尝试加载 '{filename}'")
|
||
|
||
search_paths = [filename]
|
||
if self.subroutine_path:
|
||
search_paths.append(os.path.join(self.subroutine_path, os.path.basename(filename)))
|
||
if self.current_filename:
|
||
search_paths.append(os.path.join(os.path.dirname(self.current_filename), os.path.basename(filename)))
|
||
search_paths.append(os.path.join(os.getcwd(), os.path.basename(filename)))
|
||
|
||
for path in search_paths:
|
||
if os.path.exists(path):
|
||
try:
|
||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||
content = f.read()
|
||
lines = content.split('\n')
|
||
self._collect_subroutines_from_lines(lines, path)
|
||
return True
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[DEBUG] 加载失败: {path}, 错误: {e}")
|
||
|
||
return False
|
||
|
||
def _collect_subroutines_from_lines(self, lines: List[str], source: str = ""):
|
||
i = 0
|
||
while i < len(lines):
|
||
raw_line = lines[i]
|
||
line = self._preprocess_line(raw_line)
|
||
|
||
if not line:
|
||
i += 1
|
||
continue
|
||
|
||
sub_match = re.match(r'o<([^>]+)>\s*sub', line, re.I)
|
||
if sub_match:
|
||
sub_name = sub_match.group(1)
|
||
sub_lines = []
|
||
sub_line_nums = []
|
||
i += 1
|
||
|
||
while i < len(lines):
|
||
raw_sub_line = lines[i]
|
||
sub_line = self._preprocess_line(raw_sub_line)
|
||
|
||
if sub_line and re.match(r'o<[^>]+>\s*endsub', sub_line, re.I):
|
||
break
|
||
|
||
if sub_line:
|
||
sub_lines.append(sub_line)
|
||
sub_line_nums.append(i + 1)
|
||
i += 1
|
||
|
||
self.subroutines[sub_name] = list(zip(sub_line_nums, sub_lines))
|
||
i += 1
|
||
|
||
def _preprocess_line(self, line: str) -> Optional[str]:
|
||
"""移除注释和行号,返回处理后的字符串或 None"""
|
||
if not line:
|
||
return None
|
||
|
||
# 移除行号 N...
|
||
line = re.sub(r'^N\d+\s*', '', line)
|
||
|
||
# 移除分号注释
|
||
if ';' in line:
|
||
line = line.split(';')[0]
|
||
|
||
# 使用状态机正确移除圆括号注释(处理嵌套和复杂表达式)
|
||
result_line = []
|
||
paren_depth = 0
|
||
for char in line:
|
||
if char == '(':
|
||
paren_depth += 1
|
||
elif char == ')':
|
||
if paren_depth > 0:
|
||
paren_depth -= 1
|
||
else:
|
||
if paren_depth == 0:
|
||
result_line.append(char)
|
||
|
||
line = ''.join(result_line)
|
||
|
||
# 移除头尾空白
|
||
line = line.strip()
|
||
|
||
# 如果处理后为空,返回 None
|
||
if not line:
|
||
return None
|
||
|
||
# 转换为大写进行处理
|
||
return line.upper()
|
||
|
||
def _parse_call_params(self, params_str: str) -> List[float]:
|
||
args = []
|
||
if not params_str:
|
||
return args
|
||
|
||
pattern = r'\[([^\]]+)\]'
|
||
matches = re.findall(pattern, params_str)
|
||
|
||
for match in matches:
|
||
try:
|
||
if self.collector:
|
||
value = self.collector.var_manager.evaluate_expression(match, self.call_level)
|
||
else:
|
||
value = float(match)
|
||
args.append(value)
|
||
except Exception:
|
||
args.append(0.0)
|
||
return args
|
||
|
||
|
||
|
||
def expand_named_params(self, expr):
|
||
"""
|
||
展开命名参数
|
||
|
||
将 #<_x> 替换为当前X坐标值
|
||
将 #<_y> 替换为当前Y坐标值
|
||
将 #<_z> 替换为当前Z坐标值
|
||
将 #<_a> 替换为当前A角度值
|
||
将 #<_b> 替换为当前B角度值
|
||
将 #<_c> 替换为当前C角度值
|
||
将 #<r> 替换为子程序参数值
|
||
将 #<dist> 替换为子程序参数值
|
||
|
||
Args:
|
||
expr: 包含命名参数的表达式字符串
|
||
|
||
Returns:
|
||
展开后的表达式字符串
|
||
"""
|
||
if not isinstance(expr, str):
|
||
return expr
|
||
|
||
result = expr
|
||
|
||
# ===== 展开系统变量 #<_x>, #<_y>, #<_z>, #<_a>, #<_b>, #<_c>, #<_w> =====
|
||
if self.collector:
|
||
system_vars = {
|
||
'#<_x>': str(self.collector.current_pos.x),
|
||
'#<_y>': str(self.collector.current_pos.y),
|
||
'#<_z>': str(self.collector.current_pos.z),
|
||
'#<_a>': str(self.collector.current_pos.a),
|
||
'#<_b>': str(self.collector.current_pos.b),
|
||
'#<_c>': str(self.collector.current_pos.c),
|
||
'#<_w>': '0.0', # W轴默认0
|
||
}
|
||
|
||
for var, value in system_vars.items():
|
||
result = result.replace(var, value)
|
||
|
||
# ===== 展开子程序参数 #<zmax>, #<r>, #<dist>, #<frate>, #<n>, #<a>, #<b>, #<c> 等 =====
|
||
if self.collector and hasattr(self.collector, 'var_manager'):
|
||
# 使用参数表系统的 expand_variables 展开剩余变量
|
||
result = self.collector.var_manager.expand_variables(result, self.call_level)
|
||
elif hasattr(self, 'param_stack') and self.param_stack:
|
||
# 备用方案:直接查找子程序参数栈
|
||
current_frame = self.param_stack[-1]
|
||
param_pattern = re.compile(r'#<(\w+)>')
|
||
|
||
def replace_param(match):
|
||
param_name = match.group(1)
|
||
# 跳过系统变量(已经处理过)
|
||
if param_name in ['_x', '_y', '_z', '_a', '_b', '_c', '_w']:
|
||
return match.group(0)
|
||
# 跳过HAL变量
|
||
if param_name.startswith('_hal['):
|
||
return match.group(0)
|
||
# 跳过INI变量
|
||
if param_name.startswith('_ini['):
|
||
return match.group(0)
|
||
# 查找子程序参数
|
||
if param_name in current_frame:
|
||
return str(current_frame[param_name])
|
||
return match.group(0)
|
||
|
||
result = param_pattern.sub(replace_param, result)
|
||
|
||
return result
|
||
|
||
|
||
|
||
def _parse_line(self, line: str, line_num: int) -> Optional[int]:
|
||
"""解析单行G代码 - 完整版,使用参数表系统"""
|
||
if not line or not line.strip():
|
||
return None
|
||
|
||
line_upper = line.upper().strip()
|
||
|
||
# ==================== G10 通用解析函数 ====================
|
||
# ★★★ 定义在此处,供所有 G10 分支 (L1/L10/L11/L2/L20) 共享 ★★★
|
||
def parse_g10_value(axis, expanded_upper):
|
||
"""
|
||
解析 G10 坐标值,支持:
|
||
- X1.234, X=1.234
|
||
- X[#1+#2], X=[#1+#2]
|
||
"""
|
||
patterns = [
|
||
rf'{axis}\s*=\s*([+-]?\d*\.?\d+)',
|
||
rf'{axis}\s*=\s*(\[.+?\])',
|
||
rf'{axis}\s*([+-]?\d*\.?\d+)',
|
||
rf'{axis}\s*(\[.+?\])',
|
||
rf'{axis}([+-]?\d*\.?\d+)',
|
||
]
|
||
for pattern in patterns:
|
||
match = re.search(pattern, expanded_upper, re.I)
|
||
if match:
|
||
val_str = match.group(1)
|
||
try:
|
||
if val_str.startswith('['):
|
||
return self.collector.var_manager.evaluate_expression(val_str, self.call_level)
|
||
else:
|
||
return float(val_str)
|
||
except (ValueError, Exception) as e:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 值解析失败: {axis}={val_str}, 错误: {e}")
|
||
continue
|
||
return None
|
||
|
||
# ==================== 变量赋值(使用参数表) ====================
|
||
|
||
# 数字变量赋值: #1 = 10 或 #1 = [1+2] 或 #1 = #2
|
||
var_match = re.match(r'#(\d+)\s*=\s*(.+)', line, re.I)
|
||
if var_match:
|
||
var_num = int(var_match.group(1))
|
||
value_expr = var_match.group(2).strip()
|
||
if self.collector:
|
||
# 检查只读参数
|
||
if lcnc_param.InterpParameterIndex.is_readonly(var_num):
|
||
if self.debug:
|
||
print(f"[警告] 参数 #{var_num} 是只读的,不能赋值")
|
||
else:
|
||
# 使用参数表系统计算表达式
|
||
value = self.collector.var_manager.evaluate_expression(value_expr, self.call_level)
|
||
if self.debug:
|
||
print(f"[DEBUG] 变量赋值: #{var_num} = {value_expr} -> {value}")
|
||
self.collector.var_manager.set_param(var_num, value, self.call_level > 0)
|
||
return None
|
||
|
||
# 命名变量赋值: #<zmax> = #1 或 #<zmax> = 100 或 #<zmax> = [1+2]
|
||
named_var_match = re.match(r'#<([^>]+)>\s*=\s*(.+)', line, re.I)
|
||
if named_var_match:
|
||
var_name = named_var_match.group(1)
|
||
value_expr = named_var_match.group(2).strip()
|
||
if self.collector:
|
||
# 使用参数表系统计算表达式
|
||
value = self.collector.var_manager.evaluate_expression(value_expr, self.call_level)
|
||
if self.debug:
|
||
print(f"[DEBUG] 变量赋值: #<{var_name}> = {value_expr} -> {value}")
|
||
self.collector.var_manager.set_named(var_name, value)
|
||
return None
|
||
|
||
# ==================== 子程序调用 ====================
|
||
|
||
o_call_match = re.match(r'o<([^>]+)>\s*call\s*(.*)', line, re.I)
|
||
if o_call_match:
|
||
sub_name = o_call_match.group(1)
|
||
params_str = o_call_match.group(2).strip()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] ========== 子程序调用 ==========")
|
||
print(f"[DEBUG] 子程序: {sub_name}")
|
||
print(f"[DEBUG] 原始参数字符串: '{params_str}'")
|
||
|
||
found_sub = None
|
||
for key in self.subroutines.keys():
|
||
if key.lower() == sub_name.lower():
|
||
found_sub = key
|
||
break
|
||
|
||
if not found_sub:
|
||
if self.debug:
|
||
print(f"[DEBUG] 子程序 {sub_name} 未找到,尝试加载文件...")
|
||
self.load_subroutine_file(f"{sub_name}.ngc")
|
||
for key in self.subroutines.keys():
|
||
if key.lower() == sub_name.lower():
|
||
found_sub = key
|
||
break
|
||
|
||
if found_sub:
|
||
if self.debug:
|
||
print(f"[DEBUG] 找到子程序 {found_sub},开始执行...")
|
||
|
||
# 使用参数表系统解析调用参数
|
||
args = self._parse_call_params(params_str)
|
||
if self.debug:
|
||
print(f"[DEBUG] 解析参数: {args}")
|
||
print(f"[DEBUG] 参数个数: {len(args)}")
|
||
|
||
self.call_level += 1
|
||
if self.collector:
|
||
# 推入局部变量帧
|
||
self.collector.var_manager.push_local_frame()
|
||
# 设置子程序参数 #1..#30
|
||
for i, arg in enumerate(args, 1):
|
||
if i <= lcnc_param.INTERP_SUB_PARAMS:
|
||
self.collector.var_manager.set_param(i, arg, self.call_level)
|
||
if self.debug:
|
||
print(f"[DEBUG] 设置参数 #{i} = {arg}")
|
||
|
||
# 设置 n_args 命名参数
|
||
self.collector.var_manager.add_named_param("n_args",
|
||
lcnc_param.ParameterAttribute.PA_READONLY)
|
||
self.collector.var_manager.set_named("n_args", float(len(args)))
|
||
|
||
sub_lines = self.subroutines[found_sub]
|
||
if self.debug:
|
||
print(f"[DEBUG] 子程序 {found_sub} 共 {len(sub_lines)} 行")
|
||
|
||
for sub_line_num, sub_line in sub_lines:
|
||
if self.debug:
|
||
display_line = sub_line[:80] + "..." if len(sub_line) > 80 else sub_line
|
||
print(f"[DEBUG] 执行子程序行 {sub_line_num}: {display_line}")
|
||
self._parse_line(sub_line, sub_line_num)
|
||
|
||
self.call_level -= 1
|
||
if self.collector:
|
||
# 弹出局部变量帧
|
||
self.collector.var_manager.pop_local_frame()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] ========== 子程序 {found_sub} 执行完成 ==========")
|
||
else:
|
||
if self.debug:
|
||
print(f"[DEBUG] 子程序 {sub_name} 未找到!")
|
||
|
||
return None
|
||
|
||
# ==================== 跳转指令 ====================
|
||
|
||
goto_match = re.match(r'goto\s+(\d+)', line, re.I)
|
||
if goto_match:
|
||
target_line = int(goto_match.group(1))
|
||
return target_line
|
||
|
||
n_match = re.match(r'n(\d+)', line, re.I)
|
||
if n_match:
|
||
label = int(n_match.group(1))
|
||
if self.collector and self.collector.control_handler:
|
||
self.collector.control_handler.jump_targets[str(label)] = line_num
|
||
return None
|
||
|
||
# ==================== O代码行 ====================
|
||
|
||
if line_upper.startswith('O'):
|
||
return None
|
||
|
||
# ==================== 创建行代码包装器 ====================
|
||
|
||
line_code = LineCodeWrapper(line_num, self.current_plane)
|
||
if self.collector:
|
||
if re.search(r'\bG53\b', line_upper):
|
||
self.collector._g53_active = True
|
||
self.collector.next_line(line_code)
|
||
|
||
# ==================== 程序结束 ====================
|
||
|
||
if 'M30' in line_upper or 'M02' in line_upper:
|
||
if self.collector:
|
||
self.collector.program_end()
|
||
return None
|
||
|
||
if 'M00' in line_upper:
|
||
if self.collector:
|
||
self.collector.program_stop()
|
||
return None
|
||
|
||
if 'M01' in line_upper:
|
||
if self.collector:
|
||
self.collector.optional_stop()
|
||
return None
|
||
|
||
# ==================== M70/M71/M72/M73 ====================
|
||
|
||
if 'M70' in line_upper:
|
||
if self.collector:
|
||
saved_state = self.collector.save_state(restore_on_return=False)
|
||
self.saved_states.append(saved_state)
|
||
if self.debug:
|
||
print(f"[DEBUG] M70: 保存状态,当前栈深度: {len(self.saved_states)}")
|
||
return None
|
||
|
||
if 'M73' in line_upper:
|
||
if self.collector:
|
||
saved_state = self.collector.save_state(restore_on_return=True)
|
||
self.saved_states.append(saved_state)
|
||
if self.debug:
|
||
print(f"[DEBUG] M73: 保存状态(返回时恢复),当前栈深度: {len(self.saved_states)}")
|
||
return None
|
||
|
||
if 'M72' in line_upper:
|
||
if self.collector and self.saved_states:
|
||
saved_state = self.saved_states.pop()
|
||
self.collector.restore_state(saved_state)
|
||
if self.debug:
|
||
print(f"[DEBUG] M72: 恢复状态,剩余栈深度: {len(self.saved_states)}")
|
||
return None
|
||
|
||
if 'M71' in line_upper:
|
||
if self.saved_states:
|
||
self.saved_states.pop()
|
||
if self.debug:
|
||
print(f"[DEBUG] M71: 丢弃保存的状态,剩余栈深度: {len(self.saved_states)}")
|
||
return None
|
||
|
||
# ==================== RTCP控制 ====================
|
||
|
||
# 在 _parse_line 方法中找到 G49 处理部分,修改为:
|
||
if 'G49' in line_upper:
|
||
if self.collector:
|
||
self.collector.set_rtcp_enable(False)
|
||
self.collector.cancel_tool_length_offset() # ★ 添加此行
|
||
return None
|
||
|
||
if 'G43.4' in line_upper or 'G43.5' in line_upper:
|
||
if self.collector:
|
||
h_match = re.search(r'H\s*=?\s*(\d+)', line_upper, re.I)
|
||
if not h_match:
|
||
h_match = re.search(r'H\s*(\d+)', line_upper, re.I)
|
||
if h_match:
|
||
h_code = int(h_match.group(1))
|
||
self.collector.set_tool_length_offset(h_code)
|
||
self.collector.set_rtcp_enable(True)
|
||
return None
|
||
|
||
# ==================== 运动学切换 ====================
|
||
|
||
if 'M429' in line_upper:
|
||
if self.collector:
|
||
self.collector.set_kinematics_by_type(0)
|
||
if self.debug:
|
||
print(f"[DEBUG] M429: 切换到三轴运动学 (IDENTITY)")
|
||
return None
|
||
|
||
if 'M428' in line_upper:
|
||
if self.collector:
|
||
self.collector.set_kinematics_by_type(1)
|
||
if self.debug:
|
||
print(f"[DEBUG] M428: 恢复到五轴运动学")
|
||
return None
|
||
|
||
if 'M430' in line_upper:
|
||
if self.collector:
|
||
self.collector.set_kinematics_by_type(2)
|
||
if self.debug:
|
||
print(f"[DEBUG] M430: 切换到 FIVEAXIS_BC")
|
||
return None
|
||
|
||
# ==================== G10 坐标系设置(使用参数表) ====================
|
||
|
||
# ==================== G10 错误检查 ====================
|
||
if 'G10' in line_upper:
|
||
# 检查 L 值
|
||
l_match = re.search(r'L\s*=?\s*(\d+)', line_upper, re.I) or re.search(r'L\s*(\d+)', line_upper, re.I)
|
||
l_val = int(l_match.group(1)) if l_match else 0
|
||
|
||
if l_val not in [0, 1, 2, 10, 11, 20]:
|
||
if self.debug:
|
||
print(f"[警告] G10 L{l_val} 不支持,支持的 L 值: 0, 1, 2, 10, 11, 20")
|
||
return None
|
||
|
||
# 检查 P 值是否为整数
|
||
p_match = re.search(r'P\s*=?\s*([+-]?\d*\.?\d+)', line_upper, re.I) or re.search(r'P\s*([+-]?\d*\.?\d+)', line_upper, re.I)
|
||
p_val = float(p_match.group(1)) if p_match else -1
|
||
p_int = round(p_val)
|
||
|
||
if abs(p_val - p_int) > 0.0002:
|
||
if self.debug:
|
||
print(f"[警告] G10 P 值 {p_val} 不是整数")
|
||
return None
|
||
|
||
# L2/L20: P 值范围 0-9
|
||
if l_val in [2, 20] and (p_int < 0 or p_int > 9):
|
||
if self.debug:
|
||
print(f"[警告] G10 L{l_val} P 值 {p_int} 超出范围 (0-9)")
|
||
return None
|
||
|
||
# L1/L10/L11: P 值必须 >= 1
|
||
if l_val in [1, 10, 11] and p_int < 1:
|
||
if self.debug:
|
||
print(f"[警告] G10 L{l_val} P 值 {p_int} 必须 >= 1")
|
||
return None
|
||
|
||
# L1: 必须有至少一个偏移值
|
||
if l_val == 1:
|
||
has_offset = False
|
||
for axis in ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'R', 'I', 'J', 'Q']:
|
||
if re.search(rf'{axis}\s*[=]?\s*[\d\[\.]', line_upper, re.I):
|
||
has_offset = True
|
||
break
|
||
if not has_offset:
|
||
if self.debug:
|
||
print(f"[警告] G10 L1 没有任何偏移值")
|
||
|
||
# ==================== G10 L20 坐标系设置 ====================
|
||
|
||
# G10 L20 Pn X_ Y_ Z_ A_ B_ C_ - 必须最先检测,避免被 L2 分支拦截
|
||
if 'G10' in line_upper and 'L20' in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L20 原始行: '{line}'")
|
||
print(f"[DEBUG] G10 L20 展开后: '{expanded_line}'")
|
||
|
||
p_match_g10 = re.search(r'P\s*=?\s*(\d+)', expanded_upper) or re.search(r'P\s*(\d+)', expanded_upper)
|
||
if p_match_g10:
|
||
p_val = int(p_match_g10.group(1))
|
||
|
||
# 构建参数字符串,使用表达式求值
|
||
params_parts = []
|
||
for axis in ['X', 'Y', 'Z', 'A', 'B', 'C']:
|
||
val = parse_g10_value(axis, expanded_upper)
|
||
if val is not None:
|
||
params_parts.append(f"{axis}{val}")
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L20 解析: {axis}={val}")
|
||
|
||
params_str = ' '.join(params_parts)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L20 P{p_val}: params='{params_str}'")
|
||
|
||
self.collector.set_coordinate_system_g10_l20(p_val, params_str)
|
||
return None
|
||
|
||
# ==================== G10 L2 坐标系设置 ====================
|
||
|
||
# G10 L2 Pn X_ Y_ Z_ A_ B_ C_ - 必须在 L20 之后,且排除 L20
|
||
if 'G10' in line_upper and 'L2' in line_upper and 'L20' not in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L2 原始行: '{line}'")
|
||
print(f"[DEBUG] G10 L2 展开后: '{expanded_line}'")
|
||
|
||
p_match_g10 = re.search(r'P\s*=?\s*(\d+)', expanded_upper) or re.search(r'P\s*(\d+)', expanded_upper)
|
||
if p_match_g10:
|
||
p_val = int(p_match_g10.group(1))
|
||
|
||
x = y = z = a = b = c = u = v = w = 0.0
|
||
|
||
x_val = parse_g10_value('X', expanded_upper)
|
||
y_val = parse_g10_value('Y', expanded_upper)
|
||
z_val = parse_g10_value('Z', expanded_upper)
|
||
a_val = parse_g10_value('A', expanded_upper)
|
||
b_val = parse_g10_value('B', expanded_upper)
|
||
c_val = parse_g10_value('C', expanded_upper)
|
||
|
||
if x_val is not None: x = x_val
|
||
if y_val is not None: y = y_val
|
||
if z_val is not None: z = z_val
|
||
if a_val is not None: a = a_val
|
||
if b_val is not None: b = b_val
|
||
if c_val is not None: c = c_val
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L2 P{p_val}: X={x} Y={y} Z={z} A={a} B={b} C={c}")
|
||
|
||
self.collector.set_g5x_offset(p_val, x, y, z, a, b, c, u, v, w)
|
||
return None
|
||
|
||
# ==================== G10 L1 刀具偏移设置 ====================
|
||
|
||
if 'G10' in line_upper and 'L1' in line_upper and 'L10' not in line_upper and 'L11' not in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] ========== G10 L1 刀具偏移设置 ==========")
|
||
print(f"[DEBUG] 行号: {line_num}")
|
||
print(f"[DEBUG] 原始行: '{line.strip()}'")
|
||
print(f"[DEBUG] 展开后: '{expanded_line}'")
|
||
|
||
# 解析 P 值(刀具号)
|
||
p_match = re.search(r'P\s*=?\s*(\d+)', expanded_upper) or re.search(r'P\s*(\d+)', expanded_upper)
|
||
if not p_match:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L1 缺少 P 值")
|
||
return None
|
||
|
||
tool_number = int(p_match.group(1))
|
||
|
||
# ★ 使用公共 parse_g10_value 函数 ★
|
||
x_val = parse_g10_value('X', expanded_upper)
|
||
y_val = parse_g10_value('Y', expanded_upper)
|
||
z_val = parse_g10_value('Z', expanded_upper)
|
||
a_val = parse_g10_value('A', expanded_upper)
|
||
b_val = parse_g10_value('B', expanded_upper)
|
||
c_val = parse_g10_value('C', expanded_upper)
|
||
u_val = parse_g10_value('U', expanded_upper)
|
||
v_val = parse_g10_value('V', expanded_upper)
|
||
w_val = parse_g10_value('W', expanded_upper)
|
||
|
||
# 刀具参数
|
||
r_val = parse_g10_value('R', expanded_upper)
|
||
i_val = parse_g10_value('I', expanded_upper)
|
||
j_val = parse_g10_value('J', expanded_upper)
|
||
|
||
# Q 值
|
||
q_match = re.search(r'Q\s*=?\s*(\d+)', expanded_upper) or re.search(r'Q\s*(\d+)', expanded_upper)
|
||
orientation = int(q_match.group(1)) if q_match else None
|
||
|
||
# 检查是否有任何偏移值
|
||
has_any_offset = any(v is not None for v in [x_val, y_val, z_val, a_val, b_val, c_val, u_val, v_val, w_val, r_val, i_val, j_val, q_match])
|
||
if not has_any_offset:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L1 没有任何偏移值,跳过")
|
||
return None
|
||
|
||
# 查找或创建刀具
|
||
tool_idx = self.collector.find_or_create_tool(tool_number)
|
||
if tool_idx < 0:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L1 无法创建刀具 T{tool_number}")
|
||
return None
|
||
|
||
tool = self.collector.tool_table[tool_idx]
|
||
changed = []
|
||
|
||
# 更新偏移
|
||
if x_val is not None:
|
||
tool.offset.x = x_val
|
||
changed.append(f'X={x_val:.3f}')
|
||
if y_val is not None:
|
||
tool.offset.y = y_val
|
||
changed.append(f'Y={y_val:.3f}')
|
||
if z_val is not None:
|
||
tool.offset.z = z_val
|
||
self.collector.tool_lengths[tool_number] = z_val
|
||
changed.append(f'Z={z_val:.3f}')
|
||
if a_val is not None:
|
||
tool.offset.a = a_val
|
||
changed.append(f'A={a_val:.3f}')
|
||
if b_val is not None:
|
||
tool.offset.b = b_val
|
||
changed.append(f'B={b_val:.3f}')
|
||
if c_val is not None:
|
||
tool.offset.c = c_val
|
||
changed.append(f'C={c_val:.3f}')
|
||
if u_val is not None:
|
||
tool.offset.u = u_val
|
||
changed.append(f'U={u_val:.3f}')
|
||
if v_val is not None:
|
||
tool.offset.v = v_val
|
||
changed.append(f'V={v_val:.3f}')
|
||
if w_val is not None:
|
||
tool.offset.w = w_val
|
||
changed.append(f'W={w_val:.3f}')
|
||
|
||
# 更新刀具参数
|
||
if r_val is not None:
|
||
tool.diameter = r_val
|
||
tool.radius = r_val / 2.0
|
||
self.collector.tool_radii[tool_number] = tool.radius
|
||
changed.append(f'D={r_val:.3f}')
|
||
if i_val is not None:
|
||
tool.front_angle = i_val
|
||
changed.append(f'I={i_val:.3f}')
|
||
if j_val is not None:
|
||
tool.back_angle = j_val
|
||
changed.append(f'J={j_val:.3f}')
|
||
if orientation is not None:
|
||
tool.orientation = orientation
|
||
changed.append(f'Q={orientation}')
|
||
|
||
# 同步到参数表
|
||
self.collector._sync_tool_parameters_to_table(tool_idx)
|
||
|
||
# 如果是当前刀具,更新补偿
|
||
self.collector._handle_g10_current_tool_update(tool_idx, tool_number)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L1 T{tool_number}: 修改 {', '.join(changed)}")
|
||
|
||
return None
|
||
|
||
# ==================== G10 L10 刀具偏移设置(工件坐标系基准) ====================
|
||
|
||
if 'G10' in line_upper and 'L10' in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] ========== G10 L10 刀具偏移设置(工件坐标系) ==========")
|
||
print(f"[DEBUG] 行号: {line_num}")
|
||
print(f"[DEBUG] 原始行: '{line.strip()}'")
|
||
print(f"[DEBUG] 展开后: '{expanded_line}'")
|
||
|
||
p_match = re.search(r'P\s*=?\s*(\d+)', expanded_upper) or re.search(r'P\s*(\d+)', expanded_upper)
|
||
if not p_match:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L10 缺少 P 值")
|
||
return None
|
||
|
||
tool_number = int(p_match.group(1))
|
||
|
||
destination_system = self.collector.g5x_index
|
||
tx, ty, tz, ta, tb, tc, tu, tv, tw = self.collector._get_current_in_system_without_tlo(destination_system)
|
||
dest_rotation = self.collector._get_csys_rotation(destination_system)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L10: 目标坐标系=G{53 + destination_system}, 旋转={dest_rotation}°")
|
||
print(f"[DEBUG] G10 L10: 当前位置(no TLO)=({tx:.3f},{ty:.3f},{tz:.3f})")
|
||
|
||
# ★ 使用公共 parse_g10_value 函数 ★
|
||
x_val = parse_g10_value('X', expanded_upper)
|
||
y_val = parse_g10_value('Y', expanded_upper)
|
||
z_val = parse_g10_value('Z', expanded_upper)
|
||
a_val = parse_g10_value('A', expanded_upper)
|
||
b_val = parse_g10_value('B', expanded_upper)
|
||
c_val = parse_g10_value('C', expanded_upper)
|
||
u_val = parse_g10_value('U', expanded_upper)
|
||
v_val = parse_g10_value('V', expanded_upper)
|
||
w_val = parse_g10_value('W', expanded_upper)
|
||
|
||
r_val = parse_g10_value('R', expanded_upper)
|
||
i_val = parse_g10_value('I', expanded_upper)
|
||
j_val = parse_g10_value('J', expanded_upper)
|
||
q_match = re.search(r'Q\s*=?\s*(\d+)', expanded_upper) or re.search(r'Q\s*(\d+)', expanded_upper)
|
||
orientation = int(q_match.group(1)) if q_match else None
|
||
|
||
tool_idx = self.collector.find_or_create_tool(tool_number)
|
||
if tool_idx < 0:
|
||
return None
|
||
|
||
tool = self.collector.tool_table[tool_idx]
|
||
changed = []
|
||
|
||
# X/Y 轴
|
||
if x_val is not None and y_val is not None:
|
||
dx = tx - x_val
|
||
dy = ty - y_val
|
||
rot_rad = math.radians(dest_rotation)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
tool.offset.x = dx * cos_r + dy * sin_r
|
||
tool.offset.y = -dx * sin_r + dy * cos_r
|
||
changed.append(f'X={tool.offset.x:.3f} Y={tool.offset.y:.3f}')
|
||
|
||
# Z 轴
|
||
if z_val is not None:
|
||
tool.offset.z = tz - z_val
|
||
self.collector.tool_lengths[tool_number] = tool.offset.z
|
||
changed.append(f'Z={tool.offset.z:.3f}')
|
||
|
||
# 旋转轴
|
||
if a_val is not None:
|
||
tool.offset.a = ta - a_val
|
||
changed.append(f'A={tool.offset.a:.3f}')
|
||
if b_val is not None:
|
||
tool.offset.b = tb - b_val
|
||
changed.append(f'B={tool.offset.b:.3f}')
|
||
if c_val is not None:
|
||
tool.offset.c = tc - c_val
|
||
changed.append(f'C={tool.offset.c:.3f}')
|
||
|
||
# U/V/W
|
||
if u_val is not None:
|
||
tool.offset.u = tu - u_val
|
||
if v_val is not None:
|
||
tool.offset.v = tv - v_val
|
||
if w_val is not None:
|
||
tool.offset.w = tw - w_val
|
||
|
||
# 刀具参数
|
||
if r_val is not None:
|
||
tool.diameter = r_val
|
||
tool.radius = r_val / 2.0
|
||
self.collector.tool_radii[tool_number] = tool.radius
|
||
if i_val is not None:
|
||
tool.front_angle = i_val
|
||
if j_val is not None:
|
||
tool.back_angle = j_val
|
||
if orientation is not None:
|
||
tool.orientation = orientation
|
||
|
||
# 同步
|
||
self.collector._sync_tool_parameters_to_table(tool_idx)
|
||
self.collector._handle_g10_current_tool_update(tool_idx, tool_number)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L10 T{tool_number}: 修改 {', '.join(changed)}")
|
||
|
||
return None
|
||
|
||
# ==================== G10 L11 刀具偏移设置(夹具坐标系基准) ====================
|
||
|
||
if 'G10' in line_upper and 'L11' in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] ========== G10 L11 刀具偏移设置(夹具坐标系) ==========")
|
||
print(f"[DEBUG] 行号: {line_num}")
|
||
print(f"[DEBUG] 原始行: '{line.strip()}'")
|
||
print(f"[DEBUG] 展开后: '{expanded_line}'")
|
||
|
||
p_match = re.search(r'P\s*=?\s*(\d+)', expanded_upper) or re.search(r'P\s*(\d+)', expanded_upper)
|
||
if not p_match:
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L11 缺少 P 值")
|
||
return None
|
||
|
||
tool_number = int(p_match.group(1))
|
||
|
||
fixture_system = 9 # G59.3
|
||
tx, ty, tz, ta, tb, tc, tu, tv, tw = self.collector._get_current_in_system_without_tlo(fixture_system)
|
||
fixture_rotation = self.collector._get_csys_rotation(fixture_system)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L11: 夹具坐标系 G59.3, 旋转={fixture_rotation}°")
|
||
print(f"[DEBUG] G10 L11: 当前位置(no TLO)=({tx:.3f},{ty:.3f},{tz:.3f})")
|
||
|
||
# ★ 使用公共 parse_g10_value 函数 ★
|
||
x_val = parse_g10_value('X', expanded_upper)
|
||
y_val = parse_g10_value('Y', expanded_upper)
|
||
z_val = parse_g10_value('Z', expanded_upper)
|
||
a_val = parse_g10_value('A', expanded_upper)
|
||
b_val = parse_g10_value('B', expanded_upper)
|
||
c_val = parse_g10_value('C', expanded_upper)
|
||
u_val = parse_g10_value('U', expanded_upper)
|
||
v_val = parse_g10_value('V', expanded_upper)
|
||
w_val = parse_g10_value('W', expanded_upper)
|
||
|
||
r_val = parse_g10_value('R', expanded_upper)
|
||
i_val = parse_g10_value('I', expanded_upper)
|
||
j_val = parse_g10_value('J', expanded_upper)
|
||
q_match = re.search(r'Q\s*=?\s*(\d+)', expanded_upper) or re.search(r'Q\s*(\d+)', expanded_upper)
|
||
orientation = int(q_match.group(1)) if q_match else None
|
||
|
||
tool_idx = self.collector.find_or_create_tool(tool_number)
|
||
if tool_idx < 0:
|
||
return None
|
||
|
||
tool = self.collector.tool_table[tool_idx]
|
||
changed = []
|
||
|
||
# X/Y 轴
|
||
if x_val is not None and y_val is not None:
|
||
dx = tx - x_val
|
||
dy = ty - y_val
|
||
rot_rad = math.radians(fixture_rotation)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
tool.offset.x = dx * cos_r + dy * sin_r
|
||
tool.offset.y = -dx * sin_r + dy * cos_r
|
||
changed.append(f'X={tool.offset.x:.3f} Y={tool.offset.y:.3f}')
|
||
|
||
# Z 轴
|
||
if z_val is not None:
|
||
tool.offset.z = tz - z_val
|
||
self.collector.tool_lengths[tool_number] = tool.offset.z
|
||
changed.append(f'Z={tool.offset.z:.3f}')
|
||
|
||
# 旋转轴
|
||
if a_val is not None:
|
||
tool.offset.a = ta - a_val
|
||
if b_val is not None:
|
||
tool.offset.b = tb - b_val
|
||
if c_val is not None:
|
||
tool.offset.c = tc - c_val
|
||
|
||
# U/V/W
|
||
if u_val is not None:
|
||
tool.offset.u = tu - u_val
|
||
if v_val is not None:
|
||
tool.offset.v = tv - v_val
|
||
if w_val is not None:
|
||
tool.offset.w = tw - w_val
|
||
|
||
# 刀具参数
|
||
if r_val is not None:
|
||
tool.diameter = r_val
|
||
tool.radius = r_val / 2.0
|
||
self.collector.tool_radii[tool_number] = tool.radius
|
||
if i_val is not None:
|
||
tool.front_angle = i_val
|
||
if j_val is not None:
|
||
tool.back_angle = j_val
|
||
if orientation is not None:
|
||
tool.orientation = orientation
|
||
|
||
# 同步
|
||
self.collector._sync_tool_parameters_to_table(tool_idx)
|
||
self.collector._handle_g10_current_tool_update(tool_idx, tool_number)
|
||
|
||
if self.debug:
|
||
print(f"[DEBUG] G10 L11 T{tool_number}: 修改 {', '.join(changed)}")
|
||
|
||
return None
|
||
|
||
# ==================== G28.1/G30.1 参考点设置(使用参数表) ====================
|
||
|
||
if 'G28.1' in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
x = y = z = a = b = c = 0.0
|
||
|
||
def parse_coord_simple(ax):
|
||
patterns = [
|
||
rf'{ax}\s*=\s*([+-]?\d*\.?\d+)',
|
||
rf'{ax}\s*([+-]?\d*\.?\d+)',
|
||
rf'{ax}([+-]?\d*\.?\d+)',
|
||
]
|
||
for pattern in patterns:
|
||
match = re.search(pattern, expanded_upper, re.I)
|
||
if match:
|
||
try:
|
||
return float(match.group(1))
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
|
||
x_val = parse_coord_simple('X')
|
||
y_val = parse_coord_simple('Y')
|
||
z_val = parse_coord_simple('Z')
|
||
|
||
if x_val is not None: x = x_val
|
||
if y_val is not None: y = y_val
|
||
if z_val is not None: z = z_val
|
||
|
||
self.collector.set_g28(x, y, z, a, b, c)
|
||
return None
|
||
|
||
if 'G30.1' in line_upper and self.collector:
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
x = y = z = a = b = c = 0.0
|
||
|
||
x_val = parse_coord_simple('X')
|
||
y_val = parse_coord_simple('Y')
|
||
z_val = parse_coord_simple('Z')
|
||
|
||
if x_val is not None: x = x_val
|
||
if y_val is not None: y = y_val
|
||
if z_val is not None: z = z_val
|
||
|
||
self.collector.set_g30(x, y, z, a, b, c)
|
||
return None
|
||
|
||
# ==================== 解析坐标(使用参数表展开变量) ====================
|
||
|
||
x = y = z = a = b = c = u = v = w = None
|
||
i = j = k = r_coord = None
|
||
|
||
if self.collector:
|
||
|
||
current = self.collector.current_pos
|
||
|
||
# 使用参数表展开变量
|
||
expanded_line = self.collector.var_manager.expand_variables(line, self.call_level)
|
||
expanded_upper = expanded_line.upper()
|
||
|
||
is_g0 = (re.search(r'\bG0\b', expanded_upper) or re.search(r'\bG00\b', expanded_upper) or
|
||
expanded_upper.startswith('G0') or expanded_upper.startswith('G00'))
|
||
is_g1 = (re.search(r'\bG1\b', expanded_upper) or re.search(r'\bG01\b', expanded_upper) or
|
||
expanded_upper.startswith('G1') or expanded_upper.startswith('G01'))
|
||
is_g2 = ('G2' in expanded_upper or 'G02' in expanded_upper) and not any(cmd in expanded_upper for cmd in ['G20', 'G21', 'G28', 'G92'])
|
||
is_g3 = ('G3' in expanded_upper or 'G03' in expanded_upper) and not any(cmd in expanded_upper for cmd in ['G30', 'G38'])
|
||
is_motion_line = is_g0 or is_g1 or is_g2 or is_g3
|
||
|
||
if self.debug and is_motion_line:
|
||
print(f"[DEBUG] ========== 运动指令解析 ==========")
|
||
print(f"[DEBUG] 行号: {line_num}")
|
||
print(f"[DEBUG] 原始行: '{line.strip()}'")
|
||
print(f"[DEBUG] 展开后: '{expanded_line}'")
|
||
print(f"[DEBUG] expanded_upper: '{expanded_upper}'")
|
||
print(f"[DEBUG] call_level: {self.call_level}")
|
||
print(f"[DEBUG] 当前坐标: X={current.x:.3f}, Y={current.y:.3f}, Z={current.z:.3f}")
|
||
print(f"[DEBUG] RTCP状态: {self.collector.rtcp_enabled}")
|
||
print(f"[DEBUG] is_g0={is_g0}, is_g1={is_g1}, is_g2={is_g2}, is_g3={is_g3}")
|
||
|
||
# ===== 修复后的 parse_coord 函数 =====
|
||
def parse_coord(axis):
|
||
"""
|
||
解析坐标值
|
||
支持格式:
|
||
- 纯数字: X20, Y-10.5, Z+5
|
||
- 等号格式: X=20, Y=-10.5
|
||
- 方括号表达式: X[#<_x> + #<r>], Y[20 - 10]
|
||
|
||
参数:
|
||
axis: 轴名称 ('X', 'Y', 'Z', 'A', 'B', 'C', 'I', 'J', 'K', 'R', 'F', 'S', 'P')
|
||
|
||
返回:
|
||
解析后的浮点数值,如果未找到则返回 None
|
||
"""
|
||
nonlocal_expanded = expanded_upper
|
||
|
||
# ===== 先尝试匹配方括号表达式 =====
|
||
bracket_pattern = rf'{axis}\[(.*?)\]'
|
||
bracket_match = re.search(bracket_pattern, nonlocal_expanded, re.IGNORECASE)
|
||
if bracket_match:
|
||
expr = bracket_match.group(1)
|
||
try:
|
||
# 展开命名参数
|
||
expanded_expr = self.expand_named_params(expr)
|
||
# 计算表达式
|
||
if self.collector and hasattr(self.collector, 'var_manager'):
|
||
result = self.collector.var_manager.evaluate_expression(expanded_expr, self.call_level)
|
||
else:
|
||
# 简单eval后备
|
||
result = eval(expanded_expr, {"__builtins__": {}}, {"math": math})
|
||
if self.debug:
|
||
print(f"[DEBUG] parse_coord({axis}): 模式='BRACKET EXPR', "
|
||
f"原始表达式='{expr}', 展开='{expanded_expr}', 解析值={result}")
|
||
return float(result)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[DEBUG] parse_coord({axis}): 方括号求值失败 '{expr}': {e}")
|
||
return None
|
||
|
||
# ===== 再匹配带等号的数字: X=20, Y=-10.5 =====
|
||
eq_pattern = rf'{axis}\s*=\s*([+\-]?\d+\.?\d*)'
|
||
eq_match = re.search(eq_pattern, nonlocal_expanded, re.IGNORECASE)
|
||
if eq_match:
|
||
value = float(eq_match.group(1))
|
||
if self.debug:
|
||
print(f"[DEBUG] parse_coord({axis}): 模式='EQ VALUE', "
|
||
f"原始匹配='{eq_match.group(1)}', 解析值={value}")
|
||
return value
|
||
|
||
# ===== 再匹配普通数字: X20, Y-10.5, Z+5 =====
|
||
number_pattern = rf'{axis}([+\-]?\d+\.?\d*)'
|
||
number_match = re.search(number_pattern, nonlocal_expanded, re.IGNORECASE)
|
||
if number_match:
|
||
value = float(number_match.group(1))
|
||
if self.debug:
|
||
print(f"[DEBUG] parse_coord({axis}): 模式='AXIS VALUE', "
|
||
f"原始匹配='{number_match.group(1)}', 解析值={value}")
|
||
return value
|
||
|
||
return None
|
||
|
||
# 解析所有坐标。保留原始轴字,后续 G92/G52 需要区分
|
||
# “未指定该轴”和“指定为 0”两种情况。
|
||
x_word = parse_coord('X')
|
||
y_word = parse_coord('Y')
|
||
z_word = parse_coord('Z')
|
||
a_word = parse_coord('A')
|
||
b_word = parse_coord('B')
|
||
c_word = parse_coord('C')
|
||
u_word = parse_coord('U')
|
||
v_word = parse_coord('V')
|
||
w_word = parse_coord('W')
|
||
|
||
x = x_word
|
||
y = y_word
|
||
z = z_word
|
||
a = a_word
|
||
b = b_word
|
||
c = c_word
|
||
u = u_word
|
||
v = v_word
|
||
w = w_word
|
||
|
||
i = parse_coord('I')
|
||
j = parse_coord('J')
|
||
k = parse_coord('K')
|
||
r_coord = parse_coord('R')
|
||
|
||
|
||
# 进给率
|
||
f_match = re.search(r'F\s*=?\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
if not f_match:
|
||
f_match = re.search(r'F\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
if f_match:
|
||
feed_val = float(f_match.group(1))
|
||
self.collector.set_feed_rate(feed_val)
|
||
if self.debug:
|
||
print(f"[DEBUG] 设置进给率: F={feed_val}")
|
||
|
||
# 主轴速度
|
||
s_match = re.search(r'S\s*=?\s*(\d+)', expanded_upper, re.I)
|
||
if not s_match:
|
||
s_match = re.search(r'S\s*(\d+)', expanded_upper, re.I)
|
||
if s_match:
|
||
self.collector.set_spindle_speed(float(s_match.group(1)))
|
||
if self.debug:
|
||
print(f"[DEBUG] 设置主轴速度: S={s_match.group(1)}")
|
||
|
||
# 暂停参数
|
||
p_dwell_match = re.search(r'P\s*=?\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
if not p_dwell_match:
|
||
p_dwell_match = re.search(r'P\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
|
||
# 圆弧圈数参数 (P)
|
||
p_turn_match = re.search(r'P\s*=?\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
if not p_turn_match:
|
||
p_turn_match = re.search(r'P\s*([+-]?\d*\.?\d+)', expanded_upper, re.I)
|
||
p_turn = float(p_turn_match.group(1)) if p_turn_match else 1.0
|
||
|
||
# 计算绝对/相对坐标
|
||
if not self.collector.is_absolute:
|
||
x = current.x + x if x is not None else current.x
|
||
y = current.y + y if y is not None else current.y
|
||
z = current.z + z if z is not None else current.z
|
||
a = current.a + (a if a is not None else 0)
|
||
b = current.b + (b if b is not None else 0)
|
||
c = current.c + (c if c is not None else 0)
|
||
u = getattr(current, 'u', 0.0) + (u if u is not None else 0)
|
||
v = getattr(current, 'v', 0.0) + (v if v is not None else 0)
|
||
w = getattr(current, 'w', 0.0) + (w if w is not None else 0)
|
||
else:
|
||
x = x if x is not None else current.x
|
||
y = y if y is not None else current.y
|
||
z = z if z is not None else current.z
|
||
a = a if a is not None else current.a
|
||
b = b if b is not None else current.b
|
||
c = c if c is not None else current.c
|
||
u = u if u is not None else getattr(current, 'u', 0.0)
|
||
v = v if v is not None else getattr(current, 'v', 0.0)
|
||
w = w if w is not None else getattr(current, 'w', 0.0)
|
||
|
||
# ==================== G代码运动 ====================
|
||
if self.collector:
|
||
# 刀具补偿
|
||
h_match = re.search(r'H\s*=?\s*(\d+)', line_upper, re.I)
|
||
if not h_match:
|
||
h_match = re.search(r'H\s*(\d+)', line_upper, re.I)
|
||
if h_match:
|
||
h_code = int(h_match.group(1))
|
||
self.collector.set_tool_length_offset(h_code)
|
||
|
||
d_match = re.search(r'D\s*=?\s*(\d+)', line_upper, re.I)
|
||
if not d_match:
|
||
d_match = re.search(r'D\s*(\d+)', line_upper, re.I)
|
||
if d_match:
|
||
self.collector.set_tool_radius_offset(int(d_match.group(1)))
|
||
|
||
t_match = re.search(r'T\s*=?\s*(\d+)', line_upper, re.I)
|
||
if not t_match:
|
||
t_match = re.search(r'T\s*(\d+)', line_upper, re.I)
|
||
if t_match:
|
||
self.collector.select_tool(int(t_match.group(1)))
|
||
|
||
# 换刀
|
||
if 'M6' in line_upper:
|
||
tool = int(t_match.group(1)) if t_match else 1
|
||
self.collector.change_tool(tool)
|
||
|
||
# 主轴控制
|
||
if 'M3' in line_upper:
|
||
self.collector.spindle_control(1)
|
||
elif 'M4' in line_upper:
|
||
self.collector.spindle_control(2)
|
||
elif 'M5' in line_upper:
|
||
self.collector.spindle_control(0)
|
||
|
||
# 冷却液控制
|
||
if 'M8' in line_upper:
|
||
self.collector.set_flood(True)
|
||
elif 'M7' in line_upper:
|
||
self.collector.set_mist(True)
|
||
elif 'M9' in line_upper:
|
||
self.collector.set_flood(False)
|
||
self.collector.set_mist(False)
|
||
|
||
# M66 等待输入
|
||
if 'M66' in line_upper:
|
||
e_match = re.search(r'E\s*=?\s*(\d+)', line_upper) or re.search(r'E\s*(\d+)', line_upper)
|
||
l_match = re.search(r'L\s*=?\s*(\d+)', line_upper) or re.search(r'L\s*(\d+)', line_upper)
|
||
if e_match:
|
||
pin = int(e_match.group(1))
|
||
mode = int(l_match.group(1)) if l_match else 0
|
||
self.collector.wait_input(pin, mode)
|
||
|
||
# M68 模拟量输出
|
||
if 'M68' in line_upper:
|
||
e_match = re.search(r'E\s*=?\s*(\d+)', line_upper) or re.search(r'E\s*(\d+)', line_upper)
|
||
q_match = re.search(r'Q\s*=?\s*([+-]?\d*\.?\d+)', line_upper) or re.search(r'Q\s*([+-]?\d*\.?\d+)', line_upper)
|
||
if e_match and q_match:
|
||
pin = int(e_match.group(1))
|
||
value = float(q_match.group(1))
|
||
self.collector.set_analog_output(pin, value)
|
||
|
||
current = self.collector.current_pos
|
||
|
||
# G0 - 快速移动
|
||
if is_g0:
|
||
if x is not None or y is not None or z is not None:
|
||
if self.debug:
|
||
print(f"[G0] 执行快速移动: X={x}, Y={y}, Z={z}, A={a}, B={b}, C={c}")
|
||
self.collector.straight_traverse(
|
||
x if x is not None else current.x,
|
||
y if y is not None else current.y,
|
||
z if z is not None else current.z,
|
||
a if a is not None else current.a,
|
||
b if b is not None else current.b,
|
||
c if c is not None else current.c,
|
||
u if u is not None else 0,
|
||
v if v is not None else 0,
|
||
w if w is not None else 0
|
||
)
|
||
|
||
# G1 - 直线进给
|
||
elif is_g1:
|
||
if x is not None or y is not None or z is not None:
|
||
if self.debug:
|
||
print(f"[G1] 执行直线进给: X={x}, Y={y}, Z={z}")
|
||
self.collector.straight_feed(
|
||
x if x is not None else current.x,
|
||
y if y is not None else current.y,
|
||
z if z is not None else current.z,
|
||
a if a is not None else current.a,
|
||
b if b is not None else current.b,
|
||
c if c is not None else current.c,
|
||
u if u is not None else 0,
|
||
v if v is not None else 0,
|
||
w if w is not None else 0
|
||
)
|
||
|
||
# G2 - 顺时针圆弧
|
||
|
||
|
||
# G2 - 顺时针圆弧
|
||
elif is_g2:
|
||
turn = -int(p_turn) if p_turn >= 1 else -1
|
||
|
||
# 如果只指定IJK没指定X/Y,终点=起点(全圆)
|
||
if x is None and y is None:
|
||
if i is not None or j is not None:
|
||
x = current.x
|
||
y = current.y
|
||
|
||
if x is not None or y is not None:
|
||
x = x if x is not None else current.x
|
||
y = y if y is not None else current.y
|
||
|
||
# ===== R格式圆弧:计算圆心 =====
|
||
if r_coord is not None and (i is None or j is None):
|
||
dx = x - current.x
|
||
dy = y - current.y
|
||
d = math.sqrt(dx*dx + dy*dy)
|
||
if d > 0 and abs(r_coord) >= d/2 - 1e-9:
|
||
h = math.sqrt(max(0, r_coord*r_coord - d*d/4))
|
||
# G2 (turn < 0)
|
||
if r_coord > 0:
|
||
# R>0: 小弧(<180°)
|
||
cx_calc = current.x + dx/2 - h * dy/d
|
||
cy_calc = current.y + dy/2 + h * dx/d
|
||
else:
|
||
# R<0: 大弧(>180°)
|
||
cx_calc = current.x + dx/2 + h * dy/d
|
||
cy_calc = current.y + dy/2 - h * dx/d
|
||
|
||
|
||
|
||
# 转换为增量IJ
|
||
i = cx_calc - current.x
|
||
j = cy_calc - current.y
|
||
else:
|
||
# 半圆或无效R
|
||
i = (x - current.x) / 2
|
||
j = (y - current.y) / 2
|
||
|
||
# ===== 确保IJ是增量(相对于起点) =====
|
||
# ===== IJK 处理逻辑(对照 LinuxCNC interp_convert.cc arc_data_ijk)=====
|
||
# 在 G91.1(增量模式)下,I, J 被视为相对于起点
|
||
# 在 G90.1(绝对模式)下,I, J 被视为绝对坐标
|
||
i_final = i if i is not None else 0.0
|
||
j_final = j if j is not None else 0.0
|
||
k_final = k if k is not None else 0.0
|
||
|
||
# 如果是绝对 IJK 模式 (G90.1),转换为增量
|
||
if self.collector and self.collector.ijk_distance_mode == 90: # 90 = G90.1 绝对IJK
|
||
i_final = i_final - current.x
|
||
j_final = j_final - current.y
|
||
k_final = k_final - current.z
|
||
# 在增量模式下(G91.1,默认),IJK 已经是相对于起点的值,无需转换
|
||
|
||
if self.debug:
|
||
center_x_dbg = current.x + i_final
|
||
center_y_dbg = current.y + j_final
|
||
print(f"[G2] 执行圆弧: end=({x},{y}) "
|
||
f"center=({center_x_dbg:.1f},{center_y_dbg:.1f}) "
|
||
f"I={i_final:.3f} J={j_final:.3f} turn={turn}")
|
||
|
||
self.collector.arc_feed(
|
||
x, y,
|
||
z if z is not None else current.z,
|
||
a if a is not None else current.a,
|
||
b if b is not None else current.b,
|
||
c if c is not None else current.c,
|
||
i_final if i_final != 0 else 0,
|
||
j_final if j_final != 0 else 0,
|
||
k if k is not None else 0,
|
||
turn, self.collector.feedrate,
|
||
u if u is not None else 0,
|
||
v if v is not None else 0,
|
||
w if w is not None else 0
|
||
)
|
||
|
||
# G3 - 逆时针圆弧
|
||
|
||
# G3 - 逆时针圆弧
|
||
elif is_g3:
|
||
turn = int(p_turn) if p_turn >= 1 else 1
|
||
|
||
# 如果只指定IJK没指定X/Y,终点=起点(全圆)
|
||
if x is None and y is None:
|
||
if i is not None or j is not None:
|
||
x = current.x
|
||
y = current.y
|
||
|
||
if x is not None or y is not None:
|
||
x = x if x is not None else current.x
|
||
y = y if y is not None else current.y
|
||
|
||
# ===== R格式圆弧:计算圆心 =====
|
||
if r_coord is not None and (i is None or j is None):
|
||
dx = x - current.x
|
||
dy = y - current.y
|
||
d = math.sqrt(dx*dx + dy*dy)
|
||
if d > 0 and abs(r_coord) >= d/2 - 1e-9:
|
||
h = math.sqrt(max(0, r_coord*r_coord - d*d/4))
|
||
# G3 (turn > 0)
|
||
if r_coord > 0:
|
||
# R>0: 小弧(<180°)
|
||
cx_calc = current.x + dx/2 + h * dy/d
|
||
cy_calc = current.y + dy/2 - h * dx/d
|
||
else:
|
||
# R<0: 大弧(>180°)
|
||
cx_calc = current.x + dx/2 - h * dy/d
|
||
cy_calc = current.y + dy/2 + h * dx/d
|
||
# 转换为增量IJ
|
||
i = cx_calc - current.x
|
||
j = cy_calc - current.y
|
||
else:
|
||
# 半圆或无效R
|
||
i = (x - current.x) / 2
|
||
j = (y - current.y) / 2
|
||
|
||
# ===== 确保IJ是增量(相对于起点) =====
|
||
# ===== IJK 处理逻辑(对照 LinuxCNC interp_convert.cc arc_data_ijk)=====
|
||
# 在 G91.1(增量模式)下,I, J 被视为相对于起点
|
||
# 在 G90.1(绝对模式)下,I, J 被视为绝对坐标
|
||
i_final = i if i is not None else 0.0
|
||
j_final = j if j is not None else 0.0
|
||
k_final = k if k is not None else 0.0
|
||
|
||
# 如果是绝对 IJK 模式 (G90.1),转换为增量
|
||
if self.collector and self.collector.ijk_distance_mode == 90: # 90 = G90.1 绝对IJK
|
||
i_final = i_final - current.x
|
||
j_final = j_final - current.y
|
||
k_final = k_final - current.z
|
||
# 在增量模式下(G91.1,默认),IJK 已经是相对于起点的值,无需转换
|
||
|
||
if self.debug:
|
||
center_x_dbg = current.x + i_final
|
||
center_y_dbg = current.y + j_final
|
||
print(f"[G3] 执行圆弧: end=({x},{y}) "
|
||
f"center=({center_x_dbg:.1f},{center_y_dbg:.1f}) "
|
||
f"I={i_final:.3f} J={j_final:.3f} turn={turn}")
|
||
|
||
self.collector.arc_feed(
|
||
x, y,
|
||
z if z is not None else current.z,
|
||
a if a is not None else current.a,
|
||
b if b is not None else current.b,
|
||
c if c is not None else current.c,
|
||
i_final if i_final != 0 else 0,
|
||
j_final if j_final != 0 else 0,
|
||
k if k is not None else 0,
|
||
turn, self.collector.feedrate,
|
||
u if u is not None else 0,
|
||
v if v is not None else 0,
|
||
w if w is not None else 0
|
||
)
|
||
|
||
# G4 - 暂停
|
||
elif re.search(r'\bG4\b', expanded_upper) or re.search(r'\bG04\b', expanded_upper):
|
||
dwell_time = 0.0
|
||
if p_dwell_match:
|
||
dwell_time = float(p_dwell_match.group(1)) / 1000.0
|
||
if dwell_time > 0:
|
||
self.collector.dwell(dwell_time)
|
||
|
||
# G28/G30 返回参考点
|
||
if 'G28' in line_upper and self.collector and 'G28.1' not in line_upper:
|
||
ref_x, ref_y, ref_z = self.collector.get_g28()
|
||
self.collector.straight_traverse(ref_x, ref_y, ref_z, current.a, current.b, current.c, 0, 0, 0)
|
||
|
||
if 'G30' in line_upper and self.collector and 'G30.1' not in line_upper:
|
||
ref_x, ref_y, ref_z = self.collector.get_g30()
|
||
self.collector.straight_traverse(ref_x, ref_y, ref_z, current.a, current.b, current.c, 0, 0, 0)
|
||
|
||
# ==================== 平面选择 ====================
|
||
|
||
if re.search(r'\bG17\b', line_upper):
|
||
self.current_plane = 17
|
||
if self.collector:
|
||
self.collector.select_plane(17)
|
||
elif re.search(r'\bG18\b', line_upper):
|
||
self.current_plane = 18
|
||
if self.collector:
|
||
self.collector.select_plane(18)
|
||
elif re.search(r'\bG19\b', line_upper):
|
||
self.current_plane = 19
|
||
if self.collector:
|
||
self.collector.select_plane(19)
|
||
|
||
# ==================== 坐标模式 ====================
|
||
|
||
if re.search(r'\bG90\b', line_upper) and self.collector:
|
||
self.collector.set_absolute_mode()
|
||
elif re.search(r'\bG91\b', line_upper) and self.collector:
|
||
self.collector.set_relative_mode()
|
||
|
||
# ==================== 单位 ====================
|
||
|
||
if re.search(r'\bG20\b', line_upper) and self.collector:
|
||
self.collector.state.units = 20
|
||
if self.debug:
|
||
print(f"[DEBUG] 设置为英制单位 G20")
|
||
elif re.search(r'\bG21\b', line_upper) and self.collector:
|
||
self.collector.state.units = 21
|
||
if self.debug:
|
||
print(f"[DEBUG] 设置为公制单位 G21")
|
||
|
||
# ==================== 刀具半径补偿 ====================
|
||
|
||
if re.search(r'\bG40\b', line_upper) and self.collector:
|
||
self.collector.set_cutter_compensation_state(CompType.OFF, 0.0, 0, 0)
|
||
|
||
if (re.search(r'\bG41\b', line_upper) or re.search(r'\bG41\.1\b', line_upper)) and self.collector:
|
||
d_match = re.search(r'D\s*=?\s*(\d+)', line, re.I) or re.search(r'D\s*(\d+)', line, re.I)
|
||
if d_match:
|
||
d_code = int(d_match.group(1))
|
||
else:
|
||
d_code = self.collector.current_d_code if self.collector.current_d_code > 0 else 1
|
||
|
||
radius = self.tool_radius_map.get(d_code, 0.0)
|
||
self.collector.set_cutter_compensation_state(CompType.LEFT, radius, 0, d_code)
|
||
self.collector.set_tool_radius_offset(d_code)
|
||
|
||
elif (re.search(r'\bG42\b', line_upper) or re.search(r'\bG42\.1\b', line_upper)) and self.collector:
|
||
d_match = re.search(r'D\s*=?\s*(\d+)', line, re.I) or re.search(r'D\s*(\d+)', line, re.I)
|
||
if d_match:
|
||
d_code = int(d_match.group(1))
|
||
else:
|
||
d_code = self.collector.current_d_code if self.collector.current_d_code > 0 else 1
|
||
|
||
radius = self.tool_radius_map.get(d_code, 0.0)
|
||
self.collector.set_cutter_compensation_state(CompType.RIGHT, radius, 0, d_code)
|
||
self.collector.set_tool_radius_offset(d_code)
|
||
|
||
# ==================== 固定循环 ====================
|
||
|
||
if re.search(r'\bG80\b', line_upper) and self.collector:
|
||
self.collector.canned_cycle_active = False
|
||
|
||
# ==================== 坐标系设置 ====================
|
||
|
||
if re.search(r'\bG92\b', line_upper) and self.collector and 'G92.1' not in line_upper and 'G92.2' not in line_upper and 'G92.3' not in line_upper:
|
||
self.collector.set_g92_offset(
|
||
x_word if 'x_word' in locals() else None,
|
||
y_word if 'y_word' in locals() else None,
|
||
z_word if 'z_word' in locals() else None,
|
||
a_word if 'a_word' in locals() else None,
|
||
b_word if 'b_word' in locals() else None,
|
||
c_word if 'c_word' in locals() else None
|
||
)
|
||
|
||
if re.search(r'\bG92\.1\b', line_upper) and self.collector:
|
||
self.collector.clear_g92()
|
||
|
||
if re.search(r'\bG52\b', line_upper) and self.collector:
|
||
self.collector.set_g52_offset(
|
||
x if x is not None else 0,
|
||
y if y is not None else 0,
|
||
z if z is not None else 0
|
||
)
|
||
|
||
return None
|
||
|
||
|
||
|
||
def parse_string(self, program: str, filename: str = "program.ngc") -> ToolpathData:
|
||
import time
|
||
|
||
start_time = time.time()
|
||
|
||
self.current_filename = filename
|
||
|
||
lines = program.split('\n')
|
||
processed_lines = []
|
||
line_number_map = []
|
||
|
||
for line_num, line in enumerate(lines, 1):
|
||
processed = self._preprocess_line(line)
|
||
if processed and processed.strip():
|
||
if re.match(r'o<[^>]+>\s*sub', processed.lower()):
|
||
continue
|
||
if re.match(r'o<[^>]+>\s*endsub', processed.lower()):
|
||
continue
|
||
processed_lines.append(processed)
|
||
line_number_map.append(line_num)
|
||
self._line_number_map[len(processed_lines) - 1] = line_num
|
||
|
||
self.collector = GLCanonPathCollectorWithRTCP(
|
||
colors=DEFAULT_COLORS.copy(), max_points=self.max_points,
|
||
acceleration=self.acceleration, max_rapid_rate=self.max_rapid_rate,
|
||
max_feed_rate=self.max_feed_rate, kinematics_type=self.kinematics_type,
|
||
kinematics_params=self.kinematics_params, debug=self.debug
|
||
)
|
||
|
||
for h_code, length in self.tool_length_map.items():
|
||
self.collector.tool_lengths[h_code] = length
|
||
for d_code, radius in self.tool_radius_map.items():
|
||
self.collector.tool_radii[d_code] = radius
|
||
|
||
control_handler = self.collector.control_handler
|
||
|
||
i = 0
|
||
loop_count = 0
|
||
max_iterations = len(processed_lines) * 100
|
||
|
||
while i < len(processed_lines):
|
||
loop_count += 1
|
||
if loop_count > max_iterations:
|
||
if self.debug:
|
||
print(f" [警告] 解析达到最大迭代次数")
|
||
break
|
||
|
||
if self.collector and len(self.collector.data.segments) >= self.max_points:
|
||
if self.debug:
|
||
print(f" [警告] 达到最大点数限制 {self.max_points}")
|
||
break
|
||
|
||
line = processed_lines[i]
|
||
original_line_num = line_number_map[i]
|
||
|
||
if line.startswith('/') and self.collector and self.collector.block_delete:
|
||
i += 1
|
||
continue
|
||
|
||
if control_handler and line.strip():
|
||
next_idx = control_handler.parse_o_code(
|
||
line, original_line_num, i, processed_lines, self.call_level
|
||
)
|
||
if next_idx is not None:
|
||
i = next_idx
|
||
continue
|
||
|
||
self._parse_line(line, original_line_num)
|
||
i += 1
|
||
|
||
if self.saved_states:
|
||
for saved_state in reversed(self.saved_states):
|
||
if saved_state.get('restore_on_return', False) and self.collector:
|
||
self.collector.restore_state(saved_state)
|
||
|
||
if self.debug:
|
||
print(f" [解析完成] 生成 {len(self.collector.data.segments) if self.collector else 0} 个路径段")
|
||
|
||
if self.collector:
|
||
self.collector.data.filename = filename
|
||
self.collector.data.kinematics_type = self.kinematics_type.name
|
||
self.collector.data.point_count = len(self.collector.data.segments)
|
||
self.collector.data.generation_time_ms = (time.time() - start_time) * 1000
|
||
self.collector.data.calculate_bounds()
|
||
self.collector.data.calculate_length_and_time()
|
||
|
||
return self.collector.data
|
||
|
||
return ToolpathData()
|
||
|
||
|
||
# ==================== 完整RS274解析器 ====================
|
||
|
||
class FullRS274Parser:
|
||
"""完整的RS274NGC解释器包装器 - 支持多实例"""
|
||
|
||
def __init__(self, max_points: int = 50000,
|
||
tool_radius_map: Dict[int, float] = None,
|
||
tool_length_map: Dict[int, float] = None,
|
||
acceleration: float = 500.0,
|
||
max_rapid_rate: float = 10000.0,
|
||
max_feed_rate: float = 5000.0,
|
||
kinematics_type: KinematicsType = KinematicsType.TRT_BC,
|
||
kinematics_params: Dict[str, Any] = None,
|
||
debug: bool = False):
|
||
|
||
self.max_points = max_points
|
||
self.tool_radius_map = tool_radius_map or {1: 2.0, 2: 3.0, 3: 4.0}
|
||
self.tool_length_map = tool_length_map or {1: 100.0, 2: 120.0, 3: 150.0}
|
||
self.acceleration = acceleration
|
||
self.max_rapid_rate = max_rapid_rate
|
||
self.max_feed_rate = max_feed_rate
|
||
self.kinematics_type = kinematics_type
|
||
self.kinematics_params = kinematics_params or {}
|
||
self.debug = debug
|
||
|
||
self._builtin_parser: Optional[FullGCodeParserWithRTCP] = None
|
||
self.collector: Optional[GLCanonPathCollectorWithRTCP] = None
|
||
|
||
self._init_parser()
|
||
|
||
def _init_parser(self):
|
||
self._builtin_parser = FullGCodeParserWithRTCP(
|
||
max_points=self.max_points,
|
||
tool_radius_map=self.tool_radius_map,
|
||
tool_length_map=self.tool_length_map,
|
||
acceleration=self.acceleration,
|
||
max_rapid_rate=self.max_rapid_rate,
|
||
max_feed_rate=self.max_feed_rate,
|
||
kinematics_type=self.kinematics_type,
|
||
kinematics_params=self.kinematics_params,
|
||
debug=self.debug
|
||
)
|
||
|
||
def register_subroutine(self, name: str, content: str):
|
||
if not self._builtin_parser:
|
||
self._init_parser()
|
||
|
||
lines = content.split('\n')
|
||
sub_lines = []
|
||
sub_line_nums = []
|
||
|
||
in_sub = False
|
||
for i, line in enumerate(lines):
|
||
processed = self._builtin_parser._preprocess_line(line)
|
||
if processed and processed.strip():
|
||
if re.match(r'o<([^>]+)>\s*sub', processed, re.I):
|
||
in_sub = True
|
||
continue
|
||
if re.match(r'o<([^>]+)>\s*endsub', processed, re.I):
|
||
in_sub = False
|
||
continue
|
||
if in_sub:
|
||
sub_lines.append(processed)
|
||
sub_line_nums.append(i + 1)
|
||
|
||
if sub_lines:
|
||
self._builtin_parser.subroutines[name] = list(zip(sub_line_nums, sub_lines))
|
||
|
||
def parse_string(self, program: str, filename: str = "program.ngc") -> ToolpathData:
|
||
if not self._builtin_parser:
|
||
self._init_parser()
|
||
|
||
return self._builtin_parser.parse_string(program, filename)
|
||
|
||
def parse_file(self, filename: str) -> ToolpathData:
|
||
if not os.path.exists(filename):
|
||
raise FileNotFoundError(f"文件不存在: {filename}")
|
||
|
||
with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
|
||
program = f.read()
|
||
|
||
return self.parse_string(program, os.path.basename(filename))
|
||
|
||
def set_subroutine_path(self, path: str):
|
||
if self._builtin_parser:
|
||
self._builtin_parser.set_subroutine_path(path)
|
||
|
||
|
||
# ==================== CNC内核 ====================
|
||
|
||
class CNCKernel:
|
||
"""CNC内核 - 整合所有功能的主控制器"""
|
||
|
||
def __init__(self, debug: bool = False, name: str = "CNC1"):
|
||
self.name = name
|
||
self.debug = debug
|
||
self.virtual_hal = VirtualHAL(debug=debug)
|
||
self.state_machine = MachineStateMachine(self.virtual_hal, debug=debug)
|
||
|
||
self.parser: Optional[FullRS274Parser] = None
|
||
self.current_toolpath: Optional[ToolpathData] = None
|
||
self.status = CNCRuntimeStatus()
|
||
|
||
self.is_running = False
|
||
self.is_paused = False
|
||
self.current_segment_index = 0
|
||
self.program_ended = False
|
||
self.single_step_mode = False
|
||
|
||
self.planner = TrapezoidalMotionPlanner()
|
||
|
||
self.rtcp_enabled = False
|
||
self._kinematics_type = KinematicsType.TRT_BC
|
||
self._kinematics_instance: Optional[BaseKinematics] = None
|
||
self._original_kinematics_type = KinematicsType.TRT_BC
|
||
|
||
self._update_kinematics_instance()
|
||
|
||
self.run_thread: Optional[threading.Thread] = None
|
||
self.state_monitor_thread: Optional[threading.Thread] = None
|
||
self.state_monitor_running = False
|
||
|
||
self.update_callback: Optional[Callable[[CNCRuntimeStatus], None]] = None
|
||
|
||
self.tool_table = {
|
||
1: {"length": 100.0, "diameter": 10.0, "name": "端铣刀"},
|
||
2: {"length": 120.0, "diameter": 8.0, "name": "球头刀"},
|
||
3: {"length": 80.0, "diameter": 12.0, "name": "面铣刀"},
|
||
4: {"length": 150.0, "diameter": 6.0, "name": "钻头"},
|
||
5: {"length": 90.0, "diameter": 10.0, "name": "倒角刀"},
|
||
}
|
||
|
||
# 初始化默认工具长度
|
||
self.selected_tool = 1
|
||
if 1 in self.tool_table:
|
||
self.status.tool_length = self.tool_table[1].get('length', 100.0)
|
||
self.status.tool_diameter = self.tool_table[1].get('diameter', 10.0)
|
||
self.status.current_tool = 1
|
||
|
||
# 设置默认坐标系
|
||
self.status.coordinate_mode = "G54"
|
||
self.status.current_offset = "G54"
|
||
|
||
self.work_offsets = {
|
||
54: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
55: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
56: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
57: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
58: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
59: {'x': 0, 'y': 0, 'z': 0, 'a': 0, 'b': 0, 'c': 0},
|
||
}
|
||
self.current_work_offset = 54
|
||
|
||
|
||
# 设置默认坐标系显示
|
||
self.status.coordinate_mode = "G54" # 改为G54而非G90
|
||
self.status.current_offset = "G54"
|
||
|
||
# 设置默认RTCP状态
|
||
self.status.rtcp_enabled = False # 默认关闭,等G43.4时开启
|
||
|
||
|
||
|
||
self._init_hal()
|
||
self.start_state_monitor()
|
||
|
||
|
||
print(f"✓ CNC内核 [{self.name}] 初始化完成")
|
||
|
||
@property
|
||
def kinematics_type(self) -> KinematicsType:
|
||
return self._kinematics_type
|
||
|
||
@kinematics_type.setter
|
||
def kinematics_type(self, value: KinematicsType):
|
||
if self._kinematics_type != value:
|
||
self._kinematics_type = value
|
||
self._update_kinematics_instance()
|
||
self.status.kinematics_type = value.name
|
||
|
||
def _update_kinematics_instance(self):
|
||
if self._kinematics_type == KinematicsType.IDENTITY:
|
||
self._kinematics_instance = None
|
||
else:
|
||
self._kinematics_instance = KinematicsFactory.create(
|
||
self._kinematics_type, debug=self.debug
|
||
)
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.set_tool_length(self.status.tool_length)
|
||
self._kinematics_instance.enable_rtcp(self.rtcp_enabled)
|
||
|
||
def _init_hal(self):
|
||
self.virtual_hal.set('motion.kins-type', 2)
|
||
self.virtual_hal.set('motion.pivot-length', 250.0)
|
||
self.virtual_hal.set('motion.tool-length', 100.0)
|
||
|
||
def _sync_offsets_to_status(self):
|
||
"""从解析器的收集器同步偏移信息到status对象"""
|
||
try:
|
||
if hasattr(self, 'parser') and self.parser:
|
||
parser = self.parser
|
||
if hasattr(parser, '_builtin_parser') and parser._builtin_parser:
|
||
bp = parser._builtin_parser
|
||
if bp.collector:
|
||
c = bp.collector
|
||
# 同步 G5x 偏移
|
||
self.status.g5x_offset_x = c.g5x_offset_x
|
||
self.status.g5x_offset_y = c.g5x_offset_y
|
||
self.status.g5x_offset_z = c.g5x_offset_z
|
||
self.status.g5x_offset_a = getattr(c, 'g5x_offset_a', 0.0)
|
||
self.status.g5x_offset_b = getattr(c, 'g5x_offset_b', 0.0)
|
||
self.status.g5x_offset_c = getattr(c, 'g5x_offset_c', 0.0)
|
||
|
||
# 同步 G92 偏移
|
||
self.status.g92_x = c.g92_offset_x
|
||
self.status.g92_y = c.g92_offset_y
|
||
self.status.g92_z = c.g92_offset_z
|
||
self.status.g92_a = getattr(c, 'g92_offset_a', 0.0)
|
||
self.status.g92_b = getattr(c, 'g92_offset_b', 0.0)
|
||
self.status.g92_c = getattr(c, 'g92_offset_c', 0.0)
|
||
|
||
# 同步刀具长度
|
||
self.status.tool_length = c.tool_offset.z
|
||
|
||
# ★ 关键:同步 RTCP 状态 ★
|
||
self.status.rtcp_enabled = c.rtcp_enabled
|
||
self.rtcp_enabled = c.rtcp_enabled
|
||
|
||
# 同步坐标系
|
||
if c.g5x_index:
|
||
self.status.current_offset = f'G{54 + c.g5x_index - 1}'
|
||
self.status.g5x_index = c.g5x_index
|
||
|
||
return True
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[{self.name}] 同步偏移信息失败: {e}")
|
||
return False
|
||
|
||
|
||
|
||
def _update_world_position(self):
|
||
if self.status.rtcp_enabled and self._kinematics_instance is not None:
|
||
# 根据运动学类型构建正确顺序的joints数组
|
||
# TRT_AC: [X, Y, Z, A, C] (5个)
|
||
# TRT_BC: [X, Y, Z, B, C] (5个)
|
||
config = KINEMATICS_JOINT_CONFIG.get(
|
||
self._kinematics_type if hasattr(self, '_kinematics_type') else self.kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY]
|
||
)
|
||
num_joints = config.get('num_joints', 6)
|
||
joints = [0.0] * num_joints
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
if x_idx >= 0: joints[x_idx] = self.status.machine_x
|
||
if y_idx >= 0: joints[y_idx] = self.status.machine_y
|
||
if z_idx >= 0: joints[z_idx] = self.status.machine_z
|
||
if a_idx >= 0: joints[a_idx] = self.status.machine_a
|
||
if b_idx >= 0: joints[b_idx] = self.status.machine_b
|
||
if c_idx >= 0: joints[c_idx] = self.status.machine_c
|
||
|
||
world = self._kinematics_instance.joints_to_world(joints)
|
||
self.status.world_x = world[0]
|
||
self.status.world_y = world[1]
|
||
self.status.world_z = world[2]
|
||
else:
|
||
self.status.world_x = self.status.position_x
|
||
self.status.world_y = self.status.position_y
|
||
self.status.world_z = self.status.position_z
|
||
|
||
|
||
|
||
def _update_world_position_bak(self):
|
||
if self.status.rtcp_enabled and self._kinematics_instance is not None:
|
||
# ★修复: 使用MCS(关节坐标)调用正运动学, 不是WCS(工件坐标)★
|
||
joints = [self.status.machine_x, self.status.machine_y, self.status.machine_z,
|
||
self.status.machine_a, self.status.machine_b, self.status.machine_c]
|
||
world = self._kinematics_instance.joints_to_world(joints)
|
||
self.status.world_x = world[0]
|
||
self.status.world_y = world[1]
|
||
self.status.world_z = world[2]
|
||
else:
|
||
self.status.world_x = self.status.position_x
|
||
self.status.world_y = self.status.position_y
|
||
self.status.world_z = self.status.position_z
|
||
|
||
def _update_current_from_machine(self):
|
||
"""
|
||
从机器坐标反向计算工件坐标
|
||
|
||
逆变换顺序:
|
||
机器坐标 → 正运动学 → 绝对坐标 → -偏移链 → 工件坐标
|
||
|
||
与 _sync_machine_from_current 互为逆操作:
|
||
- _sync_machine_from_current: 工件→机器
|
||
- _update_current_from_machine: 机器→工件
|
||
"""
|
||
# ===== 第1步:正运动学 - 机器坐标→绝对坐标 =====
|
||
if self.rtcp_enabled and self._kinematics_instance is not None:
|
||
# 构建符合运动学类型的关节数组
|
||
config = KINEMATICS_JOINT_CONFIG.get(
|
||
self._kinematics_type if hasattr(self, '_kinematics_type') else self.kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY]
|
||
)
|
||
num_joints = config.get('num_joints', 6)
|
||
joints = [0.0] * num_joints
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
if x_idx >= 0 and x_idx < num_joints:
|
||
joints[x_idx] = self.status.machine_x
|
||
if y_idx >= 0 and y_idx < num_joints:
|
||
joints[y_idx] = self.status.machine_y
|
||
if z_idx >= 0 and z_idx < num_joints:
|
||
joints[z_idx] = self.status.machine_z
|
||
if a_idx >= 0 and a_idx < num_joints:
|
||
joints[a_idx] = self.status.machine_a
|
||
if b_idx >= 0 and b_idx < num_joints:
|
||
joints[b_idx] = self.status.machine_b
|
||
if c_idx >= 0 and c_idx < num_joints:
|
||
joints[c_idx] = self.status.machine_c
|
||
|
||
try:
|
||
world = self._kinematics_instance.joints_to_world(joints)
|
||
abs_x = world[0]
|
||
abs_y = world[1]
|
||
abs_z = world[2]
|
||
abs_a = world[3] if len(world) > 3 else self.status.machine_a
|
||
abs_b = world[4] if len(world) > 4 else self.status.machine_b
|
||
abs_c = world[5] if len(world) > 5 else self.status.machine_c
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[{self.name}] 正运动学计算失败: {e}")
|
||
abs_x = self.status.machine_x
|
||
abs_y = self.status.machine_y
|
||
abs_z = self.status.machine_z
|
||
abs_a = self.status.machine_a
|
||
abs_b = self.status.machine_b
|
||
abs_c = self.status.machine_c
|
||
else:
|
||
# RTCP禁用:机器坐标=绝对坐标
|
||
abs_x = self.status.machine_x
|
||
abs_y = self.status.machine_y
|
||
abs_z = self.status.machine_z
|
||
abs_a = self.status.machine_a
|
||
abs_b = self.status.machine_b
|
||
abs_c = self.status.machine_c
|
||
|
||
# ===== 第2步:逆变换 - 绝对坐标→工件坐标 =====
|
||
# 获取偏移值
|
||
g5x_x = getattr(self.status, 'g5x_offset_x', 0.0)
|
||
g5x_y = getattr(self.status, 'g5x_offset_y', 0.0)
|
||
g5x_z = getattr(self.status, 'g5x_offset_z', 0.0)
|
||
g5x_a = getattr(self.status, 'g5x_offset_a', 0.0)
|
||
g5x_b = getattr(self.status, 'g5x_offset_b', 0.0)
|
||
g5x_c = getattr(self.status, 'g5x_offset_c', 0.0)
|
||
|
||
g92_x = getattr(self.status, 'g92_x', 0.0)
|
||
g92_y = getattr(self.status, 'g92_y', 0.0)
|
||
g92_z = getattr(self.status, 'g92_z', 0.0)
|
||
g92_a = getattr(self.status, 'g92_a', 0.0)
|
||
g92_b = getattr(self.status, 'g92_b', 0.0)
|
||
g92_c = getattr(self.status, 'g92_c', 0.0)
|
||
|
||
tlo = getattr(self.status, 'tool_length', 0.0)
|
||
|
||
# 逆序减去偏移(正变换的逆操作)
|
||
# 正变换: 工件 + G92 + XY旋转 + G5x + TLO = 绝对
|
||
# 逆变换: 绝对 - TLO - G5x - XY旋转 - G92 = 工件
|
||
|
||
x = abs_x
|
||
y = abs_y
|
||
z = abs_z
|
||
a = abs_a
|
||
b = abs_b
|
||
c = abs_c
|
||
|
||
# 减去 TLO(刀具长度补偿主要在Z方向)
|
||
z = z - tlo
|
||
|
||
# 减去 G5x 偏移
|
||
x = x - g5x_x
|
||
y = y - g5x_y
|
||
z = z - g5x_z
|
||
a = a - g5x_a
|
||
b = b - g5x_b
|
||
c = c - g5x_c
|
||
|
||
# 减去 G92 偏移
|
||
x = x - g92_x
|
||
y = y - g92_y
|
||
z = z - g92_z
|
||
a = a - g92_a
|
||
b = b - g92_b
|
||
c = c - g92_c
|
||
|
||
# 更新工件坐标
|
||
self.status.position_x = x
|
||
self.status.position_y = y
|
||
self.status.position_z = z
|
||
self.status.position_a = a
|
||
self.status.position_b = b
|
||
self.status.position_c = c
|
||
|
||
# 更新世界坐标
|
||
self._update_world_position()
|
||
|
||
if self.debug:
|
||
print(f"[MCS→WCS] machine({self.status.machine_x:.1f},{self.status.machine_y:.1f},"
|
||
f"{self.status.machine_z:.1f}) → workpiece({x:.1f},{y:.1f},{z:.1f})")
|
||
|
||
|
||
def _sync_offsets_to_status(self):
|
||
"""从解析器的收集器同步偏移信息到status对象"""
|
||
try:
|
||
if hasattr(self, 'parser') and self.parser:
|
||
parser = self.parser
|
||
if hasattr(parser, '_builtin_parser') and parser._builtin_parser:
|
||
bp = parser._builtin_parser
|
||
if bp.collector:
|
||
c = bp.collector
|
||
# 同步 G5x 偏移
|
||
self.status.g5x_offset_x = c.g5x_offset_x
|
||
self.status.g5x_offset_y = c.g5x_offset_y
|
||
self.status.g5x_offset_z = c.g5x_offset_z
|
||
self.status.g5x_offset_a = c.g5x_offset_a
|
||
self.status.g5x_offset_b = c.g5x_offset_b
|
||
self.status.g5x_offset_c = c.g5x_offset_c
|
||
|
||
# 同步 G92 偏移
|
||
self.status.g92_x = c.g92_offset_x
|
||
self.status.g92_y = c.g92_offset_y
|
||
self.status.g92_z = c.g92_offset_z
|
||
self.status.g92_a = c.g92_offset_a
|
||
self.status.g92_b = c.g92_offset_b
|
||
self.status.g92_c = c.g92_offset_c
|
||
|
||
# 同步刀具长度
|
||
self.status.tool_length = c.tool_offset.z
|
||
|
||
# 同步 RTCP 状态
|
||
self.status.rtcp_enabled = c.rtcp_enabled
|
||
|
||
# 同步坐标系
|
||
if c.g5x_index:
|
||
self.status.current_offset = f'G{54 + c.g5x_index - 1}'
|
||
self.status.g5x_index = c.g5x_index
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[{self.name}] 同步偏移信息失败: {e}")
|
||
|
||
|
||
def _notify_update(self):
|
||
"""通知更新 - 增强版,包含关节坐标"""
|
||
self.virtual_hal.update_axis_position('x', self.status.position_x)
|
||
self.virtual_hal.update_axis_position('y', self.status.position_y)
|
||
self.virtual_hal.update_axis_position('z', self.status.position_z)
|
||
|
||
# 同时更新HAL中的机器坐标
|
||
self.virtual_hal.set('joint.0.position', self.status.machine_x)
|
||
self.virtual_hal.set('joint.1.position', self.status.machine_y)
|
||
self.virtual_hal.set('joint.2.position', self.status.machine_z)
|
||
|
||
self.virtual_hal.set('spindle.0.speed', self.status.spindle_speed)
|
||
self.virtual_hal.set('coolant.flood', 1 if self.status.coolant_flood else 0)
|
||
self.virtual_hal.set('coolant.mist', 1 if self.status.coolant_mist else 0)
|
||
self.virtual_hal.set('tool.number', self.status.current_tool)
|
||
self.virtual_hal.set('motion.rtcp-active', 1 if self.status.rtcp_enabled else 0)
|
||
|
||
if self.update_callback:
|
||
try:
|
||
self.update_callback(self.status)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[{self.name}] 回调执行错误: {e}")
|
||
|
||
def start_state_monitor(self):
|
||
if self.state_monitor_thread is None:
|
||
self.state_monitor_running = True
|
||
self.state_monitor_thread = threading.Thread(target=self._monitor_state, daemon=True)
|
||
self.state_monitor_thread.start()
|
||
|
||
def _monitor_state(self):
|
||
while self.state_monitor_running:
|
||
self.state_machine.update()
|
||
self.status.state = self.state_machine.get_state()
|
||
time.sleep(0.1)
|
||
|
||
def ensure_operational(self) -> bool:
|
||
current_state = self.state_machine.get_state()
|
||
|
||
if current_state == MachineStateMachine.STATE_IDLE:
|
||
return True
|
||
|
||
if current_state == MachineStateMachine.STATE_OFF:
|
||
self.power_on()
|
||
time.sleep(0.5)
|
||
return self.state_machine.get_state() == MachineStateMachine.STATE_IDLE
|
||
|
||
if current_state == MachineStateMachine.STATE_ESTOP:
|
||
self.reset()
|
||
time.sleep(0.5)
|
||
return self.state_machine.get_state() == MachineStateMachine.STATE_IDLE
|
||
|
||
return False
|
||
|
||
def power_on(self) -> Dict:
|
||
if self.state_machine.request_power_on():
|
||
time.sleep(0.5)
|
||
self.status.state = self.state_machine.get_state()
|
||
self._notify_update()
|
||
return {"success": True, "message": "上电成功"}
|
||
return {"success": False, "message": "无法上电"}
|
||
|
||
def reset(self) -> Dict:
|
||
if self.state_machine.reset():
|
||
time.sleep(0.5)
|
||
self.status.state = self.state_machine.get_state()
|
||
self.status.alarm_code = 0
|
||
self.status.alarm_message = ""
|
||
self._notify_update()
|
||
return {"success": True, "message": "复位成功"}
|
||
return {"success": False, "message": "无法复位"}
|
||
|
||
def emergency_stop(self) -> Dict:
|
||
if self.state_machine.emergency_stop():
|
||
self.is_running = False
|
||
self.is_paused = False
|
||
self.status.state = MachineState.ESTOP.value
|
||
self._notify_update()
|
||
return {"success": True, "message": "急停已激活"}
|
||
return {"success": False, "message": "无法急停"}
|
||
|
||
def load_program_string(self, program: str, filename: str = "program.ngc") -> Dict:
|
||
try:
|
||
self.parser = FullRS274Parser(
|
||
debug=self.debug, kinematics_type=self.kinematics_type
|
||
)
|
||
self.current_toolpath = self.parser.parse_string(program, filename)
|
||
|
||
self.status.total_lines = len(self.current_toolpath.segments)
|
||
self.status.current_line = 0
|
||
self.status.progress_percent = 0
|
||
self.status.rtcp_enabled = self.current_toolpath.rtcp_enabled
|
||
self.status.kinematics_type = self.current_toolpath.kinematics_type
|
||
self.status.program_name = filename
|
||
self.status.path_length = self.current_toolpath.total_length
|
||
self.status.total_time = self.current_toolpath.total_time
|
||
|
||
self._notify_update()
|
||
|
||
return {"success": True, "message": f"程序加载成功", "segments": len(self.current_toolpath.segments)}
|
||
except Exception as e:
|
||
return {"success": False, "message": f"加载失败: {str(e)}"}
|
||
|
||
def start_program(self) -> Dict:
|
||
if not self.ensure_operational():
|
||
return {"success": False, "message": "机床未就绪"}
|
||
|
||
if not self.current_toolpath:
|
||
return {"success": False, "message": "没有加载程序"}
|
||
|
||
self.state_machine.start_program()
|
||
self.is_running = True
|
||
self.is_paused = False
|
||
self.current_segment_index = 0
|
||
|
||
# 在这行之后添加:
|
||
self.program_ended = False
|
||
|
||
self.status.state = MachineState.RUNNING.value
|
||
|
||
self.run_thread = threading.Thread(target=self._run_program, daemon=True)
|
||
self.run_thread.start()
|
||
|
||
self._notify_update()
|
||
return {"success": True, "message": "程序开始运行"}
|
||
|
||
def pause_program(self) -> Dict:
|
||
if not self.is_running:
|
||
return {"success": False, "message": "程序未运行"}
|
||
|
||
self.state_machine.pause_program()
|
||
self.is_paused = True
|
||
self.status.state = MachineState.PAUSED.value
|
||
self._notify_update()
|
||
return {"success": True, "message": "程序已暂停"}
|
||
|
||
def resume_program(self) -> Dict:
|
||
if not self.is_paused:
|
||
return {"success": False, "message": "程序未暂停"}
|
||
|
||
self.state_machine.resume_program()
|
||
self.is_paused = False
|
||
self.status.state = MachineState.RUNNING.value
|
||
self._notify_update()
|
||
return {"success": True, "message": "程序已恢复"}
|
||
|
||
def stop_program(self) -> Dict:
|
||
self.state_machine.stop_program()
|
||
self.is_running = False
|
||
self.is_paused = False
|
||
self.status.state = MachineState.IDLE.value
|
||
self._notify_update()
|
||
return {"success": True, "message": "程序已停止"}
|
||
|
||
|
||
def _run_program(self):
|
||
"""按照真实速度曲线执行G代码"""
|
||
if not self.current_toolpath:
|
||
return
|
||
|
||
total_segments = len(self.current_toolpath.segments)
|
||
|
||
if self.debug:
|
||
print(f"\n{'='*60}")
|
||
print(f"[执行] 开始执行程序: {self.status.program_name}")
|
||
print(f"[执行] 总段数: {total_segments}")
|
||
print(f"[执行] 路径总长: {self.current_toolpath.total_length:.2f} mm")
|
||
print(f"[执行] 预估时间: {self.current_toolpath.total_time:.2f} s")
|
||
print(f"{'='*60}\n")
|
||
|
||
# 预计算所有运动段的速度曲线(用于精确时间估算)
|
||
motion_profiles = []
|
||
for segment in self.current_toolpath.segments:
|
||
if segment.type in [MoveType.RAPID, MoveType.FEED]:
|
||
start = segment.start
|
||
end = segment.end
|
||
dx = end.x - start.x
|
||
dy = end.y - start.y
|
||
dz = end.z - start.z
|
||
# ★ 修复: 将旋转角度变化计入等效距离 ★
|
||
da = end.a - start.a
|
||
db = end.b - start.b
|
||
dc = end.c - start.c
|
||
linear_dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||
angular_dist = math.sqrt(da*da + db*db + dc*dc)
|
||
distance = math.sqrt(linear_dist*linear_dist + angular_dist*angular_dist)
|
||
if distance < CART_FUZZ and angular_dist > CART_FUZZ:
|
||
distance = angular_dist
|
||
|
||
is_rapid = (segment.type == MoveType.RAPID)
|
||
feedrate = segment.feedrate if segment.type == MoveType.FEED else None
|
||
profile = self.planner.plan_motion(distance, feedrate, is_rapid)
|
||
motion_profiles.append(profile)
|
||
elif segment.type in [MoveType.ARC_CW, MoveType.ARC_CCW]:
|
||
radius = getattr(segment, 'radius', 1.0)
|
||
turn = abs(getattr(segment, 'turn', 1))
|
||
arc_length = turn * 2.0 * math.pi * radius
|
||
profile = self.planner.plan_motion(arc_length, segment.feedrate, False)
|
||
motion_profiles.append(profile)
|
||
else:
|
||
motion_profiles.append(MotionProfile())
|
||
|
||
# 主执行循环
|
||
for seg_idx in range(total_segments):
|
||
if not self.is_running or self.program_ended:
|
||
break
|
||
|
||
# 检查急停和报警
|
||
if self.state_machine.is_estop() or self.state_machine.is_alarm():
|
||
self.is_running = False
|
||
if self.debug:
|
||
print(f"[执行] 紧急停止! 状态={self.state_machine.get_state()}")
|
||
break
|
||
|
||
# 检查暂停
|
||
while self.is_paused:
|
||
time.sleep(0.05)
|
||
if not self.is_running:
|
||
break
|
||
|
||
segment = self.current_toolpath.segments[seg_idx]
|
||
|
||
|
||
# ★ 修复: 直接从segment同步RTCP状态 ★
|
||
if segment.type == MoveType.RTCP_ON:
|
||
self.status.rtcp_enabled = True
|
||
self.rtcp_enabled = True
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(True)
|
||
elif segment.type == MoveType.RTCP_OFF:
|
||
self.status.rtcp_enabled = False
|
||
self.rtcp_enabled = False
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(False)
|
||
elif hasattr(segment, 'is_rtcp'):
|
||
self.status.rtcp_enabled = segment.is_rtcp
|
||
self.rtcp_enabled = segment.is_rtcp
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(segment.is_rtcp)
|
||
|
||
self.current_segment_index = seg_idx
|
||
|
||
|
||
# ★ 在这里添加偏移同步 ★
|
||
self._sync_offsets_to_status()
|
||
|
||
# 更新状态
|
||
self.status.current_line = segment.line_number
|
||
self.status.progress_percent = (seg_idx / total_segments) * 100.0
|
||
self.status.feedrate = segment.feedrate if segment.type == MoveType.FEED else 0
|
||
|
||
# 处理RTCP状态
|
||
# 处理RTCP状态
|
||
if segment.type == MoveType.RTCP_ON:
|
||
self.status.rtcp_enabled = True
|
||
self.rtcp_enabled = True
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(True)
|
||
elif segment.type == MoveType.RTCP_OFF:
|
||
self.status.rtcp_enabled = False
|
||
self.rtcp_enabled = False
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(False)
|
||
|
||
# ★ 每次运动段执行前同步偏移 ★
|
||
self._sync_offsets_to_status()
|
||
|
||
self._notify_update()
|
||
|
||
# 执行运动段
|
||
profile = motion_profiles[seg_idx] if seg_idx < len(motion_profiles) else None
|
||
self._execute_segment_real(segment, profile)
|
||
|
||
# 更新世界坐标
|
||
if hasattr(segment, 'world_end') and segment.world_end:
|
||
self.status.world_x = segment.world_end.x
|
||
self.status.world_y = segment.world_end.y
|
||
self.status.world_z = segment.world_end.z
|
||
|
||
self.current_segment_index = seg_idx + 1
|
||
|
||
# 单步模式
|
||
if self.single_step_mode:
|
||
self.is_paused = True
|
||
self.status.state = MachineState.PAUSED.value
|
||
self.single_step_mode = False
|
||
self._notify_update()
|
||
|
||
# 程序完成
|
||
self.program_ended = True
|
||
self.status.progress_percent = 100.0
|
||
self.is_running = False
|
||
self.state_machine.transition_to(MachineStateMachine.STATE_IDLE)
|
||
self.status.state = MachineState.IDLE.value
|
||
self._notify_update()
|
||
|
||
if self.debug:
|
||
print(f"[执行] 程序执行完成! 最终进度=100%")
|
||
|
||
def _execute_segment_real(self, segment: MoveSegment, profile: MotionProfile = None):
|
||
"""按照真实速度曲线执行运动段"""
|
||
|
||
# ★ 同步偏移信息 ★
|
||
self._sync_offsets_to_status()
|
||
|
||
# 暂停
|
||
if segment.type == MoveType.DWELL:
|
||
dwell_time = getattr(segment, 'dwell_time', 0.0)
|
||
if dwell_time > 0:
|
||
if self.debug:
|
||
print(f" [G4] 暂停 {dwell_time*1000:.0f}ms")
|
||
start = time.time()
|
||
while time.time() - start < dwell_time:
|
||
if not self.is_running:
|
||
return
|
||
while self.is_paused:
|
||
time.sleep(0.05)
|
||
start += 0.05
|
||
time.sleep(0.01)
|
||
return
|
||
|
||
# RTCP/程序结束
|
||
if segment.type in [MoveType.RTCP_ON, MoveType.RTCP_OFF, MoveType.PROGRAM_END]:
|
||
time.sleep(0.05)
|
||
return
|
||
|
||
# 无运动距离
|
||
if not profile or profile.duration <= 0:
|
||
if hasattr(segment, 'end'):
|
||
self._set_position_from_point(segment.end)
|
||
return
|
||
|
||
# ===== 直线运动 =====
|
||
if segment.type in [MoveType.RAPID, MoveType.FEED]:
|
||
self._execute_linear_real(segment, profile)
|
||
|
||
# ===== 圆弧运动 =====
|
||
elif segment.type in [MoveType.ARC_CW, MoveType.ARC_CCW]:
|
||
self._execute_arc_real(segment, profile)
|
||
|
||
def _sync_offsets_to_status(self):
|
||
"""从解析器的收集器同步偏移信息到status对象"""
|
||
try:
|
||
if hasattr(self, 'parser') and self.parser:
|
||
parser = self.parser
|
||
if hasattr(parser, '_builtin_parser') and parser._builtin_parser:
|
||
bp = parser._builtin_parser
|
||
if bp.collector:
|
||
c = bp.collector
|
||
self.status.g5x_offset_x = c.g5x_offset_x
|
||
self.status.g5x_offset_y = c.g5x_offset_y
|
||
self.status.g5x_offset_z = c.g5x_offset_z
|
||
self.status.g92_x = c.g92_offset_x
|
||
self.status.g92_y = c.g92_offset_y
|
||
self.status.g92_z = c.g92_offset_z
|
||
except Exception:
|
||
pass # 如果解析器尚未初始化,使用默认值0
|
||
|
||
|
||
def _execute_linear_real(self, segment: MoveSegment, profile: MotionProfile):
|
||
"""执行直线运动(真实速度曲线)- 适配工件坐标"""
|
||
|
||
# ★修复: 从segment同步RTCP状态★
|
||
if hasattr(segment, 'is_rtcp') and self._kinematics_instance:
|
||
expected_rtcp = segment.is_rtcp
|
||
if self.rtcp_enabled != expected_rtcp:
|
||
self.rtcp_enabled = expected_rtcp
|
||
self.status.rtcp_enabled = expected_rtcp
|
||
self._kinematics_instance.enable_rtcp(expected_rtcp)
|
||
|
||
# ★ 同步偏移信息 ★
|
||
self._sync_offsets_to_status()
|
||
|
||
# 使用工件坐标
|
||
world_start = segment.get_start_in_world()
|
||
world_end = segment.get_end_in_world()
|
||
|
||
start = world_start
|
||
end = world_end
|
||
|
||
dx = end.x - start.x
|
||
dy = end.y - start.y
|
||
dz = end.z - start.z
|
||
da = end.a - start.a
|
||
db = end.b - start.b
|
||
dc = end.c - start.c
|
||
|
||
# ★ 修复: 将旋转角度变化计入等效距离,使F值能控制旋转速度 ★
|
||
# 1度角度变化 = 1mm等效直线距离
|
||
angular_dist = math.sqrt(da*da + db*db + dc*dc)
|
||
linear_dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||
total_6d_dist = math.sqrt(linear_dist*linear_dist + angular_dist*angular_dist)
|
||
|
||
distance = max(profile.total_distance, total_6d_dist * 0.5)
|
||
# 确保distance不为零
|
||
if distance < CART_FUZZ and total_6d_dist > CART_FUZZ:
|
||
distance = total_6d_dist
|
||
|
||
if self.debug:
|
||
is_rapid = (segment.type == MoveType.RAPID)
|
||
gcode = "G0" if is_rapid else "G1"
|
||
print(f" [{gcode}] {self.planner.get_profile_summary(profile)}")
|
||
|
||
if profile.duration <= 0:
|
||
self._set_position_from_world_point(end)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return
|
||
|
||
start_time = time.time()
|
||
last_update_time = start_time
|
||
update_interval = 0.03 # ~30Hz更新
|
||
|
||
while True:
|
||
if not self.is_running:
|
||
return
|
||
if self.state_machine.is_estop() or self.state_machine.is_alarm():
|
||
return
|
||
|
||
while self.is_paused:
|
||
time.sleep(0.05)
|
||
start_time += 0.05
|
||
last_update_time += 0.05
|
||
if not self.is_running:
|
||
return
|
||
|
||
elapsed = time.time() - start_time
|
||
if elapsed >= profile.duration:
|
||
break
|
||
|
||
s = self.planner.get_position_at_time(elapsed, profile)
|
||
ratio = s / distance if distance > CART_FUZZ else 1.0
|
||
ratio = max(0.0, min(1.0, ratio))
|
||
|
||
# 插值工件坐标
|
||
wx = start.x + dx * ratio
|
||
wy = start.y + dy * ratio
|
||
wz = start.z + dz * ratio
|
||
wa = start.a + da * ratio
|
||
wb = start.b + db * ratio
|
||
wc = start.c + dc * ratio
|
||
|
||
self._set_workpiece_position(wx, wy, wz, wa, wb, wc)
|
||
|
||
# 控制更新频率
|
||
now = time.time()
|
||
if now - last_update_time >= update_interval:
|
||
self._notify_update()
|
||
last_update_time = now
|
||
|
||
time.sleep(0.005)
|
||
|
||
# 确保到达终点
|
||
self._set_workpiece_position(end.x, end.y, end.z, end.a, end.b, end.c)
|
||
self._notify_update()
|
||
|
||
# 在 gpb_GLCanon_arcseg_sim_rtcp_rs274_kins.py 中找到 _set_workpiece_position 方法
|
||
# 将其替换为以下代码:
|
||
|
||
|
||
|
||
def _set_workpiece_position(self, x, y, z, a, b, c):
|
||
"""设置当前位置(工件坐标)并计算世界坐标和机器坐标"""
|
||
# 同步偏移信息
|
||
self._sync_offsets_to_status()
|
||
|
||
# ★ 调试:检查tool_offset是否正确
|
||
if self.debug:
|
||
tlo = getattr(self.status, 'tool_length', 0.0)
|
||
print(f"[DEBUG] _set_workpiece_position: "
|
||
f"工件({x:.1f},{y:.1f},{z:.1f}) TLO={tlo:.1f} "
|
||
f"RTCP={self.rtcp_enabled}")
|
||
|
||
|
||
# ★修复: 直接使用self.rtcp_enabled★
|
||
rtcp_on = self.rtcp_enabled and self._kinematics_instance is not None
|
||
|
||
# 更新工件坐标 (WCS)
|
||
self.status.position_x = x
|
||
self.status.position_y = y
|
||
self.status.position_z = z
|
||
self.status.position_a = a
|
||
self.status.position_b = b
|
||
self.status.position_c = c
|
||
|
||
# 获取偏移值
|
||
g5x_x = getattr(self.status, 'g5x_offset_x', 0.0)
|
||
g5x_y = getattr(self.status, 'g5x_offset_y', 0.0)
|
||
g5x_z = getattr(self.status, 'g5x_offset_z', 0.0)
|
||
g5x_a = getattr(self.status, 'g5x_offset_a', 0.0)
|
||
g5x_b = getattr(self.status, 'g5x_offset_b', 0.0)
|
||
g5x_c = getattr(self.status, 'g5x_offset_c', 0.0)
|
||
|
||
g92_x = getattr(self.status, 'g92_x', 0.0)
|
||
g92_y = getattr(self.status, 'g92_y', 0.0)
|
||
g92_z = getattr(self.status, 'g92_z', 0.0)
|
||
g92_a = getattr(self.status, 'g92_a', 0.0)
|
||
g92_b = getattr(self.status, 'g92_b', 0.0)
|
||
g92_c = getattr(self.status, 'g92_c', 0.0)
|
||
|
||
tlo = getattr(self.status, 'tool_length', 0.0)
|
||
|
||
# ★ 关键修复:计算绝对坐标 ★
|
||
# 工件坐标 + G5x + G92 + TLO = 绝对坐标
|
||
abs_x = x + g5x_x + g92_x
|
||
abs_y = y + g5x_y + g92_y
|
||
abs_z = z + g5x_z + g92_z + tlo # TLO加在Z上
|
||
abs_a = a + g5x_a + g92_a
|
||
abs_b = b + g5x_b + g92_b
|
||
abs_c = c + g5x_c + g92_c
|
||
|
||
# ★修复: RTCP逆解 - 使用self.rtcp_enabled★
|
||
rtcp_on = self.rtcp_enabled and self._kinematics_instance is not None
|
||
|
||
if not rtcp_on:
|
||
# 非RTCP:绝对坐标 = 机器坐标
|
||
self.status.machine_x = abs_x
|
||
self.status.machine_y = abs_y
|
||
self.status.machine_z = abs_z
|
||
self.status.machine_a = abs_a
|
||
self.status.machine_b = abs_b
|
||
self.status.machine_c = abs_c
|
||
else:
|
||
# RTCP开启:调用逆运动学
|
||
try:
|
||
# 构造完整的世界坐标元组 (X, Y, Z, A, B, C)
|
||
world_tuple = (abs_x, abs_y, abs_z, abs_a, abs_b, abs_c)
|
||
|
||
# ★ 调用逆运动学 ★
|
||
joints = self._kinematics_instance.inverse(world_tuple)
|
||
|
||
if self.debug:
|
||
print(f"[RTCP INV] world=({abs_x:.2f},{abs_y:.2f},{abs_z:.2f},"
|
||
f"A={abs_a:.1f},B={abs_b:.1f},C={abs_c:.1f})")
|
||
print(f"[RTCP INV] joints={[f'{j:.2f}' for j in joints]}")
|
||
|
||
# 根据运动学类型赋值关节坐标
|
||
config = KINEMATICS_JOINT_CONFIG.get(
|
||
self._kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY]
|
||
)
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
# ★ 关键:确保从joints数组中正确取值 ★
|
||
if x_idx >= 0 and x_idx < len(joints):
|
||
self.status.machine_x = joints[x_idx]
|
||
else:
|
||
self.status.machine_x = abs_x
|
||
|
||
if y_idx >= 0 and y_idx < len(joints):
|
||
self.status.machine_y = joints[y_idx]
|
||
else:
|
||
self.status.machine_y = abs_y
|
||
|
||
if z_idx >= 0 and z_idx < len(joints):
|
||
self.status.machine_z = joints[z_idx]
|
||
else:
|
||
self.status.machine_z = abs_z
|
||
|
||
if a_idx >= 0 and a_idx < len(joints):
|
||
self.status.machine_a = joints[a_idx]
|
||
else:
|
||
self.status.machine_a = abs_a
|
||
|
||
if b_idx >= 0 and b_idx < len(joints):
|
||
self.status.machine_b = joints[b_idx]
|
||
else:
|
||
self.status.machine_b = abs_b
|
||
|
||
if c_idx >= 0 and c_idx < len(joints):
|
||
self.status.machine_c = joints[c_idx]
|
||
else:
|
||
self.status.machine_c = abs_c
|
||
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[RTCP] 逆运动学计算失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
# 逆解失败时使用绝对坐标作为后备
|
||
self.status.machine_x = abs_x
|
||
self.status.machine_y = abs_y
|
||
self.status.machine_z = abs_z
|
||
self.status.machine_a = abs_a
|
||
self.status.machine_b = abs_b
|
||
self.status.machine_c = abs_c
|
||
|
||
# 更新世界坐标
|
||
self._update_world_position()
|
||
|
||
|
||
|
||
def _set_position_from_world_point(self, point):
|
||
"""从 Point6D 设置当前位置(工件坐标)"""
|
||
if point is None:
|
||
return
|
||
self.status.position_x = point.x
|
||
self.status.position_y = point.y
|
||
self.status.position_z = point.z
|
||
self.status.position_a = point.a
|
||
self.status.position_b = point.b
|
||
self.status.position_c = point.c
|
||
|
||
|
||
|
||
def _execute_arc_real(self, segment: MoveSegment, profile: MotionProfile):
|
||
"""执行圆弧运动(真实速度曲线)- 适配工件坐标"""
|
||
|
||
# ★修复: 从segment同步RTCP状态★
|
||
if hasattr(segment, 'is_rtcp') and self._kinematics_instance:
|
||
expected_rtcp = segment.is_rtcp
|
||
if self.rtcp_enabled != expected_rtcp:
|
||
self.rtcp_enabled = expected_rtcp
|
||
self.status.rtcp_enabled = expected_rtcp
|
||
self._kinematics_instance.enable_rtcp(expected_rtcp)
|
||
|
||
# 使用工件坐标
|
||
world_start = segment.get_start_in_world()
|
||
world_end = segment.get_end_in_world()
|
||
|
||
start = world_start
|
||
end = world_end
|
||
|
||
center_x = segment.center_x
|
||
center_y = segment.center_y
|
||
center_z = segment.center_z
|
||
radius = segment.radius
|
||
turn = abs(segment.turn)
|
||
|
||
arc_length = profile.total_distance
|
||
|
||
dz = end.z - start.z
|
||
da = end.a - start.a
|
||
db = end.b - start.b
|
||
dc = end.c - start.c
|
||
|
||
is_cw = (segment.type == MoveType.ARC_CW)
|
||
|
||
if self.debug:
|
||
gcode = "G2" if is_cw else "G3"
|
||
print(f" [{gcode}] R={radius:.1f}mm 圈数={turn} "
|
||
f"{self.planner.get_profile_summary(profile)}")
|
||
|
||
if profile.duration <= 0:
|
||
self._set_position_from_world_point(end)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return
|
||
|
||
# 计算起始角度(工件坐标系)
|
||
start_angle = math.atan2(start.y - center_y, start.x - center_x)
|
||
|
||
start_time = time.time()
|
||
last_update_time = start_time
|
||
update_interval = 0.03
|
||
|
||
while True:
|
||
if not self.is_running:
|
||
return
|
||
if self.state_machine.is_estop() or self.state_machine.is_alarm():
|
||
return
|
||
|
||
while self.is_paused:
|
||
time.sleep(0.05)
|
||
start_time += 0.05
|
||
last_update_time += 0.05
|
||
if not self.is_running:
|
||
return
|
||
|
||
elapsed = time.time() - start_time
|
||
if elapsed >= profile.duration:
|
||
break
|
||
|
||
s = self.planner.get_position_at_time(elapsed, profile)
|
||
angle_ratio = s / arc_length if arc_length > CART_FUZZ else 1.0
|
||
angle_ratio = max(0.0, min(1.0, angle_ratio))
|
||
|
||
# 计算当前角度
|
||
if is_cw:
|
||
current_angle = start_angle - angle_ratio * turn * 2.0 * math.pi
|
||
else:
|
||
current_angle = start_angle + angle_ratio * turn * 2.0 * math.pi
|
||
|
||
# 计算圆弧上的工件坐标点
|
||
wx = center_x + radius * math.cos(current_angle)
|
||
wy = center_y + radius * math.sin(current_angle)
|
||
wz = start.z + dz * angle_ratio
|
||
wa = start.a + da * angle_ratio
|
||
wb = start.b + db * angle_ratio
|
||
wc = start.c + dc * angle_ratio
|
||
|
||
self._set_workpiece_position(wx, wy, wz, wa, wb, wc)
|
||
|
||
now = time.time()
|
||
if now - last_update_time >= update_interval:
|
||
self._notify_update()
|
||
last_update_time = now
|
||
|
||
time.sleep(0.005)
|
||
|
||
# 确保到达终点
|
||
self._set_workpiece_position(end.x, end.y, end.z, end.a, end.b, end.c)
|
||
self._notify_update()
|
||
|
||
|
||
|
||
def _set_position_from_point(self, point):
|
||
"""从 Point6D 设置当前位置(保留向后兼容)"""
|
||
if point is None:
|
||
return
|
||
self._set_workpiece_position(point.x, point.y, point.z, point.a, point.b, point.c)
|
||
|
||
|
||
|
||
def _execute_segment(self, segment: MoveSegment):
|
||
"""兼容旧接口的执行方法 - 适配工件坐标"""
|
||
if segment.type in [MoveType.RAPID, MoveType.FEED]:
|
||
world_start = segment.get_start_in_world()
|
||
world_end = segment.get_end_in_world()
|
||
dx = world_end.x - world_start.x
|
||
dy = world_end.y - world_start.y
|
||
dz = world_end.z - world_start.z
|
||
distance = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||
is_rapid = (segment.type == MoveType.RAPID)
|
||
feedrate = segment.feedrate if segment.type == MoveType.FEED else None
|
||
profile = self.planner.plan_motion(distance, feedrate, is_rapid)
|
||
elif segment.type in [MoveType.ARC_CW, MoveType.ARC_CCW]:
|
||
radius = segment.radius
|
||
turn = abs(segment.turn)
|
||
arc_length = turn * 2.0 * math.pi * radius
|
||
profile = self.planner.plan_motion(arc_length, segment.feedrate, False)
|
||
else:
|
||
profile = MotionProfile()
|
||
|
||
self._execute_segment_real(segment, profile)
|
||
|
||
|
||
def enable_rtcp(self, enable: bool = True) -> Dict:
|
||
self.status.rtcp_enabled = enable
|
||
self.rtcp_enabled = enable
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(enable)
|
||
self.virtual_hal.set('motion.rtcp-active', 1 if enable else 0)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return {"success": True, "message": f"RTCP {'启用' if enable else '禁用'}"}
|
||
|
||
def mdi_execute(self, command: str) -> Dict:
|
||
cmd_upper = command.upper().strip()
|
||
|
||
if 'G49' in cmd_upper:
|
||
return self.enable_rtcp(False)
|
||
|
||
if 'G43.4' in cmd_upper:
|
||
return self.enable_rtcp(True)
|
||
|
||
if cmd_upper == 'M429':
|
||
self._kinematics_type = KinematicsType.IDENTITY
|
||
self.status.kinematics_type = "IDENTITY"
|
||
self.rtcp_enabled = False
|
||
self.status.rtcp_enabled = False
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(False)
|
||
self.virtual_hal.set('motion.rtcp-active', 0)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return {"success": True, "message": "切换到三轴运动学"}
|
||
|
||
if cmd_upper == 'M428':
|
||
self._kinematics_type = self._original_kinematics_type
|
||
self.status.kinematics_type = self._original_kinematics_type.name
|
||
self.rtcp_enabled = True
|
||
self.status.rtcp_enabled = True
|
||
self._update_kinematics_instance()
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(True)
|
||
self.virtual_hal.set('motion.rtcp-active', 1)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return {"success": True, "message": f"切换到五轴运动学 ({self._original_kinematics_type.name})"}
|
||
|
||
if cmd_upper == 'M430' or cmd_upper.startswith('M430'):
|
||
self._original_kinematics_type = KinematicsType.FIVEAXIS_BC
|
||
self._kinematics_type = KinematicsType.FIVEAXIS_BC
|
||
self.status.kinematics_type = KinematicsType.FIVEAXIS_BC.name
|
||
self._kinematics_instance = KinematicsFactory.create(
|
||
KinematicsType.FIVEAXIS_BC, debug=self.debug
|
||
)
|
||
self.rtcp_enabled = True
|
||
self.status.rtcp_enabled = True
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.enable_rtcp(True)
|
||
self._kinematics_instance.set_tool_length(self.status.tool_length)
|
||
self.virtual_hal.set('motion.rtcp-active', 1)
|
||
self.virtual_hal.set('motion.switchkins-type', 2)
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
return {"success": True, "message": "切换到用户运动学 (FIVEAXIS_BC)"}
|
||
|
||
return {"success": True, "message": f"MDI: {command}"}
|
||
|
||
def get_status(self) -> CNCRuntimeStatus:
|
||
return self.status
|
||
|
||
def shutdown(self):
|
||
self.state_monitor_running = False
|
||
self.is_running = False
|
||
print(f"✓ CNC内核 [{self.name}] 已关闭")
|
||
|
||
|
||
|
||
def verify_jog_coordinate_chain(self) -> Dict[str, Any]:
|
||
"""
|
||
验证Jog操作的坐标变换链正确性
|
||
|
||
通过正反运动学验证工件坐标和机器坐标的一致性。
|
||
在RTCP启用时特别重要,确保刀尖跟随正确。
|
||
|
||
Returns:
|
||
dict: 验证结果,包含各部分坐标和一致性检查
|
||
"""
|
||
result = {
|
||
'timestamp': time.time(),
|
||
'rtcp_enabled': self.rtcp_enabled,
|
||
'kinematics_type': self.kinematics_type.name if self.kinematics_type else 'NONE',
|
||
'workpiece_position': {},
|
||
'absolute_position': {},
|
||
'machine_position': {},
|
||
'world_position': {},
|
||
'offsets': {},
|
||
'consistency_checks': {},
|
||
}
|
||
|
||
# 1. 收集偏移信息
|
||
g5x_x = getattr(self.status, 'g5x_offset_x', 0.0)
|
||
g5x_y = getattr(self.status, 'g5x_offset_y', 0.0)
|
||
g5x_z = getattr(self.status, 'g5x_offset_z', 0.0)
|
||
g92_x = getattr(self.status, 'g92_x', 0.0)
|
||
g92_y = getattr(self.status, 'g92_y', 0.0)
|
||
g92_z = getattr(self.status, 'g92_z', 0.0)
|
||
tlo = getattr(self.status, 'tool_length', 0.0)
|
||
|
||
result['offsets'] = {
|
||
'g5x': {'x': g5x_x, 'y': g5x_y, 'z': g5x_z},
|
||
'g92': {'x': g92_x, 'y': g92_y, 'z': g92_z},
|
||
'tlo': tlo,
|
||
}
|
||
|
||
# 2. 工件坐标
|
||
result['workpiece_position'] = {
|
||
'x': self.status.position_x,
|
||
'y': self.status.position_y,
|
||
'z': self.status.position_z,
|
||
'a': self.status.position_a,
|
||
'c': self.status.position_c,
|
||
}
|
||
|
||
# 3. 计算机器坐标
|
||
result['machine_position'] = {
|
||
'x': self.status.machine_x,
|
||
'y': self.status.machine_y,
|
||
'z': self.status.machine_z,
|
||
'a': self.status.machine_a,
|
||
'c': self.status.machine_c,
|
||
}
|
||
|
||
# 4. 计算绝对坐标
|
||
abs_x = self.status.position_x + g5x_x + g92_x
|
||
abs_y = self.status.position_y + g5x_y + g92_y
|
||
abs_z = self.status.position_z + g5x_z + g92_z + tlo
|
||
abs_a = self.status.position_a
|
||
abs_c = self.status.position_c
|
||
|
||
result['absolute_position'] = {
|
||
'x': abs_x, 'y': abs_y, 'z': abs_z,
|
||
'a': abs_a, 'c': abs_c,
|
||
}
|
||
|
||
# 5. RTCP一致性验证
|
||
if self.rtcp_enabled and self._kinematics_instance:
|
||
try:
|
||
# 正向验证:机器坐标 → 正运动学 → 应等于绝对坐标
|
||
joints = [
|
||
self.status.machine_x, self.status.machine_y,
|
||
self.status.machine_z,
|
||
self.status.machine_a, self.status.machine_b,
|
||
self.status.machine_c
|
||
]
|
||
world_fwd = self._kinematics_instance.joints_to_world(joints)
|
||
|
||
result['world_position'] = {
|
||
'x': world_fwd[0], 'y': world_fwd[1], 'z': world_fwd[2],
|
||
'a': world_fwd[3], 'c': world_fwd[5] if len(world_fwd) > 5 else 0,
|
||
}
|
||
|
||
# 检查正运动学结果是否接近绝对坐标
|
||
tolerance = 0.5 # mm,允许0.5mm误差
|
||
fwd_consistent = True
|
||
if abs(world_fwd[0] - abs_x) > tolerance:
|
||
fwd_consistent = False
|
||
if abs(world_fwd[1] - abs_y) > tolerance:
|
||
fwd_consistent = False
|
||
if abs(world_fwd[2] - abs_z) > tolerance:
|
||
fwd_consistent = False
|
||
|
||
result['consistency_checks']['forward_kinematics'] = {
|
||
'consistent': fwd_consistent,
|
||
'tolerance': tolerance,
|
||
'error_x': abs(world_fwd[0] - abs_x),
|
||
'error_y': abs(world_fwd[1] - abs_y),
|
||
'error_z': abs(world_fwd[2] - abs_z),
|
||
}
|
||
|
||
# 反向验证:绝对坐标 → 逆运动学 → 应等于机器坐标
|
||
joints_inv = self._kinematics_instance.world_to_joints(
|
||
(abs_x, abs_y, abs_z, abs_a, 0.0, abs_c)
|
||
)
|
||
|
||
inv_consistent = True
|
||
if len(joints_inv) > 0:
|
||
if abs(joints_inv[0] - self.status.machine_x) > tolerance:
|
||
inv_consistent = False
|
||
if len(joints_inv) > 1:
|
||
if abs(joints_inv[1] - self.status.machine_y) > tolerance:
|
||
inv_consistent = False
|
||
if len(joints_inv) > 2:
|
||
if abs(joints_inv[2] - self.status.machine_z) > tolerance:
|
||
inv_consistent = False
|
||
|
||
result['consistency_checks']['inverse_kinematics'] = {
|
||
'consistent': inv_consistent,
|
||
'tolerance': tolerance,
|
||
'joints_calculated': joints_inv[:5] if len(joints_inv) >= 5 else joints_inv,
|
||
}
|
||
|
||
# 总体一致性
|
||
result['consistency_checks']['overall'] = {
|
||
'consistent': fwd_consistent and inv_consistent,
|
||
'message': '坐标链一致 ✓' if (fwd_consistent and inv_consistent)
|
||
else '坐标链不一致 ✗ - 请检查运动学参数',
|
||
}
|
||
|
||
except Exception as e:
|
||
result['consistency_checks']['error'] = str(e)
|
||
else:
|
||
# RTCP禁用时,机器坐标应等于绝对坐标
|
||
machine_eq_absolute = (
|
||
abs(self.status.machine_x - abs_x) < 0.001 and
|
||
abs(self.status.machine_y - abs_y) < 0.001 and
|
||
abs(self.status.machine_z - abs_z) < 0.001
|
||
)
|
||
result['consistency_checks']['machine_equals_absolute'] = machine_eq_absolute
|
||
|
||
return result
|
||
|
||
|
||
|
||
# ==================== 完善后的 GLCanonPathCollectorWithRTCP 类 ====================
|
||
|
||
class GLCanonPathCollectorWithRTCP(GLCanonPure):
|
||
"""增强版路径收集器 - 完整五轴RTCP + 所有LinuxCNC状态管理"""
|
||
|
||
def __init__(self, colors=None, geometry=None, is_foam=0, max_points=50000,
|
||
acceleration: float = 500.0,
|
||
max_rapid_rate: float = 10000.0,
|
||
max_feed_rate: float = 5000.0,
|
||
kinematics_type: KinematicsType = KinematicsType.TRT_BC,
|
||
kinematics_params: Dict[str, Any] = None,
|
||
debug: bool = False):
|
||
|
||
if colors is None:
|
||
colors = DEFAULT_COLORS.copy()
|
||
if geometry is None:
|
||
geometry = "XYZ"
|
||
|
||
super().__init__(colors, geometry, is_foam)
|
||
|
||
self.max_points = max_points
|
||
self.debug = debug
|
||
self._point_count = 0
|
||
|
||
self.data = ToolpathData()
|
||
self.data.filename = ""
|
||
self.data.rtcp_enabled = False
|
||
self.data.kinematics_type = kinematics_type.name
|
||
|
||
self.rtcp_enabled = False
|
||
self.kinematics_type = kinematics_type
|
||
|
||
self.kinematics: Optional[BaseKinematics] = KinematicsFactory.create(
|
||
kinematics_type, debug=debug, **(kinematics_params or {})
|
||
)
|
||
|
||
# ===== 添加这一行 =====
|
||
self._saved_kinematics_type = None # 用于 M428/M429 切换时保存原始类型
|
||
|
||
self.planner = TrapezoidalMotionPlanner(
|
||
acceleration=acceleration,
|
||
max_rapid_rate=max_rapid_rate,
|
||
max_feed_rate=max_feed_rate
|
||
)
|
||
|
||
# 当前位置 - 工件坐标系
|
||
self.current_pos = Point6D(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
|
||
# 机床坐标
|
||
self.machine_x = 0.0
|
||
self.machine_y = 0.0
|
||
self.machine_z = 0.0
|
||
self.machine_a = 0.0
|
||
self.machine_b = 0.0
|
||
self.machine_c = 0.0
|
||
|
||
# 工件偏移 (G5x)
|
||
self.work_offsets: Dict[int, Dict[str, float]] = {}
|
||
for i in range(1, 10):
|
||
self.work_offsets[i] = {'X': 0.0, 'Y': 0.0, 'Z': 0.0,
|
||
'A': 0.0, 'B': 0.0, 'C': 0.0,
|
||
'U': 0.0, 'V': 0.0, 'W': 0.0, 'R': 0.0}
|
||
self.current_work_offset = 1 # G54
|
||
self.g5x_index = 1
|
||
|
||
# G92偏移
|
||
self.g92_offset_x = 0.0
|
||
self.g92_offset_y = 0.0
|
||
self.g92_offset_z = 0.0
|
||
self.g92_offset_a = 0.0
|
||
self.g92_offset_b = 0.0
|
||
self.g92_offset_c = 0.0
|
||
self.g92_offset_u = 0.0
|
||
self.g92_offset_v = 0.0
|
||
self.g92_offset_w = 0.0
|
||
self.g92_active = False
|
||
|
||
# G52偏移
|
||
self.g52_offset_x = 0.0
|
||
self.g52_offset_y = 0.0
|
||
self.g52_offset_z = 0.0
|
||
self.g52_offset_a = 0.0
|
||
self.g52_offset_b = 0.0
|
||
self.g52_offset_c = 0.0
|
||
self.g52_offset_u = 0.0
|
||
self.g52_offset_v = 0.0
|
||
self.g52_offset_w = 0.0
|
||
|
||
# XY旋转
|
||
self.rotation_xy = 0.0
|
||
self.rotation_sin = 0.0
|
||
self.rotation_cos = 1.0
|
||
|
||
# G53模式
|
||
self._g53_active = False
|
||
|
||
# 工件偏移(额外的work offset)
|
||
self.work_offset_x = 0.0
|
||
self.work_offset_y = 0.0
|
||
self.work_offset_z = 0.0
|
||
self.work_offset_a = 0.0
|
||
self.work_offset_b = 0.0
|
||
self.work_offset_c = 0.0
|
||
|
||
# 刀具数据
|
||
self.tool_lengths: Dict[int, float] = {}
|
||
self.tool_radii: Dict[int, float] = {}
|
||
self.tool_table: Dict[int, ToolData] = {}
|
||
self.current_tool: Optional[ToolData] = None
|
||
self.current_h_code = 0
|
||
self.current_d_code = 0
|
||
self.selected_tool = 0
|
||
self.selected_pocket = 0
|
||
self.current_pocket = 0
|
||
|
||
# 刀具补偿
|
||
self.comp_type = CompType.OFF
|
||
self.comp_radius = 0.0
|
||
self.comp_side = 0
|
||
self.d_word = 0.0
|
||
self.cutter_comp_active = False
|
||
self.cutter_comp_firstmove = True
|
||
self.cutter_comp_orientation = 0
|
||
self.arc_not_allowed = True
|
||
self.program_x = 0.0
|
||
self.program_y = 0.0
|
||
self.program_z = 0.0
|
||
|
||
# 刀具长度补偿
|
||
self.tool_offset = Point6D(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||
self.g43_with_zero_offset = False
|
||
|
||
# 模态状态
|
||
self.is_absolute = True
|
||
self.current_plane = 17
|
||
self.feedrate = 1000.0
|
||
self.current_line = 0
|
||
self.cumulative_time = 0.0
|
||
self.motion_mode = -1
|
||
self.retract_mode = 98
|
||
self.ijk_distance_mode = 91
|
||
self.lathe_diameter_mode = False
|
||
self.return_value = 0.0
|
||
self.value_returned = False
|
||
self.remap_level = 0
|
||
self.call_level = 0
|
||
|
||
# 状态
|
||
self.state = type('State', (), {})()
|
||
self.state.plane = 17
|
||
self.state.distance_mode = 90
|
||
self.state.feedrate = 1000.0
|
||
self.state.spindle_mode = 0
|
||
self.state.spindle_speed = 0.0
|
||
self.state.spindle_state = "OFF"
|
||
self.state.coolant_mode = 9
|
||
self.state.coolant_flood = False
|
||
self.state.coolant_mist = False
|
||
self.state.tool = 0
|
||
self.state.units = 21
|
||
self.state.feed_mode = 94
|
||
|
||
self.optional_stop = False
|
||
self.block_delete = False
|
||
|
||
# 固定循环
|
||
self.canned_cycle_active = False
|
||
self.canned_cycle_type = 0
|
||
self.canned_cycle_r = 0.0
|
||
self.canned_cycle_z = 0.0
|
||
self.canned_cycle_q = 0.0
|
||
self.canned_cycle_l = 1
|
||
self.canned_cycle_p = 0.0
|
||
self.canned_cycle_il = 0.0
|
||
self.canned_cycle_il_flag = False
|
||
|
||
|
||
# ===== 添加这一行 =====
|
||
self._saved_kinematics_type = None # 用于 M428/M429 切换时保存原始类型
|
||
|
||
# ===== 添加旋转轴保存变量 =====
|
||
self._saved_rotation_a = 0.0
|
||
self._saved_rotation_b = 0.0
|
||
self._saved_rotation_c = 0.0
|
||
|
||
|
||
# 参考点
|
||
self.g28_ref: Dict[str, float] = {axis: 0.0 for axis in 'XYZABC'}
|
||
self.g30_ref: Dict[str, float] = {axis: 0.0 for axis in 'XYZABC'}
|
||
|
||
self.state_machine: Optional[MachineStateMachine] = None
|
||
self.call_counts: Dict[str, int] = {}
|
||
self.saved_states: List[Dict] = []
|
||
|
||
self._sync_machine_from_current()
|
||
|
||
# 参数表系统
|
||
self.var_manager = LinuxCNCParameterTable(debug=debug)
|
||
self.var_manager._current_line = self.current_line
|
||
|
||
self.control_handler = ControlStructureHandler(self.var_manager, debug=debug)
|
||
self._sync_parameters_from_state()
|
||
|
||
self._gcode_buffer: List[str] = []
|
||
self._parse_errors: List[str] = []
|
||
|
||
# ===== 新增:坐标系管理 =====
|
||
self._coord_mode = 'WORLD' # 当前坐标模式: 'WORLD' | 'ABSOLUTE' | 'MACHINE'
|
||
self._last_segment_was_arc = False
|
||
|
||
# 初始化参数表中的当前位置
|
||
self._init_params_position()
|
||
|
||
if self.debug:
|
||
print(f"GLCanonPathCollectorWithRTCP 初始化完成")
|
||
print(f" 运动学类型: {kinematics_type.name}")
|
||
print(f" RTCP状态: {'启用' if self.rtcp_enabled else '禁用'}")
|
||
|
||
def _init_params_position(self):
|
||
"""初始化参数表中的当前位置为 (0,0,0,0,0,0)"""
|
||
self.var_manager._current_position = {
|
||
'X': self.current_pos.x,
|
||
'Y': self.current_pos.y,
|
||
'Z': self.current_pos.z,
|
||
'A': self.current_pos.a,
|
||
'B': self.current_pos.b,
|
||
'C': self.current_pos.c,
|
||
}
|
||
|
||
|
||
def _sync_position_to_params(self):
|
||
"""同步当前位置到参数表(5420-5428 相对位置参数)"""
|
||
# 工件坐标
|
||
self.var_manager._current_position['X'] = self.current_pos.x
|
||
self.var_manager._current_position['Y'] = self.current_pos.y
|
||
self.var_manager._current_position['Z'] = self.current_pos.z
|
||
self.var_manager._current_position['A'] = self.current_pos.a
|
||
self.var_manager._current_position['B'] = self.current_pos.b
|
||
self.var_manager._current_position['C'] = self.current_pos.c
|
||
|
||
# 同步到参数 5420-5428
|
||
self.var_manager._params[5420] = self.current_pos.x
|
||
self.var_manager._params[5421] = self.current_pos.y
|
||
self.var_manager._params[5422] = self.current_pos.z
|
||
self.var_manager._params[5423] = self.current_pos.a
|
||
self.var_manager._params[5424] = self.current_pos.b
|
||
self.var_manager._params[5425] = self.current_pos.c
|
||
|
||
|
||
|
||
def _compute_machine_coords(self, wx: float, wy: float, wz: float,
|
||
wa: float, wb: float, wc: float,
|
||
wu: float = 0.0, wv: float = 0.0, ww: float = 0.0
|
||
) -> Tuple[float, float, float, float, float, float, float, float, float]:
|
||
"""
|
||
计算机器坐标(关节坐标)
|
||
|
||
参照 LinuxCNC 源码的坐标转换链:
|
||
rs274ngc.cpp: find_current_in_system() + kinematics.cpp: kinematicsInverse()
|
||
|
||
转换步骤(与 _sync_machine_from_current 保持一致):
|
||
1. 工件坐标 + G92偏移 + XY旋转 + G5x偏移 + TLO + G52偏移 = 绝对坐标
|
||
2. 如果 RTCP 启用,调用逆运动学计算关节坐标
|
||
3. 如果 RTCP 禁用,直接返回绝对坐标
|
||
|
||
对应 C++ 中:
|
||
- settings->current_x → 工件坐标
|
||
- settings->tool_offset.tran.z → TLO
|
||
- motion controller 调用 kinematicsInverse() → 关节坐标
|
||
"""
|
||
# ===== 第1步:加上 G92 偏移 (axis_offset) =====
|
||
# 对应 C++: *x += s->axis_offset_x
|
||
abs_x = wx + self.g92_offset_x
|
||
abs_y = wy + self.g92_offset_y
|
||
abs_z = wz + self.g92_offset_z
|
||
abs_a = wa + self.g92_offset_a
|
||
abs_b = wb + self.g92_offset_b
|
||
abs_c = wc + self.g92_offset_c
|
||
abs_u = wu + self.g92_offset_u
|
||
abs_v = wv + self.g92_offset_v
|
||
abs_w = ww + self.g92_offset_w
|
||
|
||
# ===== 第2步:XY 旋转 =====
|
||
# 对应 C++: rotate(x, y, s->rotation_xy)
|
||
if abs(self.rotation_xy) > 1e-9:
|
||
rot = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot)
|
||
sin_r = math.sin(rot)
|
||
rx = abs_x * cos_r - abs_y * sin_r
|
||
ry = abs_x * sin_r + abs_y * cos_r
|
||
abs_x, abs_y = rx, ry
|
||
|
||
# ===== 第3步:加上 G5x 偏移 (origin_offset) =====
|
||
# 对应 C++: *x += s->origin_offset_x
|
||
abs_x += self.g5x_offset_x
|
||
abs_y += self.g5x_offset_y
|
||
abs_z += self.g5x_offset_z
|
||
abs_a += self.g5x_offset_a
|
||
abs_b += self.g5x_offset_b
|
||
abs_c += self.g5x_offset_c
|
||
abs_u += self.g5x_offset_u
|
||
abs_v += self.g5x_offset_v
|
||
abs_w += self.g5x_offset_w
|
||
|
||
# ===== 第4步:加上刀具长度补偿 (tool_offset) =====
|
||
# 对应 C++: *x += s->tool_offset.tran.x
|
||
# 这是 TLO,在 convert_tool_length_offset 中设置
|
||
abs_x += self.tool_offset.x
|
||
abs_y += self.tool_offset.y
|
||
abs_z += self.tool_offset.z
|
||
abs_a += self.tool_offset.a
|
||
abs_b += self.tool_offset.b
|
||
abs_c += self.tool_offset.c
|
||
abs_u += self.tool_offset.u
|
||
abs_v += self.tool_offset.v
|
||
abs_w += self.tool_offset.w
|
||
|
||
# ===== 第5步:加上 G52 偏移 =====
|
||
abs_x += self.g52_offset_x
|
||
abs_y += self.g52_offset_y
|
||
abs_z += self.g52_offset_z
|
||
abs_a += self.g52_offset_a
|
||
abs_b += self.g52_offset_b
|
||
abs_c += self.g52_offset_c
|
||
|
||
# ===== 第6步:RTCP 逆运动学 =====
|
||
# 对应 C++: motion controller 调用 kinematicsInverse()
|
||
# 传入的是包含 TLO 的绝对坐标
|
||
if self.rtcp_enabled and self.kinematics is not None:
|
||
try:
|
||
# 将绝对坐标传给运动学逆解
|
||
world_tuple = (abs_x, abs_y, abs_z, abs_a, abs_b, abs_c)
|
||
joints = self.kinematics.world_to_joints(world_tuple)
|
||
|
||
# 使用关节映射函数转换为标准9轴格式
|
||
config = KINEMATICS_JOINT_CONFIG.get(
|
||
self.kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY]
|
||
)
|
||
|
||
result = [0.0] * 9
|
||
for i in range(min(len(joints), 9)):
|
||
result[i] = joints[i]
|
||
|
||
# 根据运动学类型映射关节
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
return (
|
||
joints[x_idx] if (x_idx >= 0 and x_idx < len(joints)) else abs_x,
|
||
joints[y_idx] if (y_idx >= 0 and y_idx < len(joints)) else abs_y,
|
||
joints[z_idx] if (z_idx >= 0 and z_idx < len(joints)) else abs_z,
|
||
joints[a_idx] if (a_idx >= 0 and a_idx < len(joints)) else abs_a,
|
||
joints[b_idx] if (b_idx >= 0 and b_idx < len(joints)) else abs_b,
|
||
joints[c_idx] if (c_idx >= 0 and c_idx < len(joints)) else abs_c,
|
||
result[6], result[7], result[8]
|
||
)
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[MCS] 逆运动学计算失败: {e}")
|
||
traceback.print_exc()
|
||
# 逆解失败时返回绝对坐标作为后备
|
||
return (abs_x, abs_y, abs_z, abs_a, abs_b, abs_c, abs_u, abs_v, abs_w)
|
||
else:
|
||
# RTCP 禁用:直接返回绝对坐标
|
||
return (abs_x, abs_y, abs_z, abs_a, abs_b, abs_c, abs_u, abs_v, abs_w)
|
||
|
||
|
||
def _add_world_segment(self, move_type: MoveType, end_world: Point6D, **kwargs):
|
||
"""
|
||
统一的运动段添加方法 - 基于参数表进行坐标管理
|
||
|
||
所有运动段(直线、圆弧、暂停等)都通过此方法创建。
|
||
|
||
核心原则(参照 LinuxCNC 源码设计):
|
||
- segment.start/end 使用工件坐标系 (G代码编程坐标)
|
||
- segment.world_start/world_end 是工件坐标的副本
|
||
- segment.machine_start/machine_end 通过逆运动学计算关节坐标
|
||
- 几何验证在工件坐标系下进行
|
||
|
||
参数:
|
||
move_type: 运动类型 (RAPID, FEED, DWELL, PROGRAM_END等)
|
||
end_world: 工件坐标系下的目标位置
|
||
**kwargs: 额外参数
|
||
- center_x, center_y, center_z: 圆弧圆心(工件坐标)
|
||
- radius: 圆弧半径
|
||
- turn: 圆弧圈数
|
||
- dwell_time: 暂停时间
|
||
- gcode: G代码字符串
|
||
"""
|
||
if self._point_count >= self.max_points:
|
||
return
|
||
|
||
# 1. 工件坐标系下的起点
|
||
start_world = self.current_pos.copy()
|
||
|
||
# 2. 计算直线距离
|
||
if move_type in [MoveType.RAPID, MoveType.FEED]:
|
||
linear_distance = start_world.distance_to(end_world)
|
||
else:
|
||
linear_distance = 0.0
|
||
|
||
# 3. 规划速度曲线
|
||
if move_type == MoveType.RAPID:
|
||
result = self.planner.calculate_time(linear_distance, None, is_rapid=True)
|
||
elif move_type == MoveType.FEED:
|
||
result = self.planner.calculate_time(linear_distance, self.feedrate, is_rapid=False)
|
||
elif move_type == MoveType.DWELL:
|
||
dwell_time = kwargs.get('dwell_time', 0)
|
||
result = {
|
||
'duration': dwell_time,
|
||
'max_velocity': 0.0,
|
||
'acceleration_time': 0.0,
|
||
'constant_time': 0.0,
|
||
'deceleration_time': 0.0,
|
||
'profile_type': 'dwell'
|
||
}
|
||
elif move_type == MoveType.PROGRAM_END:
|
||
dwell_time = kwargs.get('dwell_time', 0.1)
|
||
result = {
|
||
'duration': dwell_time,
|
||
'max_velocity': 0.0,
|
||
'acceleration_time': 0.0,
|
||
'constant_time': 0.0,
|
||
'deceleration_time': 0.0,
|
||
'profile_type': 'program_end'
|
||
}
|
||
else:
|
||
result = {
|
||
'duration': 0.1,
|
||
'max_velocity': 0.0,
|
||
'acceleration_time': 0.0,
|
||
'constant_time': 0.0,
|
||
'deceleration_time': 0.0,
|
||
'profile_type': 'other'
|
||
}
|
||
|
||
# 4. 计算机器坐标(包含逆运动学)
|
||
# ★ 修改:使用 _compute_machine_coords 替代 program_to_absolute
|
||
# 参照 LinuxCNC: kinematicsInverse() 接收绝对坐标
|
||
abs_start_tuple = self._compute_machine_coords(
|
||
start_world.x, start_world.y, start_world.z,
|
||
start_world.a, start_world.b, start_world.c,
|
||
getattr(start_world, 'u', 0.0),
|
||
getattr(start_world, 'v', 0.0),
|
||
getattr(start_world, 'w', 0.0)
|
||
)
|
||
abs_end_tuple = self._compute_machine_coords(
|
||
end_world.x, end_world.y, end_world.z,
|
||
end_world.a, end_world.b, end_world.c,
|
||
getattr(end_world, 'u', 0.0),
|
||
getattr(end_world, 'v', 0.0),
|
||
getattr(end_world, 'w', 0.0)
|
||
)
|
||
|
||
abs_start_pt = Point6D(
|
||
abs_start_tuple[0], abs_start_tuple[1], abs_start_tuple[2],
|
||
abs_start_tuple[3], abs_start_tuple[4], abs_start_tuple[5]
|
||
)
|
||
abs_end_pt = Point6D(
|
||
abs_end_tuple[0], abs_end_tuple[1], abs_end_tuple[2],
|
||
abs_end_tuple[3], abs_end_tuple[4], abs_end_tuple[5]
|
||
)
|
||
|
||
# 5. 创建 MoveSegment
|
||
segment = MoveSegment(
|
||
type=move_type,
|
||
start=start_world.copy(), # 工件坐标
|
||
end=end_world.copy(), # 工件坐标
|
||
line_number=self.current_line,
|
||
feedrate=self.feedrate if move_type == MoveType.FEED else 0.0,
|
||
comp_type=self.comp_type,
|
||
comp_radius=self.comp_radius,
|
||
d_word=self.d_word,
|
||
duration=result['duration'],
|
||
max_velocity=result['max_velocity'],
|
||
acceleration_time=result['acceleration_time'],
|
||
constant_time=result['constant_time'],
|
||
deceleration_time=result['deceleration_time'],
|
||
profile_type=result['profile_type'],
|
||
is_rtcp=self.rtcp_enabled,
|
||
world_start=start_world.copy(),
|
||
world_end=end_world.copy(),
|
||
machine_start=abs_start_pt, # ★ 机器坐标(已通过逆解)
|
||
machine_end=abs_end_pt, # ★ 机器坐标(已通过逆解)
|
||
tool_number=self.state.tool,
|
||
spindle_speed=self.state.spindle_speed,
|
||
spindle_state=self.state.spindle_state,
|
||
coord_system='WORLD',
|
||
active_csys=self.var_manager.active_csys,
|
||
g92_active=self.var_manager._g92_active,
|
||
**{k: v for k, v in kwargs.items()
|
||
if k in ['center_x', 'center_y', 'center_z', 'radius',
|
||
'turn', 'dwell_time', 'gcode']}
|
||
)
|
||
|
||
# 6. 添加到路径数据
|
||
self.data.segments.append(segment)
|
||
|
||
if move_type in [MoveType.RAPID, MoveType.FEED]:
|
||
self.data.total_length += linear_distance
|
||
|
||
self.cumulative_time += result['duration']
|
||
self._point_count += 1
|
||
|
||
# 7. 更新工件坐标位置
|
||
self.current_pos = end_world.copy()
|
||
|
||
# 8. 同步到参数表
|
||
self._sync_position_to_params()
|
||
|
||
# 9. 同步机器坐标
|
||
self._sync_machine_from_current()
|
||
|
||
# 10. 同步所有参数
|
||
self._sync_parameters_from_state()
|
||
|
||
# 11. 标记运动段类型
|
||
self._last_segment_was_arc = move_type in [MoveType.ARC_CW, MoveType.ARC_CCW]
|
||
|
||
|
||
def find_or_create_tool(self, tool_number: int) -> int:
|
||
"""
|
||
查找或创建刀具,返回刀具索引
|
||
对应 C++ 中的 find_tool_index -> tooldata_find_index_for_tool
|
||
"""
|
||
# 先在现有刀具表中查找
|
||
for idx, tool in self.tool_table.items():
|
||
if tool.tool_number == tool_number:
|
||
return idx
|
||
|
||
# 创建新刀具
|
||
# 查找一个可用的 pocket
|
||
new_pocket = tool_number
|
||
if new_pocket in self.tool_table:
|
||
# 找到一个未使用的 pocket
|
||
for p in range(1, 1000):
|
||
if p not in self.tool_table:
|
||
new_pocket = p
|
||
break
|
||
|
||
new_tool = ToolData(
|
||
tool_number=tool_number,
|
||
pocket=new_pocket,
|
||
diameter=0.0,
|
||
radius=0.0,
|
||
front_angle=0.0,
|
||
back_angle=0.0,
|
||
orientation=0,
|
||
name=f"T{tool_number}"
|
||
)
|
||
self.tool_table[new_pocket] = new_tool
|
||
|
||
# 初始化刀具长度和半径映射
|
||
if tool_number not in self.tool_lengths:
|
||
self.tool_lengths[tool_number] = 0.0
|
||
if tool_number not in self.tool_radii:
|
||
self.tool_radii[tool_number] = 0.0
|
||
|
||
if self.debug:
|
||
print(f"[TOOL] 创建新刀具 T{tool_number} in pocket {new_pocket}")
|
||
|
||
return new_pocket
|
||
|
||
def _get_csys_rotation(self, csys_num: int) -> float:
|
||
"""
|
||
获取指定坐标系的 XY 旋转角度
|
||
对应 C++ 中 parameters[5210 + (origin * 20)]
|
||
"""
|
||
if csys_num in self.work_offsets:
|
||
return self.work_offsets[csys_num].get('R', 0.0)
|
||
return 0.0
|
||
|
||
def _get_current_in_system_without_tlo(self, system: int):
|
||
"""
|
||
计算当前在指定坐标系中的位置(不带刀具长度补偿)
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::find_current_in_system_without_tlo
|
||
|
||
变换顺序:
|
||
1. 从工件坐标开始
|
||
2. 加上 G92 偏移 (axis_offset)
|
||
3. 加上 G52 偏移
|
||
4. XY 旋转(正向, rotation_xy)
|
||
5. 加上 G5x 偏移 (origin_offset)
|
||
6. 加上刀具长度补偿 (tool_offset)
|
||
7. 减去目标坐标系的 G5x 偏移
|
||
8. XY 旋转(反向, 目标坐标系的 rotation_xy)
|
||
9. 如果 G92 激活且目标是夹具坐标系(9=G59.3),减去 G92 偏移
|
||
|
||
用于 G10 L10/L11 计算刀具偏移
|
||
|
||
Args:
|
||
system: 坐标系编号 (1-9, 对应 G54-G59.3)
|
||
|
||
Returns:
|
||
tuple: (x, y, z, a, b, c, u, v, w) 在目标坐标系中的坐标(不含TLO)
|
||
"""
|
||
# ===== 第1步:从工件坐标开始 =====
|
||
x = self.current_pos.x
|
||
y = self.current_pos.y
|
||
z = self.current_pos.z
|
||
a = self.current_pos.a
|
||
b = self.current_pos.b
|
||
c = self.current_pos.c
|
||
u = getattr(self.current_pos, 'u', 0.0)
|
||
v = getattr(self.current_pos, 'v', 0.0)
|
||
w = getattr(self.current_pos, 'w', 0.0)
|
||
|
||
# ===== 第2步:加上 G92 偏移 (axis_offset) =====
|
||
x += self.g92_offset_x
|
||
y += self.g92_offset_y
|
||
z += self.g92_offset_z
|
||
a += self.g92_offset_a
|
||
b += self.g92_offset_b
|
||
c += self.g92_offset_c
|
||
u += self.g92_offset_u
|
||
v += self.g92_offset_v
|
||
w += self.g92_offset_w
|
||
|
||
# ===== 第3步:加上 G52 偏移 =====
|
||
x += self.g52_offset_x
|
||
y += self.g52_offset_y
|
||
z += self.g52_offset_z
|
||
a += self.g52_offset_a
|
||
b += self.g52_offset_b
|
||
c += self.g52_offset_c
|
||
u += self.g52_offset_u
|
||
v += self.g52_offset_v
|
||
w += self.g52_offset_w
|
||
|
||
# ===== 第4步:XY 旋转(正向, rotation_xy)=====
|
||
if self.rotation_xy != 0:
|
||
rot_rad = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
|
||
rot_x = x * cos_r - y * sin_r
|
||
rot_y = x * sin_r + y * cos_r
|
||
x, y = rot_x, rot_y
|
||
|
||
# ===== 第5步:加上 G5x 偏移 (origin_offset) =====
|
||
x += self.g5x_offset_x
|
||
y += self.g5x_offset_y
|
||
z += self.g5x_offset_z
|
||
a += self.g5x_offset_a
|
||
b += self.g5x_offset_b
|
||
c += self.g5x_offset_c
|
||
u += self.g5x_offset_u
|
||
v += self.g5x_offset_v
|
||
w += self.g5x_offset_w
|
||
|
||
# ===== 第6步:加上刀具长度补偿 (tool_offset) ★ 修复:新增步骤 =====
|
||
x += self.tool_offset.x
|
||
y += self.tool_offset.y
|
||
z += self.tool_offset.z
|
||
a += self.tool_offset.a
|
||
b += self.tool_offset.b
|
||
c += self.tool_offset.c
|
||
u += self.tool_offset.u
|
||
v += self.tool_offset.v
|
||
w += self.tool_offset.w
|
||
|
||
# ===== 第7步:减去目标坐标系的 G5x 偏移 =====
|
||
target_offsets = self.work_offsets.get(system, {})
|
||
|
||
x -= target_offsets.get('X', 0.0)
|
||
y -= target_offsets.get('Y', 0.0)
|
||
z -= target_offsets.get('Z', 0.0)
|
||
a -= target_offsets.get('A', 0.0)
|
||
b -= target_offsets.get('B', 0.0)
|
||
c -= target_offsets.get('C', 0.0)
|
||
u -= target_offsets.get('U', 0.0)
|
||
v -= target_offsets.get('V', 0.0)
|
||
w -= target_offsets.get('W', 0.0)
|
||
|
||
# ===== 第8步:XY 旋转(反向, 目标坐标系的 rotation_xy)=====
|
||
target_rotation = target_offsets.get('R', 0.0)
|
||
if target_rotation != 0:
|
||
rot_rad = math.radians(-target_rotation)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
|
||
rot_x = x * cos_r - y * sin_r
|
||
rot_y = x * sin_r + y * cos_r
|
||
x, y = rot_x, rot_y
|
||
|
||
# ===== 第9步:如果 G92 激活,减去 G92 偏移 =====
|
||
if self.g92_active:
|
||
x -= self.g92_offset_x
|
||
y -= self.g92_offset_y
|
||
z -= self.g92_offset_z
|
||
a -= self.g92_offset_a
|
||
b -= self.g92_offset_b
|
||
c -= self.g92_offset_c
|
||
u -= self.g92_offset_u
|
||
v -= self.g92_offset_v
|
||
w -= self.g92_offset_w
|
||
|
||
if self.debug:
|
||
print(f"[CSYS] _get_current_in_system_without_tlo(system={system}):")
|
||
print(f" 工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
print(f" 目标坐标(不含TLO): ({x:.3f}, {y:.3f}, {z:.3f})")
|
||
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
|
||
def _get_current_in_system(self, system: int) -> Tuple[float, float, float, float, float, float, float, float, float]:
|
||
"""
|
||
计算当前在指定坐标系中的位置(带刀具长度补偿)
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::find_current_in_system
|
||
|
||
变换顺序:
|
||
1. 从工件坐标开始
|
||
2. 加上 G92 偏移 (axis_offset)
|
||
3. 加上 G52 偏移
|
||
4. XY 旋转(正向, rotation_xy)
|
||
5. 加上 G5x 偏移 (origin_offset)
|
||
6. 加上刀具长度补偿 (tool_offset)
|
||
7. 减去目标坐标系的 G5x 偏移
|
||
8. XY 旋转(反向, 目标坐标系的 rotation_xy)
|
||
9. 如果 G92 激活,减去 G92 偏移
|
||
|
||
与 _get_current_in_system_without_tlo 的区别:
|
||
- 此方法包含刀具长度补偿
|
||
- 用于 G10 L20 和坐标系切换 (G54-G59)
|
||
|
||
Args:
|
||
system: 坐标系编号 (1-9, 对应 G54-G59.3)
|
||
|
||
Returns:
|
||
tuple: (x, y, z, a, b, c, u, v, w) 在目标坐标系中的坐标
|
||
"""
|
||
# ===== 第1步:从工件坐标开始 =====
|
||
x = self.current_pos.x
|
||
y = self.current_pos.y
|
||
z = self.current_pos.z
|
||
a = self.current_pos.a
|
||
b = self.current_pos.b
|
||
c = self.current_pos.c
|
||
u = getattr(self.current_pos, 'u', 0.0)
|
||
v = getattr(self.current_pos, 'v', 0.0)
|
||
w = getattr(self.current_pos, 'w', 0.0)
|
||
|
||
# ===== 第2步:加上 G92 偏移 (axis_offset) =====
|
||
# 对应官方: x += s->axis_offset_x; ...
|
||
x += self.g92_offset_x
|
||
y += self.g92_offset_y
|
||
z += self.g92_offset_z
|
||
a += self.g92_offset_a
|
||
b += self.g92_offset_b
|
||
c += self.g92_offset_c
|
||
u += self.g92_offset_u
|
||
v += self.g92_offset_v
|
||
w += self.g92_offset_w
|
||
|
||
# ===== 第3步:加上 G52 偏移 =====
|
||
x += self.g52_offset_x
|
||
y += self.g52_offset_y
|
||
z += self.g52_offset_z
|
||
a += self.g52_offset_a
|
||
b += self.g52_offset_b
|
||
c += self.g52_offset_c
|
||
u += self.g52_offset_u
|
||
v += self.g52_offset_v
|
||
w += self.g52_offset_w
|
||
|
||
# ===== 第4步:XY 旋转(正向, rotation_xy)=====
|
||
# 对应官方: rotate(x, y, s->rotation_xy);
|
||
if self.rotation_xy != 0:
|
||
rot_rad = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
|
||
# rotate(&x, &y, theta):
|
||
# xx = *x * cos(t) - *y * sin(t)
|
||
# yy = *x * sin(t) + *y * cos(t)
|
||
rot_x = x * cos_r - y * sin_r
|
||
rot_y = x * sin_r + y * cos_r
|
||
x, y = rot_x, rot_y
|
||
|
||
# ===== 第5步:加上 G5x 偏移 (origin_offset) =====
|
||
# 对应官方: x += s->origin_offset_x; ...
|
||
x += self.g5x_offset_x
|
||
y += self.g5x_offset_y
|
||
z += self.g5x_offset_z
|
||
a += self.g5x_offset_a
|
||
b += self.g5x_offset_b
|
||
c += self.g5x_offset_c
|
||
u += self.g5x_offset_u
|
||
v += self.g5x_offset_v
|
||
w += self.g5x_offset_w
|
||
|
||
# ===== 第6步:加上刀具长度补偿 (tool_offset) =====
|
||
# 对应官方: x += s->tool_offset.tran.x; ...
|
||
x += self.tool_offset.x
|
||
y += self.tool_offset.y
|
||
z += self.tool_offset.z
|
||
a += self.tool_offset.a
|
||
b += self.tool_offset.b
|
||
c += self.tool_offset.c
|
||
u += self.tool_offset.u
|
||
v += self.tool_offset.v
|
||
w += self.tool_offset.w
|
||
|
||
# ===== 第7步:减去目标坐标系的 G5x 偏移 =====
|
||
# 对应官方:
|
||
# *x -= USER_TO_PROGRAM_LEN(p[5201 + system * 20]);
|
||
# *y -= USER_TO_PROGRAM_LEN(p[5202 + system * 20]);
|
||
target_offsets = self.work_offsets.get(system, {})
|
||
|
||
x -= target_offsets.get('X', 0.0)
|
||
y -= target_offsets.get('Y', 0.0)
|
||
z -= target_offsets.get('Z', 0.0)
|
||
a -= target_offsets.get('A', 0.0)
|
||
b -= target_offsets.get('B', 0.0)
|
||
c -= target_offsets.get('C', 0.0)
|
||
u -= target_offsets.get('U', 0.0)
|
||
v -= target_offsets.get('V', 0.0)
|
||
w -= target_offsets.get('W', 0.0)
|
||
|
||
# ===== 第8步:XY 旋转(反向, 目标坐标系的 rotation_xy)=====
|
||
# 对应官方: rotate(x, y, -p[5210 + system * 20]);
|
||
target_rotation = target_offsets.get('R', 0.0)
|
||
if target_rotation != 0:
|
||
rot_rad = math.radians(-target_rotation)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
|
||
rot_x = x * cos_r - y * sin_r
|
||
rot_y = x * sin_r + y * cos_r
|
||
x, y = rot_x, rot_y
|
||
|
||
# ===== 第9步:如果 G92 激活,减去 G92 偏移 =====
|
||
# 对应官方:
|
||
# if (p[5210]) {
|
||
# *x -= USER_TO_PROGRAM_LEN(p[5211]);
|
||
# ...
|
||
# }
|
||
if self.g92_active:
|
||
x -= self.g92_offset_x
|
||
y -= self.g92_offset_y
|
||
z -= self.g92_offset_z
|
||
a -= self.g92_offset_a
|
||
b -= self.g92_offset_b
|
||
c -= self.g92_offset_c
|
||
u -= self.g92_offset_u
|
||
v -= self.g92_offset_v
|
||
w -= self.g92_offset_w
|
||
|
||
if self.debug:
|
||
print(f"[CSYS] _get_current_in_system(system={system}):")
|
||
print(f" 工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
print(f" 目标坐标: ({x:.3f}, {y:.3f}, {z:.3f})")
|
||
|
||
return (x, y, z, a, b, c, u, v, w)
|
||
|
||
|
||
def _sync_tool_parameters_to_table(self, tool_idx: int):
|
||
"""
|
||
同步刀具参数到参数表(5400-5413)和 HAL
|
||
|
||
对应 C++ 中 convert_setup_tool 中的:
|
||
- set_tool_parameters()
|
||
- SET_TOOL_TABLE_ENTRY()
|
||
"""
|
||
if tool_idx < 0 or tool_idx not in self.tool_table:
|
||
return
|
||
|
||
tool = self.tool_table[tool_idx]
|
||
|
||
# ===== 同步到参数表 (#5400-#5413) =====
|
||
# 对应 C++ 中 setup::set_tool_parameters()
|
||
|
||
# 5400: 刀具号
|
||
# 随机换刀: 5400 = tool_table[0].toolno (如果 >= 0) 否则 -1
|
||
# 非随机换刀: 5400 = tool_table[0].toolno (如果 > 0) 否则 0
|
||
if self.current_tool and self.current_tool.tool_number == tool.tool_number:
|
||
self.var_manager._params[5400] = float(tool.tool_number)
|
||
elif tool_idx == 0:
|
||
self.var_manager._params[5400] = float(tool.tool_number) if tool.tool_number > 0 else 0.0
|
||
else:
|
||
# 非当前刀具,不影响 5400
|
||
pass
|
||
|
||
# 5401-5409: 刀具偏移
|
||
self.var_manager._params[5401] = tool.offset.x
|
||
self.var_manager._params[5402] = tool.offset.y
|
||
self.var_manager._params[5403] = tool.offset.z
|
||
self.var_manager._params[5404] = tool.offset.a
|
||
self.var_manager._params[5405] = tool.offset.b
|
||
self.var_manager._params[5406] = tool.offset.c
|
||
self.var_manager._params[5407] = tool.offset.u
|
||
self.var_manager._params[5408] = tool.offset.v
|
||
self.var_manager._params[5409] = tool.offset.w
|
||
|
||
# 5410: 刀具直径
|
||
self.var_manager._params[5410] = tool.diameter
|
||
|
||
# 5411: 前角
|
||
self.var_manager._params[5411] = tool.front_angle
|
||
|
||
# 5412: 后角
|
||
self.var_manager._params[5412] = tool.back_angle
|
||
|
||
# 5413: 刀具方向
|
||
self.var_manager._params[5413] = float(tool.orientation)
|
||
|
||
# ===== 同步到 HAL 组件 =====
|
||
# 对应 C++ 中 SET_TOOL_TABLE_ENTRY()
|
||
if hasattr(self, 'virtual_hal') and self.virtual_hal:
|
||
# 更新刀具相关 HAL 引脚
|
||
self.virtual_hal.set('tool.number', tool.tool_number)
|
||
self.virtual_hal.set('tool.diameter', tool.diameter)
|
||
self.virtual_hal.set('tool.length', tool.offset.z)
|
||
|
||
# ===== 如果是当前刀具,更新 tool_offset =====
|
||
if self.current_tool and self.current_tool.tool_number == tool.tool_number:
|
||
self.tool_offset.x = tool.offset.x
|
||
self.tool_offset.y = tool.offset.y
|
||
self.tool_offset.z = tool.offset.z
|
||
self.tool_offset.a = tool.offset.a
|
||
self.tool_offset.b = tool.offset.b
|
||
self.tool_offset.c = tool.offset.c
|
||
|
||
# 更新运动学的刀具长度
|
||
if self.kinematics and self.current_h_code > 0:
|
||
self.kinematics.set_tool_length(tool.offset.z)
|
||
|
||
if self.debug:
|
||
tool = self.tool_table[tool_idx]
|
||
print(f"[SYNC] 刀具 T{tool.tool_number} 参数已同步:")
|
||
print(f" #5400(ToolNo)={self.var_manager._params[5400]:.0f}")
|
||
print(f" #5403(Z-Off)={self.var_manager._params[5403]:.3f}")
|
||
print(f" #5410(Dia)={self.var_manager._params[5410]:.3f}")
|
||
|
||
|
||
def _handle_g10_current_tool_update(self, tool_idx: int, tool_number: int):
|
||
"""
|
||
当 G10 修改的是当前刀具时,更新所有相关状态
|
||
|
||
对应 C++ 中 convert_setup_tool 的最后部分:
|
||
- 更新 tool_table[0](如果非随机换刀)
|
||
- 更新参数 5400-5413
|
||
- 调用 SET_TOOL_TABLE_ENTRY
|
||
"""
|
||
if not self.current_tool or self.current_tool.tool_number != tool_number:
|
||
return
|
||
|
||
tool = self.tool_table[tool_idx]
|
||
|
||
# 更新当前刀具偏移
|
||
self.tool_offset.x = tool.offset.x
|
||
self.tool_offset.y = tool.offset.y
|
||
self.tool_offset.z = tool.offset.z
|
||
self.tool_offset.a = tool.offset.a
|
||
self.tool_offset.b = tool.offset.b
|
||
self.tool_offset.c = tool.offset.c
|
||
|
||
# 更新刀具长度映射
|
||
self.tool_lengths[tool_number] = tool.offset.z
|
||
|
||
# 更新刀具半径映射
|
||
self.tool_radii[tool_number] = tool.radius
|
||
|
||
# 如果有长度补偿激活,更新运动学
|
||
if self.current_h_code > 0 and self.kinematics:
|
||
self.kinematics.set_tool_length(tool.offset.z)
|
||
|
||
# 如果有半径补偿激活,更新半径
|
||
if self.cutter_comp_active:
|
||
self.comp_radius = tool.radius
|
||
|
||
# 同步到参数表
|
||
self.var_manager.update_tool_params(
|
||
tool.tool_number,
|
||
{
|
||
'x': tool.offset.x, 'y': tool.offset.y, 'z': tool.offset.z,
|
||
'a': tool.offset.a, 'b': tool.offset.b, 'c': tool.offset.c,
|
||
'u': tool.offset.u, 'v': tool.offset.v, 'w': tool.offset.w
|
||
},
|
||
tool.diameter,
|
||
tool.front_angle,
|
||
tool.back_angle,
|
||
tool.orientation
|
||
)
|
||
|
||
if self.debug:
|
||
print(f"[G10] 当前刀具 T{tool_number} 已更新")
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 参数同步 ====================
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
def _sync_parameters_from_state(self):
|
||
"""同步状态到参数表"""
|
||
self.var_manager.update_position_params(
|
||
self.current_pos.x, self.current_pos.y, self.current_pos.z,
|
||
self.current_pos.a, self.current_pos.b, self.current_pos.c
|
||
)
|
||
|
||
self.var_manager.active_csys = self.g5x_index
|
||
|
||
# 同步 G92 偏移
|
||
self.var_manager._g92_active = self.g92_active
|
||
self.var_manager._g92_offsets['X'] = self.g92_offset_x
|
||
self.var_manager._g92_offsets['Y'] = self.g92_offset_y
|
||
self.var_manager._g92_offsets['Z'] = self.g92_offset_z
|
||
self.var_manager._g92_offsets['A'] = self.g92_offset_a
|
||
self.var_manager._g92_offsets['B'] = self.g92_offset_b
|
||
self.var_manager._g92_offsets['C'] = self.g92_offset_c
|
||
|
||
# 同步 G52 偏移
|
||
self.var_manager._g52_offsets['X'] = self.g52_offset_x
|
||
self.var_manager._g52_offsets['Y'] = self.g52_offset_y
|
||
self.var_manager._g52_offsets['Z'] = self.g52_offset_z
|
||
|
||
# 同步刀具偏移
|
||
self.var_manager._tool_offset['X'] = self.tool_offset.x
|
||
self.var_manager._tool_offset['Y'] = self.tool_offset.y
|
||
self.var_manager._tool_offset['Z'] = self.tool_offset.z
|
||
self.var_manager._tool_offset['A'] = self.tool_offset.a
|
||
self.var_manager._tool_offset['B'] = self.tool_offset.b
|
||
self.var_manager._tool_offset['C'] = self.tool_offset.c
|
||
self.var_manager._tool_offset_active = (self.current_h_code > 0)
|
||
|
||
# 同步 XY 旋转
|
||
self.var_manager.set_xy_rotation(self.rotation_xy)
|
||
|
||
# 同步 G5x 坐标系偏移
|
||
if self.g5x_index in self.work_offsets:
|
||
wo = self.work_offsets[self.g5x_index]
|
||
self.var_manager.set_csys_origin(
|
||
self.g5x_index,
|
||
wo.get('X', 0.0), wo.get('Y', 0.0), wo.get('Z', 0.0),
|
||
wo.get('A', 0.0), wo.get('B', 0.0), wo.get('C', 0.0)
|
||
)
|
||
|
||
# 同步模态状态
|
||
self.var_manager._distance_mode = 'ABSOLUTE' if self.is_absolute else 'INCREMENTAL'
|
||
self.var_manager._plane_mode = {17: 'XY', 18: 'XZ', 19: 'YZ'}.get(self.current_plane, 'XY')
|
||
self.var_manager._feed_rate = self.feedrate
|
||
self.var_manager._spindle_speed = self.state.spindle_speed
|
||
|
||
self.var_manager.set_system('_x', self.current_pos.x)
|
||
self.var_manager.set_system('_y', self.current_pos.y)
|
||
self.var_manager.set_system('_z', self.current_pos.z)
|
||
self.var_manager.set_system('_feed', self.feedrate)
|
||
self.var_manager.set_system('_current_tool', float(self.state.tool))
|
||
self.var_manager.set_system('_rpm', self.state.spindle_speed)
|
||
self.var_manager.set_system('_absolute', 1.0 if self.is_absolute else 0.0)
|
||
self.var_manager.set_system('_metric', 1.0 if self.state.units == 21 else 0.0)
|
||
self.var_manager._current_line = self.current_line
|
||
|
||
|
||
def _sync_machine_from_current(self):
|
||
"""从工件坐标同步机器坐标
|
||
|
||
正变换顺序(与 _compute_machine_coords 严格对应):
|
||
工件坐标 → +G92偏移 → +XY旋转 → +G5x偏移 → +TLO → +G52偏移 → 逆运动学 → 关节坐标
|
||
|
||
参照 LinuxCNC 源码:
|
||
- rs274ngc.cpp: find_current_in_system() 计算绝对坐标
|
||
- kinematics.cpp: kinematicsInverse() 计算关节坐标
|
||
"""
|
||
# ===== 第1步:加上 G92 偏移 =====
|
||
# 对应 C++: *x += s->axis_offset_x
|
||
abs_x = self.current_pos.x + self.g92_offset_x
|
||
abs_y = self.current_pos.y + self.g92_offset_y
|
||
abs_z = self.current_pos.z + self.g92_offset_z
|
||
abs_a = self.current_pos.a + self.g92_offset_a
|
||
abs_b = self.current_pos.b + self.g92_offset_b
|
||
abs_c = self.current_pos.c + self.g92_offset_c
|
||
|
||
# ===== 第2步:XY 旋转 =====
|
||
# 对应 C++: rotate(x, y, s->rotation_xy)
|
||
if abs(self.rotation_xy) > 1e-9:
|
||
rot = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot)
|
||
sin_r = math.sin(rot)
|
||
rx = abs_x * cos_r - abs_y * sin_r
|
||
ry = abs_x * sin_r + abs_y * cos_r
|
||
abs_x, abs_y = rx, ry
|
||
|
||
# ===== 第3步:加上 G5x 偏移 =====
|
||
# 对应 C++: *x += s->origin_offset_x
|
||
abs_x += self.g5x_offset_x
|
||
abs_y += self.g5x_offset_y
|
||
abs_z += self.g5x_offset_z
|
||
abs_a += self.g5x_offset_a
|
||
abs_b += self.g5x_offset_b
|
||
abs_c += self.g5x_offset_c
|
||
|
||
# ===== 第4步:加上刀具长度补偿 (TLO) =====
|
||
# 对应 C++: *x += s->tool_offset.tran.x
|
||
abs_x += self.tool_offset.x
|
||
abs_y += self.tool_offset.y
|
||
abs_z += self.tool_offset.z
|
||
abs_a += self.tool_offset.a
|
||
abs_b += self.tool_offset.b
|
||
abs_c += self.tool_offset.c
|
||
|
||
# ===== 第5步:加上额外工件偏移 =====
|
||
abs_x += self.work_offset_x
|
||
abs_y += self.work_offset_y
|
||
abs_z += self.work_offset_z
|
||
abs_a += self.work_offset_a
|
||
abs_b += self.work_offset_b
|
||
abs_c += self.work_offset_c
|
||
|
||
# ===== 第6步:加上 G52 偏移 =====
|
||
abs_x += self.g52_offset_x
|
||
abs_y += self.g52_offset_y
|
||
abs_z += self.g52_offset_z
|
||
abs_a += self.g52_offset_a
|
||
abs_b += self.g52_offset_b
|
||
abs_c += self.g52_offset_c
|
||
|
||
# ===== 第7步:计算关节坐标(如果需要RTCP)=====
|
||
# 对应 C++: motion controller 调用 kinematicsInverse()
|
||
if self.rtcp_enabled and self.kinematics is not None:
|
||
try:
|
||
# 传入包含 TLO 的绝对坐标
|
||
world_tuple = (abs_x, abs_y, abs_z, abs_a, abs_b, abs_c)
|
||
joints = self.kinematics.world_to_joints(world_tuple)
|
||
|
||
# 根据运动学类型映射关节坐标
|
||
config = KINEMATICS_JOINT_CONFIG.get(
|
||
self.kinematics_type,
|
||
KINEMATICS_JOINT_CONFIG[KinematicsType.IDENTITY]
|
||
)
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
self.machine_x = joints[x_idx] if (x_idx >= 0 and x_idx < len(joints)) else abs_x
|
||
self.machine_y = joints[y_idx] if (y_idx >= 0 and y_idx < len(joints)) else abs_y
|
||
self.machine_z = joints[z_idx] if (z_idx >= 0 and z_idx < len(joints)) else abs_z
|
||
self.machine_a = joints[a_idx] if (a_idx >= 0 and a_idx < len(joints)) else abs_a
|
||
self.machine_b = joints[b_idx] if (b_idx >= 0 and b_idx < len(joints)) else abs_b
|
||
self.machine_c = joints[c_idx] if (c_idx >= 0 and c_idx < len(joints)) else abs_c
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[MCS] 逆运动学计算失败: {e}")
|
||
self.machine_x = abs_x
|
||
self.machine_y = abs_y
|
||
self.machine_z = abs_z
|
||
self.machine_a = abs_a
|
||
self.machine_b = abs_b
|
||
self.machine_c = abs_c
|
||
else:
|
||
# RTCP 禁用:直接使用绝对坐标作为机器坐标
|
||
self.machine_x = abs_x
|
||
self.machine_y = abs_y
|
||
self.machine_z = abs_z
|
||
self.machine_a = abs_a
|
||
self.machine_b = abs_b
|
||
self.machine_c = abs_c
|
||
|
||
|
||
def _update_current_from_machine(self):
|
||
"""从机器坐标更新工件坐标
|
||
|
||
逆变换顺序(与 _sync_machine_from_current 严格对应):
|
||
绝对坐标 → -G52偏移 → -TLO → -G5x偏移 → -XY旋转 → -G92偏移 → 工件坐标
|
||
|
||
正变换: 工件 + G92 + XY旋转 + G5x + TLO + G52 = 绝对
|
||
逆变换: 绝对 - G52 - TLO - G5x - XY旋转 - G92 = 工件
|
||
"""
|
||
# ===== 第1步:运动学正解(关节→世界)=====
|
||
if self.rtcp_enabled and self.kinematics is not None:
|
||
# 构建符合运动学类型的关节数组
|
||
config = KINEMATICS_JOINT_CONFIG.get(self.kinematics_type, {})
|
||
num_joints = config.get('num_joints', 6)
|
||
joints = [0.0] * num_joints
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
if x_idx >= 0: joints[x_idx] = self.machine_x
|
||
if y_idx >= 0: joints[y_idx] = self.machine_y
|
||
if z_idx >= 0: joints[z_idx] = self.machine_z
|
||
if a_idx >= 0: joints[a_idx] = self.machine_a
|
||
if b_idx >= 0: joints[b_idx] = self.machine_b
|
||
if c_idx >= 0: joints[c_idx] = self.machine_c
|
||
|
||
world = self.kinematics.joints_to_world(joints)
|
||
|
||
abs_x, abs_y, abs_z = world[0], world[1], world[2]
|
||
abs_a, abs_b, abs_c = world[3], world[4], world[5]
|
||
else:
|
||
# 非RTCP模式:机器坐标 = 绝对坐标
|
||
abs_x, abs_y, abs_z = self.machine_x, self.machine_y, self.machine_z
|
||
abs_a, abs_b, abs_c = self.machine_a, self.machine_b, self.machine_c
|
||
|
||
# ===== 第2步:逆变换(与 _sync_machine_from_current 严格对应)=====
|
||
# 正变换: 工件 + G92 + XY旋转 + G5x + TLO + G52 → 绝对
|
||
# 逆变换: 绝对 - G52 - TLO - G5x - XY旋转 - G92 → 工件
|
||
|
||
# ★ 关键修复:逆变换减法顺序与正变换加法顺序相反 ★
|
||
# Step 2a: 减去 G52 偏移
|
||
x = abs_x - self.g52_offset_x
|
||
y = abs_y - self.g52_offset_y
|
||
z = abs_z - self.g52_offset_z
|
||
a = abs_a - self.g52_offset_a
|
||
b = abs_b - self.g52_offset_b
|
||
c = abs_c - self.g52_offset_c
|
||
|
||
# Step 2b: 减去额外工件偏移
|
||
x -= self.work_offset_x
|
||
y -= self.work_offset_y
|
||
z -= self.work_offset_z
|
||
a -= self.work_offset_a
|
||
b -= self.work_offset_b
|
||
c -= self.work_offset_c
|
||
|
||
# Step 2c: 减去刀具长度补偿 (TLO)
|
||
x -= self.tool_offset.x
|
||
y -= self.tool_offset.y
|
||
z -= self.tool_offset.z
|
||
a -= self.tool_offset.a
|
||
b -= self.tool_offset.b
|
||
c -= self.tool_offset.c
|
||
|
||
# Step 2d: 减去 G5x 坐标系偏移
|
||
x -= self.g5x_offset_x
|
||
y -= self.g5x_offset_y
|
||
z -= self.g5x_offset_z
|
||
a -= self.g5x_offset_a
|
||
b -= self.g5x_offset_b
|
||
c -= self.g5x_offset_c
|
||
|
||
# Step 2e: 反向 XY 旋转(如果设置了旋转角度)
|
||
# 注意:这里需要在减去G92之前反向旋转
|
||
if abs(self.rotation_xy) > 1e-9:
|
||
rot = math.radians(-self.rotation_xy) # 反向旋转
|
||
cos_r = math.cos(rot)
|
||
sin_r = math.sin(rot)
|
||
rx = x * cos_r - y * sin_r
|
||
ry = x * sin_r + y * cos_r
|
||
x, y = rx, ry
|
||
|
||
# Step 2f: 减去 G92 偏移
|
||
x -= self.g92_offset_x
|
||
y -= self.g92_offset_y
|
||
z -= self.g92_offset_z
|
||
a -= self.g92_offset_a
|
||
b -= self.g92_offset_b
|
||
c -= self.g92_offset_c
|
||
|
||
# ===== 第3步:更新工件坐标 =====
|
||
self.current_pos.x = x
|
||
self.current_pos.y = y
|
||
self.current_pos.z = z
|
||
self.current_pos.a = a
|
||
self.current_pos.b = b
|
||
self.current_pos.c = c
|
||
|
||
# ===== 第4步:同步参数表 =====
|
||
self._sync_parameters_from_state()
|
||
|
||
# ==================== 坐标系管理 ====================
|
||
|
||
|
||
def set_coordinate_system(self, csys: int):
|
||
"""
|
||
G54-G59.3: 选择坐标系
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::convert_coordinate_system
|
||
|
||
关键:切换坐标系时,使用 _get_current_in_system 重新计算工件坐标
|
||
|
||
Args:
|
||
csys: 坐标系编号 (1-9)
|
||
"""
|
||
if csys < 1 or csys > 9:
|
||
if self.debug:
|
||
print(f"[CSYS] 错误: 坐标系编号 {csys} 超出范围 (1-9)")
|
||
return
|
||
|
||
if csys == self.g5x_index:
|
||
if self.debug:
|
||
print(f"[CSYS] 已经是 G{53 + csys},无需切换")
|
||
return
|
||
|
||
if self.debug:
|
||
print(f"[CSYS] ========== 切换坐标系 G{53 + self.g5x_index} → G{53 + csys} ==========")
|
||
print(f"[CSYS] 切换前工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
|
||
# ===== 第1步:计算当前在目标坐标系中的位置 =====
|
||
# 对应官方: find_current_in_system(settings, origin, &settings->current_x, ...)
|
||
(new_x, new_y, new_z, new_a, new_b, new_c, new_u, new_v, new_w) = \
|
||
self._get_current_in_system(csys)
|
||
|
||
# ===== 第2步:更新当前位置 =====
|
||
self.current_pos.x = new_x
|
||
self.current_pos.y = new_y
|
||
self.current_pos.z = new_z
|
||
self.current_pos.a = new_a
|
||
self.current_pos.b = new_b
|
||
self.current_pos.c = new_c
|
||
|
||
# ===== 第3步:更新 origin_index =====
|
||
self.g5x_index = csys
|
||
self.current_work_offset = csys
|
||
|
||
# ===== 第4步:从参数表加载新坐标系偏移 =====
|
||
# 对应官方: parameters[5201 + (origin * 20)]
|
||
new_offsets = self.work_offsets.get(csys, {})
|
||
|
||
self.g5x_offset_x = new_offsets.get('X', 0.0)
|
||
self.g5x_offset_y = new_offsets.get('Y', 0.0)
|
||
self.g5x_offset_z = new_offsets.get('Z', 0.0)
|
||
self.g5x_offset_a = new_offsets.get('A', 0.0)
|
||
self.g5x_offset_b = new_offsets.get('B', 0.0)
|
||
self.g5x_offset_c = new_offsets.get('C', 0.0)
|
||
self.g5x_offset_u = new_offsets.get('U', 0.0)
|
||
self.g5x_offset_v = new_offsets.get('V', 0.0)
|
||
self.g5x_offset_w = new_offsets.get('W', 0.0)
|
||
|
||
# ===== 第5步:更新 XY 旋转 =====
|
||
# 对应官方: settings->rotation_xy = parameters[5210 + (origin * 20)];
|
||
new_rotation = new_offsets.get('R', 0.0)
|
||
self.rotation_xy = new_rotation
|
||
t = math.radians(new_rotation)
|
||
self.rotation_sin = math.sin(t)
|
||
self.rotation_cos = math.cos(t)
|
||
|
||
# ===== 第6步:同步到参数表 =====
|
||
self._sync_parameters_from_state()
|
||
self._sync_machine_from_current()
|
||
|
||
if self.debug:
|
||
print(f"[CSYS] 切换后工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
print(f"[CSYS] 新G5x偏移: ({self.g5x_offset_x:.3f}, {self.g5x_offset_y:.3f}, {self.g5x_offset_z:.3f})")
|
||
print(f"[CSYS] 新XY旋转: {self.rotation_xy}°")
|
||
print(f"[CSYS] ========== 坐标系切换完成 ==========")
|
||
|
||
|
||
def set_g5x_offset(self, csys: int, x: float = None, y: float = None, z: float = None,
|
||
a: float = 0.0, b: float = 0.0, c: float = 0.0,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""G10 L2: 直接设置坐标系偏移"""
|
||
if csys < 1 or csys > 9:
|
||
return
|
||
|
||
if csys not in self.work_offsets:
|
||
self.work_offsets[csys] = {}
|
||
|
||
old_offsets = self.work_offsets[csys].copy()
|
||
updates = {'X': x, 'Y': y, 'Z': z, 'A': a, 'B': b, 'C': c, 'U': u, 'V': v, 'W': w}
|
||
|
||
for axis, val in updates.items():
|
||
if val is not None:
|
||
self.work_offsets[csys][axis] = val
|
||
|
||
if csys == self.g5x_index:
|
||
for axis in 'XYZABC':
|
||
if axis in old_offsets:
|
||
delta = old_offsets[axis] - self.work_offsets[csys].get(axis, 0.0)
|
||
if axis == 'X': self.current_pos.x += delta
|
||
elif axis == 'Y': self.current_pos.y += delta
|
||
elif axis == 'Z': self.current_pos.z += delta
|
||
elif axis == 'A': self.current_pos.a += delta
|
||
elif axis == 'B': self.current_pos.b += delta
|
||
elif axis == 'C': self.current_pos.c += delta
|
||
|
||
self.g5x_offset_x = self.work_offsets[csys].get('X', 0.0)
|
||
self.g5x_offset_y = self.work_offsets[csys].get('Y', 0.0)
|
||
self.g5x_offset_z = self.work_offsets[csys].get('Z', 0.0)
|
||
self.g5x_offset_a = self.work_offsets[csys].get('A', 0.0)
|
||
self.g5x_offset_b = self.work_offsets[csys].get('B', 0.0)
|
||
self.g5x_offset_c = self.work_offsets[csys].get('C', 0.0)
|
||
|
||
self.var_manager.set_csys_origin(csys,
|
||
self.work_offsets[csys].get('X', 0.0),
|
||
self.work_offsets[csys].get('Y', 0.0),
|
||
self.work_offsets[csys].get('Z', 0.0),
|
||
self.work_offsets[csys].get('A', 0.0),
|
||
self.work_offsets[csys].get('B', 0.0),
|
||
self.work_offsets[csys].get('C', 0.0))
|
||
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
|
||
|
||
|
||
def set_coordinate_system_g10_l20_bak(self, csys: int, params_str: str):
|
||
"""G10 L20: 设置坐标系使当前位置变为指定值"""
|
||
if csys < 1 or csys > 9:
|
||
return
|
||
|
||
target_csys = csys
|
||
old_offsets = self.work_offsets.get(target_csys, {})
|
||
|
||
cx = self.current_pos.x + self.g92_offset_x + self.g52_offset_x
|
||
cy = self.current_pos.y + self.g92_offset_y + self.g52_offset_y
|
||
cz = self.current_pos.z + self.g92_offset_z + self.g52_offset_z
|
||
ca = self.current_pos.a + self.g92_offset_a + self.g52_offset_a
|
||
cb = self.current_pos.b + self.g92_offset_b + self.g52_offset_b
|
||
cc = self.current_pos.c + self.g92_offset_c + self.g52_offset_c
|
||
|
||
if self.rotation_xy != 0:
|
||
crot = math.cos(math.radians(self.rotation_xy))
|
||
srot = math.sin(math.radians(self.rotation_xy))
|
||
rotx = cx * crot - cy * srot
|
||
roty = cx * srot + cy * crot
|
||
cx, cy = rotx, roty
|
||
|
||
cx += self.g5x_offset_x
|
||
cy += self.g5x_offset_y
|
||
cz += self.g5x_offset_z
|
||
ca += self.g5x_offset_a
|
||
cb += self.g5x_offset_b
|
||
cc += self.g5x_offset_c
|
||
|
||
target_g5x_x = old_offsets.get('X', 0.0)
|
||
target_g5x_y = old_offsets.get('Y', 0.0)
|
||
target_g5x_z = old_offsets.get('Z', 0.0)
|
||
|
||
cx -= target_g5x_x
|
||
cy -= target_g5x_y
|
||
cz -= target_g5x_z
|
||
|
||
target_x = cx
|
||
target_y = cy
|
||
target_z = cz
|
||
|
||
for match in re.finditer(r'([XYZ])\s*=\s*([+-]?\d*\.?\d+)', params_str, re.I):
|
||
axis = match.group(1).upper()
|
||
value = float(match.group(2))
|
||
if axis == 'X': target_x = value
|
||
elif axis == 'Y': target_y = value
|
||
elif axis == 'Z': target_z = value
|
||
|
||
new_g5x_x = target_g5x_x + (cx - target_x)
|
||
new_g5x_y = target_g5x_y + (cy - target_y)
|
||
new_g5x_z = target_g5x_z + (cz - target_z)
|
||
|
||
self.work_offsets[target_csys]['X'] = new_g5x_x
|
||
self.work_offsets[target_csys]['Y'] = new_g5x_y
|
||
self.work_offsets[target_csys]['Z'] = new_g5x_z
|
||
|
||
if target_csys == self.g5x_index:
|
||
self.g5x_offset_x = new_g5x_x
|
||
self.g5x_offset_y = new_g5x_y
|
||
self.g5x_offset_z = new_g5x_z
|
||
|
||
self.current_pos.x = target_x
|
||
self.current_pos.y = target_y
|
||
self.current_pos.z = target_z
|
||
|
||
self.var_manager.set_csys_origin(target_csys, new_g5x_x, new_g5x_y, new_g5x_z)
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
|
||
def set_coordinate_system_g10_l20(self, csys: int, params_str: str):
|
||
"""
|
||
G10 L20: 设置坐标系使当前位置变为指定值
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::convert_setup (G10 L20 分支)
|
||
|
||
核心逻辑:
|
||
1. 获取目标坐标系的旧偏移和旧旋转
|
||
2. 使用 _get_current_in_system 计算当前在目标坐标系中的位置
|
||
3. 应用用户指定的目标值
|
||
4. 正向 XY 旋转(使用旧 rotation_xy)
|
||
5. 计算新 G5x 偏移:old_position + old_origin - new_position = new_origin
|
||
6. 存储结果
|
||
7. 如果是当前坐标系,更新 rotation_xy 为 0
|
||
|
||
Args:
|
||
csys: 坐标系编号 (0=当前坐标系, 1-9=指定坐标系)
|
||
params_str: 参数字符串,如 "X0 Y0 Z100 A0 C0"
|
||
"""
|
||
# 处理 P0(当前坐标系)
|
||
if csys == 0:
|
||
csys = self.g5x_index
|
||
|
||
if csys < 1 or csys > 9:
|
||
if self.debug:
|
||
print(f"[G10 L20] 错误: 坐标系编号 {csys} 超出范围 (1-9)")
|
||
return
|
||
|
||
if self.debug:
|
||
print(f"[G10 L20] ========== 设置坐标系 G{53 + csys} ==========")
|
||
print(f"[G10 L20] 参数: '{params_str}'")
|
||
print(f"[G10 L20] 当前工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
print(f"[G10 L20] 当前G5x索引: {self.g5x_index}, 旋转: {self.rotation_xy}°")
|
||
|
||
# ===== 第1步:获取目标坐标系的旧偏移和旧旋转 =====
|
||
old_offsets = self.work_offsets.get(csys, {}).copy()
|
||
|
||
old_g5x_x = old_offsets.get('X', 0.0)
|
||
old_g5x_y = old_offsets.get('Y', 0.0)
|
||
old_g5x_z = old_offsets.get('Z', 0.0)
|
||
old_g5x_a = old_offsets.get('A', 0.0)
|
||
old_g5x_b = old_offsets.get('B', 0.0)
|
||
old_g5x_c = old_offsets.get('C', 0.0)
|
||
old_rotation = old_offsets.get('R', 0.0)
|
||
|
||
# ===== 第2步:计算当前在目标坐标系中的位置 =====
|
||
# 对应官方: find_current_in_system(settings, p_int, &cx, &cy, &cz, ...)
|
||
(cx, cy, cz, ca, cb, cc, cu, cv, cw) = self._get_current_in_system(csys)
|
||
|
||
if self.debug:
|
||
print(f"[G10 L20] 当前在G{53+csys}中的位置: ({cx:.3f}, {cy:.3f}, {cz:.3f})")
|
||
print(f"[G10 L20] 旧G5x偏移: ({old_g5x_x:.3f}, {old_g5x_y:.3f}, {old_g5x_z:.3f})")
|
||
print(f"[G10 L20] 旧XY旋转: {old_rotation}°")
|
||
|
||
# ===== 第3步:记录旧坐标并应用用户指定的值 =====
|
||
# 对应官方: double oldx = cx, oldy = cy; x = cx; y = cy;
|
||
oldx = cx
|
||
oldy = cy
|
||
x = cx
|
||
y = cy
|
||
z = cz
|
||
a = ca
|
||
b = cb
|
||
c_val = cc
|
||
|
||
# 解析用户指定的坐标值(支持 X=0 和 X0 两种格式)
|
||
for match in re.finditer(r'([XYZABC])\s*=\s*([+-]?\d*\.?\d+)', params_str, re.I):
|
||
axis = match.group(1).upper()
|
||
value = float(match.group(2))
|
||
|
||
if axis == 'X': x = value
|
||
elif axis == 'Y': y = value
|
||
elif axis == 'Z': z = value
|
||
elif axis == 'A': a = value
|
||
elif axis == 'B': b = value
|
||
elif axis == 'C': c_val = value
|
||
|
||
if self.debug:
|
||
print(f"[G10 L20] 用户指定值: ({x:.3f}, {y:.3f}, {z:.3f}, A={a}, B={b}, C={c_val})")
|
||
|
||
# ===== 第4步:正向 XY 旋转(使用旧 rotation_xy)=====
|
||
# 对应官方: rotate(&oldx, &oldy, r); rotate(&x, &y, r);
|
||
if old_rotation != 0:
|
||
rot_rad = math.radians(old_rotation)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
|
||
rot_oldx = oldx * cos_r - oldy * sin_r
|
||
rot_oldy = oldx * sin_r + oldy * cos_r
|
||
|
||
rot_x = x * cos_r - y * sin_r
|
||
rot_y = x * sin_r + y * cos_r
|
||
|
||
oldx, oldy = rot_oldx, rot_oldy
|
||
x, y = rot_x, rot_y
|
||
|
||
# ===== 第5步:计算新 G5x 偏移 =====
|
||
# 对应官方:
|
||
# x = oldx + USER_TO_PROGRAM_LEN(parameters[5201 + (p_int * 20)]) - x;
|
||
# y = oldy + USER_TO_PROGRAM_LEN(parameters[5202 + (p_int * 20)]) - y;
|
||
new_g5x_x = oldx + old_g5x_x - x
|
||
new_g5x_y = oldy + old_g5x_y - y
|
||
new_g5x_z = cz + old_g5x_z - z
|
||
new_g5x_a = ca + old_g5x_a - a
|
||
new_g5x_b = cb + old_g5x_b - b
|
||
new_g5x_c = cc + old_g5x_c - c_val
|
||
|
||
# ===== 第6步:存储新偏移 =====
|
||
if csys not in self.work_offsets:
|
||
self.work_offsets[csys] = {}
|
||
|
||
self.work_offsets[csys]['X'] = new_g5x_x
|
||
self.work_offsets[csys]['Y'] = new_g5x_y
|
||
self.work_offsets[csys]['Z'] = new_g5x_z
|
||
self.work_offsets[csys]['A'] = new_g5x_a
|
||
self.work_offsets[csys]['B'] = new_g5x_b
|
||
self.work_offsets[csys]['C'] = new_g5x_c
|
||
self.work_offsets[csys]['U'] = 0.0
|
||
self.work_offsets[csys]['V'] = 0.0
|
||
self.work_offsets[csys]['W'] = 0.0
|
||
self.work_offsets[csys]['R'] = 0.0 # G10 L20 将 XY 旋转清零
|
||
|
||
# ===== 第7步:如果是当前坐标系,更新 rotation_xy =====
|
||
# 对应官方:
|
||
# if (p_int == settings->origin_index) {
|
||
# rotate(&settings->current_x, &settings->current_y, settings->rotation_xy);
|
||
# settings->rotation_xy = 0;
|
||
# }
|
||
if csys == self.g5x_index:
|
||
# 先正向旋转当前坐标(去掉旧旋转)
|
||
if self.rotation_xy != 0:
|
||
rot_rad = math.radians(self.rotation_xy)
|
||
cos_r = math.cos(rot_rad)
|
||
sin_r = math.sin(rot_rad)
|
||
rot_x = self.current_pos.x * cos_r - self.current_pos.y * sin_r
|
||
rot_y = self.current_pos.x * sin_r + self.current_pos.y * cos_r
|
||
self.current_pos.x = rot_x
|
||
self.current_pos.y = rot_y
|
||
|
||
# 加上旧 G5x 偏移
|
||
self.current_pos.x += self.g5x_offset_x
|
||
self.current_pos.y += self.g5x_offset_y
|
||
self.current_pos.z += self.g5x_offset_z
|
||
self.current_pos.a += self.g5x_offset_a
|
||
self.current_pos.b += self.g5x_offset_b
|
||
self.current_pos.c += self.g5x_offset_c
|
||
|
||
# 减去新 G5x 偏移
|
||
self.current_pos.x -= new_g5x_x
|
||
self.current_pos.y -= new_g5x_y
|
||
self.current_pos.z -= new_g5x_z
|
||
self.current_pos.a -= new_g5x_a
|
||
self.current_pos.b -= new_g5x_b
|
||
self.current_pos.c -= new_g5x_c
|
||
|
||
# 更新 G5x 偏移变量
|
||
self.g5x_offset_x = new_g5x_x
|
||
self.g5x_offset_y = new_g5x_y
|
||
self.g5x_offset_z = new_g5x_z
|
||
self.g5x_offset_a = new_g5x_a
|
||
self.g5x_offset_b = new_g5x_b
|
||
self.g5x_offset_c = new_g5x_c
|
||
|
||
# 清零 XY 旋转
|
||
self.rotation_xy = 0.0
|
||
self.rotation_sin = 0.0
|
||
self.rotation_cos = 1.0
|
||
|
||
# ===== 第8步:同步到参数表 =====
|
||
if hasattr(self, 'var_manager') and self.var_manager:
|
||
self.var_manager.set_csys_origin(
|
||
csys, new_g5x_x, new_g5x_y, new_g5x_z,
|
||
new_g5x_a, new_g5x_b, new_g5x_c
|
||
)
|
||
self.var_manager.set_xy_rotation(
|
||
0.0 if csys == self.g5x_index else self.rotation_xy
|
||
)
|
||
|
||
# ===== 第9步:同步机器坐标和参数 =====
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
if self.debug:
|
||
print(f"[G10 L20] 新G5x偏移: ({new_g5x_x:.3f}, {new_g5x_y:.3f}, {new_g5x_z:.3f})")
|
||
print(f"[G10 L20] 更新后工件坐标: ({self.current_pos.x:.3f}, {self.current_pos.y:.3f}, {self.current_pos.z:.3f})")
|
||
print(f"[G10 L20] 更新后XY旋转: {self.rotation_xy}°")
|
||
print(f"[G10 L20] ========== G10 L20 完成 ==========")
|
||
|
||
|
||
|
||
def set_g92_offset(self, x: float = None, y: float = None, z: float = None,
|
||
a: float = None, b: float = None, c: float = None,
|
||
u: float = None, v: float = None, w: float = None):
|
||
"""G92: 设置临时坐标系偏移。
|
||
|
||
LinuxCNC 的 G92 允许把某个轴设置为 0,因此不能用 0.0
|
||
表示“该轴未指定”。这里用 None 表示未指定,只对实际出现
|
||
在 G92 程序段中的轴计算 offset。
|
||
"""
|
||
old_x, old_y, old_z = self.current_pos.x, self.current_pos.y, self.current_pos.z
|
||
|
||
any_axis = False
|
||
|
||
if x is not None:
|
||
self.g92_offset_x = self.g92_offset_x + self.current_pos.x - x
|
||
self.current_pos.x = x
|
||
any_axis = True
|
||
if y is not None:
|
||
self.g92_offset_y = self.g92_offset_y + self.current_pos.y - y
|
||
self.current_pos.y = y
|
||
any_axis = True
|
||
if z is not None:
|
||
self.g92_offset_z = self.g92_offset_z + self.current_pos.z - z
|
||
self.current_pos.z = z
|
||
any_axis = True
|
||
if a is not None:
|
||
self.g92_offset_a = self.g92_offset_a + self.current_pos.a - a
|
||
self.current_pos.a = a
|
||
any_axis = True
|
||
if b is not None:
|
||
self.g92_offset_b = self.g92_offset_b + self.current_pos.b - b
|
||
self.current_pos.b = b
|
||
any_axis = True
|
||
if c is not None:
|
||
self.g92_offset_c = self.g92_offset_c + self.current_pos.c - c
|
||
self.current_pos.c = c
|
||
any_axis = True
|
||
if u is not None:
|
||
self.g92_offset_u = self.g92_offset_u + getattr(self.current_pos, 'u', 0.0) - u
|
||
self.current_pos.u = u
|
||
any_axis = True
|
||
if v is not None:
|
||
self.g92_offset_v = self.g92_offset_v + getattr(self.current_pos, 'v', 0.0) - v
|
||
self.current_pos.v = v
|
||
any_axis = True
|
||
if w is not None:
|
||
self.g92_offset_w = self.g92_offset_w + getattr(self.current_pos, 'w', 0.0) - w
|
||
self.current_pos.w = w
|
||
any_axis = True
|
||
|
||
if not any_axis:
|
||
return
|
||
|
||
self.g92_active = True
|
||
|
||
# 同步参数表。apply_g92 需要 G92 生效前的当前位置。
|
||
self.var_manager.apply_g92(x, y, z, old_x, old_y, old_z)
|
||
self.var_manager._params[InterpParameterIndex.G92_A] = self.g92_offset_a
|
||
self.var_manager._params[InterpParameterIndex.G92_B] = self.g92_offset_b
|
||
self.var_manager._params[InterpParameterIndex.G92_C] = self.g92_offset_c
|
||
self.var_manager._params[InterpParameterIndex.G92_U] = self.g92_offset_u
|
||
self.var_manager._params[InterpParameterIndex.G92_V] = self.g92_offset_v
|
||
self.var_manager._params[InterpParameterIndex.G92_W] = self.g92_offset_w
|
||
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
def clear_g92(self):
|
||
"""G92.1: 清除G92偏移"""
|
||
self.current_pos.x += self.g92_offset_x
|
||
self.current_pos.y += self.g92_offset_y
|
||
self.current_pos.z += self.g92_offset_z
|
||
self.current_pos.a += self.g92_offset_a
|
||
self.current_pos.b += self.g92_offset_b
|
||
self.current_pos.c += self.g92_offset_c
|
||
|
||
self.g92_offset_x = 0.0
|
||
self.g92_offset_y = 0.0
|
||
self.g92_offset_z = 0.0
|
||
self.g92_offset_a = 0.0
|
||
self.g92_offset_b = 0.0
|
||
self.g92_offset_c = 0.0
|
||
self.g92_active = False
|
||
|
||
self.var_manager.clear_g92()
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_g52_offset(self, x: float = 0.0, y: float = 0.0, z: float = 0.0,
|
||
a: float = 0.0, b: float = 0.0, c: float = 0.0):
|
||
"""G52: 设置临时工件偏移"""
|
||
old_g52_x = self.g52_offset_x
|
||
old_g52_y = self.g52_offset_y
|
||
old_g52_z = self.g52_offset_z
|
||
|
||
self.g52_offset_x = x
|
||
self.g52_offset_y = y
|
||
self.g52_offset_z = z
|
||
self.g52_offset_a = a
|
||
self.g52_offset_b = b
|
||
self.g52_offset_c = c
|
||
|
||
self.current_pos.x += old_g52_x - x
|
||
self.current_pos.y += old_g52_y - y
|
||
self.current_pos.z += old_g52_z - z
|
||
self.current_pos.a += a if a != 0.0 else 0.0
|
||
self.current_pos.b += b if b != 0.0 else 0.0
|
||
self.current_pos.c += c if c != 0.0 else 0.0
|
||
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_xy_rotation(self, theta: float):
|
||
"""设置XY旋转(同步到参数表)"""
|
||
self.rotation_xy = theta
|
||
t = math.radians(theta)
|
||
self.rotation_sin = math.sin(t)
|
||
self.rotation_cos = math.cos(t)
|
||
# 同步到参数表
|
||
self.var_manager.set_xy_rotation(theta)
|
||
|
||
|
||
# ==================== 运动学切换 ====================
|
||
|
||
def set_kinematics_by_type(self, kin_type: int):
|
||
"""
|
||
运动学切换
|
||
kin_type: 0 = IDENTITY (M429), 1 = 原始五轴 (M428), 2 = FIVEAXIS_BC (M430)
|
||
"""
|
||
if kin_type == 0: # M429: 切换到三轴恒等运动学
|
||
self._switch_to_identity()
|
||
elif kin_type == 1: # M428: 恢复到原始五轴运动学
|
||
self._switch_to_original()
|
||
elif kin_type == 2: # M430: 切换到 FIVEAXIS_BC
|
||
self._switch_to_fiveaxis_bc()
|
||
else:
|
||
# 未知的运动学类型,记录警告但不中断
|
||
if self.debug:
|
||
print(f"[KINS] 警告: 未知的运动学切换类型: {kin_type}, 忽略")
|
||
|
||
|
||
def _switch_to_identity(self):
|
||
"""
|
||
M429: 切换到三轴恒等运动学 (IDENTITY)
|
||
|
||
关键修复:
|
||
1. 保存当前运动学类型(用于M428恢复)
|
||
2. 根据运动学类型保存对应的旋转轴角度
|
||
3. 清零所有旋转轴 (A, B, C)
|
||
4. 禁用RTCP
|
||
"""
|
||
if self.debug:
|
||
print(f"[KINS] M429: 切换到三轴运动学 (IDENTITY)")
|
||
|
||
# 保存切换前的运动学类型,以便 M428 恢复
|
||
if self.kinematics_type != KinematicsType.IDENTITY:
|
||
self._saved_kinematics_type = self.kinematics_type
|
||
|
||
# ★ 修复: 保存当前运动学参数,以便M428恢复 ★
|
||
if self.kinematics and hasattr(self.kinematics, 'p'):
|
||
p = self.kinematics.p
|
||
self._saved_kinematics_params = {
|
||
'rot_center_x': getattr(p, 'rot_center_x', 0.0),
|
||
'rot_center_y': getattr(p, 'rot_center_y', 0.0),
|
||
'rot_center_z': getattr(p, 'rot_center_z', -300.0),
|
||
'axis_offset_x': getattr(p, 'axis_offset_x', 0.0),
|
||
'axis_offset_y': getattr(p, 'axis_offset_y', 0.0),
|
||
'axis_offset_z': getattr(p, 'axis_offset_z', 200.0),
|
||
'tool_length': self.kinematics._tool_length,
|
||
'conventional_directions': getattr(p, 'conventional_directions', False),
|
||
}
|
||
|
||
if self.debug:
|
||
print(f"[KINS] 已保存原始运动学类型: {self._saved_kinematics_type.name}")
|
||
else:
|
||
# 已经是IDENTITY,不需要重复切换
|
||
if self.debug:
|
||
print(f"[KINS] 已经是IDENTITY模式,跳过切换")
|
||
return
|
||
|
||
# ===== 保存旋转轴位置(根据运动学类型) =====
|
||
# 不同运动学类型使用的旋转轴不同
|
||
self._saved_rotation_a = self.current_pos.a if self.current_pos.a is not None else 0.0
|
||
self._saved_rotation_b = self.current_pos.b if self.current_pos.b is not None else 0.0
|
||
self._saved_rotation_c = self.current_pos.c if self.current_pos.c is not None else 0.0
|
||
|
||
# 记录当前运动学类型,用于M428恢复时判断哪些轴需要恢复
|
||
self._saved_rotation_kinematics = self.kinematics_type
|
||
|
||
if self.debug:
|
||
kin_name = self.kinematics_type.name
|
||
# 根据运动学类型显示不同的轴信息
|
||
if self.kinematics_type == KinematicsType.TRT_AC:
|
||
print(f"[KINS] 保存{kin_name}旋转轴: A={self._saved_rotation_a:.3f}, C={self._saved_rotation_c:.3f}")
|
||
elif self.kinematics_type == KinematicsType.TRT_BC:
|
||
print(f"[KINS] 保存{kin_name}旋转轴: B={self._saved_rotation_b:.3f}, C={self._saved_rotation_c:.3f}")
|
||
elif self.kinematics_type == KinematicsType.FIVEAXIS_BC:
|
||
print(f"[KINS] 保存{kin_name}旋转轴: B={self._saved_rotation_b:.3f}, C={self._saved_rotation_c:.3f}")
|
||
elif self.kinematics_type == KinematicsType.MAXKINS_BC:
|
||
print(f"[KINS] 保存{kin_name}旋转轴: A={self._saved_rotation_a:.3f}, B={self._saved_rotation_b:.3f}, C={self._saved_rotation_c:.3f}")
|
||
else:
|
||
print(f"[KINS] 保存{kin_name}旋转轴: A={self._saved_rotation_a:.3f}, B={self._saved_rotation_b:.3f}, C={self._saved_rotation_c:.3f}")
|
||
|
||
# 切换到恒等运动学
|
||
self.kinematics_type = KinematicsType.IDENTITY
|
||
self.data.kinematics_type = "IDENTITY"
|
||
self.rtcp_enabled = False
|
||
self.data.rtcp_enabled = False
|
||
|
||
# 禁用当前运动学实例的 RTCP
|
||
if self.kinematics:
|
||
self.kinematics.enable_rtcp(False)
|
||
|
||
# 设置恒等模式:直接透传坐标
|
||
self.kinematics = None
|
||
|
||
# ===== 清零所有旋转轴 =====
|
||
# IDENTITY运动学不支持旋转轴,必须清零
|
||
self.current_pos.a = 0.0
|
||
self.current_pos.b = 0.0
|
||
self.current_pos.c = 0.0
|
||
|
||
self.machine_a = 0.0
|
||
self.machine_b = 0.0
|
||
self.machine_c = 0.0
|
||
|
||
if self.debug:
|
||
print(f"[KINS] 清零旋转轴: A→0, B→0, C→0")
|
||
|
||
# 同步参数表
|
||
self._sync_parameters_from_state()
|
||
|
||
if self.debug:
|
||
print(f"[KINS] 当前运动学: IDENTITY, RTCP: 禁用")
|
||
|
||
|
||
|
||
def _switch_to_original(self):
|
||
"""
|
||
M428: 恢复到原始五轴运动学
|
||
|
||
关键修复:
|
||
1. 恢复保存的运动学类型
|
||
2. 根据运动学类型恢复对应的旋转轴角度
|
||
3. 重新创建运动学实例
|
||
4. 恢复刀具长度补偿
|
||
5. 启用RTCP
|
||
"""
|
||
if self.debug:
|
||
print(f"[KINS] M428: 恢复到五轴运动学")
|
||
|
||
# 恢复保存的运动学类型
|
||
# 优先级1: 使用M429之前通过_switch_to_identity保存的类型
|
||
if hasattr(self, '_saved_kinematics_type') and self._saved_kinematics_type is not None:
|
||
restore_type = self._saved_kinematics_type
|
||
if self.debug:
|
||
print(f"[KINS] 使用保存的运动学类型: {restore_type.name}")
|
||
# 优先级2: 如果当前不是IDENTITY,说明从未切换过,使用当前类型
|
||
elif self.kinematics_type != KinematicsType.IDENTITY:
|
||
restore_type = self.kinematics_type
|
||
if self.debug:
|
||
print(f"[KINS] 使用当前运动学类型: {restore_type.name}")
|
||
# 优先级3: 从data.kinematics_type记录恢复
|
||
else:
|
||
saved_name = self.data.kinematics_type
|
||
type_map = {
|
||
'TRT_AC': KinematicsType.TRT_AC,
|
||
'TRT_BC': KinematicsType.TRT_BC,
|
||
'FIVEAXIS_BC': KinematicsType.FIVEAXIS_BC,
|
||
'MAXKINS_BC': KinematicsType.MAXKINS_BC,
|
||
'IDENTITY': KinematicsType.IDENTITY,
|
||
}
|
||
restore_type = type_map.get(saved_name, KinematicsType.TRT_AC)
|
||
if self.debug:
|
||
print(f"[KINS] 从data记录恢复: {saved_name} -> {restore_type.name}")
|
||
|
||
# 如果已经是目标类型,不需要重复切换
|
||
if self.kinematics_type == restore_type and self.kinematics_type != KinematicsType.IDENTITY:
|
||
if self.debug:
|
||
print(f"[KINS] 已经是{restore_type.name}模式,跳过切换")
|
||
return
|
||
|
||
self.kinematics_type = restore_type
|
||
self.data.kinematics_type = restore_type.name
|
||
self.rtcp_enabled = True
|
||
self.data.rtcp_enabled = True
|
||
|
||
# ★ 修复: 重新创建运动学实例,使用保存的参数 ★
|
||
# 获取保存的参数(如果之前有运动学实例)
|
||
saved_params = {}
|
||
if hasattr(self, '_saved_kinematics_params') and self._saved_kinematics_params:
|
||
saved_params = self._saved_kinematics_params
|
||
else:
|
||
# 从当前work_offsets或默认值重建参数
|
||
saved_params = {
|
||
'rot_center_x': getattr(self, '_saved_rot_center_x', 0.0),
|
||
'rot_center_y': getattr(self, '_saved_rot_center_y', 0.0),
|
||
'rot_center_z': getattr(self, '_saved_rot_center_z', -300.0),
|
||
'axis_offset_x': getattr(self, '_saved_axis_offset_x', 0.0),
|
||
'axis_offset_y': getattr(self, '_saved_axis_offset_y', 0.0),
|
||
'axis_offset_z': getattr(self, '_saved_axis_offset_z', 200.0),
|
||
'tool_length': 100.0, # 默认值
|
||
}
|
||
|
||
self.kinematics = KinematicsFactory.create(
|
||
restore_type, debug=self.debug, **saved_params
|
||
)
|
||
|
||
if self.kinematics:
|
||
self.kinematics.enable_rtcp(True)
|
||
# 重新设置刀具长度
|
||
if self.current_h_code > 0:
|
||
tool_len = self.tool_lengths.get(self.current_h_code, 0.0)
|
||
self.kinematics.set_tool_length(tool_len)
|
||
if self.debug:
|
||
print(f"[KINS] 恢复刀具长度补偿: H{self.current_h_code} = {tool_len:.1f}mm")
|
||
elif self.debug:
|
||
print(f"[KINS] 注意: 无刀具长度补偿激活 (current_h_code={self.current_h_code})")
|
||
|
||
# ===== 根据运动学类型恢复旋转轴 =====
|
||
if hasattr(self, '_saved_rotation_a') and hasattr(self, '_saved_rotation_kinematics'):
|
||
saved_kin = self._saved_rotation_kinematics
|
||
|
||
if self.debug:
|
||
print(f"[KINS] 保存时的运动学类型: {saved_kin.name}, 恢复为: {restore_type.name}")
|
||
|
||
# ===== TRT_AC: 只有A轴和C轴 =====
|
||
if restore_type == KinematicsType.TRT_AC:
|
||
self.current_pos.a = self._saved_rotation_a
|
||
self.current_pos.b = 0.0 # TRT_AC没有B轴,强制为0
|
||
self.machine_b = 0.0
|
||
self.current_pos.c = self._saved_rotation_c
|
||
if self.debug:
|
||
print(f"[KINS] 恢复TRT_AC旋转轴: A={self._saved_rotation_a:.3f}, "
|
||
f"C={self._saved_rotation_c:.3f}, B=0(强制)")
|
||
|
||
# ===== TRT_BC: 只有B轴和C轴 =====
|
||
elif restore_type == KinematicsType.TRT_BC:
|
||
self.current_pos.a = 0.0 # TRT_BC没有A轴,强制为0
|
||
self.machine_a = 0.0
|
||
self.current_pos.b = self._saved_rotation_b
|
||
self.current_pos.c = self._saved_rotation_c
|
||
if self.debug:
|
||
print(f"[KINS] 恢复TRT_BC旋转轴: B={self._saved_rotation_b:.3f}, "
|
||
f"C={self._saved_rotation_c:.3f}, A=0(强制)")
|
||
|
||
# ===== FIVEAXIS_BC: B轴和C轴 =====
|
||
elif restore_type == KinematicsType.FIVEAXIS_BC:
|
||
self.current_pos.a = 0.0 # FIVEAXIS_BC没有A轴
|
||
self.current_pos.b = self._saved_rotation_b
|
||
self.current_pos.c = self._saved_rotation_c
|
||
if self.debug:
|
||
print(f"[KINS] 恢复FIVEAXIS_BC旋转轴: B={self._saved_rotation_b:.3f}, "
|
||
f"C={self._saved_rotation_c:.3f}, A=0(强制)")
|
||
|
||
# ===== MAXKINS_BC: A, B, C轴都有 =====
|
||
elif restore_type == KinematicsType.MAXKINS_BC:
|
||
self.current_pos.a = self._saved_rotation_a
|
||
self.current_pos.b = self._saved_rotation_b
|
||
self.current_pos.c = self._saved_rotation_c
|
||
if self.debug:
|
||
print(f"[KINS] 恢复MAXKINS_BC旋转轴: A={self._saved_rotation_a:.3f}, "
|
||
f"B={self._saved_rotation_b:.3f}, C={self._saved_rotation_c:.3f}")
|
||
|
||
# ===== IDENTITY或其他: 保持为0 =====
|
||
else:
|
||
self.current_pos.a = 0.0
|
||
self.current_pos.b = 0.0
|
||
self.current_pos.c = 0.0
|
||
if self.debug:
|
||
print(f"[KINS] 恢复{restore_type.name}旋转轴: 全部清零")
|
||
else:
|
||
# 没有保存的旋转轴信息,全部清零
|
||
self.current_pos.a = 0.0
|
||
self.current_pos.b = 0.0
|
||
self.current_pos.c = 0.0
|
||
if self.debug:
|
||
print(f"[KINS] 无保存的旋转轴信息,全部清零")
|
||
|
||
# 重新同步机器坐标
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
if self.debug:
|
||
if self.kinematics:
|
||
world_pos = self.current_pos
|
||
tool_len = getattr(self.kinematics, '_tool_length', 0.0)
|
||
joints = self.kinematics.world_to_joints((
|
||
world_pos.x, world_pos.y, world_pos.z + tool_len,
|
||
world_pos.a, world_pos.b, world_pos.c
|
||
))
|
||
print(f"[XYZAC] INV: world({world_pos.x:.3f},{world_pos.y:.3f},"
|
||
f"{world_pos.z + tool_len:.3f},{world_pos.a:.3f},{world_pos.c:.3f}) -> "
|
||
f"joints({joints[0]:.3f},{joints[1]:.3f},{joints[2]:.3f})")
|
||
print(f"[KINS] 当前运动学: {restore_type.name}, RTCP: 启用")
|
||
|
||
def _switch_to_fiveaxis_bc(self):
|
||
"""M430: 切换到五轴桥式铣床运动学 (FIVEAXIS_BC)"""
|
||
if self.debug:
|
||
print(f"[KINS] M430: 切换到五轴桥式铣床运动学 (FIVEAXIS_BC)")
|
||
|
||
# 保存切换前的运动学类型,以便M428恢复
|
||
# 关键修复:只在当前不是FIVEAXIS_BC时才保存
|
||
if self.kinematics_type != KinematicsType.FIVEAXIS_BC:
|
||
self._saved_kinematics_type = self.kinematics_type
|
||
if self.debug:
|
||
print(f"[KINS] 已保存原始运动学类型: {self._saved_kinematics_type.name}")
|
||
|
||
# 切换到 FIVEAXIS_BC
|
||
self.kinematics_type = KinematicsType.FIVEAXIS_BC
|
||
self.data.kinematics_type = "FIVEAXIS_BC"
|
||
self.rtcp_enabled = True
|
||
self.data.rtcp_enabled = True
|
||
|
||
# 创建新的运动学实例
|
||
self.kinematics = KinematicsFactory.create(
|
||
KinematicsType.FIVEAXIS_BC, debug=self.debug
|
||
)
|
||
|
||
if self.kinematics:
|
||
self.kinematics.enable_rtcp(True)
|
||
# 恢复刀具长度补偿
|
||
if self.current_h_code > 0:
|
||
tool_len = self.tool_lengths.get(self.current_h_code, 0.0)
|
||
self.kinematics.set_tool_length(tool_len)
|
||
if self.debug:
|
||
print(f"[KINS] 设置刀具长度: H{self.current_h_code} = {tool_len:.1f}mm")
|
||
|
||
# 重新同步机器坐标
|
||
self._sync_machine_from_current()
|
||
self._sync_parameters_from_state()
|
||
|
||
if self.debug:
|
||
print(f"[KINS] 当前运动学: FIVEAXIS_BC, RTCP: 启用")
|
||
|
||
|
||
# ==================== RTCP 控制 ====================
|
||
|
||
|
||
# ==================== RTCP 控制 ====================
|
||
|
||
def set_rtcp_enable(self, enable: bool = True, with_tool_length: bool = True):
|
||
"""启用/禁用RTCP"""
|
||
if self.kinematics is None:
|
||
return
|
||
|
||
old_rtcp = self.rtcp_enabled
|
||
self.rtcp_enabled = enable
|
||
self.data.rtcp_enabled = enable
|
||
self.kinematics.enable_rtcp(enable)
|
||
|
||
# ===== 新增:生成RTCP状态切换的segment =====
|
||
if enable and not old_rtcp:
|
||
segment = MoveSegment(
|
||
type=MoveType.RTCP_ON,
|
||
start=self.current_pos.copy(),
|
||
end=self.current_pos.copy(),
|
||
line_number=self.current_line,
|
||
is_rtcp=True
|
||
)
|
||
self.data.segments.append(segment)
|
||
self._point_count += 1
|
||
elif not enable and old_rtcp:
|
||
segment = MoveSegment(
|
||
type=MoveType.RTCP_OFF,
|
||
start=self.current_pos.copy(),
|
||
end=self.current_pos.copy(),
|
||
line_number=self.current_line,
|
||
is_rtcp=False
|
||
)
|
||
self.data.segments.append(segment)
|
||
self._point_count += 1
|
||
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_tool_length_offset(self, h_code: int):
|
||
"""
|
||
G43 H: 设置刀具长度补偿(同步到参数表)
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::convert_tool_length_offset
|
||
|
||
Args:
|
||
h_code: H 代码编号
|
||
"""
|
||
self.current_h_code = h_code
|
||
tool_len = self.tool_lengths.get(h_code, 0.0)
|
||
|
||
# ★ 关键修复:设置 tool_offset.z ★
|
||
self.tool_offset.z = tool_len
|
||
|
||
# 同步刀具偏移到参数表
|
||
if hasattr(self, 'var_manager') and self.var_manager:
|
||
self.var_manager._tool_offset['Z'] = tool_len
|
||
self.var_manager._tool_offset_active = True
|
||
|
||
# 更新运动学实例的刀具长度
|
||
if self.kinematics:
|
||
self.kinematics.set_tool_length(tool_len)
|
||
if self.debug:
|
||
print(f"[TLO] 刀具长度补偿: H{h_code} = {tool_len:.1f}mm, 运动学已更新")
|
||
else:
|
||
if self.debug:
|
||
print(f"[TLO] 刀具长度补偿: H{h_code} = {tool_len:.1f}mm (运动学实例为空, IDENTITY模式)")
|
||
|
||
def cancel_tool_length_offset(self):
|
||
"""
|
||
G49: 取消刀具长度补偿
|
||
|
||
完全参照 LinuxCNC 官方实现:
|
||
rs274ngc.cpp: Interp::convert_tool_length_offset (G49 分支)
|
||
"""
|
||
self.current_h_code = 0
|
||
self.tool_offset.z = 0.0
|
||
|
||
if hasattr(self, 'var_manager') and self.var_manager:
|
||
self.var_manager._tool_offset['Z'] = 0.0
|
||
self.var_manager._tool_offset_active = False
|
||
|
||
if self.kinematics:
|
||
self.kinematics.set_tool_length(0.0)
|
||
if self.debug:
|
||
print(f"[TLO] 刀具长度补偿已取消 (G49)")
|
||
|
||
# ==================== 刀具补偿 ====================
|
||
|
||
def set_tool_radius_offset(self, d_code: int):
|
||
"""设置刀具半径补偿值(从刀具表读取)
|
||
|
||
Args:
|
||
d_code: D代码编号,对应刀具表中的刀具半径
|
||
"""
|
||
self.current_d_code = d_code
|
||
radius = self.tool_radii.get(d_code, 0.0)
|
||
self.comp_radius = radius
|
||
|
||
# 同步刀具半径到参数表
|
||
self.var_manager._cutter_comp_radius = radius
|
||
|
||
if self.debug:
|
||
print(f"[COMP] D{d_code} 刀具半径设置为: {radius:.3f} mm")
|
||
|
||
def cancel_tool_radius_offset(self):
|
||
"""取消刀具半径补偿(G40)"""
|
||
self.current_d_code = 0
|
||
self.comp_radius = 0.0
|
||
self.var_manager._cutter_comp_radius = 0.0
|
||
|
||
if self.debug:
|
||
print("[COMP] 刀具半径补偿已取消")
|
||
|
||
def set_cutter_compensation_state(self, comp_type: CompType, radius: float = 0.0,
|
||
side: int = 0, d_word: float = 0.0):
|
||
"""设置刀具半径补偿(同步到参数表)"""
|
||
old_type = self.comp_type
|
||
self.comp_type = comp_type
|
||
self.comp_radius = radius
|
||
self.d_word = d_word
|
||
self.cutter_comp_active = (comp_type != CompType.OFF)
|
||
|
||
# ========== 同步到参数表 ==========
|
||
self.var_manager.set_cutter_compensation_active(self.cutter_comp_active)
|
||
|
||
if comp_type == CompType.OFF and old_type != CompType.OFF:
|
||
self.cutter_comp_firstmove = True
|
||
self.arc_not_allowed = True
|
||
elif comp_type != CompType.OFF and old_type == CompType.OFF:
|
||
self.cutter_comp_firstmove = True
|
||
self.arc_not_allowed = False
|
||
self.program_x = self.current_pos.x
|
||
self.program_y = self.current_pos.y
|
||
self.program_z = self.current_pos.z
|
||
|
||
|
||
def cancel_cutter_compensation(self):
|
||
"""G40: 取消刀具半径补偿"""
|
||
self.set_cutter_compensation_state(CompType.OFF, 0.0, 0, 0.0)
|
||
|
||
# ==================== 运动段添加 ====================
|
||
|
||
def _add_segment_remove(self, move_type: MoveType, end_world: Point6D, **kwargs):
|
||
"""添加运动段"""
|
||
if self._point_count >= self.max_points:
|
||
return
|
||
|
||
start_world = self.current_pos.copy()
|
||
linear_distance = start_world.distance_to(end_world)
|
||
|
||
if move_type in [MoveType.RAPID, MoveType.FEED]:
|
||
feed_for_calc = self.feedrate if move_type == MoveType.FEED else None
|
||
result = self.planner.calculate_time(linear_distance, feed_for_calc,
|
||
is_rapid=(move_type == MoveType.RAPID))
|
||
elif move_type == MoveType.DWELL:
|
||
dwell_time = kwargs.get('dwell_time', 0)
|
||
result = {'duration': dwell_time, 'max_velocity': 0, 'acceleration_time': 0,
|
||
'constant_time': 0, 'deceleration_time': 0, 'profile_type': 'dwell'}
|
||
else:
|
||
result = {'duration': 0.1, 'max_velocity': 0, 'acceleration_time': 0,
|
||
'constant_time': 0, 'deceleration_time': 0, 'profile_type': 'other'}
|
||
|
||
old_machine_x = self.machine_x
|
||
old_machine_y = self.machine_y
|
||
old_machine_z = self.machine_z
|
||
old_machine_a = self.machine_a
|
||
old_machine_b = self.machine_b
|
||
old_machine_c = self.machine_c
|
||
|
||
self.current_pos = end_world.copy()
|
||
self._sync_machine_from_current()
|
||
|
||
machine_start = Point6D(old_machine_x, old_machine_y, old_machine_z,
|
||
old_machine_a, old_machine_b, old_machine_c)
|
||
machine_end = Point6D(self.machine_x, self.machine_y, self.machine_z,
|
||
self.machine_a, self.machine_b, self.machine_c)
|
||
|
||
segment = MoveSegment(
|
||
type=move_type, start=machine_start, end=machine_end,
|
||
line_number=self.current_line,
|
||
feedrate=self.feedrate if move_type == MoveType.FEED else 0,
|
||
comp_type=self.comp_type, comp_radius=self.comp_radius, d_word=self.d_word,
|
||
duration=result['duration'], max_velocity=result['max_velocity'],
|
||
acceleration_time=result['acceleration_time'],
|
||
constant_time=result['constant_time'],
|
||
deceleration_time=result['deceleration_time'],
|
||
profile_type=result['profile_type'],
|
||
is_rtcp=self.rtcp_enabled,
|
||
world_start=start_world.copy(), world_end=end_world.copy(),
|
||
tool_number=self.state.tool,
|
||
spindle_speed=self.state.spindle_speed,
|
||
spindle_state=self.state.spindle_state,
|
||
**{k: v for k, v in kwargs.items()
|
||
if k in ['center_x', 'center_y', 'center_z', 'radius', 'turn', 'dwell_time']}
|
||
)
|
||
|
||
self.data.segments.append(segment)
|
||
self.data.total_length += linear_distance
|
||
self.cumulative_time += result['duration']
|
||
self._point_count += 1
|
||
self._sync_parameters_from_state()
|
||
|
||
# ==================== 运动方法 ====================
|
||
|
||
def straight_traverse(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float, u: float, v: float, w: float):
|
||
"""G0 快速移动 - 使用工件坐标记录"""
|
||
if self._g53_active:
|
||
self._handle_g53_move(x, y, z, a, b, c, u, v, w, MoveType.RAPID)
|
||
return
|
||
|
||
# 处理绝对/相对坐标
|
||
if not self.is_absolute:
|
||
x = self.current_pos.x + (x if x is not None else 0)
|
||
y = self.current_pos.y + (y if y is not None else 0)
|
||
z = self.current_pos.z + (z if z is not None else 0)
|
||
a = self.current_pos.a + (a if a is not None else 0)
|
||
b = self.current_pos.b + (b if b is not None else 0)
|
||
c = self.current_pos.c + (c if c is not None else 0)
|
||
else:
|
||
x = x if x is not None else self.current_pos.x
|
||
y = y if y is not None else self.current_pos.y
|
||
z = z if z is not None else self.current_pos.z
|
||
a = a if a is not None else self.current_pos.a
|
||
b = b if b is not None else self.current_pos.b
|
||
c = c if c is not None else self.current_pos.c
|
||
|
||
# 创建工件坐标目标点
|
||
end = Point6D(x, y, z, a, b, c)
|
||
|
||
# 使用统一的运动段添加方法
|
||
self._add_world_segment(MoveType.RAPID, end)
|
||
|
||
# 更新 GLCanonPure 需要的坐标
|
||
self.lo = [x, y, z, a, b, c, u, v, w]
|
||
|
||
def straight_feed(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float, u: float, v: float, w: float):
|
||
"""G1 直线进给 - 使用工件坐标记录"""
|
||
if self._g53_active:
|
||
self._handle_g53_move(x, y, z, a, b, c, u, v, w, MoveType.FEED)
|
||
return
|
||
|
||
# 处理绝对/相对坐标
|
||
if not self.is_absolute:
|
||
x = self.current_pos.x + (x if x is not None else 0)
|
||
y = self.current_pos.y + (y if y is not None else 0)
|
||
z = self.current_pos.z + (z if z is not None else 0)
|
||
a = self.current_pos.a + (a if a is not None else 0)
|
||
b = self.current_pos.b + (b if b is not None else 0)
|
||
c = self.current_pos.c + (c if c is not None else 0)
|
||
else:
|
||
x = x if x is not None else self.current_pos.x
|
||
y = y if y is not None else self.current_pos.y
|
||
z = z if z is not None else self.current_pos.z
|
||
a = a if a is not None else self.current_pos.a
|
||
b = b if b is not None else self.current_pos.b
|
||
c = c if c is not None else self.current_pos.c
|
||
|
||
# 创建工件坐标目标点
|
||
end = Point6D(x, y, z, a, b, c)
|
||
|
||
# 处理刀具补偿
|
||
if self.cutter_comp_active and self.comp_radius > 0:
|
||
comp_x, comp_y, comp_z = self._compensate_straight(end.x, end.y, end.z)
|
||
end = Point6D(comp_x, comp_y, comp_z, end.a, end.b, end.c)
|
||
|
||
# 使用统一的运动段添加方法
|
||
self._add_world_segment(MoveType.FEED, end)
|
||
|
||
# 更新 GLCanonPure 需要的坐标
|
||
self.lo = [x, y, z, a, b, c, u, v, w]
|
||
|
||
|
||
def _handle_g53_move(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
u: float, v: float, w: float, move_type: MoveType):
|
||
"""处理G53模式下的运动
|
||
|
||
G53使用机床坐标系,刀具长度补偿(TLO)在G53模式下已被取消
|
||
(LinuxCNC标准行为:G53自动取消刀具长度补偿)
|
||
"""
|
||
old_mx, old_my, old_mz = self.machine_x, self.machine_y, self.machine_z
|
||
old_ma, old_mb, old_mc = self.machine_a, self.machine_b, self.machine_c
|
||
|
||
self.machine_x = x if x is not None else self.machine_x
|
||
self.machine_y = y if y is not None else self.machine_y
|
||
self.machine_z = z if z is not None else self.machine_z
|
||
self.machine_a = a if a is not None else self.machine_a
|
||
self.machine_b = b if b is not None else self.machine_b
|
||
self.machine_c = c if c is not None else self.machine_c
|
||
|
||
# G53模式下,临时清零刀具长度补偿
|
||
saved_tool_offset_z = self.tool_offset.z
|
||
self.tool_offset.z = 0.0
|
||
|
||
self._update_current_from_machine()
|
||
|
||
# 恢复刀具长度补偿
|
||
self.tool_offset.z = saved_tool_offset_z
|
||
|
||
end = self.current_pos.copy()
|
||
|
||
machine_start = Point6D(old_mx, old_my, old_mz, old_ma, old_mb, old_mc)
|
||
machine_end = Point6D(self.machine_x, self.machine_y, self.machine_z,
|
||
self.machine_a, self.machine_b, self.machine_c)
|
||
|
||
segment = MoveSegment(
|
||
type=move_type, start=machine_start, end=machine_end,
|
||
line_number=self.current_line,
|
||
feedrate=self.feedrate if move_type == MoveType.FEED else 0,
|
||
duration=0.1, profile_type='g53',
|
||
is_rtcp=False, world_start=self.current_pos.copy(), world_end=end.copy(),
|
||
tool_number=self.state.tool
|
||
)
|
||
self.data.segments.append(segment)
|
||
self._g53_active = False
|
||
self._sync_parameters_from_state()
|
||
|
||
|
||
|
||
def arc_feed_bak(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
i: float, j: float, k: float, turn: int, feed: float,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""
|
||
G2/G3 圆弧进给
|
||
|
||
参数说明:
|
||
x, y, z: 圆弧终点坐标 (工件坐标系)
|
||
a, b, c: 终点旋转轴角度
|
||
i, j, k: 圆心相对于起点的偏移量 (INCREMENTAL IJK)
|
||
turn: 圈数 (负数=G2顺时针, 正数=G3逆时针)
|
||
feed: 进给率 (mm/min)
|
||
|
||
修复说明:
|
||
I, J, K 始终作为相对于起点的增量处理
|
||
圆心 = 起点 + 偏移量
|
||
"""
|
||
start_x, start_y, start_z = self.current_pos.x, self.current_pos.y, self.current_pos.z
|
||
|
||
# 处理绝对/相对坐标
|
||
if not self.is_absolute:
|
||
x = self.current_pos.x + x
|
||
y = self.current_pos.y + y
|
||
z = self.current_pos.z + z
|
||
a = self.current_pos.a + (a if a is not None else 0)
|
||
b = self.current_pos.b + (b if b is not None else 0)
|
||
c = self.current_pos.c + (c if c is not None else 0)
|
||
else:
|
||
x = x if x is not None else self.current_pos.x
|
||
y = y if y is not None else self.current_pos.y
|
||
z = z if z is not None else self.current_pos.z
|
||
a = a if a is not None else self.current_pos.a
|
||
b = b if b is not None else self.current_pos.b
|
||
c = c if c is not None else self.current_pos.c
|
||
|
||
# ===== 关键修复:圆心计算 =====
|
||
# I, J, K 始终是相对于起点的增量偏移量
|
||
# 圆心坐标 = 起点坐标 + 偏移量
|
||
i_val = i if i is not None else 0.0
|
||
j_val = j if j is not None else 0.0
|
||
k_val = k if k is not None else 0.0
|
||
|
||
cx = start_x + i_val
|
||
cy = start_y + j_val
|
||
cz = start_z + k_val
|
||
|
||
# 计算半径
|
||
radius = math.hypot(i_val, j_val)
|
||
|
||
if self.debug:
|
||
print(f"[ARC] 起点=({start_x:.3f},{start_y:.3f}) "
|
||
f"圆心=({cx:.3f},{cy:.3f}) "
|
||
f"I={i_val:.3f} J={j_val:.3f} R={radius:.3f}")
|
||
|
||
# 验证终点在圆上
|
||
# 验证终点在圆上(对照 LinuxCNC arc_data_ijk 中的容差检查)
|
||
end_dist = math.hypot(x - cx, y - cy)
|
||
# 使用与 LinuxCNC 一致的容差(spiral_abs_tolerance)
|
||
spiral_tolerance = 0.01 # mm,对于公制单位
|
||
if abs(end_dist - radius) > spiral_tolerance * 100.0:
|
||
# 相对误差检查
|
||
rel_err = abs(end_dist - radius) / max(radius, end_dist) if max(radius, end_dist) > 0 else 0
|
||
if rel_err > 0.001 and abs(end_dist - radius) > spiral_tolerance:
|
||
if self.debug:
|
||
print(f"[ARC] ⚠ 终点({x:.3f},{y:.3f})不在圆上: "
|
||
f"距圆心={end_dist:.6f}, R={radius:.6f}, "
|
||
f"绝对误差={abs(end_dist - radius):.6f}mm, "
|
||
f"相对误差={rel_err:.6f}")
|
||
# 将终点调整到圆上(容错处理)
|
||
if end_dist > TINY:
|
||
angle = math.atan2(y - cy, x - cx)
|
||
x = cx + radius * math.cos(angle)
|
||
y = cy + radius * math.sin(angle)
|
||
if self.debug:
|
||
print(f"[ARC] 调整终点到圆上: ({x:.3f},{y:.3f})")
|
||
else:
|
||
if self.debug:
|
||
print(f"[ARC] 终点误差在容差范围内,不调整")
|
||
|
||
# 创建终点
|
||
end = Point6D(x, y, z, a, b, c)
|
||
|
||
# 保存原始进给率
|
||
original_feedrate = self.feedrate
|
||
if feed > 0:
|
||
self.feedrate = feed
|
||
|
||
# 计算弧长
|
||
arc_length = abs(turn) * 2.0 * math.pi * radius
|
||
result = self.planner.calculate_time(arc_length, self.feedrate, is_rapid=False)
|
||
|
||
# 机器坐标
|
||
machine_start = Point6D(self.machine_x, self.machine_y, self.machine_z,
|
||
self.machine_a, self.machine_b, self.machine_c)
|
||
|
||
# 计算终点关节坐标
|
||
if self.rtcp_enabled and self.kinematics is not None:
|
||
wx = x + self.work_offset_x + self.g5x_offset_x + self.g92_offset_x + self.g52_offset_x
|
||
wy = y + self.work_offset_y + self.g5x_offset_y + self.g92_offset_y + self.g52_offset_y
|
||
wz = z + self.work_offset_z + self.g5x_offset_z + self.g92_offset_z + self.g52_offset_z
|
||
wa = a + self.work_offset_a + self.g5x_offset_a + self.g92_offset_a + self.g52_offset_a
|
||
wb = b + self.work_offset_b + self.g5x_offset_b + self.g92_offset_b + self.g52_offset_b
|
||
wc = c + self.work_offset_c + self.g5x_offset_c + self.g92_offset_c + self.g52_offset_c
|
||
|
||
world_tuple = (wx, wy, wz, wa, wb, wc)
|
||
joints = self.kinematics.world_to_joints(world_tuple)
|
||
# 使用统一的关节映射函数
|
||
end_joints = joints_to_point6d(joints, self.kinematics_type)
|
||
else:
|
||
end_joints = Point6D(x, y, z, a, b, c)
|
||
|
||
machine_end = Point6D(
|
||
self.machine_x + (end_joints.x - self.current_pos.x),
|
||
self.machine_y + (end_joints.y - self.current_pos.y),
|
||
self.machine_z + (end_joints.z - self.current_pos.z),
|
||
end_joints.a, end_joints.b, end_joints.c
|
||
)
|
||
|
||
# 创建圆弧运动段
|
||
arc_segment = MoveSegment(
|
||
type=MoveType.ARC_CW if turn < 0 else MoveType.ARC_CCW,
|
||
start=machine_start, end=machine_end,
|
||
line_number=self.current_line, feedrate=self.feedrate,
|
||
center_x=cx, center_y=cy, center_z=cz,
|
||
radius=radius, turn=turn,
|
||
comp_type=self.comp_type, comp_radius=self.comp_radius, d_word=self.d_word,
|
||
duration=result['duration'], max_velocity=result['max_velocity'],
|
||
acceleration_time=result['acceleration_time'],
|
||
constant_time=result['constant_time'],
|
||
deceleration_time=result['deceleration_time'],
|
||
profile_type=result['profile_type'],
|
||
is_rtcp=self.rtcp_enabled,
|
||
world_start=self.current_pos.copy(), world_end=end.copy(),
|
||
tool_number=self.state.tool,
|
||
spindle_speed=self.state.spindle_speed,
|
||
spindle_state=self.state.spindle_state
|
||
)
|
||
self.data.segments.append(arc_segment)
|
||
self.data.total_length += arc_length
|
||
self.cumulative_time += result['duration']
|
||
|
||
# 更新当前位置
|
||
self.current_pos = end_joints.copy()
|
||
self.machine_x = machine_end.x
|
||
self.machine_y = machine_end.y
|
||
self.machine_z = machine_end.z
|
||
self.machine_a = machine_end.a
|
||
self.machine_b = machine_end.b
|
||
self.machine_c = machine_end.c
|
||
|
||
# 恢复进给率
|
||
self.feedrate = original_feedrate
|
||
self._sync_parameters_from_state()
|
||
|
||
|
||
|
||
def arc_feed(self, x: float, y: float, z: float,
|
||
a: float, b: float, c: float,
|
||
i: float, j: float, k: float, turn: int, feed: float,
|
||
u: float = 0.0, v: float = 0.0, w: float = 0.0):
|
||
"""
|
||
G2/G3 圆弧进给 - 基于参数表进行坐标管理
|
||
|
||
参照 LinuxCNC interp_convert.cc convert_arc2() 的逻辑:
|
||
1. 在工件坐标系中计算圆心
|
||
2. 验证圆弧几何(参照 arc_data_ijk 的容差检查)
|
||
3. 创建 MoveSegment(使用工件坐标)
|
||
4. 更新当前位置(工件坐标)
|
||
|
||
参数:
|
||
x, y, z: 圆弧终点(工件坐标)
|
||
a, b, c: 终点旋转轴角度(工件坐标)
|
||
i, j, k: 圆心相对起点的偏移(增量,工件坐标)
|
||
turn: 圈数(负数=G2顺时针,正数=G3逆时针)
|
||
feed: 进给率 (mm/min)
|
||
"""
|
||
# ===== 第1步:获取起点(工件坐标)=====
|
||
start_x = self.current_pos.x
|
||
start_y = self.current_pos.y
|
||
start_z = self.current_pos.z
|
||
start_a = self.current_pos.a
|
||
start_b = self.current_pos.b
|
||
start_c = self.current_pos.c
|
||
|
||
# ===== 第2步:处理绝对/相对坐标 =====
|
||
if not self.is_absolute:
|
||
x = start_x + (x if x is not None else 0)
|
||
y = start_y + (y if y is not None else 0)
|
||
z = start_z + (z if z is not None else 0)
|
||
a = start_a + (a if a is not None else 0)
|
||
b = start_b + (b if b is not None else 0)
|
||
c = start_c + (c if c is not None else 0)
|
||
else:
|
||
x = x if x is not None else start_x
|
||
y = y if y is not None else start_y
|
||
z = z if z is not None else start_z
|
||
a = a if a is not None else start_a
|
||
b = b if b is not None else start_b
|
||
c = c if c is not None else start_c
|
||
|
||
# ===== 第3步:计算圆心(工件坐标系)=====
|
||
# 参照 LinuxCNC arc_data_ijk:IJK 始终是相对于起点的增量
|
||
i_val = i if i is not None else 0.0
|
||
j_val = j if j is not None else 0.0
|
||
k_val = k if k is not None else 0.0
|
||
|
||
# 圆心 = 起点 + IJK 偏移
|
||
cx = start_x + i_val
|
||
cy = start_y + j_val
|
||
cz = start_z + k_val
|
||
|
||
# 计算半径
|
||
radius = math.hypot(i_val, j_val)
|
||
|
||
if self.debug:
|
||
print(f"[ARC] 起点=({start_x:.3f},{start_y:.3f}) "
|
||
f"圆心=({cx:.3f},{cy:.3f}) "
|
||
f"I={i_val:.3f} J={j_val:.3f} R={radius:.3f}")
|
||
|
||
# ===== 第4步:验证圆弧几何(参照 LinuxCNC 容差)=====
|
||
units = getattr(self.state, 'units', 21)
|
||
is_valid, msg, error = self.var_manager.validate_arc_geometry(
|
||
start_x, start_y, x, y, cx, cy, radius, units
|
||
)
|
||
|
||
if not is_valid and '零半径' in msg:
|
||
if self.debug:
|
||
print(f"[ARC] ⚠ {msg}")
|
||
return
|
||
|
||
if not is_valid:
|
||
if self.debug:
|
||
print(f"[ARC] ⚠ {msg}")
|
||
# 容错处理:将终点调整到圆上
|
||
end_radius = math.hypot(x - cx, y - cy)
|
||
if end_radius > TINY:
|
||
angle = math.atan2(y - cy, x - cx)
|
||
x = cx + radius * math.cos(angle)
|
||
y = cy + radius * math.sin(angle)
|
||
if self.debug:
|
||
print(f"[ARC] 调整终点到圆上: ({x:.6f}, {y:.6f})")
|
||
|
||
# ===== 第5步:计算弧长和速度曲线 =====
|
||
arc_length = abs(turn) * 2.0 * math.pi * radius
|
||
|
||
# 保存当前进给率
|
||
original_feedrate = self.feedrate
|
||
if feed > 0:
|
||
self.feedrate = feed
|
||
|
||
result = self.planner.calculate_time(arc_length, self.feedrate, is_rapid=False)
|
||
|
||
# ===== 第6步:创建工件坐标和绝对坐标点 =====
|
||
world_start = Point6D(start_x, start_y, start_z, start_a, start_b, start_c)
|
||
world_end = Point6D(x, y, z, a, b, c)
|
||
|
||
# 通过参数表计算绝对坐标
|
||
abs_start = self.var_manager.program_to_absolute(
|
||
start_x, start_y, start_z, start_a, start_b, start_c
|
||
)
|
||
abs_end = self.var_manager.program_to_absolute(x, y, z, a, b, c)
|
||
|
||
abs_start_pt = Point6D(
|
||
abs_start[0], abs_start[1], abs_start[2],
|
||
abs_start[3], abs_start[4], abs_start[5]
|
||
)
|
||
abs_end_pt = Point6D(
|
||
abs_end[0], abs_end[1], abs_end[2],
|
||
abs_end[3], abs_end[4], abs_end[5]
|
||
)
|
||
|
||
# ===== 第7步:创建 MoveSegment =====
|
||
arc_segment = MoveSegment(
|
||
type=MoveType.ARC_CW if turn < 0 else MoveType.ARC_CCW,
|
||
start=world_start.copy(),
|
||
end=world_end.copy(),
|
||
line_number=self.current_line,
|
||
feedrate=self.feedrate,
|
||
center_x=cx,
|
||
center_y=cy,
|
||
center_z=cz,
|
||
radius=radius,
|
||
turn=turn,
|
||
comp_type=self.comp_type,
|
||
comp_radius=self.comp_radius,
|
||
d_word=self.d_word,
|
||
duration=result['duration'],
|
||
max_velocity=result['max_velocity'],
|
||
acceleration_time=result['acceleration_time'],
|
||
constant_time=result['constant_time'],
|
||
deceleration_time=result['deceleration_time'],
|
||
profile_type=result['profile_type'],
|
||
is_rtcp=self.rtcp_enabled,
|
||
world_start=world_start.copy(),
|
||
world_end=world_end.copy(),
|
||
machine_start=abs_start_pt,
|
||
machine_end=abs_end_pt,
|
||
tool_number=self.state.tool,
|
||
spindle_speed=self.state.spindle_speed,
|
||
spindle_state=self.state.spindle_state,
|
||
coord_system='WORLD',
|
||
active_csys=self.var_manager.active_csys,
|
||
g92_active=self.var_manager._g92_active,
|
||
gcode=f"G{2 if turn < 0 else 3}"
|
||
)
|
||
|
||
# ===== 第8步:添加到路径数据 =====
|
||
self.data.segments.append(arc_segment)
|
||
self.data.total_length += arc_length
|
||
self.cumulative_time += result['duration']
|
||
self._point_count += 1
|
||
|
||
# 更新 GLCanonPure 需要的坐标
|
||
self.lo = [x, y, z, a, b, c, u, v, w]
|
||
|
||
# ===== 第9步:更新工件坐标位置 =====
|
||
self.current_pos = world_end.copy()
|
||
|
||
# ===== 第10步:同步到参数表 =====
|
||
self._sync_position_to_params()
|
||
|
||
# ===== 第11步:同步机器坐标 =====
|
||
self._sync_machine_from_current()
|
||
|
||
# ===== 第12步:恢复进给率 =====
|
||
self.feedrate = original_feedrate
|
||
|
||
# ===== 第13步:同步所有参数 =====
|
||
self._sync_parameters_from_state()
|
||
|
||
# ===== 第14步:标记运动段类型 =====
|
||
self._last_segment_was_arc = True
|
||
|
||
def dwell(self, seconds: float):
|
||
"""G4 暂停 - 使用工件坐标记录"""
|
||
self._add_world_segment(
|
||
MoveType.DWELL,
|
||
self.current_pos.copy(),
|
||
dwell_time=seconds
|
||
)
|
||
|
||
# ==================== 刀具补偿辅助 ====================
|
||
|
||
def _compensate_straight(self, end_x: float, end_y: float, end_z: float) -> Tuple[float, float, float]:
|
||
"""直线刀具半径补偿"""
|
||
if not self.cutter_comp_active or self.comp_radius <= 0:
|
||
return (end_x, end_y, end_z)
|
||
|
||
if self.cutter_comp_firstmove:
|
||
dx = end_x - self.program_x
|
||
dy = end_y - self.program_y
|
||
length = math.hypot(dx, dy)
|
||
|
||
if length > CART_FUZZ:
|
||
angle = math.atan2(dy, dx)
|
||
factor = 1.0 if self.comp_type == CompType.LEFT else -1.0
|
||
alpha = angle + factor * (math.pi / 2)
|
||
comp_end_x = end_x + self.comp_radius * math.cos(alpha)
|
||
comp_end_y = end_y + self.comp_radius * math.sin(alpha)
|
||
else:
|
||
comp_end_x, comp_end_y = end_x, end_y
|
||
|
||
self.cutter_comp_firstmove = False
|
||
self.program_x = end_x
|
||
self.program_y = end_y
|
||
self.program_z = end_z
|
||
return (comp_end_x, comp_end_y, end_z)
|
||
else:
|
||
dx = end_x - self.program_x
|
||
dy = end_y - self.program_y
|
||
length = math.hypot(dx, dy)
|
||
|
||
if length > CART_FUZZ:
|
||
ux, uy = dx / length, dy / length
|
||
factor = 1.0 if self.comp_type == CompType.LEFT else -1.0
|
||
perp_x, perp_y = -uy * factor, ux * factor
|
||
comp_end_x = end_x + self.comp_radius * perp_x
|
||
comp_end_y = end_y + self.comp_radius * perp_y
|
||
else:
|
||
comp_end_x, comp_end_y = end_x, end_y
|
||
|
||
self.program_x = end_x
|
||
self.program_y = end_y
|
||
self.program_z = end_z
|
||
return (comp_end_x, comp_end_y, end_z)
|
||
|
||
def _world_to_joints(self, world_pos: Point6D) -> Point6D:
|
||
"""工件坐标转关节坐标"""
|
||
if not self.rtcp_enabled or self.kinematics is None:
|
||
return world_pos
|
||
|
||
wx = world_pos.x + self.work_offset_x + self.g5x_offset_x + self.g92_offset_x + self.g52_offset_x
|
||
wy = world_pos.y + self.work_offset_y + self.g5x_offset_y + self.g92_offset_y + self.g52_offset_y
|
||
wz = world_pos.z + self.work_offset_z + self.g5x_offset_z + self.g92_offset_z + self.g52_offset_z
|
||
wa = world_pos.a + self.work_offset_a + self.g5x_offset_a + self.g92_offset_a + self.g52_offset_a
|
||
wb = world_pos.b + self.work_offset_b + self.g5x_offset_b + self.g92_offset_b + self.g52_offset_b
|
||
wc = world_pos.c + self.work_offset_c + self.g5x_offset_c + self.g92_offset_c + self.g52_offset_c
|
||
|
||
world_tuple = (wx, wy, wz, wa, wb, wc)
|
||
joints = self.kinematics.world_to_joints(world_tuple)
|
||
|
||
return joints_to_point6d(joints, self.kinematics_type)
|
||
|
||
def _joints_to_world(self, joints_pos: Point6D) -> Point6D:
|
||
"""关节坐标转工件坐标"""
|
||
if not self.rtcp_enabled or self.kinematics is None:
|
||
return joints_pos
|
||
|
||
# 构建符合运动学类型的关节数组
|
||
config = KINEMATICS_JOINT_CONFIG.get(self.kinematics_type, {})
|
||
num_joints = config.get('num_joints', 6)
|
||
joints = [0.0] * num_joints
|
||
|
||
x_idx = config.get('x_idx', 0)
|
||
y_idx = config.get('y_idx', 1)
|
||
z_idx = config.get('z_idx', 2)
|
||
a_idx = config.get('a_idx', 3)
|
||
b_idx = config.get('b_idx', 4)
|
||
c_idx = config.get('c_idx', 5)
|
||
|
||
if x_idx >= 0: joints[x_idx] = joints_pos.x
|
||
if y_idx >= 0: joints[y_idx] = joints_pos.y
|
||
if z_idx >= 0: joints[z_idx] = joints_pos.z
|
||
if a_idx >= 0: joints[a_idx] = joints_pos.a
|
||
if b_idx >= 0: joints[b_idx] = joints_pos.b
|
||
if c_idx >= 0: joints[c_idx] = joints_pos.c
|
||
|
||
world = self.kinematics.joints_to_world(joints)
|
||
|
||
wx = world[0] - self.work_offset_x - self.g5x_offset_x - self.g92_offset_x - self.g52_offset_x
|
||
wy = world[1] - self.work_offset_y - self.g5x_offset_y - self.g92_offset_y - self.g52_offset_y
|
||
wz = world[2] - self.work_offset_z - self.g5x_offset_z - self.g92_offset_z - self.g52_offset_z
|
||
wa = world[3] - self.work_offset_a - self.g5x_offset_a - self.g92_offset_a - self.g52_offset_a
|
||
wb = world[4] - self.work_offset_b - self.g5x_offset_b - self.g92_offset_b - self.g52_offset_b
|
||
wc = world[5] - self.work_offset_c - self.g5x_offset_c - self.g92_offset_c - self.g52_offset_c
|
||
|
||
return Point6D(wx, wy, wz, wa, wb, wc)
|
||
|
||
# ==================== 其他方法 ====================
|
||
|
||
def set_feed_rate(self, feed: float):
|
||
self.feedrate = feed
|
||
self.state.feedrate = feed
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_spindle_speed(self, speed: float):
|
||
self.state.spindle_speed = speed
|
||
self._sync_parameters_from_state()
|
||
|
||
def change_tool(self, tool: int):
|
||
if tool not in self.tool_table:
|
||
self.tool_table[tool] = ToolData(tool_number=tool)
|
||
self.current_tool = self.tool_table[tool]
|
||
self.state.tool = tool
|
||
self.current_pocket = tool
|
||
|
||
offsets = {'x': 0.0, 'y': 0.0, 'z': self.tool_lengths.get(tool, 0.0),
|
||
'a': 0.0, 'b': 0.0, 'c': 0.0,
|
||
'u': 0.0, 'v': 0.0, 'w': 0.0}
|
||
self.var_manager.update_tool_params(tool, offsets,
|
||
self.current_tool.diameter)
|
||
self._sync_parameters_from_state()
|
||
self.data.tool_changes.append({'tool': tool, 'line': self.current_line,
|
||
'position': self.current_pos.to_list()})
|
||
|
||
def select_tool(self, tool: int):
|
||
self.selected_tool = tool
|
||
self.selected_pocket = tool
|
||
|
||
def set_absolute_mode(self):
|
||
self.is_absolute = True
|
||
self.state.distance_mode = 90
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_relative_mode(self):
|
||
self.is_absolute = False
|
||
self.state.distance_mode = 91
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_plane(self, plane: int):
|
||
self.current_plane = plane
|
||
self.plane = plane
|
||
self.state.plane = plane
|
||
self._sync_parameters_from_state()
|
||
|
||
def spindle_control(self, mode: int):
|
||
self.state.spindle_mode = mode
|
||
if mode == 1:
|
||
self.state.spindle_state = "CW"
|
||
elif mode == 2:
|
||
self.state.spindle_state = "CCW"
|
||
elif mode == 0:
|
||
self.state.spindle_state = "OFF"
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_flood(self, enable: bool):
|
||
self.state.coolant_flood = enable
|
||
self._sync_parameters_from_state()
|
||
|
||
def set_mist(self, enable: bool):
|
||
self.state.coolant_mist = enable
|
||
self._sync_parameters_from_state()
|
||
|
||
def program_stop(self):
|
||
pass
|
||
|
||
def optional_stop(self):
|
||
pass
|
||
|
||
def program_end(self):
|
||
"""程序结束 - 使用工件坐标记录"""
|
||
self._add_world_segment(
|
||
MoveType.PROGRAM_END,
|
||
self.current_pos.copy(),
|
||
dwell_time=0.1
|
||
)
|
||
|
||
def set_g28(self, x: float, y: float, z: float, a: float = 0.0, b: float = 0.0, c: float = 0.0):
|
||
self.g28_ref = {'X': x, 'Y': y, 'Z': z, 'A': a, 'B': b, 'C': c}
|
||
self.var_manager.set_g28(x, y, z, a, b, c)
|
||
|
||
def set_g30(self, x: float, y: float, z: float, a: float = 0.0, b: float = 0.0, c: float = 0.0):
|
||
self.g30_ref = {'X': x, 'Y': y, 'Z': z, 'A': a, 'B': b, 'C': c}
|
||
self.var_manager.set_g30(x, y, z, a, b, c)
|
||
|
||
def get_g28(self) -> Tuple[float, float, float]:
|
||
return (self.g28_ref.get('X', 0.0), self.g28_ref.get('Y', 0.0), self.g28_ref.get('Z', 0.0))
|
||
|
||
def get_g30(self) -> Tuple[float, float, float]:
|
||
return (self.g30_ref.get('X', 0.0), self.g30_ref.get('Y', 0.0), self.g30_ref.get('Z', 0.0))
|
||
|
||
def save_state(self, restore_on_return: bool = False) -> Dict:
|
||
return {
|
||
'motion_mode': self.motion_mode,
|
||
'plane': self.current_plane,
|
||
'distance_mode': 90 if self.is_absolute else 91,
|
||
'current_pos': self.current_pos.copy(),
|
||
'feedrate': self.feedrate,
|
||
'tool': self.state.tool,
|
||
'restore_on_return': restore_on_return
|
||
}
|
||
|
||
def restore_state(self, saved_state: Dict = None):
|
||
if saved_state is None:
|
||
return
|
||
|
||
if 'g_codes' in saved_state:
|
||
g = saved_state['g_codes']
|
||
self.current_plane = g.get('plane', self.current_plane)
|
||
self.is_absolute = (g.get('distance_mode', 90) == 90)
|
||
|
||
if 'positions' in saved_state:
|
||
p = saved_state['positions']
|
||
self.current_pos = Point6D(
|
||
p.get('x', self.current_pos.x), p.get('y', self.current_pos.y),
|
||
p.get('z', self.current_pos.z), p.get('a', self.current_pos.a),
|
||
p.get('b', self.current_pos.b), p.get('c', self.current_pos.c)
|
||
)
|
||
self.machine_x = p.get('machine_x', self.machine_x)
|
||
self.machine_y = p.get('machine_y', self.machine_y)
|
||
self.machine_z = p.get('machine_z', self.machine_z)
|
||
|
||
self.feedrate = saved_state.get('feedrate', self.feedrate)
|
||
self.state.tool = saved_state.get('tool', self.state.tool)
|
||
self._sync_parameters_from_state()
|
||
|
||
def next_line(self, st):
|
||
if hasattr(st, 'sequence_number'):
|
||
self.current_line = st.sequence_number
|
||
self.lineno = st.sequence_number
|
||
if hasattr(st, 'plane'):
|
||
self.current_plane = st.plane
|
||
self.plane = st.plane
|
||
self.state.plane = st.plane
|
||
self._sync_parameters_from_state()
|
||
|
||
def get_total_time(self) -> float:
|
||
return self.cumulative_time
|
||
|
||
|
||
# ==================== CNCKernel 扩展方法 ====================
|
||
|
||
class CNCKernelExtensions:
|
||
"""CNCKernel 类的扩展方法集合"""
|
||
|
||
@staticmethod
|
||
def add_power_off(kernel_instance):
|
||
def power_off(self) -> Dict:
|
||
if not self.ensure_operational():
|
||
return {"success": False, "message": f"机床未就绪"}
|
||
if self.state_machine.request_power_off():
|
||
self.is_running = False
|
||
self.is_paused = False
|
||
self.status.state = MachineState.OFF.value
|
||
self.status.spindle_state = "OFF"
|
||
self.status.spindle_speed = 0
|
||
self.status.coolant_flood = False
|
||
self.status.coolant_mist = False
|
||
self._notify_update()
|
||
return {"success": True, "message": "断电成功"}
|
||
return {"success": False, "message": "无法断电"}
|
||
kernel_instance.power_off = power_off.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_home_all(kernel_instance):
|
||
def home_all(self) -> Dict:
|
||
if not self.ensure_operational():
|
||
return {"success": False, "message": "机床未就绪"}
|
||
self.state_machine.home_all()
|
||
self.status.state = MachineState.HOMING.value
|
||
self._notify_update()
|
||
|
||
def home_motion():
|
||
for axis, current in [('X', self.status.position_x), ('Y', self.status.position_y),
|
||
('Z', self.status.position_z), ('A', self.status.position_a),
|
||
('B', self.status.position_b), ('C', self.status.position_c)]:
|
||
if abs(current) < 0.001:
|
||
continue
|
||
step = current / 20
|
||
for _ in range(20):
|
||
if axis == 'X': self.status.position_x -= step
|
||
elif axis == 'Y': self.status.position_y -= step
|
||
elif axis == 'Z': self.status.position_z -= step
|
||
elif axis == 'A': self.status.position_a -= step
|
||
elif axis == 'B': self.status.position_b -= step
|
||
elif axis == 'C': self.status.position_c -= step
|
||
self._update_world_position()
|
||
self._notify_update()
|
||
time.sleep(0.05)
|
||
if axis == 'X': self.status.position_x = 0.0
|
||
elif axis == 'Y': self.status.position_y = 0.0
|
||
elif axis == 'Z': self.status.position_z = 0.0
|
||
elif axis == 'A': self.status.position_a = 0.0
|
||
elif axis == 'B': self.status.position_b = 0.0
|
||
elif axis == 'C': self.status.position_c = 0.0
|
||
self.state_machine.transition_to(MachineStateMachine.STATE_IDLE)
|
||
self.status.state = MachineState.IDLE.value
|
||
self._notify_update()
|
||
|
||
threading.Thread(target=home_motion, daemon=True).start()
|
||
return {"success": True, "message": "回零中..."}
|
||
kernel_instance.home_all = home_all.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_jog(kernel_instance):
|
||
"""添加点动功能 - 支持RTCP刀尖跟随
|
||
|
||
坐标变换链(与_set_workpiece_position保持一致):
|
||
工件坐标 → +G92偏移 → +XY旋转 → +G5x偏移 → +TLO → +G52偏移 → 逆运动学 → 关节坐标
|
||
|
||
参数:
|
||
axis: 轴名称 ('X','Y','Z','A','B','C')
|
||
direction: 方向 (1=正向, -1=负向)
|
||
distance: 移动距离 (mm或度)
|
||
|
||
返回:
|
||
dict: {'success': bool, 'message': str, 'position': dict}
|
||
"""
|
||
def jog(self, axis: str, direction: int, distance: float) -> Dict:
|
||
"""执行点动操作"""
|
||
if not self.ensure_operational():
|
||
return {"success": False, "message": "机床未就绪"}
|
||
|
||
axis_upper = axis.upper()
|
||
|
||
# 验证轴名称
|
||
valid_axes = {'X', 'Y', 'Z', 'A', 'B', 'C'}
|
||
if axis_upper not in valid_axes:
|
||
return {"success": False, "message": f"无效轴: {axis}, 有效轴: {valid_axes}"}
|
||
|
||
# 计算移动量
|
||
step = direction * distance
|
||
|
||
# 记录移动前坐标(用于调试)
|
||
old_pos = {
|
||
'x': self.status.position_x,
|
||
'y': self.status.position_y,
|
||
'z': self.status.position_z,
|
||
'a': self.status.position_a,
|
||
'b': self.status.position_b,
|
||
'c': self.status.position_c,
|
||
}
|
||
old_machine = {
|
||
'x': self.status.machine_x,
|
||
'y': self.status.machine_y,
|
||
'z': self.status.machine_z,
|
||
'a': self.status.machine_a,
|
||
'b': self.status.machine_b,
|
||
'c': self.status.machine_c,
|
||
}
|
||
|
||
# 计算目标工件坐标
|
||
target_x = self.status.position_x
|
||
target_y = self.status.position_y
|
||
target_z = self.status.position_z
|
||
target_a = self.status.position_a
|
||
target_b = self.status.position_b
|
||
target_c = self.status.position_c
|
||
|
||
if axis_upper == 'X':
|
||
target_x += step
|
||
elif axis_upper == 'Y':
|
||
target_y += step
|
||
elif axis_upper == 'Z':
|
||
target_z += step
|
||
elif axis_upper == 'A':
|
||
target_a += step
|
||
elif axis_upper == 'B':
|
||
target_b += step
|
||
elif axis_upper == 'C':
|
||
target_c += step
|
||
|
||
# ★★★ 核心修复:调用_set_workpiece_position处理完整坐标变换链 ★★★
|
||
# 该方法内部会:
|
||
# 1. 更新工件坐标
|
||
# 2. 计算绝对坐标(工件+G5x+G92+TLO)
|
||
# 3. RTCP启用时执行逆运动学得到关节坐标
|
||
# 4. RTCP禁用时直接使用绝对坐标作为关节坐标
|
||
# 5. 更新世界坐标
|
||
self._set_workpiece_position(
|
||
target_x, target_y, target_z,
|
||
target_a, target_b, target_c
|
||
)
|
||
|
||
# 更新世界坐标(正运动学验证)
|
||
self._update_world_position()
|
||
|
||
# 通知更新(包括MQTT回调,发送的是machine坐标)
|
||
self._notify_update()
|
||
|
||
# 调试输出
|
||
if self.debug:
|
||
rtcp_status = "启用" if self.rtcp_enabled else "禁用"
|
||
print(f"\n[JOG] {axis_upper}{'+' if direction > 0 else '-'}{distance} "
|
||
f"(RTCP: {rtcp_status})")
|
||
print(f" 工件坐标: ({old_pos['x']:.3f}, {old_pos['y']:.3f}, "
|
||
f"{old_pos['z']:.3f}) → ({target_x:.3f}, {target_y:.3f}, "
|
||
f"{target_z:.3f})")
|
||
print(f" 机器坐标: ({old_machine['x']:.3f}, {old_machine['y']:.3f}, "
|
||
f"{old_machine['z']:.3f}) → ({self.status.machine_x:.3f}, "
|
||
f"{self.status.machine_y:.3f}, {self.status.machine_z:.3f})")
|
||
if axis_upper in ('A', 'B', 'C'):
|
||
print(f" 旋转轴: {axis_upper}={self.status.__dict__.get(f'position_{axis_upper.lower()}', 0):.2f}°")
|
||
if self.rtcp_enabled:
|
||
# 计算RTCP补偿量
|
||
dx = self.status.machine_x - self.status.position_x
|
||
dy = self.status.machine_y - self.status.position_y
|
||
dz = self.status.machine_z - self.status.position_z
|
||
print(f" RTCP补偿: ΔX={dx:.3f}, ΔY={dy:.3f}, ΔZ={dz:.3f}")
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"点动 {axis_upper}{'+' if direction > 0 else '-'}{distance}",
|
||
"data": {
|
||
"axis": axis_upper,
|
||
"direction": direction,
|
||
"distance": distance,
|
||
"rtcp_enabled": self.rtcp_enabled,
|
||
"position": {
|
||
"workpiece": {
|
||
"x": self.status.position_x,
|
||
"y": self.status.position_y,
|
||
"z": self.status.position_z,
|
||
"a": self.status.position_a,
|
||
"b": self.status.position_b,
|
||
"c": self.status.position_c,
|
||
},
|
||
"machine": {
|
||
"x": self.status.machine_x,
|
||
"y": self.status.machine_y,
|
||
"z": self.status.machine_z,
|
||
"a": self.status.machine_a,
|
||
"b": self.status.machine_b,
|
||
"c": self.status.machine_c,
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
# 绑定方法到实例
|
||
kernel_instance.jog = jog.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
|
||
@staticmethod
|
||
def add_jog_machine(kernel_instance):
|
||
"""添加机器坐标系点动功能(G53模式)
|
||
|
||
在机器坐标系下直接点动关节,适用于:
|
||
- 手动调整关节位置
|
||
- 调试运动学参数
|
||
- 紧急情况直接控制关节
|
||
|
||
注意:机器坐标点动不走RTCP逆解,直接修改关节坐标。
|
||
"""
|
||
def jog_machine(self, axis: str, direction: int, distance: float) -> Dict:
|
||
"""在机器坐标系下执行点动"""
|
||
if not self.ensure_operational():
|
||
return {"success": False, "message": "机床未就绪"}
|
||
|
||
axis_upper = axis.upper()
|
||
valid_axes = {'X', 'Y', 'Z', 'A', 'B', 'C'}
|
||
if axis_upper not in valid_axes:
|
||
return {"success": False, "message": f"无效轴: {axis}"}
|
||
|
||
step = direction * distance
|
||
|
||
# 直接修改机器坐标
|
||
if axis_upper == 'X':
|
||
self.status.machine_x += step
|
||
elif axis_upper == 'Y':
|
||
self.status.machine_y += step
|
||
elif axis_upper == 'Z':
|
||
self.status.machine_z += step
|
||
elif axis_upper == 'A':
|
||
self.status.machine_a += step
|
||
elif axis_upper == 'B':
|
||
self.status.machine_b += step
|
||
elif axis_upper == 'C':
|
||
self.status.machine_c += step
|
||
|
||
# 反向计算工件坐标(通过_update_current_from_machine) # ★ 调用新增的方法反向计算工件坐标
|
||
self._update_current_from_machine()
|
||
|
||
# 更新世界坐标
|
||
self._update_world_position()
|
||
|
||
# 通知更新
|
||
self._notify_update()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"机器点动 {axis_upper}{'+' if direction > 0 else '-'}{distance}",
|
||
"data": {
|
||
"axis": axis_upper,
|
||
"direction": direction,
|
||
"distance": distance,
|
||
"rtcp_enabled": self.rtcp_enabled,
|
||
"position": {
|
||
"machine": {
|
||
"x": self.status.machine_x,
|
||
"y": self.status.machine_y,
|
||
"z": self.status.machine_z,
|
||
},
|
||
"workpiece": {
|
||
"x": self.status.position_x,
|
||
"y": self.status.position_y,
|
||
"z": self.status.position_z,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
kernel_instance.jog_machine = jog_machine.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
|
||
@staticmethod
|
||
def add_jog_incremental(kernel_instance):
|
||
"""添加增量式点动(明确使用增量模式)
|
||
|
||
与jog功能相同,但明确表示增量模式(G91)。
|
||
在增量模式下,每次移动都是相对于当前位置。
|
||
"""
|
||
def jog_incremental(self, axis: str, direction: int, distance: float) -> Dict:
|
||
"""增量式点动"""
|
||
# 直接委托给jog方法
|
||
return self.jog(axis, direction, distance)
|
||
|
||
kernel_instance.jog_incremental = jog_incremental.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
|
||
|
||
|
||
|
||
@staticmethod
|
||
def add_spindle_control(kernel_instance):
|
||
def spindle_start(self, direction: str = "CW", speed: float = 1000.0) -> Dict:
|
||
self.status.spindle_speed = speed
|
||
self.status.spindle_state = direction.upper()
|
||
self.virtual_hal.set('spindle.0.speed', speed)
|
||
self.virtual_hal.set('spindle.0.direction', 1 if direction.upper() == "CW" else -1)
|
||
self.virtual_hal.set('spindle.0.enable', 1)
|
||
self._notify_update()
|
||
return {"success": True, "message": f"主轴 {direction} {speed} RPM"}
|
||
|
||
def spindle_stop(self) -> Dict:
|
||
self.status.spindle_speed = 0
|
||
self.status.spindle_state = "OFF"
|
||
self.virtual_hal.set('spindle.0.speed', 0)
|
||
self.virtual_hal.set('spindle.0.enable', 0)
|
||
self._notify_update()
|
||
return {"success": True, "message": "主轴停止"}
|
||
|
||
kernel_instance.spindle_start = spindle_start.__get__(kernel_instance, type(kernel_instance))
|
||
kernel_instance.spindle_stop = spindle_stop.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_coolant_control(kernel_instance):
|
||
def coolant_on(self, coolant_type: str = "flood") -> Dict:
|
||
if coolant_type == "flood": self.status.coolant_flood = True
|
||
elif coolant_type == "mist": self.status.coolant_mist = True
|
||
elif coolant_type == "both": self.status.coolant_flood = self.status.coolant_mist = True
|
||
self._notify_update()
|
||
return {"success": True, "message": f"冷却液开启 ({coolant_type})"}
|
||
|
||
def coolant_off(self) -> Dict:
|
||
self.status.coolant_flood = False
|
||
self.status.coolant_mist = False
|
||
self._notify_update()
|
||
return {"success": True, "message": "冷却液关闭"}
|
||
|
||
kernel_instance.coolant_on = coolant_on.__get__(kernel_instance, type(kernel_instance))
|
||
kernel_instance.coolant_off = coolant_off.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_override_controls(kernel_instance):
|
||
def set_feed_override(self, value: int) -> Dict:
|
||
self.status.feed_override = max(0, min(200, value))
|
||
self.planner.set_feed_override(self.status.feed_override / 100.0)
|
||
self._notify_update()
|
||
return {"success": True, "message": f"进给倍率 {self.status.feed_override}%"}
|
||
|
||
def set_rapid_override(self, value: int) -> Dict:
|
||
self.status.rapid_override = max(0, min(100, value))
|
||
self.planner.set_rapid_override(self.status.rapid_override / 100.0)
|
||
self._notify_update()
|
||
return {"success": True, "message": f"快速倍率 {self.status.rapid_override}%"}
|
||
|
||
def set_spindle_override(self, value: int) -> Dict:
|
||
self.status.spindle_override = max(50, min(120, value))
|
||
self._notify_update()
|
||
return {"success": True, "message": f"主轴倍率 {self.status.spindle_override}%"}
|
||
|
||
kernel_instance.set_feed_override = set_feed_override.__get__(kernel_instance, type(kernel_instance))
|
||
kernel_instance.set_rapid_override = set_rapid_override.__get__(kernel_instance, type(kernel_instance))
|
||
kernel_instance.set_spindle_override = set_spindle_override.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_get_full_config(kernel_instance):
|
||
def get_full_config(self) -> Dict:
|
||
return {
|
||
'machine_id': self.name,
|
||
'machine_name': self.name,
|
||
'state': self.state_machine.get_state(),
|
||
'status': {
|
||
'position': {'x': self.status.position_x, 'y': self.status.position_y,
|
||
'z': self.status.position_z, 'a': self.status.position_a,
|
||
'b': self.status.position_b, 'c': self.status.position_c},
|
||
'world_position': {'x': self.status.world_x, 'y': self.status.world_y, 'z': self.status.world_z},
|
||
'spindle': {'speed': self.status.spindle_speed, 'state': self.status.spindle_state},
|
||
'coolant': {'flood': self.status.coolant_flood, 'mist': self.status.coolant_mist},
|
||
'tool': {'current': self.status.current_tool, 'length': self.status.tool_length},
|
||
'feedrate': self.status.feedrate,
|
||
'overrides': {'feed': self.status.feed_override, 'rapid': self.status.rapid_override},
|
||
'program': {'name': self.status.program_name, 'progress': self.status.progress_percent},
|
||
'rtcp_enabled': self.status.rtcp_enabled,
|
||
'kinematics_type': self.status.kinematics_type
|
||
},
|
||
'tool_table': self.tool_table,
|
||
'work_offsets': self.work_offsets
|
||
}
|
||
kernel_instance.get_full_config = get_full_config.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def add_change_tool(kernel_instance):
|
||
def change_tool(self, tool_number: int) -> Dict:
|
||
if tool_number in self.tool_table:
|
||
self.status.current_tool = tool_number
|
||
self.selected_tool = tool_number
|
||
self.status.tool_length = self.tool_table[tool_number]["length"]
|
||
self.status.tool_diameter = self.tool_table[tool_number]["diameter"]
|
||
self._notify_update()
|
||
return {"success": True, "message": f"换刀 T{tool_number}"}
|
||
return {"success": False, "message": f"刀具 T{tool_number} 不存在"}
|
||
kernel_instance.change_tool = change_tool.__get__(kernel_instance, type(kernel_instance))
|
||
|
||
@staticmethod
|
||
def extend_all(kernel_instance):
|
||
"""一次性扩展所有方法"""
|
||
CNCKernelExtensions.add_power_off(kernel_instance)
|
||
CNCKernelExtensions.add_home_all(kernel_instance)
|
||
CNCKernelExtensions.add_jog(kernel_instance) # ★ 已修复
|
||
CNCKernelExtensions.add_jog_machine(kernel_instance) # ★ 新增
|
||
CNCKernelExtensions.add_jog_incremental(kernel_instance) # ★ 新增
|
||
CNCKernelExtensions.add_spindle_control(kernel_instance)
|
||
CNCKernelExtensions.add_coolant_control(kernel_instance)
|
||
CNCKernelExtensions.add_override_controls(kernel_instance)
|
||
CNCKernelExtensions.add_get_full_config(kernel_instance)
|
||
CNCKernelExtensions.add_change_tool(kernel_instance)
|
||
|
||
|
||
# ==================== MachineManager 类 ====================
|
||
|
||
class MachineManager:
|
||
"""机床管理器 - 简化版"""
|
||
|
||
def __init__(self, debug: bool = False):
|
||
self.debug = debug
|
||
self._machines: Dict[str, CNCKernel] = {}
|
||
self._machine_configs: Dict[str, Dict] = {}
|
||
self._machine_programs: Dict[str, Dict] = {}
|
||
|
||
|
||
|
||
def create_machine(self, machine_id: str, config: Optional[Dict] = None) -> CNCKernel:
|
||
if machine_id in self._machines:
|
||
return self._machines[machine_id]
|
||
|
||
machine_name = config.get('machine_name', machine_id) if config else machine_id
|
||
|
||
# 使用 CNCKernelWithJSON 替代 CNCKernel
|
||
kernel = CNCKernelWithJSON(debug=self.debug, name=machine_name)
|
||
|
||
if config:
|
||
self._machine_configs[machine_id] = config
|
||
|
||
kin_config = config.get('kinematics', {})
|
||
kin_type_str = kin_config.get('type', 'TRT_BC').upper()
|
||
type_map = {
|
||
'TRT_AC': KinematicsType.TRT_AC, 'XYZAC_TRT': KinematicsType.TRT_AC,
|
||
'TRT_BC': KinematicsType.TRT_BC, 'XYZBC_TRT': KinematicsType.TRT_BC,
|
||
'IDENTITY': KinematicsType.IDENTITY, 'FIVEAXIS_BC': KinematicsType.FIVEAXIS_BC,
|
||
'MAXKINS_BC': KinematicsType.MAXKINS_BC, 'HEXAPOD': KinematicsType.HEXAPOD,
|
||
'PUMA': KinematicsType.PUMA, 'SCARA': KinematicsType.SCARA,
|
||
'LINEAR_DELTA': KinematicsType.LINEAR_DELTA,
|
||
}
|
||
kernel.kinematics_type = type_map.get(kin_type_str, KinematicsType.TRT_BC)
|
||
kernel._original_kinematics_type = kernel.kinematics_type
|
||
|
||
if kernel._kinematics_instance:
|
||
kernel._kinematics_instance.set_params(**kin_config.get('parameters', {}))
|
||
|
||
for tool in config.get('tool_table', []):
|
||
pocket = tool.get('pocket', 1)
|
||
kernel.tool_table[pocket] = {
|
||
'length': tool.get('length', 100.0),
|
||
'diameter': tool.get('diameter', 10.0),
|
||
'name': tool.get('name', f'T{pocket}')
|
||
}
|
||
|
||
for offset_name, offset in config.get('work_offsets', {}).items():
|
||
if offset_name.startswith('G'):
|
||
try:
|
||
offset_num = int(offset_name[1:])
|
||
kernel.work_offsets[offset_num] = {
|
||
'x': offset.get('X', 0.0), 'y': offset.get('Y', 0.0),
|
||
'z': offset.get('Z', 0.0), 'a': offset.get('A', 0.0),
|
||
'b': offset.get('B', 0.0), 'c': offset.get('C', 0.0)
|
||
}
|
||
except ValueError:
|
||
pass
|
||
|
||
for pin_name, pin_value in config.get('hal_pins', {}).items():
|
||
kernel.virtual_hal.set(pin_name, pin_value)
|
||
|
||
self._machines[machine_id] = kernel
|
||
if self.debug:
|
||
print(f"[MachineManager] 创建机床: {machine_id}")
|
||
return kernel
|
||
|
||
|
||
def create_machine_CNCKernel(self, machine_id: str, config: Optional[Dict] = None) -> CNCKernel:
|
||
if machine_id in self._machines:
|
||
return self._machines[machine_id]
|
||
|
||
machine_name = config.get('machine_name', machine_id) if config else machine_id
|
||
kernel = CNCKernel(debug=self.debug, name=machine_name)
|
||
CNCKernelExtensions.extend_all(kernel)
|
||
|
||
if config:
|
||
self._machine_configs[machine_id] = config
|
||
|
||
# 设置运动学类型
|
||
kin_config = config.get('kinematics', {})
|
||
kin_type_str = kin_config.get('type', 'TRT_BC').upper()
|
||
type_map = {
|
||
'TRT_AC': KinematicsType.TRT_AC, 'XYZAC_TRT': KinematicsType.TRT_AC,
|
||
'TRT_BC': KinematicsType.TRT_BC, 'XYZBC_TRT': KinematicsType.TRT_BC,
|
||
'IDENTITY': KinematicsType.IDENTITY, 'FIVEAXIS_BC': KinematicsType.FIVEAXIS_BC,
|
||
'MAXKINS_BC': KinematicsType.MAXKINS_BC, 'HEXAPOD': KinematicsType.HEXAPOD,
|
||
'PUMA': KinematicsType.PUMA, 'SCARA': KinematicsType.SCARA,
|
||
'LINEAR_DELTA': KinematicsType.LINEAR_DELTA,
|
||
}
|
||
kernel.kinematics_type = type_map.get(kin_type_str, KinematicsType.TRT_BC)
|
||
kernel._original_kinematics_type = kernel.kinematics_type
|
||
|
||
# 设置运动学参数
|
||
if kernel._kinematics_instance:
|
||
kernel._kinematics_instance.set_params(**kin_config.get('parameters', {}))
|
||
|
||
# 设置刀具表
|
||
for tool in config.get('tool_table', []):
|
||
pocket = tool.get('pocket', 1)
|
||
kernel.tool_table[pocket] = {
|
||
'length': tool.get('length', 100.0),
|
||
'diameter': tool.get('diameter', 10.0),
|
||
'name': tool.get('name', f'T{pocket}')
|
||
}
|
||
|
||
# 设置工件坐标系
|
||
for offset_name, offset in config.get('work_offsets', {}).items():
|
||
if offset_name.startswith('G'):
|
||
try:
|
||
offset_num = int(offset_name[1:])
|
||
kernel.work_offsets[offset_num] = {
|
||
'x': offset.get('X', 0.0), 'y': offset.get('Y', 0.0),
|
||
'z': offset.get('Z', 0.0), 'a': offset.get('A', 0.0),
|
||
'b': offset.get('B', 0.0), 'c': offset.get('C', 0.0)
|
||
}
|
||
except ValueError:
|
||
pass
|
||
|
||
# 设置HAL引脚
|
||
for pin_name, pin_value in config.get('hal_pins', {}).items():
|
||
kernel.virtual_hal.set(pin_name, pin_value)
|
||
|
||
self._machines[machine_id] = kernel
|
||
if self.debug:
|
||
print(f"[MachineManager] 创建机床: {machine_id}")
|
||
return kernel
|
||
|
||
def get_machine(self, machine_id: str) -> Optional[CNCKernel]:
|
||
return self._machines.get(machine_id)
|
||
|
||
def remove_machine(self, machine_id: str) -> bool:
|
||
if machine_id in self._machines:
|
||
self._machines[machine_id].shutdown()
|
||
del self._machines[machine_id]
|
||
self._machine_programs.pop(machine_id, None)
|
||
self._machine_configs.pop(machine_id, None)
|
||
return True
|
||
return False
|
||
|
||
def list_machines(self) -> List[str]:
|
||
return list(self._machines.keys())
|
||
|
||
def get_status(self, machine_id: str) -> Optional[Dict]:
|
||
kernel = self.get_machine(machine_id)
|
||
if not kernel:
|
||
return None
|
||
s = kernel.status
|
||
|
||
# 尝试从收集器获取偏移信息
|
||
g5x_x = g5x_y = g5x_z = 0.0
|
||
g92_x = g92_y = g92_z = 0.0
|
||
|
||
if hasattr(kernel, 'parser') and kernel.parser:
|
||
parser = kernel.parser
|
||
if hasattr(parser, '_builtin_parser') and parser._builtin_parser:
|
||
bp = parser._builtin_parser
|
||
if bp.collector:
|
||
c = bp.collector
|
||
g5x_x = c.g5x_offset_x
|
||
g5x_y = c.g5x_offset_y
|
||
g5x_z = c.g5x_offset_z
|
||
g92_x = c.g92_offset_x
|
||
g92_y = c.g92_offset_y
|
||
g92_z = c.g92_offset_z
|
||
|
||
return {
|
||
'machine_id': machine_id,
|
||
'machine_name': kernel.name,
|
||
'state': kernel.state_machine.get_state(),
|
||
# ... 现有字段 ...
|
||
'g5x_offset_x': g5x_x,
|
||
'g5x_offset_y': g5x_y,
|
||
'g5x_offset_z': g5x_z,
|
||
'g92_x': g92_x,
|
||
'g92_y': g92_y,
|
||
'g92_z': g92_z,
|
||
'tool_length': s.tool_length,
|
||
'coordinate_mode': s.current_offset if s.current_offset else 'G54',
|
||
'rtcp_enabled': s.rtcp_enabled,
|
||
'current_tool': s.current_tool,
|
||
'selected_tool': s.selected_tool,
|
||
'tool_length': s.tool_length,
|
||
'tool_diameter': s.tool_diameter,
|
||
'position': {'x': s.position_x, 'y': s.position_y, 'z': s.position_z,
|
||
'a': s.position_a, 'b': s.position_b, 'c': s.position_c},
|
||
'world_position': {'x': s.world_x, 'y': s.world_y, 'z': s.world_z},
|
||
'machine_position': {'x': s.machine_x, 'y': s.machine_y, 'z': s.machine_z},
|
||
'spindle_speed': s.spindle_speed,
|
||
'spindle_state': s.spindle_state,
|
||
'coolant_flood': s.coolant_flood,
|
||
'coolant_mist': s.coolant_mist,
|
||
'coolant_state': s.coolant_state,
|
||
'feedrate': s.feedrate,
|
||
'feed_override': s.feed_override,
|
||
'rapid_override': s.rapid_override,
|
||
'spindle_override': s.spindle_override,
|
||
'program_name': s.program_name,
|
||
'program_progress': s.progress_percent,
|
||
'current_line': s.current_line,
|
||
'total_lines': s.total_lines,
|
||
'path_length': s.path_length,
|
||
'total_time': s.total_time,
|
||
'alarm_code': s.alarm_code,
|
||
'alarm_message': s.alarm_message,
|
||
'coordinate_mode': s.coordinate_mode,
|
||
'plane_mode': s.plane_mode,
|
||
'unit_mode': s.unit_mode,
|
||
'cutter_comp': s.cutter_comp,
|
||
'current_offset': s.current_offset,
|
||
}
|
||
|
||
def get_all_status(self) -> Dict[str, Dict]:
|
||
result = {}
|
||
for mid in self._machines:
|
||
status = self.get_status(mid)
|
||
if status:
|
||
result[mid] = status
|
||
return result
|
||
|
||
def load_program(self, machine_id: str, program_config: Dict) -> bool:
|
||
kernel = self.get_machine(machine_id)
|
||
if not kernel:
|
||
return False
|
||
|
||
try:
|
||
self._machine_programs[machine_id] = program_config
|
||
mp = program_config.get('main_program', {})
|
||
content = mp.get('content', '')
|
||
if not content:
|
||
return False
|
||
|
||
# 获取运动学参数
|
||
kinematics_params = {}
|
||
if kernel._kinematics_instance and kernel._kinematics_instance.params:
|
||
p = kernel._kinematics_instance.params
|
||
kinematics_params = {
|
||
'rot_center_x': getattr(p, 'rot_center_x', 0.0),
|
||
'rot_center_y': getattr(p, 'rot_center_y', 0.0),
|
||
'rot_center_z': getattr(p, 'rot_center_z', 0.0),
|
||
'pivot_length': getattr(p, 'pivot_length', 250.0),
|
||
'tool_length': getattr(p, 'tool_length', 0.0),
|
||
'conventional_directions': getattr(p, 'conventional_directions', False)
|
||
}
|
||
|
||
parser = FullRS274Parser(
|
||
debug=self.debug,
|
||
kinematics_type=kernel.kinematics_type,
|
||
kinematics_params=kinematics_params,
|
||
max_points=program_config.get('simulation_config', {}).get('max_points', 50000),
|
||
acceleration=program_config.get('simulation_config', {}).get('acceleration', 500.0),
|
||
max_rapid_rate=program_config.get('simulation_config', {}).get('max_rapid_rate', 10000.0),
|
||
max_feed_rate=program_config.get('simulation_config', {}).get('max_feed_rate', 5000.0),
|
||
tool_length_map={k: v['length'] for k, v in kernel.tool_table.items()},
|
||
tool_radius_map={k: v['diameter'] / 2.0 for k, v in kernel.tool_table.items()}
|
||
)
|
||
|
||
for sub in program_config.get('subroutines', []):
|
||
if sub.get('name') and sub.get('content'):
|
||
parser.register_subroutine(sub['name'], sub['content'])
|
||
|
||
toolpath = parser.parse_string(content, mp.get('name', 'program.ngc'))
|
||
if toolpath:
|
||
kernel.current_toolpath = toolpath
|
||
kernel.status.total_lines = len(toolpath.segments)
|
||
kernel.status.path_length = toolpath.total_length
|
||
kernel.status.total_time = toolpath.total_time
|
||
kernel.status.program_name = mp.get('name', '')
|
||
kernel.status.rtcp_enabled = toolpath.rtcp_enabled
|
||
kernel.status.kinematics_type = toolpath.kinematics_type
|
||
if self.debug:
|
||
print(f"[MachineManager] 程序加载成功: {machine_id}, 段数: {len(toolpath.segments)}")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"[MachineManager] 加载程序失败: {e}")
|
||
return False
|
||
|
||
def get_machine_program_info(self, machine_id: str) -> Optional[Dict]:
|
||
kernel = self.get_machine(machine_id)
|
||
if not kernel or not kernel.current_toolpath:
|
||
return None
|
||
tp = kernel.current_toolpath
|
||
pc = self._machine_programs.get(machine_id, {})
|
||
return {
|
||
'program_id': pc.get('program_id', ''),
|
||
'program_name': pc.get('program_name', tp.filename),
|
||
'filename': tp.filename,
|
||
'total_segments': len(tp.segments),
|
||
'total_length': tp.total_length,
|
||
'total_time': tp.total_time,
|
||
'bounds': tp.bounds,
|
||
'statistics': tp.get_statistics(),
|
||
'rtcp_enabled': tp.rtcp_enabled,
|
||
'kinematics_type': tp.kinematics_type,
|
||
'tool_changes': tp.tool_changes,
|
||
'subroutines': list(tp.subroutines.keys()) if tp.subroutines else [],
|
||
'errors': tp.errors,
|
||
'warnings': tp.warnings,
|
||
}
|
||
|
||
|
||
# ==================== CNCKernelWithJSON 类 ====================
|
||
|
||
class CNCKernelWithJSON(CNCKernel):
|
||
"""支持JSON配置的CNC内核"""
|
||
|
||
def __init__(self, debug: bool = False, name: str = "CNC1",
|
||
json_config: Optional[Union[str, Dict]] = None):
|
||
super().__init__(debug=debug, name=name)
|
||
|
||
CNCKernelExtensions.extend_all(self)
|
||
|
||
self.project_config: Optional[ProjectConfig] = None
|
||
self.current_program_config: Optional[Dict] = None
|
||
self.machine_id = name
|
||
|
||
if json_config:
|
||
self.load_config(json_config)
|
||
|
||
def load_config(self, json_source: Union[str, Dict]) -> Tuple[ProjectConfig, Optional[ToolpathData]]:
|
||
"""加载JSON配置"""
|
||
if isinstance(json_source, str):
|
||
if os.path.exists(json_source):
|
||
with open(json_source, 'r', encoding='utf-8') as f:
|
||
config_data = json.load(f)
|
||
else:
|
||
config_data = json.loads(json_source)
|
||
else:
|
||
config_data = json_source
|
||
|
||
config = self._parse_config(config_data)
|
||
self.project_config = config
|
||
self._apply_machine_config(config)
|
||
|
||
toolpath = self._load_program_from_config(config)
|
||
self.current_toolpath = toolpath
|
||
|
||
if toolpath:
|
||
self.status.total_lines = len(toolpath.segments)
|
||
self.status.path_length = toolpath.total_length
|
||
self.status.total_time = toolpath.total_time
|
||
self.status.path_segments = len(toolpath.segments)
|
||
self.status.rtcp_enabled = toolpath.rtcp_enabled
|
||
self.status.kinematics_type = toolpath.kinematics_type
|
||
self.status.program_name = config.main_program.name if config.main_program else ''
|
||
|
||
self._notify_update()
|
||
return config, toolpath
|
||
|
||
def _parse_config(self, data: Dict) -> ProjectConfig:
|
||
"""解析配置数据"""
|
||
config = ProjectConfig()
|
||
|
||
if 'project' in data:
|
||
config.project = data['project']
|
||
|
||
if 'machine' in data:
|
||
mc = data['machine']
|
||
config.machine = MachineConfig(
|
||
name=mc.get('name', ''),
|
||
type=mc.get('type', '5axis_mill'),
|
||
kinematics_type=mc.get('kinematics', {}).get('type', 'TRT_BC'),
|
||
kinematics_params=mc.get('kinematics', {}).get('parameters', {}),
|
||
axes=mc.get('axes', {}),
|
||
spindle=mc.get('spindle', {}),
|
||
tool_table=mc.get('tool_table', []),
|
||
work_offsets=mc.get('work_offsets', {})
|
||
)
|
||
|
||
if 'hal_config' in data:
|
||
hc = data['hal_config']
|
||
pins = {}
|
||
for name, pin in hc.get('pins', {}).items():
|
||
if isinstance(pin, dict):
|
||
pins[name] = HALPinConfig(
|
||
name=name,
|
||
pin_type=pin.get('type', 'float'),
|
||
direction=pin.get('direction', 'in'),
|
||
default=pin.get('default', 0.0),
|
||
description=pin.get('description', '')
|
||
)
|
||
else:
|
||
pins[name] = HALPinConfig(
|
||
name=name,
|
||
default=float(pin) if isinstance(pin, (int, float)) else 0.0
|
||
)
|
||
config.hal_config = HALConfig(
|
||
version=hc.get('version', '1.0'),
|
||
description=hc.get('description', ''),
|
||
pins=pins,
|
||
connections=hc.get('connections', [])
|
||
)
|
||
else:
|
||
config.hal_config = HALConfig()
|
||
|
||
if 'main_program' in data:
|
||
mp = data['main_program']
|
||
config.main_program = ProgramConfig(
|
||
name=mp.get('name', ''),
|
||
description=mp.get('description', ''),
|
||
content=mp.get('content', '')
|
||
)
|
||
|
||
if 'subroutines' in data:
|
||
config.subroutines = [
|
||
SubroutineConfig(
|
||
name=sub.get('name', ''),
|
||
description=sub.get('description', ''),
|
||
content=sub.get('content', ''),
|
||
parameters=sub.get('parameters', [])
|
||
)
|
||
for sub in data['subroutines']
|
||
]
|
||
|
||
if 'simulation_config' in data:
|
||
sc = data['simulation_config']
|
||
config.simulation_config = SimulationConfig(
|
||
acceleration=sc.get('acceleration', 500.0),
|
||
max_rapid_rate=sc.get('max_rapid_rate', 10000.0),
|
||
max_feed_rate=sc.get('max_feed_rate', 5000.0),
|
||
arc_division=sc.get('arc_division', 64),
|
||
enable_rtcp_debug=sc.get('enable_rtcp_debug', False),
|
||
max_points=sc.get('max_points', 50000),
|
||
colors=sc.get('colors', {})
|
||
)
|
||
else:
|
||
config.simulation_config = SimulationConfig()
|
||
|
||
return config
|
||
|
||
def _apply_machine_config(self, config: ProjectConfig):
|
||
"""应用机床配置"""
|
||
# 设置HAL引脚
|
||
for pin_name, pin_config in config.hal_config.pins.items():
|
||
if isinstance(pin_config, HALPinConfig):
|
||
self.virtual_hal.set(pin_name, pin_config.default)
|
||
elif isinstance(pin_config, dict):
|
||
self.virtual_hal.set(pin_name, pin_config.get('default', 0))
|
||
else:
|
||
self.virtual_hal.set(pin_name, pin_config)
|
||
|
||
# 设置刀具表
|
||
for tool in config.machine.tool_table:
|
||
pocket = tool.get('pocket', 1)
|
||
self.tool_table[pocket] = {
|
||
'length': tool.get('length', 100.0),
|
||
'diameter': tool.get('diameter', 10.0),
|
||
'name': tool.get('name', f'T{pocket}')
|
||
}
|
||
|
||
# 设置工件坐标系
|
||
for offset_name, offset_data in config.machine.work_offsets.items():
|
||
if offset_name.startswith('G'):
|
||
try:
|
||
offset_num = int(offset_name[1:])
|
||
self.work_offsets[offset_num] = {
|
||
'x': offset_data.get('X', 0.0),
|
||
'y': offset_data.get('Y', 0.0),
|
||
'z': offset_data.get('Z', 0.0),
|
||
'a': offset_data.get('A', 0.0),
|
||
'b': offset_data.get('B', 0.0),
|
||
'c': offset_data.get('C', 0.0)
|
||
}
|
||
except ValueError:
|
||
pass
|
||
|
||
# 设置运动学类型
|
||
kin_type_str = config.machine.kinematics_type.upper()
|
||
type_map = {
|
||
'IDENTITY': KinematicsType.IDENTITY,
|
||
'TRT_AC': KinematicsType.TRT_AC,
|
||
'XYZAC_TRT': KinematicsType.TRT_AC,
|
||
'TRT_BC': KinematicsType.TRT_BC,
|
||
'XYZBC_TRT': KinematicsType.TRT_BC,
|
||
'MAXKINS_BC': KinematicsType.MAXKINS_BC,
|
||
'FIVEAXIS_BC': KinematicsType.FIVEAXIS_BC,
|
||
'HEXAPOD': KinematicsType.HEXAPOD,
|
||
'PUMA': KinematicsType.PUMA,
|
||
'SCARA': KinematicsType.SCARA,
|
||
'LINEAR_DELTA': KinematicsType.LINEAR_DELTA,
|
||
}
|
||
self._original_kinematics_type = type_map.get(kin_type_str, KinematicsType.TRT_AC)
|
||
self.kinematics_type = self._original_kinematics_type
|
||
self.status.kinematics_type = self.kinematics_type.name
|
||
|
||
# 设置运动学参数
|
||
kin_params = config.machine.kinematics_params
|
||
if self._kinematics_instance:
|
||
self._kinematics_instance.set_params(**kin_params)
|
||
|
||
self.virtual_hal.set('motion.pivot-length', kin_params.get('pivot_length', 250.0))
|
||
|
||
def _load_program_from_config(self, config: ProjectConfig) -> Optional[ToolpathData]:
|
||
"""从配置加载程序"""
|
||
sim_config = config.simulation_config
|
||
|
||
parser = FullRS274Parser(
|
||
max_points=sim_config.max_points,
|
||
acceleration=sim_config.acceleration,
|
||
max_rapid_rate=sim_config.max_rapid_rate,
|
||
max_feed_rate=sim_config.max_feed_rate,
|
||
kinematics_type=self.kinematics_type,
|
||
kinematics_params=config.machine.kinematics_params,
|
||
debug=sim_config.enable_rtcp_debug
|
||
)
|
||
|
||
# 设置刀具表
|
||
for tool in config.machine.tool_table:
|
||
pocket = tool.get('pocket', 1)
|
||
parser.tool_length_map[pocket] = tool.get('length', 100.0)
|
||
parser.tool_radius_map[pocket] = tool.get('diameter', 10.0) / 2.0
|
||
|
||
# 注册子程序
|
||
for sub in config.subroutines:
|
||
name = sub.name if hasattr(sub, 'name') else sub.get('name', '')
|
||
content = sub.content if hasattr(sub, 'content') else sub.get('content', '')
|
||
if name and content:
|
||
parser.register_subroutine(name, content)
|
||
|
||
# 解析主程序
|
||
if config.main_program and config.main_program.content:
|
||
return parser.parse_string(
|
||
config.main_program.content,
|
||
config.main_program.name
|
||
)
|
||
return None
|
||
|
||
def load_from_json(self, json_source: Union[str, Dict]) -> Tuple[ProjectConfig, Optional[ToolpathData]]:
|
||
return self.load_config(json_source)
|
||
|
||
def load_from_json_file(self, filepath: str) -> Tuple[ProjectConfig, Optional[ToolpathData]]:
|
||
return self.load_config(filepath)
|
||
|
||
def load_from_json_string(self, json_str: str) -> Tuple[ProjectConfig, Optional[ToolpathData]]:
|
||
return self.load_config(json_str)
|
||
|
||
def load_program_config(self, program_config: Dict) -> bool:
|
||
try:
|
||
self.current_program_config = program_config
|
||
full_config = self._build_config_from_program(program_config)
|
||
self.load_config(full_config)
|
||
return True
|
||
except Exception as e:
|
||
if self.debug:
|
||
print(f"加载程序配置失败: {e}")
|
||
return False
|
||
|
||
def _build_config_from_program(self, program_config: Dict) -> Dict:
|
||
machine_config = {
|
||
"name": self.machine_id,
|
||
"kinematics": {
|
||
"type": self.kinematics_type.name,
|
||
"parameters": {}
|
||
}
|
||
}
|
||
|
||
if self.project_config:
|
||
machine_config = {
|
||
"name": self.machine_id,
|
||
"type": self.project_config.machine.type,
|
||
"kinematics": {
|
||
"type": self.project_config.machine.kinematics_type,
|
||
"parameters": self.project_config.machine.kinematics_params
|
||
},
|
||
"tool_table": self.project_config.machine.tool_table,
|
||
"work_offsets": self.project_config.machine.work_offsets,
|
||
"axes": self.project_config.machine.axes
|
||
}
|
||
|
||
return {
|
||
"project": {"name": program_config.get('program_id', 'Program')},
|
||
"machine": machine_config,
|
||
"main_program": program_config.get('main_program', {}),
|
||
"subroutines": program_config.get('subroutines', []),
|
||
"simulation_config": program_config.get('simulation_config', {
|
||
"acceleration": 500.0,
|
||
"max_rapid_rate": 10000.0,
|
||
"max_feed_rate": 5000.0,
|
||
"max_points": 50000,
|
||
"enable_rtcp_debug": self.debug
|
||
}),
|
||
"hal_config": {"version": "1.0", "pins": {}}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 测试函数 ====================
|
||
|
||
def test_all_kinematics():
|
||
"""测试所有运动学类型"""
|
||
print("=" * 80)
|
||
print("测试所有 LinuxCNC 运动学类型")
|
||
print("=" * 80)
|
||
|
||
kin_types = [
|
||
KinematicsType.TRT_AC, KinematicsType.TRT_BC, KinematicsType.MAXKINS_BC,
|
||
KinematicsType.FIVEAXIS_BC, KinematicsType.HEXAPOD, KinematicsType.PUMA,
|
||
KinematicsType.SCARA, KinematicsType.SCORBOT, KinematicsType.LINEAR_DELTA,
|
||
KinematicsType.ROTARY_DELTA, KinematicsType.TRIPOD, KinematicsType.PENTAPOD,
|
||
KinematicsType.ROTATE, KinematicsType.COREXY, KinematicsType.ROSE,
|
||
]
|
||
|
||
for kin_type in kin_types:
|
||
print(f"\n测试 {kin_type.name}:")
|
||
try:
|
||
kin = KinematicsFactory.create(kin_type, debug=False)
|
||
if kin:
|
||
print(f" ✓ 创建成功")
|
||
info = KinematicsFactory.get_type_info(kin_type)
|
||
print(f" 描述: {info.get('description', 'N/A')}")
|
||
print(f" 来源: {info.get('source', 'N/A')}")
|
||
else:
|
||
print(f" - IDENTITY类型返回None")
|
||
except Exception as e:
|
||
print(f" ✗ 创建失败: {e}")
|
||
|
||
|
||
def test_kinematics():
|
||
"""测试五轴运动学"""
|
||
print("=" * 60)
|
||
print("测试五轴运动学")
|
||
print("=" * 60)
|
||
|
||
kin = FiveAxisKinematics(kin_type=KinematicsType.TRT_BC, debug=True)
|
||
kin.set_tool_length(100.0)
|
||
kin.enable_rtcp(True)
|
||
|
||
joints = [0, 0, 0, 30, 0]
|
||
world = kin.forward_transform(joints)
|
||
print(f"关节坐标 {joints} -> 世界坐标 ({world[0]:.3f}, {world[1]:.3f}, {world[2]:.3f})")
|
||
|
||
world_pos = (0, 0, 100, 30, 0, 0)
|
||
joints_back = kin.inverse_transform(world_pos)
|
||
print(f"世界坐标 {world_pos} -> 关节坐标 ({joints_back[0]:.3f}, {joints_back[1]:.3f}, {joints_back[2]:.3f})")
|
||
|
||
|
||
def test_parser():
|
||
"""测试解析器"""
|
||
print("=" * 60)
|
||
print("测试G代码解析器")
|
||
print("=" * 60)
|
||
|
||
test_program = """
|
||
G90 G17 G21 G40 G49 G80
|
||
G54
|
||
T1 M6
|
||
G43.4 H1
|
||
G0 X0 Y0 Z100
|
||
G1 X50 Y0 Z50 F1000
|
||
G1 X50 Y50 Z50
|
||
G1 X0 Y50 Z50
|
||
G1 X0 Y0 Z50
|
||
G0 Z100
|
||
G49
|
||
M30
|
||
"""
|
||
|
||
parser = FullRS274Parser(debug=True)
|
||
toolpath = parser.parse_string(test_program, "test.ngc")
|
||
|
||
print(f"\n解析结果:")
|
||
print(f" 路径段数: {len(toolpath.segments)}")
|
||
print(f" 总长度: {toolpath.total_length:.3f} mm")
|
||
print(f" 总时间: {toolpath.total_time:.3f} s")
|
||
print(f" 边界: {toolpath.bounds}")
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
print("=" * 80)
|
||
print("GLCanon 五轴刀具路径解析器 - 完整集成版")
|
||
print(f"版本: {VERSION}")
|
||
print("包含: LinuxCNC 运动学算法完整集成")
|
||
print("包含: LinuxCNC 完整参数表系统 (RS274NGC)")
|
||
print("支持的构型:")
|
||
print(" - TRT_AC (xyzac-trt-kins)")
|
||
print(" - TRT_BC (xyzbc-trt-kins)")
|
||
print(" - MAXKINS_BC (maxkins)")
|
||
print(" - FIVEAXIS_BC (5axiskins)")
|
||
print(" - HEXAPOD (genhexkins)")
|
||
print(" - SERIAL_DH (genserkins)")
|
||
print(" - PUMA (pumakins)")
|
||
print(" - SCARA (scarakins)")
|
||
print(" - SCORBOT (scorbot-kins)")
|
||
print(" - LINEAR_DELTA (lineardeltakins)")
|
||
print(" - ROTARY_DELTA (rotarydeltakins)")
|
||
print(" - TRIPOD (tripodkins)")
|
||
print(" - PENTAPOD (pentakins)")
|
||
print(" - ROTATE (rotatekins)")
|
||
print(" - COREXY (corexykins)")
|
||
print(" - ROSE (rosekins)")
|
||
print("=" * 80)
|
||
|
||
test_kinematics()
|
||
print("\n")
|
||
|
||
test_all_kinematics()
|
||
print("\n")
|
||
|
||
test_parser()
|
||
print("\n")
|
||
|
||
print("=" * 80)
|
||
print("所有测试完成!")
|
||
print("=" * 80)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|