4
\$\begingroup\$

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);
}

EDIT 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)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.