Friday, April 16, 2021

Raspberry Pi Pico/CircuitPython + ILI9341 SPI Display with Touch

In this exercise, Raspberry Pi Pico is flashed with Adafruit CircuitPython 6.2.0. Display on ILI9341 SPI Display with Touch using adafruit_ili9341 library. For the touch detection, xpt2046.py of rdagger/micropython-ili9341 was modified to work for CircuitPython. It's CircuitPython version of my former exercise RPi Pico/MicroPython + ILI9341 SPI Display with Touch.

The display used in this exercise is a 2.4-inch 65K color using ili9341 driver with touch, 2.4inch_SPI_Module_ILI9341_SKU:MSP2402.

Connection:

Connection:
ILI9341 TFT SPI	    RPi Pico
------------------------------
VCC		    3V3
GND		    GND
CS		    GP13
RESET		    GP14
DC		    GP15
SDI(MOSI)	    GP7
SCK		    GP6
LED		    3V3
SDO(MISO)
T- T_CLK	    GP10
O  T_CS		    GP12
U  T_DIN	    GP11
C  T_DO		    GP8
H- T_IRQ

Install display library for ili9341:

Visit https://circuitpython.org/libraries, download the appropriate bundle for your version of CircuitPython. Unzip the file, and copy adafruit_ili9341.mpy and  adafruit_display_text directory to /lib directory of your Raspberry Pi Pico CIRCUITPY driver.

Touch driver for xpt2046:

(If you do not use touch part, you can skip this part.)

xpt2046.py of rdagger/micropython-ili9341 is a xpt2046 driver for MicroPython. I modify to make it work for CircuitPython. The main modification are removing interrupt and re-config pin for CircuitPython using digitalio.

Save it to Raspberry Pi Pico CIRCUITPY driver, name cpy_xpt2046.py.

"""
XPT2046 Touch module for CircuitPython
modified from xpt2046.py of rdagger/micropython-ili9341
https://github.com/rdagger/micropython-ili9341/blob/master/xpt2046.py

remove interrupt and re-config pin for CircuitPython
"""
from time import sleep
import digitalio

class Touch(object):
    """Serial interface for XPT2046 Touch Screen Controller."""

    # Command constants from ILI9341 datasheet
    GET_X = const(0b11010000)  # X position
    GET_Y = const(0b10010000)  # Y position
    GET_Z1 = const(0b10110000)  # Z1 position
    GET_Z2 = const(0b11000000)  # Z2 position
    GET_TEMP0 = const(0b10000000)  # Temperature 0
    GET_TEMP1 = const(0b11110000)  # Temperature 1
    GET_BATTERY = const(0b10100000)  # Battery monitor
    GET_AUX = const(0b11100000)  # Auxiliary input to ADC
    
    """ remove support of interrupt
    def __init__(self, spi, cs, int_pin=None, int_handler=None,
                 width=240, height=320,
                 x_min=100, x_max=1962, y_min=100, y_max=1900):
    """
    def __init__(self, spi, cs, width=240, height=320,
                 x_min=100, x_max=1962, y_min=100, y_max=1900):
        """Initialize touch screen controller.

        Args:
            spi (Class Spi):  SPI interface for OLED
            cs (Class Pin):  Chip select pin
            int_pin (Class Pin):  Touch controller interrupt pin
            int_handler (function): Handler for screen interrupt
            width (int): Width of LCD screen
            height (int): Height of LCD screen
            x_min (int): Minimum x coordinate
            x_max (int): Maximum x coordinate
            y_min (int): Minimum Y coordinate
            y_max (int): Maximum Y coordinate
        """
        self.spi = spi
        self.cs = cs
        #self.cs.init(self.cs.OUT, value=1)
        self.cs_io = digitalio.DigitalInOut(cs)
        self.cs_io.direction = digitalio.Direction.OUTPUT
        self.cs_io.value=1
        
        self.rx_buf = bytearray(3)  # Receive buffer
        self.tx_buf = bytearray(3)  # Transmit buffer
        self.width = width
        self.height = height
        # Set calibration
        self.x_min = x_min
        self.x_max = x_max
        self.y_min = y_min
        self.y_max = y_max
        self.x_multiplier = width / (x_max - x_min)
        self.x_add = x_min * -self.x_multiplier
        self.y_multiplier = height / (y_max - y_min)
        self.y_add = y_min * -self.y_multiplier

        """ ignore int_pin
        if int_pin is not None:
            self.int_pin = int_pin
            self.int_pin.init(int_pin.IN)
            
            self.int_handler = int_handler
            self.int_locked = False
            int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING,
                        handler=self.int_press)
        """
        
    def get_touch(self):
        """Take multiple samples to get accurate touch reading."""
        timeout = 2  # set timeout to 2 seconds
        confidence = 5
        buff = [[0, 0] for x in range(confidence)]
        buf_length = confidence  # Require a confidence of 5 good samples
        buffptr = 0  # Track current buffer position
        nsamples = 0  # Count samples
        while timeout > 0:
            if nsamples == buf_length:
                meanx = sum([c[0] for c in buff]) // buf_length
                meany = sum([c[1] for c in buff]) // buf_length
                dev = sum([(c[0] - meanx)**2 +
                          (c[1] - meany)**2 for c in buff]) / buf_length
                if dev <= 50:  # Deviation should be under margin of 50
                    return self.normalize(meanx, meany)
            # get a new value
            sample = self.raw_touch()  # get a touch
            if sample is None:
                nsamples = 0    # Invalidate buff
            else:
                buff[buffptr] = sample  # put in buff
                buffptr = (buffptr + 1) % buf_length  # Incr, until rollover
                nsamples = min(nsamples + 1, buf_length)  # Incr. until max

            sleep(.05)
            timeout -= .05
        return None

    """
    def int_press(self, pin):
        
        if not pin.value() and not self.int_locked:
            self.int_locked = True  # Lock Interrupt
            buff = self.raw_touch()

            if buff is not None:
                x, y = self.normalize(*buff)
                self.int_handler(x, y)
            sleep(.1)  # Debounce falling edge
        elif pin.value() and self.int_locked:
            sleep(.1)  # Debounce rising edge
            self.int_locked = False  # Unlock interrupt
    """
    
    def normalize(self, x, y):
        """Normalize mean X,Y values to match LCD screen."""
        x = int(self.x_multiplier * x + self.x_add)
        y = int(self.y_multiplier * y + self.y_add)
        return x, y

    def raw_touch(self):
        """Read raw X,Y touch values.

        Returns:
            tuple(int, int): X, Y
        """
        x = self.send_command(self.GET_X)
        y = self.send_command(self.GET_Y)
        if self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max:
            return (x, y)
        else:
            return None

    def send_command(self, command):
        """Write command to XT2046 (MicroPython).

        Args:
            command (byte): XT2046 command code.
        Returns:
            int: 12 bit response
        """
        self.tx_buf[0] = command
        
        #self.cs(0)
        self.cs_io.value=0
        
        self.spi.try_lock()
        self.spi.write_readinto(self.tx_buf, self.rx_buf)
        self.spi.unlock()
        
        #self.cs(1)
        self.cs_io.value=1

        return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4)

Example code:

cpyPico_spi_ILI9341_20210416.py
from sys import implementation
from os import uname
import board
import time
import displayio
import terminalio
import busio
import adafruit_ili9341
from adafruit_display_text import label

print('=======================')    
print(implementation[0], uname()[3])

displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14
spi_mosi = board.GP7
#spi_miso = board.GP4
spi_clk = board.GP6

spi = busio.SPI(spi_clk, MOSI=spi_mosi)

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT,
                    rowstart=0, colstart=0)
display.rotation = 0

# Make the display context
splash = displayio.Group(max_size=10)
display.show(splash)

color_bitmap = displayio.Bitmap(display.width, display.height, 1)
color_palette = displayio.Palette(1)
color_palette[0] = 0x00FF00

bg_sprite = displayio.TileGrid(color_bitmap,
                               pixel_shader=color_palette, x=0, y=0)
splash.append(bg_sprite)

# Draw a smaller inner rectangle
inner_bitmap = displayio.Bitmap(display.width-2, display.height-2, 1)
inner_palette = displayio.Palette(1)
inner_palette[0] = 0x0000FF
inner_sprite = displayio.TileGrid(inner_bitmap,
                                  pixel_shader=inner_palette, x=1, y=1)
splash.append(inner_sprite)

# Draw a label
text_group1 = displayio.Group(max_size=10, scale=2, x=20, y=40)
text1 = "RPi Pico"
text_area1 = label.Label(terminalio.FONT, text=text1, color=0xFF0000)
text_group1.append(text_area1)  # Subgroup for text scaling
# Draw a label
text_group2 = displayio.Group(max_size=10, scale=1, x=20, y=60)
text2 = implementation[0] + ' ' + uname()[3]
text_area2 = label.Label(terminalio.FONT, text=text2, color=0xFFFFFF)
text_group2.append(text_area2)  # Subgroup for text scaling

