CNC Index Driver
 

 

Cost

$125 for parts, software is free

 

Instructions

The CNC Index Driver consists of three logic components: a Rasberry Pi 3 B+, a 2.7 inch Waveshare e-paper HAT V2, and a Waveshare stepper motor HAT (B).

The software needed to get it working is free and can be downloaded from the links below.

Alternatively you can purchase a boot disk with the software preinstalled or a complete unit on the Shop page.

Steps:

(1) Set up the Rasberry Pi OS using the specific legacy version specified below. When you set up the Rasberry Pi OS I recommend using the user name "pi" so that the paths in the Python script and in the start-up file that runs the script automatically don't require changing.

(2) Install the required software from the links below and test to make sure both components are working.

(3) Create a folder in home/pi called CNCIndexDriver and consolidate the dependencies for both components into that folder along with my script below, the ClearScreen.py script and corresponding CS.desktop file, the true type font required by the display, and the file wave1.wav which is the audible alert tone. A graphic entitled Pi _OS_folder_structure.bmp showing the organization of the contents of this folder is included in the download.

(4) Add the line "@lxterminal -e python3 /home/pi/CNCIndexDriver/python/CNCIndexDriver_DaytonTaylor_2024.7.19BV2.py" to the text file "/etc/xdg/lxsession/LXDE-pi/autostart" for automatic startup when powered on (this allows the unit to be used without an external monitor).

 

Video

 

Links

Raspberry Pi OS (Legacy) with desktop System: 32-bit, Kernel version: 6.1, Debian version: 11 (bullseye). Scroll down to the "Legacy" section and install this specific version or it won't work!

https://www.raspberrypi.com/software/operating-systems/#raspberry-pi-os-32-bit

Waveshare e-paper HAT V2 software

https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_Manual#Working_With_Raspberry_Pi

Waveshare stepper motor HAT (B) software

https://www.waveshare.com/wiki/Stepper_Motor_HAT_(B)

Zip file containing my Python scripts, TrueType font, .wav file and desktop file

https://drive.google.com/file/d/1UIYApfidxroahrTF6hSLYjeD0p_vOcTf/view?usp=share_link

For links to supplies including brass and steel stock, gravers, sharpeners, and gear cutters see the Shop page.

 

Parts

The total parts cost for the small motor 8mm lathe set-up (not including shipping) is under US$ 125.

These links frequently break as vendor inventory changes. Please contact me if you encounter a broken link.

Where multiple variants are offered please take care to select the variant that matches my description.

 

Raspberry Pi 3 B+ (select Pi 3B plus variant)

https://s.click.aliexpress.com/e/_Dkc3OrH

 

Waveshare E-paper HAT V2

https://s.click.aliexpress.com/e/_DDMvex9

 

Waveshare stepper motor HAT (B)

https://s.click.aliexpress.com/e/_Dd6kwjV

 

ModMyPi Modular Case for Raspberry Pi 3 B+ (NOTE: in order to finish it nicely you'll need to mill out the window for the display and pushbuttons. This can be done with a CNC end mill on your watchmaker's lathe!)

https://www.welectron.com/ModMyPi-3-Modular-Raspberry-Pi-3-Case-Black

 

10mm spacer / size extension for ModMyPi Modular Case for Raspberry Pi 3 B+

https://www.welectron.com/ModMyPi-Modular-Spacer-10mm-Black-incl-Screw-Kit

 

End mill to cut openings in case (you'll need a 3.2mm collet for your lathe to use this) (select 3.175x1.0x4x38L variant)

https://s.click.aliexpress.com/e/_DFI5Wib

 

Numeric Keypad for data entry without keyboard

https://s.click.aliexpress.com/e/_DEpc4aF

 

Buzzer for audio prompts

https://s.click.aliexpress.com/e/_Dmi6XH1

 

Double Axis NEMA 17 stepper motor (select SZ variant)

https://s.click.aliexpress.com/e/_DDZrkJl

 

Handwheel to turn the motor by hand (select both the 5mm shaft variant for the motor and the 8mm variant if the handwheel on your drawbar is not removable)

https://s.click.aliexpress.com/e/_DEeYDVl

 

Z axis flexible 5mm to 8mm coupling to connect the stepper motor to the exterior of an 8mm drawbar. If your drawbar handwheel is not removable you will need to remove the permanent handle and replace it with an 8mm removable handwheel above or purchase a spare drawbar (select the 5x8mm variant)

https://s.click.aliexpress.com/e/_DFROWab

 

Z-Axis motor mount

https://s.click.aliexpress.com/e/_DFOVtmr

 

15mm x 20mm rectangular steel tube (select S15mmxS20mmxTHK2mm, 100mm)

https://s.click.aliexpress.com/e/_DeUnpVD

 

20mm clamp for 20mm B8 lathe beds

https://s.click.aliexpress.com/e/_DmbxTiF

 

20mm mounting bracket (modify to center motor shaft) (select 2020V-4.2 variant or whichever best suits your lathe)

https://s.click.aliexpress.com/e/_DeDQHU7

Extras

The items below are for larger lathes and / or higher precision positioning. I use these on my W12 lathe. They replace the NEMA17 stepper motor and related parts above (the five items directly above). You will need additional parts to couple the 14mm gearbox output to your drawbar (see the video about this project for details).

 

80:1 reduction gearbox for NEMA23 stepper motor (select 80 to 1 gear ratio)

https://s.click.aliexpress.com/e/_DlRa2CP

 

NEMA 23 stepper motor (select shaft diameter 6.35mm)

https://s.click.aliexpress.com/e/_DlYm7b1

 
 
 
 
 
 

6.35mm to 11mm sleeve adapter for coupling motor to gearbox (select 6.35mm to 11mm)

https://s.click.aliexpress.com/e/_DkladaB

 

expansion screw for coupling gearbox to draw bar

https://s.click.aliexpress.com/e/_DeJo9EX

Code

The Python script below is the program that turns the generic hardware above into an index driver for making watch gears or other watch parts (crowns, etc) that require sequential milling operations around the circumference of a circle. It's presented below so you can read it with syntax highlighting in the browser. You can also download it in the links section above, view it locally, and edit it. The download includes both microstepping and reduction gear versions of the script. For the highest possible tolerance both microstepping and a reduction gearbox can be combined. If you'd like to combine them simply use the gearbox version and enable microstepping following the example in the microstepping version below.

Logic

1. The main program initializes the e-paper display, clears it, and prints the initial instructions to the display.

2. It then sets up the stepper motor driver and waits for the user to press the "Load Program" button.

3. The user is prompted to enter the number of gear teeth (t), and the program calculates the angle per tooth (a) and the rounded angle (angle).

4. The program then resets the motor position to zero any backlash in the gear train by rotating the motor backward and forward by 100 steps, while playing a buzzer tone.

5. The program initializes the variables totalsteps and n, and prints the initial run program instructions to the display.

6. For each gear tooth slot (n), the program:

- Calculates the current angle (a) and absolute position.

- Prompts the user to confirm the completed slot number.

- If the confirmed slot number matches the current slot (n), it moves the motor to the next position, plays a buzzer tone, and updates the display.

- If the user enters 999, the program skips to the final cut position.

7. After the last slot is cut, the program moves the motor back to the initial cut position, plays a buzzer tone, and displays the program completion message.

8. The user is then prompted to choose whether to shut down, start over, or end the program.

Python

#!/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, CNC 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()