ESP32与Xbox手柄的UART通信测试,基于Arduino框架和pyserial+pygame

1. 说明

这个项目的目标是实现使用手柄来控制ESP32。最近正在进行无人机项目,但是由于没有适合的遥控器来控制四轴,画板子也有些占用时间,所以比较有效的方法就是基于手头有的Xbox手柄来进行一个DIY,在手柄与ESP32之间建立串口通信。此处使用PC作为中继,可能速度有些慢,但是基于目前需求,速度已经足够了。下图说明了无人机项目的通信方式,红框部分为本次涉及部分。

在这里插入图片描述

2. 环境

这里我使用主要Ubuntu 18作为开发环境,Win10下也能正常运行。python版本为3.9,所需库为pygamepyserial

3. 手柄与PC之间的通信测试

手柄与PC之间通过Pygame建立通信,以下提供了两个测试程序,第一个测试程序是一个简单的终端输出,如果手柄工作正常,就会看到六轴的输出。

import pygame
import time

pygame.init()
pygame.joystick.init()
done=False

while (done != True):
    for event in pygame.event.get():  # User did something
        if event.type == pygame.QUIT:  # If user clicked close
            done = True  # Flag that we are done so we exit this loop
    joystick_count = pygame.joystick.get_count()
    for i in range(joystick_count):
        joystick = pygame.joystick.Joystick(i)
        joystick.init()
        axes = joystick.get_numaxes()
        print('================')
        time.sleep(0.1)
        for i in range(axes):
            axis = joystick.get_axis(i)
            print(axis) 

以下测试程序提供了一个简单的GUI来对每个按键进行测试

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vi:ts=4 sw=4 et
#
# This tool runs fine on both Python 2 and Python 3.
#
# https://github.com/denilsonsa/pygame-joystick-test

from __future__ import division
from __future__ import print_function

import sys
import pygame
from pygame.locals import *


class joystick_handler(object):
    def __init__(self, id):
        self.id = id
        self.joy = pygame.joystick.Joystick(id)
        self.name = self.joy.get_name()
        self.joy.init()
        self.numaxes    = self.joy.get_numaxes()
        self.numballs   = self.joy.get_numballs()
        self.numbuttons = self.joy.get_numbuttons()
        self.numhats    = self.joy.get_numhats()

        self.axis = []
        for i in range(self.numaxes):
            self.axis.append(self.joy.get_axis(i))

        self.ball = []
        for i in range(self.numballs):
            self.ball.append(self.joy.get_ball(i))

        self.button = []
        for i in range(self.numbuttons):
            self.button.append(self.joy.get_button(i))

        self.hat = []
        for i in range(self.numhats):
            self.hat.append(self.joy.get_hat(i))


class input_test(object):
    class program:
        "Program metadata"
        name    = "Pygame Joystick Test"
        version = "0.2"
        author  = "Denilson Figueiredo de Sá Maia"
        nameversion = name + " " + version

    class default:
        "Program constants"
        fontnames = [
            # Bold, Italic, Font name
            (0, 0, "Bitstream Vera Sans Mono"),
            (0, 0, "DejaVu Sans Mono"),
            (0, 0, "Inconsolata"),
            (0, 0, "LucidaTypewriter"),
            (0, 0, "Lucida Typewriter"),
            (0, 0, "Terminus"),
            (0, 0, "Luxi Mono"),
            (1, 0, "Monospace"),
            (1, 0, "Courier New"),
            (1, 0, "Courier"),
        ]
        # TODO: Add a command-line parameter to change the size.
        # TODO: Maybe make this program flexible, let the window height define
        #       the actual font/circle size.
        fontsize     = 20
        circleheight = 10
        resolution   = (640, 480)

    def load_the_fucking_font(self):
        # The only reason for this function is that pygame can find a font
        # but gets an IOError when trying to load it... So I must manually
        # workaround that.

        # self.font = pygame.font.SysFont(self.default.fontnames, self.default.fontsize)
        for bold, italic, f in self.default.fontnames:
            try:
                filename = pygame.font.match_font(f, bold, italic)
                if filename:
                    self.font = pygame.font.Font(filename, self.default.fontsize)
                    # print("Successfully loaded font: %s (%s)" % (f, filename))
                    break
            except IOError as e:
                # print("Could not load font: %s (%s)" % (f, filename))
                pass
        else:
            self.font = pygame.font.Font(None, self.default.fontsize)
            # print("Loaded the default fallback font: %s" % pygame.font.get_default_font())

    def pre_render_circle_image(self):
        size = self.default.circleheight
        self.circle = pygame.surface.Surface((size,size))
        self.circle.fill(Color("magenta"))
        basecolor  = ( 63,  63,  63, 255)  # RGBA
        lightcolor = (255, 255, 255, 255)
        for i in range(size // 2, -1, -1):
            color = (
                lightcolor[0] + i * (basecolor[0] - lightcolor[0]) // (size // 2),
                lightcolor[1] + i * (basecolor[1] - lightcolor[1]) // (size // 2),
                lightcolor[2] + i * (basecolor[2] - lightcolor[2]) // (size // 2),
                255
            )
            pygame.draw.circle(
                self.circle,
                color,
                (int(size // 4 + i // 2) + 1, int(size // 4 + i // 2) + 1),
                i,
                0
            )
        self.circle.set_colorkey(Color("magenta"), RLEACCEL)

    def init(self):
        pygame.init()
        pygame.event.set_blocked((MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN))
        # I'm assuming Font module has been loaded correctly
        self.load_the_fucking_font()
        # self.fontheight = self.font.get_height()
        self.fontheight = self.font.get_linesize()
        self.background = Color("black")
        self.statictext = Color("#FFFFA0")
        self.dynamictext = Color("white")
        self.antialias = 1
        self.pre_render_circle_image()
        # self.clock = pygame.time.Clock()
        self.joycount = pygame.joystick.get_count()
        if self.joycount == 0:
            print("This program only works with at least one joystick plugged in. No joysticks were detected.")
            self.quit(1)
        self.joy = []
        for i in range(self.joycount):
            self.joy.append(joystick_handler(i))

        # Find out the best window size
        rec_height = max(
            5 + joy.numaxes + joy.numballs + joy.numhats + (joy.numbuttons + 9) // 10
            for joy in self.joy
        ) * self.fontheight
        rec_width = max(
            [self.font.size("W" * 13)[0]] +
            [self.font.size(joy.name)[0] for joy in self.joy]
        ) * self.joycount
        self.resolution = (rec_width, rec_height)

    def run(self):
        self.screen = pygame.display.set_mode(self.resolution, RESIZABLE)
        pygame.display.set_caption(self.program.nameversion)
        self.circle.convert()

        while True:
            for i in range(self.joycount):
                self.draw_joy(i)
            pygame.display.flip()
            # self.clock.tick(30)
            for event in [pygame.event.wait(), ] + pygame.event.get():
                # QUIT             none
                # ACTIVEEVENT      gain, state
                # KEYDOWN          unicode, key, mod
                # KEYUP            key, mod
                # MOUSEMOTION      pos, rel, buttons
                # MOUSEBUTTONUP    pos, button
                # MOUSEBUTTONDOWN  pos, button
                # JOYAXISMOTION    joy, axis, value
                # JOYBALLMOTION    joy, ball, rel
                # JOYHATMOTION     joy, hat, value
                # JOYBUTTONUP      joy, button
                # JOYBUTTONDOWN    joy, button
                # VIDEORESIZE      size, w, h
                # VIDEOEXPOSE      none
                # USEREVENT        code
                if event.type == QUIT:
                    self.quit()
                elif event.type == KEYDOWN and event.key in [K_ESCAPE, K_q]:
                    self.quit()
                elif event.type == VIDEORESIZE:
                    self.screen = pygame.display.set_mode(event.size, RESIZABLE)
                elif event.type == JOYAXISMOTION:
                    self.joy[event.joy].axis[event.axis] = event.value
                elif event.type == JOYBALLMOTION:
                    self.joy[event.joy].ball[event.ball] = event.rel
                elif event.type == JOYHATMOTION:
                    self.joy[event.joy].hat[event.hat] = event.value
                elif event.type == JOYBUTTONUP:
                    self.joy[event.joy].button[event.button] = 0
                elif event.type == JOYBUTTONDOWN:
                    self.joy[event.joy].button[event.button] = 1

    def rendertextline(self, text, pos, color, linenumber=0):
        self.screen.blit(
            self.font.render(text, self.antialias, color, self.background),
            (pos[0], pos[1] + linenumber * self.fontheight)
            # I can access top-left coordinates of a Rect by indexes 0 and 1
        )

    def draw_slider(self, value, pos):
        width  = pos[2]
        height = self.default.circleheight
        left   = pos[0]
        top    = pos[1] + (pos[3] - height) // 2
        self.screen.fill(
            (127, 127, 127, 255),
            (left + height // 2, top + height // 2 - 2, width - height, 2)
        )
        self.screen.fill(
            (191, 191, 191, 255),
            (left + height // 2, top + height // 2, width - height, 2)
        )
        self.screen.fill(
            (127, 127, 127, 255),
            (left + height // 2, top + height // 2 - 2, 1, 2)
        )
        self.screen.fill(
            (191, 191, 191, 255),
            (left + height // 2 + width - height - 1, top + height // 2 - 2, 1, 2)
        )
        self.screen.blit(
            self.circle,
            (left + (width - height) * (value + 1) // 2, top)
        )

    def draw_hat(self, value, pos):
        xvalue =  value[0] + 1
        yvalue = -value[1] + 1
        width  = min(pos[2], pos[3])
        height = min(pos[2], pos[3])
        left   = pos[0] + (pos[2] - width ) // 2
        top    = pos[1] + (pos[3] - height) // 2
        self.screen.fill((127, 127, 127, 255), (left, top              , width, 1))
        self.screen.fill((127, 127, 127, 255), (left, top + height // 2, width, 1))
        self.screen.fill((127, 127, 127, 255), (left, top + height  - 1, width, 1))
        self.screen.fill((127, 127, 127, 255), (left             , top, 1, height))
        self.screen.fill((127, 127, 127, 255), (left + width // 2, top, 1, height))
        self.screen.fill((127, 127, 127, 255), (left + width  - 1, top, 1, height))
        offx = xvalue * (width  - self.circle.get_width() ) // 2
        offy = yvalue * (height - self.circle.get_height()) // 2
        # self.screen.fill((255,255,255,255),(left + offx, top + offy) + self.circle.get_size())
        self.screen.blit(self.circle, (left + offx, top + offy))

    def draw_joy(self, joyid):
        joy = self.joy[joyid]
        width = self.screen.get_width() // self.joycount
        height = self.screen.get_height()
        pos = Rect(width * joyid, 0, width, height)
        self.screen.fill(self.background, pos)

        # This is the number of lines required for printing info about this joystick.
        # self.numlines = 5 + joy.numaxes + joy.numballs + joy.numhats + (joy.numbuttons+9)//10

        # Joy name
        # 0 Axes:
        # -0.123456789
        # 0 Trackballs:
        # -0.123,-0.123
        # 0 Hats:
        # -1,-1
        # 00 Buttons:
        # 0123456789

        # Note: the first character is the color of the text.
        text_colors = {
            "D": self.dynamictext,
            "S": self.statictext,
        }
        output_strings = [
            "S%s"             % joy.name,
            "S%d axes:"       % joy.numaxes
        ]+[ "D    %d=% .3f"   % (i, v) for i, v in enumerate(joy.axis) ]+[
            "S%d trackballs:" % joy.numballs
        ]+[ "D%d=% .2f,% .2f" % (i, v[0], v[1]) for i, v in enumerate(joy.ball) ]+[
            "S%d hats:"       % joy.numhats
        ]+[ "D  %d=% d,% d"   % (i, v[0], v[1]) for i, v in enumerate(joy.hat ) ]+[
            "S%d buttons:"    % joy.numbuttons
        ]
        for l in range(joy.numbuttons // 10 + 1):
            s = []
            for i in range(l * 10, min((l + 1) * 10, joy.numbuttons)):
                if joy.button[i]:
                    s.append("%d" % (i % 10))
                else:
                    s.append(" ")
            output_strings.append("D" + "".join(s))

        for i, line in enumerate(output_strings):
            color = text_colors[line[0]]
            self.rendertextline(line[1:], pos, color, linenumber=i)

        tmpwidth = self.font.size("    ")[0]
        for i, v in enumerate(joy.axis):
            self.draw_slider(
                v,
                (
                    pos[0],
                    pos[1] + (2 + i) * self.fontheight,
                    tmpwidth,
                    self.fontheight
                )
            )

        tmpwidth = self.font.size("  ")[0]
        for i, v in enumerate(joy.hat):
            self.draw_hat(
                v,
                (
                    pos[0],
                    pos[1] + (4 + joy.numaxes + joy.numballs + i) * self.fontheight,
                    tmpwidth,
                    self.fontheight
                )
            )
        # self.draw_hat((int(joy.axis[3]),int(joy.axis[4])), (pos[0], pos[1] + (4+joy.numaxes+joy.numballs+0)*self.fontheight, tmpwidth, self.fontheight))

    def quit(self, status=0):
        pygame.quit()
        sys.exit(status)


if __name__ == "__main__":
    program = input_test()
    program.init()
    program.run()  # This function should never return

如果环境没有问题,就会看到如下的GUI界面。这时候按动手柄上的相关按键,就能看到数值的实时更新。

在这里插入图片描述

4. python与ESP32的通信测试

接下来测试python与Esp32之间的通信。 这里在PC上直接使用pyserial 库,

import serial 
import time

ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=9600,
    parity=serial.PARITY_ODD,
    stopbits=serial.STOPBITS_TWO,
    bytesize=serial.SEVENBITS
)

ser.isOpen()

# read a string
while 1:
    trans_data = "Hello World"
    ser.write(trans_data.encode('utf-8')) # write a string
    received_data = ser.readline().decode() # read a byte

    print(received_data)


ESP32使用Arduino框架,,只需要使用串口就可以了。程序的逻辑也非常简单, 就是读取上位机发送的信息,并返回信息。

#include <Arduino.h>

String received_data;

void ReadData(void)
{
    if ( Serial.available() ) {
        received_data = Serial.readString();
    }
}
void setup() {
    Serial.begin(9600);
}

void loop() {
    ReadData();
    Serial.println(received_data);
    delay(200);
}

如果一切正常,那么输出如下:

Hello World
Hello World
Hello World
Hello World
Hello World
...

5. 手柄与ESP32的通信测试

如果以上两个的测试正常通过,我们接下来就可以测试手柄和ESP之间的通信了。这里我们的目标是使用UART发送给ESP32相应的手柄的值,并返回一个解码的值。

首先我们需要对原始数据进行一个处理,因为原始的手柄数据都是浮点值,为了方便esp的处理,我们在PC端就需要对原始数据进行一个处理,将其全部转换为整型。并将所有轴上的数据拼合成一个字符串来方便发送。

首先我们需要对上位机程序进行一个整合,上位机程序需要做的任务是读取手柄的值,然后转码并发送到ESP32,然后读取串口上的值。
程序如下,

import pygame
import time
import serial

class Joystick:
    def __init__(self):
        # initialization for joystick
        pygame.init()
        pygame.joystick.init()
        self.joystick_count = pygame.joystick.get_count()
        self.joystick = pygame.joystick.Joystick(0)
        self.joystick.init()
        self.axes = self.joystick.get_numaxes()
        self.joy_val = ""

        # initialization for serial
        self.ser = serial.Serial(
            port='/dev/ttyUSB0',
            baudrate=9600,
            timeout=1
        )
        self.ser.isOpen()
        self.done=False

    def JoystickRead(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.done = True

        self.joy_val = ""
        for i in range(self.axes):
            axis = int(round(self.joystick.get_axis(i), 2)*100) + 100
            axis_str = str(axis).zfill(3)
            self.joy_val = self.joy_val + axis_str

        #print("joy_val: ", self.joy_val)

    def SerialWriteAndRead(self):
            self.ser.write(self.joy_val.encode())
            # print(axis_str)
            received_data = self.ser.readline().decode()
            print(received_data, '\n')


if __name__ == '__main__':
    joystick = Joystick()
    while (joystick.done != True):
        joystick.JoystickRead()
        joystick.SerialWriteAndRead()
        time.sleep(0.05)
           

接下来对esp32的程序进行一个更改。在这里,esp32任务是读取上位机发来的手柄的值,然后解码。之后将解码后的值发送给上位机来验证手柄信息是否正常发送。相关代码如下,

#include <Arduino.h>

using namespace std;


String received_data;
string received_data_string;
string output_data;
char *p_data;

String default_data = "100100000100100000";

String joy_left_x;
String joy_left_y;
String joy_right_x;
String joy_right_y;
const int ktest= 100;


void ReadData(void)
{
    if ( Serial.available() ) {
        received_data = Serial.readString();
    }
    else {
        received_data = default_data;
    }
    delay(20);
}

void ProcessData(void)
{
    if (received_data.length() > 17)
    {
        joy_left_x = received_data.substring(0, 3);
        int left_x = joy_left_x.toInt();
        joy_left_y = received_data.substring(3, 6);
        int left_y = joy_left_y.toInt();
        joy_right_x = received_data.substring(6, 9);
        int right_x = joy_right_x.toInt();
        joy_right_y = received_data.substring(9, 12);
        int right_y = joy_right_y.toInt();
        delay(20);
    }
}

void ShowData(void)
{
    // Serial.print("Data received: \n");
    // Serial.println(received_data);
    // Serial.print("Joy Left X: \n");
    Serial.println(joy_left_x);
    Serial.println(joy_left_y);
    // Serial.println(joy_right_x);
    // Serial.println(joy_right_y);
}

void setup() {
    Serial.begin(9600);
}

void loop() {
    ReadData();
    ProcessData();
    ShowData();
    delay(20);
}

首先下载ESP32的程序,再运行上位机程序。如果正常输出,则会看到,

100

100

100

...

这是因为默认状态下,左摇杆的 x x x轴位置为0。为了方便进行串口通信,我们取到小数点后两位小数,并将其放大100倍。最后我们将其映射到0~200这个范围,这也是为什么输出为100的原因。

在调试过程中,需要注意的是UART的配置问题,如果配置有误,则会造成上位机输出信息错误。此外UART时序也是一个需要注意的问题。如果在调试过程中,发现结果不对,但是程序逻辑是正常的, 那么需要检查一下两侧的UART配置。

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DIY使用ESP32的手持游戏手柄并通过BLE进行通信 硬件部件: esp32× 1个 ws2812b× 6 游戏杆× 2 角度按钮× 2 按钮× 8 18560电池和电池座× 2 三向拨动开关× 1个 TP4056带保护的电池充电电路× 1个 MT3608升压转换器× 1个 软件应用程序和在线服务: Arduino IDE 手动工具和制造机: 烙铁(通用) 这是一个使用ESP32微控制器制作的DIY手持游戏手柄。您可能之前已经看过这样的游戏控制器。但大多数使用Arduino。或更具体地说,是Arduino pro micro或Leonardo开发板。因为这些板支持HID或通过USB的人机接口设备。但是esp32没有这种功能。那么我们该怎么做呢?嗯,esp32确实具有BLE或低功耗蓝牙,我们可以使用它通过蓝牙进行无线通信。 因此,我们有ESP32模块作为中央控制器,然后有USB C型端口和一个3.3v稳压器,可将5v转换为3.3v。然后我们有了编程电路,使用具有自动编程模式的ch340c usb到串行转换器和使用双工晶体管的自动复位电路。我们还具有电池充电电路,以及过充电和过放电保护功能。然后使用升压转换器ic将其升压至5v。接下来,我们有操纵杆,D-Pad按钮,触发按钮,Neopixel LED和i2c端口。最后是3向开关,可在USB电源和电池电源之间切换。 游戏手柄有 2个模拟游戏杆 2个触发按钮 2个D-Pads 6个可寻址RGB LED 和一个i2c端口 它可以使用2个18650锂离子电池运行,可以使用USB C型端口对其进行更改。它也可以用来对微控制器进行编程。该设计的灵感来自新的ps5控制器,以使其外观更好。GPIO4使用分压器连接到电池。这样我们就可以测量电池电压了。NeoPixel LED将指示控制器是否连接到设备或电池电压是否低。 该代码基于lemmingDev的BLE Gamepad库。在代码中,我为不同的输入定义了所有GPIO引脚。然后在设置中有一些初始的led动画。板子一旦连接到设备,所有的LED就会变成绿色。并且它将按先前定义的时间间隔检查电池电压。然后我们获得按钮状态,并相应地设置游戏手柄按钮。然后,我们采用模拟输入,并将其映射到合适的值。最后根据我们收到的值设置轴。上载代码后,在智能手机或PC上打开蓝牙菜单,您会看到一个新设备弹出。单击该按钮将其连接,就可以开始游戏了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stan Fu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值