CoolFace
Apppublic

nicoaspra/G-code_Programming_Assistant

sourceHugging Faceupdated 2y agoView on Hugging Face
1likes
python_gcode_checker.py503 linesDownload Raw Back to root
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!")