Tkinter using mainloop and another loop - python

I'm doing a project where I read info from a socket and then intend to display it on a gui using tkinter. The thing is, my read info from socket is a loop and for the gui I need another loop.
I'm pretty inexperienced with both Python and Tkinter, which probably explains my mistake here.
The fd_dict is a dictionary with the properties and respective values of a car ex: gear, power, speed, etc (theme of my project).
The main problem is either I get the values from the socket or I display the gui, never both obviously, since it stays on the earlier loop.
while True:
# UDP server part of the connection
message, address = server_socket.recvfrom(1024)
del address
fdp = ForzaDataPacket(message)
fdp.wall_clock = dt.datetime.now()
# Get all properties
properties = fdp.get_props()
# Get parameters
data = fdp.to_list(params)
assert len(data) == len(properties)
# Zip into a dictionary
fd_dict = dict(zip(properties, data))
# Add timestamp
fd_dict['timestamp'] = str(fdp.wall_clock)
# Print of various testing values
print('GEAR: ', fd_dict['gear'])
print('SPEED(in KMH): ', fd_dict['speed'] * 3.6) #speed in kph
print('POWER(in HP): ', fd_dict['power'] * 0.0013596216173) #power in hp
#print('PERFORMANCE INDEX: ', fd_dict['car_performance_index'])
print('\n')
The tkinter code:
window = Tk()
window.title('Forza Horizon 5 Telemetry')
window.geometry("1500x800")
window.configure(bg="#1a1a1a")
frame = Frame(window)
frame.pack()
label_gear = Label(text = '0')
label_gear.configure(bg="darkgrey")
label_gear.pack()
I read about using after() and using classes, but I've never used them, and can't figure out how to apply them here.
Thanks in advance.

Related

How can the GUI interface pass messages to python via multi-threads?

I have a robotic project which invites the audience to type down sentences, then the robotic speaks and moves according to what the audience types. The problem is the GUI interface cannot pass the long sentences audience type, but only shorter sentences pass.
For now, there are two versions of codes, one version of code (test3.py) can interact with the speaking and movement of my robotics, but it has no GUI interface to interact. The other version of code has GUI interface, but it only passes very short sentences, not a comparatively longer sentences.
For now I have tried with two technicians who were testing from own computers, one works, one doesn't. I just don't know if there is any logic issue with the code with GUI interface within them. Here is the code for main3.py, which moves and speaks but having no GUI:
The code above works perfectly fine with the movement and the speaking, but it just does not work for the GUI interface. Here is the other version of code (main.py)which has GUI interface, but it cannot pass longer sentences to python to operate:
# Import Module
import tkinter as tk
from subprocess import call
# Create Object
window = tk.Tk()
# Set geometry
window.geometry("800x800")
def get_text():
\# Clear the output - This method is a dirty hack - fix when time !
usr_text = " "
label1 = tk.Label(window, text=usr_text)
canvas1.create_window(400, 460, window=label1)
# Get new input from user
usr_text = input_text.get()
\# Display user input to canvas
label1 = tk.Label(window, text=usr_text)
canvas1.create_window(400, 460, window=label1)
\# Call speak code - speak.py should be in the same folder
call(\["python3", "speak.py", usr_text\])
input_text.delete(0, 'end')
# -----------------------------------Main Code -----------------------------------
# Create GUI
canvas1 = tk.Canvas(window, width=800, height=600)
canvas1.pack()
input_text = tk.Entry(window)
canvas1.create_window(400, 280, window=input_text)
button1 = tk.Button(text='Enter your text', command=get_text)
canvas1.create_window(400, 360, window=button1)
window.mainloop()
```
----------------------------------
Accompaning this there is a speak2.py:
```
import sys
import pyttsx3 as tts
import nltk
import serial
from time import sleep
from threading import Thread
ser = serial.Serial(port='/dev/cu.usbmodem14101', baudrate = 9600) # Change this to the port
your arduino is connected to
\#ser = serial.Serial('/dev/cu.usbmodem2101', 9600)
arpabet = nltk.corpus.cmudict.dict()
def init_engine():
\# settings for Text to Speech (pyttsx3) library
speaking = tts.init()
speaking.setProperty('rate', 150) # setting up new voice rate: Increase to speed up |
decrease to slow down
voices = speaking.getProperty('voices') # getting details of current voice
speaking.setProperty('voice', voices\[0\].id) # changing index, changes voices. 1 for female
| o for male return speaking
# function - Text to speach
def say(all_words):
sleep(0.05)
speak.say(all_words)
speak.runAndWait() # blocks
# function - convert from words to phonemes and send to arduino/serial
def move_mouth(my_text):
array_length = len(results)
for x in range(0, array_length):
word_length = len(results\[x\]\[0\])
for y in range(0, word_length):
print(results\[x\]\[0\]\[y\], end='')
ser.write(results\[x\]\[0\]\[y\].encode())
print(".", end='')
ser.write('.'.encode())
print("$", end='')
ser.write('$'.encode())
print(" wrote to arduino")
# -----------------------------------Main Code -----------------------------------
results = \[\]`your text`
sentence = str(sys.argv\[1\]).lower()
print("You Typed:", sentence)
for words in sentence:
try:
results.append(arpabet\[words\])
except:
print('Spelling Error in text')
speak = init_engine()
# create two new threads
t1 = Thread(target=move_mouth(sentence))
t2 = Thread(target=say(sentence))
# start the threads
t2.start()
t1.start()
# wait for both threads to complete
t1.join()
t2.join()
I would really like to ask is there any logic issue with main.py or speak2.py codes? I would like to offer monetary payment if anyone could offer some help!
Looking forward to hearing any advice at this point!
Thank you very much in advance!
Chang

