Reputation: 125
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
Reputation: 12357
You have several problems here:
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.
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
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