Python Exemplary - RPi Tutorial
deutsch     english    

TCP/IP COMMUNICATION

 

 

Data link over TCP/IP

 

Information exchange between a program running on the Raspberry Pi and a partner program running on a remote computer systems becomes important when the Raspberry Pi is a front end of a measurement system and transfers sensor data in real-time to a control station or when a remote PC sends commands a RPi based robot (remote control mode).

In most situations the data link is established via TCP/IP and the well-established client/server technology based on socket programming is used. In this chapter we show typical programs for simple TCP communication without going into much details (consult other tutorials about socket programming to acquire a full understanding).

Most important and somewhat unexpected is the fact that the communication partners, the server and the client, are not symmetrical. Rather, first the server program must be started before the client program can engage a connection to it. In order to identify the two computers on the Internet, their IP address is used. In addition, server and client specify one of 65536 communication channels (IP ports).

When the server starts, it creates a server socket (like a electrical plug) that uses a particular port and goes in a wait state. We say that the server is "listening" for an incoming client. The client then creates a client socket (the plug counterpart) and tries to establish a communication link to the server using its IP address and port number. In the most single arrangement client and server are connected to the same router and use the same IP segment.
COMM1

 

 

Experiment 1: Programming with a low-level socket library

 

In a client/server application between a Raspberry Pi and a remote PC, the RPi can be server while the PC is a client or the roles may be interchanged. It depends on the specific situation which mode is preferred. In the following example the RPi acts as a measurement server that reports data from an attached sensor to a remote PC client. The programs are written by using the basic Python socket module and thus require a lot of code.

Aim:
Run a socket server on the RPi that reports the state of a GPIO input (button pressed/release) to a PC client each time the client sends a "go" command.

Program:[►]

# DataServer1.py

from threading import Thread
import socket
import time
import RPi.GPIO as GPIO

VERBOSE = False
IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

def debug(text):
    if VERBOSE:
        print "Debug:---", text

# ---------------------- class SocketHandler ------------------------
class SocketHandler(Thread):
    def __init__(self, conn):
        Thread.__init__(self)
        self.conn = conn

    def run(self):
        global isConnected
        debug("SocketHandler started")
        while True:
            cmd = ""
            try:
                debug("Calling blocking conn.recv()")
                cmd = self.conn.recv(1024)
            except:
                debug("exception in conn.recv()") 
                # happens when connection is reset from the peer
                break
            debug("Received cmd: " + cmd + " len: " + str(len(cmd)))
            if len(cmd) == 0:
                break
            self.executeCommand(cmd)
        conn.close()
        print "Client disconnected. Waiting for next client..."
        isConnected = False
        debug("SocketHandler terminated")

    def executeCommand(self, cmd):
        debug("Calling executeCommand() with  cmd: " + cmd)
        if cmd[:-1] == "go":  # remove trailing "\0"
            if GPIO.input(P_BUTTON) == GPIO.LOW:
                state = "Button pressed"
            else:
                state = "Button released"
            print "Reporting current state:", state
            self.conn.sendall(state + "\0")
# ----------------- End of SocketHandler -----------------------

setup()
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# close port when process exits:
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
debug("Socket created")
HOSTNAME = "" # Symbolic name meaning all available interfaces
try:
    serverSocket.bind((HOSTNAME, IP_PORT))
except socket.error as msg:
    print "Bind failed", msg[0], msg[1]
    sys.exit()
serverSocket.listen(10)

print "Waiting for a connecting client..."
isConnected = False
while True:
    debug("Calling blocking accept()...")
    conn, addr = serverSocket.accept()
    print "Connected with client at " + addr[0]
    isConnected = True
    socketHandler = SocketHandler(conn)
    # necessary to terminate it at program termination:
    socketHandler.setDaemon(True)  
    socketHandler.start()
    t = 0
    while isConnected:
        print "Server connected at", t, "s"
        time.sleep(10)
        t += 10
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
The server enters the listening state by calling the blocking function serverSocket.accept() that returns with connect information when a link with a client is established. Then a SocketHandler thread is started that handles the connection with the client. Normally the main program could enter the listening state and wait for another client, but in our program the main thread just "hangs" in a loop without doing something special.

It is recommended to define a simple flag to enter a "debug mode", where verbose information is written to stdout. Once the program works successfully, the verbose mode can be turned off.

The clients just sends a "go" about every 2 seconds and displays the server reply.

Program:[►]

# DataClient1.py

from threading import Thread
import socket, time

VERBOSE = False
IP_ADDRESS = "192.168.0.17"
IP_PORT = 22000

def debug(text):
    if VERBOSE:
        print "Debug:---", text

# ------------------------- class Receiver ---------------------------
class Receiver(Thread):
    def run(self):
        debug("Receiver thread started")
        while True:
            try:
                rxData = self.readServerData()
            except:
                debug("Exception in Receiver.run()")
                isReceiverRunning = False
                closeConnection()
                break
        debug("Receiver thread terminated")

    def readServerData(self):
        debug("Calling readResponse")
        bufSize = 4096
        data = ""
        while data[-1:] != "\0": # reply with end-of-message indicator
            try:
                blk = sock.recv(bufSize)
                if blk != None:
                    debug("Received data block from server, len: " + \
                        str(len(blk)))
                else:
                    debug("sock.recv() returned with None")
            except:
                raise Exception("Exception from blocking sock.recv()")
            data += blk
        print "Data received:", data
# ------------------------ End of Receiver ---------------------

def startReceiver():
    debug("Starting Receiver thread")
    receiver = Receiver()
    receiver.start()

def sendCommand(cmd):
    debug("sendCommand() with cmd = " + cmd)
    try:
        # append \0 as end-of-message indicator
        sock.sendall(cmd + "\0")
    except:
        debug("Exception in sendCommand()")
        closeConnection()

def closeConnection():
    global isConnected
    debug("Closing socket")
    sock.close()
    isConnected = False

def connect():
    global sock
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    debug("Connecting...")
    try:
        sock.connect((IP_ADDRESS, IP_PORT))
    except:
        debug("Connection failed.")
        return False
    startReceiver()
    return True

sock = None
isConnected = False

if connect():
    isConnected = True
    print "Connection established"
    time.sleep(1)
    while isConnected:
        print "Sending command: go..."
        sendCommand("go")
        time.sleep(2)
else:
    print "Connection to %s:%d failed" % (IP_ADDRESS, IP_PORT)
print "done"    
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
The client also starts a thread to handle the incoming data. This is not really necessary in this situation, but usual for more complicated client/server applications.

 

 

ttt

Experiment 2: Easy coding with an event-driven socket library

 

No special knowledge about socket programming is needed, if you use our event-driven tcpcom package (see www.aplu.ch/tcpcom for full information). Download tcpcom.py from here and copy it in the same folder with your program. The code is dramatically simplified while maintaining the same functionality.

Program:[►]

# DataServer2.py

from tcpcom import TCPServer
import time
import RPi.GPIO as GPIO

IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def onStateChanged(state, msg):
    if state == "LISTENING":
        print "Server:-- Listening..."
    elif state == "CONNECTED":
        print "Server:-- Connected to", msg
    elif state == "MESSAGE":
        print "Server:-- Message received:", msg
        if msg == "go":
            if GPIO.input(P_BUTTON) == GPIO.LOW:
                server.sendMessage("Button pressed")
            else:
                server.sendMessage("Button released")

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

setup()
server = TCPServer(IP_PORT, stateChanged = onStateChanged)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
You may run the same client as above, but the client code is also very much simplified by the tcpcom module.

Program:[►]

# DataClient2.py

from tcpcom import TCPClient
import time

IP_ADDRESS = "192.168.0.17"
IP_PORT = 22000

def onStateChanged(state, msg):
    global isConnected
    if state == "CONNECTING":
       print "Client:-- Waiting for connection..."
    elif state == "CONNECTED":
       print "Client:-- Connection estabished."
    elif state == "DISCONNECTED":
       print "Client:-- Connection lost."
       isConnected = False
    elif state == "MESSAGE":
       print "Client:-- Received data:", msg