# Draw a label
text_group3 = displayio.Group(max_size=10, scale=2, x=20, y=100)
text3 = adafruit_ili9341.__name__
text_area3 = label.Label(terminalio.FONT, text=text3, color=0xF0F0F0)
text_group3.append(text_area3)  # Subgroup for text scaling
# Draw a label
text_group4 = displayio.Group(max_size=10, scale=2, x=20, y=120)
text4 = adafruit_ili9341.__version__
text_area4 = label.Label(terminalio.FONT, text=text4, color=0xF0F0F0)
text_group4.append(text_area4)  # Subgroup for text scaling

splash.append(text_group1)
splash.append(text_group2)
splash.append(text_group3)
splash.append(text_group4)

rot = 0
print('rot: ', rot, '\t-', display.width," x ", display.height)
time.sleep(3.0)

while True:
    time.sleep(5.0)
    rot = rot + 90
    if (rot>=360):
        rot =0
    display.rotation = rot
    print('rot: ', rot, '\t-', display.width," x ", display.height)

print('- bye -')


cpyPico_spi_ILI9341_bitmap_20210416.py

"""
Example of CircuitPython/Raspberry Pi Pico
to display on 320x240 ili9341 SPI display
"""

import os
import board
import time
import terminalio
import displayio
import busio
from adafruit_display_text import label
import adafruit_ili9341

print("==============================")
print(os.uname())
print("Hello Raspberry Pi Pico/CircuitPython ILI8341 SPI Display")
print(adafruit_ili9341.__name__ + " version: " + adafruit_ili9341.__version__)
print()

# Release any resources currently in use for the displays
displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_spi_clk = board.GP6
tft_spi_mosi = board.GP7
#tft_spi_miso = board.GP4
tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14

tft_spi = busio.SPI(tft_spi_clk, MOSI=tft_spi_mosi)

display_bus = displayio.FourWire(
    tft_spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT)
display.rotation = 90
print('rot: ', display.rotation, '\t-', display.width," x ", display.height)

group = displayio.Group(max_size=10)
display.show(group)

bitmap = displayio.Bitmap(display.width, display.height, display.width)

palette = displayio.Palette(display.width)
for p in range(display.width):
    palette[p] = (0x010000*p) + (0x0100*p) + p

for y in range(display.height):
    for x in range(display.width):
        bitmap[x,y] = x
        
tileGrid = displayio.TileGrid(bitmap, pixel_shader=palette, x=0, y=0)
group.append(tileGrid)

time.sleep(3.0)

while True:
    for p in range(display.width):
        palette[p] = p
    time.sleep(3.0)

    for p in range(display.width):
        palette[p] = 0x0100 * p
    time.sleep(3.0)

    for p in range(display.width):
        palette[p] = 0x010000 * p
    time.sleep(3.0)
    
print('-bye -')

cpyPico_spi_ILI9341_touch_20210416.py

"""
Example of CircuitPython/Raspberry Pi Pico
to display on 320x240 ili9341 SPI display
with touch detection
"""

from sys import implementation
from os import uname
import board
import time
import terminalio
import displayio
import busio
from adafruit_display_text import label
import adafruit_ili9341
from cpy_xpt2046 import Touch

print("==============================")
print(implementation[0], uname()[3])
print("Hello Raspberry Pi Pico/CircuitPython ILI8341 SPI Display")
print("with touch")
print(adafruit_ili9341.__name__ + " version: " + adafruit_ili9341.__version__)
print()

# Release any resources currently in use for the displays
displayio.release_displays()

TFT_WIDTH = 320
TFT_HEIGHT = 240

tft_spi_clk = board.GP6
tft_spi_mosi = board.GP7
#tft_spi_miso = board.GP4
tft_cs = board.GP13
tft_dc = board.GP15
tft_res = board.GP14

touch_spi_clk = board.GP10
touch_spi_mosi = board.GP11
touch_spi_miso = board.GP8

touch_cs = board.GP12
#touch_int = board.GP0

touch_x_min = 64
touch_x_max = 1847
touch_y_min = 148
touch_y_max = 2047

touch_spi = busio.SPI(touch_spi_clk, MOSI=touch_spi_mosi, MISO=touch_spi_miso)
touch = Touch(touch_spi, cs=touch_cs,
              x_min=touch_x_min, x_max=touch_x_max,
              y_min=touch_y_min, y_max=touch_y_max)

tft_spi = busio.SPI(tft_spi_clk, MOSI=tft_spi_mosi)


display_bus = displayio.FourWire(
    tft_spi, command=tft_dc, chip_select=tft_cs, reset=tft_res)

display = adafruit_ili9341.ILI9341(display_bus,
                    width=TFT_WIDTH, height=TFT_HEIGHT)
display.rotation = 90
scrWidth = display.width
scrHeight = display.height
print('rot: ', display.rotation, '\t-', scrWidth," x ", scrHeight)


group = displayio.Group(max_size=10)
display.show(group)

bitmap = displayio.Bitmap(display.width, display.height, 5)

BLACK = 0
WHITE = 1
RED   = 2
GREEN = 3
BLUE  = 4
palette = displayio.Palette(5)
palette[0] = 0x000000
palette[1] = 0xFFFFFF
palette[2] = 0xFF0000
palette[3] = 0x00FF00
palette[4] = 0x0000FF

for y in range(display.height):
    for x in range(display.width):
        bitmap[x,y] = BLACK
        
tileGrid = displayio.TileGrid(bitmap, pixel_shader=palette, x=0, y=0)
group.append(tileGrid)

taskInterval_50ms = 0.050
NxTick = time.monotonic() + taskInterval_50ms

EVT_NO = const(0)
EVT_PenDown = const(1)
EVT_PenUp   = const(2)
EVT_PenRept = const(3)
touchEvent  = EVT_NO

touchSt_Idle_0     = const(0)
touchSt_DnDeb_1    = const(1)
touchSt_Touching_2 = const(2)
touchSt_UpDeb_3    = const(3)
touchSt = touchSt_Idle_0

touchDb_NUM = const(3)
touchDb = touchDb_NUM
touching = False
"""
state diagram for touch debounce

    touchStIdle_0       validXY!=None->     touchSt_DnDeb_1
                      <-validXY==None
    ^                                       validXY!=None
    |                                       |
    validXY==None                           V
                                          
    touchSt_UpDeb_3  <- validXY==None       touchSt_Touching_2
                        validXY!=None->
"""

"""
    None: no touch or invalid touch
    normailzedX, normailzedY: valid touch
"""
def validTouch():

    xy = touch.raw_touch()
    
    if xy == None:
        return None
    
    normailzedX, normailzedY = touch.normalize(*xy)
    if (normailzedX < 0 or normailzedX >= scrWidth
            or normailzedY < 0 or normailzedY >= scrHeight):
            return None
        
    return (normailzedX, normailzedY)

def TouchDetTask():
    global touch
    global touching
    global touchSt
    global touchEvent
    global touchedX, touchedY
    global touchDb
    
    validXY = validTouch()

    if touchSt == touchSt_Idle_0:
        if validXY != None:
            touchDb = touchDb_NUM
            touchSt = touchSt_DnDeb_1
    
    elif touchSt == touchSt_DnDeb_1:
        if validXY != None:
            touchDb = touchDb-1
            if touchDb==0:
                touchSt = touchSt_Touching_2
                touchEvent = EVT_PenDown
                touchedX, touchedY = validXY
                touching = True
        else:
            touchSt = touchSt_Idle_0
            
    elif touchSt == touchSt_Touching_2:
        if validXY != None:
            touchedX, touchedY = validXY
            touchEvent = EVT_PenRept
        else:
            touchDb=touchDb_NUM
            touchSt = touchSt_UpDeb_3
            
    elif touchSt == touchSt_UpDeb_3:
        if validXY != None:
            touchSt = touchSt_Touching_2
        else:
            touchDb=touchDb-1
            if touchDb==0:
                touchSt = touchSt_Idle_0
                touchEvent = EVT_PenUp
                touching = False
    
def drawCross(x, y, col):
    if y>=0 and y<scrHeight:
        for i in range(x-5, x+5):
            if i>=0 and i<scrWidth: 
                bitmap[i, y] = col
    
    if x>=0 and y<scrWidth:
        for i in range(y-5, y+5):
            if i>=0 and i<scrHeight:
                bitmap[x, i] = col
    
while True:
    
    curTick = time.monotonic()
    if curTick >= NxTick:
        NxTick = curTick + taskInterval_50ms
        #print(NxTick)
        TouchDetTask()
        
    #handle touch event
    if touchEvent != EVT_NO:
        if touchEvent == EVT_PenDown:
            print('ev PenDown - ', touchedX, " : ", touchedY)

            drawCross(touchedX, touchedY, WHITE)
        if touchEvent == EVT_PenUp:
            print('ev PenUp - ')
            drawCross(touchedX, touchedY, RED)
        if touchEvent == EVT_PenRept:
            if (touchedX>=0 and touchedX<scrWidth
                and touchedY>=0 and touchedY<scrHeight):
                bitmap[touchedX, touchedY] = GREEN
            
        touchEvent = EVT_NO
    
print('-bye -')



Remark about CircuitPython support for interrupt:

According to CircuitPython Frequently Asked Questions, CircuitPython does not currently support interrupts.



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.