第十三章 制作一个Tello无人机的WiFi无线遥控器(ESP8266+JoyStick Shield)(Arduino边做边学:从点亮Led到物联网)

本系列文章为作者原创,未经作者书面同意,不得转载!

首先声明一下:本文将要制作的Tello无人机遥控器是基于睿炽科技官网公开的Tello SDK,网址链接,Tello无人机是一款教育编程无人机,用户可以根据睿炽科技公开的SDK编程控制无人机,为了让读者更好的理解程序和基于本文能够自己动手自做一个遥控器,本文会对睿炽科技的SDK做一些必要的引用以对开源的程序做一些解释,如涉及版权问题,请第一时间联系作者,谢谢!

Tello无人机是大疆跟睿炽科技合作开发的一款教育编程无人机,针对STEAM(科学、技术、工程、艺术、数学)教育场景及需求。


首图.png

Tello支持Scratch、Python等语言进行编程控制,儿子最近在捣鼓Scratch编程,于是这个无人机变成了他六一儿童节礼物。


002.jpg
001.jpg

这个无人机其实非常小巧,室内都能飞行,给小孩玩是非常不错的。但是无人机并没有附带遥控手柄,而是在官网提供了遥控APP,安装到手机上,通过WiFi连接上无人机后进行控制(当然也可以通过Scratch编程进行控制,这部分内容我会在另一个系列《Scratch边玩边学:从动画、游戏到算法入门》中介绍)。

其实手柄是有的,不过要单独购买:


手柄.png

多少钱?忘了,反正太贵(你有没有发现,随着学习Arduino的深入,你会发现市面上的电子产品给人的感觉越来越贵了,呵呵,开个玩笑!),既然觉得贵,那就自己做一个吧!Tello本身就是一款教育编程机器人,手柄贵,不就是要让我们自己动手来做一个吗?你说是不是?


好吧,今天我们要做的项目就是Tello无人机遥控手柄,通过遥控手柄实现Tello无人机起飞、降落,前后左右飞行以及上升下降。

在开始之前先介绍一下Tello无人机支持的无线连接方式,Tello无人机是基于WiFi UDP协议跟控制器(遥控手柄、电脑、手机APP)实现连接的,所以我们需要准备的组件就要包括一个WiFi模块。

1 本章您将学到

在这个项目中,您将学到的:

  • 学会基于第三方SDK文档进行简单项目开发
  • 通过ESP8266模块实现UDP消息透传
  • JoyStick摇杆扩展板的使用

2 工具和组件

2.1 工具列表

本项目不需要额外的工具。

2.2 元器件列表

元器件 型号 数量 备注
主控板 arduino Uno 1
JoyStick摇杆扩展板 1
ESP8266 12N 1
杜邦线 4
数据线 Uno数据线 1

2.3 工具和元器件介绍

2.3.1 JoyStick摇杆扩展板

JoyStick Shield游戏摇杆扩展板是我们在项目中第一次使用,我们简单介绍一下:


003.jpg

这个扩展板是我偶然发现的,原先的设计是通过一个摇杆+4个按键进行设计,摇杆都买好了:


004.jpg

在购买3D打印机主控的时候偶然发现了JoyStick Shield,这个太好了,省去了搭建电路的麻烦,爽!其实Arduino最吸引人的地方就是它的外围模块太丰富了,只有你想不到,呵呵!言归正传,我们还是来介绍这个扩展板吧!

Joystick Shield还添加了nRF24L01的RF接口和Nokia5110 LCD接口,这样非常方便二次的游戏开发。

2.3.1.1 技术参数

这个似乎没什么可介绍的,这个扩展板其实就是一个遥杆+六个按键,注意中间部位还有两个小按键,按键我们在本系列文章之前就有介绍,记得是交通灯那一篇,有不清楚的可以去翻翻那一篇文章看看。

另外,扩展板上还有一个开关可以在3.3V 和5V 之间切换,可以将此模块用于其它3.3V单片机平台,比如STM32,由于Arduino UNO支持5V和3.3V供电,所以这个开关在UNO上似乎没有什么意义,经测试也的确没什么用。

2.3.1.2 摇杆的原理

前面介绍过JoyStick Shield游戏摇杆扩展板就是一个双轴按键摇杆+6个按键,按键我们都清楚了,那这个双轴按键摇杆是个什么东东呢?其实它也很简单,就是两个电位计+一个按键。

那现在我们应该很清楚了,JoyStick Shield游戏摇杆扩展板就是7个按键+两个电位计。

我们知道了这个扩展板的组成,但是你也许还有疑惑,我们怎么能够知道摇杆到底朝那个方向摇动呢?其实就是通过模拟输入口读取两个电位计的电压值,摇杆朝不同的方向摇动会导致这两个值发生变化,根据这个变化,我们就能判断摇杆的方向。感兴趣的朋友可以自己测试一下。

2.3.1.3 跟UNO的连接

JoyStick Shield游戏摇杆扩展板在实际使用时直接插在UNO电路板上即可,不过我们还是需要了解一下它跟UNO的实际连接。

前面说过,JoyStick Shield游戏摇杆扩展板就是7个按键+两个电位计,如果你需要使用全部的7个按键,那么就需要7个数字输入引脚+2个模拟输入引脚,另外还需要连接5V和GND,7个数字引脚用的是(2、3、4、5、6、7、8),扩展板提供了从9到13数字引脚的接口,可以直接使用。

两个模拟口可以自定义,A0到A5都可以,后面我们在介绍JoyStickShield扩展库的时候会再介绍。

2.3.2 ESP-12F WiFi模块

我们重点介绍一下这个模块。
ESP-12F是一款超低功耗的UART-WiFi 透传模块,专为移动设备和物联网应用设计,可将用户的物理设备连接到Wi-Fi 无线网络上,进行互联网或局域网通信,实现联网功能。


12F.png

这个模块使用之前需要焊接到转接板上,下图是转接板:


12F board.png

下面两张图是焊接完成后的样子:


12F-01.png
12F-02.png

ESP-12F模块引脚间距是2mm的,焊接起来比较费劲。本来想采用ESP-01模块的,这个模块不需要焊接,有引脚直接可以用,不过ESP-01模块对供电要求比较高,而且Flash才8Mbit,可用引脚也比较少,可玩性跟12F差太多,所以就不推荐大家使用了,不过如果是做一个实际项目,有成本控制且只做无线透传,ESP-01就相对合适一些(其实ESP8266模块本身就是一个MCU,跟Arduino的主控板一样,也能在Arduino IDE下编程)。

2.3.2.1 产品特性

  • 支持无线802.11 b/g/n 标准
  • 支持STA/AP/STA+AP 三种工作模式
  • 内置TCP/IP协议栈,支持多路TCP Client连接
  • 支持丰富的Socket AT指令
  • 支持UART/GPIO数据通信接口
  • 支持Smart Link 智能联网功能
  • 支持远程固件升级(OTA)
  • 内置32位MCU,可兼作应用处理器
  • 超低能耗,适合电池供电应用
  • 3.3V 单电源供电

注意:最后一条,3.3V供电,建议由电池组或者电源模块单独供电,用一个降压模块,直接用UNO的3.3V供电很不稳定。

2.3.2.2 模块使用方法

这部分内容比较关键,ESP8266系列模块在使用前都需要进行调试和模式的设定,包括工作模式和串口通讯速率,如果模块烧录了非AT固件,还需要重新对模块进行烧录,好在如果你是新买的模块,或者买回来后没有对其进行过其它固件烧录,那么就没有烧录的必要,它出厂就默认烧录好了AT固件。

那么我们只需要设置一下它的串口通信速率即可,ESP8266模块默认的串口通信速率是:115200,这个速率对于UNO主控板的软串口来说太高了,不稳定,所以我们需要将其设定为:9600。

设定方法:通过串口模块跟ESP8266连接上电脑后,通过串口指令进行设定,指令如下:

AT+UART_DEF=9600,8,1,0,0

这部分内容还待详细整理后再发布出来...

3 电路设计

3.1 电路图

根据我们的项目需求,设计电路图如下:


UNO Tello Controller_bb.png

3.2 电路原理

这个电路图其实比较简单,JoyStick Shield游戏摇杆扩展板直接安装到UNO板上,数字9、10口作为软串口的RX、TX引脚跟ESP8266-12N连接,图中ESP8266-12N模块由UNO直接供电,VCC接的是3.3V,但在实际项目中,采用的是单独供电,单独供电的时候,ESP8266-12N需要和UNO共地。

4 程序设计

4.1 类库介绍

这个项目用的库比较多,有四个,我们分别介绍一下:

这里有必要说明一下:本系列文章在开篇就介绍过,我希望能照顾到不同的读者,所以对于初学者,关于程序中引用的库,你只需要知道怎么使用,用到库中的那几个方法,这几个方法是做什么的,我觉得就OK了,刚开始没有必要钻入一个过细的小问题而影响了自己的学习,毕竟有些问题还是需要一定的背景知识,有些甚至是大学阶段才能接触到的,所以不必操之过急,知道怎么使用,然后能够基于这些东西创造属于自己的作品,实现自己的设计才是最重要的,那些小小的牛角尖,相信我,随着你学习的进步,它们会迎刃而解的!

但是对于那些有一定基础,希望对Arduino了解更深入一些的同学,你可以对这部分的内容做一个全面的学习、理解。

4.1.1 JoystickShield.h库介绍

4.1.1.1 JoystickShield.h库的下载

JoystickShield.h库下载地址:百度网盘链接
下载解压缩后,直接放到Arduino项目文件夹(一般在:我的电脑 \ 文档 \ Arduino \)中的libraries子目录中。

4.1.1.1 JoystickShield.h库的介绍

我们来看一下这个库的头文件,

#ifndef JoystickShield_H
#define JoystickShield_H

#define CENTERTOLERANCE 5

// Compatibility for Arduino 1.0

#if ARDUINO >= 100
    #include <Arduino.h>
#else    
    #include <WProgram.h>
#endif

/**
 * Enum to hold the different states of the Joystick
 *
 */
enum JoystickStates {
    CENTER,  // 0
    UP,
    RIGHT_UP,
    RIGHT,
    RIGHT_DOWN,
    DOWN,
    LEFT_DOWN,
    LEFT,
    LEFT_UP   //8
};


static const bool ALL_BUTTONS_OFF[7] = {false, false, false, false, false, false, false};

/**
 * Class to encapsulate JoystickShield
 */
class JoystickShield {

public:

    JoystickShield(); // constructor

    void setJoystickPins (byte pinX, byte pinY);
    void setButtonPins(byte pinSelect, byte pinUp, byte pinRight, byte pinDown, byte pinLeft, byte pinF, byte pinE);
    void setButtonPinsUnpressedState(byte pinSelect, byte pinUp, byte pinRight, byte pinDown, byte pinLeft, byte pinF, byte pinE);
    void setThreshold(int xLow, int xHigh, int yLow, int yHigh);

    void processEvents();
    void processCallbacks();
    
    void calibrateJoystick();

    // Joystick events
    bool isCenter();
    bool isUp();
    bool isRightUp();
    bool isRight();
    bool isRightDown();
    bool isDown();
    bool isLeftDown();
    bool isLeft();
    bool isLeftUp();
    bool isNotCenter();
    
    // Joystick coordinates
    int xAmplitude();
    int yAmplitude();

    // Button events
    bool isJoystickButton();
    bool isUpButton();
    bool isRightButton();
    bool isDownButton();
    bool isLeftButton();
    bool isFButton();
    bool isEButton();

    // Joystick callbacks
    void onJSCenter(void (*centerCallback)(void));
    void onJSUp(void (*upCallback)(void));
    void onJSRightUp(void (*rightUpCallback)(void));
    void onJSRight(void (*rightCallback)(void));
    void onJSRightDown(void (*rightDownCallback)(void));
    void onJSDown(void (*downCallback)(void));
    void onJSLeftDown(void (*leftDownCallback)(void));
    void onJSLeft(void (*leftCallback)(void));
    void onJSLeftUp(void (*leftUpCallback)(void));
    void onJSnotCenter(void (*notCenterCallback)(void));

    // Button callbacks
    void onJoystickButton(void (*jsButtonCallback)(void));
    void onUpButton(void (*upButtonCallback)(void));
    void onRightButton(void (*rightButtonCallback)(void));
    void onDownButton(void (*downButtonCallback)(void));
    void onLeftButton(void (*leftButtonCallback)(void));
    void onFButton(void (*FButtonCallback)(void));
    void onEButton(void (*EButtonCallback)(void));
    
private:

    // threshold values
    int x_threshold_low;
    int x_threshold_high;
    int y_threshold_low;
    int y_threshold_high;

    // joystick pins
    byte pin_analog_x;
    byte pin_analog_y;

    //button pins
    byte pin_joystick_button;
    byte pin_up_button;
    byte pin_right_button;
    byte pin_down_button;
    byte pin_left_button;
    byte pin_F_button;
    byte pin_E_button;

    byte pin_joystick_button_unpressed;
    byte pin_up_button_unpressed;
    byte pin_right_button_unpressed;
    byte pin_down_button_unpressed;
    byte pin_left_button_unpressed;
    byte pin_F_button_unpressed;
    byte pin_E_button_unpressed;

    // joystick
    byte joystickStroke;
    int x_position;
    int y_position;

    //current states of Joystick
    JoystickStates currentStatus;

    // array of button states to allow multiple buttons to be pressed concurrently
    // order is up, right, down, left, e, f, joystick
    bool buttonStates[7];

    // Joystick callbacks
    void (*centerCallback)(void);
    void (*upCallback)(void);
    void (*rightUpCallback)(void);
    void (*rightCallback)(void);
    void (*rightDownCallback)(void);
    void (*downCallback)(void);
    void (*leftDownCallback)(void);
    void (*leftCallback)(void);
    void (*leftUpCallback)(void);
    void (*notCenterCallback)(void);

    // Button callbacks
    void (*jsButtonCallback)(void);
    void (*upButtonCallback)(void);
    void (*rightButtonCallback)(void);
    void (*downButtonCallback)(void);
    void (*leftButtonCallback)(void);
    void (*FButtonCallback)(void);
    void (*EButtonCallback)(void);
    

    // helper functions
    void clearButtonStates();
    void initializeCallbacks();
};

#endif

库的说明待补充...

4.1.2 WiFiEsp.h库介绍

4.1.2.1 WiFiEsp.h库的下载

WiFiEsp.h库下载地址:百度网盘链接
下载解压缩后,直接放到Arduino项目文件夹(一般在:我的电脑 \ 文档 \ Arduino \)中的libraries子目录中。

这个库其实包含好几个库:WiFiEsp.h、WiFiEspClient.h、WiFiEspServer.h、WiFiEspUdp.h,这个是一个非常优秀的ESP8266 AT指令封装库,在本文中会用到两个库:WiFiEsp.h、WiFiEspUdp.h,我们简单了解一下,其它两个我们在别的文章中还会继续介绍。

4.1.2.1 WiFiEsp.h库的介绍

看一下这个库的头文件:

#ifndef WiFiEsp_h
#define WiFiEsp_h