How to make a Tkinter loading screen that runs side-by-side the main program

I am trying to create a small python app with Tkinter which involves collecting data in several lists, like this:
def main_process():
list1 = []
list2 = []
list3 = []
#Data collection process
#Resulting lists ready
The data collection process takes about 10 seconds, so I wanted to make a "Loading Screen" that works side-by-side the data collection process, without one following the other. For the status bar, I thought of introducing a variable n with value starting from 0, and increasing its value as and when each resultant lists get ready.
I attempted to create a function loading_screen(n) which will be called before the data processing, with n being the aforementioned variable containing a numeric value. And as the data_processing occurs, I shall run the loading_screen function to introduce the updated values of n, like so:
def main_process():
def loading_screen(n):
root = Tk()
root.title('Stock Market App')
root.geometry('800x300')
status_label = Label(root, text = n)
status_label.pack()
loading_screen(0)
# list1 Ready
n += 10
root.after(0, lambda: loading_screen(n))
# list2 ready
n += 10
root.after(0, lambda: loading_screen(n))
# And so on...
But it ended up showing me the loading screen after all the data processing has occurred. Can somebody please help me on this?
Thanks.
Putting function inside a function is not a good idea, better to create a class.
Then create two separate Tk windows.
Also, after is able to pass parameters so lambda is not necessary.
I've made a few small adjustments to conform to your question. The windows will now sit side by side.
Is this what you were looking for?
import tkinter as tk
class main_process:
def loading_screen( self, n ):
self.status_label[ 'text' ] = '{}'.format( n )
self.root.deiconify()
self.status_label.focus()
self.root.update()
# insert processing here
def __init__( self, x ):
self.root = tk.Tk()
self.root.withdraw()
self.root.title('Stock Market App')
self.root.geometry( f'700x300+{x}+240')
self.status_label = tk.Label( self.root, font = 'Helvetica 20 normal')
self.status_label.pack()
self.root.update()
screen_1 = main_process( 0 )
screen_1.root.after( 10, screen_1.loading_screen, 1 )
screen_2 = main_process( 705 )
screen_2.root.after( 10, screen_2.loading_screen, 2 )
screen_2.root.mainloop()
Hi you can remove the gui like this
Labelone.destroy()
.
.
.
Loading_screen.place(...)
#you data collection code
Loading_screen.destroy()
Labelone.place(...)
.
.
.
So your data collection is going background and loading gui is place so user sees loading while python is collecting data

Micro Switch with pyserial RS232 starts/stops a timer in a tkinter thread but continues to run even when stopped

I have been using a micro switch connected to an RS232/USB serial converter cable on my windows PC to start stop and reset a timer.
The program runs smoothly most of the time but every so often updating the timer widget gets stuck running and the timer will not stop.
With the serial protocol i want to receive 1 byte b'\x00' for off and anything that's not b'\x00' should signify on.
I have replaced the micro switch with button widgets to simulate the switch and don't get the same error or i just have not kept at it for long enough.
It could be an issue with the RS232 causing an error i cannot see but my knowledge on this is sketchy and have exhausted all avenues looking online for any information on this.
import time
import sys
import serial
import threading
from tkinter import *
from tkinter import ttk
class Process(Frame):
def __init__(self, root, parent=None, **kw):
Frame.__init__(self, parent, kw)
self.root = root
self._cycStart = 0.0
self._cycTimeElapsed = 0.0
self._cycRunning = 0.0
self.cycTimeStr = StringVar()
self.cycTime_label_widget()
self.ser = serial.Serial(
port='COM4',
baudrate=1200,
timeout=0
)
self.t1 = threading.Thread(target=self.start_stop, name='t1')
self.t1.start()
def initUI(self):
root.focus_force()
root.title("")
root.bind('<Escape>', lambda e: root.destroy())
def cycTime_label_widget(self):
# Make the time label
cycTimeLabel = Label(root, textvariable=self.cycTimeStr, font=
("Ariel 12"))
self._cycleSetTime(self._cycTimeElapsed)
cycTimeLabel.place(x=1250, y=200)
cycTimeLabel_2 = Label(root, text="Cycle Timer:", font=("Ariel
12"))
cycTimeLabel_2.place(x=1150, y=200)
def _cycleUpdate(self):
""" Update the label with elapsed time. """
self._cycTimeElapsed = time.time() - self._cycStart
self._cycleSetTime(self._cycTimeElapsed)
self._cycTimer = self.after(50, self._cycleUpdate)
def _cycleSetTime(self, elap):
""" Set the time string to Minutes:Seconds:Hundreths """
minutes = int(elap/60)
seconds = int(elap - minutes*60.0)
hseconds = int((elap - minutes*60.0 - seconds)*100)
self.cycTimeStr.set('%02d:%02d:%02d' % (minutes, seconds,
hseconds))
return
def cycleStart(self):
""" Start the stopwatch, ignore if running. """
if not self._cycRunning:
self._cycStart = time.time() - self._cycTimeElapsed
self._cycleUpdate()
self._cycRunning = 1
else:
self.cycleReset()
def cycleStop(self):
""" Stop the stopwatch, ignore if stopped. """
if self._cycRunning:
self.after_cancel(self._cycTimer)
self._cycTimeElapsed = time.time() - self._cycStart
self._cycleSetTime(self._cycTimeElapsed)
self._cycRunning = 0
self._cycTimeElapsed = round(self._cycTimeElapsed, 1)
self.cycleTimeLabel = Label(root, text=(self._cycTimeElapsed,
"seconds"), font=("Ariel 35"))
self.cycleTimeLabel.place(x=900, y=285)
self.cycleReset()
def cycleReset(self):
""" Reset the stopwatch. """
self._cycStart = time.time()
self._cycTimeElapsed = 0
self._cycleSetTime(self._cycTimeElapsed)
def start_stop(self):
while True :
try:
data_to_read = self.ser.inWaiting()
if data_to_read != 0: # read if there is new data
data = self.ser.read(size=1).strip()
if data == bytes(b'\x00'):
self.cycleStop()
print("Off")
elif data is not bytes(b'\x00'):
self.cycleStart()
print("On")
except serial.SerialException as e:
print("Error")
if __name__ == '__main__':
root = Tk()
application = Process(root)
root.mainloop()
I expect the timer to start running when the micro switch is pressed. when depressed it should stop and reset back to zero and wait for the next press
With a better understanding of what you're trying to do better solutions come to mind.
As it turns out, you're not using your serial port to send or receive serial data. What you're actually doing is wiring a switch to its RX line and toggling it manually with a mechanical switch, feeding a high or low level depending on the position of the switch.
So what you're trying to do is emulating a digital input line with the RX line of your serial port. If you take a look a how a serial port works you'll see that when you send a byte the TX line toggles from low to high at the baud rate, but on top of the data you have to consider the start and stop bits. So, why your solution works (at least sometimes): that's easy to see when you look at a scope picture:
This is a screenshot of the TX line sending the \x00 byte, measured between pins 3 (TX) and 5 (GND) with no parity bit. As you can see the step only lasts for 7.5 ms (with a 1200 baud rate). What you are doing with your switch is something similar but ideally infinitely long (or until you toggle your switch back, which will be way after 7.5 ms no matter how fast you do it). I don't have a switch to try but if I open a terminal on my port and use a cable to shortcircuit the RX line to pin 4 (on a SUB-D9 connector) sometimes I do get a 0x00 byte, but mostly it's something else. You can try this experiment yourself with PuTTy or RealTerm and your switch, I guess you'll get better results but still not always the byte you expect because of the contacts bouncing.
Another approach: I'm sure there might be ways to improve on what you have, maybe reducing the baud rate to 300 or 150 bps, checking for a break in the line or other creative ideas.
But what you're trying to do is more akin to reading a GPIO line, and actually, the serial port has several digital lines intended (in the old days) for flow control.
To use these lines you should connect the common pole on your switch to the DSR line (pin 6 on a SUB-D9) and the NO and NC poles to lines DTR (pin 4) and RTS (pin 7).
The software side would be actually simpler than reading bytes: you just have to activate hardware flow control :
self.ser = serial.Serial()
self.ser.port='COM4'
self.ser.baudrate=1200 #Baud rate does not matter now
self.ser.timeout=0
self.ser.rtscts=True
self.ser.dsrdtr=True
self.ser.open()
Define the logical levels for your switch:
self.ser.setDTR(False) # We use DTR for low level state
self.ser.setRTS(True) # We use RTS for high level state
self.ser.open() # Open port after setting everything up, to avoid unkwnown states
And use ser.getDSR() to check the logical level of the DSR line in your loop:
def start_stop(self):
while True :
try:
switch_state = self.ser.getDSR()
if switch_state == False and self._cycRunning == True:
self.cycleStop()
print("Off")
elif switch_state == True and self._cycRunning == False:
self.cycleStart()
print("On")
except serial.SerialException as e:
print("Error")
I defined your self._cycRunning variable as boolean (in your initialization code you had defined it as float, but that was probably a typo).
This code works with no glitches at all even using a stripped wire as a switch.
You don't explain very well how your protocol works (I mean what is your switch supposed to be sending, or if it's sending a state change only once or several times or continuously).
But there are some red flags on your code anyway:
-With data = self.ser.read(size=1).strip() you read 1 byte but immediately you check if you have received 2 bytes. Is there a reason to do that?
-Your timer stop condition works comparing with the NULL character. That should not be a problem, but depending on your particular configuration it might (in some configurations the NULL character is read as something else, so it's wise to make sure you're really receiving it correctly).
-Your timer start condition seems too loose. Whatever you receive on the port, if it's one byte, you start your timer. Again, I don't know if that's the way your protocol works but it seems prone to trouble.
-When you replace your hardware switch with a software emulation it works as intended, but that is not surprising since you're probably imposing the condition. When you read from the serial port you have to deal with real world issues like noise, communication errors or the switch bouncing back and forth from ON to OFF. Maybe for a very simple protocol you don't need to use any error checking method, but it seems wise to at least check for parity errors. I'm not completely sure it would be straight-forward to do that with pyserial; on a quick glance I found this issue that's been open for a while.
-Again, the lack of info on your protocol: should you be using XON-XOFF flow control and two stop bits? I guess you have a reason to do it, but you should be very aware of why and how you're using those.
EDIT: With the comments below I can try to improve a bit my answer. This is just an idea for you to develop: instead of making the stop condition comparing exactly with 0x00 you can count the number of bits set to 1 and stop the counter if it's less or equal to 2. That way you can account for bits that are not received correctly.
You can do the same with the start condition but I don't know what hex value you send.
Credits for the bit counting function go to this question.
...
def numberOfSetBits(i):
i = i - ((i >> 1) & 0x55555555)
i = (i & 0x33333333) + ((i >> 2) & 0x33333333)
return (((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) & 0xffffffff) >> 24
def start_stop(self):
while True :
try:
data_to_read = self.ser.inWaiting()
if data_to_read != 0: # read if there is new data
data = self.ser.read(size=1).strip()
if numberOfSetBits(int.from_bytes(data, "big")) <= 2:
self.cycleStop()
print("Off")
elif numberOfSetBits(int.from_bytes(data, "big")) >= 3: #change the condition here according to your protocol
self.cycleStart()
print("On")
except serial.SerialException as e:
print("Error")

Why is this code returning blank? In both the command line and inside the text box? No errors, but also no data

I am trying to create a program that collects a count total of the number of unread messages inside an inbox. I am having no issues with collecting the data, the problem I have is in displaying it in real-time into a text box so that when a new mail comes in the number ticks up, instead of creating a new line underneath it, or having to completely restart the program.
After asking this previous question (How to change the data output received externally in realtime within a program's text window?) a great community member of StackOverflow gave me the following code to work through.
Now it appears to collect the data from my inbox as normal, but it isn't posting the results. I think it must have something to do with how I am using [info] but I am at a total loss.
Thank you for your help!
#! /usr/bin/env python3.4
import imaplib
import email
import tkinter as tk
WIDTH = 500
HEIGHT = 500
def update():
mail=imaplib.IMAP4_SSL('imap.gmail.com',993)
mail.login('email"gmail.com','password')
mail.select("Submissions")
typ, messageIDs = mail.search(None, "UNSEEN")
messageIDsString = str( messageIDs[0], encoding='utf8' )
listOfSplitStrings = messageIDsString.split(" ")
number = len(listOfSplitStrings)
if number == 0:
info['text'] = 'no submissions'
else:
info['text'] = '{} submissions[s]'.format(number)
root.after(5000, update)
root = tk.Tk()
root.title('submissions counter')
x = (root.winfo_screenwidth()//2) - (WIDTH//2)
y = (root.winfo_screenheight()//2) - (HEIGHT//2)
root.geometry('{}x{}+{}+{}'.format(WIDTH, HEIGHT, x, y))
info = tk.Label(root, text='no submissions')
info.pack
update()
root.mainloop()
info.pack()
Instead of:
info.pack
A simple mistake anyone could make.

How to play a sound when a value increases by 1 between loops? What does the code look like that identifies the increment by 1?

I have designed and created a program using Python 3 that reads unread messages in my Gmail inbox under two labels.
By using tkinter I have two lovely boxes that display the total messages in each label. One for sales of one particular product and the other for another.
They use the update loop to recheck each label every few seconds.
Then after the business day, I use a cleanup script in Gmail that flushes the inboxes two labels.
The program is for use on my team's sales floor. They can see daily, the number of sales, and get a real-time readout to the success of certain marketing campaigns. It has done wonders for morale.
Now I would like to implement some sounds when sales go up. A cliche bell ring, a "chhaaaching" perhaps, you get the drift.
So, I am currently tackling with my limited knowledge and have searched all throughout StackOverflow and other sites for an answer. My guess is that I need something like the following...
"if an integer value changes on the next loop from it's previous value, by an increment of 1 play soundA, or, play soundB if that value decreases by 1."
I can't for the life of me figure out what would be the term for 'increases by 1', I am also clueless on how to attach a sound to any changes made to the integer on the proceeding loop. Help!!
If I wasn't clear enough, I am more than happy to explain and go into this further.
Thank you so much guys.
Here is my code as it stands so far...
#! /usr/bin/python3
import imaplib
import email
import tkinter as tk
WIDTH = 1000
HEIGHT = 100
def update():
mail=imaplib.IMAP4_SSL('imap.gmail.com',993)
mail.login('email#gmail.com','MyPassword')
mail.select("Submissions")
typ, messageIDs = mail.search(None, "UNSEEN")
FirstDataSetSUBS = str(messageIDs[0], encoding='utf8')
if FirstDataSetSUBS == '':
info1['text'] = 'no submissions'
else:
SecondDataSetSUBS = FirstDataSetSUBS.split(" ")
nosubs = len(SecondDataSetSUBS)
nosubs = int(nosubs)
info1['text'] = '{} submission[s]'.format(nosubs)
subs.after(1000, update)
def update_2():
mail=imaplib.IMAP4_SSL('imap.gmail.com',993)
mail.login('email#gmail.com','MyPassword')
mail.select("Memberships")
typ, messageIDs = mail.search(None, "UNSEEN")
FirstDataSetMSGS = str(messageIDs[0], encoding='utf8')
if FirstDataSetMSGS == '':
info2['text'] = 'no memberships'
else:
SecondDataSetMSGS = FirstDataSetMSGS.split(" ")
memberships = len(SecondDataSetMSGS)
memberships = int(memberships)
info2['text'] = '{} membership[s]'.format(memberships)
membs.after(1000, update_2)
membs = tk.Tk()
subs = tk.Tk()
membs.title('memberships counter')
membs.configure(background="black")
subs.title('submissions counter')
subs.configure(background="black")
x = (subs.winfo_screenwidth()//5) - (WIDTH//5)
y = (subs.winfo_screenheight()//5) - (HEIGHT//5)
subs.geometry('{}x{}+{}+{}'.format(WIDTH, HEIGHT, x, y))
info1 = tk.Label(subs, text='nothing to display', bg="black", fg="green", font="Lucida_Console 40")
info1.pack()
x = (membs.winfo_screenwidth()//2) - (WIDTH//2)
y = (membs.winfo_screenheight()//2) - (HEIGHT//2)
membs.geometry('{}x{}+{}+{}'.format(WIDTH, HEIGHT, x, y))
info2 = tk.Label(membs, text='nothing to display', bg="black", fg="red", font="Lucida_Console 40")
info2.pack()
update()
update_2()
membs.mainloop
subs.mainloop()
for playing audio you can use pyaudio module
in ubuntu, install by pip3 install pyaudio include sudo if required
import pygame
pygame.init()
increased=0
inc_music=pygame.mixer.music
dec_music=pygame.mixer.music
inc_music.load("/home/pratik/Documents/pos.wav")
dec_music.load("/home/pratik/Documents/neg.wav")
def get_inc():
global increased
a=increased
return a
def pl():
global inc_music
global dec_music
while True:
increased=get_inc()
if increased == 1:
inc_music.play()
increased=increased-1
elif increased == -1:
dec_music.play()
increased=increased+1
here increased is a global variable. Make sure whenever your sales is increased it is set to +1 and whenever it is decreased it is set to -1 and call pl function in a separate thread by using threading library in the background. Since, it is a forever loop it will continuosly run in background and ring the bells.
from threading import Thread
th=Thread(target=pl)
th.setDaemon(True)
th.start()
While writing the above I assumed at a moment there is either an increase or a decrease in sales, both dont occour simultaneously. If they do, use another global variable decreased which is also initialised with 0 and decreased by -1 each time decrease in sales. And return both of them from get_inc() .
Below code produces a label that gets updated randomly, and plays the sound files located in "C:\Windows\Media\" based on the change:
import tkinter as tk
import random
from playsound import playsound
def sound(is_positive=True):
if is_positive:
playsound(r"C:\Windows\Media\chimes.wav", False)
else:
playsound(r"C:\Windows\Media\chord.wav", False)
def random_update():
_old = label['text']
_new = random.randint(1, 100)
if _new > _old: # use (_new - _old) == 1 for checking
sound() # increment of 1 exclusively
elif _new < _old: # use (_new - _old) == -1 for checking
sound(False) # decrement of 1 exclusively
label['text'] = _new
label.after(2000, random_update)
if __name__ == '__main__':
root = tk.Tk()
label = tk.Label(root, text=1)
label.after(2000, random_update)
label.pack()
root.mainloop()
playsound is not a built-in package so it needs to be installed separately. You can install using:
pip install playsound
or:
python -m pip install playsound
in the command prompt on windows.

Categories