Telemetry

Welcome back! I hope you enjoyed Part 1 of this series and are now eager for more.

In this part we tap into the telemetry data that every Tello drone broadcasts. A UDP listener is required to receive the incoming packets from the drone. This listener is implemented using Apple’s Network framework. How the listener is implemented is the core part of this tutorial.

The finished app looks like this.

On the left are the controls to connect to the drone, put it into command mode, and perform a few flying actions like takeoff and landing. These functions were covered in Part 1.

The right side shows the new content from this tutorial: the telemetry data, for example:

  • the percentage of the current battery level
  • the flying height in cm
  • the attitude angles: pitch, roll, and yaw
  • speeds along the x, y, and z axes

The complete code for this tutorial can be downloaded from here.

The tutorial is structured into the following sections according to how the data flows:

  • receiving,
  • processing, and
  • displaying

the telemetry data.

Let’s spin up our motors and get started.

Receiving the Telemetry Data

The basis for receiving telemetry data is a UDP listener implemented using Apple’s Network framework.

Creating the Listener

Everything is based on the NWListener, described as

“An object you use to listen for incoming network connections.”

Let’s begin by instantiating one.

var listener: NWListener?
 
do {
  listener = try NWListener(using:.udp, on: port)
} catch {
  print("exception upon creating listener")
}

Next, define the behavior of stateUpdateHandler. It is described as follows:

“A handler that receives listener state updates.”

listener?.stateUpdateHandler = {(newState) in
  switch newState {
  case.ready:
    print("ready")
  default:
    break
  }
}

Accepting new Connections

For now, the listener can handle state updates. The fun begins when a new connection arrives.

We need to implement another handler: the newConnectionHandler, which the documentation describes as:

“A handler that receives inbound connections.”

listener?.newConnectionHandler = {(newConnection) in
  newConnection.stateUpdateHandler = {newState in
    switch newState {
    case.ready:
      print("ready")
    default:
      break
    }
  }
  newConnection.start(queue: DispatchQueue(label: "newconn"))
}

Receiving Messages

When the connection is ready, assign a handler to receiveMessage, which “schedules a single receive completion handler for a complete message, as opposed to a range of bytes.”

connection.receiveMessage { (data, context, isComplete, error) in
  // Decode and continue processing data
}

Starting the Listener

Everything is in place to start the listener with start. It is described as:

“Registers for listening and sets the queue on which all listener events are delivered.”

listener?.start(queue:.main)

Processing the Telemetry Data

This section describes the TelemetryPublisher class of the example application, which uses Apple’s Combine framework.

From this framework, two types are used:

The Published property wrapper:

A type that publishes a property marked with an attribute.

The PassthroughSubject:

A subject that broadcasts elements to downstream subscribers.

When Data is received by the UdpListener, it is forwarded to the PassthroughSubject, which decodes it into a String for further parsing.

This String looks like this:

mid:257;x:0;y:0;z:0;mpry:0,0,0;pitch:0;roll:0;yaw:0;vgx:0;vgy:0;vgz:0;templ:76;temph:77;tof:10;h:0;bat:32;baro:531.77;time:0;agx:11.00;agy:-15.00;agz:-1002.00;

This will be spliced up and parsed into a TelemetryData object which is then published via the @Published property wrapper.

The TelemetryPublisher must conform to ObservableObject.

“A type of object with a publisher that emits before the object has changed.”

This is especially important for the next step. For now, we have received the telemetry data, parsed it, and provided a mechanism to update our user interface. Now we need to use this mechanism in our UI so it updates whenever the telemetry data changes.

Displaying the Telemetry Data

Our ObservableObject from the previous step needs to be made available in the SwiftUI environment. In the tutorial application this is our ContentView class.

We are going to provide the TelemetryPublisher as an EnvironmentObject.

A property wrapper type for an observable object supplied by a parent or ancestor view.

In code, that’s as easy as:

ContentView().environmentObject(telemtryPublisher)

In the ContentView, the TelemetryPublisher is provided with:

@EnvironmentObject var telemetryPublisher: TelemetryPublisher

Now we just need to access the telemetry values and display them in ContentView, for example:

Text("Battery: \(String(format: "%.2f", telemetryPublisher.data.bat))")

Thanks to SwiftUI’s declarative nature, that’s all we need to do. The ContentView will update automatically whenever the telemetry data changes.

Conclusion

We are done. After this second part, we can not only command the drone but also receive valuable telemetry data that informs us about the drone’s state.

This concludes the second part of this tutorial series.

The complete code for this tutorial can be downloaded from here.

Resources