Compare commits
6 Commits
b4a774b2b9
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ade1aba1b1 | ||
| 6447a11768 | |||
| a1d73a26da | |||
| fa1122b32c | |||
| f9232df233 | |||
| 20c943b73c |
@@ -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
210
gcodeinterpreter.py
Normal 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]]
|
||||
8
main.py
8
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,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
18
port-finder.py
Executable 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()
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pygame==2.6.1
|
||||
pygcode==0.2.1
|
||||
colorama==0.4.6
|
||||
pyserial
|
||||
26
set_pos.py
Executable file
26
set_pos.py
Executable 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
1561
test/test_hatch_fill.gcode
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user