Telloのバイナリ通信プロトコル(初歩編)
概要
Telloと直接通信して非公開のバイナリコードを送って色々やる方法の解説です。ネットに公開されている情報をかき集めてアプリとして仕立てました。秘匿するのもなんなので情報公開しておきます。
バグもあると思いますので鵜呑みにしないようにしてください。通信手順もイマイチよく分かってない部分も多いです。
ソケット通信とか書くの20年ぶりかも。TCP/IPのソケット通信はC++で書いたことありますがUDP通信は初めて書きました。
適宜突っ込んでください。
想定読者
なんらかのプログラミング言語で通信系のなにかを書いたことがある人。Telloを持ってる物好きプログラマー。
Telloに関する情報
UDP通信で誰とでもつながります
TelloはWi-Fi接続によるUDPソケット通信でコントロールするドローンなので、色んなネットワーク機器と接続可能です。
TelloのWi-Fiに接続して『IP: 192.168.10.1 UDP PORT: 8889』に接続し、適切なコマンドを送れば返答が返ってきます。
C#のサンプルがあります。これでWindowsでもMacでも、VisualStudioが動く環境ならTelloと接続してコントロールできます。
Scratchは?
公式のSDKはScratch対応ですが、こちらで出来ることは今のところ限られています。
離陸、直陸、上下左右前後のcm指定での移動、旋回、フリップ(宙返り)のみです。速度や飛行時間は取れるけど高度や方位、姿勢までは取れません(この辺は未解析ですが、データストリームを見てると流れてきてるっぽい)。
公式SDKではカメラ映像も取れません。
映像についてはまだ未着手ですが、Go言語でやってる方がいました。Javaでもなんとかなるでしょう。
以上を踏まえてAndroidアプリを作りました
ということで(飛躍)、Androidスマホで動くアプリをJavaで作ってみました。
送ってるコマンドはバイナリコードで、下記のPythonに載ってた一覧表を参考にしています。
通信手順
1.DatagramSocketを作って、コネクト(UDPってコネクトとかないんじゃなかったの?)。
2.別スレッドでreceive。タイムアウト無しの無限ループで待つ。
3.『conn_req:(ビデオポート番号?)』を送る。
4.『conn_ack:(ビデオポート番号?)』が返ってきたらコネクト成功。接続に成功すると色んなデータがreceiveスレッドに飛んできます。
5.適切なフォーマットでパケットを作って送る。レスポンスはreceiveスレッドに来るのでよしなに処理する。CRCチェックはエラーになりがち(こっちのCRC計算のバグかも)。
6.アプリがスリープしたら切断。
大雑把に言えばこんな感じです。
まずconn_reqとconn_ackのやり取りをしないと、コマンドを送ってもなにも起こりません。
↓通信用に作ったクラス。エラー処理は雑。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
public class SocketUDP {
private static final String TAG = "SocketUDP";
interface IReceiveCallBack {
public void receiveCallback(DatagramPacket receivePacket);
}
private IReceiveCallBack _receiveCallback = null;
private DatagramSocket _client = null;
private String _address;
private int _port;
public SocketUDP(String address,int port,IReceiveCallBack receiveCallback) {
_address = address;
_port = port;
_receiveCallback = receiveCallback;
try {
_client = new DatagramSocket();
}
catch (SocketException e) {
_client = null;
}
}
public void connect() {
if(_client == null) return;
InetSocketAddress remoteAddress = new InetSocketAddress(_address, _port);
try {
_client.connect(remoteAddress);
}
catch (IOException e) {
e.printStackTrace();
}
}
public void close() {
if(_client != null) {
_client.close();
}
}
//受信
public void receive() {
if(_client != null) {
new Thread(new Runnable() {
@Override
public void run() {
byte[] receiveBuffer = new byte[256];
while(true) {
try {
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
_client.receive(receivePacket);
if(_receiveCallback != null) {
_receiveCallback.receiveCallback(receivePacket);
}
//System.out.println("receive Tello hex:" + TelloPacket.packetToHexString(receivePacket));
} catch (SocketException e) {
// e.printStackTrace();
} catch (IOException e) {
// e.printStackTrace();
}
}
}
}).start();
}
}
// 送信
public boolean send(byte[] sendBuffer) {
if(_client == null) return false;
try {
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length);
_client.send(sendPacket);
return true;
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
receive()の部分はTelloから色んな情報が常に送られてきます。おそらく姿勢とか高度とか速度とかが入ってると思いますが未解析です。
Telloは電源を入れて置いておくだけで結構熱くなりますが、こんだけ通信してたらそりゃ熱くもなりますわ、ってくらいデータが来ます。
MainActivityで通信の初期化をしてる部分。
//初期化
_client = new SocketUDP(Tello.TELLO_ADDRESS,Tello.TELLO_PORT_SEND,this);
if(_client != null) {
new Thread(new Runnable() {
@Override
public void run() {
_client.connect();
_client.receive();
sendTello(TelloPacket.createConnectReqPacket());
}
}).start();
}
else {
UIUtility.showMsg(this,R.string.error,getString(R.string.not_net),null);
}
↓パケットのフォーマット
例えば離着陸のパケットはこんな感じ。
public static byte[] takeOffPacket = new byte[] { (byte)0xcc, (byte)0x58, (byte)0x00, (byte)0x7c, (byte)0x68, (byte)0x54, (byte)0x00, (byte)0xe4, (byte)0x01, (byte)0xc2, (byte)0x16 };
public static byte[] landPacket = new byte[] { (byte)0xcc ,(byte)0x60, (byte)0x00, (byte)0x27, (byte)0x68, (byte)0x55, (byte)0x00, (byte)0xe5, (byte)0x01, (byte)0x00, (byte)0xba, (byte)0xc7 };
MainActivityでボタンを押したら離着陸するコードはこんな感じ。
//離陸
public void onClickTakeoff(View v) {
sendTello(TelloPacket.takeOffPacket);
}
//着陸
public void onClickLand(View v) {
sendTello(TelloPacket.landPacket);
}
//データ送信
public boolean sendTello(byte[] bytes) {
if(_client != null) {
return _client.send(bytes);
}
return false;
}
_clientはSocketUDPのインスタンスです。
高度制限解除のパケットを作るとこはこんな感じ。byteでやりゃいいところとshortにしてるのは、byteだと128以上の数値を入れるとエラーになるからなんですが、Javaってこういう感じなんでしたっけ?
public static byte[] createSetAltitudePacket(int altitude)
{
//template
short[] packet = new short[] { 0xcc, 0x00, 0x00, 0x00, 0x68, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// start size crc type command seq data crc 13byte
int len = packet.length;
packet[ 1] = (short)(len << 3);//サイズは3ビット左 0x0d->0x68
packet[ 9] = (short)(altitude & 0xff);
packet[10] = (short)((altitude >> 8) & 0xff);
calcUCRC(packet, 4);
//calc crc for packet.
calcCrc(packet, packet.length);
return shortArrayToByteArray(packet);
}
CRCの計算はTelloPCのコードを移植しました。C#なのでコピペしてちょっと直せば動きます。
高度制限値の取得コマンドは、パケットタイプ0x48、コマンド0x1056です。データ部分は無しで11バイトのバイナリデータを作って送ると設定値が同様のフォーマットのパケットで帰ってきます。
MainActivityから高度制限値を送信する部分はこんな感じ。20m以上の場合はアラートを出すようにしてます(でもってコードは汚い)。
//高度設定送信
public void onClickSendButton(View v) {
SeekBar seekBarHeight = (SeekBar)findViewById(R.id.seekBarHeight);
final int alt = seekBarHeight.getProgress() + 1;
if(alt >= 20) {
UIUtility.showYesNoDialog(this, getString(R.string.confirm), getString(R.string.not_recommend),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
_isSettingAlt = true;
sendTello(TelloPacket.createSetAltitudePacket(alt));//変更
sendTello(TelloPacket.build11Packet((short)0x48,0x1056));//変更値をリクエスト
}
},
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
}
else {
_isSettingAlt = true;
sendTello(TelloPacket.createSetAltitudePacket(alt));//変更
sendTello(TelloPacket.build11Packet((short)0x48,0x1056));//変更値をリクエスト
}
}
UIUtilityってのはオリジナルの楽ちんクラスライブラリです。単に変更依頼と結果取得なら3行。
_isSettingAlt = true;
sendTello(TelloPacket.createSetAltitudePacket(alt));//変更
sendTello(TelloPacket.build11Packet((short)0x48,0x1056));//変更値をリクエスト
パケットのパースはこんな感じ。扱いやすいようにTelloPacketってクラスに変換します。
//パースしてTelloPacketにする
public static TelloPacket parsePacket(byte[] bytes) {
int start = (int)bytes[0] & 0xff;
if( bytes.length < 11) return null;
if(start != 0xcc) return null;
TelloPacket retPacket = new TelloPacket();
retPacket._size = LittleEndianToIntWithShift(bytes[1],bytes[2]);
retPacket._crc8 = (int)bytes[3] & 0xff;
retPacket._typeID = (int)bytes[4] & 0xff;
retPacket._commandID = LittleEndianToInt(bytes[5],bytes[6]);
retPacket._seqNo = LittleEndianToInt(bytes[7],bytes[8]);
int dataSize = retPacket._size - 11;
if(dataSize > 0) {
retPacket._data = new byte[dataSize];
for(int i = 0; i < dataSize; i++) {
retPacket._data[i] = bytes[i + 9];
}
}
retPacket._crc16 = LittleEndianToInt(bytes[( bytes.length - 2)],bytes[( bytes.length - 1)]);
short[] shortArray = byteArrayToShortArray(bytes);
int crc8Check = calcUCRCBToInt(shortArray, 4);
int crc16Check = calcCrcToInt(shortArray, bytes.length);
if(crc8Check != retPacket._crc8 || crc16Check != retPacket._crc16) {
retPacket._isCheckCRC = false;
}
else {
retPacket._isCheckCRC = true;
}
return retPacket;
}
CRCのチェックをしてますが、結構CRCエラーになります。こちらのCRCの計算部分にバグがあるのかも知れません。今はCRCエラーは無視して値を使ってしまってます(^^;。細かい部分はあとで。
ライフサイクル処理
onPauseとonResumeで切断と初期化をしています。じゃないとバックグラウンドでも通信しっぱなしになって他のTelloアプリが接続できなくなります。
@Override
public void onPause() {
super.onPause();
if(_client != null) {
_client.close();
_client = null;
}
_isConnect = false;
}
@Override
public void onResume() {
super.onResume();
if(!General.isConnectNetWork(getApplicationContext())) {
UIUtility.showErrorAlertMsg(this,getString(R.string.not_net),null);
_client = null;
}
else {
//初期化
_client = new SocketUDP(Tello.TELLO_ADDRESS,Tello.TELLO_PORT_SEND,this);
if(_client != null) {
new Thread(new Runnable() {
@Override
public void run() {
_client.connect();
_client.receive();
sendTello(TelloPacket.createConnectReqPacket());
}
}).start();
}
else {
UIUtility.showMsg(this,R.string.error,getString(R.string.not_net),null);
}
}
}
途中に出てくる『General.isConnectNetWork』はこんな感じの関数です。
// ネットワーク接続確認
public static boolean isConnectNetWork(Context context){
ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if(networkInfo != null){
return networkInfo.isConnected();
}
else {
return false;
}
}
通信が繋がってないとソケットの初期化で落ちるのでチェックを入れました。
他のコード
まだ空っぽのTelloクラス。多分、今後色々増えていきます。
public class Tello {
public static String TELLO_ADDRESS = "192.168.10.1";
public static int TELLO_PORT_SEND = 8889;
public static byte[] shortArrayToByteArray(short[] shortArray) {
byte[] ret = new byte[shortArray.length];
for(int i = 0; i < shortArray.length; i++) {
ret[i] = (byte)shortArray[i];
}
return ret;
}
}
Telloのコントロールパケットを作ったり解析したりするクラス。CRCの計算部分はTelloPCのコードをほぼ流用してます。作者さんありがとう。
import java.net.DatagramPacket;
public class TelloPacket {
int _size = 0;
int _crc8 = 0;
int _typeID = 0;
int _commandID = 0;
int _seqNo = 0;
byte[] _data = null;
int _crc16 = 0;
boolean _isCheckCRC = false;
//Utils to calculate packet CRCs
//FCS crc
public static int poly = 13970;
public static int[] fcstab = { 0, 4489, 8978, 12955, 17956, 22445, 25910, 29887, 35912, 40385, 44890, 48851, 51820, 56293, 59774, 63735, 4225, 264, 13203, 8730, 22181, 18220, 30135, 25662, 40137, 36160, 49115, 44626, 56045, 52068, 63999, 59510, 8450, 12427, 528, 5017, 26406, 30383, 17460, 21949, 44362, 48323, 36440, 40913, 60270, 64231, 51324, 55797, 12675, 8202, 4753, 792, 30631, 26158, 21685, 17724, 48587, 44098, 40665, 36688, 64495, 60006, 55549, 51572, 16900, 21389, 24854, 28831, 1056, 5545, 10034, 14011, 52812, 57285, 60766, 64727, 34920, 39393, 43898, 47859, 21125, 17164, 29079, 24606, 5281, 1320, 14259, 9786, 57037, 53060, 64991, 60502, 39145, 35168, 48123, 43634, 25350, 29327, 16404, 20893, 9506, 13483, 1584, 6073, 61262, 65223, 52316, 56789, 43370, 47331, 35448, 39921, 29575, 25102, 20629, 16668, 13731, 9258, 5809, 1848, 65487, 60998, 56541, 52564, 47595, 43106, 39673, 35696, 33800, 38273, 42778, 46739, 49708, 54181, 57662, 61623, 2112, 6601, 11090, 15067, 20068, 24557, 28022, 31999, 38025, 34048, 47003, 42514, 53933, 49956, 61887, 57398, 6337, 2376, 15315, 10842, 24293, 20332, 32247, 27774, 42250, 46211, 34328, 38801, 58158, 62119, 49212, 53685, 10562, 14539, 2640, 7129, 28518, 32495, 19572, 24061, 46475, 41986, 38553, 34576, 62383, 57894, 53437, 49460, 14787, 10314, 6865, 2904, 32743, 28270, 23797, 19836, 50700, 55173, 58654, 62615, 32808, 37281, 41786, 45747, 19012, 23501, 26966, 30943, 3168, 7657, 12146, 16123, 54925, 50948, 62879, 58390, 37033, 33056, 46011, 41522, 23237, 19276, 31191, 26718, 7393, 3432, 16371, 11898, 59150, 63111, 50204, 54677, 41258, 45219, 33336, 37809, 27462, 31439, 18516, 23005, 11618, 15595, 3696, 8185, 63375, 58886, 54429, 50452, 45483, 40994, 37561, 33584, 31687, 27214, 22741, 18780, 15843, 11370, 7921, 3960 };
public static int fsc16(short[] bytes, int len, int poly) {
if (bytes == null) {
return 65535;
}
int i = 0;
int j = poly;
poly = len;
len = j;
while (true) {
j = len;
if (poly == 0)
{
break;
}
j = bytes[i];
len = fcstab[((len ^ j) & 0xFF)] ^ len >> 8;
i += 1;
poly -= 1;
}
return j;
}
public static byte[] shortArrayToByteArray(short[] shortArray) {
byte[] ret = new byte[shortArray.length];
for(int i = 0; i < shortArray.length; i++) {
ret[i] = (byte)shortArray[i];
}
return ret;
}
public static short[] byteArrayToShortArray(byte[] byteArray) {
short[] ret = new short[byteArray.length];
for(int i = 0; i < byteArray.length; i++) {
ret[i] = (byte)byteArray[i];
}
return ret;
}
//write fsc16 crc into the last 2 bytes of the array.
public static void calcCrc(short[] bytes, int len)
{
int i = calcCrcToInt(bytes,len);
bytes[(len - 2)] = ((short)(i & 0xFF));
bytes[(len - 1)] = ((short)(i >> 8 & 0xFF));
}
public static int calcCrcToInt(short[] bytes, int len)
{
if ((bytes == null) || (len <= 2))
{
return -1;
}
return fsc16(bytes, len - 2, poly);
}
//uCRC
public static short[] uCRCTable = { 0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139, 87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22, 233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53 };
public static int uCRC(short[] bytes, int len, int poly) {
int j = 0;
int i = poly;
poly = j;
while (len != 0) {
j = bytes[poly] ^ i;
i = j;
if (j < 0) {
i = j + 256;
}
i = uCRCTable[i];
poly += 1;
len -= 1;
}
return i;
}
//write uCRC to bytes[len-1]
public static void calcUCRC(short[] bytes, int len) {
int i = calcUCRCBToInt(bytes,len);
bytes[(len - 1)] = ((short)(i & 0xFF));
}
public static int calcUCRCBToInt(short[] bytes, int len) {
if ((bytes.length == 0) || (len <= 2))
{
return -1;
}
return uCRC(bytes, len - 1, 119) & 0xff;
}
public static byte[] build11Packet(short type,int command) {
//template joy packet.
short[] packet = new short[] { 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// start size crc type command seq crc 11byte
int len = packet.length;
packet[ 1] = (short)(len << 3);
calcUCRC(packet, 4);
packet[4] = type;
packet[5] = (short)(command & 0xff);
packet[6] = (short)((command >> 8) & 0xff);
calcCrc(packet, packet.length);
return shortArrayToByteArray(packet);
}
public static byte[] createGetAltitudePacket() {
return build11Packet((short)0x48,0x1056);
}
public static byte[] takeOffPacket = new byte[] { (byte)0xcc, (byte)0x58, (byte)0x00, (byte)0x7c, (byte)0x68, (byte)0x54, (byte)0x00, (byte)0xe4, (byte)0x01, (byte)0xc2, (byte)0x16 };
public static byte[] landPacket = new byte[] { (byte)0xcc ,(byte)0x60, (byte)0x00, (byte)0x27, (byte)0x68, (byte)0x55, (byte)0x00, (byte)0xe5, (byte)0x01, (byte)0x00, (byte)0xba, (byte)0xc7 };
public static byte[] createConnectReqPacket() {
byte[] connectPacket = "conn_req:00".getBytes();
connectPacket[connectPacket.length - 2] = (byte)0x96;
connectPacket[connectPacket.length - 1] = (byte)0x17;
return connectPacket;
}
public static byte[] createConnectAckPacket() {
byte[] connectPacket = "conn_ack:00".getBytes();
connectPacket[connectPacket.length - 2] = (byte)0x96;
connectPacket[connectPacket.length - 1] = (byte)0x17;
return connectPacket;
}
public static byte[] createSetAltitudePacket(int altitude) {
//template
short[] packet = new short[] { 0xcc, 0x00, 0x00, 0x00, 0x68, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// start size crc type command seq data crc 13byte
int len = packet.length;
packet[ 1] = (short)(len << 3);//サイズは3ビット左 0x0d->0x68
packet[ 9] = (short)(altitude & 0xff);
packet[10] = (short)((altitude >> 8) & 0xff);
calcUCRC(packet, 4);
//calc crc for packet.
calcCrc(packet, packet.length);
return shortArrayToByteArray(packet);
}
//パースしてTelloPacketにする
public static TelloPacket parsePacket(byte[] bytes) {
int start = (int)bytes[0] & 0xff;
if( bytes.length < 11) return null;
if(start != 0xcc) return null;
TelloPacket retPacket = new TelloPacket();
retPacket._size = LittleEndianToIntWithShift(bytes[1],bytes[2]);
retPacket._crc8 = (int)bytes[3] & 0xff;
retPacket._typeID = (int)bytes[4] & 0xff;
retPacket._commandID = LittleEndianToInt(bytes[5],bytes[6]);
retPacket._seqNo = LittleEndianToInt(bytes[7],bytes[8]);
int dataSize = retPacket._size - 11;
if(dataSize > 0) {
retPacket._data = new byte[dataSize];
for(int i = 0; i < dataSize; i++) {
retPacket._data[i] = bytes[i + 9];
}
}
retPacket._crc16 = LittleEndianToInt(bytes[( bytes.length - 2)],bytes[( bytes.length - 1)]);
short[] shortArray = byteArrayToShortArray(bytes);
int crc8Check = calcUCRCBToInt(shortArray, 4);
int crc16Check = calcCrcToInt(shortArray, bytes.length);
if(crc8Check != retPacket._crc8 || crc16Check != retPacket._crc16) {
retPacket._isCheckCRC = false;
}
else {
retPacket._isCheckCRC = true;
}
return retPacket;
}
public static void IntToLittleEndianWithShift(int value,byte[] src) {
src[0] = (byte)((value << 3) & 0xff);
src[1] = (byte)((value >> 8) & 0xff);
}
public static void IntToLittleEndian(int value,byte[] src) {
src[0] = (byte)(value & 0xff);
src[1] = (byte)((value >> 8) & 0xff);
}
public static int LittleEndianToIntWithShift(byte b0,byte b1) {
return ((((int)b1) & 0xff) << 8) + ((int)b0 >> 3) & 0xff;
}
public static int LittleEndianToInt(byte b0,byte b1) {
return ((((int)b1) & 0xff) << 8) + ((int)b0 & 0xff);
}
public static String packetToHexString(DatagramPacket packet) {
StringBuilder hexString = new StringBuilder();
for(int i = 0; i < packet.getLength(); i++) {
hexString.append(String.format("%02x ",packet.getData()[i]));
}
return hexString.toString();
}
//バイト配列を比較
public static boolean compareByteArray(byte[] a,byte[] b, int len) {
if(a.length < len || b.length < len) return false;
for(int i = 0; i < len; i++) {
if(a[i] != b[i]) return false;
}
return true;
}
}
急いで書いたので汚いとか言われても困る。
俺たちの冒険ははじまったばかりだ!
まだまだよく分かってない部分が多いし、サンプルコードも多くありません。
Javaで書けばスマホのアプリとして持ち出せるのでトライしてみました。楽しかったので次はSwiftでもやってみようかなーとか思ってます。
動画の受信が出来ればOpenCVと組み合わせてジェスチャー操作や顔認識、自動追尾なんかも出来ると思います。
細かい操作はTelloPCの『public static byte[] createJoyPacket(float fRx, float fRy, float fLx, float fLy, float unk)』を移植すればいいんじゃないかと。
というわけで、TelloはUDPで直接通信するととても楽しいので、プログラマーはみんな買って遊ぶといいといいですよ。
今買えば明日届きます!
追記:Swiftバージョン
Swiftでも超簡単なやつ書きました。
Swiftの方は今後頑張ります。
わぁい、サポート、あかりサポートだい好きー。