PrintBuddy/main.py

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!")