Arduino手势识别实现指南

物联网(IoT)设备中,触摸屏往往成为与设备直接交互的唯一方式,而手势识别则为这种交互提供了极大的便利。然而,对于这些小型设备来说,实现手势识别的信息并不丰富。本文旨在展示如何使用Arduino兼容设备和TFT触摸屏实现简单的滑动手势识别

前提条件

为了实现这一功能,需要以下设备和软件:

  • 一个Arduino兼容设备。本文使用的是ESP32,但也可以使用其他设备,只需进行一些接线修改。
  • 一个TFT显示屏。本文的代码是为RA8875编写的,但也可以在其他显示屏上使用,如ILI9341,只需进行少量代码修改。
  • ArduinoIDE及其相应的开发板管理器。
  • 一些跳线和原型板。

概念化实现

在编写代码之前,需要解决几个问题。读取触摸事件可能会有些棘手,因为没有全面的框架来处理这些细节。还需要为不同的目的实现计时功能。此外,还需要实际实现手势计算。

让从简单的模式开始——无延迟的计时器。在loop()函数中使用delay()可能会导致问题,因为它会阻塞,从而可能导致其他活动在等待时停止。有时需要在允许loop()继续运行的同时,定时运行某些操作。可以通过一个全局变量来记录时间戳,并使用millis()函数来实现这一点:

#define INTERVAL 100 // 1/10秒 uint32_t _timeoutTS = millis(); void loop() { if (millis() - _timeoutTS > INTERVAL) { _timeoutTS = millis(); // 执行操作 } }

这样做的目的是让"执行操作"部分每秒只运行10次,而不是每次调用loop()时都运行。这比使用delay(100)更可取,因为后者会减慢整个loop()过程,而不仅仅是那一部分代码。将在后面使用这种技术,所以请记住,这是解决计时问题的方法。

请注意,这可能因设备而异。请查看库示例代码,了解如何读取触摸事件。一个几乎普遍存在的问题是,设备处理触摸事件的速度没有loop()调用的速度快。SPI总线的速度没有CPU快。为了解决这个问题,只使用前面概述的技术,在定时间隔内处理触摸事件。然而,要做的是提供一个隐藏细节的单一例程。它被称为tryGetTouchEvent(),它将填充一个tsPoint_t结构,并在发生触摸事件时返回true。设备是如何做到这一点的并不重要,但将向展示RA8875的代码。

请注意,大多数这些屏幕需要校准。它们从触摸事件返回的点与屏幕上的点不匹配,必须进行偏移。幸运的是,不需要精确的物理坐标。只需要知道相对于彼此的坐标,而不是物理设备。不会在这里涵盖校准。

对于任何手势计算,需要知道设备何时被触摸以及何时触摸被释放。为此,以设备能够处理的间隔轮询触摸事件——这里为1/10秒。跟踪设备是否被触摸以及它当前是否被触摸。这很重要。需要这些边缘条件来确定当有人将手指放在屏幕上时,然后继续直到他们释放它。基本上,要做的就是这样:

_touched = tryGetTouchEvent(&pt); if (_touched != _touchedOld) { if (_touched) { // 触摸事件处理代码 } else { // 释放事件处理代码 } } _touchedOld = _touched;

这应该相当简单。只是看看触摸状态是否发生了变化,如果是的话,确定它是触摸还是释放。

对于复杂的手势,需要保留一个数组,记录在触摸显示屏时"绘制"的点,并在触摸释放时处理它们。然而,由于正在做基本的滑动,可以走一个重要的捷径,大大简化代码。

只需要跟踪他们第一次触摸显示屏的点,以及他们释放的点。然后比较x和y值的差异。最大的差异决定了垂直滑动(y差异最大),还是水平滑动(x差异最大)。

还需要确保他们实际上滑动得足够远才能注册,但这很简单。所要做的就是确保x或y坐标的差异足够长。有垂直和水平的不同阈值,因为屏幕不是正方形的。

当看到代码时,会看到所有这些在行动中。

构建实现

请注意,这是硬件特定的。想法是将显示屏连接到设备的主SPI总线,然后连接任何额外的引脚,如RST。在ESP32上使用RA8875看起来像这样:

  • RA8875 VIN ➜ ESP32 VIN (+5vdc)
  • RA8875 GND ➜ ESP32 GND
  • RA8875 3Vo ➜ N/C
  • RA8875 LITE ➜ N/C
  • RA8875 SCK ➜ ESP32 GPIO18
  • RA8875 MISO ➜ESP32GPIO19
  • RA8875 MOSI ➜ ESP32 GPIO23
  • RA8875 CS ➜ESP32GPIO5
  • RA8875 RST ➜ESP32GPIO15
  • RA8875 WAIT ➜ N/C
  • RA8875 INT ➜ N/C
  • RA8875 Y+ ➜ N/C
  • RA8875 Y- ➜ N/C
  • RA8875 X- ➜ N/C
  • RA8875 X+ ➜ N/C

最后,来看代码。由于已经探讨了技术,将通过查看整个tft_swipe.ino文件来了解它们在行动中的所有内容:

// touch configuration #define TOUCH_INTERVAL 100 #define TOUCH_THRESHOLD_X 200 #define TOUCH_THRESHOLD_Y 120 // time before text indicator disappears #define TEXT_TIMEOUT 2000 // hardware configuration #define RA8875_CS 5 #define RA8875_RST 15 #include #include #include void drawCentered(char * sz); bool tryGetTouchEvent(tsPoint_t * point); Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RST); // for handling touched/release events bool _touchedOld = false; bool _touched = _touchedOld; uint32_t _touchTS = 0; // first and last points touched tsPoint_t _touchFirst; tsPoint_t _touchLast; // the timestamp for making text go away uint32_t _textTimeoutTS = 0; void setup() { Serial.begin(115200); // init the display if (!tft.begin(RA8875_800x480)) { Serial.println(F("RA8875 Not Found!")); while (1); } tft.displayOn(true); tft.GPIOX(true); // Enable TFT - display enable tied to GPIOX tft.PWM1config(true, RA8875_PWM_CLK_DIV1024); // PWM output for backlight tft.PWM1out(255); tft.touchEnable(true); tft.fillScreen(RA8875_WHITE); } void loop() { if (millis() - _touchTS > TOUCH_INTERVAL) { _touchTS = millis(); // process our touch events tsPoint_t pt; _touched = tryGetTouchEvent(&pt); if (_touched) _touchLast = pt; if (_touched != _touchedOld) { if (_touched) { _touchFirst = pt; } else { pt = _touchLast; // compute differences int32_t dx = pt.x-_touchFirst.x; int32_t dy = pt.y-_touchFirst.y; uint32_t adx=abs(dx); uint32_t ady=abs(dy); // swipe horizontal if (adx > ady && adx > TOUCH_THRESHOLD_X) { if (0 > dx) { // swipe right to left drawCentered("Right to left"); _textTimeoutTS=millis(); } else { // swipe left to right drawCentered("Left to right"); _textTimeoutTS=millis(); } // swipe vertical } else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { if (0 > dy) { // swipe bottom to top drawCentered("Bottom to top"); _textTimeoutTS=millis(); } else { // swipe top to bottom drawCentered("Top to bottom"); _textTimeoutTS=millis(); } } } } _touchedOld = _touched; } // make the text we displayed disappear, if necessary if (_textTimeoutTS && millis() - _textTimeoutTS > TEXT_TIMEOUT) { _textTimeoutTS = 0; tft.fillScreen(RA8875_WHITE); } } // read the TFT touch bool tryGetTouchEvent(tsPoint_t * point) { uint16_t x, y; tft.touchRead(&x, &y); delay(1); if (tft.touched()) { tft.touchRead(&x, &y); point->x = x; point->y = y; return true; } return false; } // draws text centered on the display // BUG: getTextBounds() doesn't seem // to work right for computing width void drawCentered(const char *sz) { int16_t x, y; uint16_t w, h; tft.textMode(); tft.setTextWrap(false); tft.setTextSize(1); tft.textEnlarge(1); tft.getTextBounds(sz, 0, 0, &x, &y, &w, &h); tft.textTransparent(RA8875_BLACK); tft.textSetCursor((tft.width() - w) / 2, (tft.height() - h) / 2); tft.textWrite(sz); }

鉴于在前面的部分已经涵盖了这些内容,大部分内容应该相当清楚。运行两个"计时器",一个用于触摸事件,一个用于清除文本。在释放时通过计算差异来处理触摸事件。当滑动时,将滑动命令写入显示屏,然后启动"计时器"来清除文本。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485