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:

Install Visual Studio Code on Raspberry Pi, with apt command.

Visual Studio Code was added to the Raspberry Pi OS APT repository, in both 32-bit and 64-bit variants. Now you can install code with apt command:

$ sudo apt update
$ sudo apt install code



ref:

Monday, March 22, 2021

min. version of RPi/Python Code to control Camera Module with preview on local HDMI

It's a minimum version of my another exercise "RPi/Python Code to control Camera Module with preview on local HDMI"; with minimum functions preview and capture only, using PyQt5 GUI, fixed resolution 240x240, and display the captured image on GUI. You can also choice save file name; image.jpg or img_<timestamp>.jpg, under Desktop folder. It's used to prepare images for my coming exercise "Raspberry Pi/Python send image to ESP32/MicroPython via WiFi TCP socket".

In my usage scenario: The Raspberry Pi 4B/8G is installed with HQ Camera Module (mount with manual focus lens) and a 4 inch HDMI IPS Touch Display. Remote control with Android with xrdp/Microsoft Remote Desktop. Such that I can control camera on remote Android device, adjust focus/aperture on the lens, and check the effect on local HDMI preview at real-time.


Python3 code, qCam240_20210323.py
import sys
import picamera
from pkg_resources import require
import time
import picamera

from PyQt5.QtWidgets import (QApplication, QWidget,
                             QPushButton, QLabel, QRadioButton,
                             QMessageBox, QHBoxLayout, QVBoxLayout)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.Qt import qRed, qGreen, qBlue
from signal import signal, SIGINT

print(sys.version)
print(require('picamera'))

rpi_icon = 'rpi_icon_240.png'

class AppWindow(QWidget):
    
    camPreviewState = False  #not in Preview
    
    def __init__(self):
        super().__init__()

        self.camera = picamera.PiCamera()
        self.camera.resolution = (240, 240)
        
        lbSysInfo = QLabel('Python:\n' + sys.version)
        lbPicameraInfo = QLabel(str(require('picamera')))
        vboxInfo = QVBoxLayout()
        vboxInfo.addWidget(lbSysInfo)
        vboxInfo.addWidget(lbPicameraInfo)
        
        #setup UI
        btnPreview = QPushButton("Start Preview", self)
        btnPreview.clicked.connect(self.evBtnPreviewClicked)
        btnCapture = QPushButton("Capture", self)
        btnCapture.clicked.connect(self.evBtnCaptureClicked)
        
        lbFileName = QLabel('save as (Desktop/):')
        self.rbtnImage = QRadioButton('image.jpg')
        self.rbtnImage.setChecked(True)
        self.rbtnStamp = QRadioButton('img_<timestamp>.jpg')
        
        vboxCamControl = QVBoxLayout()
        vboxCamControl.addWidget(btnPreview)
        vboxCamControl.addWidget(btnCapture)
        vboxCamControl.addWidget(lbFileName)
        vboxCamControl.addWidget(self.rbtnImage)
        vboxCamControl.addWidget(self.rbtnStamp)
        vboxCamControl.addStretch()
        
        self.lbImg = QLabel(self)
        self.lbImg.resize(240, 240)
        self.lbImg.setStyleSheet("border: 1px solid black;")
        
        try:
            with open(rpi_icon):
                pixmap = QPixmap(rpi_icon)
                self.lbImg.setPixmap(pixmap)
        except FileNotFoundError:
            print('File Not Found Error')
        
        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()

    def evBtnPreviewClicked(self):
        if self.camPreviewState:
            print('Stop Preview')
            self.camera.stop_preview()
            self.sender().setText('Start Preview')
            self.camPreviewState = False
        else:
            print('Start Preview')
            self.camera.start_preview()
            self.sender().setText('Stop Preview')
            self.camPreviewState = True
        
    def evBtnCaptureClicked(self):
        print('evBtnCaptureClicked()')
        print("Capture")
        
        if self.rbtnImage.isChecked():
            targetPath="/home/pi/Desktop/image.jpg" 
        else:
            timeStamp = time.strftime("%Y%m%d-%H%M%S")
            targetPath="/home/pi/Desktop/img_"+timeStamp+".jpg"
        
        print(targetPath)
        
        self.camera.capture(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')
        
    def closeEvent(self, event):
        confirmClose = QMessageBox.question(self,
                                            "Quit App?",
                                            "Confirm to Quit?",
                                            QMessageBox.No | QMessageBox.Yes,
                                            QMessageBox.Yes)
        if confirmClose == QMessageBox.Yes:
            print('Confirmed Close')
            self.camera.close()
            event.accept()
        else:
            event.ignore()
        
if __name__ == '__main__':
    print('run __main__')
    app = QApplication(sys.argv)
    window = AppWindow()
    sys.exit(app.exec_())

print("- bye -")

Download the thumbnail image, save as "rpi_icon_240.png" in the same folder. Used as a default image only.



Saturday, March 20, 2021

ESP32 (ESP32-DevKitC V4)/MicroPython + 240*240 IPS (ST7789 SPI) using russhughes/st7789py_mpy lib

To display with 240*240 IPS (ST7789 SPI) LCD on ESP32 (ESP32-DevKitC V4)/MicroPython using russhughes/st7789py_mpy lib.

Connection:

In my exercise, SPI2 is used to send command to ST7789.

ST7789		ESP32-DevKitC
GND		GND
VCC		3V3
SCL		GPIO18
SDA		GPIO23
RES		GPIO5
DC		GPIO4
BLK		3V3


Install library:

Visit https://github.com/russhughes/st7789py_mpy, download lib/st7789py.py, save to ESP32.

In my test, have to edit st7789py.py littlle bit, otherwise it will be reported with error:
AttributeError: 'ST7789' object has no attribute 'xstart'

Edit st7789py.py, to add the line under  def __init__():
	self.xstart = xstart
	self.ystart = ystart

Download fonts/vga1_16x32.py (or any font files you want) to ESP32 under new directory "fonts".

Example code:

upyESP32_st7789.py
"""
ESP32-DevKitC V4/MicroPython exercise
240x240 ST7789 SPI LCD
using MicroPython library:
https://github.com/russhughes/st7789py_mpy

"""

import uos
import machine
import st7789py as st7789
from fonts import vga1_16x32 as font
import random
import ustruct as struct
import utime

"""
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
CENTER_Y = int(disp_width/2)
CENTER_X = int(disp_height/2)

print(uos.uname())
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)
display.text(font, "Hello!", 10, 10)
display.text(font, "ESP32", 10, 40)
display.text(font, "MicroPython", 10, 70)
display.text(font, "ST7789 SPI", 10, 100)
display.text(font, "240*240 IPS", 10, 130)

for i in range(1000):
    display.pixel(random.randint(0, disp_width),
          random.randint(0, disp_height),
          st7789.color565(random.getrandbits(8),random.getrandbits(8),random.getrandbits(8)))

# Helper function to draw a circle from a given position with a given radius
# This is an implementation of the midpoint circle algorithm,
# see https://en.wikipedia.org/wiki/Midpoint_circle_algorithm#C_example 
# for details
def draw_circle(xpos0, ypos0, rad, col=st7789.color565(255, 255, 255)):
    x = rad - 1
    y = 0
    dx = 1
    dy = 1
    err = dx - (rad << 1)
    while x >= y:
        display.pixel(xpos0 + x, ypos0 + y, col)
        display.pixel(xpos0 + y, ypos0 + x, col)
        display.pixel(xpos0 - y, ypos0 + x, col)
        display.pixel(xpos0 - x, ypos0 + y, col)
        display.pixel(xpos0 - x, ypos0 - y, col)
        display.pixel(xpos0 - y, ypos0 - x, col)
        display.pixel(xpos0 + y, ypos0 - x, col)
        display.pixel(xpos0 + x, ypos0 - y, col)
        if err <= 0:
            y += 1
            err += dy
            dy += 2
        if err > 0:
            x -= 1
            dx += 2
            err += dx - (rad << 1)
            
draw_circle(CENTER_X, CENTER_Y, 100, st7789.color565(255, 255, 255))
draw_circle(CENTER_X, CENTER_Y, 97, st7789.color565(255, 0, 0))
draw_circle(CENTER_X, CENTER_Y, 94, st7789.color565(0, 255, 0))
draw_circle(CENTER_X, CENTER_Y, 91, st7789.color565(0, 0, 255))
utime.sleep(2)

display.fill(st7789.BLACK)
display.text(font, "Test various", 20, 10)
display.text(font, "approach to", 20, 50)
display.text(font, "fill pixels", 20, 90)
utime.sleep(2)

#test various approach to fill pixels
display.fill(st7789.BLACK)
display.text(font, "pixel()", 20, 10)
display.text(font, "optimized", 20, 70)
display.text(font, "blit_buffer()", 20, 130)
display.text(font, "fill_rect()", 20, 190)
utime.sleep(1)

# fill area with display.pixel()
ms_start = utime.ticks_ms()
for y in range(60):
    for x in range(240):
        display.pixel(x, y, st7789.color565(x, 0, 0))
ms_now = utime.ticks_ms()
display.text(font, str(utime.ticks_diff(ms_now,ms_start))+" ms", 50, 10)

# fill area optimized
#!!! may be NOT suit your setup
ms_start = utime.ticks_ms()
display.set_window(0, 60, 239, 119)
pin_st7789_dc.on()
for y in range(60, 120):
    for x in range(240):
        spi2.write(struct.pack(st7789._ENCODE_PIXEL,
                               (0 & 0xf8) << 8 | (x & 0xfc) << 3 | 0 >> 3))
ms_now = utime.ticks_ms()
display.text(font, str(utime.ticks_diff(ms_now,ms_start))+" ms", 50, 70)

# fill with blit_buffer(buffer, x, y, width, height)
buffer = bytearray(240*60*2)

ms_pre = utime.ticks_ms()
#prepare buffer
for y in range(60):
    for x in range(240):
        idx = ((y*240) + x)*2
        pxCol = (y & 0xf8) << 8 | (0 & 0xfc) << 3 | x >> 3
        packedPx = struct.pack(st7789._ENCODE_PIXEL, pxCol)
        buffer[idx] = packedPx[0]
        buffer[idx+1] = packedPx[1]

ms_start = utime.ticks_ms()
display.blit_buffer(buffer, 0, 120, 240, 60)
ms_now = utime.ticks_ms()
strToDisp = str(utime.ticks_diff(ms_start,ms_pre)) + \
    "/" + str(utime.ticks_diff(ms_now,ms_start)) + " ms"
display.text(font, strToDisp, 50, 130)

# fill area with display.fill_rect()
ms_start = utime.ticks_ms()
display.fill_rect(0, 180, 240, 60, st7789.color565(0, 150, 150))
ms_now = utime.ticks_ms()
display.text(font, str(utime.ticks_diff(ms_now,ms_start))+" ms", 50, 190)

print("- bye-")
Next:
Raspberry Pi/Python send image to ESP32/MicroPython via WiFi TCP socket, display on this 240*240 IPS (ST7789 SPI) LCD.


Remark about SPI@2021-04-07:

The above exercise run on version MicroPython version 'v1.14 on 2021-03-17', SPI2 defined with default pin assignment.

Recently, I tested it on 'MicroPython v1.14 2021-02-02' (stable version esp32spiram-idf4-20210202-v1.14.bin). It's founded there are NO default pin assigned to SPI.


So you have to define the pins in your code,like this:
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)
On current latest unstable version esp32spiram-20210407-unstable-v1.14-142-gcb396827f.bin, default pins are defined. Pins can be omitted using default assignment.




Friday, March 19, 2021

Install and run rpi-imager (Raspberry Pi Imager) on Raspberry Pi

Raspberry Pi Imager update to v1.6.

To install Raspberry Pi Imager on Raspberry Pi (running Raspberry Pi OS 32-bit), enter the command in Terminal:

$ sudo apt install rpi-imager




Once installed, run it in Start Menu > Accessories > Imager




In my first test, it fail when run remotely using xrdp/Remote Desktop.


but work normally on host locally.





Thursday, March 18, 2021

Python exercise: get RGB array of image using matplotlib

Python code to get get RGB array of image using matplotlib:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

"""
ref:
A short tutorial on plotting images with Matplotlib:
https://matplotlib.org/stable/tutorials/introductory/images.html

numpy.ndarray:
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html

"""
imageFile = '/home/pi/Desktop/image.jpg'
img = mpimg.imread(imageFile)

print(type(img))
print(dir(img))

print('shape (= length of each dimension):')
print(img.shape)
print()
print(img[0][0])
print(img[0][249])
print(img[249][0])
print(img[249][249])

print()
print(img)

imgplot = plt.imshow(img)
plt.show()




Saturday, March 13, 2021

RPi Pico + ESP32-S remote control ESP32-DevKitC via WiFi TCP, using MicroPython.

This exercise program Raspberry Pi Pico/MicroPython + ESP32-S (ESP-AT) as WiFi TCP client to remote control ESP32-DevKitC V4/MicroPython WiFi TCP server onboard LED.

In Client side:

ESP32-S is a wireless module based on ESP32. It's flashed with AT-command firmware ESP-AT. It's act as a WiFi co-processor. Raspberry Pi Pico/MicroPython send AT-command to ESP32-S via UART. Please note that for ESP32 flashed with ESP-AT: UART1 (IO16/IO17) is used to send AT commands and receive AT responses, connected to GP0/GP1 of Pico.

Pico GP15 connected to ESP32-S EN pin, to reset it in power up.

Pico GP16 is used control remote LED.

In Server side:

ESP32-DevKitC V4 (with ESP32-WROVER-E module)/MicroPython is programed as WiFi server, receive command from client to turn ON/OFF LED accordingly.

Connection:


MicroPython code:

Client side, mpyPico_ESP32S_remoteCli_.py run on Raspberry Pi Pico.
import uos
import machine
import utime
"""
Raspberry Pi Pico/MicroPython + ESP32-S exercise

ESP32-S with AT-command firmware (ESP-AT):
---------------------------------------------------
AT version:2.1.0.0(883f7f2 - Jul 24 2020 11:50:07)
SDK version:v4.0.1-193-ge7ac221
compile time(0ad6331):Jul 28 2020 02:47:21
Bin version:2.1.0WROM-3)
---------------------------------------------------

Pico send AT command to ESP32-S via UART,
Send command to server (ESP32/MicroPython)
to turn ON/OFF LED on server.
---------------------------------------------------
Connection:
powered by separated power supply
Pico                  ESP32-S
GND                   GND
GP0 (TX) (pin 1)      IO16 (RXD)
GP1 (RX) (pin 2)      IO17 (TXD)
GP15 (pin 20)         EN

GP16 (pin 21) button
---------------------------------------------------
"""
#server port & ip hard-coded,
#have to match with server side setting
server_ip="192.168.4.1"
server_port=8000

network_ssid = "ESP32-ssid"
network_password = "password"

ESP_EN = 15
PIN_ESP_EN = machine.Pin(ESP_EN, machine.Pin.IN, machine.Pin.PULL_UP)
DIn = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP)


print()
print("Machine: \t" + uos.uname()[4])
print("MicroPython: \t" + uos.uname()[3])
#indicate program started visually
led_onboard = machine.Pin(25, machine.Pin.OUT)
led_onboard.value(0)     # Toggle onboard LED 
utime.sleep(0.5)         # to indiacte program start
led_onboard.value(1)
utime.sleep(1)
led_onboard.value(0)

#Reset ESP
PIN_ESP_EN = machine.Pin(ESP_EN, machine.Pin.OUT)
PIN_ESP_EN.value(1)
utime.sleep(0.5)
PIN_ESP_EN.value(0)
utime.sleep(0.5)
PIN_ESP_EN.value(1)
PIN_ESP_EN = machine.Pin(ESP_EN, machine.Pin.IN, machine.Pin.PULL_UP)

uart = machine.UART(0, baudrate=115200)
print(uart)

RES_OK = b'OK\r\n'
len_OK = len(RES_OK)

RESULT_OK      = '0'
RESULT_TIMEOUT = '1'
def sendCMD_waitResult(cmd, timeout=2000):
    print("CMD: " + cmd)
    uart.write(cmd)

    prvMills = utime.ticks_ms()
    result = RESULT_TIMEOUT
    resp = b""
    
    while (utime.ticks_ms()-prvMills)<timeout:
        if uart.any():
            resp = b"".join([resp, uart.read(1)])
            resp_len = len(resp)
            if resp[resp_len-len_OK:]==RES_OK:
                print(RES_OK + " found!")
                result = RESULT_OK
                break
    
    print("resp:")
    try:
        print(resp.decode())
    except UnicodeError:
        print(resp)

    return result

#to make it simple to detect, RMCMD & RMSTA designed same length
#Remote Command from client to serve
RMCMD_len = 6
RMCMD_ON  = "LEDONN"   #turn LED ON
RMCMD_OFF = "LEDOFF"   #turn LED OFF
#Remote status from server to client
RMSTA_len = 6
RMSTA_timeout = "timeout" #time out without/unknown reply
RMSTA_LEDON  = "LedOnn"
RMSTA_LEDOFF = "LedOff"
"""
#Expected flow to send command to wifi is:
Pico (client) to ESP-01S    response from ESP-01S to Pico
AT+CIPSEND=<cmd len>\r\n
                            AT+CIPSEND=<cmd len>\r\n
                            OK\r\n
                            >\r\n
<cmd>
                            Recv x bytes\r\n
                            SEND OK\r\n           ---> ESP (server)
                                                  <--- ESP (server) end with OK\r\n
                            +IPD,10:<RMSTA>OK\r\n
                            +IPD,2:\r\n
"""
def sendRemoteCmd(rmcmd, timeout=2000):
    result = RMSTA_timeout
    
    if sendCMD_waitResult('AT+CIPSEND=' + str(len(rmcmd)) + '\r\n', timeout)==RESULT_OK:
        
        #dummy read '>'
        while not uart.any():
            pass
        print(uart.read(1))
        
        print("Remote CMD: " + rmcmd)
        if sendCMD_waitResult(rmcmd) == RESULT_OK:
            
            endMills = utime.ticks_ms() + timeout
            resp = b""
            
            while utime.ticks_ms()<endMills:
                if uart.any():
                    resp = b"".join([resp, uart.read(1)])
                    resp_len = len(resp)
                    if resp[resp_len-len_OK:]==RES_OK:
                        print(RES_OK + " found!")
                        rmSta=resp[resp_len-len_OK-RMSTA_len:resp_len-len_OK]
                        strRmSta=rmSta.decode()   #convert bytes to string
                        print(strRmSta)
                        if strRmSta == RMSTA_LEDON:
                            result = strRmSta
                        elif strRmSta == RMSTA_LEDOFF:
                            result = strRmSta
                        break
            print("resp:")
            try:
                print(resp.decode())
            except UnicodeError:
                print(resp)
    
    return result

def sendCMD_waitResp(cmd, timeout=2000):
    print("CMD: " + cmd)
    uart.write(cmd)
    waitResp(timeout)
    print()
    
def waitResp(timeout=2000):
    prvMills = utime.ticks_ms()
    resp = b""
    while (utime.ticks_ms()-prvMills)<timeout:
        if uart.any():
            resp = b"".join([resp, uart.read(1)])
    print("resp:")
    try:
        print(resp.decode())
    except UnicodeError:
        print(resp)
        
"""
everytimes send command to server:
- join ESP32 network
- connect to ESP32 socket
- send command to server and receive status
"""
def connectRemoteSendCmd(cmdsend):
    clearRxBuf()
    print("join wifi network: " + "ESP32-ssid")
    while sendCMD_waitResult('AT+CWJAP="' + network_ssid + '","'
                       + network_password + '"\r\n') != RESULT_OK:
        pass
    
    sendCMD_waitResp('AT+CIFSR\r\n')    #Obtain the Local IP Address
    sendCMD_waitResult('AT+CIPSTATUS\r\n')
    
    print("wifi network joint")
    print("connect socket")

    if sendCMD_waitResult('AT+CIPSTART="TCP","'
                      + server_ip
                      + '",'
                      + str(server_port) + '\r\n', timeout=5000) == RESULT_OK:
        sendCMD_waitResult('AT+CIPSTATUS\r\n')
        print("RMST: " + sendRemoteCmd(rmcmd=cmdsend))

    clearRxBuf()
    sendCMD_waitResult('AT+CIPSTATUS\r\n')
    sendCMD_waitResult('AT+CWQAP\r\n')

            
def clearRxBuf():
    print("--- clear Rx buffer ---")
    buf = b""
    while uart.any():
        buf = b"".join([buf, uart.read(1)])
    print(buf)
    print("-----------------------")
    
led_onboard.value(0)
clearRxBuf()
sendCMD_waitResult('AT\r\n')          #Test AT startup
sendCMD_waitResult('AT+CWMODE=1\r\n') #Set the Wi-Fi mode 1 = Station mode
sendCMD_waitResult('AT+CIPMUX=0\r\n') #single connection.

led_onboard.value(1)
connectRemoteSendCmd(cmdsend=RMCMD_ON)
utime.sleep(1)
connectRemoteSendCmd(cmdsend=RMCMD_OFF)

#fast toggle led 5 times to indicate startup finished
for i in range(5):
    led_onboard.value(0)
    utime.sleep(0.2)
    led_onboard.value(1)
    utime.sleep(0.2)
    led_onboard.value(0)

print("Started")
print("waiting for button")
#read digital input every 10ms
dinMills = utime.ticks_ms() + 30
prvDin = 1
debounced = False
while True:
    if utime.ticks_ms() > dinMills:
        dinMills = utime.ticks_ms() + 30
        curDin = DIn.value()
        if curDin != prvDin:
            #Din changed
            prvDin = curDin
            debounced = False
        else:
            if not debounced:
                #DIn changed for > 30ms
                debounced = True
                if curDin:
                    connectRemoteSendCmd(cmdsend=RMCMD_OFF)
                else:
                    connectRemoteSendCmd(cmdsend=RMCMD_ON)
                    
                
    

Server side, upyESP32_AP_RemoteSvr_.py run on ESP32-DevKitC V4.
import utime
import uos
import network
import usocket
from machine import Pin

"""
ESP32/MicroPython exercise:
ESP32 act as Access Point,
and setup a simple TCP server

receive command from client and turn ON/OFF LED,
and send back status.
"""

ssid= "ESP32-ssid"
password="password"

led=Pin(13,Pin.OUT)

print("----- MicroPython -----")
for u in uos.uname():
    print(u)
print("-----------------------")

for i in range(3):
    led.on()
    utime.sleep(0.5)
    led.off()
    utime.sleep(0.5)
    

ap = network.WLAN(network.AP_IF) # Access Point
ap.config(essid=ssid,
          password=password,
          authmode=network.AUTH_WPA_WPA2_PSK) 
ap.config(max_clients=1)  # max number of client
ap.active(True)           # activate the access point

print(ap.ifconfig())
print(dir(ap))

mysocket = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
mysocket.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)

port = 8000
mysocket.bind(('',8000))
print("bind: " + str(port))
mysocket.listen(1)

#tomake it simple to detect, RMCMD & RMSTA designed same length
#Remote Command from client to serve
RMCMD_len = 6
RMCMD_ON  = "LEDONN"    #turn LED ON
RMCMD_OFF = "LEDOFF"   #turn LED OFF
#Remote status from server to client
RMSTA_len = 6
RMSTA_timeout = "timeout" #time out without/unknown reply
RMSTA_LEDON  = "LedOnn"
RMSTA_LEDOFF = "LedOff"

while True:
  conn, addr = mysocket.accept()
  print('Connected from: %s' % str(addr))
  print()
  request = conn.recv(1024)
  print('request: %s' % str(request))
  print()
  
  strRqs = request.decode()
  print("strRqs: "+strRqs)
  if strRqs==RMCMD_ON:
      conn.send(RMSTA_LEDON+'OK\r\n')
      led.on()
  elif strRqs==RMCMD_OFF:
      conn.send(RMSTA_LEDOFF+'OK\r\n')
      led.off()
  else:
      #unknown command
      conn.send(request.upper())
  conn.send('\r\n')
  conn.close()


Tuesday, March 9, 2021

RPi/Python Code to control Camera Module with preview on local HDMI

It's a Python3 code run on Raspberry Pi to control Camera Module with various setting. The preview display on local HDMI. 


In my usage scenario: The Raspberry Pi 4B/8G is installed with HQ Camera Module (mount with manual focus lens) and a 4 inch HDMI IPS Touch Display. Remote control with Android with xrdp/Microsoft Remote Desktop. Such that I can control and change setting on remote Android device, adjust focus/aperture on the lens, and check the effect on local HDMI preview at real-time.

It's a long time ago, I make a similar code with preview stream video, both control and preview on remote desktop, Python to capture image from Pi Camera Module, with image effects. But I'm not satisfied by the delay of stream video, that's why I re-develop this code again.


The lens shown on the video is a Nikkor ais 28mm f2.8 manual focus lens, connected to HQ Camera Module via a Nikon F to C mount adapter.

rpiCam_20210309a.py
import sys
import picamera
from pkg_resources import require
import time

"""
ref: Picamera
https://picamera.readthedocs.io/en/release-1.13/
"""

#from tkinter import *
#tkinter for Python 3
import tkinter as tk
from tkinter import ttk

def close_window():
    #close tasks
    camera.close()
    print("Camera closed")
    
    print("close_window()")
    print("Window closed")
    root.destroy()

#event callback functions
def evStartPreviewBtnPressed():
    print("Start Preview")
    camera.start_preview()

def evStopPreviewBtnPressed():
    print("Stop Preview")
    camera.stop_preview()

def evCaptureBtnPressed():
    print("Capture")
    timeStamp = time.strftime("%Y%m%d-%H%M%S")
    targetPath="/home/pi/Desktop/img_"+timeStamp+".jpg"
    print(targetPath)
    camera.capture(targetPath)
    
def cmdScaleSharpness(new_value):
    camera.sharpness = scaleSharpness.get()
    print("sharpness: " + str(camera.sharpness))
    
def cmdScaleContrast(new_value):
    camera.contrast = scaleContrast.get()
    print("contrast: " + str(camera.contrast))
    
def cmdScaleBrightness(new_value):
    camera.brightness = scaleBrightness.get()
    print("brightness: " + str(camera.brightness))
    
def cmdScaleSaturation(new_value):
    camera.saturation = scaleSaturation.get()
    print("saturation: " + str(camera.saturation))
    
def cmdScaleExpCompensation(NEW_VALUE):
    camera.exposure_compensation = scaleExpCompensation.get()
    print("exposure_compensation: " + str(camera.exposure_compensation))
    
def cmdRB_Iso():
    camera.iso = varIso.get()
    print("iso: " + str(camera.iso))

print(sys.version)
print(require('picamera'))
strInfo = str(sys.version) + str(require('picamera'))
type(sys.version)
type(require('picamera'))

#Prepare camera
camera = picamera.PiCamera()
#set default
camera.sharpness = +5
camera.contrast = 0
camera.brightness = 50
camera.saturation = 0
camera.iso = 100
camera.video_stabilization = False
camera.exposure_compensation = 0
camera.exposure_mode = 'auto'
camera.meter_mode = 'average'
camera.awb_mode = 'auto'
camera.image_effect = 'none'
camera.color_effects = None
camera.rotation = 0
camera.hflip = False
camera.vflip = False
camera.crop = (0.0, 0.0, 1.0, 1.0)
  
# not work for HQ
#camera.resolution = (4056, 3040)  # HQ
#camera.resolution = (2592, 1944)  # V1
camera.resolution = (3280, 2464)  # V2
#end of set default

SCALE_WIDTH = 940;

#Prepare GUI
root = tk.Tk()
#root.geometry("650x550")
root.geometry("960x800")
root.wm_title("doCamII")
root.protocol("WM_DELETE_WINDOW", close_window)

labelInfo = tk.Label(root,
                  text=strInfo, fg="gray",
                  font=("Helvetica", 14))
labelInfo.pack()

#Main control
frameMain = tk.Frame(root, bg="lightgray")

btnStartPreview = tk.Button(frameMain, text="Start Preview",
                    command=evStartPreviewBtnPressed)
btnStopPreview = tk.Button(frameMain, text="Stop Preview",
                    command=evStopPreviewBtnPressed)
btnStartPreview.grid(row=2, column=0, sticky=tk.W+tk.E)
btnStopPreview.grid(row=2, column=1, sticky=tk.W+tk.E)

btnCapture = tk.Button(frameMain, text="Capture",
                    command=evCaptureBtnPressed)
btnCapture.grid(row=3, columnspan=2, sticky=tk.W+tk.E)
frameMain.pack(padx=10,pady=10)

#Setting
notebook = ttk.Notebook(root)
frame1 = ttk.Frame(notebook)
frame2 = ttk.Frame(notebook)
frame3 = ttk.Frame(notebook)
notebook.add(frame1, text='Setting 1')
notebook.add(frame2, text='Setting 2')
notebook.add(frame3, text='Setting 3')
notebook.pack()

lfSharpness = ttk.LabelFrame(frame1, text="sharpness")
lfSharpness.pack(fill="x", expand="no", anchor=tk.N)
scaleSharpness = tk.Scale(
    lfSharpness,
    from_=-100, to=100,
    length=SCALE_WIDTH,
    orient=tk.HORIZONTAL,
    command=cmdScaleSharpness)
scaleSharpness.set(camera.sharpness)
scaleSharpness.pack()

lfContrast = ttk.LabelFrame(frame1, text="contrast")
lfContrast.pack(fill="x", expand="no", anchor=tk.N)
scaleContrast = tk.Scale(
    lfContrast,
    from_=-100, to=100,
    length=SCALE_WIDTH,
    orient=tk.HORIZONTAL,
    command=cmdScaleContrast)
scaleContrast.set(camera.contrast)
scaleContrast.pack()

lfBrightness = ttk.LabelFrame(frame1, text="brightness")
lfBrightness.pack(fill="x", expand="no", anchor=tk.N)
scaleBrightness = tk.Scale(
    lfBrightness,
    from_=0, to=100,
    length=SCALE_WIDTH,
    orient=tk.HORIZONTAL,
    command=cmdScaleBrightness)
scaleBrightness.set(camera.brightness)
scaleBrightness.pack()

lfSaturation = ttk.LabelFrame(frame1, text="saturation")
lfSaturation.pack(fill="x", expand="no", anchor=tk.N)
scaleSaturation = tk.Scale(
    lfSaturation,
    from_=-100, to=100,
    length=SCALE_WIDTH,
    orient=tk.HORIZONTAL,
    command=cmdScaleSaturation)
scaleSaturation.set(camera.saturation)
scaleSaturation.pack()

lfExpCompensation = ttk.LabelFrame(frame1, text="exposure_compensation")
lfExpCompensation.pack(fill="x", expand="no", anchor=tk.N)
scaleExpCompensation = tk.Scale(
    lfExpCompensation,
    from_=-25, to=25,
    length=SCALE_WIDTH,
    orient=tk.HORIZONTAL,
    command=cmdScaleExpCompensation)
scaleExpCompensation.set(camera.exposure_compensation)
scaleExpCompensation.pack()

#==========================================================
varIso = tk.IntVar()
varIso.set(camera.iso)
lfIso = ttk.LabelFrame(frame2, text="iso")
lfIso.pack(fill="x", expand="no", anchor=tk.N)
tk.Radiobutton(lfIso, variable=varIso,
        text='0 (auto)',value=0,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfIso, variable=varIso,
        text='100',value=100,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfIso, variable=varIso,
        text='200',value=200,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfIso, variable=varIso,
        text='400',value=400,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfIso, variable=varIso,
        text='800',value=800,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfIso, variable=varIso,
        text='1600',value=1600,command=cmdRB_Iso).pack(
            anchor=tk.W, side=tk.LEFT)
#==========================================================
#-- meter_mode command
def cmdMeterMode():
    camera.meter_mode = varMeterMode.get()
    print("meter_mode: " + str(camera.meter_mode))
#-- exposure_mode
lfMeterMode = ttk.LabelFrame(frame2, text="meter_mode")
lfMeterMode.pack(fill="x", expand="no", anchor=tk.N)
varMeterMode = tk.StringVar()
varMeterMode.set(camera.meter_mode)

tk.Radiobutton(lfMeterMode, variable=varMeterMode,
        text='average',value='average',command=cmdMeterMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfMeterMode, variable=varMeterMode,
        text='spot',value='spot',command=cmdMeterMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfMeterMode, variable=varMeterMode,
        text='backlit',value='backlit',command=cmdMeterMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfMeterMode, variable=varMeterMode,
        text='matrix',value='matrix',command=cmdMeterMode).pack(
            anchor=tk.W, side=tk.LEFT)


#==========================================================
#-- exposure_mode command
def cmdEposureMode():
    camera.exposure_mode = varExposureMode.get()
    print("exposure_mode: " + str(camera.exposure_mode))
#-- exposure_mode
lfExpMode = ttk.LabelFrame(frame2, text="exposure_mode")
lfExpMode.pack(fill="x", expand="no", anchor=tk.N)
varExposureMode = tk.StringVar()
varExposureMode.set(camera.exposure_mode)
lfExposure_mode1 = ttk.Frame(lfExpMode)
lfExposure_mode1.pack(fill="x", expand="no", anchor=tk.N)
lfExposure_mode2 = ttk.Frame(lfExpMode)
lfExposure_mode2.pack(fill="x", expand="no", anchor=tk.N)

tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='off',value='off',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='auto',value='auto',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='night',value='night',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)

tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='nightpreview',value='nightpreview',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='backlight',value='backlight',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='spotlight',value='spotlight',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='sports',value='sports',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode1, variable=varExposureMode,
        text='snow',value='snow',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)

tk.Radiobutton(lfExposure_mode2, variable=varExposureMode,
        text='beach',value='beach',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode2, variable=varExposureMode,
        text='verylong',value='verylong',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode2, variable=varExposureMode,
        text='fixedfps',value='fixedfps',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode2, variable=varExposureMode,
        text='antishake',value='antishake',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfExposure_mode2, variable=varExposureMode,
        text='fireworks',value='fireworks',command=cmdEposureMode).pack(
            anchor=tk.W, side=tk.LEFT)
#==========================================================
#common button handler for ImageEffect without parameter setting,
#simple set camera.image_effect
def butComImageEffect():
    camera.image_effect=varImageEffect.get()
    labelImageEffectVar.set(camera.image_effect)

#----- ImageEffect 'solarise' ui event
def butImageEffect_solarize():
    camera.image_effect=varImageEffect.get()
    if cbSolarize_yuv_Var.get():
        yuv = 1
    else:
        yuv = 0
    solarize_para = (yuv,
                     scSolarize_x0_Var.get(),
                     scSolarize_y0_Var.get(),
                     scSolarize_y1_Var.get(),
                     scSolarize_y2_Var.get())
    camera.image_effect_params = solarize_para
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_solarizePara(new_value=None):
    varImageEffect.set("solarize")
    butImageEffect_solarize()

#----- ImageEffect 'watercolor' ui event ---
def butImageEffect_watercolor():
    camera.image_effect=varImageEffect.get()
    
    if cbWatercolor_uv_Var.get():
        watercolor_para = (scWatercolor_u_Var.get(),
                           scWatercolor_v_Var.get())
        camera.image_effect_params = watercolor_para
    else:
        watercolor_para = ()
        camera.image_effect_params = watercolor_para
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_watercolorPara(new_value=None):
    varImageEffect.set("watercolor")
    butImageEffect_watercolor()
    
#----- ImageEffect 'film' ui event ---
def butImageEffect_film():
    camera.image_effect=varImageEffect.get()
    
    film_para = (scFilm_strength_Var.get(),
                 scFilm_u_Var.get(),
                 scFilm_v_Var.get())
    camera.image_effect_params = film_para
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_filmPara(new_value=None):
    varImageEffect.set("film")
    butImageEffect_film()
    
#----- ImageEffect 'blur' ui event ---
def butImageEffect_blur():
    camera.image_effect=varImageEffect.get()
    
    camera.image_effect_params = scBlur_size_Var.get()
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_blurPara(new_value=None):
    varImageEffect.set("blur")
    butImageEffect_blur()
    
#----- ImageEffect 'colorswap' ui event ---
def butImageEffect_colorswap():
    camera.image_effect=varImageEffect.get()
    
    camera.image_effect_params = cbColorswap_dir_Var.get()
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_colorswapPara(new_value=None):
    varImageEffect.set("colorswap")
    butImageEffect_colorswap()

#----- ImageEffect 'posterise' ui event ---
def butImageEffect_posterise():
    camera.image_effect=varImageEffect.get()

    camera.image_effect_params = scPosterise_steps_Var.get()

    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_posterisePara(new_value=None):
    varImageEffect.set("posterise")
    butImageEffect_posterise()

#----- ImageEffect 'colorpoint' ui event ---
def butImageEffect_colorpoint():
    camera.image_effect=varImageEffect.get()

    camera.image_effect_params = quadrantVar.get()
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_colorpointPara(new_value=None):
    varImageEffect.set("colorpoint")
    butImageEffect_colorpoint()

#----- ImageEffect 'colorbalance' ui event ---
def butImageEffect_colorbalance():
    camera.image_effect=varImageEffect.get()

    colorbalance_para = (scColorbalance_lens_Var.get(),
                         scColorbalance_r_Var.get(),
                         scColorbalance_g_Var.get(),
                         scColorbalance_b_Var.get(),
                         scColorbalance_u_Var.get(),
                         scColorbalance_v_Var.get())
    camera.image_effect_params = colorbalance_para
    
    labelImageEffectVar.set(camera.image_effect +
                            " " + str(camera.image_effect_params))
    
def ev_colorbalancePara(new_value=None):
    varImageEffect.set("colorbalance")
    butImageEffect_colorbalance()
#-----------------------------------------------------
#-----------------------------------------------------
    
# Tab Image Effect
varImageEffect = tk.StringVar()
labelImageEffectVar = tk.StringVar()
image_effect_setting = camera.image_effect
varImageEffect.set(image_effect_setting)
labelImageEffectVar.set(image_effect_setting)
tk.Label(frame3, textvariable=labelImageEffectVar).pack(anchor=tk.N)

#-- image_effect

lfNoParaOpts1 = ttk.Frame(frame3)
lfNoParaOpts1.pack(fill="x", expand="yes", anchor=tk.N)

tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='none',value='none',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='negative',value='negative',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='sketch',value='sketch',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='denoise',value='denoise',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='emboss',value='emboss',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='oilpaint',value='oilpaint',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='hatch',value='hatch',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts1, variable=varImageEffect,
        text='gpen',value='gpen',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)

lfNoParaOpts2 = ttk.Frame(frame3)
lfNoParaOpts2.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='pastel',value='pastel',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='saturation',value='saturation',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='washedout',value='washedout',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='cartoon',value='cartoon',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='deinterlace1',value='deinterlace1',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfNoParaOpts2, variable=varImageEffect,
        text='deinterlace2',value='deinterlace2',command=butComImageEffect).pack(
            anchor=tk.W, side=tk.LEFT)

lfSolarize = ttk.LabelFrame(frame3, text="solarize")
lfSolarize.pack(fill="x", expand="yes", anchor=tk.N)

tk.Radiobutton(lfSolarize, variable=varImageEffect,
        text='solarize',value='solarize',command=butImageEffect_solarize).pack(
            anchor=tk.W, side=tk.LEFT)

cbSolarize_yuv_Var = tk.BooleanVar()
tk.Checkbutton(lfSolarize, text="yuv",
    variable=cbSolarize_yuv_Var, command=ev_solarizePara).pack(
        anchor=tk.W, side=tk.LEFT)

scSolarize_x0_Var = tk.IntVar()
scSolarize_x0_Var.set(128)
tk.Scale(lfSolarize, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="x0",
    variable=scSolarize_x0_Var, command=ev_solarizePara).pack(
        anchor=tk.W, side=tk.LEFT)

scSolarize_y0_Var = tk.IntVar()
scSolarize_y0_Var.set(128)
tk.Scale(lfSolarize, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="y0",
    variable=scSolarize_y0_Var, command=ev_solarizePara).pack(
        anchor=tk.W, side=tk.LEFT)

scSolarize_y1_Var = tk.IntVar()
scSolarize_y1_Var.set(128)
tk.Scale(lfSolarize, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="y1",
    variable=scSolarize_y1_Var, command=ev_solarizePara).pack(
        anchor=tk.W, side=tk.LEFT)

scSolarize_y2_Var = tk.IntVar()
scSolarize_y2_Var.set(0)
tk.Scale(lfSolarize, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="y2",
    variable=scSolarize_y2_Var, command=ev_solarizePara).pack(
        anchor=tk.W, side=tk.LEFT)

lfwatercolor = ttk.LabelFrame(frame3, text="watercolor")
lfwatercolor.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfwatercolor, variable=varImageEffect,
        text='watercolor',value='watercolor',command=butImageEffect_watercolor
               ).pack(anchor=tk.W, side=tk.LEFT)

cbWatercolor_uv_Var = tk.BooleanVar()
cbWatercolor_uv_Var.set(False)
tk.Checkbutton(lfwatercolor, text="uv",
    variable=cbWatercolor_uv_Var, command=ev_watercolorPara).pack(
        anchor=tk.W, side=tk.LEFT)

scWatercolor_u_Var = tk.IntVar()
scWatercolor_u_Var.set(0)
tk.Scale(lfwatercolor, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="u",
    variable=scWatercolor_u_Var, command=ev_watercolorPara).pack(
        anchor=tk.W, side=tk.LEFT)
scWatercolor_v_Var = tk.IntVar()
scWatercolor_v_Var.set(0)
tk.Scale(lfwatercolor, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="v",
    variable=scWatercolor_v_Var, command=ev_watercolorPara).pack(
        anchor=tk.W, side=tk.LEFT)

lffilm = ttk.LabelFrame(frame3, text="film")
lffilm.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lffilm, variable=varImageEffect,
        text='film',value='film',command=butImageEffect_film).pack(
            anchor=tk.W, side=tk.LEFT)

scFilm_strength_Var = tk.IntVar()
scFilm_strength_Var.set(0)
tk.Scale(lffilm, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="strength",
    variable=scFilm_strength_Var, command=ev_filmPara).pack(
        anchor=tk.W, side=tk.LEFT)
scFilm_u_Var = tk.IntVar()
scFilm_u_Var.set(0)
tk.Scale(lffilm, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="u",
    variable=scFilm_u_Var, command=ev_filmPara).pack(anchor=tk.W, side=tk.LEFT)
scFilm_v_Var = tk.IntVar()
scFilm_v_Var.set(0)
tk.Scale(lffilm, from_=0, to=255,
    orient=tk.HORIZONTAL, length=200, label="v",
    variable=scFilm_v_Var, command=ev_filmPara).pack(anchor=tk.W, side=tk.LEFT)

lfblur = ttk.LabelFrame(frame3, text="blur")
lfblur.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfblur, variable=varImageEffect,
        text='blur',value='blur',command=butImageEffect_blur).pack(
            anchor=tk.W, side=tk.LEFT)
scBlur_size_Var = tk.IntVar()
scBlur_size_Var.set(1)
tk.Scale(lfblur, from_=1, to=2,
    orient=tk.HORIZONTAL, length=100, label="size",
    variable=scBlur_size_Var, command=ev_blurPara).pack(anchor=tk.W, side=tk.LEFT)

lfcolorswap = ttk.LabelFrame(frame3, text="colorswap")
lfcolorswap.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfcolorswap, variable=varImageEffect,
        text='colorswap',value='colorswap',command=butImageEffect_colorswap).pack(
            anchor=tk.W, side=tk.LEFT)
cbColorswap_dir_Var = tk.BooleanVar()
cbColorswap_dir_Var.set(False)
tk.Checkbutton(lfcolorswap, text="dir - 0:RGB to BGR/1:RGB to BRG",
    variable=cbColorswap_dir_Var, command=ev_colorswapPara).pack(
        anchor=tk.W, side=tk.LEFT)

lfposterise = ttk.LabelFrame(frame3, text="posterise")
lfposterise.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfposterise, variable=varImageEffect,
        text='posterise',value='posterise',command=butImageEffect_posterise).pack(
            anchor=tk.W, side=tk.LEFT)
scPosterise_steps_Var = tk.IntVar()
scPosterise_steps_Var.set(4)
tk.Scale(lfposterise, from_=2, to=32,
    orient=tk.HORIZONTAL, length=200, label="steps",
    variable=scPosterise_steps_Var, command=ev_posterisePara).pack(
        anchor=tk.W, side=tk.LEFT)

lfcolorpoint = ttk.LabelFrame(frame3, text="colorpoint")
lfcolorpoint.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfcolorpoint, variable=varImageEffect,
        text='colorpoint',value='colorpoint',command=butImageEffect_colorpoint).pack(
            anchor=tk.W, side=tk.LEFT)
quadrantVar = tk.IntVar()
quadrantVar.set(0)
tk.Radiobutton(lfcolorpoint, text="green",
    variable=quadrantVar, value=0, command=ev_colorpointPara).pack(
        anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfcolorpoint, text="red/yellow",
    variable=quadrantVar, value=1, command=ev_colorpointPara).pack(
        anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfcolorpoint, text="blue",
    variable=quadrantVar, value=2, command=ev_colorpointPara).pack(
        anchor=tk.W, side=tk.LEFT)
tk.Radiobutton(lfcolorpoint, text="purple",
    variable=quadrantVar, value=3, command=ev_colorpointPara).pack(
        anchor=tk.W, side=tk.LEFT)

lfcolorbalance = ttk.LabelFrame(frame3, text="colorbalance: I can't see any effect!")
lfcolorbalance.pack(fill="x", expand="yes", anchor=tk.N)
tk.Radiobutton(lfcolorbalance, variable=varImageEffect,
        text='colorbalance',value='colorbalance',command=butImageEffect_colorbalance).pack(
            anchor=tk.W, side=tk.LEFT)

scColorbalance_lens_Var = tk.DoubleVar()
scColorbalance_lens_Var.set(0)
tk.Scale(lfcolorbalance, from_=0, to=256,
    orient=tk.HORIZONTAL, length=140, label="lens",
    variable=scColorbalance_lens_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

scColorbalance_r_Var = tk.DoubleVar()
scColorbalance_r_Var.set(1)
tk.Scale(lfcolorbalance, from_=0, to=256,
    orient=tk.HORIZONTAL, length=140, label="r",
    variable=scColorbalance_r_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

scColorbalance_g_Var = tk.DoubleVar()
scColorbalance_g_Var.set(1)
tk.Scale(lfcolorbalance, from_=0, to=256,
    orient=tk.HORIZONTAL, length=140, label="g",
    variable=scColorbalance_g_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

scColorbalance_b_Var = tk.DoubleVar()
scColorbalance_b_Var.set(1)
tk.Scale(lfcolorbalance, from_=0, to=256,
    orient=tk.HORIZONTAL, length=140, label="b",
    variable=scColorbalance_b_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

scColorbalance_u_Var = tk.IntVar()
scColorbalance_u_Var.set(0)
tk.Scale(lfcolorbalance, from_=0, to=255,
    orient=tk.HORIZONTAL, length=140, label="u",
    variable=scColorbalance_u_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

scColorbalance_v_Var = tk.IntVar()
scColorbalance_v_Var.set(0)
tk.Scale(lfcolorbalance, from_=0, to=255,
    orient=tk.HORIZONTAL, length=140, label="v",
    variable=scColorbalance_v_Var, command=ev_colorbalancePara).pack(
        anchor=tk.W, side=tk.LEFT)

#==========================================================
root.mainloop()

print("- bye -")


Remark:
As this is writing, picamera is still 1.13, so the maximum resolution is 3280x2464 for V2, not HQ. 



~ Another minimum version:  with minimum functions preview and capture only, using PyQt5 GUI, fixed resolution 240x240, and display the captured image on GUI. You can also choice save file name; image.jpg or img_<timestamp>.jpg, under Desktop folder.

Related:

Saturday, March 6, 2021

RPi Pico/MicroPython + ILI9341 SPI Display with Touch, using rdagger/micropython-ili9341

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. I have other exercises using jeffmer/micropython-ili9341 library. This exercise using another lib rdagger/micropython-ili9341.

rdagger/micropython-ili9341 is a MicroPython ILI9341 Display and XPT2046 Touch Screen Drivers.

Connection:



Library:

Visit rdagger/micropython-ili9341, copy ili9341.py, xglcd_font.py and fonts/Unispace12x24.c to your Raspberry Pi Pico device.

Exercise code:

To make it easy to update the lib's examples, I define the hardware connection in separated code, mySetup.py. Save it to Raspberry Pi Pico device.
from ili9341 import Display
from machine import Pin, SPI

TFT_CLK_PIN = const(6)
TFT_MOSI_PIN = const(7)
TFT_MISO_PIN = const(4)

TFT_CS_PIN = const(13)
TFT_RST_PIN = const(14)
TFT_DC_PIN = const(15)

def createMyDisplay():
    #spi = SPI(0, baudrate=40000000, sck=Pin(TFT_CLK_PIN), mosi=Pin(TFT_MOSI_PIN))
    spiTFT = SPI(0, baudrate=51200000,
                 sck=Pin(TFT_CLK_PIN), mosi=Pin(TFT_MOSI_PIN))
    display = Display(spiTFT,
                      dc=Pin(TFT_DC_PIN), cs=Pin(TFT_CS_PIN), rst=Pin(TFT_RST_PIN))
    return display
        


My exercise code, mPico_ili9341_test.py.
"""
Raspperry Pi Pico exercise display on ili9341 SPI Display
using rdagger/micropython-ili9341,
MicroPython ILI9341 Display and XPT2046 Touch Screen Drivers
https://github.com/rdagger/micropython-ili9341
"""
from machine import Pin, SPI
from sys import implementation
from os import uname
import utime

import ili9341
from xglcd_font import XglcdFont

import mySetup

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

print(SPI(0))
print(SPI(1))

display = mySetup.createMyDisplay()

print('Loading fonts...')
print('Loading unispace')
unispace = XglcdFont('fonts/Unispace12x24.c', 12, 24)

display.draw_text(0, 0, ili9341.__name__, unispace,
                  ili9341.color565(255, 128, 0))
display.draw_text(0, 25, ili9341.implementation.name, unispace,
                  ili9341.color565(0, 0, 200))
display.draw_text(0, 50, str(ili9341.implementation.version), unispace,
                  ili9341.color565(0, 0, 200))

display.draw_text(0, 100, "https://github.com/", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 125, "rdagger/micropython-ili9341", unispace,
                  ili9341.color565(200, 200, 200))

display.draw_text(0, 175, "ABCDEFGHIJKLMNOPQRS", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 200, "TUVWXYZ", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 225, "abcdefghijklmnopqrs", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 250, "tuvwxyz", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 275, "01234567890", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 300, "~!@#$%^&*()_+`-={}[]", unispace,
                  ili9341.color565(200, 200, 200))
display.draw_text(0, 325, "\|;:'<>,.?/", unispace,
                  ili9341.color565(200, 200, 200))
    
for i in range(320):
    display.scroll(i)
    utime.sleep(0.02)
    
for i in range(320, 0, -1):
    display.scroll(i)
    utime.sleep(0.02)

utime.sleep(0.5)
# Display inversion on
display.write_cmd(display.INVON)
utime.sleep(2)
# Display inversion off
display.write_cmd(display.INVOFF)

while True:
    pass

print("- bye -")


I also modify some of the examples in the library.

demo_bouncing_boxes_.py
"""ILI9341 demo (bouncing boxes)."""
from machine import Pin, SPI
from random import random, seed
from ili9341 import Display, color565
from utime import sleep_us, ticks_cpu, ticks_us, ticks_diff
import mySetup

class Box(object):
    """Bouncing box."""

    def __init__(self, screen_width, screen_height, size, display, color):
        """Initialize box.

        Args:
            screen_width (int): Width of screen.
            screen_height (int): Width of height.
            size (int): Square side length.
            display (ILI9341): display object.
            color (int): RGB565 color value.
        """
        self.size = size
        self.w = screen_width
        self.h = screen_height
        self.display = display
        self.color = color
        # Generate non-zero random speeds between -5.0 and 5.0
        seed(ticks_cpu())
        r = random() * 10.0
        self.x_speed = 5.0 - r if r < 5.0 else r - 10.0
        r = random() * 10.0
        self.y_speed = 5.0 - r if r < 5.0 else r - 10.0

        self.x = self.w / 2.0
        self.y = self.h / 2.0
        self.prev_x = self.x
        self.prev_y = self.y

    def update_pos(self):
        """Update box position and speed."""
        x = self.x
        y = self.y
        size = self.size
        w = self.w
        h = self.h
        x_speed = abs(self.x_speed)
        y_speed = abs(self.y_speed)
        self.prev_x = x
        self.prev_y = y

        if x + size >= w - x_speed:
            self.x_speed = -x_speed
        elif x - size <= x_speed + 1:
            self.x_speed = x_speed

        if y + size >= h - y_speed:
            self.y_speed = -y_speed
        elif y - size <= y_speed + 1:
            self.y_speed = y_speed

        self.x = x + self.x_speed
        self.y = y + self.y_speed

    def draw(self):
        """Draw box."""
        x = int(self.x)
        y = int(self.y)
        size = self.size
        prev_x = int(self.prev_x)
        prev_y = int(self.prev_y)
        self.display.fill_hrect(prev_x - size,
                                prev_y - size,
                                size, size, 0)
        self.display.fill_hrect(x - size,
                                y - size,
                                size, size, self.color)


def test():
    """Bouncing box."""
    try:
        # Baud rate of 40000000 seems about the max
        #spi = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13))
        #display = Display(spi, dc=Pin(4), cs=Pin(16), rst=Pin(17))
        display = mySetup.createMyDisplay()
        
        display.clear()

        colors = [color565(255, 0, 0),
                  color565(0, 255, 0),
                  color565(0, 0, 255),
                  color565(255, 255, 0),
                  color565(0, 255, 255),
                  color565(255, 0, 255)]
        sizes = [12, 11, 10, 9, 8, 7]
        boxes = [Box(239, 319, sizes[i], display,
                 colors[i]) for i in range(6)]

        while True:
            timer = ticks_us()
            for b in boxes:
                b.update_pos()
                b.draw()
            # Attempt to set framerate to 30 FPS
            timer_dif = 33333 - ticks_diff(ticks_us(), timer)
            if timer_dif > 0:
                sleep_us(timer_dif)

    except KeyboardInterrupt:
        display.cleanup()


test()


demo_colored_squares_.py
"""ILI9341 demo (colored squares)."""
from time import sleep
from ili9341 import Display
from machine import Pin, SPI
from sys import modules
import mySetup

RED = const(0XF800)  # (255, 0, 0)
GREEN = const(0X07E0)  # (0, 255, 0)
BLUE = const(0X001F)  # (0, 0, 255)
YELLOW = const(0XFFE0)  # (255, 255, 0)
FUCHSIA = const(0XF81F)  # (255, 0, 255)
AQUA = const(0X07FF)  # (0, 255, 255)
MAROON = const(0X8000)  # (128, 0, 0)
DARKGREEN = const(0X0400)  # (0, 128, 0)
NAVY = const(0X0010)  # (0, 0, 128)
TEAL = const(0X0410)  # (0, 128, 128)
PURPLE = const(0X8010)  # (128, 0, 128)
OLIVE = const(0X8400)  # (128, 128, 0)
ORANGE = const(0XFC00)  # (255, 128, 0)
DEEP_PINK = const(0XF810)  # (255, 0, 128)
CHARTREUSE = const(0X87E0)  # (128, 255, 0)
SPRING_GREEN = const(0X07F0)  # (0, 255, 128)
INDIGO = const(0X801F)  # (128, 0, 255)
DODGER_BLUE = const(0X041F)  # (0, 128, 255)
CYAN = const(0X87FF)  # (128, 255, 255)
PINK = const(0XFC1F)  # (255, 128, 255)
LIGHT_YELLOW = const(0XFFF0)  # (255, 255, 128)
LIGHT_CORAL = const(0XFC10)  # (255, 128, 128)
LIGHT_GREEN = const(0X87F0)  # (128, 255, 128)
LIGHT_SLATE_BLUE = const(0X841F)  # (128, 128, 255)
WHITE = const(0XFFF)  # (255, 255, 255)

colors = [RED,
          GREEN,
          BLUE,
          YELLOW,
          FUCHSIA,
          AQUA,
          MAROON,
          DARKGREEN,
          NAVY,
          TEAL,
          PURPLE,
          OLIVE,
          ORANGE,
          DEEP_PINK,
          CHARTREUSE,
          SPRING_GREEN,
          INDIGO,
          DODGER_BLUE,
          CYAN,
          PINK,
          LIGHT_YELLOW,
          LIGHT_CORAL,
          LIGHT_GREEN,
          LIGHT_SLATE_BLUE,
          WHITE ]

def test():
    """Test code."""
    """
    # Baud rate of 40000000 seems about the max
    spi = SPI(0, baudrate=40000000, sck=Pin(TFT_CLK_PIN), mosi=Pin(TFT_MOSI_PIN))
    display = Display(spi, dc=Pin(TFT_DC_PIN), cs=Pin(TFT_CS_PIN), rst=Pin(TFT_RST_PIN))
    """
    display = mySetup.createMyDisplay()

    """
    # Build color list from all upper case constants (lazy approach)
    colors = [getattr(modules[__name__], name) for name in dir(
        modules[__name__]) if name.isupper() and name is not 'SPI']
    """

    colors.sort()
    c = 0
    for x in range(0, 240, 48):
        for y in range(0, 320, 64):
            display.fill_rectangle(x, y, 47, 63, colors[c])
            c += 1
    sleep(9)
    display.cleanup()


test()


demo_color_palette_.py
"""ILI9341 demo (color palette)."""
from time import sleep
from ili9341 import Display, color565
from machine import Pin, SPI
import mySetup

def hsv_to_rgb(h, s, v):
    """
    Convert HSV to RGB (based on colorsys.py).

        Args:
            h (float): Hue 0 to 1.
            s (float): Saturation 0 to 1.
            v (float): Value 0 to 1 (Brightness).
    """
    if s == 0.0:
        return v, v, v
    i = int(h * 6.0)
    f = (h * 6.0) - i
    p = v * (1.0 - s)
    q = v * (1.0 - s * f)
    t = v * (1.0 - s * (1.0 - f))
    i = i % 6

    v = int(v * 255)
    t = int(t * 255)
    p = int(p * 255)
    q = int(q * 255)

    if i == 0:
        return v, t, p
    if i == 1:
        return q, v, p
    if i == 2:
        return p, v, t
    if i == 3:
        return p, q, v
    if i == 4:
        return t, p, v
    if i == 5:
        return v, p, q


def test():
    """Test code."""
    # Baud rate of 40000000 seems about the max
    #spi = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13))
    #display = Display(spi, dc=Pin(4), cs=Pin(16), rst=Pin(17))
    display = mySetup.createMyDisplay()

    c = 0
    for x in range(0, 240, 20):
        for y in range(0, 320, 20):
            color = color565(*hsv_to_rgb(c / 192, 1, 1))
            display.fill_circle(x + 9, y + 9, 9, color)
            c += 1
    sleep(9)
    display.cleanup()


test()


demo_color_wheel_.py
"""ILI9341 demo (color wheel)."""
from time import sleep
from ili9341 import Display, color565
from machine import Pin, SPI
from math import cos, pi, sin
import mySetup

HALF_WIDTH = const(120)
HALF_HEIGHT = const(160)
CENTER_X = const(119)
CENTER_Y = const(159)
ANGLE_STEP_SIZE = 0.05  # Decrease step size for higher resolution
PI2 = pi * 2


def hsv_to_rgb(h, s, v):
    """
    Convert HSV to RGB (based on colorsys.py).

        Args:
            h (float): Hue 0 to 1.
            s (float): Saturation 0 to 1.
            v (float): Value 0 to 1 (Brightness).
    """
    if s == 0.0:
        return v, v, v
    i = int(h * 6.0)
    f = (h * 6.0) - i
    p = v * (1.0 - s)
    q = v * (1.0 - s * f)
    t = v * (1.0 - s * (1.0 - f))
    i = i % 6

    v = int(v * 255)
    t = int(t * 255)
    p = int(p * 255)
    q = int(q * 255)

    if i == 0:
        return v, t, p
    if i == 1:
        return q, v, p
    if i == 2:
        return p, v, t
    if i == 3:
        return p, q, v
    if i == 4:
        return t, p, v
    if i == 5:
        return v, p, q


def test():
    """Test code."""
    # Baud rate of 40000000 seems about the max
    #spi = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13))
    #display = Display(spi, dc=Pin(4), cs=Pin(16), rst=Pin(17))
    display = mySetup.createMyDisplay()

    x, y = 0, 0
    angle = 0.0
    #  Loop all angles from 0 to 2 * PI radians
    while angle < PI2:
        # Calculate x, y from a vector with known length and angle
        x = int(CENTER_X * sin(angle) + HALF_WIDTH)
        y = int(CENTER_Y * cos(angle) + HALF_HEIGHT)
        color = color565(*hsv_to_rgb(angle / PI2, 1, 1))
        display.draw_line(x, y, CENTER_X, CENTER_Y, color)
        angle += ANGLE_STEP_SIZE

    sleep(5)

    for r in range(CENTER_X, 0, -1):
        color = color565(*hsv_to_rgb(r / HALF_WIDTH, 1, 1))
        display.fill_circle(CENTER_X, CENTER_Y, r, color)

    sleep(9)
    display.cleanup()


test()


demo_shapes_.py
"""ILI9341 demo (shapes)."""
from time import sleep
from ili9341 import Display, color565
from machine import Pin, SPI
import mySetup


def test():
    """Test code."""
    # Baud rate of 40000000 seems about the max
    #spi = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13))
    #display = Display(spi, dc=Pin(4), cs=Pin(16), rst=Pin(17))
    display = mySetup.createMyDisplay()

    display.clear(color565(64, 0, 255))
    sleep(1)

    display.clear()

    display.draw_hline(10, 319, 229, color565(255, 0, 255))
    sleep(1)

    display.draw_vline(10, 0, 319, color565(0, 255, 255))
    sleep(1)

    display.fill_hrect(23, 50, 30, 75, color565(255, 255, 255))
    sleep(1)

    display.draw_hline(0, 0, 222, color565(255, 0, 0))
    sleep(1)

    display.draw_line(127, 0, 64, 127, color565(255, 255, 0))
    sleep(2)

    display.clear()

    coords = [[0, 63], [78, 80], [122, 92], [50, 50], [78, 15], [0, 63]]
    display.draw_lines(coords, color565(0, 255, 255))
    sleep(1)

    display.clear()
    display.fill_polygon(7, 120, 120, 100, color565(0, 255, 0))
    sleep(1)

    display.fill_rectangle(0, 0, 15, 227, color565(255, 0, 0))
    sleep(1)

    display.clear()

    display.fill_rectangle(0, 0, 163, 163, color565(128, 128, 255))
    sleep(1)

    display.draw_rectangle(0, 64, 163, 163, color565(255, 0, 255))
    sleep(1)

    display.fill_rectangle(64, 0, 163, 163, color565(128, 0, 255))
    sleep(1)

    display.draw_polygon(3, 120, 286, 30, color565(0, 64, 255), rotate=15)
    sleep(3)

    display.clear()

    display.fill_circle(132, 132, 70, color565(0, 255, 0))
    sleep(1)

    display.draw_circle(132, 96, 70, color565(0, 0, 255))
    sleep(1)

    display.fill_ellipse(96, 96, 30, 16, color565(255, 0, 0))
    sleep(1)

    display.draw_ellipse(96, 256, 16, 30, color565(255, 255, 0))

    sleep(5)
    display.cleanup()


test()




Detect Touch:


To detect touch, visit rdagger/micropython-ili9341, download xpt2046.py to Raspberry Pi Pico.

Save mySetupX.py to Raspberry Pi Pico, with setup for xpd2046. Connect extra pins accordingly.

mySetupX.py
from ili9341 import Display
from machine import Pin, SPI
from xpt2046 import Touch

TFT_CLK_PIN = const(6)
TFT_MOSI_PIN = const(7)
TFT_MISO_PIN = const(4)

TFT_CS_PIN = const(13)
TFT_RST_PIN = const(14)
TFT_DC_PIN = const(15)

XPT_CLK_PIN = const(10)
XPT_MOSI_PIN = const(11)
XPT_MISO_PIN = const(8)

XPT_CS_PIN = const(12)
XPT_INT = const(0)

def createMyDisplay():
    spiTFT = SPI(0, baudrate=40000000, sck=Pin(TFT_CLK_PIN), mosi=Pin(TFT_MOSI_PIN))
    display = Display(spiTFT,
                      dc=Pin(TFT_DC_PIN), cs=Pin(TFT_CS_PIN), rst=Pin(TFT_RST_PIN))
    return display

def createXPT(touch_handler):
    spiXPT = SPI(1, baudrate=1000000,
                 sck=Pin(XPT_CLK_PIN), mosi=Pin(XPT_MOSI_PIN), miso=Pin(XPT_MISO_PIN))

    xpt = Touch(spiXPT, cs=Pin(XPT_CS_PIN), int_pin=Pin(XPT_INT),
                int_handler=touch_handler)

    return xpt

Run xpt_cal_.py, touch the corners to found the min/max of x/y. The detected min/max of x/y will be shown on screen. (Please check the video, y_max=2047 may be mis-detected, but very close. It apear before touched.)

xpt_cal_.py
from machine import Pin, SPI
from sys import implementation
from os import uname
import ili9341
from xglcd_font import XglcdFont
import mySetupX

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

print(SPI(0))
print(SPI(1))

minX = maxX = minY = maxY = 500

def xpt_touch(x, y):
    global xptTouch
    global minX, maxX, minY, maxY
    
    touchXY = xptTouch.get_touch()
    rawX = xptTouch.send_command(xptTouch.GET_X)
    rawY = xptTouch.send_command(xptTouch.GET_Y)
    
    if rawX != 0:
        if rawX > maxX:
            maxX = rawX
        elif rawX < minX:
            minX = rawX
    if rawY != 0:    
        if rawY > maxY:
            maxY = rawY
        elif rawY < minY:
            minY = rawY
    
    display.fill_circle(x, y, 2, ili9341.color565(0, 255, 0))
    print(str(x) + ":" + str(y) + " / " + str(rawX) + ":" + str(rawY))
    
    if touchXY != None:
        touchX = touchXY[0]
        touchY = touchXY[1]
        display.fill_circle(touchX, touchY, 2, ili9341.color565(255, 0, 0))
        print(str(touchX) + ":" + str(touchY))
        
    xReading = "X: " + str(minX) + " - " + str(maxX) + "       "
    yReading = "Y: " + str(minY) + " - " + str(maxY) + "       "
        
    display.draw_text(0, 100, xReading, unispace,
                  ili9341.color565(255, 128, 0))
    display.draw_text(0, 125, yReading, unispace,
                  ili9341.color565(255, 128, 0))

display = mySetupX.createMyDisplay()
xptTouch = mySetupX.createXPT(xpt_touch)

print('Loading fonts...')
print('Loading unispace')
unispace = XglcdFont('fonts/Unispace12x24.c', 12, 24)

display.draw_text(0, 0, ili9341.__name__, unispace,
                  ili9341.color565(255, 128, 0))
display.draw_text(0, 25, ili9341.implementation.name, unispace,
                  ili9341.color565(0, 0, 200))
display.draw_text(0, 50, str(ili9341.implementation.version), unispace,
                  ili9341.color565(0, 0, 200))

while True:
    pass

print("- bye -")

Update x_min, x_max, y_min and y_max in mySetupX.py accordingly.
    xpt = Touch(spiXPT, cs=Pin(XPT_CS_PIN), int_pin=Pin(XPT_INT),
                int_handler=touch_handler,
                x_min=64, x_max=1847, y_min=148, y_max=2047)

Run my exercise code, xpt_moveII_2.4.py.

xpt_moveII_2.4.py
from machine import Pin, SPI, Timer
from sys import implementation
from os import uname
import ili9341
from xglcd_font import XglcdFont
import mySetupX

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

print(SPI(0))
print(SPI(1))

led = Pin(25, Pin.OUT)

#=== variable share btween ISR and main loop ===
x_passedTo_ISR = 0
y_passwsTo_ISR = 0

EVT_NO = const(0)
EVT_PenDown = const(1)
EVT_PenUp   = const(2)
event = EVT_NO

TimerReached = False
#===============================================

def xpt_touch(x, y):
    global event, x_passedTo_ISR, y_passedTo_ISR
    event = EVT_PenDown
    x_passedTo_ISR = x
    y_passedTo_ISR = y

display = mySetupX.createMyDisplay()
xptTouch = mySetupX.createXPT(xpt_touch)

print('Loading fonts...')
print('Loading unispace')
unispace = XglcdFont('fonts/Unispace12x24.c', 12, 24)

display.clear()
display.draw_text(0, 0, ili9341.__name__, unispace,
                  ili9341.color565(255, 128, 0))
display.draw_text(0, 25, ili9341.implementation.name, unispace,
                  ili9341.color565(0, 0, 200))
display.draw_text(0, 50, str(ili9341.implementation.version), unispace,
                  ili9341.color565(0, 0, 200))

tim = Timer()
def TimerTick(timer):
    global TimerReached
    TimerReached = True

tim.init(freq=50, mode=Timer.PERIODIC, callback=TimerTick)

touching = False

lastX = lastY = 0

while True:
    curEvent = event
    event = EVT_NO
    if curEvent!= EVT_NO:
        if curEvent == EVT_PenDown:
            print("Pen Down")
            touching = True
            lastX = x_passedTo_ISR
            lastY = y_passedTo_ISR
    
            touchXY = xptTouch.get_touch()
            rawX = xptTouch.send_command(xptTouch.GET_X)
            rawY = xptTouch.send_command(xptTouch.GET_Y)
    
            display.clear()
            display.fill_circle(x_passedTo_ISR, y_passedTo_ISR,
                                8, ili9341.color565(0, 255, 0))
            print(str(x_passedTo_ISR) + ":" + str(y_passedTo_ISR) +
                  " / " + str(rawX) + ":" + str(rawY))
    
            if touchXY != None:
                touchX = touchXY[0]
                touchY = touchXY[1]
                display.fill_circle(touchX, touchY, 5, ili9341.color565(255, 0, 0))
                print(str(touchX) + ":" + str(touchY))
            
        elif curEvent == EVT_PenUp:
            print("Pen Up")
            pass
        else:
            print("unknown event!!!")
            
    if TimerReached:
        TimerReached = False
        
        if touching:
            led.toggle()
            buff = xptTouch.raw_touch()
            if buff is not None:
                x, y = xptTouch.normalize(*buff)
                lastX = x
                lastY = y
                display.fill_circle(x, y, 1, ili9341.color565(255, 255, 255))
                print("... " + str(x) + " : " + str(y))
            else:
                event = EVT_PenUp
                touching = False
                led.off()
                display.fill_circle(lastX, lastY, 5, ili9341.color565(0, 0, 255))

print("- bye -")

Tested on my another unit of 2.8 inch version, 2.8" TFT SPI 240*320. X-position is in reverse order, so edit for 2.8 inch version.

xpt_moveII_2.8.py
from machine import Pin, SPI, Timer
from sys import implementation
from os import uname
import ili9341
from xglcd_font import XglcdFont
import mySetupX

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

print(SPI(0))
print(SPI(1))

led = Pin(25, Pin.OUT)

#=== variable share btween ISR and main loop ===
x_passedTo_ISR = 0
y_passwsTo_ISR = 0

EVT_NO = const(0)
EVT_PenDown = const(1)
EVT_PenUp   = const(2)
event = EVT_NO

TimerReached = False
#===============================================

def xpt_touch(x, y):
    global event, x_passedTo_ISR, y_passedTo_ISR
    event = EVT_PenDown
    x_passedTo_ISR = x
    y_passedTo_ISR = y

display = mySetupX.createMyDisplay()
xptTouch = mySetupX.createXPT(xpt_touch)

print('Loading fonts...')
print('Loading unispace')
unispace = XglcdFont('fonts/Unispace12x24.c', 12, 24)

display.clear()
display.draw_text(0, 0, ili9341.__name__, unispace,
                  ili9341.color565(255, 128, 0))
display.draw_text(0, 25, ili9341.implementation.name, unispace,
                  ili9341.color565(0, 0, 200))
display.draw_text(0, 50, str(ili9341.implementation.version), unispace,
                  ili9341.color565(0, 0, 200))

tim = Timer()
def TimerTick(timer):
    global TimerReached
    TimerReached = True

tim.init(freq=50, mode=Timer.PERIODIC, callback=TimerTick)

touching = False

lastX = lastY = 0

while True:
    curEvent = event
    event = EVT_NO
    if curEvent!= EVT_NO:
        if curEvent == EVT_PenDown:
            print("Pen Down")
            touching = True
            lastX = x_passedTo_ISR
            lastY = y_passedTo_ISR
    
            touchXY = xptTouch.get_touch()
            rawX = xptTouch.send_command(xptTouch.GET_X)
            rawY = xptTouch.send_command(xptTouch.GET_Y)
    
            display.clear()
            display.fill_circle(240-x_passedTo_ISR, y_passedTo_ISR,
                                8, ili9341.color565(0, 255, 0))
            print(str(x_passedTo_ISR) + ":" + str(y_passedTo_ISR) +
                  " / " + str(rawX) + ":" + str(rawY))
    
            if touchXY != None:
                touchX = touchXY[0]
                touchY = touchXY[1]
                display.fill_circle(240-touchX, touchY, 5, ili9341.color565(255, 0, 0))
                print(str(touchX) + ":" + str(touchY))
            
        elif curEvent == EVT_PenUp:
            print("Pen Up")
            pass
        else:
            print("unknown event!!!")
            
    if TimerReached:
        TimerReached = False
        
        if touching:
            led.toggle()
            buff = xptTouch.raw_touch()
            if buff is not None:
                x, y = xptTouch.normalize(*buff)
                lastX = x
                lastY = y
                display.fill_circle(240-x, y, 1, ili9341.color565(255, 255, 255))
                print("... " + str(x) + " : " + str(y))
            else:
                event = EVT_PenUp
                touching = False
                led.off()
                display.fill_circle(240-lastX, lastY, 5, ili9341.color565(0, 0, 255))

print("- bye -")


Related: