#!/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角度值 将 # 替换为子程序参数值 将 # 替换为子程序参数值 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) # ===== 展开子程序参数 #, #, #, #, #, #, #, # 等 ===== 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 # 命名变量赋值: # = #1 或 # = 100 或 # = [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> + #], 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()