Skip to main content

flaight

light → flight → flaight.

By Dominic Neuburg — MIT License

PyPI version Python versions License: MIT CI

Choreography library for Crazyflie drone light shows. This project is maintained with the help of vibe coding. Define timed sequences of movements and light effects in Python, simulate them without hardware, then fly them for real.

The centrepiece feature: describe a show in plain language — "two drones take off, fly apart and alternate between red and blue" — and flaight uses a local language model to turn that description directly into a Show object, ready to simulate or fly.


Installation

pip install -e .

Requires Python 3.10+. Hardware flight requires a Crazyradio USB dongle and cflib. The natural-language parser requires a running Ollama server.


Core concepts

A Show holds one or more named drones, each with a DroneTrack — an ordered list of (time, action) pairs called keyframes. All times are in seconds from show start. Multiple actions on the same drone at the same time are allowed (e.g. Takeoff and SetColor simultaneously).


Quick start

from flaight import Color, FadeColor, FlightRunner, Hover, Land, MoveTo, SetColor, Show, Takeoff

show = Show()
d1 = show.add_drone("cf1", uri="radio://0/80/2M/E7E7E7E701")

d1.at(0.0, Takeoff(height=1.0, duration=2.0))
d1.at(0.0, SetColor(Color.RED))
d1.at(2.0, MoveTo(x=1.0, y=0.0, z=1.0, duration=3.0))
d1.at(5.0, FadeColor(color=Color.BLUE, duration=1.5))
d1.at(6.5, Land(duration=2.0))
d1.at(6.5, SetColor(Color.OFF))

Simulate without hardware:

from flaight import DryRunRunner
DryRunRunner(show).run()            # instant printout
DryRunRunner(show, realtime=True).run()  # sleep-accurate timing

Fly for real:

FlightRunner(show).run()

FlightRunner runs a collision check before connecting. If two drones come within 20 cm of each other it raises CollisionError and aborts.


Actions

Action Key parameters Notes
Takeoff(height, duration) height in metres, duration in seconds First action for any flying drone
Land(duration) duration in seconds Last action for any flying drone
Hover(duration) duration in seconds Stay in place
MoveTo(x, y, z, yaw, duration) x/y/z in metres from origin, yaw in degrees Absolute world-frame position
SetColor(color) Color instance Instant LED change
FadeColor(color, duration, steps) target color, fade time, interpolation steps Smooth LED transition

Colors

from flaight import Color

# Named presets
Color.RED, Color.GREEN, Color.BLUE
Color.WHITE, Color.OFF
Color.YELLOW, Color.CYAN, Color.MAGENTA
Color.ORANGE, Color.PURPLE

# Custom RGB (0–255)
teal = Color(r=0, g=180, b=160)

# Interpolate between two colors
mid = Color.RED.lerp(Color.BLUE, 0.5)   # t in 0..1

Two-drone example

from flaight import Color, FadeColor, FlightRunner, Hover, Land, MoveTo, SetColor, Show, Takeoff

show = Show()
d1 = show.add_drone("cf1", uri="radio://0/80/2M/E7E7E7E7E7")
d2 = show.add_drone("cf2", uri="radio://0/81/2M/E7E7E7E7E7")

# cf1 — left drone, starts blue
d1.at(0.0, Takeoff(height=1.0, duration=2.0))
d1.at(0.0, SetColor(Color.BLUE))
d1.at(2.5, MoveTo(x=-0.5, y=0.0, z=1.2, duration=2.0))
d1.at(4.5, FadeColor(color=Color.RED, duration=1.5))
d1.at(6.0, Hover(duration=2.0))
d1.at(8.0, Land(duration=2.0))
d1.at(8.0, SetColor(Color.OFF))

# cf2 — right drone, mirrors cf1 with swapped colors
d2.at(0.0, Takeoff(height=1.0, duration=2.0))
d2.at(0.0, SetColor(Color.RED))
d2.at(2.5, MoveTo(x=0.5, y=0.0, z=1.2, duration=2.0))
d2.at(4.5, FadeColor(color=Color.BLUE, duration=1.5))
d2.at(6.0, Hover(duration=2.0))
d2.at(8.0, Land(duration=2.0))
d2.at(8.0, SetColor(Color.OFF))

if __name__ == "__main__":
    import sys
    if "--fly" in sys.argv:
        FlightRunner(show).run()
    else:
        from flaight import DryRunRunner
        DryRunRunner(show, realtime="--realtime" in sys.argv).run()

Collision validation

from flaight import validate, CollisionError

try:
    validate(show)              # default safety distance: 20 cm
    validate(show, safety=0.5)  # custom distance in metres
except CollisionError as e:
    print(e)

FlightRunner and DryRunRunner both call validate automatically before running.


Generating shows from natural language (AI parser)

The centrepiece of flaight: describe a drone show in plain words — in any language — and a local language model (via Ollama) translates that description directly into flaight objects. No exec(), no generated code being run: the model returns structured JSON that flaight safely converts into a Show.

There are two modes of input:

Mode What you provide What the model does
Direct (parse) Explicit choreography instructions Follows them literally — no added creativity
Thematic (parse_thematic) A theme, mood, or emotion Freely designs formations, movement arcs, and a colour palette to express it

Prerequisites

Ollama must be running locally with a model pulled, e.g.:

ollama pull llama3.2
ollama serve

Mode 1 — direct choreography

Describe exactly what the drones should do. The model follows your instructions without adding anything not asked for.

from flaight import DryRunRunner, ShowParser

parser = ShowParser(host="http://localhost:11434", model="llama3.2")

show = parser.parse(
    "Two drones take off, fly apart to opposite sides, alternate red and blue, then land.",
    uris=["radio://0/80/2M/E7E7E7E701", "radio://0/80/2M/E7E7E7E702"],
)
DryRunRunner(show).run()