client = TCPClient(IP_ADDRESS, IP_PORT, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    isConnected = True
    while isConnected:
        print "Client:-- Sending command: go..."
        client.sendMessage("go")
        time.sleep(2)
    print "Done"    
else:
    print "Client:-- Connection failed"      
Highlight program code (Ctrl+C copy, Ctrl+V paste)

 

 

Experiment 3: Accessing a local server from the Internet

 

In this experiment we exchange the roles of client and server and run the client on the Raspberry Pi and the server on a PC connected through a WLAN or Ethernet router located anywhere on the Internet.

To make the your PC server accessible from the Internet, you must setup the router for "IP forwarding" by specifying a single or a range of IP ports. (The setup is sometimes called "Virtual Servers".) Any request from outside to the router's IP address with a port in the specified range is then forwarded to your PC.
comm2

There are two problems: Because your PC's IP address is given by your router's DHCP, it is subject to change. Thus the IP forwarding to the specified IP may fail. To avoid this problem, you can either setup your PC to use a fixed IP address or bind the IP address to the fixed Mac address of your PC's WLAN or Ethernet adapter by an "address reservation" (if available on your router).

The second problem arises because your router's IP address is delivered by your provider using DHCP, and thus is also subject to change. Therefore your PC server cannot be accessed from outside with a fixed, well-known IP address. This problem can be solved by using an Dynamic Update Client (DUC), provided for free from noip.com. The DUC connects every 5 minutes (or another interval you chose) to the no-ip server and announces your router's IP address. From the TCP client the link is established using the IP alias provided by no-ip and the connection request is then forwarded by the no-ip server to your router that finally forwards it to the PC.

comm3

Aim:
You run a simple socket server on a PC listening on port 22000 that just displays state messages in a console. Try to setup your router so that the server is visible from anywhere on the Internet. A client on the Raspberry Pi connects to the server and sends sensor information (here just the state of a button) every second.

If you use the tcpcom library, the programs remain almost the same. The PC server just displays in the console the sensor information received from the client.

Program:[►]

# DataServer3.py

from tcpcom import TCPServer

IP_PORT = 22000

def onStateChanged(state, msg):
    if state == "LISTENING":
        print "Server:-- Listening..."
    elif state == "CONNECTED":
        print "Server:-- Connected to", msg
    elif state == "MESSAGE":
        print "Server:-- Message received:", msg

server = TCPServer(IP_PORT, stateChanged = onStateChanged)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

The client code on the Raspberry Pi is simple too.

(If server and client are sitting on the same internal IP segment for a test arrangement, the client connects using the PC's IP address that you may find out by running ipconfig on Windows or ifconfig on Mac/Linux or by using another network tool.)

Program:[►]

# DataClient3.py

from tcpcom import TCPClient
import time
import RPi.GPIO as GPIO

IP_ADDRESS = "192.168.0.111" # PC on same IP segment
#IP_ADDRESS = "5.149.19.200" # router's WAN address
#IP_ADDRESS = "raplu.zapto.org" # router's no-ip alias
IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

def onStateChanged(state, msg):
    global isConnected
    if state == "CONNECTING":
       print "Client:-- Waiting for connection..."
    elif state == "CONNECTED":
       print "Client:-- Connection estabished."
    elif state == "DISCONNECTED":
       print "Client:-- Connection lost."
       isConnected = False
    elif state == "MESSAGE":
       print "Client:-- Received data:", msg

setup()
client = TCPClient(IP_ADDRESS, IP_PORT, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    isConnected = True
    while isConnected:
        if GPIO.input(P_BUTTON) == GPIO.LOW:
            reply = "Button pressed"
        else:
            reply = "Button released"
        client.sendMessage(reply)
        print "Client:-- Sending message:", reply
        time.sleep(2)
    print "Done"    
else:
    print "Client:-- Connection failed"      
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
After you succeed in establishing the data link with server and client on the same network segment (attached to the same router), you may exercise the situation when the PC is accessed from the Internet. Setup your router with IP forwarding as described above and establish a connection from the Raspberry Pi using the IP address (or the no-ip alias) of your router.

If you do not know the IP address of the router, it is displayed by visiting www.portchecktool.com with a PC browser. As you see, an external server can trace back the route to your router's IP address that is given by your provider and where possibly your name/address is registered.

If everything works find, you can move your Raspberry Pi to another place (even far away) and establish an Internet connection from there. You may also acquire an account with no-ip.org, download and run a DUC (Dynamic Update Client) on your PC and access the PC server from the Raspberry Pi with your no-ip IP alias.

If you prefer to exchange the roles and use a Raspberry Pi as server and DUC, just do it know.