Justin
Justin

Reputation: 125

Python 3: UDP Packet send/receive in a Tkinter gui

So, I am able to easily write a small script to listen for UDP packets at a certain IP/Port, but I'm struggling implementing it into a Tkinter GUI.

Whenever I try using an infinite while True: loop triggered by a button, the gui application crashes. I did some further research and read a little about using delays, but I'm unable to get it to work properly. I've tried putting the while loop in the proxy function which calls the startreceiving function, but it too crashes the gui. The below code gets a gui up and running with my current issues.

The ultimate question: How can I get a button to trigger an event to begin sending packets, while still being able to accept button events to start and stop receiving packets?

import socket
import tkinter as tk
import tkinter.font as tkFont

UDP_IP = "127.0.0.1"
UDP_PORT = 5005
MESSAGE = b"Hello, world"

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

sock.bind((UDP_IP, UDP_PORT))

def startsending(run=True):
    while run is True:
        print("Sending Message.")
        sock.sendto(MESSAGE, (UDP_IP, UDP_PORT))

def startreceiving(run=True):
    while run is True:
        try:
            data, addr = sock.recvfrom(1024)
            print("received message:", data)
            print("from: ", addr)
        except OSError:
            break

class App(tk.Frame):
    STRIDE = 8
    DELAY = 100

    variables = []
    for i in range(10):
        variables.append(i)

    sensors = []
    for i in range(3):
        sensors.append(i)

    fields = []
    for i in range(len(sensors) * len(variables)):
        fields.append(i)

    def __init__(self, master=None):
        tk.Frame.__init__(self,master)
        self.grid()
        self.create_widgets()
        self.after(self.DELAY, self.update, self.DELAY)

    #---- Create the GUI Layout ----
    def create_widgets(self):
        self.btn_font = tkFont.Font(family="Helvetica", size=12, weight='bold')
        self.gui_buttons = []
        self.send_button = tk.Button(self,
                                     text = format("Begin Sending."),
                                     font = self.btn_font,
                                     relief = tk.RIDGE,
                                     pady = 4,
                                     command = self.send_message)
        self.send_button.grid(column=2, row=11)
        self.start_button = tk.Button(self,
                                 text = format("Begin Receiving."),
                                 font = self.btn_font,
                                 relief = tk.RIDGE,
                                 pady = 4,
                                 command = self.start_receiving)
        self.start_button.grid(column=3, row=11)
        self.stop_button = tk.Button(self,
                                     text = format("Stop Receiving."),
                                     font = self.btn_font,
                                     relief = tk.RIDGE,
                                     pady = 4,
                                     padx = 6,
                                     state='disabled',
                                     command = self.stop_receiving)

        self.stop_button.grid(column=3, row=12)
        x = 0
        y = 1
        for i, label in enumerate(self.variables):
            label = tk.Label(self,
                                text = format("Variable " + str(i)),
                                font = self.btn_font,
                                padx = 10)
            label.grid(column=x, row=y)
            y += 1

        x = 1
        y = 0
        for i, label in enumerate(self.sensors):
            sensor = tk.Label(self,
                                text = format("Sensor " + str(i)),
                                font = self.btn_font,
                                padx = 20,
                                relief = tk.RIDGE)
            sensor.grid(column=x, row=y)
            x += 1

        x = 1
        y = 1
        for i, field in enumerate(self.fields):
            field = tk.Entry(self,
                             width=10,
                             text=format("field val " + str(i)),
                             font=self.btn_font,
                             state='disabled')
            field.grid(column=x, row=y)
            y += 1
            if y > len(self.variables):
                y = 1
                x += 1

    #----Proxy to call the start receiving method using a delay and set the corresponding buttons to normal/disabled.
    def start_receiving(self):
        self.start_button.config(state='disabled')
        self.stop_button.config(state='normal')

        self.after(self.DELAY, startreceiving, self.DELAY)

    #----Proxy to call the stop receiving method using a delay and set the corresponding buttons to normal/disabled.
    def stop_receiving(self):
        self.stop_button.config(state='disabled')
        self.start_button.config(state='normal')

        self.after(self.DELAY, startreceiving(False), self.DELAY)
        self.after(self.DELAY, startsending(False), self.DELAY)

    #----Proxy to call the start sending method using a delay.
    def send_message(self):
        self.after(self.DELAY, startsending, self.DELAY)

