第十三章 制作一個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 總結擴展

因為時間的關系,我并沒有將這個手柄做得更加完善,只是搭建了一個原型,您可以根據這個原型來自己設計一個更加完善的遙控手柄,增加外殼,用電池進行供電,甚至增加一個小的液晶屏,直接來顯示連接狀態和命令發送的相關信息。

另外這個手柄上還有兩個小的按鈕,我的想法是您可以增加兩個自定義飛行動作系列,讓無人機能夠表演一連串的復雜動作,當然,程序您需要再修改一下,怎么修改?我相信您肯定能夠辦到,呵呵,實在不行就請關注我們的微信號留言吧!

如果您喜歡本文,您可以點擊一下下面的喜歡按鈕,您也可以關注我,謝謝您的支持!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容