Mode 2 — thematic / emotional

Describe a theme, mood, or feeling. The model acts as a creative choreographer: it picks formations, movement arcs, altitude dynamics, and a colour palette that express the theme.

show = parser.parse_thematic(
    "Melancholy — a lone farewell slowly fading into darkness.",
    uris=["radio://0/80/2M/E7E7E7E701", "radio://0/80/2M/E7E7E7E702",
          "radio://0/80/2M/E7E7E7E703"],
)
DryRunRunner(show, realtime=True).run()

Other examples of thematic prompts:

  • "Euphoria — an explosion of energy and colour"
  • "A thunderstorm building and breaking"
  • "Sunrise over the mountains"
  • "Tense standoff, then sudden release"

Summary before generating

Before building the full choreography, ask the model for a plain-text preview — useful to check that the description was understood or that the creative interpretation fits your vision:

# Mode 1 — summarizes the explicit instructions
summary = parser.summarize(description, uris=uris)

# Mode 2 — describes the creative concept the model would build
summary = parser.summarize_thematic(description, uris=uris)

print(summary)

Interactive session (REPL)

run_interactive starts a loop: enter a description → confirm the summary → generate the show → simulate or fly → next show.

parser.run_interactive(
    uris=["radio://0/80/2M/E7E7E7E701", "radio://0/80/2M/E7E7E7E702"],
    fly=False,       # True → real hardware instead of dry-run
    realtime=True,   # play dry-run in real time
)

Saving generated shows as scripts

Any AI-generated show can be exported as a standalone Python script — for archiving, manual editing, or re-running later:

from flaight import save_script
path = save_script(show)           # → ai_generated/show_<timestamp>.py
print(f"Saved: {path}")

The saved script is fully self-contained and supports the same --fly / --realtime flags as the bundled examples.


Exporting shows as scripts

Any Show — whether hand-crafted or AI-generated — can be exported as a standalone Python script:

from flaight import save_script, to_script

print(to_script(show))                        # print to stdout
path = save_script(show)                      # ai_generated/show_<timestamp>.py
path = save_script(show, output_dir="shows")  # custom directory

Coordinate system

  • Origin (0, 0, 0) is the centre of the flying area.
  • x/y range: roughly −2 m to +2 m.
  • z = 0 is the floor; typical flying height is 1.0 m.
  • Yaw is in degrees; 0° faces the positive x direction.

CLI reference

flaight — AI parser (installed command)

After pip install -e . the flaight command is available directly in your shell — no script to create.

At the start of each iteration you choose between the two input modes:

  [1] Describe the choreography directly
  [2] Describe a theme / emotion — AI designs the show

Mode 1 follows your instructions literally; mode 2 lets the model act as a creative choreographer and design formations, movement arcs, and a colour palette from a theme or emotion. After choosing a mode you enter a description, review a plain-text summary, confirm, and the show is generated and run.

# Basic interactive session
flaight

# Custom Ollama host and model
flaight --host http://localhost:11434 --model llama3.2

# Remote Ollama server on the local network
flaight --host http://192.168.1.10:11434 --model mistral

# Three drones with explicit URIs
flaight --uris radio://0/80/2M/E7E7E7E701 radio://0/80/2M/E7E7E7E702 radio://0/80/2M/E7E7E7E703

# Real-time dry-run
flaight --realtime

# Fly for real
flaight --fly

# Print raw LLM response for debugging
flaight --verbose
Flag Default Description
--host http://localhost:11434 Ollama server URL
--model llama3.2 Ollama model name
--uris two placeholder URIs Ordered list of Crazyflie URIs
--realtime off Play dry-run in real time
--fly off Run on real hardware
--verbose off Log raw LLM JSON response
--timeout 300 Ollama request timeout in seconds

Example scripts

The examples/ directory contains additional runnable scripts. All share a common set of flags:

Flag Description
(none) Dry-run: print all events instantly, no hardware needed
--realtime Dry-run with real-time delays (sleeps between events)
--fly Execute on real Crazyflie hardware via Crazyradio

examples/two_drones.py — hand-coded show: synchronized takeoff, mirrored movement, cross-fading colors.

python examples/two_drones.py
python examples/two_drones.py --realtime
python examples/two_drones.py --fly

examples/color_test.py — cycles both drones through eight colors using FadeColor, with a 1-second wave offset. Drones stay on the ground.

python examples/color_test.py
python examples/color_test.py --realtime
python examples/color_test.py --fly

examples/ai_show.py — generates two shows programmatically: one from explicit instructions (direct mode) and one from a theme (thematic mode). Each result is saved as a script via save_script().

python examples/ai_show.py
python examples/ai_show.py --realtime
python examples/ai_show.py --fly

Metadata

Release files for flaight 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flaight 0.1.0
File Size Uploaded
flaight-0.1.0.tar.gz 28.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flaight 0.1.0
File Interpreter ABI Platform
flaight-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 51.9 kB

Release files / flaight-0.1.0.tar.gz

Download URL flaight-0.1.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c599e61af1925d5c9fc21d27dd94e074b4c5a992bce463cb026c3dbd7be0e779
BLAKE2b-256 checksum
How to use checksums
b114dfa57c5849615aa1b6dd9331f5bf0ea0dc8cb7bbfb1e6f4f3da69ee15cbb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / flaight-0.1.0-py3-none-any.whl

Download URL flaight-0.1.0-py3-none-any.whl
Size 23.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4b9ca9abcb4d3250ba933858e7dfa944ba42d39195b2acd3016bf0a2c1c30d8f
BLAKE2b-256 checksum
How to use checksums
2591dca62aa84b819ba6effbc3f3e80c16331df4f3a087a8196fc5deae7f9342
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page