Download python_gcode_checker.py from nicoaspra/G-code_Programming_Assistant: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/spaces/nicoaspra/G-code_Programming_Assistant/resolve/cf1225231bb2ac1d8db36e8fdeb282ec2f3b9884/python_gcode_checker.py
- Command line
-
hf download hf://spaces/nicoaspra/G-code_Programming_Assistant@cf1225231bb2ac1d8db36e8fdeb282ec2f3b9884/python_gcode_checker.py
-
curl -L -o python_gcode_checker.py https://huggingface.co/spaces/nicoaspra/G-code_Programming_Assistant/resolve/cf1225231bb2ac1d8db36e8fdeb282ec2f3b9884/python_gcode_checker.py
16.9 kB
| import re | |
| from decimal import Decimal, getcontext | |
| import decimal | |
| # Define interpolation and movement commands | |
| interpolation_commands = {"G01", "G02", "G03"} | |
| movement_commands = {"G00"} | |
| # Define a pattern to recognize common G-code commands | |
| gcode_pattern = re.compile( | |
| r"(G\d+|M\d+|X[-+]?\d*\.?\d+|Y[-+]?\d*\.?\d+|" | |
| r"Z[-+]?\d*\.?\d+|I[-+]?\d*\.?\d+|J[-+]?\d*\.?\d+|" | |
| r"F[-+]?\d*\.?\d+|S[-+]?\d*\.?\d+)" | |
| ) | |
| def standardize_codes(line): | |
| """ | |
| Standardizes M-codes and G-codes to two digits by adding a leading zero if necessary. | |
| """ | |
| line = re.sub(r"\b(M|G)(\d)\b", r"\g<1>0\2", line) | |
| return line | |
| def remove_comments(line): | |
| """ | |
| Removes comments from a G-code line. Supports both ';' and '()' style comments. | |
| """ | |
| # Remove anything after a ';' | |
| line = line.split(';')[0] | |
| # Remove anything inside parentheses '()' | |
| line = re.sub(r'\(.*?\)', '', line) | |
| return line.strip() | |
| def preprocess_gcode(gcode): | |
| """ | |
| Removes comments from the G-code and returns a list of tuples (original_line_number, cleaned_line). | |
| Includes all lines to maintain accurate line numbering. | |
| """ | |
| cleaned_lines = [] | |
| lines = gcode.splitlines() | |
| for idx, line in enumerate(lines): | |
| original_line_number = idx + 1 # Line numbers start from 1 | |
| line = standardize_codes(line.strip()) | |
| # Remove comments | |
| line_no_comments = remove_comments(line) | |
| # Include all lines to maintain accurate line numbering | |
| cleaned_lines.append((original_line_number, line_no_comments)) | |
| return cleaned_lines | |
| def check_required_gcodes(lines_with_numbers): | |
| """ | |
| Checks that the G-code contains required G-codes: G20/G21, G90/G91, G54-G59, and G17. | |
| Returns a list of errors with individual entries for each missing group. | |
| """ | |
| required_groups = { | |
| "units": {"G20", "G21"}, | |
| "mode": {"G90", "G91"}, | |
| "work_coordinates": {"G54", "G55", "G56", "G57", "G58", "G59"}, | |
| "plane": {"G17", "G18", "G19"}, | |
| } | |
| # Create a set to track found codes and their line numbers | |
| found_codes = {} | |
| for original_line_number, line in lines_with_numbers: | |
| tokens = line.split() | |
| for token in tokens: | |
| found_codes.setdefault(token, original_line_number) # Record the line number where the code was found | |
| # List to hold individual errors for each missing group | |
| missing_group_errors = [] | |
| # Check for presence of required codes | |
| for category, codes in required_groups.items(): | |
| found = any(code in found_codes for code in codes) | |
| if not found: | |
| missing_codes = "/".join(sorted(codes)) | |
| # Assume missing codes should be on the first line where G-codes start | |
| for original_line_number, line in lines_with_numbers: | |
| if gcode_pattern.search(line): | |
| missing_group_errors.append((original_line_number, f"(Error) Missing required G-codes: ({category}) {missing_codes}")) | |
| break | |
| else: | |
| # Default to line 1 if no G-code commands are found | |
| missing_group_errors.append((1, f"(Error) Missing required G-codes: ({category}) {missing_codes}")) | |
| return missing_group_errors | |
| def check_end_gcode(lines_with_numbers): | |
| """ | |
| Checks that M30 is the last G-code command. | |
| Allows blank lines or '%' symbols after M30. | |
| """ | |
| found_m30 = False | |
| # Collect errors with line numbers | |
| errors = [] | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| if not line.strip() or line.strip() == "%": | |
| continue # Skip empty lines or lines with only '%' | |
| if "M30" in line: | |
| if found_m30: | |
| errors.append((original_line_number, "(Error) M30 must be the last G-code command in the G-code.")) | |
| found_m30 = True | |
| continue # Continue to check if any G-code commands appear after M30 | |
| # After M30, no other G-code commands should appear | |
| if found_m30 and gcode_pattern.search(line): | |
| errors.append((original_line_number, f"(Error) No G-code commands should appear after M30. Found '{line.strip()}'.")) | |
| if not found_m30: | |
| if lines_with_numbers: | |
| last_line_number = lines_with_numbers[-1][0] | |
| else: | |
| last_line_number = 1 | |
| errors.append((last_line_number, "(Error) M30 is missing from the G-code.")) | |
| return errors | |
| def check_spindle(lines_with_numbers): | |
| """ | |
| Checks spindle-related issues in the G-code. | |
| """ | |
| issues = [] | |
| spindle_on = False | |
| spindle_started = False | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| # Skip processing lines that are empty or contain only '%' | |
| if not line.strip() or line.strip() == "%": | |
| continue | |
| tokens = line.split() | |
| # Check for valid G-code commands | |
| if not gcode_pattern.search(line): | |
| issues.append((original_line_number, f"(Error) Invalid G-code command or syntax error -> {line.strip()}")) | |
| # Check for spindle on | |
| if "M03" in tokens or "M04" in tokens: | |
| # Check if spindle is already on | |
| if spindle_on: | |
| issues.append((original_line_number, "(Warning) Spindle is already on.")) | |
| # Check if spindle speed is specified with 'S' command | |
| s_value_present = any(token.startswith("S") for token in tokens) | |
| if not s_value_present: | |
| issues.append((original_line_number, "(Error) Spindle speed (S value) is missing when turning on the spindle with M03/M04.")) | |
| spindle_on = True | |
| spindle_started = True | |
| # Check for spindle off | |
| if "M05" in tokens: | |
| spindle_on = False | |
| # Check if movement commands are given without spindle on | |
| if any(cmd in tokens for cmd in interpolation_commands): | |
| if not spindle_on: | |
| issues.append((original_line_number, f"(Error) Move command without spindle on -> {line.strip()}")) | |
| # Check if spindle was turned off before M30 | |
| if spindle_on: | |
| last_line_number = lines_with_numbers[-1][0] | |
| issues.append((last_line_number, "(Error) Spindle was not turned off (M05) before the end of the program.")) | |
| # Check if spindle was never turned on | |
| if not spindle_started: | |
| issues.append((0, "(Error) Spindle was never turned on in the G-code.")) | |
| return issues | |
| def check_feed_rate(lines_with_numbers): | |
| """ | |
| Checks feed rate related issues in the G-code. | |
| """ | |
| issues = [] | |
| last_feed_rate = None | |
| interpolation_command_seen = False | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| # Skip processing lines that are empty or contain only '%' | |
| if not line.strip() or line.strip() == "%": | |
| continue | |
| tokens = line.split() | |
| commands = set(tokens) | |
| feed_rates = [token for token in tokens if token.startswith("F")] | |
| # Check if feed rate is beside non-interpolation commands | |
| if feed_rates and not any(cmd in interpolation_commands for cmd in commands): | |
| issues.append((original_line_number, f"(Warning) Feed rate specified without interpolation command -> {line.strip()}")) | |
| # Check for interpolation commands | |
| if any(cmd in commands for cmd in interpolation_commands): | |
| if not interpolation_command_seen: | |
| interpolation_command_seen = True | |
| if not feed_rates and last_feed_rate is None: | |
| issues.append((original_line_number, f"(Error) First interpolation command must have a feed rate -> {line.strip()}")) | |
| else: | |
| # Set initial feed rate | |
| if feed_rates: | |
| last_feed_rate = feed_rates[-1] | |
| else: | |
| # Check if feed rate is specified | |
| if feed_rates: | |
| current_feed_rate = feed_rates[-1] | |
| if current_feed_rate == last_feed_rate: | |
| issues.append((original_line_number, f"(Warning) Feed rate {current_feed_rate} is already set; no need to specify again.")) | |
| else: | |
| last_feed_rate = current_feed_rate | |
| return issues | |
| def check_depth_of_cut(lines_with_numbers, depth_max=0.1): | |
| """ | |
| Checks that all cutting moves on the Z-axis have a uniform depth and do not exceed the maximum depth. | |
| """ | |
| getcontext().prec = 6 # Set precision as needed | |
| depth_max = Decimal(str(depth_max)) | |
| issues = [] | |
| positioning_mode = "G90" # Default to absolute positioning | |
| current_z = Decimal('0.0') | |
| depths = set() | |
| z_negative_seen = False | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| # Skip processing lines that are empty or contain only '%' | |
| if not line.strip() or line.strip() == "%": | |
| continue | |
| tokens = line.split() | |
| if "G90" in tokens: | |
| positioning_mode = "G90" | |
| elif "G91" in tokens: | |
| positioning_mode = "G91" | |
| if any(cmd in tokens for cmd in interpolation_commands.union(movement_commands)): | |
| z_values = [token for token in tokens if token.startswith("Z")] | |
| if z_values: | |
| try: | |
| z_value = Decimal(z_values[-1][1:]) | |
| except (ValueError, decimal.InvalidOperation): | |
| issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}")) | |
| continue | |
| if positioning_mode == "G90": | |
| new_z = z_value | |
| elif positioning_mode == "G91": | |
| new_z = current_z + z_value | |
| if new_z < Decimal('0.0'): | |
| z_negative_seen = True | |
| depth = abs(new_z) | |
| depth = depth.quantize(Decimal('0.0001')).normalize() # Round and remove trailing zeros | |
| depths.add(depth) | |
| if depth > depth_max: | |
| issues.append((original_line_number, f"(Error) Depth of cut {depth} exceeds maximum allowed depth of {depth_max.normalize()} -> {line.strip()}")) | |
| current_z = new_z | |
| if z_negative_seen: | |
| if len(depths) > 1: | |
| depth_values = ', '.join(str(d.normalize()) for d in sorted(depths)) | |
| issues.append((0, f"(Warning) Inconsistent depths of cut detected: {depth_values}")) | |
| else: | |
| issues.append((0, "(Error) No cutting moves detected on the Z-axis.")) | |
| return issues | |
| def check_interpolation_depth(lines_with_numbers): | |
| """ | |
| Checks that all interpolation commands moving in X or Y are executed at a negative Z depth (i.e., cutting). | |
| Does not report errors for interpolation commands used for plunging or retracting (Z-axis movements only). | |
| """ | |
| getcontext().prec = 6 # Set precision as needed | |
| issues = [] | |
| positioning_mode = "G90" # Default to absolute positioning | |
| current_z = Decimal('0.0') | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| # Skip processing lines that are empty or contain only '%' | |
| if not line.strip() or line.strip() == "%": | |
| continue | |
| tokens = line.split() | |
| # Update positioning mode if G90 or G91 is found | |
| if "G90" in tokens: | |
| positioning_mode = "G90" | |
| elif "G91" in tokens: | |
| positioning_mode = "G91" | |
| # Check for Z-axis movement | |
| z_values = [token for token in tokens if token.startswith("Z")] | |
| if z_values: | |
| try: | |
| z_value = Decimal(z_values[-1][1:]) | |
| except (ValueError, decimal.InvalidOperation): | |
| issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}")) | |
| continue | |
| # Calculate the new Z position based on positioning mode | |
| if positioning_mode == "G90": | |
| current_z = z_value | |
| elif positioning_mode == "G91": | |
| current_z += z_value | |
| # Check for interpolation commands | |
| if any(cmd in tokens for cmd in interpolation_commands): | |
| # Check if the command includes X or Y movement | |
| has_xy_movement = any(token.startswith(('X', 'Y')) for token in tokens) | |
| if has_xy_movement and current_z >= Decimal('0.0'): | |
| issues.append((original_line_number, f"(Warning) Interpolation command with XY movement executed without cutting depth (Z={current_z}) -> {line.strip()}")) | |
| return issues | |
| def check_plunge_retract_moves(lines_with_numbers): | |
| """ | |
| Checks that plunging and retracting moves along the Z-axis use G01 instead of G00. | |
| Reports an error if G00 is used for Z-axis movements to Z positions less than or equal to zero. | |
| """ | |
| issues = [] | |
| positioning_mode = "G90" # Default to absolute positioning | |
| current_z = None # Keep track of the current Z position | |
| for idx, (original_line_number, line) in enumerate(lines_with_numbers): | |
| # Skip processing lines that are empty or contain only '%' | |
| if not line.strip() or line.strip() == "%": | |
| continue | |
| tokens = line.split() | |
| # Update positioning mode if G90 or G91 is found | |
| if "G90" in tokens: | |
| positioning_mode = "G90" | |
| elif "G91" in tokens: | |
| positioning_mode = "G91" | |
| # Check for Z-axis movement | |
| z_values = [token for token in tokens if token.startswith("Z")] | |
| if z_values: | |
| try: | |
| z_value = Decimal(z_values[-1][1:]) | |
| except (ValueError, decimal.InvalidOperation): | |
| issues.append((original_line_number, f"(Error) Invalid Z value -> {line.strip()}")) | |
| continue | |
| # Calculate the new Z position based on positioning mode | |
| if current_z is None: | |
| current_z = z_value | |
| else: | |
| if positioning_mode == "G90": | |
| current_z = z_value | |
| elif positioning_mode == "G91": | |
| current_z += z_value | |
| # Check for G00 commands moving to Z ≤ 0 | |
| # Check for G00 commands moving to Z ≤ 0 | |
| if "G00" in tokens and current_z <= Decimal('0.0'): | |
| issues.append((original_line_number, f"(Error) G00 used for plunging to Z={current_z}. Use G01 to safely approach the workpiece -> {line.strip()}")) | |
| return issues | |
| def run_checks(gcode, depth_max=0.1): | |
| """ | |
| Runs all checks and returns a tuple containing lists of errors and warnings. | |
| """ | |
| errors = [] | |
| warnings = [] | |
| # Preprocess G-code to remove comments and get cleaned lines with original line numbers | |
| lines_with_numbers = preprocess_gcode(gcode) | |
| # Run all checks | |
| required_gcode_issues = check_required_gcodes(lines_with_numbers) | |
| spindle_issues = check_spindle(lines_with_numbers) | |
| feed_rate_issues = check_feed_rate(lines_with_numbers) | |
| depth_issues = check_depth_of_cut(lines_with_numbers, depth_max) | |
| end_gcode_issues = check_end_gcode(lines_with_numbers) | |
| interpolation_depth_issues = check_interpolation_depth(lines_with_numbers) | |
| plunge_retract_issues = check_plunge_retract_moves(lines_with_numbers) | |
| # Combine all issues | |
| all_issues = ( | |
| required_gcode_issues | |
| + spindle_issues | |
| + feed_rate_issues | |
| + depth_issues | |
| + end_gcode_issues | |
| + interpolation_depth_issues | |
| + plunge_retract_issues | |
| ) | |
| # Separate issues into errors and warnings | |
| for line_num, message in all_issues: | |
| if "(Error)" in message: | |
| errors.append((line_num, message)) | |
| elif "(Warning)" in message: | |
| warnings.append((line_num, message)) | |
| # Sort issues by line number | |
| errors.sort(key=lambda x: x[0]) | |
| warnings.sort(key=lambda x: x[0]) | |
| return errors, warnings | |
| if __name__ == "__main__": | |
| # Example usage | |
| gcode_sample = """ | |
| % | |
| G21 G90 G54 G17 | |
| G00 X0 Y0 Z5.0 | |
| M03 S1000 | |
| G01 Z-0.1 F100 ; Plunge using rapid movement (should be G01) | |
| G01 X10 Y10 | |
| G01 X20 Y20 | |
| G00 Z5.0 ; Retract using rapid movement (allowed since Z > 0) | |
| M05 | |
| M30 | |
| % | |
| """ | |
| depth_max = 0.1 # Set the maximum allowed depth of cut | |
| errors, warnings = run_checks(gcode_sample, depth_max) | |
| # Prepare the output as a string | |
| output_lines = [] | |
| if errors or warnings: | |
| output_lines.append("Issues found in G-code:") | |
| for line_num, message in errors + warnings: | |
| if line_num > 0: | |
| output_lines.append(f"Line {line_num}: {message}") | |
| else: | |
| output_lines.append(message) | |
| print('\n'.join(output_lines)) | |
| else: | |
| print("Your G-code looks good!") |