This is a simple simulation of a Tello drone using Ursina. The drone can be controlled via tcp calls.
In the repo there is the simulation server along with a client class that can be used to interact with the sim server
The easiest way to get started is using the provided dev container, which sets up GUI/X11/VNC support for you:
-
Setup the dev container for your platform:
.devcontainer/setup.sh
This will auto-detect your platform (macOS, Linux, Windows, WSL) and generate the appropriate
devcontainer.json. -
Open in VS Code:
- Install the "Dev Containers" extension
- Open Command Palette (Cmd/Ctrl + Shift + P)
- Run "Dev Containers: Reopen in Container"
-
Install the Python dependencies (macOS/Linux only — the Windows config does this for you automatically via
setup-windows.sh):pip install -r requirements.txt export PYTHONPATH=$PWD
-
Platform-specific requirements:
- macOS: Install XQuartz (
brew install --cask xquartz) and runxhost +localhost - Linux: X11 forwarding should work out of the box
- Windows: Access GUI via VNC at
http://localhost:5901(password:vncpass)
- macOS: Install XQuartz (
If you prefer to set up the environment manually:
-
Create the virtual environment by running:
python3.12 -m venv --copies venv
--copiescopies the interpreter into the venv instead of symlinking it. Without it, a Homebrew/pyenv upgrade that removes the Python version you built the venv from leaves the venv silently broken (see Troubleshooting). -
Activate the virtual environment by running:
source venv/bin/activate -
Install the required packages by running:
pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt
-
Export the python path by running:
export PYTHONPATH=$PWD
To run the simulation, run the following command:
python tello_sim/run_sim.pyLeave it running. The examples connect to it, and tello.connect() fails with
an explanatory error if the simulator is not up.
You can try running some of the examples to see how the simulation works. The examples are located in the examples folder.
The gates and the pedestrians are positioned from a separate window, so nothing is drawn over the 3D view — the FPV video path captures the whole framebuffer, and any on-screen panel would be burned into your photos and recordings.
Start both windows at once:
python run.pyOr run them separately (the editor waits for the simulator, so either order works):
python tello_sim/run_sim.py # the 3D window
python tools/scene_editor.py # the controls windowrun.py --no-editor starts the simulator alone; run.py --editor-only attaches
the editor to a simulator that is already running.
The editor has two tabs:
| Tab | Controls |
|---|---|
| Gates | Pick a gate, then set its X, Z, diameter, height above the ground, and heading. |
| People | Pick a pedestrian, then set its X, Y, Z and heading. |
Edits apply live in the 3D window. Save writes the layout to tello_sim/gates.json and tello_sim/people.json, which are loaded on the next start; delete either file to go back to the built-in defaults.
The editor is a Tkinter app and needs Tk available to your Python. If
import tkinter fails on macOS with Homebrew Python, install the matching Tk
package — for example brew install python-tk@3.13.
Scripts can drive the same thing through SimulatorClient.get_scene(),
set_gate(), set_person() and save_scene().
The simulator server code lives in the tello_sim folder. The client side is
three modules in the repo root:
| Module | Class | What it is |
|---|---|---|
| tello_sim_client.py | TelloSimClient |
The drone. Every method mirrors the real DJI Tello / djitellopy API, so a script written against it also runs on real hardware. |
| simulator_client.py | SimulatorClient |
The simulator. Ground-truth position, the telemetry stream, motion-completion polling — things a real drone cannot do. |
| sim_connection.py | SimConnection |
Shared TCP plumbing. You do not normally use this directly. |
They are independent objects, so a privileged call is always visible as sim.
at the call site:
from tello_sim_client import TelloSimClient
from simulator_client import SimulatorClient
tello = TelloSimClient() # what a real Tello can do
sim = SimulatorClient() # what only a simulator can do
tello.connect()
tello.takeoff()
tello.move_forward(50)
sim.wait_until_motion_complete()
print(sim.get_position())
tello.land()If you only ever touch tello, your script is portable to a real Tello.
SimulatorClient exposes the drone's position and state two ways — poll it on
demand, or subscribe to a push stream:
-
Poll (TCP port 9999): the commands
get_positionandget_statereturn JSON. Via the client:sim.get_position() # {'x': -1.54, 'y': 0.2, 'z': 0.5, 'yaw': 0.0} sim.get_state() # position + pitch/roll/speeds/battery/flying/time
-
Subscribe (UDP port 9998): send the datagram
subscribeand the simulator pushes the same JSON state at ~10 Hz until you sendunsubscribe(or stop resubscribing for 10 s). Via the client:sim.subscribe_state(lambda state: print(state["x"], state["z"])) ... sim.unsubscribe_state()
x/z are metres in the simulator's world frame, y is height above the
ground in metres (matching get_height), and yaw is degrees in [-180, 180].
See examples/15_position_telemetry.py.
The venv's interpreter is probably a dangling symlink. Venvs created without --copies symlink venv/bin/python to the Python they were built from (e.g. /opt/homebrew/opt/python@3.12/...); if Homebrew later removes that version (brew upgrade / brew cleanup), the symlink dies. Your prompt still shows (venv), but the shell skips the dead symlink during PATH lookup and falls through to some other Python (e.g. conda base) that doesn't have the packages — so imports fail no matter what you do with PYTHONPATH.
Diagnose it:
venv/bin/python --version # "no such file or directory" => venv is dead
which python # points outside venv/ despite (venv) prompt => same problemFix: recreate the venv against a Python that exists, using --copies so it can't happen again:
rm -rf venv
python3.12 -m venv --copies venv
source venv/bin/activate
venv/bin/python -m pip install -r requirements.txtThe dev container images (.devcontainer/Dockerfile, .devcontainer/Dockerfile.windows) are currently pinned to Python 3.9, while requirements.txt pins numpy==2.2.3, which does not publish wheels for Python 3.9 (2.0.2 is the newest 3.9-compatible release). If pip install -r requirements.txt fails inside the dev container with a "no matching distribution" error for numpy, this version mismatch is why — use the manual setup with Python 3.12 instead, or downgrade the numpy pin, until the container images are updated.
-
For a specific python version on macOS, consider using pyenv to manage multiple python versions.
-
Another alternative for macOS users is to use Homebrew to install the desired python version:
brew install python@3.12
-
Conda users can create an environment with the desired python version:
conda create -n tello-sim python=3.12 conda activate tello-sim pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt export PYTHONPATH=$PWD
