nicoaspra/G-code_Programming_Assistant
1
1import re2from decimal import Decimal, getcontext3import decimal4 5# Define interpolation and movement commands6interpolation_commands = {"G01", "G02", "G03"}7movement_commands = {"G00"}8 9# Define a pattern to recognize common G-code commands10gcode_pattern = re.compile(11 r"(G\d+|M\d+|X[-+]?\d*\.?\d+|Y[-+]?\d*\.?\d+|"12 r"Z[-+]?\d*\.?\d+|I[-+]?\d*\.?\d+|J[-+]?\d*\.?\d+|"13 r"F[-+]?\d*\.?\d+|S[-+]?\d*\.?\d+)"14)15 16def standardize_codes(line):17 """18 Standardizes M-codes and G-codes to two digits by adding a leading zero if necessary.19 """20 line = re.sub(r"\b(M|G)(\d)\b", r"\g<1>0\2", line)21 return line22 23def remove_comments(line):24 """25 Removes comments from a G-code line. Supports both ';' and '()' style comments.26 """27 # Remove anything after a ';'28 line = line.split(';')[0]29 # Remove anything inside parentheses '()'30 line = re.sub(r'\(.*?\)', '', line)31 return line.strip()32 33def preprocess_gcode(gcode):34 """35 Removes comments from the G-code and returns a list of tuples (original_line_number, cleaned_line).36 Includes all lines to maintain accurate line numbering.37 """38 cleaned_lines = []39 lines = gcode.splitlines()40 41 for idx, line in enumerate(lines):42 original_line_number = idx + 1 # Line numbers start from 143 line = standardize_codes(line.strip())44 # Remove comments45 line_no_comments = remove_comments(line)46 # Include all lines to maintain accurate line numbering47 cleaned_lines.append((original_line_number, line_no_comments))48 49 return cleaned_lines50 51def check_required_gcodes(lines_with_numbers):52 """53 Checks that the G-code contains required G-codes: G20/G21, G90/G91, G54-G59, and G17.54 Returns a list of errors with individual entries for each missing group.55 """56 required_groups = {57 "units": {"G20", "G21"}, # Metric or Imperial Units58 "mode": {"G90", "G91"}, # Absolute or Incremental Mode59 "work_coordinates": {"G54", "G55", "G56", "G57", "G58", "G59"}, # Work Offsets60 "plane": {"G17", "G18", "G19"}, # Selected Plane61 }62 63 # Create a set to track found codes and their line numbers64 found_codes = {}65 for original_line_number, line in lines_with_numbers:66 tokens = line.split()67 for token in tokens:68 found_codes.setdefault(token, original_line_number) # Record the line number where the code was found69 70 # List to hold individual errors for each missing group71 missing_group_errors = []72 73 # Check for presence of required codes74 for category, codes in required_groups.items():75 # Only flag as missing if both options in a group are absent76 found = any(code in found_codes for code in codes)77 if not found:78 missing_codes = "/".join(sorted(codes))79 # Assume missing codes should be on the first line where G-codes start80 for original_line_number, line in lines_with_numbers:81 if gcode_pattern.search(line):82 missing_group_errors.append((original_line_number, f"(Error) Missing required G-codes: ({category}) {missing_codes}"))83 break84 else:85 # Default to line 1 if no G-code commands are found86 missing_group_errors.append((1, f"(Error) Missing required G-codes: ({category}) {missing_codes}"))87 88 return missing_group_errors89 90def check_required_gcodes_position(lines_with_numbers):91 """92 Ensures required G-codes appear before movement commands.93 Flags changes in critical settings (e.g., units) after movement commands.94 """95 issues = []96 movement_seen = False97 required_groups = {98 "units": {"G20", "G21"},99 "mode": {"G90", "G91"},100 "work_coordinates": {"G54", "G55", "G56", "G57", "G58", "G59"},101 "plane": {"G17", "G18", "G19"},102 }103 critical_gcodes = {104 "units": {"G20", "G21"},105 "plane": {"G17", "G18", "G19"},106 }107 108 # Track codes found before movement commands109 codes_before_movement = set()110 111 for original_line_number, line in lines_with_numbers:112 tokens = line.split()113 114 # Check if movement commands are encountered115 if not movement_seen and any(cmd in tokens for cmd in {"G00", "G01", "G02", "G03"}):116 movement_seen = True117 118 if not movement_seen:119 # Collect required G-codes found before movement120 codes_before_movement.update(tokens)121 else:122 # After movement commands have been seen, check for critical G-codes123 for token in tokens:124 for category, codes in critical_gcodes.items():125 if token in codes:126 issues.append((original_line_number, f"(Warning) {token} appears after movement commands. Ensure this change is intentional -> {line.strip()}"))127 128 # Check for missing required G-codes before movement commands129 missing_groups = []130 for category, codes in required_groups.items():131 if not any(code in codes_before_movement for code in codes):132 missing_codes = "/".join(sorted(codes))133 missing_groups.append(f"({category}) {missing_codes}")134 135 if missing_groups:136 first_movement_line = next(137 (line_num for line_num, line in lines_with_numbers if any(cmd in line for cmd in {"G00", "G01", "G02", "G03"})),138 1139 )140 issues.append((first_movement_line, f"(Error) Missing required G-codes before first movement: {', '.join(missing_groups)}"))141 142 return issues143 144def check_end_gcode(lines_with_numbers):145 """146 Checks that M30 is the last G-code command.147 Allows blank lines or '%' symbols after M30.148 """149 found_m30 = False150 151 # Collect errors with line numbers152 errors = []153 154 for idx, (original_line_number, line) in enumerate(lines_with_numbers):155 if not line.strip() or line.strip() == "%":156 continue # Skip empty lines or lines with only '%'157 158 if "M30" in line:159 if found_m30:160 errors.append((original_line_number, "(Error) M30 must be the last G-code command in the G-code."))161 found_m30 = True162 continue # Continue to check if any G-code commands appear after M30163 164 # After M30, no other G-code commands should appear165 if found_m30 and gcode_pattern.search(line):166 errors.append((original_line_number, f"(Error) No G-code commands should appear after M30. Found '{line.strip()}'."))167 168 if not found_m30:169 if lines_with_numbers:170 last_line_number = lines_with_numbers[-1][0]171 else:172 last_line_number = 1173 errors.append((last_line_number, "(Error) M30 is missing from the G-code."))174 175 return errors176 177def check_spindle(lines_with_numbers):178 """179 Checks spindle-related issues in the G-code.180 """181 issues = []182 spindle_on = False183 spindle_started = False184 185 for idx, (original_line_number, line) in enumerate(lines_with_numbers):186 # Skip processing lines that are empty or contain only '%'187 if not line.strip() or line.strip() == "%":188 continue189 190 tokens = line.split()191 192 # Check for valid G-code commands193 if not gcode_pattern.search(line):194 issues.append((original_line_number, f"(Error) Invalid G-code command or syntax error -> {line.strip()}"))195 196 # Check for spindle on197 if "M03" in tokens or "M04" in tokens:198 # Check if spindle is already on199 if spindle_on:200 issues.append((original_line_number, "(Warning) Spindle is already on."))201 202 # Check if spindle speed is specified with 'S' command203 s_value_present = any(token.startswith("S") for token in tokens)204 if not s_value_present:205 issues.append((original_line_number, "(Error) Spindle speed (S value) is missing when turning on the spindle with M03/M04."))206 207 spindle_on = True208 spindle_started = True209 210 # Check for spindle off211 if "M05" in tokens:212 spindle_on = False213 214 # Check if movement commands are given without spindle on215 if any(cmd in tokens for cmd in interpolation_commands):216 if not spindle_on:217 issues.append((original_line_number, f"(Error) Move command without spindle on -> {line.strip()}"))218 219 # Check if spindle was turned off before M30220 if spindle_on:221 last_line_number = lines_with_numbers[-1][0]222 issues.append((last_line_number, "(Error) Spindle was not turned off (M05) before the end of the program."))223 224 # Check if spindle was never turned on225 if not spindle_started:226 issues.append((0, "(Error) Spindle was never turned on in the G-code."))227 228 return issues229 230def check_feed_rate(lines_with_numbers):231 """232 Checks feed rate related issues in the G-code.233 """234 issues = []235 last_feed_rate = None236 interpolation_command_seen = False237 238 for idx, (original_line_number, line) in enumerate(lines_with_numbers):239 # Skip processing lines that are empty or contain only '%'240 if not line.strip() or line.strip() == "%":241 continue242 243 tokens = line.split()244 commands = set(tokens)245 feed_rates = [token for token in tokens if token.startswith("F")]246 247 # Check if feed rate is beside non-interpolation commands248 if feed_rates and not any(cmd in interpolation_commands for cmd in commands):249 issues.append((original_line_number, f"(Warning) Feed rate specified without interpolation command -> {line.strip()}"))250 251 # Check for interpolation commands252 if any(cmd in commands for cmd in interpolation_commands):253 if not interpolation_command_seen:254 interpolation_command_seen = True255 if not feed_rates and last_feed_rate is None:256 issues.append((original_line_number, f"(Error) First interpolation command must have a feed rate -> {line.strip()}"))257 else:258 # Set initial feed rate259 if feed_rates:260 last_feed_rate = feed_rates[-1]261 else:262 # Check if feed rate is specified263 if feed_rates:264 current_feed_rate = feed_rates[-1]265 if current_feed_rate == last_feed_rate:266 issues.append((original_line_number, f"(Warning) Feed rate {current_feed_rate} is already set; no need to specify again."))267 else:268 last_feed_rate = current_feed_rate269 270 return issues271 272def check_depth_of_cut(lines_with_numbers, depth_max=0.1):273 """274 Checks that all cutting moves on the Z-axis have a uniform depth and do not exceed the maximum depth.275 """276 getcontext().prec = 6 # Set precision as needed277 depth_max = Decimal(str(depth_max))278 issues = []279 280 positioning_mode = "G90" # Default to absolute positioning281 current_z = Decimal('0.0')282 depths = set()283 z_negative_seen = False284 285 for idx, (original_line_number, line) in enumerate(lines_with_numbers):286 # Skip processing lines that are empty or contain only '%'287 if not line.strip() or line.strip() == "%":288 continue289 290 tokens = line.split()291 292 if "G90" in tokens:293 positioning_mode = "G90"294 elif "G91" in tokens:295 positioning_mode = "G91"296 297 if any(cmd in tokens for cmd in interpolation_commands.union(movement_commands)):298 z_values = [token for token in tokens if token.startswith("Z")]299 if z_values:300 try:301 z_value = Decimal(z_values[-1][1:])302 except (ValueError, decimal.InvalidOperation):303 issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}"))304 continue305 306 if positioning_mode == "G90":307 new_z = z_value308 elif positioning_mode == "G91":309 new_z = current_z + z_value310 311 if new_z < Decimal('0.0'):312 z_negative_seen = True313 depth = abs(new_z)314 depth = depth.quantize(Decimal('0.0001')).normalize() # Round and remove trailing zeros315 depths.add(depth)316 317 if depth > depth_max:318 issues.append((original_line_number, f"(Error) Depth of cut {depth} exceeds maximum allowed depth of {depth_max.normalize()} -> {line.strip()}"))319 320 current_z = new_z321 322 if z_negative_seen:323 if len(depths) > 1:324 depth_values = ', '.join(str(d.normalize()) for d in sorted(depths))325 issues.append((0, f"(Warning) Inconsistent depths of cut detected: {depth_values}"))326 else:327 issues.append((0, "(Error) No cutting moves detected on the Z-axis."))328 329 return issues330 331def check_interpolation_depth(lines_with_numbers):332 """333 Checks that all interpolation commands moving in X or Y are executed at a negative Z depth (i.e., cutting).334 Does not report errors for interpolation commands used for plunging or retracting (Z-axis movements only).335 """336 getcontext().prec = 6 # Set precision as needed337 issues = []338 339 positioning_mode = "G90" # Default to absolute positioning340 current_z = Decimal('0.0')341 342 for idx, (original_line_number, line) in enumerate(lines_with_numbers):343 # Skip processing lines that are empty or contain only '%'344 if not line.strip() or line.strip() == "%":345 continue346 347 tokens = line.split()348 349 # Update positioning mode if G90 or G91 is found350 if "G90" in tokens:351 positioning_mode = "G90"352 elif "G91" in tokens:353 positioning_mode = "G91"354 355 # Check for Z-axis movement356 z_values = [token for token in tokens if token.startswith("Z")]357 if z_values:358 try:359 z_value = Decimal(z_values[-1][1:])360 except (ValueError, decimal.InvalidOperation):361 issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}"))362 continue363 364 # Calculate the new Z position based on positioning mode365 if positioning_mode == "G90":366 current_z = z_value367 elif positioning_mode == "G91":368 current_z += z_value369 370 # Check for interpolation commands371 if any(cmd in tokens for cmd in interpolation_commands):372 # Check if the command includes X or Y movement373 has_xy_movement = any(token.startswith(('X', 'Y')) for token in tokens)374 if has_xy_movement and current_z >= Decimal('0.0'):375 issues.append((original_line_number, f"(Warning) Interpolation command with XY movement executed without cutting depth (Z={current_z}) -> {line.strip()}"))376 377 return issues378 379def check_plunge_retract_moves(lines_with_numbers):380 """381 Checks that plunging and retracting moves along the Z-axis use G01 instead of G00.382 Reports an error if G00 is used for Z-axis movements to Z positions less than or equal to zero.383 """384 issues = []385 positioning_mode = "G90" # Default to absolute positioning386 current_z = None # Keep track of the current Z position387 388 for idx, (original_line_number, line) in enumerate(lines_with_numbers):389 # Skip processing lines that are empty or contain only '%'390 if not line.strip() or line.strip() == "%":391 continue392 393 tokens = line.split()394 395 # Update positioning mode if G90 or G91 is found396 if "G90" in tokens:397 positioning_mode = "G90"398 elif "G91" in tokens:399 positioning_mode = "G91"400 401 # Check for Z-axis movement402 z_values = [token for token in tokens if token.startswith("Z")]403 if z_values:404 try:405 z_value = Decimal(z_values[-1][1:])406 except (ValueError, decimal.InvalidOperation):407 issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}"))408 continue409 410 # Calculate the new Z position based on positioning mode411 if current_z is None:412 current_z = z_value413 else:414 if positioning_mode == "G90":415 current_z = z_value416 elif positioning_mode == "G91":417 current_z += z_value418 419 # Check for G00 commands moving to Z ≤ 0420 # Check for G00 commands moving to Z ≤ 0421 if "G00" in tokens and current_z <= Decimal('0.0'):422 issues.append((original_line_number, f"(Error) G00 used for plunging to Z={current_z}. Use G01 to safely approach the workpiece -> {line.strip()}"))423 424 return issues425 426def run_checks(gcode, depth_max=0.1):427 """428 Runs all checks and returns a tuple containing lists of errors and warnings.429 """430 errors = []431 warnings = []432 433 # Preprocess G-code to remove comments and get cleaned lines with original line numbers434 lines_with_numbers = preprocess_gcode(gcode)435 436 # Collect issues from all checks437 required_gcode_issues = check_required_gcodes(lines_with_numbers)438 required_gcode_position_issues = check_required_gcodes_position(lines_with_numbers)439 spindle_issues = check_spindle(lines_with_numbers)440 feed_rate_issues = check_feed_rate(lines_with_numbers)441 depth_issues = check_depth_of_cut(lines_with_numbers, depth_max)442 end_gcode_issues = check_end_gcode(lines_with_numbers)443 interpolation_depth_issues = check_interpolation_depth(lines_with_numbers)444 plunge_retract_issues = check_plunge_retract_moves(lines_with_numbers)445 446 # Combine all issues447 all_issues = (448 required_gcode_issues449 + required_gcode_position_issues450 + spindle_issues451 + feed_rate_issues452 + depth_issues453 + end_gcode_issues454 + interpolation_depth_issues455 + plunge_retract_issues456 )457 458 # Separate issues into errors and warnings459 for line_num, message in all_issues:460 if "(Error)" in message:461 errors.append((line_num, message))462 elif "(Warning)" in message:463 warnings.append((line_num, message))464 465 # Sort issues by line number466 errors.sort(key=lambda x: x[0])467 warnings.sort(key=lambda x: x[0])468 469 return errors, warnings470 471if __name__ == "__main__":472 # Example usage473 gcode_sample = """474 %475 G21 G90 G17 G54476 G00 X0 Y0 Z5.0477 M03 S1000478 G01 Z-0.1 F100 ; Plunge using rapid movement (should be G01)479 G54480 G01 Z-0.1481 G01 X10 Y10 482 G01 X20 Y20483 G00 Z5.0 ; Retract using rapid movement (allowed since Z > 0)484 M05485 M30486 %487 """488 489 depth_max = 0.1 # Set the maximum allowed depth of cut490 errors, warnings = run_checks(gcode_sample, depth_max)491 492 # Prepare the output as a string493 output_lines = []494 if errors or warnings:495 output_lines.append("Issues found in G-code:")496 for line_num, message in errors + warnings:497 if line_num > 0:498 output_lines.append(f"Line {line_num}: {message}")499 else:500 output_lines.append(message)501 print('\n'.join(output_lines))502 else:503 print("Your G-code looks good!")