Display serial incoming data on tkinter scrollbar - python-3.x

I downloaded a small arduino program in order for it to produce serial data.
My goal (first iteration) is to create an application that will replace the arduino IDE serial - since i only want to read serial data.
This is how the data looks like on the arduino IDE serial interface.
This is my code so far:
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
from tkinter import scrolledtext
#new stuff
import time
import serial
import threading
import continuous_threading
#to be used on our canvas
HEIGHT = 700
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
#Serial Stuff-----------------------------------------
ser = serial.Serial('COM16', baudRate)
val1 = 0
index = []
def readSerial():
global val1
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
index.append(val1)
disp = tk.Label(frame2, text=index[0])
disp.config(font=("TkDefaultFont", 8))
disp.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
t1 = continuous_threading.PeriodicThread(0.5, readSerial)
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
# --- frame 2 ---
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')
# --- frame 2 ---
t1.start()
root.mainloop() #here we run our app
I have a frame2 which is a scrollText that i want my data to appear (i also have another frame above it, that i removed it here so that i don't confuse people with unnecessary code).
Ideally i would like the data to appear like in the arduino IDE, with autoscroll - but first i have to walk before i can run.
All the serial "action" happens in the portion i have marked with comments called 'Serial stuff'.
Is my placement code correct?
disp = tk.Label(frame2, text=index[0])
disp.config(font=("TkDefaultFont", 8))
disp.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
I want each line to be placed in a new line in the textbox, like my first picture
EDIT: I followed scotty3685's advice (Thanks a lot sir!) but look at what i get now at my tkinter frame:
If you compare with the first picture, it's close but it's not really there.

The way to insert text into a ScrolledText widget is as follows
s.insert("end","some_text that I want to insert")
s is the name of the scrolledtext widget (in your case this is
called scrollbar confusingly)
The first argument to insert "end"
tells the scrolled text widget to place the new text at the end of
the current text in the textbox.
and the second argument to insert is
the text you want to insert (in your case, val1).

Related

Program stucks when sending serial data

I am developing a small application, in roder to interface with my arduino.
However, in order for the arduino to start sending serial data, i have to trigger it with a specific string.
When i press the button that will send the string and will initiate the serial read, the program stucks.
No error is shown on the terminal.
Here is the relevant code:
#setting the serial object
def on_select(event=None):
global ser
COMPort = cb.get()
ser = serial.Serial(port = COMPort, baudrate=9600)
# read serial data
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
after_id=root.after(100,readSerial)
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
global stop
stop_ = False
ser.write(str.encode('rf')) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
The first two functions work, because i gave been able to read serial data, when the arduino just spits data, without requiring activation by sending it text.
It's the third function that makes the program stuck somehow.
EDIT: This is more code - the bigger picture.
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports #for a list of all the COM ports
from tkinter import scrolledtext
#to be used on our canvas
HEIGHT = 800
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
# this is the global variable that will hold the serial object value
ser = None #initial value. will change at 'on_select()'
# --- functions ---
#the following two functtions are for the seria port selection, on frame 1
#this function populates the combobox on frame1, with all the serial ports of the system
def serial_ports():
return serial.tools.list_ports.comports()
#when the user selects one serial port from the combobox, this function will execute
def on_select(event=None):
global ser
COMPort = cb.get()
string_separator = "-"
COMPort = COMPort.split(string_separator, 1)[0] #remove everything after '-' character
COMPort = COMPort[:-1] #remove last character of the string (which is a space)
ser = serial.Serial(port = COMPort, baudrate=9600)
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
if vsb.get()[1]==1.0: #if the scrollbar is down to the bottom, then autoscroll
text.see("end")
after_id=root.after(100,readSerial)
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
global stop_
stop_ = False
button_stop['state']='normal' #make the 'Stop Measurement' button accessible
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
# --- frame 1 ---
frame1 = tk.Frame(root)
frame1.place(relx=0, rely=0.05, relheight=0.03, relwidth=1, anchor='nw') #we use relheight and relwidth to fill whatever the parent is - in this case- root
label0 = tk.Label(frame1, text="Select the COM port that the device is plugged in: ")
label0.config(font=("TkDefaultFont", 8))
label0.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
cb = ttk.Combobox(frame1, values=serial_ports())
cb.place(relx=0.5, rely=0.5, anchor='center')
cb.bind('<<ComboboxSelected>>', on_select)
# --- frame 1 ---
# --- frame 2 ---
frame2 = tk.Frame(root, bd=5) #REMOVED THIS bg='#80c1ff' (i used it to see the borders of the frame)
frame2.place(relx=0, rely=0.1, relheight=0.07, relwidth=1, anchor='nw')
#Button for 'Measure All Sensors'
#it will be enabled initially
button_all = tk.Button(frame2, text="Measure all Sensors", bg='#80c1ff', fg='red', state='normal', command=measure_all) #bg='gray'
button_all.place(relx=0.2, rely=0.5, anchor='center')
#frame 6 will be the area with the texct field
# --- frame 6 ---
frame6 = tk.Frame(root, bg='#80c1ff') #remove color later
frame6.place(relx=0.0, rely=0.4, relheight=1, relwidth=1, anchor='nw')
text_frame=tk.Frame(frame6)
text_frame.place(relx=0, rely=0, relheight=0.6, relwidth=1, anchor='nw')
text=tk.Text(text_frame)
text.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')
vsb=tk.Scrollbar(text_frame)
vsb.pack(side='right',fill='y')
text.config(yscrollcommand=vsb.set)
vsb.config(command=text.yview)
# --- frame 6 ---
stop_=True # Stop Flag. True when no measuring is happening
root.mainloop() #here we run our app
# --- main ---

Python serial fails

I am trying to log serial incoming data to a tkinter frame.
This is my code so far:
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
#global variable that will hold the COM port
COMPort = 0
#to be used on our canvas
HEIGHT = 700
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
#make our own buffer
#useful for parsing commands
#Serial.readline seems unreliable at times too
serBuffer = ""
ser = 0 #initial value. will chane at 'on_select('
# --- functions ---
#the following two functtions are for the seria port selection, on frame 1
def serial_ports():
return serial.tools.list_ports.comports()
def on_select(event=None):
global COMPort
COMPort = cb.get()
print(COMPort)
# get selection from event
#print("event.widget:", event.widget.get())
# or get selection directly from combobox
#print("comboboxes: ", cb.get())
global ser
ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #ensure non-blocking
def readSerial():
while True:
c = ser.read() # attempt to read a character from Serial
#was anything read?
if len(c) == 0:
break
# get the buffer from outside of this function
global serBuffer
# check if character is a delimeter
if c == '\r':
c = '' # don't want returns. chuck it
if c == '\n':
serBuffer += "\n" # add the newline to the buffer
#add the line to the TOP of the log
log.insert('0.0', serBuffer)
serBuffer = "" # empty the buffer
else:
serBuffer += c # add to the buffer
root.after(10, readSerial) # check serial again soon
# --- functions ---
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
#we use frames to organize all the widgets in the screen
# --- frame 1 ---
frame1 = tk.Frame(root)
frame1.place(relx=0, rely=0.05, relheight=0.03, relwidth=1, anchor='nw') #we use relheight and relwidth to fill whatever the parent is - in this case- root
label0 = tk.Label(frame1, text="Select the COM port that the device is plugged in: ")
label0.config(font=("TkDefaultFont", 8))
label0.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
cb = ttk.Combobox(frame1, values=serial_ports())
cb.place(relx=0.5, rely=0.5, anchor='center')
# assign function to cmbobox
cb.bind('<<ComboboxSelected>>', on_select)
# --- frame 1 ---
# --- frame 2 ---
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=0.07, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = Scrollbar(frame2)
scrollbar.pack(side=RIGHT, fill=Y)
# make a text box to put the serial output
log = Text ( frame2, width=30, height=30, takefocus=0)
log.pack()
# attach text box to scrollbar
log.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=log.yview)
# --- frame 2 ---
# after initializing serial, an arduino may need a bit of time to reset
root.after(100, readSerial)
root.mainloop() #here we run our app
I get this error however:
ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #ensure non-b
locking
NameError: name 'Serial' is not defined
EDIT:
When i tried:
from Serial import Serial
i got:
ModuleNotFoundError: No module named 'Serial'
When i tried:
import serial
i got:
AttributeError: module 'serial' has no attribute 'tools'
When i tried:
import Serial
i got:
ModuleNotFoundError: No module named 'Serial'
The Serial class is part of the serial module. You import it the way you import anything else. Both of the following work, depending on your preference:
import serial
...
ser = serial.Serial(...)
-or-
from serial import Serial
...
ser = Serial(...)

