Wednesday, March 24, 2021

Raspberry Pi/Python Server send image to ESP32/MicroPython Client via WiFi TCP socket


In this exercise, Raspberry Pi/Python3 act as socket server, ESP32/MicroPython act as client connect to server via WiFi TCP. Once received, server (Pi/Python) send a image (240x240) to client (ESP32/MicroPython), then the client display the image on a 240*240 IPS (ST7789 SPI) LCD.


To make it more flexible, the image is in 240 batch of 240 pixel x 3 color (r, g, b).

protocol:

Server			|	    |	Client
(Raspberry Pi/Python)	|	    |	(ESP32/MicroPython)
			|	    |
Start			|	    |	Reset
			|	    |
Setup as 		|	    |
socketserver.TCPServer	|	    |
			|	    |
			|	    |	Join the WiFi network
		        |	    |	Connect to server with socket
			|	    |	
			|<-- ACK ---|	send ACK
send the 0th line	|---------->|	display the 0th line
			|<-- ACK ---|	send ACK
send the 1st line	|---------->|	display the 1st line
			    .
			    .
			    .
send the 239th line	|---------->|	display the 239th line
			|<-- ACK ---|	send ACK
close socket		|	    |	close socket
			|	    |
wait next		|	    |	bye	


Client side:

(ESP32/MicroPython)

The ESP32 used is a ESP32-DevKitC V4, display is a 240*240 IPS (ST7789 SPI) LCD. Library setup and connection, refer to former post "ESP32 (ESP32-DevKitC V4)/MicroPython + 240*240 IPS (ST7789 SPI) using russhughes/st7789py_mpy lib".

upyESP32_ImgClient_20210324c.py, MicroPython code run on ESP32. Modify ssid/password and serverIP for your WiFi network.
from os import uname
from sys import implementation
import machine
import network
import socket
import ubinascii
import utime
import st7789py as st7789
from fonts import vga1_16x32 as font
import ustruct as struct
"""
ST7789 Display  ESP32-DevKitC (SPI2)
SCL             GPIO18
SDA             GPIO23
                GPIO19  (miso not used)

ST7789_rst      GPIO5
ST7789_dc       GPIO4
"""
#ST7789 use SPI(2)

st7789_res = 5
st7789_dc  = 4
pin_st7789_res = machine.Pin(st7789_res, machine.Pin.OUT)
pin_st7789_dc = machine.Pin(st7789_dc, machine.Pin.OUT)

disp_width = 240
disp_height = 240

ssid = "your ssid"
password = "your password"

serverIP = '192.168.1.30'
serverPort = 9999

print(implementation.name)
print(uname()[3])
print(uname()[4])
print()

spi2 = machine.SPI(2, baudrate=40000000, polarity=1)
print(spi2)
display = st7789.ST7789(spi2, disp_width, disp_width,
                          reset=pin_st7789_res,
                          dc=pin_st7789_dc,
                          xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLACK)

mac = ubinascii.hexlify(network.WLAN().config('mac'),':').decode()
print("MAC: " + mac)
print()

#init ESP32 as STA
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
utime.sleep(1)

def do_connect():
    global wlan
    print('connect to network...')
    display.fill(st7789.BLACK)
    display.text(font, "connect...", 10, 10)
    
    wlan.active(True)
    if not wlan.isconnected():
        print('...')
        wlan.connect(ssid, password)
        while not wlan.isconnected():
            pass
    
    print()
    print('network config:')
    print("interface's IP/netmask/gw/DNS addresses")
    print(wlan.ifconfig())
    
    display.fill(st7789.BLACK)
    display.text(font, "connected", 10, 10)
    
def do_scan():
    global wlan
    print('scan network...')
    wlan.active(True)
    for network in wlan.scan():
        print(network)
        
def do_connectServer():
    global wlan
    global display
    
    addr = socket.getaddrinfo(serverIP, serverPort)[0][-1]
    print(addr)
    s = socket.socket()
    s.connect(addr)
    
    print('---')
    display.fill(st7789.BLACK)
    display.text(font, "waiting...", 10, 10)

    print('Send ACK')
    s.sendall(bytes("ACK","utf-8"))
        
    display.set_window(0, 0, disp_width-1, disp_height-1)
    pin_st7789_dc.on()
    for j in range(disp_height):
        
        buff = s.recv(disp_width*3)
        for i in range(disp_width):
            offset= i*3
            spi2.write(struct.pack(st7789._ENCODE_PIXEL,
                                   (buff[offset] & 0xf8) << 8 |
                                   (buff[offset+1] & 0xfc) << 3 |
                                   buff[offset+2] >> 3))
            
        s.sendall(bytes("ACK","utf-8"))

    s.close()
    print('socket closed')
    
do_connect()
try:
    do_connectServer()
except:
    print('error')
    display.text(font, "Error", 10, 200)
finally:
    print('wlan.disconnect()')
    wlan.disconnect()
    
print('\n- bye -')

Server Side:
(Raspberry Pi/Python)

The server will send Desktop/image.jpg with fixed resolution 240x240 (match with the display in client side). My former post "min. version of RPi/Python Code to control Camera Module with preview on local HDMI" is prepared for this purpose to capture using Raspberry Pi Camera Module .

pyMyTCP_ImgServer_20210324c.py, Python3 code run on Raspberry Pi.
import socketserver
import platform
import matplotlib.image as mpimg

imageFile = '/home/pi/Desktop/image.jpg'

print("sys info:")
for info in platform.uname():
    print(info)

class MyTCPHandler(socketserver.BaseRequestHandler):

    #wait client response in 3 byte len
    def wait_RESPONSE(self, client):
        client.settimeout(10)
        res = str()
        data = client.recv(4)
        return data.decode("utf-8")

    def handle(self):
        msocket = self.request

        print("{} connected:".format(self.client_address[0]))
        imgArray = mpimg.imread(imageFile)

        self.wait_RESPONSE(msocket)     #dummy assume 'ACK' received
        print('first RESPONSE received')

        for j in range(240):
            b = bytes(imgArray[j])
            msocket.sendall(bytes(b))
            self.wait_RESPONSE(msocket)  #dummy assume 'ACK' received

        print('image sent finished')

        msocket.close()


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

    # Create the server, binding to localhost on port 9999
    #with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
    with socketserver.TCPServer(('', PORT), MyTCPHandler) as server:
        # Activate the server; this will keep running until you
        # interrupt the program with Ctrl-C

        server.serve_forever()
This socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server example is modify from Python 3 socketserver.TCPServer Example. I assume socketserver.TCPServer will handle Ctrl-C with port close. But in my test, SOMETIMES throw OSError of "Address already in use". In my practice, try pressing Ctrl-C in REPL/restart repeatedly.


Next:

No comments: