Metadata-Version: 2.4
Name: flaight
Version: 0.1.0
Summary: Crazyflie drone light show choreography library
Project-URL: Homepage, https://flaight.dev
Project-URL: Repository, https://github.com/donework/flaight
Project-URL: Issues, https://github.com/donework/flaight/issues
Author-email: Dominic Neuburg <dom@flaight.dev>
License: MIT
License-File: LICENSE
Keywords: ai,choreography,crazyflie,drone,drone show,light show,llm,ollama
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: cflib>=0.1.26
Requires-Dist: ollama>=0.3
Provides-Extra: dev
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
Requires-Dist: mkdocs>=1.6; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# flaight

**light → flight → flaight.**

By [Dominic Neuburg](mailto:dom@flaight.dev) — MIT License

[![PyPI version](https://img.shields.io/pypi/v/flaight)](https://pypi.org/project/flaight/)
[![Python versions](https://img.shields.io/pypi/pyversions/flaight)](https://pypi.org/project/flaight/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/donework/flaight/ci.yml?label=tests)](https://github.com/donework/flaight/actions)


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

```bash
pip install -e .
```

Requires Python 3.10+. Hardware flight requires a Crazyradio USB dongle and [`cflib`](https://github.com/bitcraze/crazyflie-lib-python). The natural-language parser requires a running [Ollama](https://ollama.com) 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

```python
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:

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

Fly for real:

```python
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

```python
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

```python
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

```python
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](https://ollama.com)) 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.:

```bash
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.

```python
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.

```python
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:

```python
# 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.

```python
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:

```python
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:

```python
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.

```bash
# 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.

```bash
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.

```bash
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()`.

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