#!/usr/bin/env python3 """ G-code边界超限分析工具 Analyzes G-code files to find moves that exceed build volume boundaries. 用法 / Usage: python analyze_gcode_bounds.py [options] 示例 / Examples: python analyze_gcode_bounds.py output.gcode --bed-size 200 200 250 python analyze_gcode_bounds.py output.gcode --bed-type circle --radius 100 """ import re import sys import argparse from enum import Enum from dataclasses import dataclass from typing import List, Tuple, Optional import math class BedType(Enum): RECTANGLE = "rectangle" CIRCLE = "circle" class MoveType(Enum): TRAVEL = "Travel" EXTRUDE = "Extrude" ARC_CW = "Arc CW (G2)" # 顺时针弧线 ARC_CCW = "Arc CCW (G3)" # 逆时针弧线 RETRACT = "Retract" UNKNOWN = "Unknown" class ViolationType(Enum): X_MIN = "X < Min" X_MAX = "X > Max" Y_MIN = "Y < Min" Y_MAX = "Y > Max" Z_MAX = "Z > Max" RADIUS = "Radius > Max (Circle bed)" @dataclass class Position: x: float = 0.0 y: float = 0.0 z: float = 0.0 e: float = 0.0 def copy(self): return Position(self.x, self.y, self.z, self.e) @dataclass class Violation: line_num: int line_content: str position: Position move_type: MoveType violation_types: List[ViolationType] distance_out: float # 超出距离 (mm) def __str__(self): vio_str = ", ".join([v.value for v in self.violation_types]) return (f"Line {self.line_num}: {self.move_type.value} - {vio_str}\n" f" Position: X={self.position.x:.3f} Y={self.position.y:.3f} " f"Z={self.position.z:.3f} E={self.position.e:.3f}\n" f" Out by: {self.distance_out:.3f} mm\n" f" G-code: {self.line_content.strip()}") class GCodeAnalyzer: def __init__(self, bed_type: BedType, bed_min: Tuple[float, float], bed_max: Tuple[float, float], max_z: float, radius: float = None): self.bed_type = bed_type self.bed_min = bed_min self.bed_max = bed_max self.max_z = max_z self.radius = radius # For circle bed self.center = ((bed_max[0] + bed_min[0]) / 2, (bed_max[1] + bed_min[1]) / 2) if bed_type == BedType.CIRCLE else None self.current_pos = Position() self.violations: List[Violation] = [] # 统计 self.total_moves = 0 self.travel_moves = 0 self.extrude_moves = 0 def parse_gcode_file(self, filename: str): """解析G-code文件""" print(f"正在分析文件: {filename}") print(f"床类型: {self.bed_type.value}") if self.bed_type == BedType.RECTANGLE: print(f"床边界: X[{self.bed_min[0]:.1f}, {self.bed_max[0]:.1f}] " f"Y[{self.bed_min[1]:.1f}, {self.bed_max[1]:.1f}] " f"Z[0, {self.max_z:.1f}]") else: print(f"床中心: ({self.center[0]:.1f}, {self.center[1]:.1f})") print(f"床半径: {self.radius:.1f} mm, Z[0, {self.max_z:.1f}]") print("=" * 70) try: with open(filename, 'r', encoding='utf-8') as f: for line_num, line in enumerate(f, 1): self._parse_line(line_num, line) except FileNotFoundError: print(f"错误: 文件未找到 '{filename}'") sys.exit(1) except Exception as e: print(f"错误: 读取文件时出错: {e}") sys.exit(1) def _parse_line(self, line_num: int, line: str): """解析单行G-code""" # 移除注释 if ';' in line: code_part = line[:line.index(';')] comment = line[line.index(';'):] else: code_part = line comment = "" code_part = code_part.strip().upper() if not code_part: return # Check for G0/G1/G2/G3 commands (using word boundary to avoid matching G28, G29, etc.) g_match = re.match(r'G([0-3])\b', code_part) if not g_match: return g_code = int(g_match.group(1)) # Parse coordinates x_match = re.search(r'X([-+]?\d*\.?\d+)', code_part) y_match = re.search(r'Y([-+]?\d*\.?\d+)', code_part) z_match = re.search(r'Z([-+]?\d*\.?\d+)', code_part) e_match = re.search(r'E([-+]?\d*\.?\d+)', code_part) i_match = re.search(r'I([-+]?\d*\.?\d+)', code_part) j_match = re.search(r'J([-+]?\d*\.?\d+)', code_part) # G2/G3 arc commands if g_code in [2, 3] and (i_match or j_match): self._parse_arc(line_num, line, code_part, g_code, x_match, y_match, z_match, e_match, i_match, j_match) return # G0/G1 linear moves new_pos = self.current_pos.copy() has_move = False has_xy_move = False if x_match: new_pos.x = float(x_match.group(1)) has_move = True has_xy_move = True if y_match: new_pos.y = float(y_match.group(1)) has_move = True has_xy_move = True if z_match: new_pos.z = float(z_match.group(1)) has_move = True if e_match: new_pos.e = float(e_match.group(1)) if not has_move: return # 判断移动类型 move_type = self._classify_move(code_part, self.current_pos, new_pos) if move_type == MoveType.TRAVEL: self.travel_moves += 1 elif move_type == MoveType.EXTRUDE: self.extrude_moves += 1 self.total_moves += 1 # 检查边界 - Only check XY bounds if X or Y actually moved if has_xy_move: violations = self._check_bounds(new_pos) if violations: distance = self._calculate_distance_out(new_pos) self.violations.append(Violation( line_num=line_num, line_content=line, position=new_pos.copy(), move_type=move_type, violation_types=violations, distance_out=distance )) self.current_pos = new_pos def _parse_arc(self, line_num: int, line: str, code_part: str, g_code: int, x_match, y_match, z_match, e_match, i_match, j_match): """Parse G2/G3 arc commands and check arc path for boundary violations""" start_x = self.current_pos.x start_y = self.current_pos.y start_z = self.current_pos.z i = float(i_match.group(1)) if i_match else 0.0 j = float(j_match.group(1)) if j_match else 0.0 center_x = start_x + i center_y = start_y + j radius = math.sqrt(i * i + j * j) end_x = float(x_match.group(1)) if x_match else None end_y = float(y_match.group(1)) if y_match else None end_z = float(z_match.group(1)) if z_match else start_z e = float(e_match.group(1)) if e_match else self.current_pos.e if end_x is None and end_y is None: end_angle = math.atan2(start_y - center_y, start_x - center_x) + (2 * math.pi if g_code == 3 else -2 * math.pi) end_x = center_x + radius * math.cos(end_angle) end_y = center_y + radius * math.sin(end_angle) elif end_x is None: end_x = start_x elif end_y is None: end_y = start_y start_angle = math.atan2(start_y - center_y, start_x - center_x) end_angle = math.atan2(end_y - center_y, end_x - center_x) if g_code == 2: if end_angle > start_angle: end_angle -= 2 * math.pi angle_sweep = start_angle - end_angle else: if end_angle < start_angle: end_angle += 2 * math.pi angle_sweep = end_angle - start_angle num_samples = max(8, int(abs(angle_sweep) * radius / 5)) move_type = MoveType.ARC_CCW if g_code == 3 else MoveType.ARC_CW if move_type == MoveType.ARC_CW: self.travel_moves += 1 else: self.extrude_moves += 1 self.total_moves += 1 for n in range(num_samples + 1): t = n / num_samples angle = start_angle + (angle_sweep * t if g_code == 3 else -angle_sweep * t) sample_x = center_x + radius * math.cos(angle) sample_y = center_y + radius * math.sin(angle) sample_z = start_z + (end_z - start_z) * t sample_pos = Position(sample_x, sample_y, sample_z, e) violations = self._check_bounds(sample_pos) if violations: distance = self._calculate_distance_out(sample_pos) self.violations.append(Violation( line_num=line_num, line_content=line, position=sample_pos, move_type=move_type, violation_types=violations, distance_out=distance )) break self.current_pos.x = end_x self.current_pos.y = end_y self.current_pos.z = end_z self.current_pos.e = e def _classify_move(self, code: str, old_pos: Position, new_pos: Position) -> MoveType: """分类移动类型""" # G0 通常是快速移动(Travel) if code.startswith('G0'): return MoveType.TRAVEL # G1 可能是Travel或Extrude,看E值 if code.startswith('G1'): if abs(new_pos.e - old_pos.e) > 0.001: # 有挤出 return MoveType.EXTRUDE else: return MoveType.TRAVEL # G2/G3 是弧线,通常是挤出 if code.startswith('G2') or code.startswith('G3'): return MoveType.EXTRUDE return MoveType.UNKNOWN def _check_bounds(self, pos: Position) -> List[ViolationType]: """检查坐标是否超出边界""" violations = [] epsilon = 0.01 # 允许的误差 if self.bed_type == BedType.RECTANGLE: if pos.x < self.bed_min[0] - epsilon: violations.append(ViolationType.X_MIN) if pos.x > self.bed_max[0] + epsilon: violations.append(ViolationType.X_MAX) if pos.y < self.bed_min[1] - epsilon: violations.append(ViolationType.Y_MIN) if pos.y > self.bed_max[1] + epsilon: violations.append(ViolationType.Y_MAX) elif self.bed_type == BedType.CIRCLE: dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2) if dist > self.radius + epsilon: violations.append(ViolationType.RADIUS) # Z轴检查 if self.max_z > 0 and pos.z > self.max_z + epsilon: violations.append(ViolationType.Z_MAX) return violations def _calculate_distance_out(self, pos: Position) -> float: """计算超出边界的距离""" if self.bed_type == BedType.RECTANGLE: dx = max(0, self.bed_min[0] - pos.x, pos.x - self.bed_max[0]) dy = max(0, self.bed_min[1] - pos.y, pos.y - self.bed_max[1]) dz = max(0, pos.z - self.max_z) if self.max_z > 0 else 0 return math.sqrt(dx**2 + dy**2 + dz**2) elif self.bed_type == BedType.CIRCLE: dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2) return max(0, dist - self.radius) return 0.0 def print_report(self): """打印分析报告""" print("\n" + "=" * 70) print("分析报告 / Analysis Report") print("=" * 70) print(f"\n总移动数: {self.total_moves}") print(f" - Travel移动: {self.travel_moves}") print(f" - Extrude移动: {self.extrude_moves}") print(f" - 其他: {self.total_moves - self.travel_moves - self.extrude_moves}") print(f"\n发现超限: {len(self.violations)} 处") if not self.violations: print("\n✅ 所有移动都在边界内!") return # 按类型分组 travel_violations = [v for v in self.violations if v.move_type == MoveType.TRAVEL] extrude_violations = [v for v in self.violations if v.move_type == MoveType.EXTRUDE] print(f" - Travel超限: {len(travel_violations)}") print(f" - Extrude超限: {len(extrude_violations)}") # 按超限类型统计 print("\n超限类型统计:") from collections import Counter all_vio_types = [] for v in self.violations: all_vio_types.extend(v.violation_types) vio_counter = Counter(all_vio_types) for vio_type, count in vio_counter.most_common(): print(f" {vio_type.value}: {count} 次") # 详细列出超限 print("\n" + "=" * 70) print("详细超限列表 (前50个):") print("=" * 70) for i, violation in enumerate(self.violations[:50], 1): print(f"\n[{i}] {violation}") if len(self.violations) > 50: print(f"\n... 还有 {len(self.violations) - 50} 个超限未显示") # 保存到文件 output_file = "gcode_violations.txt" with open(output_file, 'w', encoding='utf-8') as f: f.write("G-code边界超限详细报告\n") f.write("=" * 70 + "\n\n") for i, violation in enumerate(self.violations, 1): f.write(f"[{i}] {violation}\n\n") print(f"\n完整报告已保存到: {output_file}") def main(): parser = argparse.ArgumentParser( description='分析G-code文件的边界超限问题', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: # 矩形床 200x200x250mm python %(prog)s output.gcode --bed-size 200 200 250 # 矩形床,指定原点偏移 python %(prog)s output.gcode --bed-size 200 200 250 --bed-origin 0 0 # 圆形床(如Delta打印机) python %(prog)s output.gcode --bed-type circle --radius 100 --max-z 250 """ ) parser.add_argument('gcode_file', help='G-code文件路径') parser.add_argument('--bed-type', choices=['rectangle', 'circle'], default='rectangle', help='床类型 (默认: rectangle)') parser.add_argument('--bed-size', type=float, nargs=3, metavar=('X', 'Y', 'Z'), help='床尺寸 X Y Z (mm), 例如: 200 200 250') parser.add_argument('--bed-origin', type=float, nargs=2, metavar=('X', 'Y'), default=(0, 0), help='床原点坐标 (默认: 0 0)') parser.add_argument('--radius', type=float, help='圆形床半径 (mm)') parser.add_argument('--max-z', type=float, help='最大Z高度 (mm)') args = parser.parse_args() # 解析床参数 bed_type = BedType(args.bed_type) if bed_type == BedType.RECTANGLE: if not args.bed_size: print("错误: 矩形床需要指定 --bed-size") sys.exit(1) bed_min = (args.bed_origin[0], args.bed_origin[1]) bed_max = (args.bed_origin[0] + args.bed_size[0], args.bed_origin[1] + args.bed_size[1]) max_z = args.bed_size[2] radius = None elif bed_type == BedType.CIRCLE: if not args.radius or not args.max_z: print("错误: 圆形床需要指定 --radius 和 --max-z") sys.exit(1) bed_min = (-args.radius, -args.radius) bed_max = (args.radius, args.radius) max_z = args.max_z radius = args.radius # 分析G-code analyzer = GCodeAnalyzer(bed_type, bed_min, bed_max, max_z, radius) analyzer.parse_gcode_file(args.gcode_file) analyzer.print_report() if __name__ == '__main__': main()