diff --git a/gcode_parser.py b/gcode_parser.py deleted file mode 100644 index 29c9337..0000000 --- a/gcode_parser.py +++ /dev/null @@ -1,21 +0,0 @@ - -class GCodeParser(): - def __init__(self, gcode_file): - self.gcode_file = gcode_file - self.lines = [] - self._parse_gcode() - - def _parse_gcode(self): - from pygcode import Line, Machine - machine = Machine() - positions = [] - with open(self.gcode_file, "r") as gcode_file: - for line_text in gcode_file: - line = Line(line_text) - if line.block.gcodes: - machine.process_gcodes(*line.block.gcodes) - positions.append((machine.pos.X, machine.pos.Y, machine.pos.Z)) - self.lines = positions - - def get_positions(self): - return self.lines diff --git a/gcodeinterpreter.py b/gcodeinterpreter.py new file mode 100644 index 0000000..2b7d876 --- /dev/null +++ b/gcodeinterpreter.py @@ -0,0 +1,210 @@ +import math +import re + +class GcodeInterpreter: + """ + A basic G-code interpreter that converts G-code commands into a list of coordinates. + Supports G00, G01, G02, and G03 commands. + + Attributes: + current_position (list): The current position in 3D space [X, Y, Z]. + feed_rate (float): The current feed rate. + coordinates (list): The list of calculated coordinates. + segment_multiplier (float): Multiplier for distance to determine number of segments. + max_segments (int): Maximum number of segments allowed. + """ + + def __init__(self, initial_position=[0, 0, 0], initial_feed_rate=0, segment_multiplier=2, max_segments=50): + """ + Initializes the GcodeInterpreter with the initial position, feed rate, segment multiplier, and maximum segments. + + Args: + initial_position (list, optional): The initial position [X, Y, Z]. Defaults to [0, 0, 0]. + initial_feed_rate (float, optional): The initial feed rate. Defaults to 0. + segment_multiplier (float, optional): Multiplier for distance to determine number of segments. Defaults to 10.0 + max_segments (int, optional): Maximum number of segments allowed. Defaults to 100. + """ + self.current_position = initial_position[:] # Use a copy to avoid modifying the original + self.feed_rate = initial_feed_rate + self.coordinates = [initial_position[:]] # Store the initial position + self.segment_multiplier = segment_multiplier # Added segment multiplier + self.max_segments = max_segments # Added max segments + + def parse_file(self, filename): + """ + Parses a G-code file and processes each line. + + Args: + filename (str): The path to the G-code file. + + Returns: + GcodeInterpreter: The instance of the GcodeInterpreter with processed coordinates. + """ + with open(filename, 'r') as file: + for line in file: + self.parse_line(line) + return self + + def parse_line(self, line): + """ + Parses a single line of G-code. + + Args: + line (str): The G-code line to parse. + """ + line = line.strip() + if not line or line.startswith('%') or line.startswith('('): # Skip empty lines and lines starting with '%' + return + + # Use regular expression for more robust parsing + parts = re.findall(r'([A-Za-z])([-+]?\d*\.?\d*)', line) # Find all letter-number pairs + if not parts: + return # Skip lines without any recognized G-code commands + + command = parts[0][0].upper() + parts[0][1] + args = {} + for part in parts: + if part[0].upper() != command: + try: + args[part[0].upper()] = float(part[1]) if part[1] else 0.0 # convert the number part to float + except ValueError: + print(f"Warning: Could not convert value '{part[1]}' to float for argument {part[0]}. Skipping.") + args[part[0].upper()] = 0.0 + self.process_command(command, args) + + def process_command(self, command, args): + """ + Processes a G-code command and calls the appropriate function. + + Args: + command (str): The G-code command (e.g., 'G00', 'G01', 'G02', 'G03'). + args (dict): A dictionary of arguments for the command (e.g., {'X': 10, 'Y': 20}). + """ + if not command: # Add this check + return + if command in ('G00', 'G01'): + self.process_g00_g01(command, args) + elif command == 'G02': + self.process_g02_g03(command, args, clockwise=True) + elif command == 'G03': + self.process_g02_g03(command, args, clockwise=False) + elif command == 'G04': + self.process_g04(args) + elif command == 'F': + self.feed_rate = args.get('F', self.feed_rate) # Keep the current feed rate if not provided + # Add other G-code commands as needed (e.g., G02, G03, G28, etc.) + else: + print(f"Unsupported G-code command: {command}") + + def process_g00_g01(self, command, args): + """ + Processes G00 (Rapid Linear Move) and G01 (Controlled Linear Move) commands. + + Args: + command (str): The G-code command ('G00' or 'G01'). + args (dict): A dictionary of arguments for the command (e.g., {'X': 10, 'Y': 20, 'Z': 5}). + """ + new_position = [args.get('X', self.current_position[0]), + args.get('Y', self.current_position[1]), + args.get('Z', self.current_position[2])] + + # Calculate the distance between the current position and the new position + distance = math.sqrt( + (new_position[0] - self.current_position[0]) ** 2 + + (new_position[1] - self.current_position[1]) ** 2 + + (new_position[2] - self.current_position[2]) ** 2 + ) + + # Number of segments. Increase for smoother lines, but use a reasonable maximum. + segments = int(distance * self.segment_multiplier) + 1 + segments = min(segments, self.max_segments) # Limit the maximum number of segments + + for i in range(segments + 1): + x = self.current_position[0] + (new_position[0] - self.current_position[0]) * i / segments + y = self.current_position[1] + (new_position[1] - self.current_position[1]) * i / segments + z = self.current_position[2] + (new_position[2] - self.current_position[2]) * i / segments + self.current_position = [x, y, z] + self.coordinates.append([x, y, z]) + + def process_g02_g03(self, command, args, clockwise=True): + """ + Processes G02 (Clockwise Circular Interpolation) and G03 (Counter-Clockwise Circular Interpolation) commands. + + Args: + command (str): The G-code command ('G02' or 'G03'). + args (dict): A dictionary of arguments for the command. + clockwise (bool): True for G02 (clockwise), False for G03 (counter-clockwise). + """ + new_position = [args.get('X', self.current_position[0]), + args.get('Y', self.current_position[1]), + args.get('Z', self.current_position[2])] + center = [self.current_position[0] + args.get('I', 0), + self.current_position[1] + args.get('J', 0), + self.current_position[2] + args.get('K', 0)] + + radius = math.sqrt((self.current_position[0] - center[0]) ** 2 + + (self.current_position[1] - center[1]) ** 2 + + (self.current_position[2] - center[2]) ** 2) + # Handle the case where the radius is zero + if radius == 0: + print(f"Warning: Radius is zero for {command}. No interpolation performed.") + return + + # Calculate the angle between the current point and the target point + start_angle = math.atan2(self.current_position[1] - center[1], self.current_position[0] - center[0]) + end_angle = math.atan2(new_position[1] - center[1], new_position[0] - center[0]) + + # Ensure angles are between 0 and 2*pi + if start_angle < 0: + start_angle += 2 * math.pi + if end_angle < 0: + end_angle += 2 * math.pi + + # Calculate the total angle + total_angle = end_angle - start_angle + if clockwise: + if total_angle > 0: + total_angle -= 2 * math.pi + else: + if total_angle < 0: + total_angle += 2 * math.pi + + # Number of segments. Increase for smoother curves. + arc_length = radius * abs(total_angle) + segments = int(arc_length * self.segment_multiplier) + 1 # Dynamic segments, at least 1. + segments = min(segments, self.max_segments) # Limit max segments + angle_increment = total_angle / segments + + for i in range(segments + 1): + angle = start_angle + i * angle_increment + x = center[0] + radius * math.cos(angle) + y = center[1] + radius * math.sin(angle) + z = (self.current_position[2] + (new_position[2] - self.current_position[2]) * i / segments) + self.current_position = [x, y, z] + self.coordinates.append([x, y, z]) + + def process_g04(self, args): + """Processes G04 (Dwell) command. For now, just prints a message. + + Args: + args (dict): A dictionary of arguments, containing 'P' for the dwell time in milliseconds + """ + dwell_time = args.get('P', 0) + print(f"G04 Dwell for {dwell_time} milliseconds") + # In a real implementation, you would pause execution here. For this example, we just print. + pass + + def get_coordinates(self): + """ + Returns the list of calculated coordinates. + + Returns: + list: The list of coordinates. + """ + return self.coordinates + + def reset(self): + """Resets the interpreter to its initial state.""" + self.current_position = [0, 0, 0] + self.feed_rate = 0 + self.coordinates = [[0,0,0]] diff --git a/main.py b/main.py index 50b847d..53d5a61 100755 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ #! /usr/bin/env python from argparse import ArgumentParser -from gcode_parser import GCodeParser +from gcodeinterpreter import GcodeInterpreter from pwm_driver import PWMDriver from visualizer import Visualizer @@ -18,8 +18,8 @@ if __name__ == "__main__": args = argparser.parse_args() - parser = GCodeParser(args.file) - positions = parser.get_positions() + parser = GcodeInterpreter().parse_file(args.file) + positions = parser.get_coordinates() if args.visualize: screen_dimensions = (1024*1.5, 1024*1.5 * args.height/args.width) diff --git a/requirements.txt b/requirements.txt index d06d8c8..0a2378e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ pygame==2.6.1 -pygcode==0.2.1 colorama==0.4.6 pyserial \ No newline at end of file