这个项目的目标是实现使用手柄来控制ESP32。最近正在进行无人机项目,但是由于没有适合的遥控器来控制四轴,画板子也有些占用时间,所以比较有效的方法就是基于手头有的Xbox手柄来进行一个DIY,在手柄与ESP32之间建立串口通信。此处使用PC作为中继,可能速度有些慢,但是基于目前需求,速度已经足够了。下图说明了无人机项目的通信方式,红框部分为本次涉及部分。
这里我使用主要Ubuntu 18作为开发环境,Win10下也能正常运行。python版本为3.9,所需库为pygame
与pyserial
。
手柄与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界面。这时候按动手柄上的相关按键,就能看到数值的实时更新。
接下来测试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
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
...
如果以上两个的测试正常通过,我们接下来就可以测试手柄和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
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配置。