A dependency-free Java client for the DJI/Ryze Tello SDK 1.3: send flight commands, receive
telemetry, and view the live video stream with ffplay. Pure JDK, no external libraries, no build
tool beyond javac/java (tested with JDK 25).
Tello exposes three independent UDP channels once you're connected to its Wi-Fi access point
(the aircraft is always 192.168.10.1):
| Channel | Direction | Port | Purpose |
|---|---|---|---|
| Command | PC ⇄ Tello | 8889 | Send text commands, receive ok/error/values |
| State | Tello → PC | 8890 | Continuous telemetry push once SDK mode is active |
| Video | Tello → PC | 11111 | Raw H.264 elementary stream, pushed after streamon |
The first command sent must always be command, which puts the aircraft into SDK mode.
If no command is received for 15 seconds, Tello lands automatically as a safety feature. To avoid
tripping this while flying, tello-java runs a background idle keep-alive: if 10 seconds pass with
no command sent while the aircraft is flying, it resends command to reset Tello's timer.
Only one process can bind a given UDP port. Since this application is the one listening on
11111 to receive frames from the aircraft, a media player can't also bind 11111. Instead,
tello-java binds 11111 itself and immediately re-sends every received datagram, unchanged, to a
second local port (11112 by default) that the player listens on instead — a plain byte-for-byte
relay, no re-encoding.
./build.sh
./run.sh
run.sh builds automatically if out/ doesn't exist yet. Useful flags:
./run.sh --video-relay-port=11112 --video-relay-host=127.0.0.1 --record-video --ui=auto
--video-relay-port/--video-relay-host: where the video relay forwards frames (default127.0.0.1:11112).--record-video: additionally saves the raw stream totello-video-<timestamp>.h264in the current directory (playable/convertible later withffplay/ffmpeg).--ui=auto|dashboard|plain: console mode (defaultauto— see "Dashboard console" below).
Connect your computer to the Tello's Wi-Fi network before running.
This project documents ffplay (bundled with ffmpeg) as the primary way to view the stream.
Install it with apt install ffmpeg / brew install ffmpeg if you don't already have it.
Using VLC instead: since VLC 3.0, its udp:// input defaults to reading raw UDP in fixed
1316-byte (7×188) chunks — a hardcoded MPEG-TS-over-UDP convention. Tello's H.264 packets run
close to Ethernet MTU (~1460 bytes), so without any override nearly every packet gets truncated,
corrupting NAL units (udp stream error: ... packet truncated, h26x demux error: this doesn't look like a h264 ES stream). Forcing VLC's raw H.264 demuxer instead of relying on that
MPEG-TS-over-UDP assumption avoids this:
vlc udp://@:11112 :demux=h264
With :demux=h264 set, a main input error: ES_OUT_SET_(GROUP_)PCR is called too late (pts_delay increased to 1000 ms) message is expected and benign for this raw elementary stream — it isn't a
sign of failure.
-
In the console, type
commandthenvideo(orstreamon). The console will print theffplaycommand and then wait for you to press Enter before actually telling Tello to start streaming:ffplay -f h264 -fflags nobuffer -flags low_delay -i udp://@:11112Run that in another terminal first, then come back and press Enter. This ordering matters: Tello sends its stream header (the H.264 parameter sets every frame after it depends on) essentially once, right when streaming starts. If
ffplayisn't already listening on the relay port at that moment, it misses that header for good (UDP has no replay) and has to wait for Tello's next periodic resync point, which is what a multi-second initial delay andnon-existing PPS referenced/decode_slice_header errormessages in ffplay's log mean.-f h264tells ffmpeg definitively that this is a raw H.264 Annex-B byte stream, skipping generic container auto-probing (the source of "Could not find codec parameters" / "consider increasing probesize" messages) entirely.-fflags nobuffer -flags low_delayare the standard low-latency flags for a raw live feed.
If the picture stutters or you see buffer-overrun warnings (common on a busy Wi-Fi link, especially since Tello's onboard radio shares bandwidth between the video and command channels), give ffmpeg's UDP input more slack instead:
ffplay -f h264 -fflags nobuffer -flags low_delay -i "udp://@:11112?fifo_size=1000000&overrun_nonfatal=1"fifo_size/overrun_nonfatalenlarge ffmpeg's own UDP receive queue and stop it from treating a momentary overrun as fatal, at the cost of a little more latency under load.
Control: command takeoff land streamon streamoff
Movement: up x | down x | left x | right x | forward x | back x (x: 20-500 cm)
Rotation: cw x | ccw x (x: 1-3600 deg)
Flip: flip l|r|f|b
Go: go x y z speed (x/y/z: -500 to 500 cm, speed: 10-100)
Curve: curve x1 y1 z1 x2 y2 z2 speed (speed: 10-60)
Set: speed x (10-100) | wifi ssid pass
Read: speed? battery? time? height? temp? attitude? baro? acceleration? tof? wifi?
Extra: video (start ffplay/vlc relay) | end
When run in a real, interactive terminal at least 32 rows by 90 columns, tello-java switches
automatically to a full-screen dashboard: an ASCII "TELLO" banner and current configuration
(addresses/ports) at the top; a left column split into a COMMANDS box (colored history: cyan for
the command you typed, green for success, yellow for an unrecognized command, red for errors) on
top and a LOG box below it (video setup hints and anything reported via
java.util.logging, kept out of the COMMANDS box so it never gets mixed in with actual command
responses); and on the right, a static command reference on top with a live flight-data box (its
own bounded panel, refreshing twice a second) pinned to the bottom, mirroring the left column's
COMMANDS-over-LOG split. Piped/redirected input (including how this project's own testing has been
done throughout) automatically falls back to the plain scrolling console instead — no ANSI codes,
no terminal size requirement — so nothing about scripting or automation changes.
No raw terminal mode is used anywhere: the COMMANDS box is a completely normal, line-buffered
prompt (backspace and line editing behave exactly as your terminal already handles them). The
flight-data box and the LOG box update on their own via background threads/callbacks, using ANSI
cursor save/move/restore so they never touch whatever you're mid-typing in the COMMANDS box; both
redraw their entire fixed row range on every update (blank-padding unused rows) rather than only
writing however many lines happen to be available, so stale content can't linger at a box's edges.
Tello/TelloConnection/TelloStateReceiver/TelloVideoRelay's loggers are redirected into the
LOG box for the duration of the dashboard session (instead of the JVM's default handler writing
raw, unpositioned text straight to the terminal) so they can't corrupt the layout — see the next
paragraph for what that corruption used to look like.
The dashboard runs in the terminal's alternate screen buffer (the same mechanism vim/htop/less use), entered on start and exited on quit. This gives it a stable, scroll-isolated canvas: without it, a real scroll event (window resize, a mouse-wheel scroll, or any other interaction that shifts the terminal's actual scrollback) would desync the dashboard's fixed row positions from what's physically on screen, permanently, for the rest of the session — which is what used to happen whenever a log message got written straight to the terminal (e.g. the video relay starting up), causing the flight data panel to drift down over a long session and progressively overwrite the command help section. On exit, the terminal's original content and cursor position are restored automatically, exactly as they were before the dashboard started.
Force one mode or the other with --ui=dashboard or --ui=plain (default --ui=auto, i.e. the
terminal-detection behavior above). If the dashboard is requested but the terminal is smaller than
32x90 (or its size can't be detected), tello-java prints a notice and falls back to the plain
console rather than drawing a broken layout.
The video/streamon confirm-before-streaming step (see above) works the same way in the
dashboard: the ffplay/vlc commands appear in the LOG box, and you press Enter at the prompt
once one of them is running, exactly as in the plain console.
src/com/tello/core/ Command connection, state receiver, the Tello facade, response parsing
src/com/tello/video/ TelloVideoRelay: receives from Tello on 11111, forwards to ffplay
src/com/tello/cli/ TelloCli (main class, plain console), Dashboard (full-screen console),
AnsiTerminal (ANSI escape-code helpers, terminal size detection)
Commands feel slow, or you see a No response yet for '...' (attempt N/3), retrying...
warning: Tello has a single, bandwidth-limited onboard Wi-Fi radio shared by the command,
telemetry, and video channels — streaming video can measurably slow down command responses. The
client waits up to 7 seconds per attempt (3 attempts) before giving up on a command, and now logs
a warning each time it has to retry so a slow response doesn't look like a silent hang. This is
intentionally not made more aggressive: Tello has no per-command request IDs, so shortening the
timeout would increase the odds of resending a flight command while the original is still being
executed, which is worse than an occasional slow reply.
Error: Command failed: error No valid imu: this is the aircraft's own firmware response,
relayed as-is — not a bug in this client. It means Tello's IMU wasn't ready for that command,
typically because a flight command was sent too soon after command, or the aircraft was moved or
sitting on an uneven surface during its startup calibration. Give it a moment after entering SDK
mode before flight commands, keep it still and level, and power-cycle it if the error persists.
try (Tello tello = new Tello()) {
tello.connect();
tello.takeOff();
tello.forward(50);
tello.land();
}Tello throws TelloException on command failures/timeouts and IllegalArgumentException on
out-of-range parameters, validated client-side against the documented limits before anything is
sent to the aircraft.