Saturday, June 8, 2019

Programming the Tello Drone using Swift (Part 1)

The Tello Drone




In this article we will explore how to write a simple iOS app in Swift to allow control of the Tello.

Tello is a mini drone equipped with a HD camera that is manufactured by Ryze Robotics and includes a flight controller with DJI smarts. It is a great drone to learn to fly on as you can use it indoors and because it is so light (80 grams), crashing is fairly painless if you have the prop guards on. I have crashed mine (a lot) and the worst that has happened is that a propeller came off, which is easy to replace. It is also relatively inexpensive. You can manually control it using either an app (iOS or Android) on your phone, or a combination of the app and a dedicated Bluetooth remote. Either works fine. If you do get the Bluetooth remote be careful of not moving out of Bluetooth range of your phone while you are flying the drone.

Tello Specifications


Tello is Powered by a DJIGlobal flight control system and an Intel processor (Movidius MA2x chipset). The MA2x is based on a SARC LEON processor which has two RISC CPUs to run the RTOS, firmware, and runtime scheduler (Ref: RyzeTelloFirmware). The other specifications are:

  • Weight: Approximately 80 g (Propellers and Battery Included)
  • Dimensions: 98×92.5×41 mm
  • Propeller: 3 inches
  • Built-in Functions: Range Finder, Barometer, LED, Vision System, 2.4 GHz 802.11n Wi-Fi, 720p Live View
  • Port: Micro USB Charging Port
  • Max Flight Distance: 100m
  • Max Speed: 8m/s
  • Max Flight Time: 13min
  • Max Flight Height: 30m

Programming - Firmware Versions


Apart from being a good platform to earn your flying chops, the best thing about the Tello from my perspective is that you can write a script or a program to control the drone remotely. This opens up a lot of possibilities.

Note that there are three different Tello's that you can buy (the Tello, the newer Tello EDU and the Ironman Edition), and they use slightly different API's. So make sure that you use the appropriate version for your drone.

You can work out which firmware you have by connecting your mobile to the Tello WiFi, opening the Tello app, tapping on settings (the gear icon), then tap on the More button, and finally tap on the "..." button to the left of the screen. This should bring up the screen shown below which includes the firmware and app version numbers. My Tello is running firmware version 1.03.33.01. You can download the relevant SDK document for this version.



The Tello EDU uses version 2.0 of the SDK. You can download a PDF of the V2 SDK from here.

Commands that are available in SDK v1.3 but not v2.0 are:

  • height?
  • temp?
  • attitude?
  • baro?
  • acceleration?
  • tof?

Conversely, commands that are available in SDK v2.0 but not v1.3 are:

  • stop (hover)
  • go x y z speed mid (same as go x y z speed but uses the mission pad)
  • curve x1 y1 z1 x2 y2 z2 speed mid (same as curve x1 y1 z1 x2 y2 z2 speed but uses the mission pad)
  • jump x y z speed yaw mid1 mid2 (Fly to coordinates x, y and z of mission pad 1 and recognize coordinates 0, 0 and z of mission pad 2 and rotate to the yaw value)
  • mon
  • moff
  • mdirection
  • ap ssid pass
  • sdk?
  • sn?

The Tello EDU also has a swarm mode if you want to control a bunch of drones.

Programming - Python


There are plenty of examples on how to use Python to control your Tello. For drones running v1.3 have a look at the DroneBlocks code. For the Tello EDU (i.e. v2.0 SDK), Ryze Robotics provide some sample code for you to download and try out.

I uploaded the DroneBlocks code using my Raspberry Pi connected to the Tello WiFi and it worked a treat. Given that there are lots of Python examples, I thought I would put together something in Swift and work up to an app which provides additional functionality not found in the official Tello app.

Programming - Swift (iOS)


We access the Tello API by connecting to the airframe via a WiFi UDP port. Once a connection is in place, the drone is controlled using simple text commands.



