Drone-Sim / app.py
workbykait's picture
Update app.py
692cdfb verified
import gradio as gr
import numpy as np
import plotly.graph_objects as go
import pandas as pd # ← Explicit early import fixes Plotly's pandas validator crash
# ====================== DRONE SIMULATOR CLASS ======================
class DroneSimulator:
def __init__(self):
self.reset()
def reset(self):
self.x = self.y = self.z = 0.0
self.vx = self.vy = self.vz = 0.0
self.roll = self.pitch = self.yaw = 0.0
self.battery = 100.0
self.armed = False
self.mode = "DISARMED"
self.waypoints = []
self.current_wp = 0
self.sim_time = 0.0
self.history = {'t': [], 'x': [], 'y': [], 'z': [], 'battery': []}
self.logs = ["[0.0s] Ground station online – High-reliability autonomy demo ready."]
self.link_quality = 95.0
self.gps_ok = True
self.home = (0.0, 0.0, 0.0)
self.failsafe_active = False
def add_log(self, msg):
ts = f"[{self.sim_time:.1f}s]"
self.logs.append(f"{ts} {msg}")
if len(self.logs) > 50: self.logs = self.logs[-50:]
def arm(self):
if not self.armed:
self.armed = True
self.mode = "STABILIZE"
self.add_log("DRONE ARMED – Stabilize mode active")
return True
return False
def disarm(self):
self.armed = False
self.mode = "DISARMED"
self.vx = self.vy = self.vz = 0.0
self.add_log("DRONE DISARMED")
def load_waypoints(self, text):
if not text or not text.strip():
self.waypoints = []
self.add_log("Waypoints cleared")
return True
try:
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
self.waypoints = [tuple(float(p.strip()) for p in line.split(',')) for line in lines]
self.current_wp = 0
self.add_log(f"βœ… Loaded {len(self.waypoints)} waypoints")
return True
except:
self.add_log("❌ Invalid format – use x,y,z one per line")
return False
def update(self, dt=0.5):
if not self.armed:
self.z = max(0.0, self.z - dt)
self.sim_time += dt
self.record_history()
return
self.sim_time += dt
self.battery = max(0.0, self.battery - (0.35 + 0.08 * (abs(self.vx) + abs(self.vy) + abs(self.vz))) * dt)
self.link_quality = max(10.0, self.link_quality - np.random.normal(0, 0.08))
self.check_failsafes()
target_z = self.z
tvx = tvy = 0.0
if self.mode == "TAKEOFF":
target_z = 15.0
if abs(self.z - 15.0) < 1.5:
self.mode = "AUTO" if self.waypoints else "STABILIZE"
self.add_log("Takeoff complete β†’ autonomous mode")
elif self.mode == "LAND":
target_z = 0.0
if self.z < 0.5:
self.disarm()
self.add_log("Safe landing confirmed")
elif self.mode == "RTL":
tx, ty, tz = self.home
dx = tx - self.x; dy = ty - self.y
dist = (dx**2 + dy**2)**0.5
if dist < 3.0 and abs(self.z - tz) < 2.0:
self.mode = "LAND"
else:
speed = 7.0
if dist > 0:
tvx = (dx / dist) * speed
tvy = (dy / dist) * speed
target_z = tz
elif self.mode == "AUTO" and self.waypoints and self.current_wp < len(self.waypoints):
tx, ty, tz = self.waypoints[self.current_wp]
dx = tx - self.x; dy = ty - self.y; dz = tz - self.z
dist = (dx**2 + dy**2 + dz**2)**0.5
if dist < 3.0:
self.current_wp += 1
self.add_log(f"Waypoint {self.current_wp-1} reached")
if self.current_wp >= len(self.waypoints):
self.mode = "RTL"
self.add_log("Mission complete – RTL initiated")
else:
speed = 8.0
if dist > 0:
tvx = (dx / dist) * speed
tvy = (dy / dist) * speed
target_z = tz
self.vx += (tvx - self.vx) * 1.2 * dt
self.vy += (tvy - self.vy) * 1.2 * dt
self.vz += ((target_z - self.z) * 2.0 - self.vz) * 0.9 * dt
self.x += self.vx * dt
self.y += self.vy * dt
self.z = max(0.0, self.z + self.vz * dt)
self.roll = np.clip(self.vy * 3, -30, 30)
self.pitch = np.clip(-self.vx * 3, -30, 30)
self.record_history()
if int(self.sim_time) % 2 == 0:
self.add_log(f"TX Telemetry β†’ Ground (alt={self.z:.1f}m, batt={self.battery:.0f}%)")
def check_failsafes(self):
if self.battery < 25 and self.mode not in ["RTL", "LAND"]:
self.mode = "RTL"; self.failsafe_active = True
self.add_log("*** CRITICAL FAILSAFE: Low battery β†’ RTL ACTIVATED ***")
if self.link_quality < 25 and self.mode not in ["RTL", "LAND"]:
self.mode = "LAND"
self.add_log("*** CRITICAL FAILSAFE: Link loss β†’ Emergency land ***")
if not self.gps_ok and self.mode == "AUTO":
self.mode = "RTL"
self.add_log("*** FAILSAFE: GPS failure β†’ RTL ***")
def record_history(self):
self.history['t'].append(self.sim_time)
self.history['x'].append(self.x)
self.history['y'].append(self.y)
self.history['z'].append(self.z)
self.history['battery'].append(self.battery)
if len(self.history['t']) > 300:
for k in self.history: self.history[k] = self.history[k][-250:]
def trigger_failure(self, ftype):
if ftype == "low_battery": self.battery = 18.0
elif ftype == "gps_loss": self.gps_ok = False
elif ftype == "comm_loss": self.link_quality = 12.0
elif ftype == "imu":
self.roll = np.random.normal(0, 18)
self.pitch = np.random.normal(0, 18)
self.add_log(f"SIMULATED FAILURE: {ftype.replace('_', ' ').upper()}")
self.check_failsafes()
# ====================== PLOTS ======================
def gauge(value, title, maxv=100, suffix="", color="#00ffcc"):
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=value,
title={"text": title, "font": {"size": 18, "color": "#f8f8f8"}},
gauge={"axis": {"range": [0, maxv], "tickcolor": "#f8f8f8"},
"bar": {"color": color},
"steps": [{"range": [0, maxv*0.3], "color": "#ff3333"},
{"range": [maxv*0.3, maxv*0.7], "color": "#ffaa00"},
{"range": [maxv*0.7, maxv], "color": "#00ff88"}]},
number={"font": {"color": "#f8f8f8", "size": 28}}
))
fig.update_layout(height=210, margin=dict(l=10,r=10,t=50,b=10), paper_bgcolor="#0d0d0d")
return fig
def path_plot(d):
fig = go.Figure()
if d.history['x']:
fig.add_trace(go.Scatter(x=d.history['x'], y=d.history['y'], mode='lines',
line=dict(color='#00ffcc', width=3), name='Path'))
fig.add_trace(go.Scatter(x=[d.x], y=[d.y], mode='markers+text',
marker=dict(size=20, color='#ff00aa', symbol='star'),
text=["🚁"], textposition="top center", name='Live'))
pad = 8 if d.history['x'] else 25
if d.history['x']:
minx = min(d.history['x']) - pad
maxx = max(d.history['x']) + pad
miny = min(d.history['y']) - pad
maxy = max(d.history['y']) + pad
else:
minx = maxx = miny = maxy = 0
minx -= pad; maxx += pad; miny -= pad; maxy += pad
fig.update_layout(
title="Top-Down Flight Map (X-Y)",
xaxis_title="East (m)", yaxis_title="North (m)",
height=460, plot_bgcolor="#111111", paper_bgcolor="#0d0d0d",
font_color="#f8f8f8",
xaxis=dict(range=[minx, maxx], gridcolor="#333333", zerolinecolor="#555555"),
yaxis=dict(range=[miny, maxy], gridcolor="#333333", zerolinecolor="#555555"),
)
return fig
def alt_plot(d):
fig = go.Figure(go.Scatter(x=d.history['t'], y=d.history['z'], mode='lines', line=dict(color='#ffaa00', width=3)))
fig.update_layout(title="Altitude History", xaxis_title="Time (s)", yaxis_title="Altitude (m)",
height=260, plot_bgcolor="#111111", paper_bgcolor="#0d0d0d", font_color="#f8f8f8")
return fig
def batt_plot(d):
fig = go.Figure(go.Scatter(x=d.history['t'], y=d.history['battery'], mode='lines', line=dict(color='#00ccff', width=3)))
fig.update_layout(title="Battery History", xaxis_title="Time (s)", yaxis_title="Battery %",
height=260, plot_bgcolor="#111111", paper_bgcolor="#0d0d0d", font_color="#f8f8f8")
return fig
# ====================== CUSTOM CSS (2px radius + premium dark) ======================
custom_css = """
.gradio-container { background-color: #0d0d0d !important; color: #f8f8f8 !important; }
.gr-button, .gr-textbox textarea, .gr-plot, .gr-textbox, .gr-tab {
border-radius: 2px !important;
border: 1px solid #333333 !important;
transition: all 0.2s;
}
.gr-button.primary { background-color: #ff5e00 !important; color: #f8f8f8 !important; }
.gr-button.secondary { background-color: #333333 !important; color: #f8f8f8 !important; }
.gr-button:hover { filter: brightness(1.15); }
.gr-markdown h1, .gr-markdown h2, .gr-markdown p { color: #f8f8f8 !important; }
"""
# ====================== DARK THEME ======================
dark_theme = gr.themes.Default().set(
body_background_fill="#0d0d0d",
body_text_color="#f8f8f8",
panel_background_fill="#1a1a1a",
block_background_fill="#1a1a1a",
block_border_color="#333333",
button_primary_background_fill="#ff5e00",
button_primary_background_fill_hover="#ff7a33",
button_secondary_background_fill="#333333",
button_secondary_background_fill_hover="#444444",
input_background_fill="#222222",
input_border_color="#444444"
)
# ====================== GRADIO UI ======================
with gr.Blocks(title="AeroGuard – High-Reliability Autonomous Drone Simulator") as demo:
gr.Markdown("# 🚁 AeroGuard – High-Reliability Autonomous Drone Simulator\n"
"**Real-time telemetry β€’ Autonomous waypoint navigation β€’ Layered fail-safes β€’ Ground-station comms**")
sim_state = gr.State(DroneSimulator())
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Ground Station Controls")
with gr.Row():
arm_btn = gr.Button("ARM", variant="primary", size="large")
disarm_btn = gr.Button("DISARM", size="large")
with gr.Row():
takeoff_btn = gr.Button("TAKEOFF", variant="primary")
land_btn = gr.Button("LAND")
rtl_btn = gr.Button("RTL")
emergency_btn = gr.Button("🚨 EMERGENCY STOP", variant="stop", size="large")
gr.Markdown("### Waypoints (x,y,z one per line)")
demo_value = "0,0,15\n30,0,20\n30,30,15\n0,30,10"
wp_box = gr.Textbox(lines=6, value=demo_value, placeholder="0,0,15\n30,0,20...")
with gr.Row():
load_btn = gr.Button("Load Mission", variant="secondary")
demo_btn = gr.Button("Load Demo Mission", variant="secondary")
with gr.Column(scale=2):
gr.Markdown("### Live Telemetry")
with gr.Row():
alt_g = gr.Plot(label="Altitude")
batt_g = gr.Plot(label="Battery")
link_g = gr.Plot(label="Comms Link")
with gr.Row():
status = gr.Textbox(label="Status", interactive=False)
position = gr.Textbox(label="Position (m)", interactive=False)
attitude = gr.Textbox(label="Attitude (Β°)", interactive=False)
path = gr.Plot(label="Flight Map")
with gr.Tabs():
with gr.Tab("Plots"):
with gr.Row():
alt_h = gr.Plot()
batt_h = gr.Plot()
with gr.Tab("Ground Station Log"):
log_box = gr.Textbox(lines=18, autoscroll=True, interactive=False)
with gr.Tab("Fail-Safe Tester"):
gr.Markdown("**Inject failures to see autonomous response**")
with gr.Row():
low_batt_btn = gr.Button("Low Battery")
gps_btn = gr.Button("GPS Loss")
comms_btn = gr.Button("Comms Loss")
imu_btn = gr.Button("IMU Drift")
with gr.Row():
advance_btn = gr.Button("Advance 1 s", variant="primary")
auto_btn = gr.Button("Auto Simulate 10 s", variant="primary")
reset_btn = gr.Button("Reset", variant="secondary")
def ui_outputs():
return [alt_g, batt_g, link_g, status, position, attitude, path, alt_h, batt_h, log_box]
def update_ui(s):
return (
gauge(s.z, "Altitude", 50, " m", "#ffaa00"),
gauge(s.battery, "Battery", 100, " %", "#00ccff"),
gauge(s.link_quality, "Link", 100, " %", "#00ffcc"),
f"{s.mode} | Armed: {s.armed} | Failsafe: {s.failsafe_active}",
f"{s.x:.1f}, {s.y:.1f}, {s.z:.1f}",
f"Roll {s.roll:.1f}Β° / Pitch {s.pitch:.1f}Β° / Yaw {s.yaw:.1f}Β°",
path_plot(s), alt_plot(s), batt_plot(s),
"\n".join(s.logs)
)
# ====================== EVENTS ======================
arm_btn.click(lambda s: (s if s.arm() else s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
disarm_btn.click(lambda s: (s if s.disarm() else s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
takeoff_btn.click(lambda s: (setattr(s, 'mode', 'TAKEOFF') or s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
land_btn.click(lambda s: (setattr(s, 'mode', 'LAND') or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
rtl_btn.click(lambda s: (setattr(s, 'mode', 'RTL') or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
emergency_btn.click(lambda s: (s if s.disarm() else s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
load_btn.click(lambda s, txt: (s if s.load_waypoints(txt) else s, *update_ui(s)), inputs=[sim_state, wp_box], outputs=[sim_state] + ui_outputs())
demo_btn.click(lambda s: (s if s.load_waypoints(demo_value) else s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
low_batt_btn.click(lambda s: (s.trigger_failure("low_battery") or s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
gps_btn.click(lambda s: (s.trigger_failure("gps_loss") or s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
comms_btn.click(lambda s: (s.trigger_failure("comm_loss") or s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
imu_btn.click(lambda s: (s.trigger_failure("imu") or s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
advance_btn.click(lambda s: (s.update() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
auto_btn.click(lambda s: ([s.update() for _ in range(20)] or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
reset_btn.click(lambda s: (s.reset() or s, *update_ui(s)), inputs=sim_state, outputs=[sim_state] + ui_outputs())
demo.load(lambda s: update_ui(s), inputs=sim_state, outputs=ui_outputs())
# ====================== LAUNCH (Gradio 6+ compatible) ======================
if __name__ == "__main__":
demo.launch(
theme=dark_theme,
css=custom_css,
server_name="0.0.0.0",
server_port=7860
)