Various bugfixes, moved svgtogcode to its own file

Aussi, on peut exercise toute la communication c'est trop chouette!!!
This commit is contained in:
Sélène Corbineau 2025-01-15 01:12:27 +01:00
parent b43c0b64c3
commit f06589562c
7 changed files with 198 additions and 176 deletions

View file

@ -131,10 +131,14 @@ class GCodeToMotors:
# HARDWARE.realize() applies the current commands to the actual hardware
def move(self):
print(self.target_units)
print(self.feedrate)
self.HARDWARE.probe(self)
while not self.CONTROLLER(self): # Allow controller to alter self
self.HARDWARE.realize(self)
time.sleep(self.ctrl_step)
print("realized?")
time.sleep(self.ctrl_step)
self.HARDWARE.probe(self)
self.calculate_deltas()
@ -150,7 +154,7 @@ class GCodeToMotors:
or has_command('Z', instruction):
code = search_string('G', instruction)
match code:
case 0, 1, 2, 3:
case 0 | 1 | 2 | 3:
if self.abs_mode:
if has_command('X', instruction):
fp[0] = search_string('X', instruction)
@ -172,17 +176,17 @@ class GCodeToMotors:
fp[2] = self.current_units[2] + search_string('Z', instruction)
case _:
pass
match code:
case 0, 1:
self.set_position(fp[0], fp[1], fp[2])
case 0 | 1:
self.set_target(fp[0], fp[1], fp[2])
if has_command('G', instruction) and code == 1:
self.feedrate = search_string('F', instruction)
if self.feedrate == 0:
self.feedrate = self.get_max_speed()
self.move()
case 2, 3:
case 2 | 3:
center = [0., 0., 0.]
center[0] = search_string('I', instruction) + self.current_units[0]
center[1] = search_string('J', instruction) + self.current_units[1]
@ -258,7 +262,7 @@ class GCodeToMotors:
case 5.1:
raise NotImplementedError("PAS DE SPLINE QUADRATIQUE J'AI LA FLEMME")
case 5.2, 5.3:
case 5.2 | 5.3:
raise NotImplementedError("Experimental Unimplemented Feature")
case 6:
@ -338,9 +342,11 @@ def has_command(key: str, instruction: str) -> bool:
return key in instruction
def search_string(key: str, instruction: str) -> int:
def search_string(key: str, instruction: str) -> float:
index = instruction.find(key)
if(index==-1):
return float(0.)
tmp = instruction[index+1:].split(' ')[0]
return int(tmp)
return float(tmp)

View file

@ -1,4 +1,4 @@
import numpy as np
class Controller:
def __init__(self):
pass
@ -58,23 +58,27 @@ class DummyController(Controller):
def __call__(self, gtm, *args, **kwargs):
print("Call Me")
feed = self.feedrate
dx, dy, dz = self.delta_units
feed_x = feed * dx/(dx**2 + dy**2 + dz**2)
feed_y = feed * dy/(dx**2 + dy**2 + dz**2)
feed_z = feed * dz/(dz**2 + dy**2 + dz**2)
feed = gtm.feedrate
dx, dy, dz = gtm.delta_units
feed_x = feed * dx/(dx**2 + dy**2 + dz**2)
feed_y = feed * dy/(dx**2 + dy**2 + dz**2)
feed_z = feed * dz/(dz**2 + dy**2 + dz**2)
gtm.x_direction = 1 if gtm.delta_steps[0] > 0 else 0
gtm.y_direction = 1 if gtm.delta_steps[1] > 0 else 0
gtm.z_direction = 1 if gtm.delta_steps[2] > 0 else 0
gtm.throttle_x = floor(gtm.FAST_XY_FEEDRATE/feed_x)
gtm.throttle_y = floor(gtm.FAST_XY_FEEDRATE/feed_y)
gtm.throttle_z = floor(gtm.FAST_Z_FEEDRATE/feed_z)
feed_x,feed_y,feed_z = abs(feed_x), abs(feed_y), abs(feed_z)
gtm.throttle_x = 0 if gtm.throttle_x >= 65536 else gtm.throttle_x
gtm.throttle_y = 0 if gtm.throttle_y >= 65536 else gtm.throttle_y
gtm.throttle_z = 0 if gtm.throttle_z >= 65536 else gtm.throttle_z
gtm.x_throttle = 0 if feed_x == 0. else np.floor(gtm.FAST_XY_FEEDRATE/feed_x)
gtm.y_throttle = 0 if feed_y == 0. else np.floor(gtm.FAST_XY_FEEDRATE/feed_y)
gtm.z_throttle = 0 if feed_z == 0. else np.floor(gtm.FAST_Z_FEEDRATE/feed_z)
# If we are close enough, we're good
return (gtm.ctrl_step*(feed*1000./60) >= 3.*(dx**2+dy**2+dz**2)**.5):
gtm.x_throttle = 0 if gtm.x_throttle >= 65536 else gtm.x_throttle
gtm.y_throttle = 0 if gtm.y_throttle >= 65536 else gtm.y_throttle
gtm.z_throttle = 0 if gtm.z_throttle >= 65536 else gtm.z_throttle
print("Throttles : ", gtm.x_throttle, gtm.y_throttle, gtm.z_throttle)
# If we are close enough, we're good
return (gtm.ctrl_step*(feed*1000./60) >= 3.*(dx**2+dy**2+dz**2)**.5)

View file

@ -1,6 +1,7 @@
import socket
import GCode_Interpreterdc
from controller import DummyController
class Hardware:
def __init__(self, bind):
@ -20,11 +21,11 @@ class SimuHardware(Hardware):
def probe(self, gtm):
self.s.send("request".encode())
self.s.send("request\n".encode())
encoder_x = int(socket.SocketIO(self.s, "rw").readline())
encoder_y = int(socket.SocketIO(self.s, "rw").readline())
encoder_z = int(socket.SocketIO(self.s, "rw").readline())
print("HAS PROBED")
last_x, last_y, last_z = gtm.current_steps
# We are now at some position which realizes the encoder positions
# curr_x = encoder_x + k*X_MOTOR_STEPS
@ -43,33 +44,34 @@ class SimuHardware(Hardware):
# Note that if we don't probe regularly enough, we lose track of the position
# Really, we should be able to set_steps
gtm.set_position([curr_x / gtm.X_STEPS_PER_MM,
curr_y / gtm.Y_STEPS_PER_MM,
curr_z / gtm.Z_STEPS_PER_MM])
gtm.set_position(curr_x / gtm.X_STEPS_PER_MM,
curr_y / gtm.Y_STEPS_PER_MM,
curr_z / gtm.Z_STEPS_PER_MM)
def realize(self, gtm):
self.s.send("realize".encode())
self.s.send("realize\n".encode())
if gtm.throttle_x == 0:
self.s.send(gtm.x_direction + " 0")
if gtm.x_throttle == 0.:
self.s.send(str(gtm.x_direction).encode() + b" 0\n")
else:
self.s.send(gtm.x_direction + " " + gtm.FAST_XY_FEEDRATE / gtm.x_throttle)
self.s.send(str(gtm.x_direction).encode() + b" " + str(gtm.FAST_XY_FEEDRATE / gtm.x_throttle).encode() + b"\n")
if gtm.throttle_y == 0:
self.s.send(gtm.y_direction + " 0")
if gtm.y_throttle == 0.:
self.s.send(str(gtm.y_direction).encode() + b" 0\n")
else:
self.s.send(gtm.y_direction + " " + gtm.FAST_XY_FEEDRATE / gtm.y_throttle)
self.s.send(str(gtm.y_direction).encode() + b" " + str(gtm.FAST_XY_FEEDRATE / gtm.y_throttle).encode() + b"\n")
if gtm.throttle_z == 0:
self.s.send(gtm.z_direction + " 0")
if gtm.z_throttle == 0.:
self.s.send(str(gtm.z_direction).encode() + b" 0\n")
else:
self.s.send(gtm.z_direction + " " + gtm.FAST_Z_FEEDRATE / gtm.z_throttle)
self.s.send(str(gtm.z_direction).encode() + b" " + str(gtm.FAST_Z_FEEDRATE / gtm.z_throttle).encode() + b"\n")
if __name__ == '__main__':
b = "tmp.txt"
# sim = simulator.Simulator(b)
b = "socket.sock"
sh = SimuHardware(b)
gtm1 = GCode_Interpreterdc.GCodeToMotors
ctrl=DummyController()
gtm1 = GCode_Interpreterdc.GCodeToMotors(ctrl, sh)
sh.probe(gtm1)
print(gtm1.current_units)

133
main.py
View file

@ -1,126 +1,9 @@
import svgpathtools
import math
from typing import List, Optional
import hardware as hw
import controller as ctl
import GCode_Interpreterdc as gci
class SVGToGCodeConverter:
"""
General SVG to GCode converter, parametrized with the available functions.
"""
def __init__(self, supported_g_functions: List[str]):
"""Initialize the converter with the supported G-functions.
Args:
supported_g_functions (List[str]): List of supported G-code functions (e.g., ["G1", "G2", "G3", "G4", "G5", "G6", "G7"]).
"""
self.supported_g_functions = supported_g_functions
self._warned_about_g5 = False
def point_to_gcode(self, x: float, y: float, feedrate: Optional[float] = None) -> str:
gcode = f"G1 X{x:.4f} Y{y:.4f}"
if feedrate is not None:
gcode += f" F{feedrate}"
return gcode
@staticmethod
def move_to_gcode(x: float, y: float) -> str:
return f"G0 X{x:.4f} Y{y:.4f}"
def line_to_gcode(self, start: complex, end: complex) -> str:
return self.point_to_gcode(end.real, end.imag)
def arc_to_gcode(self, start: complex, end: complex, center: complex, clockwise: bool) -> str:
if "G2" not in self.supported_g_functions and "G3" not in self.supported_g_functions:
raise NotImplementedError("Arc support requires G2/G3 functions.")
i_offset = center.real - start.real
j_offset = center.imag - start.imag
g_command = "G2" if clockwise else "G3"
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} I{i_offset:.4f} J{j_offset:.4f}"
def bezier_to_gcode(self, start: complex, control1: complex, control2: complex, end: complex, steps: int = 20) -> List[str]:
if "G5" in self.supported_g_functions:
return [
f"G5 X{end.real:.4f} Y{end.imag:.4f} I{control1.real:.4f} J{control1.imag:.4f} P{control2.real:.4f} Q{control2.imag:.4f}"
]
else:
if not self._warned_about_g5:
print("Warning: G5 is not supported. Approximating Bézier curve with linear segments.")
self._warned_about_g5 = True
gcode_lines = []
for t in [i / steps for i in range(1, steps + 1)]:
x = (1 - t)**3 * start.real + 3 * (1 - t)**2 * t * control1.real + 3 * (1 - t) * t**2 * control2.real + t**3 * end.real
y = (1 - t)**3 * start.imag + 3 * (1 - t)**2 * t * control1.imag + 3 * (1 - t) * t**2 * control2.imag + t**3 * end.imag
gcode_lines.append(self.point_to_gcode(x, y))
return gcode_lines
# Following two are not canonical, I don't know where I found those lol
def ellipse_to_gcode(self, start: complex, end: complex, center: complex, rx: float, ry: float, rotation: float, clockwise: bool) -> str:
if "G7" not in self.supported_g_functions:
raise NotImplementedError("Ellipse support requires G7 function.")
i_offset = center.real - start.real
j_offset = center.imag - start.imag
g_command = "G7" # Assuming G7 is used for ellipses
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} I{i_offset:.4f} J{j_offset:.4f} R1={rx:.4f} R2={ry:.4f} ROT={rotation:.4f}"
def parabola_to_gcode(self, start: complex, vertex: complex, end: complex) -> str:
if "G6" not in self.supported_g_functions:
raise NotImplementedError("Parabola support requires G6 function.")
g_command = "G6" # Assuming G6 is used for parabolas
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} VERTEX_X{vertex.real:.4f} VERTEX_Y{vertex.imag:.4f}"
def wait_time_gcode(self, seconds: float) -> str:
if "G4" not in self.supported_g_functions:
raise NotImplementedError("Wait time support requires G4 function.")
return f"G4 P{seconds:.3f}"
def parse_svg_to_gcode(self, svg_path: svgpathtools.Path) -> List[str]:
gcode = []
for segment in svg_path:
if isinstance(segment, svgpathtools.Line):
gcode.append(self.line_to_gcode(segment.start, segment.end))
elif isinstance(segment, svgpathtools.CubicBezier):
gcode.extend(self.bezier_to_gcode(segment.start, segment.control1, segment.control2, segment.end))
elif isinstance(segment, svgpathtools.Arc):
center = segment.center
if "G7" in self.supported_g_functions:
rx, ry = segment.radius.real, segment.radius.imag
rotation = segment.rotation
gcode.append(self.ellipse_to_gcode(segment.start, segment.end, center, rx, ry, rotation, segment.sweep_flag == 0))
else:
gcode.append(self.arc_to_gcode(segment.start, segment.end, center, segment.sweep_flag == 0))
elif hasattr(segment, "vertex") and "G6" in self.supported_g_functions:
gcode.append(self.parabola_to_gcode(segment.start, segment.vertex, segment.end))
else:
raise ValueError(f"Unsupported path segment: {segment}")
return gcode
def svg_to_gcode(self, file_path: str, output_path: str) -> None:
"""Convert an SVG file to G-code.
Args:
file_path (str): Path to the input SVG file.
output_path (str): Path to save the output G-code file.
"""
paths, attributes = svgpathtools.svg2paths(file_path)
gcode = ["G21 ; Set units to mm", "G90 ; Absolute positioning"] # G-code header
for path in paths:
if path:
start_point = path[0].start
gcode.append(self.move_to_gcode(start_point.real, start_point.imag))
gcode.extend(self.parse_svg_to_gcode(path))
with open(output_path, "w") as gcode_file:
gcode_file.write("\n".join(gcode))
print(f"G-code saved to {output_path}")
# Example usage:
# converter = SVGToGCodeConverter(["G1", "G2", "G3", "G5", "G6", "G7"])
# converter.svg_to_gcode("example.svg", "output.gcode")
b = "socket.sock"
sh = hw.SimuHardware(b)
ctrl= ctl.DummyController()
gtm1 = gci.GCodeToMotors(ctrl, sh)
gtm1.instruction_converter("G0 X2.000 Y30.000 F1.000")

