本系列文章為作者原創(chuàng),未經(jīng)作者書面同意,不得轉(zhuǎn)載!
首先聲明一下:本文將要制作的Tello無(wú)人機(jī)遙控器是基于睿熾科技官網(wǎng)公開的Tello SDK,網(wǎng)址鏈接,Tello無(wú)人機(jī)是一款教育編程無(wú)人機(jī),用戶可以根據(jù)睿熾科技公開的SDK編程控制無(wú)人機(jī),為了讓讀者更好的理解程序和基于本文能夠自己動(dòng)手自做一個(gè)遙控器,本文會(huì)對(duì)睿熾科技的SDK做一些必要的引用以對(duì)開源的程序做一些解釋,如涉及版權(quán)問題,請(qǐng)第一時(shí)間聯(lián)系作者,謝謝!
Tello無(wú)人機(jī)是大疆跟睿熾科技合作開發(fā)的一款教育編程無(wú)人機(jī),針對(duì)STEAM(科學(xué)、技術(shù)、工程、藝術(shù)、數(shù)學(xué))教育場(chǎng)景及需求。

Tello支持Scratch、Python等語(yǔ)言進(jìn)行編程控制,兒子最近在搗鼓Scratch編程,于是這個(gè)無(wú)人機(jī)變成了他六一兒童節(jié)禮物。


這個(gè)無(wú)人機(jī)其實(shí)非常小巧,室內(nèi)都能飛行,給小孩玩是非常不錯(cuò)的。但是無(wú)人機(jī)并沒有附帶遙控手柄,而是在官網(wǎng)提供了遙控APP,安裝到手機(jī)上,通過WiFi連接上無(wú)人機(jī)后進(jìn)行控制(當(dāng)然也可以通過Scratch編程進(jìn)行控制,這部分內(nèi)容我會(huì)在另一個(gè)系列《Scratch邊玩邊學(xué):從動(dòng)畫、游戲到算法入門》中介紹)。
其實(shí)手柄是有的,不過要單獨(dú)購(gòu)買:

多少錢?忘了,反正太貴(你有沒有發(fā)現(xiàn),隨著學(xué)習(xí)Arduino的深入,你會(huì)發(fā)現(xiàn)市面上的電子產(chǎn)品給人的感覺越來(lái)越貴了,呵呵,開個(gè)玩笑?。?,既然覺得貴,那就自己做一個(gè)吧!Tello本身就是一款教育編程機(jī)器人,手柄貴,不就是要讓我們自己動(dòng)手來(lái)做一個(gè)嗎?你說(shuō)是不是?
好吧,今天我們要做的項(xiàng)目就是Tello無(wú)人機(jī)遙控手柄,通過遙控手柄實(shí)現(xiàn)Tello無(wú)人機(jī)起飛、降落,前后左右飛行以及上升下降。
在開始之前先介紹一下Tello無(wú)人機(jī)支持的無(wú)線連接方式,Tello無(wú)人機(jī)是基于WiFi UDP協(xié)議跟控制器(遙控手柄、電腦、手機(jī)APP)實(shí)現(xiàn)連接的,所以我們需要準(zhǔn)備的組件就要包括一個(gè)WiFi模塊。
1 本章您將學(xué)到
在這個(gè)項(xiàng)目中,您將學(xué)到的:
- 學(xué)會(huì)基于第三方SDK文檔進(jìn)行簡(jiǎn)單項(xiàng)目開發(fā)
- 通過ESP8266模塊實(shí)現(xiàn)UDP消息透?jìng)?/li>
- JoyStick搖桿擴(kuò)展板的使用
2 工具和組件
2.1 工具列表
本項(xiàng)目不需要額外的工具。
2.2 元器件列表
| 元器件 | 型號(hào) | 數(shù)量 | 備注 |
|---|---|---|---|
| 主控板 | arduino Uno | 1 | |
| JoyStick搖桿擴(kuò)展板 | 1 | ||
| ESP8266 | 12N | 1 | |
| 杜邦線 | 4 | ||
| 數(shù)據(jù)線 | Uno數(shù)據(jù)線 | 1 |
2.3 工具和元器件介紹
2.3.1 JoyStick搖桿擴(kuò)展板
JoyStick Shield游戲搖桿擴(kuò)展板是我們?cè)陧?xiàng)目中第一次使用,我們簡(jiǎn)單介紹一下:

這個(gè)擴(kuò)展板是我偶然發(fā)現(xiàn)的,原先的設(shè)計(jì)是通過一個(gè)搖桿+4個(gè)按鍵進(jìn)行設(shè)計(jì),搖桿都買好了:

在購(gòu)買3D打印機(jī)主控的時(shí)候偶然發(fā)現(xiàn)了JoyStick Shield,這個(gè)太好了,省去了搭建電路的麻煩,爽!其實(shí)Arduino最吸引人的地方就是它的外圍模塊太豐富了,只有你想不到,呵呵!言歸正傳,我們還是來(lái)介紹這個(gè)擴(kuò)展板吧!
Joystick Shield還添加了nRF24L01的RF接口和Nokia5110 LCD接口,這樣非常方便二次的游戲開發(fā)。
2.3.1.1 技術(shù)參數(shù)
這個(gè)似乎沒什么可介紹的,這個(gè)擴(kuò)展板其實(shí)就是一個(gè)遙桿+六個(gè)按鍵,注意中間部位還有兩個(gè)小按鍵,按鍵我們?cè)诒鞠盗形恼轮熬陀薪榻B,記得是交通燈那一篇,有不清楚的可以去翻翻那一篇文章看看。
另外,擴(kuò)展板上還有一個(gè)開關(guān)可以在3.3V 和5V 之間切換,可以將此模塊用于其它3.3V單片機(jī)平臺(tái),比如STM32,由于Arduino UNO支持5V和3.3V供電,所以這個(gè)開關(guān)在UNO上似乎沒有什么意義,經(jīng)測(cè)試也的確沒什么用。
2.3.1.2 搖桿的原理
前面介紹過JoyStick Shield游戲搖桿擴(kuò)展板就是一個(gè)雙軸按鍵搖桿+6個(gè)按鍵,按鍵我們都清楚了,那這個(gè)雙軸按鍵搖桿是個(gè)什么東東呢?其實(shí)它也很簡(jiǎn)單,就是兩個(gè)電位計(jì)+一個(gè)按鍵。
那現(xiàn)在我們應(yīng)該很清楚了,JoyStick Shield游戲搖桿擴(kuò)展板就是7個(gè)按鍵+兩個(gè)電位計(jì)。
我們知道了這個(gè)擴(kuò)展板的組成,但是你也許還有疑惑,我們?cè)趺茨軌蛑罁u桿到底朝那個(gè)方向搖動(dòng)呢?其實(shí)就是通過模擬輸入口讀取兩個(gè)電位計(jì)的電壓值,搖桿朝不同的方向搖動(dòng)會(huì)導(dǎo)致這兩個(gè)值發(fā)生變化,根據(jù)這個(gè)變化,我們就能判斷搖桿的方向。感興趣的朋友可以自己測(cè)試一下。
2.3.1.3 跟UNO的連接
JoyStick Shield游戲搖桿擴(kuò)展板在實(shí)際使用時(shí)直接插在UNO電路板上即可,不過我們還是需要了解一下它跟UNO的實(shí)際連接。
前面說(shuō)過,JoyStick Shield游戲搖桿擴(kuò)展板就是7個(gè)按鍵+兩個(gè)電位計(jì),如果你需要使用全部的7個(gè)按鍵,那么就需要7個(gè)數(shù)字輸入引腳+2個(gè)模擬輸入引腳,另外還需要連接5V和GND,7個(gè)數(shù)字引腳用的是(2、3、4、5、6、7、8),擴(kuò)展板提供了從9到13數(shù)字引腳的接口,可以直接使用。
兩個(gè)模擬口可以自定義,A0到A5都可以,后面我們?cè)诮榻BJoyStickShield擴(kuò)展庫(kù)的時(shí)候會(huì)再介紹。
2.3.2 ESP-12F WiFi模塊
我們重點(diǎn)介紹一下這個(gè)模塊。
ESP-12F是一款超低功耗的UART-WiFi 透?jìng)髂K,專為移動(dòng)設(shè)備和物聯(lián)網(wǎng)應(yīng)用設(shè)計(jì),可將用戶的物理設(shè)備連接到Wi-Fi 無(wú)線網(wǎng)絡(luò)上,進(jìn)行互聯(lián)網(wǎng)或局域網(wǎng)通信,實(shí)現(xiàn)聯(lián)網(wǎng)功能。

這個(gè)模塊使用之前需要焊接到轉(zhuǎn)接板上,下圖是轉(zhuǎn)接板:

下面兩張圖是焊接完成后的樣子:


ESP-12F模塊引腳間距是2mm的,焊接起來(lái)比較費(fèi)勁。本來(lái)想采用ESP-01模塊的,這個(gè)模塊不需要焊接,有引腳直接可以用,不過ESP-01模塊對(duì)供電要求比較高,而且Flash才8Mbit,可用引腳也比較少,可玩性跟12F差太多,所以就不推薦大家使用了,不過如果是做一個(gè)實(shí)際項(xiàng)目,有成本控制且只做無(wú)線透?jìng)?,ESP-01就相對(duì)合適一些(其實(shí)ESP8266模塊本身就是一個(gè)MCU,跟Arduino的主控板一樣,也能在Arduino IDE下編程)。
2.3.2.1 產(chǎn)品特性
- 支持無(wú)線802.11 b/g/n 標(biāo)準(zhǔn)
- 支持STA/AP/STA+AP 三種工作模式
- 內(nèi)置TCP/IP協(xié)議棧,支持多路TCP Client連接
- 支持豐富的Socket AT指令
- 支持UART/GPIO數(shù)據(jù)通信接口
- 支持Smart Link 智能聯(lián)網(wǎng)功能
- 支持遠(yuǎn)程固件升級(jí)(OTA)
- 內(nèi)置32位MCU,可兼作應(yīng)用處理器
- 超低能耗,適合電池供電應(yīng)用
- 3.3V 單電源供電
注意:最后一條,3.3V供電,建議由電池組或者電源模塊單獨(dú)供電,用一個(gè)降壓模塊,直接用UNO的3.3V供電很不穩(wěn)定。
2.3.2.2 模塊使用方法
這部分內(nèi)容比較關(guān)鍵,ESP8266系列模塊在使用前都需要進(jìn)行調(diào)試和模式的設(shè)定,包括工作模式和串口通訊速率,如果模塊燒錄了非AT固件,還需要重新對(duì)模塊進(jìn)行燒錄,好在如果你是新買的模塊,或者買回來(lái)后沒有對(duì)其進(jìn)行過其它固件燒錄,那么就沒有燒錄的必要,它出廠就默認(rèn)燒錄好了AT固件。
那么我們只需要設(shè)置一下它的串口通信速率即可,ESP8266模塊默認(rèn)的串口通信速率是:115200,這個(gè)速率對(duì)于UNO主控板的軟串口來(lái)說(shuō)太高了,不穩(wěn)定,所以我們需要將其設(shè)定為:9600。
設(shè)定方法:通過串口模塊跟ESP8266連接上電腦后,通過串口指令進(jìn)行設(shè)定,指令如下:
AT+UART_DEF=9600,8,1,0,0
這部分內(nèi)容還待詳細(xì)整理后再發(fā)布出來(lái)...
3 電路設(shè)計(jì)
3.1 電路圖
根據(jù)我們的項(xiàng)目需求,設(shè)計(jì)電路圖如下:

3.2 電路原理
這個(gè)電路圖其實(shí)比較簡(jiǎn)單,JoyStick Shield游戲搖桿擴(kuò)展板直接安裝到UNO板上,數(shù)字9、10口作為軟串口的RX、TX引腳跟ESP8266-12N連接,圖中ESP8266-12N模塊由UNO直接供電,VCC接的是3.3V,但在實(shí)際項(xiàng)目中,采用的是單獨(dú)供電,單獨(dú)供電的時(shí)候,ESP8266-12N需要和UNO共地。
4 程序設(shè)計(jì)
4.1 類庫(kù)介紹
這個(gè)項(xiàng)目用的庫(kù)比較多,有四個(gè),我們分別介紹一下:
這里有必要說(shuō)明一下:本系列文章在開篇就介紹過,我希望能照顧到不同的讀者,所以對(duì)于初學(xué)者,關(guān)于程序中引用的庫(kù),你只需要知道怎么使用,用到庫(kù)中的那幾個(gè)方法,這幾個(gè)方法是做什么的,我覺得就OK了,剛開始沒有必要鉆入一個(gè)過細(xì)的小問題而影響了自己的學(xué)習(xí),畢竟有些問題還是需要一定的背景知識(shí),有些甚至是大學(xué)階段才能接觸到的,所以不必操之過急,知道怎么使用,然后能夠基于這些東西創(chuàng)造屬于自己的作品,實(shí)現(xiàn)自己的設(shè)計(jì)才是最重要的,那些小小的牛角尖,相信我,隨著你學(xué)習(xí)的進(jìn)步,它們會(huì)迎刃而解的!
但是對(duì)于那些有一定基礎(chǔ),希望對(duì)Arduino了解更深入一些的同學(xué),你可以對(duì)這部分的內(nèi)容做一個(gè)全面的學(xué)習(xí)、理解。
4.1.1 JoystickShield.h庫(kù)介紹
4.1.1.1 JoystickShield.h庫(kù)的下載
JoystickShield.h庫(kù)下載地址:百度網(wǎng)盤鏈接。
下載解壓縮后,直接放到Arduino項(xiàng)目文件夾(一般在:我的電腦 \ 文檔 \ Arduino \)中的libraries子目錄中。
4.1.1.1 JoystickShield.h庫(kù)的介紹
我們來(lái)看一下這個(gè)庫(kù)的頭文件,
#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
庫(kù)的說(shuō)明待補(bǔ)充...
4.1.2 WiFiEsp.h庫(kù)介紹
4.1.2.1 WiFiEsp.h庫(kù)的下載
WiFiEsp.h庫(kù)下載地址:百度網(wǎng)盤鏈接。
下載解壓縮后,直接放到Arduino項(xiàng)目文件夾(一般在:我的電腦 \ 文檔 \ Arduino \)中的libraries子目錄中。
這個(gè)庫(kù)其實(shí)包含好幾個(gè)庫(kù):WiFiEsp.h、WiFiEspClient.h、WiFiEspServer.h、WiFiEspUdp.h,這個(gè)是一個(gè)非常優(yōu)秀的ESP8266 AT指令封裝庫(kù),在本文中會(huì)用到兩個(gè)庫(kù):WiFiEsp.h、WiFiEspUdp.h,我們簡(jiǎn)單了解一下,其它兩個(gè)我們?cè)趧e的文章中還會(huì)繼續(xù)介紹。
4.1.2.1 WiFiEsp.h庫(kù)的介紹
看一下這個(gè)庫(kù)的頭文件:
#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
庫(kù)的說(shuō)明待補(bǔ)充...
4.1.2.2 WiFiEspUdp.h庫(kù)的介紹
看一下這個(gè)庫(kù)的頭文件:
#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
庫(kù)的說(shuō)明待補(bǔ)充...
4.1.3 SoftwareSerial.h庫(kù)介紹
4.1.3.1 SoftwareSerial.h庫(kù)的下載
SoftwareSerial.h庫(kù)為Arduino的自帶核心庫(kù),無(wú)需下載,可直接引用。
4.1.3.1 SoftwareSerial.h庫(kù)的介紹
我們來(lái)看一下這個(gè)庫(kù)的頭文件:
#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
這個(gè)庫(kù),我們先看一下它的繼承關(guān)系:
class SoftwareSerial : public Stream
從這個(gè)頭文件可以看到SoftwareSerial 類繼承自類Stream,而Stream是繼承的Print類,Print類的作用是打印數(shù)據(jù),通過不同的設(shè)備(串口、LCD1602,還是其它TFT的彩色屏幕)打印的過程都是一樣的,只是最底層實(shí)現(xiàn)不一樣,感興趣的朋友可以去看看一些顯示屏的驅(qū)動(dòng)庫(kù),基本上都會(huì)包含Print這個(gè)類。
我們簡(jiǎn)單的看一下這個(gè)頭文件的public部分的兩個(gè)方法(函數(shù)):
SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic = false);
這個(gè)是構(gòu)造函數(shù),有三個(gè)參數(shù):receivePin、transmitPin和inverse_logic,前兩個(gè)參數(shù)就是我們定義軟串口的RX和TX引腳,第三個(gè)參數(shù)inverse_logic有缺省值false,在定義軟串口時(shí)可以不帶,這個(gè)參數(shù)的作用是:在初始化軟串口時(shí)對(duì)RX和TX引腳是否拉高。
void begin(long speed);
這個(gè)函數(shù)作用是設(shè)置串口傳送波特率,軟串口波特率我們一般采用9600,這個(gè)波特率需要跟與串口通信的設(shè)備或者模塊保持一致。
4.1.4 IPAddress.h庫(kù)介紹
4.1.4.1 IPAddress.h庫(kù)的下載
IPAddress.h庫(kù)為Arduino的自帶核心庫(kù),無(wú)需下載,可直接引用。
4.1.4.1 IPAddress.h庫(kù)的介紹
我們來(lái)看一下這個(gè)庫(kù)的頭文件:
/*
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
這個(gè)類就是定義一個(gè)IP地址對(duì)象,說(shuō)實(shí)話,直到開始寫這部分內(nèi)容時(shí),我才意識(shí)到這里弄復(fù)雜了,其實(shí)IP地址可以用一個(gè)字符串定義,就像下面這樣:
const char *telloAddr= "192,168,10,1";
為什么可以這么做呢?
我們可以看一下這個(gè)IP地址對(duì)象在哪兒使用了(你可以先看一下主程序,找到這行代碼):
Udp.beginPacket(telloAddr, telloPort);
這行代碼的作用就是對(duì)Udp對(duì)象進(jìn)行初始化,這里會(huì)用到一個(gè)IP地址對(duì)象tellAddr和端口號(hào)telloPort,程序的開始都有定義。
但實(shí)際上beginPacket這個(gè)方法在WiFiEspUDP對(duì)象中是重載的,它定義了兩個(gè)beginPacket方法,如下:
virtual int beginPacket(IPAddress ip, uint16_t port);
virtual int beginPacket(const char *host, uint16_t port);
所以beginPacket方法的第一個(gè)參數(shù)可以是一個(gè)IPAddress對(duì)象,也可以是一個(gè)字符數(shù)組指針變量。
這樣你就很清楚了,其實(shí)我們的主程序可以更加簡(jiǎn)化的,不過咱們是為了學(xué)習(xí)而來(lái),弄懂程序背后的意義才是重點(diǎn)。
關(guān)于UDP協(xié)議:UDP是一個(gè)傳輸層協(xié)議,與之對(duì)應(yīng)的還有TCP協(xié)議,它們都工作在IP協(xié)議上,它們之間區(qū)別就是TCP是面向連接的,而UDP不是,可能有的朋友還是不理解這一點(diǎn),我簡(jiǎn)單的舉個(gè)例子說(shuō)明一下:
假設(shè)某個(gè)周末的下午,你到小區(qū)的院子里跟小朋友玩耍,你媽媽忙著做晚飯,不一會(huì)兒,媽媽的晚飯做好了,而你呢?玩得正嗨,忘了回家的時(shí)間。好了,你媽媽需要叫你回家吃飯了,現(xiàn)在你媽媽有兩種做法,一種是按照TCP的模式,一種是UDP的模式,假設(shè)你家的陽(yáng)臺(tái)正對(duì)著小區(qū)院子,陽(yáng)臺(tái)到院子之間可以通過聲音交流(類似IP協(xié)議提供的服務(wù)),你的小名叫:阿福(類似IP地址),我們來(lái)看看這兩種模式的區(qū)別:
TCP模式:
你媽媽在陽(yáng)臺(tái)對(duì)著院子大聲的喊:“阿福、阿福!”
你聽到了,趕緊回答:“媽媽,媽媽,干嘛!”
你媽媽又說(shuō):“回家吃飯了!”
你回答:“好的,馬上就回來(lái)!”
你媽媽聽到后,知道你一會(huì)兒就回來(lái)吃飯,然后開始去忙別的了。UDP模式:
你的媽媽來(lái)到陽(yáng)臺(tái),對(duì)著院子大喊一聲:“阿福,回家吃飯了!”
你的媽媽覺得你肯定能夠聽到,反正回家吃飯這件事也沒什么大不了,媽媽認(rèn)為你一會(huì)兒就會(huì)回家吃飯,然后她就忙別的去了。
你呢?你可能聽到了,也可能沒聽到,小朋友在一起玩耍時(shí)本身就是吵吵鬧鬧的,當(dāng)你聽到了,你肯定就會(huì)回家吃飯,這種情況發(fā)生的概率很大,畢竟小區(qū)院子就正對(duì)著你家陽(yáng)臺(tái),你媽媽的聲音也夠響亮。如果萬(wàn)一沒聽到呢?沒關(guān)系,你媽媽隔一會(huì)發(fā)現(xiàn)你還沒回家,又會(huì)跑到陽(yáng)臺(tái),再喊一聲:“阿福,回家吃飯了!”
現(xiàn)在你能理解這兩種通信方式的區(qū)別了嗎?
4.2 Tello SDK介紹
這部分內(nèi)容主要是對(duì)Tello SDK文檔做一個(gè)簡(jiǎn)單的介紹。
4.2.1 WiFi連接
Tello無(wú)人機(jī)IP地址:192.168.10.1;
Tello無(wú)人機(jī)UDP監(jiān)聽端口:8889。
4.2.2 命令參數(shù)
| 命令 | 功能描述 | 可能的響應(yīng) |
|---|---|---|
| command | 進(jìn)入命令模式 | OK 或者 FALSE |
| takeoff | 自動(dòng)起飛 | OK 或者 FALSE |
| land | 自動(dòng)降落 | 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 |
注意:命令參數(shù)的單位為:距離是厘米、角度是度、速度為厘米/秒。
關(guān)于SDK暫時(shí)就介紹這些指令,這也是我們?cè)诤竺娉绦蛑行枰玫降?,?dāng)然,官方給出的SDK文檔還有更多的指令,感興趣的朋友可以到官網(wǎng)下載。
4.3 主程序設(shè)計(jì)
/********************************
Name: Tello無(wú)人機(jī)遙控器
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,這個(gè)需要根據(jù)無(wú)人機(jī)的實(shí)際值進(jìn)行修改,啟動(dòng)Tello無(wú)人機(jī)后,用電腦掃描一下WiFi網(wǎng)絡(luò),以TELLO開頭的熱點(diǎn)即是
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服務(wù)端的IP地址
const int telloPort = 8889; //Tello UDP監(jiān)聽端口號(hào)
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進(jìn)入命令模式
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無(wú)人機(jī)消息后的處理
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);
}
主程序就不單獨(dú)解釋了,程序中的注釋已經(jīng)非常清楚了!
5 安裝調(diào)試
下面我們根據(jù)電路圖將兩個(gè)模塊跟UNO連接上:

將Tello無(wú)人機(jī)開機(jī),打開電腦串口,觀察一下遙控器是否跟Tello連接上,連接上后,串口會(huì)有WiFi狀態(tài)打印。
如果連接成功,您就可以通過遙控手柄控制Tello無(wú)人機(jī)的起飛、降落,上升、下降,前后左右飛行了。
5 總結(jié)擴(kuò)展
因?yàn)闀r(shí)間的關(guān)系,我并沒有將這個(gè)手柄做得更加完善,只是搭建了一個(gè)原型,您可以根據(jù)這個(gè)原型來(lái)自己設(shè)計(jì)一個(gè)更加完善的遙控手柄,增加外殼,用電池進(jìn)行供電,甚至增加一個(gè)小的液晶屏,直接來(lái)顯示連接狀態(tài)和命令發(fā)送的相關(guān)信息。
另外這個(gè)手柄上還有兩個(gè)小的按鈕,我的想法是您可以增加兩個(gè)自定義飛行動(dòng)作系列,讓無(wú)人機(jī)能夠表演一連串的復(fù)雜動(dòng)作,當(dāng)然,程序您需要再修改一下,怎么修改?我相信您肯定能夠辦到,呵呵,實(shí)在不行就請(qǐng)關(guān)注我們的微信號(hào)留言吧!
如果您喜歡本文,您可以點(diǎn)擊一下下面的喜歡按鈕,您也可以關(guān)注我,謝謝您的支持!