I made an etch-a-sketch project on a Raspberry Pi 4b, using python3, with turtle. Turning rotary encoders, you draw a picture. The project works, but over time, the lines being drawn lag behind making it increasingly difficult to control what it is you want to draw. This problem seems to be exacerbated when in full-screen mode.
I've tried for the last few days to fix the issue, but its evident I need help. Even with Firefox open and the program running, I'm only using 750MB ram, at 14% load.
Maybe there is something obvious I am missing, please look at my code:
#!/usr/bin/python3
import RPi.GPIO as GPIO
import turtle
# Setup GPIO
clearBtn = 12
liftBtn = 16
ColorBtn = 20
# GPIO pins for the encoders
encoder1_pins = (24, 23) # A and B pins for encoder 1 (x-axis)
encoder2_pins = (14, 15) # A and B pins for encoder 2 (y-axis)
GPIO.setmode(GPIO.BCM)
GPIO.setup(clearBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(liftBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(ColorBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder1_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder1_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder2_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder2_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Variables to store encoder state
encoder1_last_state = (GPIO.input(encoder1_pins[0]) << 1) | GPIO.input(encoder1_pins[1])
encoder2_last_state = (GPIO.input(encoder2_pins[0]) << 1) | GPIO.input(encoder2_pins[1])
# Encoder positions
x_coord = 0
y_coord = 0
penState = False # False for pen down, True for pen up
i = 0 # index of current color
# Set up the turtle screen
screen = turtle.Screen()
#screen.setup(width=1.0, height=1.0) # Set the window to full-screen mode
canvas = screen.getcanvas()
# Create a RawTurtle object with the canvas
raw_turtle = turtle.RawTurtle(canvas)
raw_turtle.shape('circle')
raw_turtle.shapesize(0.5) # Cursor size: 0.5 is half of normal
raw_turtle.width(6) # Line width
raw_turtle.speed(0)
#BUTTON FUNCTIONS
#toggle though colors
def Change_Color(channel):
global i
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 1, 1), (0, 0, 0)] # List of RGB colors
raw_turtle.pencolor(colors[i % len(colors)]) # Set pen color based on the current index of the length of the color list
i += 1
def ClearScrnBtn(channel):
raw_turtle.reset() # Reset the turtle, resets everything including point size
raw_turtle.shapesize(0.5) # Cursor size: 0.5 is half of normal
raw_turtle.width(6)
# Callback function to lift or lower the pen
def lift_pen(channel):
global penState
penState = not penState
if penState:
raw_turtle.penup()
else:
raw_turtle.pendown()
# Callback function for encoder 1 (x-axis)
def encoder1_callback(channel):
global encoder1_last_state, x_coord
a = GPIO.input(encoder1_pins[0])
b = GPIO.input(encoder1_pins[1])
new_state = (a << 1) | b
if (encoder1_last_state == 0b00 and new_state == 0b10) or (encoder1_last_state == 0b11 and new_state == 0b01):
x_coord += 5
elif (encoder1_last_state == 0b10 and new_state == 0b00) or (encoder1_last_state == 0b01 and new_state == 0b11):
x_coord -= 5
encoder1_last_state = new_state
# Callback function for encoder 2 (y-axis)
def encoder2_callback(channel):
global encoder2_last_state, y_coord
a = GPIO.input(encoder2_pins[0])
b = GPIO.input(encoder2_pins[1])
new_state = (a << 1) | b
if (encoder2_last_state == 0b00 and new_state == 0b10) or (encoder2_last_state == 0b11 and new_state == 0b01):
y_coord -= 5
elif (encoder2_last_state == 0b10 and new_state == 0b00) or (encoder2_last_state == 0b01 and new_state == 0b11):
y_coord += 5
encoder2_last_state = new_state
# Add event detection for encoder 1 (x-axis)
GPIO.add_event_detect(encoder1_pins[0], GPIO.BOTH, callback=encoder1_callback)
GPIO.add_event_detect(encoder1_pins[1], GPIO.BOTH, callback=encoder1_callback)
# Add event detection for encoder 2 (y-axis)
GPIO.add_event_detect(encoder2_pins[0], GPIO.BOTH, callback=encoder2_callback)
GPIO.add_event_detect(encoder2_pins[1], GPIO.BOTH, callback=encoder2_callback)
# Add event listener to the button pins
GPIO.add_event_detect(clearBtn, GPIO.FALLING, callback=ClearScrnBtn, bouncetime=300)
GPIO.add_event_detect(liftBtn, GPIO.FALLING, callback=lift_pen, bouncetime=300)
GPIO.add_event_detect(ColorBtn, GPIO.FALLING, callback=Change_Color, bouncetime=300)
# Function to update the turtle's position based on encoder callbacks
def update_position():
global x_coord, y_coord
screen.title(f"X: {x_coord}, Y: {y_coord}")
raw_turtle.goto(x_coord, y_coord)
screen.ontimer(update_position, 100) # Schedule the next update
# Start updating the position
update_position()
# Start the turtle main loop
turtle.mainloop()
# Clean up GPIO on keyboard interrupt
GPIO.cleanup()
#turtle.done() #not mentioned in turtle documentation for raw
I've tried adjusting the screen.ontimer() to poll more or less often. I think this is the issue, but I'm not sure how to fix it.
Ive tried using turtle functions in place of global variables, such as isdown() for the penup() pendown() function. I don't think this is a meaningful change in performance, and it lead to additional strange behavior, which I did not want to include in this question.
I have tried adjusting the rate at which the screen updating takes place, to try and limit cpu load. I've been reading the documentation for turtle methods, but haven't found anything which corrects the issue. I've tried running the code using $python3 ./prog.py 2> error_log.txt
no errors are generated.
I've tried to make the functions as efficient as possible, and limit use of global variables.