Skip to content

Commit a665ad1

Browse files
committed
Added missing tests and more examples.
1 parent c460127 commit a665ad1

17 files changed

Lines changed: 1132 additions & 3 deletions

examples/custom_color.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Custom RGB colors: shows that Color() accepts arbitrary r/g/b values (0–255).
3+
4+
Two drones cycle through a warm sunset palette built from custom colors that
5+
have no named preset in flaight — demonstrating that the LED is not limited
6+
to the built-in constants.
7+
8+
Usage:
9+
python examples/custom_color.py # dry-run (instant)
10+
python examples/custom_color.py --realtime # dry-run, real-time pacing
11+
python examples/custom_color.py --fly # real hardware
12+
"""
13+
from flaight import Color, FadeColor, Hover, Land, SetColor, Show, Takeoff
14+
15+
URIS = [
16+
"radio://0/80/2M/E7E7E7E7E7",
17+
"radio://0/81/2M/E7E7E7E7E7",
18+
]
19+
20+
# Custom palette — none of these are named presets
21+
SUNSET = [
22+
Color(r=255, g=20, b=0), # deep red
23+
Color(r=255, g=80, b=10), # burnt orange
24+
Color(r=255, g=140, b=0), # dark amber
25+
Color(r=255, g=200, b=30), # golden yellow
26+
Color(r=255, g=120, b=60), # peach
27+
Color(r=200, g=40, b=80), # crimson rose
28+
Color(r=120, g=0, b=60), # deep violet
29+
]
30+
31+
FADE = 1.8 # seconds per color transition
32+
HOLD = 0.4 # seconds to hold between fades
33+
34+
show = Show()
35+
d1 = show.add_drone("cf1", uri=URIS[0])
36+
d2 = show.add_drone("cf2", uri=URIS[1])
37+
38+
for drone, offset in ((d1, 0.0), (d2, FADE + HOLD)):
39+
drone.at(0.0, Takeoff(height=1.0, duration=2.0))
40+
drone.at(0.0, SetColor(Color.OFF))
41+
42+
total = FADE * len(SUNSET) + HOLD * (len(SUNSET) - 1)
43+
drone.at(2.5, Hover(duration=total + offset))
44+
45+
t = 2.5 + offset
46+
for color in SUNSET:
47+
drone.at(t, FadeColor(color=color, duration=FADE))
48+
t += FADE + HOLD
49+
50+
drone.at(t, FadeColor(color=Color.OFF, duration=FADE))
51+
drone.at(t + FADE, Land(duration=2.0))
52+
53+
if __name__ == "__main__":
54+
import sys
55+
if "--fly" in sys.argv:
56+
from flaight import FlightRunner
57+
FlightRunner(show).run()
58+
else:
59+
from flaight import DryRunRunner
60+
DryRunRunner(show, realtime="--realtime" in sys.argv).run()

examples/export_script.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Export demo: build a show programmatically, then render it as a standalone script.
3+
4+
Two export helpers are shown:
5+
- to_script(show) → returns the script as a string (useful for previewing)
6+
- save_script(show) → writes it to ai_generated/ and returns the path
7+
8+
The exported script is self-contained and can be run directly with Python.
9+
10+
Usage:
11+
python examples/export_script.py
12+
"""
13+
from flaight import Color, FadeColor, Hover, Land, MoveTo, SetColor, Show, Takeoff, save_script, to_script
14+
15+
URIS = [
16+
"radio://0/80/2M/E7E7E7E7E7",
17+
"radio://0/81/2M/E7E7E7E7E7",
18+
]
19+
20+
21+
def build_show() -> Show:
22+
show = Show()
23+
d1 = show.add_drone("cf1", uri=URIS[0])
24+
d2 = show.add_drone("cf2", uri=URIS[1])
25+
26+
d1.at(0.0, Takeoff(height=1.0, duration=2.0))
27+
d1.at(0.0, SetColor(Color.GREEN))
28+
d1.at(2.5, MoveTo(x=-0.5, y=0.0, z=1.2, duration=2.0))
29+
d1.at(4.5, FadeColor(color=Color.CYAN, duration=1.5))
30+
d1.at(6.0, Hover(duration=1.5))
31+
d1.at(7.5, Land(duration=2.0))
32+
d1.at(7.5, SetColor(Color.OFF))
33+
34+
d2.at(0.0, Takeoff(height=1.0, duration=2.0))
35+
d2.at(0.0, SetColor(Color.MAGENTA))
36+
d2.at(2.5, MoveTo(x=0.5, y=0.0, z=1.2, duration=2.0))
37+
d2.at(4.5, FadeColor(color=Color.PURPLE, duration=1.5))
38+
d2.at(6.0, Hover(duration=1.5))
39+
d2.at(7.5, Land(duration=2.0))
40+
d2.at(7.5, SetColor(Color.OFF))
41+
42+
return show
43+
44+
45+
if __name__ == "__main__":
46+
show = build_show()
47+
48+
print("=== Generated script ===\n")
49+
print(to_script(show))
50+
51+
path = save_script(show)
52+
print(f"Script saved to: {path}")
53+
print("Run it directly with: python", path)

examples/figure_eight.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Figure-eight flight path: one drone traces a lemniscate in the horizontal plane.
3+
4+
Waypoints are sampled from the parametric curve
5+
x = 0.8 * sin(t), y = 0.6 * sin(2t)
6+
which produces a smooth figure-8 within the recommended ±1 m flight area.
7+
8+
Usage:
9+
python examples/figure_eight.py # dry-run (instant)
10+
python examples/figure_eight.py --realtime # dry-run, real-time pacing
11+
python examples/figure_eight.py --fly # real hardware
12+
"""
13+
import math
14+
15+
from flaight import Color, FadeColor, Land, MoveTo, SetColor, Show, Takeoff
16+
17+
URI = "radio://0/80/2M/E7E7E7E7E7"
18+
19+
# Lemniscate waypoints — 8 points + closing return to origin
20+
_N = 8
21+
_WAYPOINTS = [
22+
(round(0.8 * math.sin(2 * math.pi * i / _N), 2),
23+
round(0.6 * math.sin(4 * math.pi * i / _N), 2))
24+
for i in range(_N + 1)
25+
]
26+
27+
# Color wheel to cycle through during the flight
28+
_COLORS = [
29+
Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN,
30+
Color.CYAN, Color.BLUE, Color.PURPLE, Color.MAGENTA,
31+
]
32+
33+
LEG_DURATION = 2.0 # seconds per waypoint segment
34+
HEIGHT = 1.2
35+
36+
show = Show()
37+
d1 = show.add_drone("cf1", uri=URI)
38+
39+
d1.at(0.0, Takeoff(height=HEIGHT, duration=2.0))
40+
d1.at(0.0, SetColor(Color.RED))
41+
42+
# Fly to start position (first waypoint is the origin, so go straight to index 1)
43+
t = 2.5
44+
for i, (x, y) in enumerate(_WAYPOINTS[1:], start=1):
45+
d1.at(t, MoveTo(x=x, y=y, z=HEIGHT, duration=LEG_DURATION))
46+
d1.at(t, FadeColor(color=_COLORS[i % len(_COLORS)], duration=LEG_DURATION * 0.8))
47+
t += LEG_DURATION
48+
49+
d1.at(t, Land(duration=2.0))
50+
d1.at(t, SetColor(Color.OFF))
51+
52+
if __name__ == "__main__":
53+
import sys
54+
if "--fly" in sys.argv:
55+
from flaight import FlightRunner
56+
FlightRunner(show).run()
57+
else:
58+
from flaight import DryRunRunner
59+
DryRunRunner(show, realtime="--realtime" in sys.argv).run()

examples/pulse.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Breathing-light effect: drone hovers while the LED pulses between a color and off.
3+
4+
Two drones pulse in antiphase so the overall light breathes like a heartbeat.
5+
6+
Usage:
7+
python examples/pulse.py # dry-run (instant)
8+
python examples/pulse.py --realtime # dry-run, real-time pacing
9+
python examples/pulse.py --fly # real hardware
10+
"""
11+
from flaight import Color, FadeColor, Hover, Land, SetColor, Show, Takeoff
12+
13+
URIS = [
14+
"radio://0/80/2M/E7E7E7E7E7",
15+
"radio://0/81/2M/E7E7E7E7E7",
16+
]
17+
18+
PULSE_COLOR = Color.WHITE
19+
PULSE_DURATION = 1.2 # seconds for one fade-in or fade-out
20+
PULSES = 6 # number of on/off cycles per drone
21+
HOVER_START = 2.5 # when pulsing begins (after takeoff)
22+
23+
show = Show()
24+
d1 = show.add_drone("cf1", uri=URIS[0])
25+
d2 = show.add_drone("cf2", uri=URIS[1])
26+
27+
total_pulse_time = PULSES * 2 * PULSE_DURATION
28+
29+
for drone, phase_offset in ((d1, 0.0), (d2, PULSE_DURATION)):
30+
drone.at(0.0, Takeoff(height=1.0, duration=2.0))
31+
drone.at(0.0, SetColor(Color.OFF))
32+
drone.at(HOVER_START, Hover(duration=total_pulse_time + phase_offset))
33+
34+
t = HOVER_START + phase_offset
35+
for _ in range(PULSES):
36+
drone.at(t, FadeColor(color=PULSE_COLOR, duration=PULSE_DURATION))
37+
t += PULSE_DURATION
38+
drone.at(t, FadeColor(color=Color.OFF, duration=PULSE_DURATION))
39+
t += PULSE_DURATION
40+
41+
land_t = HOVER_START + phase_offset + total_pulse_time
42+
drone.at(land_t, Land(duration=2.0))
43+
44+
if __name__ == "__main__":
45+
import sys
46+
if "--fly" in sys.argv:
47+
from flaight import FlightRunner
48+
FlightRunner(show).run()
49+
else:
50+
from flaight import DryRunRunner
51+
DryRunRunner(show, realtime="--realtime" in sys.argv).run()

examples/single_drone.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Minimal single-drone show: takeoff, hover, land.
3+
4+
The simplest possible flaight program — a good starting point.
5+
6+
Usage:
7+
python examples/single_drone.py # dry-run (instant)
8+
python examples/single_drone.py --realtime # dry-run, real-time pacing
9+
python examples/single_drone.py --fly # real hardware
10+
"""
11+
from flaight import Color, FadeColor, Hover, Land, SetColor, Show, Takeoff
12+
13+
URI = "radio://0/80/2M/E7E7E7E7E7"
14+
15+
show = Show()
16+
d1 = show.add_drone("cf1", uri=URI)
17+
18+
d1.at(0.0, Takeoff(height=1.0, duration=2.0))
19+
d1.at(0.0, SetColor(Color.RED))
20+
21+
d1.at(2.5, FadeColor(color=Color.BLUE, duration=2.0))
22+
d1.at(4.5, Hover(duration=2.0))
23+
24+
d1.at(6.5, Land(duration=2.0))
25+
d1.at(6.5, SetColor(Color.OFF))
26+
27+
if __name__ == "__main__":
28+
import sys
29+
if "--fly" in sys.argv:
30+
from flaight import FlightRunner
31+
FlightRunner(show).run()
32+
else:
33+
from flaight import DryRunRunner
34+
DryRunRunner(show, realtime="--realtime" in sys.argv).run()

examples/validate_collision.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Collision-detection demo: shows how validate() catches unsafe shows before flight.
3+
4+
Two scenarios are run:
5+
1. Unsafe — both drones fly to the same point → CollisionError is raised and caught.
6+
2. Safe — drones fly to opposite sides → validate() passes silently.
7+
8+
No hardware required; validation runs entirely in Python.
9+
"""
10+
from flaight import CollisionError, Color, Land, MoveTo, SetColor, Show, Takeoff, validate
11+
12+
13+
def unsafe_show() -> Show:
14+
show = Show()
15+
d1 = show.add_drone("cf1", uri="radio://0/80/2M/E7E7E7E7E7")
16+
d2 = show.add_drone("cf2", uri="radio://0/81/2M/E7E7E7E7E7")
17+
18+
for drone in (d1, d2):
19+
drone.at(0.0, Takeoff(height=1.0, duration=2.0))
20+
# Both drones converge on the exact same point
21+
drone.at(2.0, MoveTo(x=0.0, y=0.0, z=1.0, duration=3.0))
22+
drone.at(5.0, Land(duration=2.0))
23+
24+
return show
25+
26+
27+
def safe_show() -> Show:
28+
show = Show()
29+
d1 = show.add_drone("cf1", uri="radio://0/80/2M/E7E7E7E7E7")
30+
d2 = show.add_drone("cf2", uri="radio://0/81/2M/E7E7E7E7E7")
31+
32+
d1.at(0.0, Takeoff(height=1.0, duration=2.0))
33+
d1.at(0.0, SetColor(Color.BLUE))
34+
d1.at(2.0, MoveTo(x=-0.8, y=0.0, z=1.0, duration=3.0))
35+
d1.at(5.0, Land(duration=2.0))
36+
37+
d2.at(0.0, Takeoff(height=1.0, duration=2.0))
38+
d2.at(0.0, SetColor(Color.RED))
39+
d2.at(2.0, MoveTo(x=0.8, y=0.0, z=1.0, duration=3.0))
40+
d2.at(5.0, Land(duration=2.0))
41+
42+
return show
43+
44+
45+
if __name__ == "__main__":
46+
print("--- Scenario 1: unsafe show (drones collide) ---")
47+
try:
48+
validate(unsafe_show())
49+
print("No collision detected.")
50+
except CollisionError as exc:
51+
print(f"CollisionError: {exc}\n")
52+
53+
print("--- Scenario 2: safe show (drones separated) ---")
54+
try:
55+
validate(safe_show())
56+
print("Validation passed — show is safe to fly.")
57+
except CollisionError as exc:
58+
print(f"CollisionError: {exc}")

flaight/validate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ def validate(show: Show, safety: float = SAFETY_DISTANCE) -> None:
105105
dist = math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2)))
106106
if dist < safety:
107107
raise CollisionError(
108-
f"Kollisionsgefahr bei t={t:.2f}s: {d1} und {d2} sind "
109-
f"nur {dist * 100:.0f} cm voneinander entfernt "
110-
f"(Sicherheitsabstand: {safety * 100:.0f} cm).\n"
108+
f"Collision risk at t={t:.2f}s: {d1} and {d2} are "
109+
f"only {dist * 100:.0f} cm apart "
110+
f"(safety distance: {safety * 100:.0f} cm).\n"
111111
f" {d1}: ({p1[0]:.2f}, {p1[1]:.2f}, {p1[2]:.2f}) m\n"
112112
f" {d2}: ({p2[0]:.2f}, {p2[1]:.2f}, {p2[2]:.2f}) m"
113113
)

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import sys
2+
from pathlib import Path
3+
4+
# Make the examples/ directory importable so test_example_*.py can do
5+
# "import single_drone" etc.
6+
sys.path.insert(0, str(Path(__file__).parent.parent / "examples"))

0 commit comments

Comments
 (0)