defmodule Drone.Adapters.Tello do
@moduledoc """
Tello drone adapter for ex_drone.
This adapter communicates with DJI Tello and Tello EDU drones over
Wi-Fi UDP using the official Tello SDK protocol.
## Default Configuration
- Drone IP: `192.168.10.1`
- Drone port: `8889`
- Local port: `8889`
- Command timeout: `10_000` ms
## Usage
{:ok, drone} = Drone.connect(:tello, name: :tello_1)
Drone.connect_sdk(drone)
Drone.takeoff(drone)
Drone.move(drone, :forward, 50)
Drone.land(drone)
Drone.disconnect(drone)
## Custom Configuration
{:ok, drone} = Drone.connect(:tello,
name: :tello_1,
drone_ip: {192, 168, 10, 1},
drone_port: 8889,
local_port: 9030,
timeout: 15_000
)
**Safety warning**: The Tello is a real physical drone. Always test in
the simulator first. Use prop guards. Do not fly near faces. Have an
emergency stop ready.
"""
@behaviour Drone.Adapter
alias Drone.{
Adapters.Tello.Connection,
Adapters.Tello.Encoder,
Adapters.Tello.Parser,
Command,
Geometry
}
defstruct [
:socket,
:drone_ip,
:drone_port,
:timeout,
mode: :idle,
flying: false,
x: 0,
y: 0,
z: 0,
yaw: 0
]
@type t :: %__MODULE__{
socket: port() | nil,
drone_ip: :inet.ip_address(),
drone_port: non_neg_integer(),
timeout: non_neg_integer(),
mode: :idle | :sdk_mode | :flying | :emergency,
flying: boolean(),
x: integer(),
y: integer(),
z: integer(),
yaw: integer()
}
@impl Drone.Adapter
def connect(opts) do
ip = Keyword.get(opts, :drone_ip, Connection.default_drone_ip())
port = Keyword.get(opts, :drone_port, Connection.default_drone_port())
local_port = Keyword.get(opts, :local_port, Connection.default_local_port())
timeout = Keyword.get(opts, :timeout, Connection.default_timeout())
case Connection.open(local_port: local_port) do
{:ok, socket} ->
state = %__MODULE__{
socket: socket,
drone_ip: ip,
drone_port: port,
timeout: timeout
}
{:ok, state}
{:error, reason} ->
{:error, {:connection_error, reason}}
end
end
@impl Drone.Adapter
def command(
%__MODULE__{socket: socket, drone_ip: ip, drone_port: port, timeout: timeout} = state,
%Command{} = cmd
) do
encoded = Encoder.encode(cmd)
conn_opts = [drone_ip: ip, drone_port: port, timeout: timeout]
case Connection.send_command(socket, encoded, conn_opts) do
{:ok, response} ->
case Parser.parse(response) do
{:ok, :ok} ->
new_state = update_state(state, cmd)
{:ok, :ok, new_state}
{:ok, value} ->
new_state = update_state(state, cmd)
{:ok, value, new_state}
{:error, reason} ->
{:error, reason, state}
end
{:error, reason} ->
{:error, reason, state}
end
end
@impl Drone.Adapter
def telemetry(%__MODULE__{} = state) do
{:ok,
%{
x: state.x,
y: state.y,
z: state.z,
yaw: state.yaw,
flying: state.flying,
mode: state.mode
}, state}
end
@impl Drone.Adapter
def disconnect(%__MODULE__{socket: socket} = _state) do
if socket, do: Connection.close(socket)
:ok
end
defp update_state(%__MODULE__{} = state, %Command{type: :sdk_mode}) do
%{state | mode: :sdk_mode}
end
defp update_state(%__MODULE__{} = state, %Command{type: :takeoff}) do
%{state | z: 30, flying: true, mode: :flying}
end
defp update_state(%__MODULE__{} = state, %Command{type: :land}) do
%{state | z: 0, flying: false, mode: :sdk_mode}
end
defp update_state(%__MODULE__{} = state, %Command{type: :emergency}) do
%{state | mode: :emergency, flying: false}
end
defp update_state(%__MODULE__{} = state, %Command{type: :move, args: args}) do
direction = Keyword.fetch!(args, :direction)
distance = Keyword.fetch!(args, :distance)
{dx, dy, dz} = Geometry.move_delta(direction, distance, state.yaw)
%{state | x: state.x + dx, y: state.y + dy, z: max(0, state.z + dz)}
end
defp update_state(%__MODULE__{} = state, %Command{type: :rotate, args: args}) do
direction = Keyword.fetch!(args, :direction)
degrees = Keyword.fetch!(args, :degrees)
%{state | yaw: Geometry.rotate_yaw(direction, state.yaw, degrees)}
end
defp update_state(%__MODULE__{} = state, %Command{type: :flip, args: args}) do
direction = Keyword.fetch!(args, :direction)
{dx, dy} = Geometry.flip_delta(direction)
%{state | x: state.x + dx, y: state.y + dy}
end
defp update_state(%__MODULE__{} = state, %Command{type: :speed, args: _args}) do
state
end
defp update_state(%__MODULE__{} = state, %Command{type: :stop}) do
state
end
defp update_state(%__MODULE__{} = state, %Command{type: :hover}) do
state
end
defp update_state(%__MODULE__{} = state, %Command{type: :query, args: args}) do
query_type = Keyword.fetch!(args, :type)
case query_type do
:height -> %{state | z: nil}
_ -> state
end
end
end