Thursday, April 8, 2021

ESP32/MicroPython server + Raspberry Pi/Python client, transmit image via WiFi TCP socket.

In this exercise, ESP32 (ESP32-DevKitC V4)/MicroPython play the role of AP, act as socket server. Raspberry Pi connect to ESP32 WiFi network, run Python code to load image, act as client and transmit the image to ESP32 server. The ESP32 server display the image on a 240*240 IPS (ST7789 SPI) LCD. It's is role reversed version of my previous exercise "Raspberry Pi/Python Server send image to ESP32/MicroPython Client via WiFi TCP socket".



protocol:

Client			|	    |	Server
(Raspberry Pi/Python)	|	    |	(ESP32/MicroPython)
			|	    |
			|	    |	Reset
			|           |	Setup AP
			|	    |	Setup socket server
(connect to ESP32 WiFi)	|	    |
			|	    |
connect to ESP32 server	|	    |	accepted
			|<-- ACK ---|	
send the 0th line	|---------->|	display the 0th line
			|<-- ACK ---|	send ACK
send the 1st line	|---------->|	display the 1st line
			|<-- ACK ---|	send ACK
			    .
			    .
			    .
send the 239th line	|---------->|	display the 239th line
			|<-- ACK ---|	send ACK
close socket		|           |	close socket
			|	    |
	
Server 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_ImgServer_AP_20210408a.py, MicroPython code run on ESP32. Save to ESP32 named main.py, such that it can run on power-up without host connected.
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 = "ssid"
AP_ssid  = "ESP32"
password = "password"

serverIP = '192.168.1.30'
serverPort = 80

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

#spi2 = machine.SPI(2, baudrate=40000000, polarity=1)
pin_spi2_sck = machine.Pin(18, machine.Pin.OUT)
pin_spi2_mosi = machine.Pin(23, machine.Pin.OUT)
pin_spi2_miso = machine.Pin(19, machine.Pin.IN)
spi2 = machine.SPI(2, sck=pin_spi2_sck, mosi=pin_spi2_mosi, miso=pin_spi2_miso,
                   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())
    myIP = wlan.ifconfig()[0]
    print(myIP)
    
    display.fill(st7789.BLACK)
    display.text(font, myIP, 10, 10)
    
def do_setupAP():
    global wlan
    print('setup AP...')
    display.fill(st7789.BLACK)
    display.text(font, "setup AP...", 10, 10)
    utime.sleep(1)
        
    ap = network.WLAN(network.AP_IF)
    ap.active(True)
    ap.config(essid=AP_ssid, password=password)

    while ap.active() == False:
      pass
    
    print(ap.active())
    print()
    print('network config:')
    myIP = ap.ifconfig()
    print(myIP)
    
    display.fill(st7789.BLACK)
    display.text(font, myIP[0], 10, 10)

def do_setupServer():
    global wlan
    global display
    
    addr = socket.getaddrinfo('0.0.0.0', serverPort)[0][-1]
    s = socket.socket()
    s.bind(addr)
    s.listen(1)
    
    print('listening on', addr)
    display.text(font, ':'+str(serverPort), 10, 50)
    
    while True:
        print('waiting connection...')
        cl, addr = s.accept()
        cl.settimeout(5)
        print('client connected from:', addr)
        
        display.fill(st7789.BLACK)
        display.text(font, addr[0], 10, 10)
    
        cl.sendall('ACK')
        print('Firt ACK sent')
        
        display.set_window(0, 0, disp_width-1, disp_height-1)
        pin_st7789_dc.on()
        for j in range(disp_height):
            
            try:
                buff = cl.recv(disp_width*3)
                #print('recv ok: ' + str(j))
            except:
                print('except: ' + str(j))
            
            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))
            #print('send ACK : ' + str(j))
            cl.sendall(bytes("ACK","utf-8"))
            #print('ACK -> : ' + str(j))
        utime.sleep(1)
        cl.close()
    print('socket closed')
    
#do_connect()
do_setupAP()

try:
    do_setupServer()
except:
    print('error')
    display.text(font, "Error", 10, 200)
finally:
    print('wlan.disconnect()')
    wlan.disconnect()
    
print('\n- bye -')
Client Side:
(Raspberry Pi/Python)

Connect Raspberry Pi to ESP32 WiFi network before run the Python code.

Load and send images with fixed resolution 240x240 (match with the display in client side) to server. 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 .

pyqTCP_ImgClient_20210408a.py
import sys
from pkg_resources import require
import time
import matplotlib.image as mpimg
import socket