View file

@ -7,6 +7,7 @@ pkgs.mkShell {
ps.numpy
ps.pyaudio
ps.matplotlib
ps.scipy]))
ps.scipy
]))
];
}

View file

@ -159,10 +159,10 @@ class NaiveSimulator(Simulator, Douche):
self.pos += self.speed * self.time_step
self.speed += 1./self.J * (gamma - self.f*self.speed) * self.time_step
print("timestep ", self.time_step)
print("gamma ", gamma)
print("speed ", self.speed)
print("position ", self.pos)
#print("timestep ", self.time_step)
#print("gamma ", gamma)
#print("speed ", self.speed)
#print("position ", self.pos)
self.steps = np.ceil(self.pos * np.array([self.X_STEPS_PER_MM,
self.Y_STEPS_PER_MM,
@ -170,9 +170,9 @@ class NaiveSimulator(Simulator, Douche):
self.add_point(self.pos[0], self.pos[1])
def request(self):
self.s.send(f"{self.steps[0]}".encode())
self.s.send(f"{self.steps[1]}".encode())
self.s.send(f"{self.steps[2]}".encode())
self.s.send(f"{int(self.steps[0])}\n".encode())
self.s.send(f"{int(self.steps[1])}\n".encode())
self.s.send(f"{int(self.steps[2])}\n".encode())
def realize(self):
print("I JUST REALIZED: MA VIE C'EST DE LA MERDE")
@ -181,7 +181,7 @@ class NaiveSimulator(Simulator, Douche):
z_v = parse_speed(socket.SocketIO(self.s, 'r').readline())
print("Everything parsed")
self.command_spd = np.array([x_v, y_v, z_v])
self.s.send("Realized".encode())
print("New speeds : ", self.command_spd)
#self.s.send("Realized\n".encode())
simu = NaiveSimulator("socket.sock")

126
svgtogcode.py Normal file
View file

@ -0,0 +1,126 @@
import svgpathtools
import math
from typing import List, Optional
class SVGToGCodeConverter:
"""
General SVG to GCode converter, parametrized with the available functions.
"""
def __init__(self, supported_g_functions: List[str]):
"""Initialize the converter with the supported G-functions.
Args:
supported_g_functions (List[str]): List of supported G-code functions (e.g., ["G1", "G2", "G3", "G4", "G5", "G6", "G7"]).
"""
self.supported_g_functions = supported_g_functions
self._warned_about_g5 = False
def point_to_gcode(self, x: float, y: float, feedrate: Optional[float] = None) -> str:
gcode = f"G1 X{x:.4f} Y{y:.4f}"
if feedrate is not None:
gcode += f" F{feedrate}"
return gcode
@staticmethod
def move_to_gcode(x: float, y: float) -> str:
return f"G0 X{x:.4f} Y{y:.4f}"
def line_to_gcode(self, start: complex, end: complex) -> str:
return self.point_to_gcode(end.real, end.imag)
def arc_to_gcode(self, start: complex, end: complex, center: complex, clockwise: bool) -> str:
if "G2" not in self.supported_g_functions and "G3" not in self.supported_g_functions:
raise NotImplementedError("Arc support requires G2/G3 functions.")
i_offset = center.real - start.real
j_offset = center.imag - start.imag
g_command = "G2" if clockwise else "G3"
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} I{i_offset:.4f} J{j_offset:.4f}"
def bezier_to_gcode(self, start: complex, control1: complex, control2: complex, end: complex, steps: int = 20) -> List[str]:
if "G5" in self.supported_g_functions:
return [
f"G5 X{end.real:.4f} Y{end.imag:.4f} I{control1.real:.4f} J{control1.imag:.4f} P{control2.real:.4f} Q{control2.imag:.4f}"
]
else:
if not self._warned_about_g5:
print("Warning: G5 is not supported. Approximating Bézier curve with linear segments.")
self._warned_about_g5 = True
gcode_lines = []
for t in [i / steps for i in range(1, steps + 1)]:
x = (1 - t)**3 * start.real + 3 * (1 - t)**2 * t * control1.real + 3 * (1 - t) * t**2 * control2.real + t**3 * end.real
y = (1 - t)**3 * start.imag + 3 * (1 - t)**2 * t * control1.imag + 3 * (1 - t) * t**2 * control2.imag + t**3 * end.imag
gcode_lines.append(self.point_to_gcode(x, y))
return gcode_lines
# Following two are not canonical, I don't know where I found those lol
def ellipse_to_gcode(self, start: complex, end: complex, center: complex, rx: float, ry: float, rotation: float, clockwise: bool) -> str:
if "G7" not in self.supported_g_functions:
raise NotImplementedError("Ellipse support requires G7 function.")
i_offset = center.real - start.real
j_offset = center.imag - start.imag
g_command = "G7" # Assuming G7 is used for ellipses
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} I{i_offset:.4f} J{j_offset:.4f} R1={rx:.4f} R2={ry:.4f} ROT={rotation:.4f}"
def parabola_to_gcode(self, start: complex, vertex: complex, end: complex) -> str:
if "G6" not in self.supported_g_functions:
raise NotImplementedError("Parabola support requires G6 function.")
g_command = "G6" # Assuming G6 is used for parabolas
return f"{g_command} X{end.real:.4f} Y{end.imag:.4f} VERTEX_X{vertex.real:.4f} VERTEX_Y{vertex.imag:.4f}"
def wait_time_gcode(self, seconds: float) -> str:
if "G4" not in self.supported_g_functions:
raise NotImplementedError("Wait time support requires G4 function.")
return f"G4 P{seconds:.3f}"
def parse_svg_to_gcode(self, svg_path: svgpathtools.Path) -> List[str]:
gcode = []
for segment in svg_path:
if isinstance(segment, svgpathtools.Line):
gcode.append(self.line_to_gcode(segment.start, segment.end))
elif isinstance(segment, svgpathtools.CubicBezier):
gcode.extend(self.bezier_to_gcode(segment.start, segment.control1, segment.control2, segment.end))
elif isinstance(segment, svgpathtools.Arc):
center = segment.center
if "G7" in self.supported_g_functions:
rx, ry = segment.radius.real, segment.radius.imag
rotation = segment.rotation
gcode.append(self.ellipse_to_gcode(segment.start, segment.end, center, rx, ry, rotation, segment.sweep_flag == 0))
else:
gcode.append(self.arc_to_gcode(segment.start, segment.end, center, segment.sweep_flag == 0))
elif hasattr(segment, "vertex") and "G6" in self.supported_g_functions:
gcode.append(self.parabola_to_gcode(segment.start, segment.vertex, segment.end))
else:
raise ValueError(f"Unsupported path segment: {segment}")
return gcode
def svg_to_gcode(self, file_path: str, output_path: str) -> None:
"""Convert an SVG file to G-code.
Args:
file_path (str): Path to the input SVG file.
output_path (str): Path to save the output G-code file.
"""
paths, attributes = svgpathtools.svg2paths(file_path)
gcode = ["G21 ; Set units to mm", "G90 ; Absolute positioning"] # G-code header
for path in paths:
if path:
start_point = path[0].start
gcode.append(self.move_to_gcode(start_point.real, start_point.imag))
gcode.extend(self.parse_svg_to_gcode(path))
with open(output_path, "w") as gcode_file:
gcode_file.write("\n".join(gcode))
print(f"G-code saved to {output_path}")
# Example usage:
# converter = SVGToGCodeConverter(["G1", "G2", "G3", "G5", "G6", "G7"])
# converter.svg_to_gcode("example.svg", "output.gcode")