#include <Arduino.h>
#include <Stream.h>
#include <IPAddress.h>
#include <inttypes.h>


#include "WiFiEspClient.h"
#include "WiFiEspServer.h"
#include "utility/EspDrv.h"
#include "utility/RingBuffer.h"
#include "utility/debug.h"


class WiFiEspClass
{

public:

    static int16_t _state[MAX_SOCK_NUM];
    static uint16_t _server_port[MAX_SOCK_NUM];

    WiFiEspClass();


    /**
    * Initialize the ESP module.
    *
    * param espSerial: the serial interface (HW or SW) used to communicate with the ESP module
    */
    static void init(Stream* espSerial);


    /**
    * Get firmware version
    */
    static char* firmwareVersion();


    // NOT IMPLEMENTED
    //int begin(char* ssid);

    // NOT IMPLEMENTED
    //int begin(char* ssid, uint8_t key_idx, const char* key);


    /**
    * Start Wifi connection with passphrase
    * the most secure supported mode will be automatically selected
    *
    * param ssid: Pointer to the SSID string.
    * param passphrase: Passphrase. Valid characters in a passphrase
    *         must be between ASCII 32-126 (decimal).
    */
    int begin(const char* ssid, const char* passphrase);


    /**
    * Change Ip configuration settings disabling the DHCP client
    *
    * param local_ip:   Static ip configuration
    */
    void config(IPAddress local_ip);


    // NOT IMPLEMENTED
    //void config(IPAddress local_ip, IPAddress dns_server);

    // NOT IMPLEMENTED
    //void config(IPAddress local_ip, IPAddress dns_server, IPAddress gateway);

    // NOT IMPLEMENTED
    //void config(IPAddress local_ip, IPAddress dns_server, IPAddress gateway, IPAddress subnet);

    // NOT IMPLEMENTED
    //void setDNS(IPAddress dns_server1);

    // NOT IMPLEMENTED
    //void setDNS(IPAddress dns_server1, IPAddress dns_server2);

    /**
    * Disconnect from the network
    *
    * return: one value of wl_status_t enum
    */
    int disconnect(void);

    /**
    * Get the interface MAC address.
    *
    * return: pointer to uint8_t array with length WL_MAC_ADDR_LENGTH
    */
    uint8_t* macAddress(uint8_t* mac);

    /**
    * Get the interface IP address.
    *
    * return: Ip address value
    */
    IPAddress localIP();


    /**
    * Get the interface subnet mask address.
    *
    * return: subnet mask address value
    */
    IPAddress subnetMask();

    /**
    * Get the gateway ip address.
    *
    * return: gateway ip address value
    */
   IPAddress gatewayIP();

    /**
    * Return the current SSID associated with the network
    *
    * return: ssid string
    */
    char* SSID();

    /**
    * Return the current BSSID associated with the network.
    * It is the MAC address of the Access Point
    *
    * return: pointer to uint8_t array with length WL_MAC_ADDR_LENGTH
    */
    uint8_t* BSSID(uint8_t* bssid);


    /**
    * Return the current RSSI /Received Signal Strength in dBm)
    * associated with the network
    *
    * return: signed value
    */
    int32_t RSSI();


    /**
    * Return Connection status.
    *
    * return: one of the value defined in wl_status_t
    *         see https://www.arduino.cc/en/Reference/WiFiStatus
    */
    uint8_t status();


    /*
      * Return the Encryption Type associated with the network
      *
      * return: one value of wl_enc_type enum
      */
    //uint8_t   encryptionType();

    /*
     * Start scan WiFi networks available
     *
     * return: Number of discovered networks
     */
    int8_t scanNetworks();

    /*
     * Return the SSID discovered during the network scan.
     *
     * param networkItem: specify from which network item want to get the information
     *
     * return: ssid string of the specified item on the networks scanned list
     */
    char*   SSID(uint8_t networkItem);

    /*
     * Return the encryption type of the networks discovered during the scanNetworks
     *
     * param networkItem: specify from which network item want to get the information
     *
     * return: encryption type (enum wl_enc_type) of the specified item on the networks scanned list
     */
    uint8_t encryptionType(uint8_t networkItem);

    /*
     * Return the RSSI of the networks discovered during the scanNetworks
     *
     * param networkItem: specify from which network item want to get the information
     *
     * return: signed value of RSSI of the specified item on the networks scanned list
     */
    int32_t RSSI(uint8_t networkItem);


    // NOT IMPLEMENTED
    //int hostByName(const char* aHostname, IPAddress& aResult);



    ////////////////////////////////////////////////////////////////////////////
    // Non standard methods
    ////////////////////////////////////////////////////////////////////////////

    /**
    * Start the ESP access point.
    *
    * param ssid: Pointer to the SSID string.
    * param channel: WiFi channel (1-14)
    * param pwd: Passphrase. Valid characters in a passphrase
    *         must be between ASCII 32-126 (decimal).
    * param enc: encryption type (enum wl_enc_type)
    * param apOnly: Set to false if you want to run AP and Station modes simultaneously
    */
    int beginAP(const char* ssid, uint8_t channel, const char* pwd, uint8_t enc, bool apOnly=true);

