I recently received a DJI/Ryze Tello as a gift from family. The Tello is a small drone weighing just 80g that works both indoors and outdoors (although you need good light and good conditions, ie, no wind, to be safe when using it outside). It has a downward facing vision system that allows it to stay steady when hovering. It works up to 100m away and is controlled by a smartphone or table via the Tello app.
The Tello has a 5mp camera on the front that streams 720p video to a phone or tablet. When you have a strong wifi signal, you can capture some fairly decent footage (not Spark, Air, or Mavic quality, mind you). There is no gimbal, although it does have electronic image stabilisation (EIS) which when the drone is in slow mode, it keeps the video level and fairly steady regardless of what direction you move the drone.
The most important part is that the Tello also has an SDK that lets you communicate directly with it from a Mac, PC, iOS, or Android device. If you don’t have a Tello then this tutorial might not have much value for you. Alternatively, you can buy a Tello direct from DJI*(aff link) to follow along this tutorial. The tutorial is primarily aimed at the regular Tello, but it will also work for Tello EDU which is the newer version designed for education.
Connecting to the Tello with iOS
The Tello is controlled by a smartphone such as the iPhone or an Android device. To control it you connect to the built in wireless network on the Tello. When connected, you open the Tello app and can control the drone with either pre-set commands such as circle, up and away, or rotate, or you can use the two on-screen joysticks to fly forward, back, left, right, up, down, and rotate left and right.
Building a Custom iOS App to Control the Tello
As an iOS developer, I thought of ways of how I could build an iPhone app to connect to the Tello. Importantly: Can I create an app that enhances the already provided functions that the Tello app already provides? In short, yes, I think there can be some room for improvement, particularly in the Flight Modes section where the circle, for example, has a fixed radius. Likewise, can I add a time-lapse mode to the drone? Although it doesn’t have GPS to accurately fly to and from specified points, perhaps it can be automatically controlled to slowly drift and capture a time-lapse. Who knows how steady the image will be; but I think it’s worth testing.
Tello Tutorial
In this particular tutorial the focus is to connect the iPhone to the Tello with a custom built app. This was the first step I wanted to take to make sure that I could access it, and understand what can and cannot be done. I won’t cover some of the more advanced parts, but only because I haven’t got that far yet; but I will cover them in future tutorials.
Before getting to the iOS part of the tutorial, I will first show you how to connect a Mac to the Tello and control it.
Connecting a Mac to the Tello
Step one was to figure out how to connect to the Tello. On the Ryze website it has a link to the SDK documentation. This explains that the SDK is version 1.3.0.0 and then provides some simple instructions on how you connect. You need to connect with Wi-Fi on a UDP port.
To test, I downloaded a Mac app called Packet Sender. This is capable of sending a packet over UDP to a specified address, which is exactly what is needed.
The next job is to connect your Mac to the Tello drone. Switch the drone on, and then open up the wireless networks list and select the drone. It should be easy to spot.
Next, open Packet Sender and enter the details as seen in the screenshot above. You might want to hit “save” when done so that you have quick access to the “command” command, as well as others that you can add.
When done, add another command called TakeOff with the word “takeoff” in the ASCII field. Save that. Note that all commands are case sensitive. Check the documentation to make sure you are getting them right (lower case).
With those two commands in place, make sure the drone is free from obstructions above and around and then first tap “Send” for the command to be sent. The logs should present an OK which means the Tello received the request and that all is OK.
Next, click “Send” to takeoff. Your drone should lift in to the air. If you touch nothing for 15 seconds, the Tello will land itself.
Have a look around the Tello SDK documentation and add in more commands as you see fit.
With Packet Sender working, it shows that you can easily connect to the Tello and pass it commands. You can also stream video from it to your Mac, although I’ll cover that another time.
Sending the Tello Commands from an iOS App
The next challenge is to try replicate what we have done on the Mac and put it in to an iOS app. We know that by simply being on the same network, we can send a UDP packet to the correct IP address and port, and get a response from the drone. This shouldn’t be too challenging to do the same from an iOS app.
CocoaAsyncSocket
Rather than muck around with the low-level network in an iOS app, I decided to use the excellent CocoaAsyncSocket by Robbie Hanson which provides a quick way of connecting to a socket and sending data across it.
Instructions
Create a new Xcode project with a single view.
In Terminal, navigate to the folder of the project. Enter the command “pod init”. This will generate Podfile which we need to use the CocoaAsyncSocket pod. Open up Podfile with a text editor, and enter the following (just add line 9):
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'Tello' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for Tello
pod 'CocoaAsyncSocket'
end
When done, save the file.
In the terminal, run the command “pod install” which will do what it needs to do to fetch CocoaAsyncSocket and import it in to the project.
When that completes, you will notice a .workspace file has been created in the folder. Use this to open the project.
In ViewController, import CocoaAsyncSocket, and specify that the class conforms to the GCDAsyncUdpSocketDelegate as follows:
import UIKit
import CocoaAsyncSocket
class ViewController: UIViewController, GCDAsyncUdpSocketDelegate {
Create some variables (and constants) as follows:
var socket = GCDAsyncUdpSocket()
let sendHost = "192.168.10.1"
let sendPort: UInt16 = 8889
let statePort: UInt16 = 8890
Socket is used, and no surprise here, to create a socket.
The sendHost constant is the IP address of the Tello. This IP is fixed.
The sendPort is the port needed to send commands to the Tello, as seen in the Packet Sender part of this tutorial.
The statePort is what we use to listen to what the drone is broadcasting. This includes information from its sensors such as pitch, yaw, height, barometer reading, temperature, and so on.
The next task is to override the viewWillAppear method:
override func viewWillAppear(_ animated: Bool) {
setupCommand()
setupListener()
}
I added two method calls in here. The first will be used to send the “command” command to the drone. This is the essential first step.
Next, we set up a listener which will connect to the appropriate port to receive the telemetry from the drone.
setupCommand()
func setupCommand() {
// Set the delegate and dispatch queue
socket.setDelegate(self)
socket.setDelegateQueue(DispatchQueue.main)
// Send the "command" command to the socket.
do {
try socket.bind(toPort: sendPort)
try socket.enableBroadcast(true)
try socket.beginReceiving()
socket.send("command".data(using: String.Encoding.utf8)!,
toHost: sendHost,
port: sendPort,
withTimeout: 0,
tag: 0)
} catch {
print("Command command sent.")
}
}
The code above is what we use to send the initial “command” to the drone.
Line 3 we set self as the delegate for the socket. This means that anything that the drone sends back will be sent to use via the delegate methods we implement.
Line 4 is used to set the queue to main.
On line 8 we bind the port to the socket, and on line 9 and 10 we enable broadcast and begin receiving. Sending commands to the Tello will always respond with some kind of message; often an OK, sometimes an Error, and other times a message.
On line 11 we send the word “command” to the socket with he specified host and port.
The next task is to set up the listening port which we do as follows:
func setupListener() {
let receiveSocket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
do {
try receiveSocket.bind(toPort: statePort)
} catch {
print("Bind Problem")
}
do {
try receiveSocket.beginReceiving()
} catch {
print("Receiving Problem")
}
}
Line 2, we declare the receiving socket just like the other socket, except we use a different initialiser and specify self as delegate and main as the dispatch queue.
Line 4 we bind the port.
Line 10 we begin receiving data on the port.
Implementing the Delegate Method
To keep this tutorial focussed on just receiving the data, rather than implementing all delegate methods, we will just implement the didReceiveData delegate method:
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
let dataString = String(data: data, encoding: String.Encoding.utf8)
if (sock.localPort() == sendPort) {
print(dataString)
}
}
Line 2 is where we encode the data received in to a utf8 string.
In line 3 we show that we are only interested if the port is 8889 which is the one that provides the response from when you send commands to the drone…. ie, you will see logs entered (because of line 4 printing it) as “OK” if a command was successful, or you will see some kind of error in the console if there was a problem.
If you run the app now, you should get a response. Just make sure that you first connect to iPhone to the Tello network prior to running the app. When the app opens, you should get an OK response which means the app and Tello are communicating.
Receiving Telemetry
The next task we will tackle is receiving the telemetry. As it is now, the telemetry is being sent via the delegate method because of setting up the listener on port 8890 earlier. However, we are currently ignoring it because of the if statement in the delegate method which only prints out the dataString from the commands we send.
To fix that, add the following to the delegate method:
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
let dataString = String(data: data, encoding: String.Encoding.utf8)
if (sock.localPort() == sendPort) {
print(dataString)
}
if (sock.localPort() == statePort) {
print(dataString)
}
}
Lines 1 – 5 were from the previous part of the tutorial.
Line 7 is where we check that the data has arrived on port 8890.
Line 8 is where we print the telemetry information.
If you run the app now, you’ll see a number of updates per second that show the live telemetry data from the drone. If you pick up the drone and move it around, you will see many of the numbers changing.
Sending Other Commands to the Tello
In this final part of this tutorial we will send some commands to the drone.
Add a few buttons to the ViewController in the storyboard.
There is nothing really exciting to see in the storyboard above, but it’s functional enough to demonstrate sending commands to the drone.
Connect each of these up to IBOutlets which you can add to the ViewController.swift file.
@IBAction func takeOff(_ sender: Any) {
sendCommand(command: "takeoff")
}
@IBAction func go(_ sender: Any) {
sendCommand(command: "go 50 0 0 10")
}
@IBAction func emergency(_ sender: Any) {
sendCommand(command: "emergency")
}
@IBAction func rotateCW(_ sender: Any) {
sendCommand(command: "cw 360")
}
The first one instructs the Tello to takeoff. The second tells it to move forward 50cm at a speed of 10cm/second. The third is the “emergency” button which immediately stops the rotors making the drone fall to the ground (be careful with this). The last tells it to rotate clockwise 360 degrees.
Of course, we need to implement the sendCommand: method.
func sendCommand(command: String) {
let message = command.data(using: String.Encoding.utf8)
socket.send(message!, toHost: sendHost, port: sendPort, withTimeout: 2, tag: 0)
}
Line 2 is used to convert the command to data. Line 3 is used to send that message to the Tello.
Testing
Because you are controlling the Tello with a very basic and potentially buggy test app, please do so with caution (and I also recommend that you do not use it outside). The Tello will typically take off to about 70 or 80cm. Sometimes it jumps higher and then settles down, and other times it takes off, and then rises gently to 70 or 80cm. At this height, an emergency stop shouldn’t do any damage, but be careful. Likewise, if you run the go command, make sure you have 50cm in front of the drone.
The full code for the sample project is below. In the next tutorial, I’ll look at what is needed to make this a more functional app that allows you to actually control the drone. At some point I’ll also look at using the video feed from the camera to capture video and still photos.
You might notice in the code below that the delegate method parses the data. I didn’t cover this in the tutorial, but if you have any questions about how that works then please post a comment. Briefly: We create a dictionary of String and Double. We next separate the string by the semi-colon which inserts each pair in to an array. The next part loops around the array and separates each string stored by the colon character and from this, we can put the key and value in to the dictionary. This will be used in an upcoming tutorial so that we can display some of the telemetry on screen.
//
// ViewController.swift
// Tello
//
// Created by Matthew Newill on 07/01/2019.
// Copyright © 2019 DevFright. All rights reserved.
//
import UIKit
import CocoaAsyncSocket
class ViewController: UIViewController, GCDAsyncUdpSocketDelegate {
var socket = GCDAsyncUdpSocket()
let sendHost = "192.168.10.1"
let sendPort: UInt16 = 8889
let statePort: UInt16 = 8890
override func viewWillAppear(_ animated: Bool) {
setupCommand()
setupListener()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func sendCommand(command: String) {
let message = command.data(using: String.Encoding.utf8)
socket.send(message!, toHost: sendHost, port: sendPort, withTimeout: 2, tag: 0)
}
@IBAction func takeOff(_ sender: Any) {
sendCommand(command: "takeoff")
}
@IBAction func go(_ sender: Any) {
sendCommand(command: "go 50 0 0 10")
}
@IBAction func emergency(_ sender: Any) {
sendCommand(command: "emergency")
}
@IBAction func rotateCW(_ sender: Any) {
sendCommand(command: "cw 360")
}
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
let dataString = String(data: data, encoding: String.Encoding.utf8)
if (sock.localPort() == sendPort) {
print(dataString)
}
if (sock.localPort() == statePort) {
var telloStateDictionary = [String:Double]()
let stateArray = dataString?.components(separatedBy: ";")
for itemState in stateArray! {
let keyValueArray = itemState.components(separatedBy: ":")
if (keyValueArray.count == 2) {
telloStateDictionary[keyValueArray[0]] = Double(keyValueArray[1])
}
}
processStreamData(streamData: telloStateDictionary)
}
}
func processStreamData(streamData: Dictionary<String, Double>) {
//print(streamData)
}
func setupCommand() {
// Set the delegate and dispatch queue
socket.setDelegate(self)
socket.setDelegateQueue(DispatchQueue.main)
// Send the "command" command to the socket.
do {
try socket.bind(toPort: sendPort)
try socket.enableBroadcast(true)
try socket.beginReceiving()
socket.send("command".data(using: String.Encoding.utf8)!,
toHost: sendHost,
port: sendPort,
withTimeout: 0,
tag: 0)
} catch {
print("Command command sent.")
}
}
func setupListener() {
let receiveSocket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
do {
try receiveSocket.bind(toPort: statePort)
} catch {
print("Bind Problem")
}
do {
try receiveSocket.beginReceiving()
} catch {
print("Receiving Problem")
}
}
}
Matrix4d says
Hello,
Thanks a lot for this tutorial and advices
Did you test the recording video stream from the tello ?
thx