Tkinter: autoscroll to the bottom when user is not scrolling

I have made a small tkinter application, that receives data from the serial port and displays them on a ScrolledText frame.
I have manaded to make the frame autoscroll down to the end, when new data appears.
There is a problem however. If the user wants to see a particular value the autoscrolling option will make him lose it.
This is why i want to make it autoscroll, only when the user is not scrolling manually.
I based my code on this answer:
Python: Scroll a ScrolledText automatically to the end if the user is not scrolling manually
This is my code:
def readSerial():
global val1
fully_scrolled_down = scrollbar.yview()[1] == 1.0
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
scrollbar.insert("end", val1)
if fully_scrolled_down:
scrollbar.see("end") #autoscroll to the end of the scrollbar
However, this is not working. This code just constantly autoscrolls down, regardless of the use is manually scrolling up.
UPDATE: This is the code from the scrolledText frame:
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=0.9, relwidth=1, anchor='nw')
UPDATE 2:
Full code
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
from tkinter import scrolledtext
import time
import serial
import threading
import continuous_threading
#to be used on our canvas
HEIGHT = 700
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
ser = serial.Serial('COM16', baudRate)
val1 = 0
def readSerial():
global val1
#https://stackoverflow.com/questions/51781247/python-scroll-a-scrolledtext-automatically-to-the-end-if-the-user-is-not-scroll
fully_scrolled_down = scrollbar.yview()[1] == 1.0 #remove for ayutoscroll when not afafa
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
scrollbar.insert("end", val1)
if fully_scrolled_down: #remove for ayutoscroll when not afafa
scrollbar.see("end") #autoscroll to the end of the scrollbar
t1 = continuous_threading.PeriodicThread(0.1, readSerial)
#----------------------------------------------------------------------
#--------------------------------------------------------------------------------
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
# --- frame 2 ---
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=0.9, relwidth=1, anchor='nw')
# --- frame 2 ---
#--------------------------------------------------------------------------------
t1.daemon=True
t1.start()
root.mainloop() #here we run our app
yview()[1] does not consistently return 1.0 since your text widget is constantly being updated. Instead of using scrolledtext module you can create one yourself, that way you have a control over the Scrollbar's attributes. Check the below example.
from tkinter import *
import random
def foo():
val=random.randint(1000,9999)
label.config(text=val)
text.insert(END,f"{val}\n")
if vsb.get()[1]==1.0:
text.see(END)
root.after(200,foo)
root=Tk()
label=Label(root)
label.pack()
text_frame=Frame(root)
text_frame.pack()
text=Text(text_frame)
text.pack(side='left')
vsb=Scrollbar(text_frame)
vsb.pack(side='left',fill='y')
text.config(yscrollcommand=vsb.set)
vsb.config(command=text.yview)
foo()
root.mainloop()
get method of Scrollbar return a tuple of (top,bottom) coordinates accurately and you can make use of this.

How to enhance window size selection on a tkinter project including button-image as label?

I'm currently working on a little project on python-3.x including some tkinter ressources. My program is made to display on a screen a list of pictures included in a directory, each picture is put on a button that is a sixth of the original image, and if we click on it, it display the image on his original size on a new window. The original window is set by the amount of pictures i put in the columns (i can choose in the code) and i ve made a scrollbar because i have to work with a lot of pictures.
But here is my problem, it's works fine except that if i change the window size, like reduce it for example, the buttons don't follow, they just vanish behind the window, and with the scrollbar.
I'm not particularly good in python so i was wondering that maybe by doing like a threading we could get the window size in live and then if the window size is inferior/superior of our columns of buttons, we could resize it and change the amount of columns then reload the page, but i will have to work with multiple image so it will take a lot of time.
from tkinter import *
from tkinter.filedialog import *
from tkinter.messagebox import *
from PIL import Image, ImageTk
import tkinter as tk
import glob
import os
import cv2
import copy
import _thread
import time
folder = 'X:/users/Robin/data/dataset-valid/visu/*.jpg'
a=glob.glob(folder)
fic = "../data/list.txt"
fichObj=open(fic,"w")
p = []
for f in a:
fichObj.write(f+"\n")
fichObj.close()
class SuperPhoto(object):
def __init__(self, photo , image):
self.photo = photo
temp = cv2.resize(image, (int((self.photo.width())/6) , int((self.photo.height())/6)))
red = temp[:,:,2].copy()
blue = temp[:,:,0].copy()
temp[:,:,0] = red
temp[:,:,2] = blue
temp = Image.fromarray(temp)
self.miniature = ImageTk.PhotoImage(temp)
def agrandir(self):
Newfen=Toplevel()
Newfen.geometry("+60+60")
#self.photo.resize((500,500))
print(type(self.photo))
label = Label(Newfen, image=self.photo, width=self.photo.width(), height=self.photo.height())
label.image = self.photo # keep a reference!
label.pack()
if os.path.exists (fic): #os.path utile
count = len(open(fic).readlines())
print(count)
#lin = open(fic).readlines()
#print(lin)
class ScrollableCanvas(Frame):
def __init__(self, parent, *args, **kw):
Frame.__init__(self, parent, *args, **kw)
canvas=Canvas(self,bg='#FFFFFF',width=300,height=300,scrollregion=(0,0,500,500))
canvas.update_idletasks()
vbar=Scrollbar(self,orient=VERTICAL)
vbar.pack(side=RIGHT, fill=Y)
vbar.config(command=canvas.yview)
canvas.config(width=1200,height=700)
canvas.config(yscrollcommand=vbar.set)
canvas.pack(side=LEFT,expand=True,fill=BOTH)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW )
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
class Main_frame(Frame):
# Init
def __init__(self, fenetre_principale=None):
Frame.__init__(self, fenetre_principale)
self.grid()
self.scrollable_canvas = ScrollableCanvas(self)
self.scrollable_canvas.grid(row=1,column=1)
nbCol = 4
for file in a:
image = Image.open(file)
photo = ImageTk.PhotoImage(image)
w = photo.width()
L.append(int(w/6))
#print(L)
sumL = int(sum(L)/nbCol)
print(sumL)
p.append(SuperPhoto(photo, cv2.imread(file)))
for ligne in range(int(count/nbCol)):
for colonne in range(nbCol):
photo = p[ligne * nbCol + colonne]
button = Button(self.scrollable_canvas.interior, image=photo.miniature, command=photo.agrandir)
button.grid(row=ligne, column=colonne)
if __name__ == "__main__":
root = Tk()
root.title("VISU")
root.geometry("+0+0")
L= []
interface = Main_frame(fenetre_principale=root)
root.update_idletasks()
print(root.winfo_width())
print(root.geometry())
interface.mainloop()
So, I except this program to work like a classic directory display, with the columns that change automatically when we resize the window and with the scrollbar that follow it.
If you have any solutions it will really help me ..
You can try it, just put some jpeg pictures in a directory and change the folder variable with the link of your directory.
Thanks in advance for your help, if you have any questions to understand more clearly what i've said don't hesitate.
Each time the root window is resized, a <Configure> event is triggered. Catch it as follows:
def resize(event):
root.update_idletasks()
#update all image sizes here if needed
#all widgets can be 're-grided' here based on new width and height of root window
root.bind('<Configure>', resize)
If you want to ensure that your window cannot be resized, use the following:
root.resizable(False, False)

tkinter only displays top of label text

I am making an alarm clock radio using python3, tkinter on a raspberry pi 3 using a 2.8in PiTFT. And I am using a true type font called digital-7. When I create a tkinter Label to hold the time, then only the top 3/4s of the text is displayed. If I add a "\n", then the full text displays. I've done a lot of searching but haven't been able to find a solution. Any help would be appreciated.
Here is a snippet of the code:
import datetime
import tkinter as tk
root = tk.Tk()
root.configure(background='black')
# make root use full screen
root.overrideredirect(True)
root.geometry("{0}x{1}+0+0".format(root.winfo_screenwidth(), root.winfo_screenheight()))
# using 2.8 PiTFT with 320x240 display
timeText = tk.StringVar()
# digital-7 is a true type font, which shows clock in 7 segment display
timeLabel = tk.Label(root, font=('digital-7', 120), fg='red', bg='black', textvariable=timeText, anchor='n')
timeLabel.grid(row=0, columnspan=6)
def updateDate():
global timeText
dt = datetime.datetime.now()
# without the '\n' the bottom part of the time gets cropped
tts = dt.strftime('%H:%M')
timeText.set(tts+'\n')
root.after(2000, updateDate)
updateDate()
root.mainloop()
The full code is here (it is still a work in progress):
https://raw.githubusercontent.com/dumbo25/tkinter-alarm-clock-radio-gui/master/acr.py
digital-7 goes in /home/pi/.fonts and here is where I got digital 7 from:
https://github.com/dec/angular-canvas-gauge/blob/master/Sample/fonts/digital-7-mono.ttf

Resources