Breaking data from Serial into 2 variables

Hi there,

I am writing a code in Python (well maybe this is a little presumptuous from my side…) to make a GUI which function is to read a couple of coordinates given by an Arduino in x;y format through Serial, being x and y a couple of natural positive numbers from 1 to 2800.

The 2 numbers are separate by a ; in order to know when one finishes and the second starts, but I could change this for any other method to differentiate the 2 numbers.

The GUI will “draw” a red rectangle where the coordinates of Arduino are pointing.

Here is my problem: I can see the coordinates coming through with a simple sketch that shows them on the monitor, but when I think of putting the 2 received numbers into actual variables in the main code in Python I don’t know how to do it (beginner minus level…). I would like my program to read the Serial data, recognize the first number (before the “;” ) and put it into a variable xpos. Then take the second number (after the “;”) and put it into the second variable, ypos.

I am using Tkinter to draw the background image and all graphic aspects, and plan to use the xpos and ypos variables to draw the rectangle where the coordinates indicate.

This is the code I have written so far:

from serial import *
from tkinter import *


serialPort = "/dev/ttyUSB0"
baudRate = 9600
ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #ensure non-blocking

root = Tk() 
root.geometry("1280x800")
root.attributes('-fullscreen',True)
bg = PhotoImage(file = "/home/pi/Pictures/SCREEN2.png") 

canvas = Canvas( root, width = 1280, height = 800) 
canvas.pack(fill = "both", expand = True) 
canvas.create_image( 0, 0, image = bg, anchor = "nw") 

scrollbar = Scrollbar(root)
scrollbar.pack(side=RIGHT, fill=Y)

# make a text box to put the serial output
log = Text ( root, width=30, height=30, takefocus=0)
log.pack()

# attach text box to scrollbar
log.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=log.yview)

#make our own buffer
#useful for parsing commands
#Serial.readline seems unreliable at times too
serBuffer = ""

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


# after initializing serial, an arduino may need a bit of time to reset
root.after(100, readSerial)

#canvas.create_rectangle(200, 200, 300,300, width = 0, fill = 'red')



# Execute tkinter 
root.mainloop()

Needless to say that the return of this sketch is TypeError: can only concatenate str (not “bytes”) to str

In fact this is cut-paste from different examples and I didn’t expect to get the 2 coordinates with the code. If anyone would be able to drive me through the way I should get those variables I would appreciate it.

Thanks for watching.

Joan.

Hi Joan, and welcome!

Thanks for showing your code, that’s excellent. Unfortunately many of us
can’t run it: we may not have an Arduino to read data from, or the
serial library, or even tkinter to run the GUI code.

When possible, it is best to isolate the problem into a self-contained
issue that can be run in any environment.

Possibly the most important piece of information you gave us was your
comment about the TypeError you get, unfortunately you don’t tell us
where you get the error, so I have to guess.

I think you are getting a TypeError at this line:

serBuffer += c # add to the buffer

Am I correct?

If so, that tells me that reading from the serial port is returning
bytes, but you are trying to compare and add to text strings. There are
lots of ways to fix this, but I think the easiest way is this: when you
read a single character (actually a single byte) from the serial port,
immediately change it to a string:

c = ser.read()
c = c.decode('ascii')

If you are curious what’s going on in that second line of code, a good
place to start is here:

and remember that ASCII text is a subset of Unicode. Please feel free to
ask follow up questions.

I’m going to assume that this is enough to solve the TypeError problem.
Now I’m going to jump forward: I assume that you can get your readSerial
to collect a string that looks something like this:

"123.45;678.98"

If that’s not the case, again feel free to ask further questions.

Let’s create a function that takes a string that looks like the above,
and returns two float numbers:

def split_coords(astring):
    """Split astring on semi-colon and convert to floats."""
    a, b = astring.split(';')
    a = float(a)
    b = float(b)
    return (a, b)

Being a function, it is easy for us to write some code to test that it
works as we expect:

x, y = split_coords("9552.378;-89.0123")
print(x == 9552.378)  # should print True
print(y == -89.0123)  # should print True

Now all you need do is get the string out of readSerial and pass it to
the split_coords. The place to do that is here:

