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).
|
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: 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 Remarks: 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" Remarks: |
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) Remarks: 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" |
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.
Aim: 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) The client code on the Raspberry Pi is simple too. 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" Remarks: 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. |