app = App()
app.master.title('ESDR')
app.master.geometry('640x480')
app.mainloop()

Upvotes: 1

Views: 2762

Answers (2)

Gil Hamilton
Gil Hamilton

Reputation: 12357

You have several problems here:

  1. The after function isn't being called correctly. One example: self.after(self.DELAY, startreceiving(False), self.DELAY). First of all -- it's calling startreceiving immediately which is not what you want. Second, the 3rd and subsequent arguments to after are provided as arguments to the callback function. So you're sending self.DELAY as an argument to startreceiving but that argument should be a boolean as you have it coded.

  2. An after function shouldn't enter an infinite loop as that steals control from tkinter. Instead (as @scotty3785 pointed out), you should either create a new thread for the operation, or make the after callback short and have it "reschedule" itself afterward.

As a fun learning exercise for myself, I reworked your code with a thread each for sender and receiver. Included some annotations in the comments.

from threading import Thread
import time
import socket
import select
import tkinter as tk
import tkinter.font as tkFont

UDP_IP = "127.0.0.1"
UDP_PORT = 5005

class Sender(Thread):
    MESSAGE = b"Hello, world"
    def __init__(self, sock):
        # Call Thread constructor
        super().__init__()
        self.sock = sock
        self.keep_running = True

    def stop(self):
        # Call this from another thread to stop the sender
        self.keep_running = False

    def run(self):
        # This will run when you call .start method
        while self.keep_running:
            print("Sending Message.")
            try:
                self.sock.sendto(self.MESSAGE, (UDP_IP, UDP_PORT))
                time.sleep(0.5) # REMOVE ME: Just to slow things down a bit for debugging
            except socket.error as err:
                print("Error from sending socket {}".format(err))
                break


class Receiver(Thread):
    def __init__(self, sock):
        # Call Thread constructor
        super().__init__()
        self.sock = sock
        self.keep_running = True

    def stop(self):
        # Call this from another thread to stop the receiver
        self.keep_running = False

    def run(self):
        # This will run when you call .start method
        while self.keep_running:
            # We use select here so that we are not *hung* forever in recvfrom.
            # We'll wake up every .5 seconds to check whether we should keep running
            rfds, _wfds, _xfds = select.select([self.sock], [], [], 0.5)
            if self.sock in rfds:
                try:
                    data, addr = self.sock.recvfrom(1024)
                    print("received message:", data)
                    print("from: ", addr)
                except socket.error as err:
                    print("Error from receiving socket {}".format(err))
                    break