    /*
    * Start the ESP access point with open security.
    */
    int beginAP(const char* ssid);
    int beginAP(const char* ssid, uint8_t channel);

    /**
    * Change IP address of the AP
    *
    * param ip: Static ip configuration
    */
    void configAP(IPAddress ip);



    /**
    * Restart the ESP module.
    */
    void reset();

    /**
    * Ping a host.
    */
    bool ping(const char *host);


    friend class WiFiEspClient;
    friend class WiFiEspServer;
    friend class WiFiEspUDP;

private:
    static uint8_t getFreeSocket();
    static void allocateSocket(uint8_t sock);
    static void releaseSocket(uint8_t sock);

    static uint8_t espMode;
};

extern WiFiEspClass WiFi;

#endif

库的说明待补充...

4.1.2.2 WiFiEspUdp.h库的介绍

看一下这个库的头文件:

#ifndef WiFiEspUdp_h
#define WiFiEspUdp_h

#include <Udp.h>

#define UDP_TX_PACKET_MAX_SIZE 24

class WiFiEspUDP : public UDP {
private:
  uint8_t _sock;  // socket ID for Wiz5100
  uint16_t _port; // local port to listen on
  
  
  uint16_t _remotePort;
  char _remoteHost[30];
  

public:
  WiFiEspUDP();  // Constructor

  virtual uint8_t begin(uint16_t);  // initialize, start listening on specified port. Returns 1 if successful, 0 if there are no sockets available to use
  virtual void stop();  // Finish with the UDP socket

  // Sending UDP packets

  // Start building up a packet to send to the remote host specific in ip and port
  // Returns 1 if successful, 0 if there was a problem with the supplied IP address or port
  virtual int beginPacket(IPAddress ip, uint16_t port);

  // Start building up a packet to send to the remote host specific in host and port
  // Returns 1 if successful, 0 if there was a problem resolving the hostname or port
  virtual int beginPacket(const char *host, uint16_t port);

  // Finish off this packet and send it
  // Returns 1 if the packet was sent successfully, 0 if there was an error
  virtual int endPacket();

  // Write a single byte into the packet
  virtual size_t write(uint8_t);

  // Write size bytes from buffer into the packet
  virtual size_t write(const uint8_t *buffer, size_t size);

  using Print::write;

  // Start processing the next available incoming packet
  // Returns the size of the packet in bytes, or 0 if no packets are available
  virtual int parsePacket();

  // Number of bytes remaining in the current packet
  virtual int available();

  // Read a single byte from the current packet
  virtual int read();

  // Read up to len bytes from the current packet and place them into buffer
  // Returns the number of bytes read, or 0 if none are available
  virtual int read(unsigned char* buffer, size_t len);

  // Read up to len characters from the current packet and place them into buffer
  // Returns the number of characters read, or 0 if none are available
  virtual int read(char* buffer, size_t len) { return read((unsigned char*)buffer, len); };

  // Return the next byte from the current packet without moving on to the next byte
  virtual int peek();

  virtual void flush(); // Finish reading the current packet

  // Return the IP address of the host who sent the current incoming packet
  virtual IPAddress remoteIP();

  // Return the port of the host who sent the current incoming packet
  virtual uint16_t remotePort();


  friend class WiFiEspServer;
};

#endif

库的说明待补充...

4.1.3 SoftwareSerial.h库介绍

4.1.3.1 SoftwareSerial.h库的下载

SoftwareSerial.h库为Arduino的自带核心库,无需下载,可直接引用。

4.1.3.1 SoftwareSerial.h库的介绍

我们来看一下这个库的头文件:

#ifndef SoftwareSerial_h
#define SoftwareSerial_h

#include <inttypes.h>
#include <Stream.h>

#ifndef _SS_MAX_RX_BUFF
#define _SS_MAX_RX_BUFF 64 // RX buffer size
#endif

#ifndef GCC_VERSION
#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
#endif

class SoftwareSerial : public Stream
{
private:
  // per object data
  uint8_t _receivePin;
  uint8_t _receiveBitMask;
  volatile uint8_t *_receivePortRegister;
  uint8_t _transmitBitMask;
  volatile uint8_t *_transmitPortRegister;
  volatile uint8_t *_pcint_maskreg;
  uint8_t _pcint_maskvalue;

  // Expressed as 4-cycle delays (must never be 0!)
  uint16_t _rx_delay_centering;
  uint16_t _rx_delay_intrabit;
  uint16_t _rx_delay_stopbit;
  uint16_t _tx_delay;

  uint16_t _buffer_overflow:1;
  uint16_t _inverse_logic:1;

  // static data
  static uint8_t _receive_buffer[_SS_MAX_RX_BUFF]; 
  static volatile uint8_t _receive_buffer_tail;
  static volatile uint8_t _receive_buffer_head;
  static SoftwareSerial *active_object;

  // private methods
  inline void recv() __attribute__((__always_inline__));
  uint8_t rx_pin_read();
  void setTX(uint8_t transmitPin);
  void setRX(uint8_t receivePin);
  inline void setRxIntMsk(bool enable) __attribute__((__always_inline__));