The first thing we want to determine is whether our device is connected to the Tello WiFi. There are a couple of Swift functions which can assist with establishing this. The Tello SSID name contains the string "TELLO" (see image above), so this is what we will use to determine wether we are connected to the correct WiFi network.

//
// WiFi.swift
// FlightPlan
//
// Requires System Configuration Framework and enable
// Access WiFi Information capability in Target -> Capabilities.
// Doesn't work in simulator.
//
// Created and modified by David Such on 7/6/19.
// Copyright © 2019 Kintarla Pty Ltd. All rights reserved.
//
import UIKit
import SystemConfiguration.CaptiveNetwork
func currentSSID() -> [String] {
// function credit: https://forums.developer.apple.com/thread/50302
guard let interfaceNames = CNCopySupportedInterfaces() as? [String] else {
return []
}
return interfaceNames.compactMap { name in
guard let info = CNCopyCurrentNetworkInfo(name as CFString) as? [String:AnyObject] else {
return nil
}
guard let ssid = info[kCNNetworkInfoKeySSID as String] as? String else {
return nil
}
return ssid
}
}
func connectedToSSID(ssidArray: Array<String>, SSID: String) -> Bool {
if ssidArray.isEmpty {
return false
}
else {
for ssid in ssidArray {
if ssid.contains(SSID) {return true}
}
}
return false
}
view raw WiFi.swift hosted with ❤ by GitHub

We can use the code above in our ViewController to ensure that we are hooked up to the Tello, and if not provide an alert. The screenshot below shows this implemented in my proof of concept app.


The code for the ViewController is shown next. It should be fairly self explanatory.

//
// ViewController.swift
// FlightPlan
//
// Created by David Such on 3/6/19.
// Copyright © 2019 Kintarla Pty Ltd. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var WiFiImageView: UIImageView!
let tello = Tello()
// MARK: - IBActions
@IBAction func takeOffTapped(_ sender: UIButton) {
switch tello.state {
case .disconnected:
showNoWiFiAlert()
break
case .wifiUp:
tello.enterCommandMode()
tello.takeOff()
break
case .command:
tello.takeOff()
break
}
}
@IBAction func landTapped(_ sender: UIButton) {
tello.land()
}
@IBAction func stopTapped(_ sender: UIButton) {
tello.stop()
}
@IBAction func settingsTapped(_ sender: UIButton) {
}
// MARK: - View Management
func showNoWiFiAlert() {
let alert = UIAlertController(title: "Not Connected to Tello WiFi", message: "In order to control the Tello you must be connected to its WiFi network. Turn on the Tello and then go to Settings -> WiFi to connect.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Check if connected to Tello WiFi
let ssidArray = currentSSID()
if connectedToSSID(ssidArray: ssidArray, SSID: "TELLO") {
tello.state = .wifiUp
self.WiFiImageView.image = UIImage(named: "WiFi100")
print("Connected to Tello WiFi.")
}
else {
self.WiFiImageView.image = UIImage(named: "WiFiDisconnected")
showNoWiFiAlert()
}
}
}


UDP


UDP (User Datagram Protocol) is a communications protocol, similar to Transmission Control Protocol (TCP), but used primarily for establishing low-latency, low-bandwidth and loss-tolerating connections. UDP sends messages, called datagrams, and is considered a best-effort mode of communications. With UDP there is no checking and resending of lost messages (unlike TCP).

Both UDP and TCP run on top of the Internet Protocol (IP) and are sometimes referred to as UDP/IP or TCP/IP.

UDP provides two services not provided by the IP layer. It provides port numbers to help distinguish different user requests and, optionally, a checksum capability to verify that the data arrived intact.

The Tello IP address is 192.168.10.1. The UDP Services available are:

UDP PORT: 8889 - Send command and receive a response.
UDP SERVER: 0.0.0.0 UDP PORT: 8890 - Receive Tello state.
UDP SERVER: 0.0.0.0 UDP PORT: 11111 - Receive Tello video stream.

If you want to send and receive via UDP on iOS then the two main libraries in use appear to be SwiftSocket and GCDAsyncUDPSocket.

Swift Socket looks to be the simpler of the two libraries, so I used that for my initial attempt. I put together a Tello Swift class to do the heavy lifting. It is reproduced below and works as advertised. You will need to put together your own UI but if you hook up the relevant buttons in the View Controller then you shouldn't have any problem reproducing what I have done.

I will add a bit more functionality to the app (e.g. video) and then stick it up on the app store for download.

//
// Tello.swift
// FlightPlan
//
// Swift Class to interact with the DJI/Ryze Tello drone using the official Tello api.
// Tello API documentation:
// https://dl-cdn.ryzerobotics.com/downloads/tello/20180910/Tello%20SDK%20Documentation%20EN_1.3.pdf
//
// Notes:
// 1. According to the SDK document, if the tello does not receive a command input within 15 seconds it will land.
// I tested mine and even after 60 seconds it didn't land with no commands.
// 2. Sending rapid commands in succession are ignored. This is the reason for the TIME_BTW_COMMANDS constant.
//
// Created by David Such on 6/6/19.
// Copyright © 2019 Kintarla Pty Ltd. All rights reserved.
//
import UIKit
import SwiftSocket
enum STATE: String {
case disconnected = "WiFi disconnected"
case wifiUp = "WiFi Up"
case command = "command mode"
}
struct RECV {
static let ok = "OK"
}
struct CMD {
static let start = "command"
static let streamOn = "streamon"
static let streamOff = "streamoff"
static let takeOff = "takeoff"
static let land = "land"
static let stop = "emergency"
}
class Tello : CustomStringConvertible {
var description: String {
return "Tello:: IP: 192.168.10.1"
}
let IP_ADDRESS = "192.168.10.1"
let UDP_CMD_PORT = 8889
let UDP_STATE_PORT = 8890
let UDP_VS_PORT = 11111
let TIME_BTW_COMMANDS = 0.5
var state: STATE
var client: UDPClient?
init(port: Int32) {
self.state = .disconnected
client = UDPClient(address: IP_ADDRESS, port: port)
}
convenience init() {
self.init(port: 8889)
}
// MARK: - UDP Methods
@discardableResult
func sendMessage(msg: String) -> String {
guard let client = self.client else { return "Error - UDP client not found" }
switch client.send(string: msg) {
case .success:
print("\(msg) command sent to UDP Server.")
let (byteArray, senderIPAddress, senderPort) = client.recv(1024)
// Use optional chaining to fail gracefully if response is invalid
if let data = byteArray, let string = String(data: Data(data), encoding: .utf8) {
print("Cient received: \(string)\nsender: \(senderIPAddress)\nport: \(senderPort)")
if string == RECV.ok {
self.state = .command
return RECV.ok
}
}
else {
print("Client error while trying to receive.")
return "Error - UDP client received invalid data."
}
case .failure(let error):
print(String(describing: error))
return "Error - " + String(describing: error)
}
return "Error - SwiftSocket unknown response."
}
// MARK: - Tello Command Methods
func enterCommandMode() {
sendMessage(msg: CMD.start)
}
func takeOff() {
sendMessage(msg: CMD.takeOff)
}
func land() {
sendMessage(msg: CMD.land)
}
func stop() {
sendMessage(msg: CMD.land)
DispatchQueue.main.asyncAfter(deadline: .now() + TIME_BTW_COMMANDS, execute: {
self.sendMessage(msg: CMD.stop)
})
}
}
view raw Tello.swift hosted with ❤ by GitHub


2 comments:

  1. This is a really good blog wish more people would read this , you offer some really good suggestions on Drone Racing For Schools. Thanks for sharing.

    ReplyDelete