#HOST = '192.168.1.34'   # The server's hostname or IP address
HOST = '192.168.4.1'
PORT = 80               # The port used by the server

from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,
                             QFileDialog, QHBoxLayout, QVBoxLayout)
from PyQt5.QtGui import QPixmap, QImage

print(sys.version)

class AppWindow(QWidget):

    camPreviewState = False  #not in Preview
    fileToUpload = ""

    def __init__(self):
        super().__init__()

        lbSysInfo = QLabel('Python:\n' + sys.version)
        vboxInfo = QVBoxLayout()
        vboxInfo.addWidget(lbSysInfo)

        #setup UI

        btnOpenFile = QPushButton("Open File", self)
        btnOpenFile.clicked.connect(self.evBtnOpenFileClicked)
        self.btnUpload = QPushButton("Upload", self)
        self.btnUpload.clicked.connect(self.evBtnUploadClicked)
        self.btnUpload.setEnabled(False)

        vboxCamControl = QVBoxLayout()
        vboxCamControl.addWidget(btnOpenFile)
        vboxCamControl.addWidget(self.btnUpload)
        vboxCamControl.addStretch()

        self.lbImg = QLabel(self)
        self.lbImg.resize(240, 240)
        self.lbImg.setStyleSheet("border: 1px solid black;")

        hboxCam = QHBoxLayout()
        hboxCam.addWidget(self.lbImg)
        hboxCam.addLayout(vboxCamControl)

        self.lbPath = QLabel(self)

        vboxMain = QVBoxLayout()
        vboxMain.addLayout(vboxInfo)
        vboxMain.addLayout(hboxCam)
        vboxMain.addWidget(self.lbPath)
        vboxMain.addStretch()
        self.setLayout(vboxMain)

        self.setGeometry(100, 100, 500,400)
        self.show()

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

    def sendImageToServer(self, imgFile):
        print(imgFile)
        imgArray = mpimg.imread(imgFile)

        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((HOST, PORT))
            print(self.wait_RESP(s))

            for j in range(240):
                time.sleep(0.1)
                b = bytes(imgArray[j])

                #print('send img : ' + str(j))
                s.sendall(bytes(b))
                #print('img sent : ' + str(j))

                ans = self.wait_RESP(s)
                #print(ans + " : " + str(j))

            print('image sent finished')
            s.close()

    def evBtnUploadClicked(self):
        print("evBtnUploadClicked()")
        print(self.fileToUpload)
        self.sendImageToServer(self.fileToUpload)

    def evBtnOpenFileClicked(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        targetPath, _ = QFileDialog.getOpenFileName(
            self, 'Open file', '/home/pi/Desktop',
            'Image files (*.jpg)', options=options)

        if targetPath:
            print(targetPath)
            self.lbPath.setText(targetPath)

            with open(targetPath):
                pixmap = QPixmap(targetPath)

                #accept 240x240 image only
                if pixmap.width()==240 and pixmap.height()==240:
                    self.lbImg.setPixmap(pixmap)
                    self.btnUpload.setEnabled(True)
                    self.fileToUpload = targetPath
                else:
                    self.btnUpload.setEnabled(False)


    def evBtnOpenFileClickedX(self):

        targetPath="/home/pi/Desktop/image.jpg"

        print(targetPath)
        self.lbPath.setText(targetPath)

        try:
            with open(targetPath):
                pixmap = QPixmap(targetPath)
                self.lbImg.setPixmap(pixmap)

                #as a exercise, get some info from pixmap
                print('\npixmap:')
                print(pixmap)
                print(type(pixmap))
                print(str(pixmap.width()) + " : " + str(pixmap.height()))
                print()

                print('convert to Image')
                qim = pixmap.toImage()
                print(qim)
                print(type(qim))
                print()

                print('read a pixel from image')
                qrgb = qim.pixel(0, 0)
                print(hex(qrgb))
                print(type(qrgb))

                r, g, b = qRed(qrgb), qGreen(qrgb), qBlue(qrgb)
                print([hex(r), hex(g), hex(b)])
                print()

        except FileNotFoundError:
            print('File Not Found Error')

if __name__ == '__main__':
    print('run __main__')
    app = QApplication(sys.argv)
    window = AppWindow()
    sys.exit(app.exec_())

print("- bye -")
Remark:
At beginning, I tried to set ESP32 as STA, by calling do_connect(), connect to my home/mobile WiFi network. But the result is very unstable in: Pi client send 240 pixel but ESP32 server can't receive all. That's why you can find some commented debug code (using print()) in the program.

After then, set ESP32 as AP, Pi connect to ESP32 WiFi network. The result have great improved and very stable.




No comments: