[TH] Control movement from a joystick via WiFi with MicroPython.

บทความนี้เกิดจากการนำบทความการเขียนโปรแกรมไคลเอนต์/เซิร์ฟเวอร์สำหรับสถานีอากาศผ่านระบบเครือข่ายไร้สาย หรือ WiFi มาปรับเปลี่ยนจากการอ่านข้อมูลจากเซ็นเซอร์มาเป็นจอยสติกชิลด์ (Arduino Joystick Shield) เพื่อให้กลายเป็นเกมคอนโทรลเลอร์แบบไร้สายโดยใช้ MycroPython กับไมโครคอนโทรลเลอร์ ESP32 ดังภาพที่ 1 ทำให้สามารถควบคุมการเคลื่อนที่ของวัตถุในจอแสดงผลผ่านจอ TFT แบบ ST7735 ที่เชื่อมต่อกับ ESP32 อีกตัวหนึ่งได้ ซึ่งจะพบว่าการใช้งานภาษาไพธอนของ MicroPython นั้นสามารถนำมาใช้งานได้กับกรณีตัวอย่างนี้ และด้วยภาษาที่เขียนได้ง่ายประกอบกับสามารถปรับแก้โค้ดได้โดยไม่ต้องคอมไพล์และอัพโหลดใหม่จึงสะดวกต่อการเขียนโค้ดต้นแบบเพื่อนำไปพัฒนาให้มีความเร็วในการทำงานที่สูงขึ้นต่อไป

Control movement from a joystick via WiFi
ภาพที่ 1 อุปกรณ์การทดลองในบทความ

อุปกรณ์

อุปกรณ์ฝั่งแสดงผล

  1. จอแสดงผล ST7735 GREENTAB80x160
  2. บอร์ดไมโครคอนโทรลเลอร์ ESP32 โดยในบทความนี้ใช้ ESP32CAM
  3. การเชื่อมต่อจากจอแสดงผลกับ ESP32 โดยใช้บัส SPI ช่องสัญญาณ 2
    1. SCK ของจอแสดงผลเชื่อมต่อกับ GPIO14 ของ ESP32
    2. MOSI ของจอแสดงผลเชื่อมต่อกับ GPIO12 ของ ESP32
    3. DC ของจอแสดงผลเชื่อมต่อกับ GPIO15 ของ ESP32
    4. RST ของจอแสดงผลเชื่อมต่อกับ GPIO13 ของ ESP32
    5. CS ของจอแสดงผลเชื่อมต่อกับ GPIO2 ของ ESP32

อุปกรณ์ฝั่งควบคุม

  1. ชุดจอยสติก
  2. บัซเซอร์
  3. บอร์ดไมโครคอนโทรลเลอร์ ESP32
  4. การเชื่อมต่อ
    1. โมดูลบัซเซอร์ต่อเข้ากับขา GPIO25 ของ ESP32 เพื่อนำออกสัญญาณแอนาล็อก
    2. คันโยกแกน X ต่อเข้ากับขา GPIO36 ของ ESP32 เพื่ออ่านค่าแอนาล็อก
    3. คันโยกแกน Y ต่อเข้ากับขา GPIO39 ของ ESP32 เพื่ออ่านค่าแอนาล็อก
    4. สวิตช์ A ต่อกับขา GPIO32 เพื่อนำเข้าข้อมูลดิจิทัล
    5. สวิตช์ B ต่อเข้ากับขา GPIO33 ของ ESP32 เพื่อนำเข้าข้อมูลดิจิทัล
    6. สวิตช์ C ต่อเข้ากับขา GPIO35 เพื่อนำเข้าข้อมูล
    7. สวิตช์ D ต่อเข้ากับขา GPIO34 เพื่อนำเข้าข้อมูลดิจิทัล
    8. สวิตช์ E ต่อเข้ากับขา GPIO27 เพื่อนำเข้าข้อมูลสัญญาณดิจิทัล
    9. สวิตช์ F ต่อเข้ากับขา GPIO26 เพื่อนำเข้าข้อมูล

ตัวอย่างโปรแกรม

ตัวอย่างโปรแกรมสำหรับควบคุมกล่องสี่เหลี่ยมสีแดงบนจอของบอร์ด miniML ด้วยการโยกคันโยกขอวจอยสติกที่อยู่กับบอร์ด microML โดยส่งข้อมูลผ่านเครือข่ายอินเทอร์เน็ต ซึ่งข้อมูลที่ส่งนั้นมีขนาด 16 บิต หรือ 2 ไบต์ ซึ่งเกิดจากการเข้ารหัสบิตของข้อมูลจากการรับข้อมูลจากจอยสติก ตามภาพที่ 2 และ ดังรายละเอียดต่อไป

  • บิต 11 ถึง 15 ไม่ได้ใช้
  • บิต 10 เก็บสถานะของการโยกคันโยกลงด้านซ้าย
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 9 เก็บสถานะของการโยกคันโยกลงด้านขวา
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 8 ไม่ได้ใช้
  • บิต 7 เก็บสถานะของการโยกคันโยกขึ้นด้านบน
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 6 เก็บสถานะของการโยกคันโยกลงด้านล่าง
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 5 เก็บสถานะของการกดปุ่ม swA
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 4 เก็บสถานะของการกดปุ่ม swB
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 3 เก็บสถานะของการกดปุ่ม swC
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 2 เก็บสถานะของการกดปุ่ม swD
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 1 เก็บสถานะของการกดปุ่ม swE
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
  • บิต 0 เก็บสถานะของการกดปุ่ม swF
    • ถ้าเป็น 1 หมายถึง ถูกกด
    • ถ้าเป็น 0 หมายถึง ไม่ถูกกด
ภาพที่ 2 บิตของการรับ/ส่ง

โปรแกรมฝั่งแสดงผล

หน้าที่ของส่วนแสดงผลคือ ทำตัวเองเป็น AP ของเครือข่ายอินเทอร์เน็ต โดยตั้งค่าไว้ดังนี้

  • ssid เป็น dCoreMiniML
  • passwd เป็น _miniML_dCore_
  • IP Address ของอุปกรณ์เป็น 192.168.4.1
  • พอร์ตสำหรับสื่อสารเป็น 1592

เมื่อทำหน้าที่เป็น AP และรอการเชื่อมต่อ เมื่อเชื่อมต่อจากส่วนของจอยสติกสำเร็จจะคอยรอรับข้อมูลจำนวน 2 ไบต์จากฝั่งตัวควบคุมเพื่อปรับเปลี่ยนค่า pos ซึ่งเก็บค่าพิกัดของกล่องสีแดง ตามเงื่อนไขต่อไปนี้

  • ลดค่า x หรือ pos[0] ถ้าโยกคันโยกไปทางซ้าย
  • เพิ่มค่า x หรือ pos[0] เมื่อโยกคันโยกไปทางขวา
  • ลดค่า y หรือ pos[1] ถ้าโยกคันโยกขึ้นด้านบน
  • เพิ่มค่า y หรือ pos[1] เมื่อผู้ใช้โยกคันโยกลงด้านล่าง

โค้ดโปรแกรมเป็นดังนี้

#############################################################
#GameDisplay.py
# สำหรับเครื่องให้บริการ
# (C) 2021, JarutEx
#############################################################
from machine import Pin,SPI,SDCard,ADC
import dht
import machine
import gc
import time
import os
import sys
import machine as mc
import network as nw
import ubinascii as ua
import socket
from st7735x import TFT
#############################################################
# system
gc.enable()
gc.collect()
machine.freq(240000000)

ssid = 'dCoreMiniML'
passwd = '_miniML_dCore_'
serverIP = '192.168.4.1'
serverPort = 1592

class coreDisplay:
    def __init__(self):
        self.spi = None
        self.dev = None
        self.use()
        self.unused()
    
    def use(self):
        self.spi = SPI(2, baudrate=33000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
        # dc, rst, cs
        self.dev=TFT(self.spi,15,13,2)
    
    def unused(self):
        self.spi.deinit() # ปิด


tft = coreDisplay()
tft.use()

########################################
## เปิดใช้งาน TFT

tft.dev.fill(tft.dev.BLACK)
tft.dev.text("(C)2020-21",(10,24),tft.dev.YELLOW)
tft.dev.text("JarutEx",(92,244),tft.dev.WHITE)
tft.dev.text("JarutEx",(93,24),tft.dev.WHITE)
tft.dev.swap()

###########################################################
# Main program
APif = nw.WLAN(nw.AP_IF)
# Set WiFi access point name (formally known as ESSID) and WiFi channel
APif.config(essid=ssid,
            password=passwd,
            authmode=nw.AUTH_WPA2_PSK,
            channel=1, hidden=True)
APif.active(True)
print(APif.ifconfig())

# main loop
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
s.bind(('',serverPort))
s.listen(1)
conn, addr = s.accept()

pos = [78,38]
tft.dev.fill(tft.dev.BLACK)
tft.dev.fillrect(pos,(4,4),tft.dev.RED)
tft.dev.swap()

try:
    while True:
        request = (int).from_bytes(conn.recv(2),'big')
        print(request)
        if (request & 0x0400): # left 1024
            pos[0] -= 1
        if (request & 0x0200): # right 512
            pos[0] += 1
        if (request & 0x0080): # up 128
            pos[1] -= 1
        if (request & 0x0040): # down 64
            pos[1] += 1
        tft.dev.fill(tft.dev.BLACK)
        tft.dev.fillrect(pos,(4,4),tft.dev.RED)
        tft.dev.swap()
except KeyboardInterrupt:
    pass
conn.close()
s.close()
APif.active(False)

โปรแกรมฝั่งควบคุม

โปรแกรมฝั่งควบคุมประกอบไปด้วยโค้ดโปรแกรม 2 ส่วน คือ ส่วนของไลบรารี keypad ที่ทำหน้าที่เชื่อมต่อกับ Joystick Shield และส่วนของโปรแกรมหลักที่ทำหน้าที่เชื่อมต่อกับ AP ของส่วนแสดงผลและส่งข้อมูลจากจอยสติกไปยังส่วนแสดงผล

ไลบรารี keypad

########################################################################################
# keypad.py
# เป้าหมาย สร้างอุปกรณ์ใช้ Game Pad Controller แบบไร้สาย
# ผู้พัฒนา JarutEx
# 2021-09-02 ปรับปรุงเรื่อง JoyStick
# 2021-08-20 สร้างคลาส Keypad ให้รองรับ JoyStick และปุ่ม A,B,C,D,E,F ยกเว้นปุ่มที่จอยสติก
#
########################################################################################
from machine import Pin, ADC

########################################################################################
class Keypad:
    def __init__(self):
        self.swA = Pin(32, Pin.IN)
        self.swB = Pin(33, Pin.IN)
        self.swC = Pin(35, Pin.IN)
        self.swD = Pin(34, Pin.IN)
        self.swF = Pin(26, Pin.IN) # select
        self.swE = Pin(27, Pin.IN) # start
        self.jst = (ADC(Pin(36)), ADC(Pin(39))) # X,Y
        self.jst[0].width( ADC.WIDTH_12BIT ) # 12bit
        self.jst[0].atten( ADC.ATTN_11DB ) # 3.3V
        self.jst[1].width( ADC.WIDTH_12BIT ) # 12bit
        self.jst[1].atten( ADC.ATTN_11DB ) # 3.3V
        self.bits = 0x000 # 10-bits
        self.x = self.jst[0].read()
        self.y = 4095-self.jst[1].read()
        self.a = self.swA.value()
        self.b = self.swB.value()
        self.c = self.swC.value()
        self.d = self.swD.value()
        self.e = self.swE.value()
        self.f = self.swF.value()
    def read(self):
        self.x = self.jst[0].read()
        self.y = 4095-self.jst[1].read()
        self.a = self.swA.value()
        self.b = self.swB.value()
        self.c = self.swC.value()
        self.d = self.swD.value()
        self.e = self.swE.value()
        self.f = self.swF.value()
    def show(self):
        self.read()
        print("{} ({},{}) SWs: A{} B{} C{} D{} E{} F{}".format(
            hex(self.status()),
            self.x,self.y,
            self.a,self.b,self.c,self.d,self.e,self.f))
    def status(self):
        self.bits = 0x000 # 10-bits
        if (self.x < 1500): # left
            self.bits = self.bits | 0x400
        elif (self.x > 2500): # right
            self.bits = self.bits | 0x200
        if (self.y < 1500): # up
            self.bits = self.bits | 0x080
        elif (self.y > 2500): # down
            self.bits = self.bits | 0x040
        if (self.a == 0):
            self.bits = self.bits | 0x020
        if (self.b == 0):
            self.bits = self.bits | 0x010
        if (self.c == 0):
            self.bits = self.bits | 0x008
        if (self.d == 0):
            self.bits = self.bits | 0x004
        if (self.e == 0):
            self.bits = self.bits | 0x002
        if (self.f == 0):
            self.bits = self.bits | 0x001
        return self.bits

โปรแกรมหลัก

โปรแกรมหลักทำงานเชื่อมต่อกับ dCoreMiniML และเปล่งเสียงออกลำโพง 1 ครั้งเมื่อการเชื่อมต่อสำเร็จ หลังจากนั้นตัวตั้งเวลาจะส่งข้อมูลจากจอยสติกไปให้เครื่องให้บริการหรือส่วนแสดงผลทุก 30 มิลลิวินาที ดังโค้ดต่อไปนี้

###################################################################
### GamePad
# (C) 2021, JarutEx
###################################################################
from machine import Pin, ADC, Timer, SPI
from keypad import Keypad
import time
import gc
import network as nw
import socket
import machine

###################################################################
### system setting
###################################################################
gc.enable()
gc.collect()
machine.freq(240000000)
keypad = Keypad()
tmSwitch = Timer(0) # คุมการรับข้อมูลจากสวิตช์
ssid = 'dCoreMiniML'
passwd = '_miniML_dCore_'
serverIP = '192.168.4.1'
serverPort = 1592
# WiFi
print("Start WiFi")
sta = nw.WLAN( nw.STA_IF )
sta.active(False)
sta.active(True)
# เชื่อมต่อ
sta.connect(ssid, passwd)
while not sta.isconnected():
    time.sleep_ms(200)
print(sta.ifconfig())
serverInfo = socket.getaddrinfo( serverIP, serverPort, 0, socket.SOCK_STREAM )[0]
print("Server info : {}".format(serverInfo))

###################################################################
### Speaker
###################################################################
pinSpk = Pin(25,Pin.OUT)

def beep():
    pinSpk.value(0) # on
    time.sleep_ms(100)
    pinSpk.value(1) # off

def spkOff():
    pinSpk.value(1) # off

spkOff()
beep()

###################################################################
## doInput()
###################################################################
def doInput(x):
    #keypad.show()
    keypad.read()
    status = keypad.status()
    data = bytearray([(status & 0xff00) >> 8,status&0x00ff])
    print(status, data)
    s.write(bytearray(data))

###################################################################
### Main program
###################################################################
# start program
s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
s.connect((serverIP,serverPort))
tmSwitch.init(period=30, mode=Timer.PERIODIC, callback=doInput)
# main loop
try:
    while True:
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
# end program
tmSwitch.deinit()
sta.disconnect()
sta.active(False)
s.close()

สรุป

จากบทความนี้จะพบว่า เราสามารถประยุกต์ใช้ระบบไมโครคอนโทรลเลอร์ ESP32 ได้หลากหลายมากขึ้นเมื่อเปลี่ยนเซ็นเซอร์หรือส่วนแสดงผลและผนวกเข้ากับการสื่อสาร เช่นตัวอย่างก่อนหน้านี้ใช้ ESP32 ตัวหนึ่งเป็น Web Server และอีกตัวเป็นเซ็นเซอร์ โดยให้ตัว Server คอยอ่านข้อมูลจากบอร์ดที่เป็นตัวเซ็นเซอร์เมื่อมีการเรียกดูเว็บ ขณะในบทความนี้เป็นใช้จอแสดงผลติดกับบอร์ดที่เป็น Server และเปลี่ยนจากเซ็นเซอร์เป็นจอยสติก หลังจากนั้นให้ฝั่งลูกข่ายทำหน้าที่ส่งข้อมูลไปให้เซิฟร์ฟเวอร์แทน ซึ่งจะเห็นว่า ถ้าผู้เขียนโปรแกรมเข้าใจหลักการทำงานและวิธีการเขียนโค้ดจะสามารถปรับเปลี่ยนการเขียนโค้ดเพื่อให้ตอบสนองวัตถุประสงค์ของการทำงานได้หลากหลาย ดังนั้น จงฝึกฝนเยอะ ๆ ทดลองมาก ๆ และขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ได้เลยครับ

(C) 2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-10-14, 2021-12-28