Compare commits

..

6 Commits

8 changed files with 1822 additions and 33 deletions

View File

@@ -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

210
gcodeinterpreter.py Normal file
View File

@@ -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]]

View File

@@ -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,11 +18,11 @@ 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 * HEIGHT_MM/WIDTH_MM)
screen_dimensions = (1024*1.5, 1024*1.5 * args.height/args.width)
visualizer = Visualizer(positions, screen_dimensions, (args.width, args.height))
visualizer.visualize()

18
port-finder.py Executable file
View File

@@ -0,0 +1,18 @@
#! /usr/bin/env python3
import serial.tools.list_ports
from serial.tools.list_ports_common import ListPortInfo
if __name__ == "__main__":
ports = serial.tools.list_ports.comports()
for port in ports:
if port.interface and port.interface.startswith("CircuitPython CDC2"): # CDC2 is the data-port
print(f"Port: {port.device}")
print(f" Description: {port.description}")
print(f" Manufacturer: {port.manufacturer}")
print(f" VID: {port.vid}")
print(f" PID: {port.pid}")
print(f" Serial Number: {port.serial_number}")
print(f" Location: {port.location}")
print(f" Interface: {port.interface}")
print()

View File

@@ -9,8 +9,7 @@ class PWMDriver():
def _init_pwm(self) -> None:
from serial import Serial
pass
#self.serial = Serial(self.port, self.baudrate)
self.serial = Serial(self.port, self.baudrate)
def _send_position(self, pos : tuple[float, float, float]) -> None:
x, y, z = pos
@@ -22,14 +21,11 @@ class PWMDriver():
# Send PWM values to the serial port
print(f"Sending PWM values: X={pwm_x:<5}, Y={pwm_y:<5}, Pen={pen}")
#self.serial.write(f"{pwm_x},{pwm_y},{pen}\n".encode())
self.serial.write(f"{pwm_x},{pwm_y},{pen}\n".encode())
def _position_reached(self) -> bool:
# Check if the position has been reached
from time import sleep
sleep(0.5)
return True
# return self.serial.readline().decode().strip() == "OK"
return self.serial.readline().decode().strip() == "OK"
def _wait_for_position(self) -> None:
# Wait until the position is reached

View File

@@ -1,4 +1,3 @@
pygame==2.6.1
pygcode==0.2.1
colorama==0.4.6
pyserial

26
set_pos.py Executable file
View File

@@ -0,0 +1,26 @@
#! /usr/bin/env python3
import serial
import argparse
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Set the position of the PWM driver.")
parser.add_argument("x", type=float, help="X coordinate [0 - 1]")
parser.add_argument("y", type=float, help="Y coordinate [0 - 1]")
parser.add_argument("pen", type=int, help="pen down (True) or pen up (False)")
parser.add_argument("-p", "--port", type=str, required=True, help="Serial port")
args = parser.parse_args()
# Open the serial port
ser = serial.Serial(args.port, 115200)
# Saturate values to [0, 1]
args.x = int(max(0, min(1, args.x)) * 0xFFFF)
args.y = int(max(0, min(1, args.y)) * 0xFFFF)
args.z = int(max(0, min(1, args.pen)))
# Send the position
print(f"{args.x},{args.y},{args.z}\n")
ser.write(f"{args.x},{args.y},{args.z}\n".encode())
# Close the serial port
ser.close()

1561
test/test_hatch_fill.gcode Normal file

File diff suppressed because it is too large Load Diff