549 lines
14 KiB
Python
549 lines
14 KiB
Python
|
#
|
||
|
# Copyright (C) 2021 Thomas Van Acker
|
||
|
#
|
||
|
# This program is free software: you can redistribute it and/or modify it under
|
||
|
# the terms of the GNU General Public License as published by the Free Software
|
||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||
|
# version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License along with
|
||
|
# this program. If not, see <https://www.gnu.org/licenses/>.""")
|
||
|
#
|
||
|
|
||
|
import serial
|
||
|
import time
|
||
|
import threading
|
||
|
import math
|
||
|
|
||
|
|
||
|
print("PrintBuddy V0.1")
|
||
|
print("""
|
||
|
Copyright (C) 2021 Thomas Van Acker
|
||
|
|
||
|
This program comes with ABSOLUTELY NO WARRANTY.
|
||
|
|
||
|
This program is free software: you can redistribute it and/or modify it under
|
||
|
the terms of the GNU General Public License as published by the Free Software
|
||
|
Foundation, either version 3 of the License, or (at your option) any later
|
||
|
version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License along with
|
||
|
this program. If not, see <https://www.gnu.org/licenses/>.""")
|
||
|
print("")
|
||
|
|
||
|
# Static var init
|
||
|
COMMANDS = {
|
||
|
"help":"List all commands with explanation",
|
||
|
"exit":"Disconnect from printer and exit",
|
||
|
"raw":"Send raw gcode commands",
|
||
|
"home":"Auto-home the machine",
|
||
|
"load":"Load a gcode file",
|
||
|
"add":"Manipulate the loaded gcode",
|
||
|
"print":"Print the loaded gcode",
|
||
|
"level":"Start bed leveling wizard",
|
||
|
"mesh":"Start mesh bed leveling wizard"
|
||
|
}
|
||
|
|
||
|
GCODE_FILCH_LAYER = ["G28 X Y", "M84", "M300 P3000", "M0", "M600", "M400", "G28 X Y", "G0 F1500"]
|
||
|
|
||
|
# Function declarations
|
||
|
def send(c, silent=False):
|
||
|
global s
|
||
|
if not silent:
|
||
|
print("%s"%c)
|
||
|
c += "\n"
|
||
|
s.write(c.encode("utf-8"))
|
||
|
while True:
|
||
|
r = s.readline().decode("utf-8").strip()
|
||
|
if not silent:
|
||
|
print(" > %s"%r)
|
||
|
if r[0:2] == "ok":
|
||
|
break
|
||
|
|
||
|
def threadPrintInput():
|
||
|
# Checks for input from user during printing process
|
||
|
global abortPrint
|
||
|
while printing:
|
||
|
i = input()
|
||
|
|
||
|
if i == "stop" or i == "exit":
|
||
|
abortPrint = True
|
||
|
break
|
||
|
|
||
|
def meshZ(X, Y):
|
||
|
# Interpolates values of mesh
|
||
|
x = X/300*(MESH_SIZE-1)
|
||
|
y = Y/300*(MESH_SIZE-1)
|
||
|
|
||
|
xmin = math.floor(x)
|
||
|
xmax = math.ceil(x)
|
||
|
ymin = math.floor(y)
|
||
|
ymax = math.ceil(y)
|
||
|
|
||
|
z0 = mesh[ymin][xmin] + (mesh[ymin][xmax]-mesh[ymin][xmin])*(x-xmin)
|
||
|
z1 = mesh[ymax][xmin] + (mesh[ymax][xmax]-mesh[ymax][xmin])*(x-xmin)
|
||
|
|
||
|
z = z0 + (z1-z0)*(y-ymin)
|
||
|
return z
|
||
|
|
||
|
# Var init
|
||
|
serialPort = "/dev/ttyUSB0"
|
||
|
serialBaud = 115200
|
||
|
|
||
|
gcodeRaw = ""
|
||
|
gcodeLines = []
|
||
|
gcodeLayers = 1
|
||
|
printing = False
|
||
|
abortPrint = False
|
||
|
|
||
|
MESH_SIZE = 9 # Number of points of a side in the mesh.
|
||
|
MESH_OFFSET = 1.00 # The distance above Z=0 where printbed is located. Mesh needs to be added to this number to get the absolute Z height.
|
||
|
mesh = [[0 for _ in range(MESH_SIZE)] for __ in range(MESH_SIZE)]
|
||
|
meshPositions = [["X%d Y%d"%(280/(MESH_SIZE-1)*x+10, 280/(MESH_SIZE-1)*y+10) for x in range(MESH_SIZE)] for y in range(MESH_SIZE)]
|
||
|
|
||
|
levelingCornersPos = {0:"X10 Y45",1:"X10 Y260",2:"X280 Y260",3:"X280 Y45"}
|
||
|
|
||
|
|
||
|
# Read mesh file
|
||
|
print("Reading mesh file...")
|
||
|
try:
|
||
|
f = open("mesh.txt", "r")
|
||
|
t = f.read()[:-1]
|
||
|
f.close()
|
||
|
|
||
|
lines = t.split("\n")
|
||
|
MESH_SIZE = int(lines[0])
|
||
|
mesh = [[0 for _ in range(MESH_SIZE)] for __ in range(MESH_SIZE)]
|
||
|
meshPositions = [["X%d Y%d"%(280/(MESH_SIZE-1)*x+10, 280/(MESH_SIZE-1)*y+10) for x in range(MESH_SIZE)] for y in range(MESH_SIZE)]
|
||
|
|
||
|
for i in range(MESH_SIZE*MESH_SIZE):
|
||
|
mesh[int(i/MESH_SIZE)][int(i%MESH_SIZE)] = float(lines[i+1])
|
||
|
|
||
|
print("Mesh loaded.")
|
||
|
except Exception as e:
|
||
|
print("Couldn't read mesh file. Using default mesh.")
|
||
|
print(e)
|
||
|
print("Current mesh:")
|
||
|
for i in mesh:
|
||
|
for j in i:
|
||
|
print("{:5.2f}".format(j), end=" ")
|
||
|
print()
|
||
|
|
||
|
# Connect to printer
|
||
|
print("\nConnecting to printer...")
|
||
|
s = serial.Serial(serialPort, serialBaud)
|
||
|
time.sleep(1)
|
||
|
s.write(b"\r\n\r\n") # Wake printer
|
||
|
time.sleep(2)
|
||
|
readBytes = s.inWaiting()
|
||
|
r = s.read(readBytes).decode("utf-8")
|
||
|
print(" > %s"%r)
|
||
|
time.sleep(5)
|
||
|
print("Connected!")
|
||
|
|
||
|
# Start main loop
|
||
|
while True:
|
||
|
print("\n# Please enter a command ('help' for command list)")
|
||
|
cmd = input()
|
||
|
|
||
|
if cmd == "help":
|
||
|
# List all commands
|
||
|
print("\nCOMMAND\t\tACTION")
|
||
|
for k in COMMANDS.keys():
|
||
|
print("%s\t\t%s"%(k, COMMANDS[k]))
|
||
|
|
||
|
elif cmd == "home":
|
||
|
# Home machine
|
||
|
send("G28", silent=True) #Auto-home
|
||
|
send("M84", silent=True) #Disable steppers
|
||
|
|
||
|
elif cmd == "raw":
|
||
|
# Send raw gcode commands
|
||
|
print("Entering raw mode. Proceed at your own risk. Type 'exit' to exit raw mode.")
|
||
|
|
||
|
while True:
|
||
|
print("\n# RAW: Please enter a gcode command (or 'exit' to exit)");
|
||
|
r = input()
|
||
|
if r == "exit":
|
||
|
break
|
||
|
send(r)
|
||
|
|
||
|
print("Exiting raw mode.")
|
||
|
|
||
|
elif cmd == "load":
|
||
|
# Load gcode file
|
||
|
print("# Please enter the path to the gcode file you want to load")
|
||
|
path = input()
|
||
|
|
||
|
# Try to open and parse file
|
||
|
try:
|
||
|
print("Trying to open file...")
|
||
|
f = open(path, "r")
|
||
|
print("Reading...")
|
||
|
gcodeRaw = f.read()[:-1]
|
||
|
f.close()
|
||
|
print("Parsing...")
|
||
|
gcodeLines = gcodeRaw.split("\n")
|
||
|
|
||
|
# Iterate through all lines to get info
|
||
|
gcodeLines = 0
|
||
|
for l in range(len(gcodeLines)):
|
||
|
line = gcodeLines[l]
|
||
|
|
||
|
if line[0] == ";":
|
||
|
# When comment
|
||
|
if line.startswith(";LAYER:"):
|
||
|
l = int(line[line.index(":") +1:])
|
||
|
if l > gcodeLayers:
|
||
|
gcodeLayers = l
|
||
|
|
||
|
print("#Layers: %d"%gcodeLayers)
|
||
|
print("%d lines"%len(gcodeLines))
|
||
|
|
||
|
print("File loaded! Use 'add' to manipulate this gcode.")
|
||
|
except Exception as e:
|
||
|
print("Couldn't read file:")
|
||
|
print(e)
|
||
|
|
||
|
# Reset gcode vars
|
||
|
gcodeRaw = ""
|
||
|
gcodeLines = []
|
||
|
gcodeLayers = 1
|
||
|
|
||
|
elif cmd == "add":
|
||
|
# Manipulate gcode
|
||
|
|
||
|
# Check if gcode present
|
||
|
if gcodeRaw == "" or len(gcodeLines) == 0:
|
||
|
print("No gcode loaded. Use 'load' to load gcode.")
|
||
|
continue
|
||
|
|
||
|
# Display all commands
|
||
|
print("COMMAND\t\tEXPLANATION")
|
||
|
print("exit\t\tReturn")
|
||
|
print("mesh\t\tEnable Mesh Bed Leveling")
|
||
|
print("filch\t\tFilament change at specific layer")
|
||
|
print("")
|
||
|
print("# Please enter one of the above actions.")
|
||
|
|
||
|
# Getting input
|
||
|
act = input()
|
||
|
|
||
|
if act == "exit":
|
||
|
print("Exiting.")
|
||
|
pass
|
||
|
|
||
|
elif act == "mesh":
|
||
|
# Add Mesh Bed Leveling
|
||
|
print("Recalculating Z with Mesh Bed Leveling...")
|
||
|
layerZ = 0
|
||
|
layerIndex = 0
|
||
|
for l in range(len(gcodeLines)):
|
||
|
line = gcodeLines[l]
|
||
|
|
||
|
# Check if comment
|
||
|
if line[0] == ";":
|
||
|
# Check if new layer
|
||
|
if line.startswith(";LAYER:"):
|
||
|
layerIndex = int(line[line.index(":") +1:])
|
||
|
|
||
|
if not (line.startswith("G0") or line.startswith("G1")):
|
||
|
continue
|
||
|
|
||
|
# When this is a movement line
|
||
|
tokens = line.split(" ")
|
||
|
indexX = -1
|
||
|
indexY = -1
|
||
|
indexZ = -1
|
||
|
for t in range(len(tokens)):
|
||
|
if tokens[t][0] == "X":
|
||
|
indexX = t
|
||
|
elif tokens[t][0] == "Y":
|
||
|
indexY = t
|
||
|
elif tokens[t][0] == "Z":
|
||
|
indexZ = t
|
||
|
|
||
|
# Check if Z is given
|
||
|
if not indexZ == -1:
|
||
|
# When Z is set -> save as layer height
|
||
|
layerZ = float(tokens[indexZ][1:])
|
||
|
|
||
|
# Add Z if X or Y are set
|
||
|
if indexX == -1 or indexY == -1:
|
||
|
continue
|
||
|
|
||
|
X = float(tokens[indexX][1:])
|
||
|
Y = float(tokens[indexY][1:])
|
||
|
Z = layerZ + MESH_OFFSET + (meshZ(X, Y)*max(0, float(10-layerIndex)/10.0) if layerIndex < 10 else 0)
|
||
|
|
||
|
# Add Z coordinate
|
||
|
tokens.append("Z{:.3f}".format(Z))
|
||
|
newLine = " ".join(tokens)
|
||
|
gcodeLines[l] = newLine
|
||
|
print("Done!")
|
||
|
|
||
|
elif act == "filch":
|
||
|
# Filament change at layer
|
||
|
layer = 0
|
||
|
while True:
|
||
|
# Ask for layer
|
||
|
print("# FILCH: At which layer do you want to change filament?")
|
||
|
l = input()
|
||
|
|
||
|
# Check if valid
|
||
|
try:
|
||
|
layer = int(l)
|
||
|
|
||
|
if layer > 0 and layer <= gcodeLayers:
|
||
|
break
|
||
|
|
||
|
print("Invalid.")
|
||
|
except:
|
||
|
print("Invalid.")
|
||
|
|
||
|
if layer == 0:
|
||
|
continue # exit
|
||
|
|
||
|
# Search for start of this layer
|
||
|
print("Searching for layer...")
|
||
|
success = False
|
||
|
for l in range(len(gcodeLines)):
|
||
|
line = gcodeLines[l]
|
||
|
|
||
|
if line[0] == ";":
|
||
|
if line.startswith(";LAYER:"):
|
||
|
thisLayer = int(line[line.find(":")+1:])
|
||
|
|
||
|
# Check if right layer
|
||
|
if thisLayer == layer:
|
||
|
# Add filch gcode
|
||
|
print("Adding gcode for filch...")
|
||
|
gcodeLines[l:l] = GCODE_FILCH_LAYER
|
||
|
success = True
|
||
|
break
|
||
|
print("Done." if success else "Couldn't find layer.")
|
||
|
|
||
|
else:
|
||
|
# When add action not found
|
||
|
print("Command not found.")
|
||
|
|
||
|
elif cmd == "print":
|
||
|
# Print loaded gcode
|
||
|
if len(gcodeLines) == 0:
|
||
|
print("No file loaded yet. Use 'load' to load gcode.")
|
||
|
else:
|
||
|
# Start print
|
||
|
printing = True
|
||
|
abortPrint = False
|
||
|
print("Print started. Type 'stop' at any time to abort the print.")
|
||
|
send("M75", silent=True) # Start timer
|
||
|
|
||
|
# Start thread to check for commands
|
||
|
threadInput = threading.Thread(target=threadPrintInput, daemon=True)
|
||
|
threadInput.start()
|
||
|
|
||
|
# Send gcode line by line to printer
|
||
|
for l in range(len(gcodeLines)):
|
||
|
line = gcodeLines[l]
|
||
|
|
||
|
# Check if needs to abort print
|
||
|
if abortPrint:
|
||
|
print("Aborting print!")
|
||
|
break
|
||
|
|
||
|
# Check if comment
|
||
|
if line[0] == ";":
|
||
|
# Check if new layer
|
||
|
if line.startswith(";LAYER:"):
|
||
|
layer = int(line[line.index(":") +1:])
|
||
|
print("Printing layer %d/%d"%(layer, gcodeLayers))
|
||
|
else:
|
||
|
# Send line to printer
|
||
|
send(line, silent=True)
|
||
|
|
||
|
# Stop input thread
|
||
|
printing = False
|
||
|
|
||
|
# Stop print
|
||
|
send("M104 S0", silent=True) # Hotend off
|
||
|
send("M140 S0", silent=True) # Bed off
|
||
|
send("M107", silent=True) # Fan off
|
||
|
send("M77", silent=True) # Stop timer
|
||
|
send("M31", silent=True) # Display time on lcd
|
||
|
send("G28 X Y", silent=True) # Home X and Y
|
||
|
send("G0 Y300 F1500", silent=True) # Move platform to user
|
||
|
send("M84", silent=True) # Disable steppers
|
||
|
send("M400", silent=True) # Wait for previous commands to finish
|
||
|
send("M300 P3000", silent=True) # Beep
|
||
|
print("Print done!")
|
||
|
|
||
|
elif cmd == "level":
|
||
|
# Normal leveling wizard
|
||
|
print("Starting leveling wizard...")
|
||
|
send("G28", silent=True) #Home
|
||
|
send("G0 F7500", silent=True) # Set feedrate
|
||
|
|
||
|
print("Please adjust the knob under the nozzle so that a piece of paper can slide under the nozzle with just a little bit of friction.")
|
||
|
|
||
|
# Start corner loop
|
||
|
i = 0
|
||
|
while True:
|
||
|
# Move to corner
|
||
|
print("Moving to corner %d..."%i)
|
||
|
send("G0 Z10", silent=True)
|
||
|
send("G0 %s"%levelingCornersPos[i], silent=True)
|
||
|
send("G0 Z0", silent=True)
|
||
|
send("M84", silent=True) #Disable steppers
|
||
|
|
||
|
# Prompt user
|
||
|
print("Press ENTER to move to next corner. Type 'exit' to exit wizard.")
|
||
|
r = input()
|
||
|
|
||
|
if r == "exit":
|
||
|
break
|
||
|
|
||
|
# Next corner
|
||
|
i = (i+1)%4
|
||
|
|
||
|
# End wizard
|
||
|
send("G0 F1500", silent=True) #Set slower feedrate
|
||
|
send("G28", silent=True) #Home
|
||
|
send("M84", silent=True) #Disable steppers
|
||
|
|
||
|
|
||
|
elif cmd == "mesh":
|
||
|
# Mesh leveling wizard
|
||
|
print("Starting mesh leveling wizard...")
|
||
|
send("G28", silent=True) #Home
|
||
|
send("G0 F7500", silent=True) # Set feedrate
|
||
|
|
||
|
print("\nSTEP 1: Normal leveling")
|
||
|
print("Please adjust the knob under the nozzle so that a piece of paper can slide under the nozzle with just a little bit of friction.")
|
||
|
|
||
|
# Start corner loop
|
||
|
i = 0
|
||
|
stop = False
|
||
|
while True:
|
||
|
# Move to corner
|
||
|
print("Moving to corner %d..."%i)
|
||
|
send("G0 Z10", silent=True)
|
||
|
send("G0 %s"%levelingCornersPos[i], silent=True)
|
||
|
send("G0 Z{:.2f}".format(MESH_OFFSET), silent=True)
|
||
|
|
||
|
# Prompt user
|
||
|
print("Press ENTER to move to next corner. Type 'next' to go to step 2. Type 'exit' to exit wizard.")
|
||
|
r = input()
|
||
|
|
||
|
if r == "exit":
|
||
|
stop = True
|
||
|
break
|
||
|
elif r == "next":
|
||
|
break
|
||
|
|
||
|
# Next corner
|
||
|
i = (i+1)%4
|
||
|
|
||
|
# Step 2: get mesh deltas
|
||
|
if not stop:
|
||
|
print("\nSTEP 2: Mesh leveling")
|
||
|
print("For every mesh point, lower the nozzle using the '+' and '-' buttons until the nozzle is at the right height. Then, enter 'next' to go to the next mesh point. Type 'exit' to exit the wizard.")
|
||
|
|
||
|
# Move through mesh
|
||
|
for y in range(MESH_SIZE):
|
||
|
for x in range(MESH_SIZE):
|
||
|
Z = 1.00 # Offset on top of MESH_OFFSET
|
||
|
print("\nMoving to mesh point x=%d, y=%d"%(x,y))
|
||
|
send("G0 Z10", silent=True)
|
||
|
send("G0 %s"%meshPositions[y][x], silent=True)
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
|
||
|
# Input loop
|
||
|
stop = False
|
||
|
while True:
|
||
|
i = input()
|
||
|
|
||
|
if i == "+":
|
||
|
Z += 0.01
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "-":
|
||
|
Z -= 0.01
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "++":
|
||
|
Z += 0.1
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "--":
|
||
|
Z -= 0.1
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "+++":
|
||
|
Z += 0.5
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "---":
|
||
|
Z -= 0.5
|
||
|
send("G0 Z{:.3f}".format(Z+MESH_OFFSET), silent=True)
|
||
|
elif i == "exit":
|
||
|
stop = True
|
||
|
break
|
||
|
elif i == "next":
|
||
|
mesh[y][x] = Z
|
||
|
print("Mesh value saved (Z={:.3f})".format(Z))
|
||
|
break
|
||
|
|
||
|
# Check if needs to exit
|
||
|
if stop:
|
||
|
break
|
||
|
# Check if needs to exit
|
||
|
if stop:
|
||
|
break
|
||
|
|
||
|
# Print mesh
|
||
|
print("\nCurrent Mesh:")
|
||
|
for i in mesh:
|
||
|
for j in i:
|
||
|
print("{:6.3f}".format(j), end=" ")
|
||
|
print()
|
||
|
|
||
|
|
||
|
# Save mesh to file
|
||
|
print("Writing mesh to file...")
|
||
|
try:
|
||
|
t = ""
|
||
|
t += "%d\n"%MESH_SIZE
|
||
|
for y in range(MESH_SIZE):
|
||
|
for x in range(MESH_SIZE):
|
||
|
t += "{:.3f}\n".format(mesh[y][x])
|
||
|
f = open("mesh.txt", "w")
|
||
|
f.write(t)
|
||
|
f.close()
|
||
|
except Exception as e:
|
||
|
print("Couldn't write mesh to file.")
|
||
|
print(e)
|
||
|
|
||
|
|
||
|
# End wizard
|
||
|
send("G0 F1500", silent=True) #Set slower feedrate
|
||
|
send("G28", silent=True) #Home
|
||
|
send("M84", silent=True) #Disable steppers
|
||
|
|
||
|
|
||
|
elif cmd == "exit":
|
||
|
break
|
||
|
else:
|
||
|
print("Command not found. Enter 'help' to list all commands.")
|
||
|
|
||
|
|
||
|
|
||
|
# Close serial
|
||
|
print("Disconnecting from printer...")
|
||
|
s.close()
|
||
|
|
||
|
# End of script
|
||
|
print("Thank you for using PrintBuddy!")
|