class App(tk.Frame):
    STRIDE = 8
    DELAY = 100

    # pythonic list comprehensions equivalent to your previous loops
    variables = [i for i in range(10)]
    sensors = [i for i in range(3)]
    fields = [i for i in range(len(sensors) * len(variables))]

    def __init__(self, sock, master=None):
        # Call superclass constructor
        super().__init__(master)
        self.sock = sock
        self.sender = None
        self.receiver = None
        self.grid()
        self.create_widgets()
        self.update()

    #---- Create the GUI Layout ----
    def create_widgets(self):
        self.btn_font = tkFont.Font(family="Helvetica", size=12, weight='bold')
        self.gui_buttons = []
        # Buttons renamed for orthogonality
        self.sstart_button = tk.Button(self,
                                     text = format("Begin Sending."),
                                     font = self.btn_font,
                                     relief = tk.RIDGE,
                                     pady = 4,
                                     command = self.start_sending)
        self.sstart_button.grid(column=2, row=11)

        # Adding a stop button for the sender too
        self.sstop_button = tk.Button(self,
                                     text = format("Stop Sending."),
                                     font = self.btn_font,
                                     relief = tk.RIDGE,
                                     pady = 4,
                                     padx = 6,
                                     state='disabled',
                                     command = self.stop_sending)

        self.sstop_button.grid(column=2, row=12)

        self.rstart_button = tk.Button(self,
                                 text = format("Begin Receiving."),
                                 font = self.btn_font,
                                 relief = tk.RIDGE,
                                 pady = 4,
                                 command = self.start_receiving)
        self.rstart_button.grid(column=3, row=11)
        self.rstop_button = tk.Button(self,
                                     text = format("Stop Receiving."),
                                     font = self.btn_font,
                                     relief = tk.RIDGE,
                                     pady = 4,
                                     padx = 6,
                                     state='disabled',
                                     command = self.stop_receiving)

        self.rstop_button.grid(column=3, row=12)
        x = 0
        y = 1
        for i, label in enumerate(self.variables):
            label = tk.Label(self,
                                text = format("Variable " + str(i)),
                                font = self.btn_font,
                                padx = 10)
            label.grid(column=x, row=y)
            y += 1

        x = 1
        y = 0
        for i, label in enumerate(self.sensors):
            sensor = tk.Label(self,
                                text = format("Sensor " + str(i)),
                                font = self.btn_font,
                                padx = 20,
                                relief = tk.RIDGE)
            sensor.grid(column=x, row=y)
            x += 1

        x = 1
        y = 1
        for i, field in enumerate(self.fields):
            field = tk.Entry(self,
                             width=10,
                             text=format("field val " + str(i)),
                             font=self.btn_font,
                             state='disabled')
            field.grid(column=x, row=y)
            y += 1
            if y > len(self.variables):
                y = 1
                x += 1

    def mainloop(self, *args):
        # Overriding mainloop so that we can do cleanup of our threads
        # *If* any arguments were provided, we would pass them on to Tk.frame
        super().mainloop(*args)

        # When main loop finishes, shutdown sender and/or receiver if necessary
        if self.sender:
            self.sender.stop()
        if self.receiver:
            self.receiver.stop()


    #----Start the receiver thread
    def start_receiving(self):
        self.rstart_button.config(state='disabled')
        self.rstop_button.config(state='normal')
        # Create and start receiver thread
        self.receiver = Receiver(self.sock)
        self.receiver.start()

    #----Stop the receiver
    def stop_receiving(self):
        self.rstop_button.config(state='disabled')
        self.rstart_button.config(state='normal')
        self.receiver.stop()
        self.receiver.join()
        self.receiver = None

    #----Start the sender thread
    def start_sending(self):
        self.sstart_button.config(state='disabled')
        self.sstop_button.config(state='normal')
        self.sender = Sender(self.sock)
        self.sender.start()

    #----Stop the sender
    def stop_sending(self):
        self.sstop_button.config(state='disabled')
        self.sstart_button.config(state='normal')
        self.sender.stop()
        self.sender.join()
        self.sender = None

def main():
    # Got rid of sock as global variable
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((UDP_IP, UDP_PORT))
    app = App(sock)
    app.master.title('ESDR')
    app.master.geometry('640x480')
    app.mainloop()

if __name__ == '__main__':
    main()

Upvotes: 1

scotty3785
scotty3785

Reputation: 7006

Follow on from my comments on your question. Here is a quick example of how I might approach the problems (I'd usually do more OOP but this is a quick example).

I'd use the tkinter .after method to schedule my send/receive functions to be run periodically.

import tkinter as tk

sending_enabled = False

def send_message():
    if sending_enabled:
        print("Sending Message")
        root.after(500,send_message)

def receive_messages():
    print("Getting Messages")
    root.after(1000,recieve_messages)


def start_sending():
    global sending_enabled
    if not sending_enabled:
        root.after(500,send_message)
        sending_enabled = True

def stop_sending():
    global sending_enabled
    sending_enabled = False


root = tk.Tk()

startButton = tk.Button(root,text="Start",command=start_sending)
startButton.grid()
stopButton = tk.Button(root,text="Stop",command=stop_sending)
stopButton.grid()
root.after(1000,receive_messages)

root.mainloop()

The receive_message function is scheduled to first be run 1000ms after the program starts and then will call itself every 1000ms

The send_message function is first scheduled to run 1000ms after the start button is pressed. It will then continue to call itself until the sending_enabled flag is set to false by the stop_sending function.

Note that neither the send or receive functions have while loops in them.

Upvotes: 1

Related Questions