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)