#!/usr/bin/env python3
# CNC_Index_Driver_2024.7.19BV2 with 1/32 microstepping
# Copyright ©2024 Dayton Taylor
import os # operating system functions for shutdown when finished
import sys # system functions to support exit option at end of process
import RPi.GPIO as GPIO # general purpose input output functions for stepper motor GPIO controls
import time # for time delay function
from datetime import datetime # module to get time and date for display (only works with
# an internet connection because stock Rasberry Pi doesn't have realtime clock)
#
# HR8825 stepper motor chipset driver
from HR8825 import HR8825 # chip-specific function module for Waveshare stepper motor HAT (B)
#
# GPIO functions for button control for e-paper display buttons
from gpiozero import Button # GPIO functions for button control for e-paper display buttons
from gpiozero import Buzzer # GPIO function module for audible system ready prompt
from gpiozero import TonalBuzzer # Buzzer module of tonal functions for audible system ready prompt
from gpiozero.tones import Tone # TonalBuzzer module supporting TonalBuzzer tone profiles
#
#import epd2in7 # function module to support Waveshare e-paper 2.7 inch black & white e-paper display driver
from waveshare_epd import epd2in7_V2
from PIL import Image, ImageDraw, ImageFont # display drawing functions to support drawing on e-paper display
#
from pygame import mixer # game design module to support audible system ready prompt when speakers are available
# the two lines below initiate the pygame .wav player for beep sound (beep prompt only works when external
# sound output device is available and selected in system sound preferences)
mixer.init()
alert=mixer.Sound('/home/pi/CNCIndexDriver/python/wave1.wav')
#b initiate TonalBuzzer module in gpiozero
b = TonalBuzzer(2) # set TonalBuzzer signal output pin to GPIO pin 2
# assign gpiozero support for e-paper display buttons to physical GPIO pins on Waveshare e-paper display
buttonA = Button(5)
buttonB = Button(6)
buttonC = Button(13)
buttonD = Button(19)
epd = epd2in7_V2.EPD() # use the epd2in7_V2 module to name the e-paper display 'epd'
epd.init() # initialize the display by referencing its name and the 'init' function
print("") # print a carriage return (blank line) to console (command line terminal window)
print("Clear e-paper display") # print text to indicate that the e-paper display is being cleared
print("") # print another blank line
epd.Clear() # clear the display by referencing its name and the 'clear' function
def printToDisplay(string): # define what happens when e-paper printToDisplay function is called
epdImage = Image.new('1', (epd2in7_V2.EPD_HEIGHT, epd2in7_V2.EPD_WIDTH), 255)
draw = ImageDraw.Draw(epdImage) # create draw object and pass the image layer epdImage
# set the font paths, file names, and sizes (Font.ttc is the GilSans TrueType font)
font = ImageFont.truetype('/home/pi/CNCIndexDriver/Font.ttc', 18)
fontsmall = ImageFont.truetype('/home/pi/CNCIndexDriver/Font.ttc', 12)
fontsupersmall = ImageFont.truetype('/home/pi/CNCIndexDriver/Font.ttc', 9)
draw.line((36, 23, 264, 23), fill=0) # draw top line
#
# The line below is the text on the display that changes step-by-step.
# The remaining elements below it are the same on every page.
# draw the page text (page1, page 2, page3 or page4) starting 42 pixels to the right and 2 pixels down
draw.text((42, 2), string, font=font, fill=0)
#
# draw the bottom section of the screen with copyright, date and time.
# assign the name 'now' to the current date and time
now = datetime.now()
# define the layout of the datetime string using the function 'strftime'
dt_string = now.strftime("%Y / %m / %d %H:%M:%S")
# strips the first two characters (20 from 2024) off the date time string
dt2_string = dt_string[2:]
#
draw.line((36, 158, 264, 158), fill=0) # Draw bottom line
# draw the © and Date Time String text at bottom
draw.text((44, 160), f"©2024 Dayton Taylor {dt2_string} ", font=fontsmall, fill=0)
# draw the left section of the screen with button descriptions
draw.text((2, 0), f""" A """, font=font, fill=0) # buttonA Text
draw.text((2, 18), f""" Start """, font=fontsupersmall, fill=0) # buttonA Text
draw.text((2, 28), f""" Process """, font=fontsupersmall, fill=0) # buttonA Text
draw.line((36, 0, 36, 176), fill=0) # vertical line to the right of button descriptions
draw.text((3, 45), f""" B """, font=font, fill=0) # buttonB Text
draw.text((2, 63), f""" Load """, font=fontsupersmall, fill=0) # buttonB Text
draw.text((2, 73), f"""Program """, font=fontsupersmall, fill=0) # buttonB Text
draw.line((0, 44, 36, 44), fill=0) # horizontal line under the buttonA description
draw.text((1, 89), f""" C """, font=font, fill=0) # buttonC Text
draw.text((2, 107), f""" Run """, font=fontsupersmall, fill=0) # buttonC Text
draw.text((2, 117), f"""Program """, font=fontsupersmall, fill=0) # buttonC Text
draw.line((0, 88, 36, 88), fill=0) # horizontal line under the buttonB description
draw.text((2, 133), f""" D """, font=font, fill=0) # buttonD Text
draw.text((2, 151), f""" About """, font=fontsupersmall, fill=0) # buttonD Text
draw.text((2, 161), f""" Exit """, font=fontsupersmall, fill=0) # buttonD Text
draw.line((0, 132, 36, 132), fill=0) # horizontal line under the buttonC description
#this function sends the image that has been created above (epdImage)to the display
epd.display(epd.getbuffer(epdImage))
time.sleep(2)
def handleBtnPress(btn):
# switcher number represents the GPIO button pin number
# value is the message it will print - variables page1 - page4 will be defined below
switcher = {
5: f"{page1}",
6: f"{page2}",
13: f"{page3}",
19: f"{page4}"
}
# get the string based on the passed button and send it to printToDisplay()
msg = switcher.get(btn.pin.number, "Error")
printToDisplay(msg)
# tell the buttons what to do when pressed
buttonA.when_pressed = handleBtnPress
buttonB.when_pressed = handleBtnPress
buttonC.when_pressed = handleBtnPress
buttonD.when_pressed = handleBtnPress
# now that we've initiated the functions and defined the screen layout and button behavior
# we can begin the full program 'while True' loop so the process can be repeated multiple times
while True:
# The section below is the initial text for the screens that are called when you press a button.
#
# Note that this text is updated later in the script and can contain variables
# provided that they are initialized and are if the type 'string'
# the reason this text is inside of the first 'while True' loop is so that page3 is reverted back to its
# start condition if the process is restarted, which is one of the options at the end of the script
#
# Note also that page3 is initiated for a special condition where button C is pressed before
# n has been defined - it will be redefined to contain applicable variables
# after those variables are initiated
page1 = f"""A) CNC INDEX DRIVER
To start process:
Enable numlock on keypad
Press button 'B' on this unit
"""
page2 = f"""B) LOAD PROGRAM
Enter number of gear teeth
Wait for backlash reset
Wait for display prompt and
beep before cutting each slot
"""
page3 = f"""C) RUN PROGRAM
You may now cut gear
slot 1
When finished enter 1
(or 999 to return to
zero position)
"""
page4 = f"""D) ABOUT / EXIT
Enter 0 to shut down
Enter 1 to start over
Enter 2 to end the program
12V, 3A, Index Driver
HR8825 200 Steps/360 1.8º
1/32 Microsteps, 6400 Steps
"""
# this next major section of the script initiates the process by asking the user to input the number of gear
# teeth to be cut before dropping into the loop that will repeat for each position
#
# start the user interface - both the command line and the e-paper display begin guiding the user through
# the process
try:
print("Print 'Start Process' instructions to e-paper display")
print("")
printToDisplay(page1) # Print e-paper page1 contents to e-paper display.
#
# set up motor driver GPIO driver (this must be done initially
# and again each time e-paper printToDisplay has been called to toggle between GPIO control
# by the gpiozero module (which controls the display) and the RPi.GPIO module (which controls
# the stepper motor)
#
Motor2 = HR8825(dir_pin=24, step_pin=18, enable_pin=4, mode_pins=(21, 22, 27))
#
# this next section is a comment noting microstepping command syntax for the stepper motor
# this script is designed to use microstepping with no gearbox at 1/32step microstep resolution
# this is equivalent to 6400 steps per 360 degrees of rotation
#
"""
# 1.8 degree: nema23, nema14
# softward Control :
# 'fullstep': A cycle = 200 steps
# 'halfstep': A cycle = 200 * 2 steps
# '1/4step': A cycle = 200 * 4 steps
# '1/8step': A cycle = 200 * 8 steps
# '1/16step': A cycle = 200 * 16 steps
# '1/32step': A cycle = 200 * 32 steps
"""
#
# mirror e-paper Start Program page instructions to command line console.
print("To start process:")
print("")
print("Enable numlock on keypad if present")
print("")
print("Press button 'B' (Load Program) on controller")
print("")
buttonB.wait_for_press()
#
# get the number of gear teeth to be cut while also checking that the input is numeric
# check is because numeric USB keypads output non-numeric ASCII codes when numlock is inactive
# checking with "while True" loop allows user to keep trying until they get it right
# (for example if they forgot to enable numlock)
#
while True:
try:
t = input("Enter the number of gear teeth: ")
if t.isnumeric():
break
except:
print("Invalid input") # prints error to console if input is not numeric
# and then loops back so user can try again - if you're only using the e-paper
# display there is no error message but you can nevertheless try again
#
t = int(t) # make t an integer type variable as gear teeth can only be integers
a = 360.0/t # calculate the angle "a" per tooth by dividing 360 by the number of teeth
# save a rounded to 3 decimal places version of the variable "a" for use on the display
# so we're not displaying unnecessary / irrelevant decimal places
angle = round(a,3)
time.sleep(0.5) # pause for a half second
print("")
print(t,"gear teeth calculated")
print("")
print("Wait for backlash reset")
print("")
# 100 steps is approximately 5 degrees at the current 1/32 step microstepping ratio
print("Rotating 100 steps (~five degrees) to zero out any backlash in the gear train")
print("")
time.sleep(0.5)
# the next line uses gpiozero TonalBuzzer to play a tone on a hardware buzzer inside the controller
# this tone plays whenever the motor is moving as a reminder not to move the cutter while the motor
# is active
b.play(Tone(220.0)) # start playing a 220 Hz buzz audio prompt through gpiozero tonal buzzer hardware
Motor2.SetMicroStep('softward','1/32step') # set the motor mode to fullstep under software control
#
# reverse & then advance ~5 degrees to take up any backlash (slack) in the gear train.
#
# note that because the forward and reverse numbers cancel each other out this process
# can be repeated without skewing relative teeth positions for multiple cutting passes
# or to resume cutting after a temporary interruption (i.e. forgetting to cut a slot, or
# realizing that the slots all need to be deeper, or changing cutters, etc.)
#
# also note that if direct drive microstepping is used without a gearbox
# there should not be any backlash in the direct driving of the drawbar
# however, because of the snapping into position when any stepper motor is first powered on
# it's good to leave this bit in - it also allows you to check that the motor is controlling the spindle
# and that the spindle is moving freely before cutting the first slot
#
Motor2.TurnStep(Dir='backward', steps=100, stepdelay = 0.003)
time.sleep(0.5)
b.stop() # turn off the 220 Hz buzzer to indicate that the motor has stopped moving
time.sleep(0.25)
b.play(Tone(220.0)) # start playing a 220 Hz buzz audio prompt through gpiozero tonal buzzer hardware
Motor2.TurnStep(Dir='forward', steps=100, stepdelay = 0.003)
time.sleep(0.5)
b.stop() # turn off the 220 Hz buzzer to indicate that the motor has stopped moving
#
# set cummulative step register (totalsteps) to 0 before starting loop
# this is necessary to calculate the absolute position of the motor to compensate for
# rounding errors because the number of integer steps per gear slot cut can vary plus or
# minus one step as a result of rounding
# this variation is only 1/6400 of a 360 (0.0562 degrees), which is equal to 3.372 arc minutes
# one arc minute is 1/60th of a degree (0.016666...), or 1/21,600 of a full 360 circle
# 3.372 arc minutes is probably less error than the positional tolerance
# of the milling attachment spindle, however, for greater accuracy a gearbox with a further reduction ratio
# can be added to the system
totalsteps = 0 # total steps "register" (variable) starts at zero
n = 1 # initiate variable "n" ("n" for number of teeth slots cut)
#
# now that n has a value we can update page 3 to include that value as a variable that changes
# with each gear tooth slot cut
#
page3 = f"""C) RUN PROGRAM
{t} gear teeth calculated
{angle} degrees per tooth
You may now cut gear
slot {n} When finished
enter {n} (or 999 to
return to zero position) """
# next we have what I will call the minor while True loop that starts the gear slot cutting and then
# drops us into what I will call the major while true loop
#
# do the following while loop for the number of gear teeth (t) specified
# NOTE: we're calculating the next position in advance so that it can be displayed and the user can
# confirm that the data for the next position is correct before cutting the first position
# because of this the formulas that display the current position require subtracting the steps to the
# next position
#
while n <= t:
# the alert.play() line below and pygame import at beginning of the script play the audio prompt to
# the system sound device (HDMI out, USB speaker, headphone, etc) to indicate the next slot can be cut
# this is different than the buzzer audio which is to indicate that the motor is moving
#
alert.play() # play a brief system audio beep to signal that the gear slot can now be cut
#
# in the following print statement n is always 1 because after we pass this part of the script
# we drop into the major loop for the remainder of the slots
# the reason for this is so that we can drop out of the main loop before the last slot and advance
# back to the start position
#
print("You may now cut gear tooth slot",n)
print("")
try:
printToDisplay(page3)
except:
print("display error")
print("")
#
# calculate the total angle 'a' of the current position and subtract the n = 1 angle so the
# first cut starts at zero angle
a = 360.0/t*n-360.0/t
# print the current angle in degrees rounded to three decimal places to console
print("Angle: ",str(round(a,3)),"degrees")
# calculate current position in steps by multiplying steps-per-slot
# (6400/t) by current number of slots 'n'
absolute_position = (6400.0/t)*n
# print the current position as the current absolute position minus one gear tooth so the
# first cut starts at zero steps
print("Current position:",str(int(round(absolute_position-6400.0/t))),"steps of 6400 steps total")
# subtract totalsteps from absolute position to get newsteps
# this is where we use the non-zero absolute position to calculate how many new steps to advance
newsteps = int(round(absolute_position - totalsteps))
#
print("")
#
# now we drop into the major while True loop
#
while n < t: # until the last cut (n = t) repeat the following loop:
totalsteps = totalsteps + newsteps # accumulate the total steps with each pass through this loop
print("(next rotation will be",newsteps,"steps to angle",str(round(360.0/t*n,3)),"degrees)")
print(" ")
# do the following loop for the number of gear teeth (t) specified
# NOTE: this loop is for all slots subsequent to the first slot
while True:
try:
confirm = input("Enter completed slot number to continue (or '999' to return to zero): ")
print("")
if int(confirm) == int(n):
break
elif int(confirm) == 999:
n = t - 1
print("")
print("Motor will return to zero after the next cut position")
print("")
break
except:
print("Invalid input")
page3A = f"""C) RUN PROGRAM
Please wait while the motor
advances """
try:
printToDisplay(page3A)
except:
print("display error")
# the next line uses gpiozero TonalBuzzer to play an audio prompt on a hardware buzzer inside
# the controller
b.play(Tone(220.0)) # play audio prompt through gpiozero tonal buzzer hardware
print("Rotating to next cut position")
Motor2 = HR8825(dir_pin=24, step_pin=18, enable_pin=4, mode_pins=(21, 22, 27))
Motor2.TurnStep(Dir='forward', steps=newsteps, stepdelay = 0.003)
time.sleep(0.1)
last = str(n)
n = n + 1
page3 = f"""C) RUN PROGRAM
Slot {last} of {t} completed
You may now cut gear
slot {n}
When finished enter {n}
(or 999 to return to
zero position) """
time.sleep(0.1)
b.stop()
print(" ")
print("You may now cut gear tooth slot",n)
# the alert.play() line below and pygame import at beginning of script play audio prompts
# through system sound device (HDMI out, USB speaker, headphone, etc)
alert.play()
try:
printToDisplay(page3)
except:
print("display error")
a = 360.0/t*n-360.0/t
print("Angle: ",str(round(a,3)),"degrees")
absolute_position = (6400.0/t)*n
print("Current position:",str(int(round(absolute_position-6400.0/t))),"steps of 6400 steps total")
newsteps = int(absolute_position - totalsteps)
print("This is the final cut position")
print("The next calculated position is the zero position:",str(int(absolute_position)),"steps total")
print("")
while True:
try:
confirm = input("Enter completed slot number to continue: ")
if int(confirm) == int(n):
break
except:
print("Invalid input")
page3A = f"""C) RUN PROGRAM
Please wait while the motor
advances """
try:
printToDisplay(page3A)
except:
print("display error")
print(" ")
print("Rotating to initial cut position")
b.play(Tone(220.0)) # play audio prompt through gpiozero tonal buzzer hardware
Motor2 = HR8825(dir_pin=24, step_pin=18, enable_pin=4, mode_pins=(21, 22, 27))
Motor2.TurnStep(Dir='forward', steps=newsteps, stepdelay = 0.003)
b.stop()
alert.play()
n = n + 1
print("Completed index cycle for ",t,"gear teeth")
time.sleep(0.5)
a = 360.0/t
page3 = f"""C) PROGRAM ENDED
{t} gear teeth cut
{angle} degrees per tooth
Press button 'D' on this unit
to continue
"""
try:
printToDisplay(page3)
except:
print("display error")
time.sleep(1)
print("")
print("Press button 'D' on the controller to continue")
print("")
buttonD.wait_for_press()
try:
endloop = input("Enter 0 to shut down, 1 to start over, or 2 to end the program: ")
print("")
if int(endloop) == 0: # end program and power off
print("Clear e-paper display") # print to console to indicate that e-paper display is being cleared
epd.Clear() # clear the e-paper display
time.sleep(2)
print("\nMotor stop")
Motor2.Stop()
os.system('sudo poweroff')
exit()
break
elif int(endloop) == 1: # start over at the top of process
print("Clear e-paper display") # print to console to indicate that e-paper display is being cleared
print("")
epd.Clear() # clear the e-paper display
time.sleep(2)
elif int(endloop) == 2: # terminate the program
exit()
except:
exit()
except:
print("Clear e-paper display") # print to console to indicate that e-paper display is being cleared
epd.Clear() # clear the e-paper display
epd2in7_V2.epdconfig.module_exit
print("\nMotor stop")
Motor2.Stop()
exit()
|