  // Return num - sub, or 1 if the result would be < 1
  static uint16_t subtract_cap(uint16_t num, uint16_t sub);

  // private static method for timing
  static inline void tunedDelay(uint16_t delay);

public:
  // public methods
  SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic = false);
  ~SoftwareSerial();
  void begin(long speed);
  bool listen();
  void end();
  bool isListening() { return this == active_object; }
  bool stopListening();
  bool overflow() { bool ret = _buffer_overflow; if (ret) _buffer_overflow = false; return ret; }
  int peek();

  virtual size_t write(uint8_t byte);
  virtual int read();
  virtual int available();
  virtual void flush();
  operator bool() { return true; }
  
  using Print::write;

  // public only for easy access by interrupt handlers
  static inline void handle_interrupt() __attribute__((__always_inline__));
};

// Arduino 0012 workaround
#undef int
#undef char
#undef long
#undef byte
#undef float
#undef abs
#undef round

#endif

这个库,我们先看一下它的继承关系:

class SoftwareSerial : public Stream

从这个头文件可以看到SoftwareSerial 类继承自类Stream,而Stream是继承的Print类,Print类的作用是打印数据,通过不同的设备(串口、LCD1602,还是其它TFT的彩色屏幕)打印的过程都是一样的,只是最底层实现不一样,感兴趣的朋友可以去看看一些显示屏的驱动库,基本上都会包含Print这个类。

我们简单的看一下这个头文件的public部分的两个方法(函数):

  SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic = false);

这个是构造函数,有三个参数:receivePin、transmitPin和inverse_logic,前两个参数就是我们定义软串口的RX和TX引脚,第三个参数inverse_logic有缺省值false,在定义软串口时可以不带,这个参数的作用是:在初始化软串口时对RX和TX引脚是否拉高。

  void begin(long speed);

这个函数作用是设置串口传送波特率,软串口波特率我们一般采用9600,这个波特率需要跟与串口通信的设备或者模块保持一致。

4.1.4 IPAddress.h库介绍

4.1.4.1 IPAddress.h库的下载

IPAddress.h库为Arduino的自带核心库,无需下载,可直接引用。

4.1.4.1 IPAddress.h库的介绍

我们来看一下这个库的头文件:

/*
  IPAddress.h - Base class that provides IPAddress
  Copyright (c) 2011 Adrian McEwen.  All right reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifndef IPAddress_h
#define IPAddress_h

#include <stdint.h>
#include "Printable.h"
#include "WString.h"

// A class to make it easier to handle and pass around IP addresses

class IPAddress : public Printable {
private:
    union {
    uint8_t bytes[4];  // IPv4 address
    uint32_t dword;
    } _address;

    // Access the raw byte array containing the address.  Because this returns a pointer
    // to the internal structure rather than a copy of the address this function should only
    // be used when you know that the usage of the returned uint8_t* will be transient and not
    // stored.
    uint8_t* raw_address() { return _address.bytes; };

public:
    // Constructors
    IPAddress();
    IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet);
    IPAddress(uint32_t address);
    IPAddress(const uint8_t *address);

    bool fromString(const char *address);
    bool fromString(const String &address) { return fromString(address.c_str()); }

    // Overloaded cast operator to allow IPAddress objects to be used where a pointer
    // to a four-byte uint8_t array is expected
    operator uint32_t() const { return _address.dword; };
    bool operator==(const IPAddress& addr) const { return _address.dword == addr._address.dword; };
    bool operator==(const uint8_t* addr) const;

    // Overloaded index operator to allow getting and setting individual octets of the address
    uint8_t operator[](int index) const { return _address.bytes[index]; };
    uint8_t& operator[](int index) { return _address.bytes[index]; };

    // Overloaded copy operators to allow initialisation of IPAddress objects from other types
    IPAddress& operator=(const uint8_t *address);
    IPAddress& operator=(uint32_t address);

    virtual size_t printTo(Print& p) const;

    friend class EthernetClass;
    friend class UDP;
    friend class Client;
    friend class Server;
    friend class DhcpClass;
    friend class DNSClient;
};

const IPAddress INADDR_NONE(0,0,0,0);

#endif

这个类就是定义一个IP地址对象,说实话,直到开始写这部分内容时,我才意识到这里弄复杂了,其实IP地址可以用一个字符串定义,就像下面这样:

const char *telloAddr= "192,168,10,1";

为什么可以这么做呢?
我们可以看一下这个IP地址对象在哪儿使用了(你可以先看一下主程序,找到这行代码):

Udp.beginPacket(telloAddr, telloPort);

这行代码的作用就是对Udp对象进行初始化,这里会用到一个IP地址对象tellAddr和端口号telloPort,程序的开始都有定义。
但实际上beginPacket这个方法在WiFiEspUDP对象中是重载的,它定义了两个beginPacket方法,如下:

virtual int beginPacket(IPAddress ip, uint16_t port);
virtual int beginPacket(const char *host, uint16_t port);

所以beginPacket方法的第一个参数可以是一个IPAddress对象,也可以是一个字符数组指针变量。

这样你就很清楚了,其实我们的主程序可以更加简化的,不过咱们是为了学习而来,弄懂程序背后的意义才是重点。

关于UDP协议:UDP是一个传输层协议,与之对应的还有TCP协议,它们都工作在IP协议上,它们之间区别就是TCP是面向连接的,而UDP不是,可能有的朋友还是不理解这一点,我简单的举个例子说明一下:
假设某个周末的下午,你到小区的院子里跟小朋友玩耍,你妈妈忙着做晚饭,不一会儿,妈妈的晚饭做好了,而你呢?玩得正嗨,忘了回家的时间。

好了,你妈妈需要叫你回家吃饭了,现在你妈妈有两种做法,一种是按照TCP的模式,一种是UDP的模式,假设你家的阳台正对着小区院子,阳台到院子之间可以通过声音交流(类似IP协议提供的服务),你的小名叫:阿福(类似IP地址),我们来看看这两种模式的区别:

TCP模式:
你妈妈在阳台对着院子大声的喊:“阿福、阿福!”
你听到了,赶紧回答:“妈妈,妈妈,干嘛!”
你妈妈又说:“回家吃饭了!”
你回答:“好的,马上就回来!”
你妈妈听到后,知道你一会儿就回来吃饭,然后开始去忙别的了。

UDP模式:
你的妈妈来到阳台,对着院子大喊一声:“阿福,回家吃饭了!”
你的妈妈觉得你肯定能够听到,反正回家吃饭这件事也没什么大不了,妈妈认为你一会儿就会回家吃饭,然后她就忙别的去了。
你呢?你可能听到了,也可能没听到,小朋友在一起玩耍时本身就是吵吵闹闹的,当你听到了,你肯定就会回家吃饭,这种情况发生的概率很大,毕竟小区院子就正对着你家阳台,你妈妈的声音也够响亮。

如果万一没听到呢?没关系,你妈妈隔一会发现你还没回家,又会跑到阳台,再喊一声:“阿福,回家吃饭了!”

现在你能理解这两种通信方式的区别了吗?

4.2 Tello SDK介绍

这部分内容主要是对Tello SDK文档做一个简单的介绍。

4.2.1 WiFi连接

Tello无人机IP地址:192.168.10.1;
Tello无人机UDP监听端口:8889。

4.2.2 命令参数

命令 功能描述 可能的响应
command 进入命令模式 OK 或者 FALSE
takeoff 自动起飞 OK 或者 FALSE
land 自动降落 OK 或者 FALSE
up xx 向上飞xx厘米(xx范围20~500CM) OK 或者 FALSE
down xx 向下飞xx厘米(xx范围20~500CM) OK 或者 FALSE
left xx 向左飞xx厘米(xx范围20~500CM) OK 或者 FALSE
right xx 向右飞xx厘米(xx范围20~500CM) OK 或者 FALSE
forward xx 向前飞xx厘米(xx范围20~500CM) OK 或者 FALSE
back xx 向后飞xx厘米(xx范围20~500CM) OK 或者 FALSE

注意:命令参数的单位为:距离是厘米、角度是度、速度为厘米/秒。

关于SDK暂时就介绍这些指令,这也是我们在后面程序中需要用到的,当然,官方给出的SDK文档还有更多的指令,感兴趣的朋友可以到官网下载。

4.3 主程序设计

/********************************
Name:     Tello无人机遥控器
Module:   UNO + Joystick + ESP8266-12N
Author:   You xianke
Version:  V1.0
Init:     2018-6-25
Modify: 
*******************************/
#include <JoystickShield.h> // include JoystickShield Library
#include <WiFiEsp.h>
#include <WiFiEspUdp.h>

#include <SoftwareSerial.h>
#include <IPAddress.h>

char ssid[] = "TELLO-AA32D0";    // Tello SSID,这个需要根据无人机的实际值进行修改,启动Tello无人机后,用电脑扫描一下WiFi网络,以TELLO开头的热点即是
char pass[] = "";                // WiFi password is NULL

int status = WL_IDLE_STATUS;     // the Wifi radio's status

JoystickShield joystickShield; // create an instance of JoystickShield object

const int RXPin = 9;   //定义软串口针脚
const int TXPin = 10;

unsigned int localPort = 9000;        // local port to listen for UDP packets

const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive
char packetBuffer[64];          // buffer to hold incoming packet

// A UDP instance to let us send and receive packets over UDP
WiFiEspUDP Udp;
IPAddress telloAddr(192,168,10,1);   //Tello的UdpServer服务端的IP地址
const int telloPort = 8889;          //Tello UDP监听端口号

SoftwareSerial espSerial(RXPin,TXPin); // 定义连接ESP-12N串口

void PrintWifiStatus();
void SendCommand(const char* command);

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

  WiFi.init(&espSerial);
  // WiFi.mode(WIFI_STA);

    // check for the presence of the shield:
    if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
      // don't continue:
      while (true);
    }

    // attempt to connect to WiFi network
    while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
      Serial.println(ssid);
      // Connect to WPA/WPA2 network
      status = WiFi.begin(ssid, pass);
    }
    
    delay(1000);   
    Serial.println("Connected to wifi");
    PrintWifiStatus();

    Serial.println("\nStarting listening a UDP port...");
    // if you get a connection, report back via serial:
    Udp.begin(localPort);     
    Serial.print("Listening on port ");
    Serial.println(localPort);

    SendCommand("command");   //Tello进入命令模式
  delay(100);

  joystickShield.calibrateJoystick();
}