log.insert('0.0', serBuffer)
serBuffer = "" # empty the buffer

Before you empty the buffer, call your “split_coords” function:

log.insert('0.0', serBuffer)
x, y = split_coords(serBuffer)
serBuffer = "" # empty the buffer

Good luck!

Steve

Steve, you are my hero!

I have used your information to improve the code (I just realized I did not need a buffer to print the coordinates as my final goal was to paint rectangles, you will understand as soon as you see the code) and tried to use the coordinates to paint the corresponding rectangles on the GUI.

So it seems I have learned to read a line and break it into 2 variables (I forgot about reading single bytes and went for the line) using your .split command and function but I did not learn how to do probably the easiest part… painting :frowning:

Just for you to understand I have a 4 button GUI that I want to highlight when pressed. The origin of the coordinates is the arduino, and now I can read them and put them into x and ythanks to you. I just need to compare the reading of x and y and paint the corresponding rectangle.

The problem now is how to do that. See what I wrote (perpetrated) that give the following error:

x: 32 - y: 28
OK button1
Traceback (most recent call last):
  File "/home/pi/Documents/Touch-GUI-TKInter_4.py", line 52, in <module>
    paintRectangle1()
  File "/home/pi/Documents/Touch-GUI-TKInter_4.py", line 26, in paintRectangle1
    canvas.create_rectangle(200, 120, 400, 220, fill='red')
  File "/usr/lib/python3.7/tkinter/__init__.py", line 2501, in create_rectangle
    return self._create('rectangle', args, kw)
  File "/usr/lib/python3.7/tkinter/__init__.py", line 2480, in _create
    *(args + self._options(cnf, kw))))
_tkinter.TclError: invalid command name ".!canvas"

As you can see, when pressing buttin 1 the code correctly reads the serial, breaks the line into x and y and interprets the “if” statement printing “OK button 1”.

However, my very non elegant program does not paint what I want and throws me the error above.

The simplified code is this:

from serial import *
from tkinter import *
import time

root = Tk() 
root.geometry("1280x800")
root.attributes('-fullscreen',True)
bg = PhotoImage(file = "/home/pi/Pictures/SCREEN2.png") 

canvas = Canvas( root, width = 1280, height = 800) 
canvas.pack(fill = "both", expand = True) 
canvas.create_image( 0, 0, image = bg, anchor = "nw") 
    
serialPort = "/dev/ttyUSB0"
ser = Serial(serialPort, 9600, timeout=0)
ser.flush()

def split_coords(astring):
    a, b = astring.split(';')
    a = int(a)
    b = int(b)
    
    return (a, b)

def paintRectangle1():
    canvas.create_rectangle(200, 120, 400, 220, fill='red')
    time.sleep(0.3)
    
def paintRectangle2():
    canvas.create_rectangle(600, 120, 800, 220, fill='red')
    time.sleep(0.3)
    
def paintRectangle3():
    canvas.create_rectangle(200, 260, 400, 360, fill='red')
    time.sleep(0.3)
    
def paintRectangle4():
    canvas.create_rectangle(600, 260, 800, 360, fill='red')
    time.sleep(0.3)

root.mainloop() 

while True:
        
    if ser.in_waiting> 0:
        line = ser.readline().decode('utf-8').rstrip()
        x, y = split_coords(line)
        print("x:", x, "- y:", y)
        
        if (x>19) and (x<60) and (y>20) and (y<40):
            print("OK button1")
            paintRectangle1()
        if (x>68) and (x<110) and (y>20) and (y<40):
            print("OK button2")
            paintRectangle2()
        if (x>19) and (x<60) and (y>45) and (y<80):
            print("OK button3")
            paintRectangle3()
        if (x>68) and (x<110) and (y>45) and (y<80):
            print("OK button4")
            paintRectangle4()
            
        #else:
            #paint the backgrand image again after the 0.3 seconds defined into the paintRectangle function

As you can see I didn’t even try to finish the “else” operation to reprint the background image to “erase” the rectangles after 0.3 seconds defined in the paintRectangle functions.

If you could help with that it would be done.

Thank you again for your invaluable support!

Joan.

Sorry Joan, I think I have hit my limit of knowledge about tkinter. The
error you are getting:

_tkinter.TclError: invalid command name ".!canvas"

is utterly mysterious to me. Googling suggests that it happens if you
attempt to draw to the canvas after it is destroyed:

but I have no idea how or why that has happened.

Using GUI toolkits like tkinter often requires that you follow a very
strict pattern in your code, often with mysterious and cryptic errors if
you deviate even a little. Perhaps this example might help?

As an aside, you can simplify your coordinate tests:

if (x>19) and (x<60) and (y>20) and (y<40):

like this:

if (19 < x < 60) and (20 < y < 40):

If you do solve your problem, please reply back here with the solution.
Good luck!

Thank you Steve. I will continue digging and see if I reach somewhere with an acceptable outcome.

Best.

Joan.

Ok, I got it!

I finally found out what was going on. First: when I was calling the function of paintRectangleX I was only creating the rectangle with the canvas.create_rectangle, but not actually “placing” it onto the root. You need the pack() command for that (one of the options of tkinter).

Second, time.sleep did not work in my script. The idea was to light a rectangle for 0.3 secs and then return to white. Instead I had to use the .after function.

This is my final code (I know it could be much more elegant by removing repetition, but still don’t know how to do that):

from serial import *
from tkinter import *
import time
import sys

def split_coords(astring):
a, b = astring.split(‘;’)
a = int(a)
b = int(b)

return (a, b)

def paintRectangle1():
print(“entered Rect 1”)
canvas.create_rectangle(207, 152, 616, 375, outline = “”, fill=‘red’)
canvas.pack()
root.after(200, repaintRectangles)

def paintRectangle2():
print(“entered Rect 2”)
canvas.create_rectangle(665, 152, 1073, 375, outline = “”, fill=‘red’)
canvas.pack()
root.after(200, repaintRectangles)

def paintRectangle3():
print(“entered Rect 3”)
canvas.create_rectangle(207, 425, 616, 648, outline = “”, fill=‘red’)
canvas.pack()
root.after(200, repaintRectangles)

def paintRectangle4():
print(“entered Rect 4”)
canvas.create_rectangle(665, 425, 1073, 648, outline = “”, fill=‘red’)
canvas.pack()
root.after(200, repaintRectangles)

def repaintRectangles():
print(“entered Repaint”)
canvas.create_rectangle(207, 152, 616, 375, outline = “”, fill=‘white’)
canvas.create_rectangle(665, 152, 1073, 375, outline = “”, fill=‘white’)
canvas.create_rectangle(207, 425, 616, 648, outline = “”, fill=‘white’)
canvas.create_rectangle(665, 425, 1073, 648, outline = “”, fill=‘white’)
canvas.pack()

root = Tk()
root.geometry(“1280x800”)
root.attributes(‘-fullscreen’,True)
bg = PhotoImage(file = “/home/pi/Pictures/SCREEN2.png”)

canvas = Canvas(root)
canvas.pack(fill = “both”, expand = True)
canvas.config(cursor = ‘none’)
canvas.create_image( 0, 0, image = bg, anchor = “nw”)

serialPort = “/dev/ttyUSB0”
ser = Serial(serialPort, 9600, timeout=0)
ser.flush()

while True:

root.update()

if ser.in_waiting> 0:
    line = ser.readline().decode('utf-8').rstrip()
    if (line == "inits"):
        x, y = 1000, 1000
        
    else:
        x, y = split_coords(line)
        print("x:", x, "- y:", y)
        ser.flush()           
    
    if (19 < x < 60) and (20 < y < 40):
        print("OK button1")
        paintRectangle1()
    if (68 < x < 110) and (20 < y < 40):
        print("OK button2")
        paintRectangle2()
    if (19 < x < 60) and (45 < y < 80):
        print("OK button3")
        paintRectangle3()
    if (68 < x < 110) and (45 < y < 80):
        print("OK button4")
        paintRectangle4()
    if (-10 < x < 20) and (-10 < y < 20):
        sys.exit()

root.mainloop()

As you can see I have adde a “quit” secret door at 1 corner of the screen to kill the program and also used your suggestion with comparisons of x and y between < :wink:

Again, thanks for your help, I has been fun to code this GUI.

Joan.