SocketServer used to control PiBot remotely (python)

4k views Asked by At

this is my first question! (despite using the site to find most of the answers to programming questions i've ever had)

I have created a PiBotController which i plan to run on my laptop which i want to pass the controls (inputs from arrow keys) to the raspberry pi controlling my robot. Now the hardware side of this isn't the issue i have created a program that responds to arrow key inputs and i can control the motors on the pi through a ssh connection.

Searching online i found the following basic server and client code using socketserver which i can get to work sending a simple string.

Server:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
"""
The RequestHandler class for our server.

It is instantiated once per connection to the server, and must
override the handle() method to implement communication to the
client.
"""

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

# Create the server, binding to localhost on port 9999
server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
server.serve_forever()

Client:

import socket
import sys

HOST, PORT = "192.168.2.12", 9999
data = "this here data wont send!! "


# Create a socket (SOCK_STREAM means a TCP socket)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))

    # Receive data from the server and shut down
    received = str(sock.recv(1024), "utf-8")
finally:
    sock.close()

print("Sent:     {}".format(data))
print("Received: {}".format(received))

Now this works fine and prints the results both on my Raspberry Pi (server) and on my laptop (client) however i have tried multiple times to combine it into a function that activates along with my key press' and releases' like i have in my controller.

PiBotController

#import the tkinter module for the GUI and input control
try:
    # for Python2
    import Tkinter as tk
    from Tkinter import *
except ImportError:
    # for Python3
    import tkinter as tk
    from tkinter import *

import socket
import sys

#variables
Drive = 'idle'
Steering = 'idle'




#setting up the functions to deal with key presses
def KeyUp(event):
    Drive = 'forward'
    drivelabel.set(Drive)
    labeldown.grid_remove()
    labelup.grid(row=2, column=2)
def KeyDown(event):
    Drive = 'reverse'
    drivelabel.set(Drive)
    labelup.grid_remove()
    labeldown.grid(row=4, column=2)
def KeyLeft(event):
    Steering = 'left'
    steeringlabel.set(Steering)
    labelright.grid_remove()
    labelleft.grid(row=3, column=1)
def KeyRight(event):
    Steering = 'right'
    steeringlabel.set(Steering)
    labelleft.grid_remove()
    labelright.grid(row=3, column=3)
def key(event):
    if event.keysym == 'Escape':
        root.destroy()

#setting up the functions to deal with key releases
def KeyReleaseUp(event):
    Drive = 'idle'
    drivelabel.set(Drive)
    labelup.grid_remove()
def KeyReleaseDown(event):
    Drive = 'idle'
    drivelabel.set(Drive)
    labeldown.grid_remove()
def KeyReleaseLeft(event):
    Steering = 'idle'
    steeringlabel.set(Steering)
    labelleft.grid_remove()
def KeyReleaseRight(event):
    Steering = 'idle'
    steeringlabel.set(Steering)
    labelright.grid_remove()

#connection functions
def AttemptConnection():
    connectionmessagetempvar = connectionmessagevar.get()
    connectionmessagevar.set(connectionmessagetempvar + "\n" + "Attempting to        connect...") 

def transmit(event):
    HOST, PORT = "192.168.2.12", 9999
    data = "this here data wont send!! "


    # Create a socket (SOCK_STREAM means a TCP socket)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
         # Connect to server and send data
         sock.connect((HOST, PORT))
         sock.sendall(bytes(data + "\n", "utf-8"))

         # Receive data from the server and shut down
         received = str(sock.recv(1024), "utf-8")
    finally:
         sock.close()

    print("Sent:     {}".format(data))
    print("Received: {}".format(received))





#setting up GUI window        
root = tk.Tk()
root.minsize(300,140)
root.maxsize(300,140)
root.title('PiBot Control Centre')
root.grid_columnconfigure(0, minsize=50)
root.grid_columnconfigure(1, minsize=35)
root.grid_columnconfigure(2, minsize=35)
root.grid_columnconfigure(3, minsize=35)
root.grid_rowconfigure(2, minsize=35)
root.grid_rowconfigure(3, minsize=35)
root.grid_rowconfigure(4, minsize=35)
root.configure(background='white')
root.option_add("*background", "white")




#set up the labels to display the current drive states
drivelabel = StringVar()
Label(root, textvariable=drivelabel).grid(row=0, column=1, columnspan=2)
steeringlabel = StringVar()
Label(root, textvariable=steeringlabel).grid(row=1, column=1, columnspan=2)
Label(root, text="Drive: ").grid(row=0, column=0, columnspan=1)
Label(root, text="Steering: ").grid(row=1, column=0, columnspan=1)

#set up the buttons and message for connecting etc..
messages=tk.Frame(root, width=150, height=100)
messages.grid(row=1,column=4, columnspan=2, rowspan=4)


connectionbutton = Button(root, text="Connect", command=AttemptConnection)
connectionbutton.grid(row=0, column=4)
connectionmessagevar = StringVar()
connectionmessage = Message(messages, textvariable=connectionmessagevar, width=100, )
connectionmessage.grid(row=1, column=1, rowspan=1, columnspan=1)
disconnectionbutton = Button(root, text="Disconnect")
disconnectionbutton.grid(row=0, column=5)







#pictures
photodown = PhotoImage(file="down.gif")
labeldown = Label(root, image=photodown)
labeldown.photodown = photodown
#labeldown.grid(row=4, column=1)

photoup = PhotoImage(file="up.gif")
labelup = Label(root, image=photoup)
labelup.photoup = photoup
#labelup.grid(row=2, column=1)

photoleft = PhotoImage(file="left.gif")
labelleft = Label(root, image=photoleft)
labelleft.photoleft = photoleft
#labelleft.grid(row=3, column=0)

photoright = PhotoImage(file="right.gif")
labelright = Label(root, image=photoright)
labelright.photoright = photoright
#labelright.grid(row=3, column=2)

photoupleft = PhotoImage(file="upleft.gif")
labelupleft = Label(root, image=photoupleft)
labelupleft.photoupleft = photoupleft
#labelupleft.grid(row=2, column=0)

photodownleft = PhotoImage(file="downleft.gif")
labeldownleft = Label(root, image=photodownleft)
labeldownleft.photodownleft = photodownleft
#labeldownleft.grid(row=4, column=0)

photoupright = PhotoImage(file="upright.gif")
labelupright = Label(root, image=photoupright)
labelupright.photoupright = photoupright
#labelupright.grid(row=2, column=2)

photodownright = PhotoImage(file="downright.gif")
labeldownright = Label(root, image=photodownright)
labeldownright.photodownright = photodownright
#labeldownright.grid(row=4, column=2)




#bind all key presses and releases to the root window
root.bind_all('<Key-Up>', KeyUp)
root.bind_all('<Key-Down>', KeyDown)
root.bind_all('<Key-Left>', KeyLeft)
root.bind_all('<Key-Right>', KeyRight)

root.bind_all('<KeyRelease-Up>', KeyReleaseUp)
root.bind_all('<KeyRelease-Down>', KeyReleaseDown)
root.bind_all('<KeyRelease-Left>', KeyReleaseLeft)
root.bind_all('<KeyRelease-Right>', KeyReleaseRight)

root.bind_all('<Key>', key)
root.bind_all('<Key>', transmit)




#set the labels to an initial state
steeringlabel.set('idle')
drivelabel.set('idle')
connectionmessagevar.set ('PiBotController Initiated')

#initiate the root window main loop
root.mainloop()

this program compiles fine but then doesn't send any data to the server? (i'm aware its still just sending a string but i thought id start with something easy... and well evidently i got stuck so it is probably for the best)

Any suggestions to make it work just sending the string or sending the varibales drive and steering every time they change would be greatly appreciated.

Dave xx

EDIT

here is the transmit function i got to it work in the sense it sends data whenever i do a key press/release (like i wanted before) however it only sends the initial setting for the variables of 'idle'. Looking at the code now as well i think i should probably take the host and port info and creating a socket connection out of the function that runs every time? but im not sure so here is what i have right now anyway.

def transmit():
    HOST, PORT = "192.168.2.12", 9999
    DriveSend = drivelabel.get
    SteeringSend = steeringlabel.get


    # Create a socket (SOCK_STREAM means a TCP socket)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # Connect to server and send data
        sock.connect((HOST, PORT))
        sock.sendall(bytes(Drive + "\n", "utf-8"))
        sock.sendall(bytes(Steering + "\n", "utf-8"))

        # Receive data from the server and shut down
        received = str(sock.recv(1024), "utf-8")
    finally:
        sock.close()

    print("Sent:     {}".format(Steering))
    print("Sent:     {}".format(Drive))
    print("Received: {}".format(received))
1

There are 1 answers

5
Brionius On BEST ANSWER

The problem is that when Tkinter catches a key event, it triggers the more specific binding first (for example 'Key-Up'), and the event is never passed to the more general binding ('Key'). Therefore, when you press the 'up' key, KeyUp is called, but transmit is never called.

One way to solve this would be to just call transmit() within all the callback functions (KeyUp, KeyDown, etc).

For example, KeyUp would become

def KeyUp(event):
    Drive = 'forward'
    drivelabel.set(Drive)
    labeldown.grid_remove()
    labelup.grid(row=2, column=2)
    transmit()

Then you can get rid of the event binding to 'Key'.

Another option would be to make "Drive" and "Steering" into Tkinter.StringVar objects, then bind to write events using "trace", like this:

Drive = tk.StringVar()
Drive.set('idle')
Drive.trace('w', transmit)

Note that trace sends a bunch of arguments to the callback, so you'd have to edit transmit to accept them.

EDIT

Ok, I see the problems - there are three.

1. When you write

Drive = 'forward'

in your callback functions, you're not setting the variable Drive in your module namespace, you're setting Drive in the local function namespace, so the module-namespace Drive never changes, so when transmit accesses it, it's always the same.

2. In transmit, you write

DriveSend = drivelabel.get
SteeringSend = steeringlabel.get

This is a good idea, but you're just referencing the functions, not calling them. You need

DriveSend = drivelabel.get()
SteeringSend = steeringlabel.get()

3. In transmit, the values you send through the socket are the module-level variables Drive and Steering (which never change as per problem #1), rather than DriveSend and SteeringSend.

Solution:

I would recommend doing away with all the Drive and Steering variables entirely, and just using the StringVars 'drivelabelandsteeringlabel`. Thus your callbacks can become:

def KeyUp(event):
#    Drive = 'forward'   (this doesn't actually do any harm, but to avoid confusion I'd just get rid of the Drive variables altogether)
    drivelabel.set('forward')
    labeldown.grid_remove()
    labelup.grid(row=2, column=2)

(and so on for the rest of the callbacks) and your transmit function will become

def transmit():
    HOST, PORT = "192.168.2.12", 9999
    DriveSend = drivelabel.get()        # Note the ()
    SteeringSend = steeringlabel.get()

    # Create a socket (SOCK_STREAM means a TCP socket)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # Connect to server and send data
        sock.connect((HOST, PORT))
        sock.sendall(bytes(DriveSend + "\n", "utf-8"))    # Note Drive ==> DriveSend
        sock.sendall(bytes(SteeringSend + "\n", "utf-8")) # Note Steering ==> SteeringSend

        # Receive data from the server and shut down
        received = str(sock.recv(1024), "utf-8")
    finally:
        sock.close()

    print("Sent:     {}".format(SteeringSend))   # Note Steering ==> SteeringSend
    print("Sent:     {}".format(DriveSend))      # Note Drive ==> DriveSend
    print("Received: {}".format(received))

modified solution (from OP):

Since playing around with this method for a little while i found that constantly changing the variables every 100ms due to key being held down is troublesome and causes isues with the smoothness of the motor control when i am for example just driving forward. to fix this i used the following edit into each function

def KeyUp(event):
    if drivelabel.get() == "forward":
        pass
    else:
        drivelabel.set("forward")
        labeldown.grid_remove()
        labelup.grid(row=2, column=2)
        transmit()
        print (drivelabel.get())

The code now checks if the varibale is already set to the relevent direction if it is it does nothing, otherwise it modifies it. the print line is just there for me to check it was working properly and could be removed or commented out.