void loop() {
  //遥控指令的处理
  joystickShield.processEvents(); // process events

  if (joystickShield.isUp()) {
    Serial.println("Up") ;
    Serial.println("Tello forward 50CM!") ;
    SendCommand("forward 50");   //Tello向前50CM
    delay(1000);
  }

  if (joystickShield.isRightUp()) {
    Serial.println("RightUp") ;
  }

  if (joystickShield.isRight()) {
    Serial.println("Right") ;
    Serial.println("Tello turn right 50CM!") ;
    SendCommand("right 50");   //Tello向右50CM
    delay(1000);
  }

  if (joystickShield.isRightDown()) {
    Serial.println("RightDown") ;
  }

  if (joystickShield.isDown()) {
    Serial.println("Down") ;
    Serial.println("Tello turn back 50CM!") ;
    SendCommand("back 50");   //Tello向后50CM
    delay(1000);
  }

  if (joystickShield.isLeftDown()) {
    Serial.println("LeftDown") ;
  }

  if (joystickShield.isLeft()) {
    Serial.println("Left") ;
    Serial.println("Tello turn left 50CM!") ;
    SendCommand("left 50");   //Tello向左50CM
    delay(1000);
  }

  if (joystickShield.isLeftUp()) {
    Serial.println("LeftUp") ;
  }

  if (joystickShield.isJoystickButton()) {
    Serial.println("Joystick Clicked") ;
  }

  if (joystickShield.isUpButton()) {
    Serial.println("Up Button Clicked") ;
    Serial.println("Tello land!") ;
    SendCommand("up 50");   //Tello上升50CM
    delay(1000);
  }

  if (joystickShield.isRightButton()) {
    Serial.println("Right Button Clicked") ;
    Serial.println("Tello land!") ;
    SendCommand("land");   //Tello降落
    delay(2000);
  }

  if (joystickShield.isDownButton()) {
    Serial.println("Down Button Clicked") ;
    Serial.println("Tello land!") ;
    SendCommand("down 50");   //Tello下降50CM
    delay(1000);
  }

  if (joystickShield.isLeftButton()) {
    Serial.println("Left Button Clicked") ;
    Serial.println("Tello takeoff!") ;
    SendCommand("takeoff");   //Tello起飞
    delay(2000);
  }

  // new eventfunctions
  if (joystickShield.isEButton()) {
    Serial.println("E Button Clicked") ;
  }

  if (joystickShield.isFButton()) {
    Serial.println("F Button Clicked") ;
  }  
  
  if (joystickShield.isNotCenter()){
    Serial.println("NotCenter") ;
  }
  
  // new position functions
  Serial.print("x ");   
  Serial.print(joystickShield.xAmplitude());
  Serial.print(" y ");
  Serial.println(joystickShield.yAmplitude());

  // 接收到Tello无人机消息后的处理
  int packetSize = Udp.parsePacket();
  if (packetSize) {
    Serial.print("Received packet of size ");
    Serial.println(packetSize);
    Serial.print("From Tello ");
    IPAddress remoteIp = Udp.remoteIP();
    Serial.print(remoteIp);
    Serial.print(", port ");
    Serial.println(Udp.remotePort());

    // read the packet into packetBufffer
    int len = Udp.read(packetBuffer, 64);
    if (len > 0) {
      packetBuffer[len] = 0;
    }
    Serial.println("Contents:");
    Serial.println(packetBuffer);
  }
delay(500);
}

void PrintWifiStatus(){
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

void SendCommand(const char* command){
  Udp.beginPacket(telloAddr, telloPort);
  Udp.write(command, strlen(command));
  Udp.endPacket();
  delay(1000);
}

主程序就不单独解释了,程序中的注释已经非常清楚了!

5 安装调试

下面我们根据电路图将两个模块跟UNO连接上:

组装01.jpg

将Tello无人机开机,打开电脑串口,观察一下遥控器是否跟Tello连接上,连接上后,串口会有WiFi状态打印。

如果连接成功,您就可以通过遥控手柄控制Tello无人机的起飞、降落,上升、下降,前后左右飞行了。

5 总结扩展

因为时间的关系,我并没有将这个手柄做得更加完善,只是搭建了一个原型,您可以根据这个原型来自己设计一个更加完善的遥控手柄,增加外壳,用电池进行供电,甚至增加一个小的液晶屏,直接来显示连接状态和命令发送的相关信息。

另外这个手柄上还有两个小的按钮,我的想法是您可以增加两个自定义飞行动作系列,让无人机能够表演一连串的复杂动作,当然,程序您需要再修改一下,怎么修改?我相信您肯定能够办到,呵呵,实在不行就请关注我们的微信号留言吧!

如果您喜欢本文,您可以点击一下下面的喜欢按钮,您也可以关注我,谢谢您的支持!

推荐阅读更多精彩内容