At the outset, I am a beginner at Python, having started out about 2 weeks back.
Aim- To create a GUI program, that can adequately work as a buzzer-management system. A buzzer, like those used in quizzes, should display who pressed the switch first, and also, who is currently pressing their switch.
Arduino part- With the current setup, the Arduino collects information on the switches and communicates that over the serial. It also has the information on a master switch, pressing which will reset the program, and then the one who presses first will be the winner until the next reset. The Arduino sketch used to test the python program is below. So basically, the Arduino keeps sending data like 000000;0\r\n
where each of the first six digits corresponds to whether the corresponding switch has been pressed, and the number after the semicolon is the number of the participant who pressed first in the session.
Main Program - I have added the comments in the code to try and clarify the purpose of the stuff there. In general, I am using background threads to continually read the serial, and update the GUI.
from tkinter import *
from tkinter import simpledialog
from threading import *
from serial import *
# Reads the serial constantly and updates the global variables pressed_status[], won and who_won in real time
def serial_handler():
global pressed_status
global who_won
global won
global arduino
global players
while not kill:
curr = str(arduino.readline(), encoding='UTF-8')
for i in range(no_of_players):
pressed_status[i] = int(curr[i])
who_won = int(curr[7]) # Indexes can be improved to include more participants by using a string split before
if won == True and who_won == 0:
reset_function()
# resets global who, deletes existing rectangles (to mark the first_pressed) and resets the canvases
def reset_function():
global won
won = False
global players
global canvasnames
for i in range(no_of_players):
players[i].delete('all')
canvasnames = []
for i in range(no_of_players):
c = players[i].create_text((players[i].winfo_width() / 2), (players[i].winfo_height() / 2),
font = ("Calibri", 20), text = playernames[i].get())
canvasnames.append(c)
# To ensure all the threads quit on the main window being closed. Kill is the flag checked by all threads (except
# daemon threads) to continue.
def quit_func():
global kill
kill = True
time.sleep(1)
root.destroy()
# Updates the canvas background and the first-pressed rectangle in real time. Will also handle the status text.
def main_constant_update():
global players
global won
while not kill:
for i in range(no_of_players):
players[i].config(bg=status_colors[pressed_status[i]])
if won == False and who_won != 0:
won = True # Why putting this line at the end make this thing work in a bullshit manner?
main_won_update(int(who_won))
adjust_playerscores(0,par=1)
stat_display.config(state=NORMAL)
stat_display.delete(1.0, END)
stat_display.insert(END, "Statistics for the current session\n\n")
a = ""
for i in range(no_of_players):
a += (playernames[i].get() + "\t\t\t\t" + str(playerscores[i]) + "\n")
stat_display.insert(END, a)
stat_display.config(state=DISABLED)
time.sleep(0.2)
def adjust_playerscores(event,par):
global playerscores
if par == 1:
playerscores[who_won-1] += 1
if par == 0:
playerscores = []
for i in range(no_of_players):
playerscores.append(0)
# To create the rectangle to indicate the first-pressed.
def main_won_update(who):
global players
global rectindex
rectindex = players[who - 1].create_rectangle(21, 21, players[who - 1].winfo_width() - 21,
players[who - 1].winfo_height() - 21,
width=20, outline="red")
# Updates the names in the global variable playernames[]
def update_names(event, which):
global playernames
global howmany
currentname = entrybox.get()
if currentname == '':
currentname = ("Player" + str(which + 1))
entrybox.delete(0, END)
playernames[which].set(value=currentname)
howmany = (which + 1)
# Alters the global answer flag to let the update_names function continue.
def answered(event):
global answer
answer = True
# Called once at the beginning. Can be called again. Changes the names of the players. Also creates the text in
# canvases the first time.
def namesetup():
time.sleep(3)
global kill
global first
global namechanging
namechanging = True
global howmany
howmany = 0
global answer
answer = False
global players
global canvasnames
global playernames
maintext.set(value="Do you want to change the names of the players?(Y/N)")
func = entrybox.bind('<Return>', answered)
entrybox.focus_get()
while not answer:
time.sleep(1)
entrybox.unbind('<Return>', func)
if entrybox.get() == 'Y':
entrybox.delete(0, END)
func = entrybox.bind('<Return>', lambda event: update_names(event, howmany)) # Change this and check once
while howmany <= (no_of_players - 1):
maintext.set(value=("Enter the name of Player" + " " + str(howmany + 1)))
time.sleep(0.5)
entrybox.unbind('<Return>', func)
maintext.set("Player Names Updated")
time.sleep(3)
namechanging = False
else:
entrybox.delete(0, END)
namechanging = False
if first:
for i in range(no_of_players):
c = players[i].create_text((players[i].winfo_width() / 2), (players[i].winfo_height() / 2),
font=("Calibri", 20), text=playernames[i].get())
canvasnames.append(c)
first = False
tupdate = Thread(target=main_constant_update)
tupdate.start()
arduino.flushInput()
tserial = Thread(target=serial_handler)
tserial.start()
for i in range(no_of_players):
players[i].itemconfig(canvasnames[i], text=playernames[i].get())
# Handles the main text in the input frame in real time, indicating the current status of the game.
def maintexthandler():
global won
global namechanging
global who_won
global kill
global reaction
global playerscores
time.sleep(10)
while not kill:
if namechanging == False and won == False and reaction == False:
maintext.set(value="Ready To Play!!")
elif namechanging == False and won == True and reaction == False:
maintext.set(value=(playernames[who_won - 1].get() + " " + "pressed first!!"))
if reaction:
maintext.set(value = "Rection Mode is on. Do not press any button. Wait for the Stat area to turn pink") # This is for a reaction-times mode I am planning to incorporate soon
time.sleep(1)
def re_change(event):
Thread(target = namesetup, daemon=True).start()
playerscores = []
reaction = False
rectindex = 0 # Since instead of canvas.delete('all') in the reset_function, we can only delete this rectangle.
arduino = Serial('COM3', 9600)
status_colors = ['white', 'orange'] # To allow a "0" in the serial to give a white bg, and "1" an orange bg.
pressed_status = [0, 0, 0, 0, 0, 0] # Global variable handling the real-time status of the pressed switch
howmany = 0 # global variable used while counting how many player names updated.
won = False # global variable indicating if this round has been "won", i.e. someone has become the first-to-press
namechanging = False # For the maintexthandler() to know when not to overide the message in the input frame
who_won = 0 # The player who was the first-to-press
answer = False # bool to allow the namesetup() to continue
canvasnames = [] # holds the id of the canvas text objects
first = True # bool telling the namesetup() whether it is the first run of the program
kill = False # bool allowing threads to quit if the main window closed
playernames = [] # Holds the current names of the players
players = [] # Holds the corresponding canvas objects
root = Tk()
maintext = StringVar(value="Welcome!")
no_of_players = simpledialog.askinteger(prompt="Enter Here:", title="Number of Players")
for i in range(no_of_players):
playerscores.append(0)
input_frame = Frame(root, bg="black", borderwidth=2, relief=SUNKEN)
input_frame.pack(fill=X)
entrybox = Entry(input_frame, width=15, font=("Calibri", 30))
entrybox.pack(side=RIGHT, padx=12, pady=12)
label_maintext = Label(input_frame, textvariable=maintext, justify=CENTER, bg='black', fg='gray',
font=('Lucida console', 15), wraplength=1000)
label_maintext.pack(expand=True, fill=X)
statistics_frame = Frame(root, bg='white')
statistics_frame.pack(fill=BOTH, expand=True)
status_frame = Frame(root, bg='pink', height=500)
status_frame.pack(fill=X)
control_panel = Frame(statistics_frame, width=350)
control_panel.pack(side=RIGHT, fill=Y, expand=True)
Label(control_panel, text ='Control Panel', bg="light blue", font=("Times New Roman", 20)).pack(expand=True, fill=BOTH, side=TOP)
name_change_button = Button(control_panel, text = "Change the player names")
name_change_button.pack(fill=BOTH, expand=True, side=TOP)
name_change_button.bind("<Button-1>", re_change)
stat_reset_button = Button(control_panel, text = "Reset the statistics")
stat_reset_button.bind("<Button-1>", lambda event: adjust_playerscores(event, 0))
stat_reset_button.pack(fill=BOTH, expand=True, side=TOP)
stat_display = Text(statistics_frame, bg='gray', font=('Lucida console', 15))
stat_display.pack(fill=BOTH, expand=True, side=RIGHT)
for i in range(no_of_players): # Kept a StringVar since initially planned to use on a label, rather than a canvas_text
a = StringVar(value=("Player" + str(i + 1)))
playernames.append(a)
for i in range(no_of_players):
c = Canvas(status_frame, width=1, height=200, bg="white", bd=5)
players.append(c)
players[i].pack(side=LEFT, fill=X, expand=True)
tname = Thread(target=namesetup, daemon=True) # So that if the user quits while this is running, it shuts down.
tname.start()
tmain = Thread(target=maintexthandler)
tmain.start()
root.protocol("WM_DELETE_WINDOW", quit_func)
root.mainloop()
Places where I think the code can be improved:
- Efficiency. Currently, the threads-intercommunication and general updating are very patchy using a lot of variables. I am not sure if all those are necessary.
- Structure. Often, the program raises several exceptions, especially during startup, and I am assuming this is because of how the threads are handled.
- General improvements. Since I am not conversant with using classes. I believe using them, the program can be simplified.
- Faster. Currently, a lot of updating is limited in its speed. Is there a way to make the code faster, and hence a better resolution of the updating, without owerworking the CPU?
- To minimize Tkinter interaction by all threads apart from the main since Tkinter is not unequivocally thread-safe.
To-be-added functionalities
Ability to reset from the program. I am thinking of adding a reset button in the control panel, which sends a
0
over the serial. The Arduino will be preprogrammed to consider that as a reset.Highly needed. A reaction-times mode, wherein the GUI gives a visual signal, (for example, bg of the text widget turning red), and then all of the participants press. The moment of the signal is sent to the arduino, and arduino returns the reaction times of the individuals. Currently, I am thinking of adding another thread on the button being pressed, and all other threads wait for this thread to be executed (
thread.join()
) and this thread sends and reads the data into proper variables which can be displayed in the stat_frame. But I don't have an easier alternative.Any other suggestions are welcome.
Arduino Reference code- I used the following to test the application, since the actual setup is not yet ready. Let me know if I should post the sketch I am actually planning to use also.
void setup() {
Serial.begin(9600);
Serial.read();
delay(15000);
}
void loop() {
delay(100);
Serial.println("000000;0");
delay(1000);
Serial.println("100000;1");
delay(1000);
Serial.println("001000;1");
delay(1000);
Serial.println("110000;1");
delay(1000);
Serial.println("000000;0");
delay(1000);
Serial.println("011100;2");
delay(1000);
Serial.println("000000;0");
delay(1000);
Serial.println("110000;1");
delay(1000);
Serial.println("000000;0");
delay(1000);
Serial.println("001000;3");
delay(1000);
Serial.println("000000;0");
delay(1000);
Serial.println("110100;4");
delay(1000);
Serial.println("000000;0");
delay(1000);
Serial.println("100000;1");
delay(1000);
Serial.println("110000;2");
delay(1000);
Serial.println("000000;2");
delay(1000);
}
I added the following code so that we need not mention what com
is the Arduino plugged into.
com = str(subprocess.check_output(['python','-m','serial.tools.list_ports']), encoding='UTF-8')
if (com == ''):
root = Tk()
F = Frame(root, width=500, height=500, bg='black')
l = Label(F, width=30, height = 5, bg='black', fg='gray', wraplength=400,
font=('Lucida Console', 20), text='Could not find '
'open ports. First plug in the Arduino and then reopen the program.')
l2 = Label(F, width=50, height = 10, bg='black', fg='gray', wraplength=200, font=('Lucida Console',10),
text='In case the arduino is plugged in, please check the connection.')
F.pack()
l.pack()
l2.pack()
root.mainloop()
quit(1)
comf = 'COM' + com[3]
arduino = Serial(comf, 9600)
1 Answer 1
Documentation
The PEP 8 style guide recommends adding docstrings for functions. You can convert the comments into docstrings. For example:
def serial_handler():
"""
Reads the serial constantly and updates the global variables pressed_status[],
won and who_won in real time
"""
Comments
Long lines make it harder to understand the code:
maintext.set(value = "Rection Mode is on. Do not press any button. Wait for the Stat area to turn pink") # This is for a reaction-times mode I am planning to incorporate soon
The line above should be split into 2 lines with the comment above the line of code:
# This is for a reaction-times mode I am planning to incorporate soon
maintext.set(value = "Rection Mode is on. Do not press any button. Wait for the Stat area to turn pink")
Globals
Using too many global variables makes the code hard to understand and maintain.
The number of global variables can be reduced by passing more inputs into functions
and by using return
to pass values out of functions.
Imports
The following line unnecessarily imports many unused items:
from tkinter import *
It is common to use the following:
import tkinter as tk
This requires you to prefix everything from tkinter
with tk
, such as:
root = tk.Tk()
name_change_button = tk.Button(control_panel, text = "Change the player names")
However, the benefit is that the code is more self-documenting in the sense
that we don't need to guess where Button
came from.
The same is true for the other *
imports.
Naming
PEP 8 recommends snake_case for function and variable names. For example,
def namesetup():
is better as :
def name_setup():
And, canvasnames
is better as canvas_names
.
Simpler
There is no need to compare a variable with True
:
if won == True and who_won == 0:
This is simpler:
if won and who_won == 0:
The same is true for other comparisons to True
and False
.
C code
For the "Arduino Reference code", add block comments to document the purpose of the code.
The code should be indented properly:
void setup() {
Serial.begin(9600);
Serial.read();
delay(15000);
}
In the loop
function, the repetition can be reduced by adding a new function
to call these 2 functions:
Serial.println("000000;0");
delay(1000);
Explore related questions
See similar questions with these tags.