{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ ๋๋ก 5๋ ๊ตฐ์ง ํธ๋ ๋นํ ์๋ฎฌ๋ ์ดํฐ (Google Colab)\n", "### 5-Drone Swarm Formation Simulator (Three.js 3D & Python Backend)\n", "\n", "๋ณธ ๋ ธํธ๋ถ์ Three.js 3D ๊ทธ๋ํฝ ์์ง๊ณผ Python ๋ฐฑ์๋(Starlette + Uvicorn + WebSockets)๋ก ๊ตฌ๋๋๋ **๋๋ก 5๋(๋ฆฌ๋ 1๋ + ์๋งจ 4๋) ๊ตฐ์ง ํธ๋ ๋นํ ์๋ฎฌ๋ ์ดํฐ**๋ฅผ Google Colab ํ๊ฒฝ์์ ์ํด๋ฆญ์ผ๋ก ์คํํ ์ ์๋๋ก ๊ตฌ์ฑ๋์์ต๋๋ค.\n", "\n", "---\n", "\n", "### ๐ ์ฃผ์ ๊ธฐ๋ฅ\n", "- **5๋ ๊ตฐ์ง ๋นํ ์ ์ด**: ๋ฆฌ๋ ๋๋ก (VIPER-01) ๋ง์ฐ์ค ์ ๋ ๋นํ ์ 4๋์ ์๋งจ ๋๋ก (GHOST, PHANTOM, SPECTER, SHADOW)์ด ์ค์๊ฐ ํธ๋ ์ ์ง\n", "- **6๊ฐ์ง ํธ๋ ๋ชจ๋**: V ํธ๋, ๋ค์ด์๋ชฌ๋, ํก๋ ์ผ๋ ฌ, ์ข ๋ ์ง๋ ฌ, ๊ตฐ์ง ์ ํ(Orbit), ์ฌ์ ํธ๋\n", "- **์ค์๊ฐ 60Hz ํ ๋ ๋ฉํธ๋ฆฌ**: WebSocket ์๋ฐฉํฅ ํต์ ๊ธฐ๋ฐ ์์น, ์๋, ๊ณ ๋, ๋ฐฐํฐ๋ฆฌ, ๋ชจํฐ RPM ๋๊ธฐํ\n", "- **2D ์ ์ ๋ ์ด๋ HUD & 3D ๋ ์ด์ ํธ๋ ๋งํฌ**: ๊ตฐ์ง ๊ถค์ ํ์ ๋ฐ ํธ๋ ๋ ์ด์ ์๊ฐํ\n", "- **์นด๋ฉ๋ผ ๋ชจ๋ 4์ข **: ๊ตฐ์ง ๊ถค๋๋ทฐ(1), ๋ฆฌ๋ ์ถ์ ๋ทฐ(2), FPV ์กฐ์ข ์(3), ํธ๋ ํ๋กํ๋ทฐ(4)\n", "- **Web Audio ์ค์๊ฐ ์ฌ์ด๋**: ๋กํฐ RPM ์ฐ๋ ์ฌ์ด๋ ์ ์์ฌ์ด์ \n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1๋จ๊ณ: ํ์ ํจํค์ง ๋ฐ ํฐ๋๋ง ๋๊ตฌ ์ค์น\n", "Starlette, Uvicorn, WebSockets ๋ฑ ๋ฐฑ์๋ ๊ตฌ๋ ํจํค์ง์ Colab ์ธ๋ถ ์ ์์ ์ํ Cloudflared๋ฅผ ์ค์นํฉ๋๋ค." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 1. ํ์ Python ํจํค์ง ์ค์น\n", "!pip install -q starlette uvicorn websockets pyngrok nest_asyncio\n", "\n", "# 2. ๊ณ ์ ๋ฌด๋ฃ ํฐ๋ Cloudflared ๋ฐ์ด๋๋ฆฌ ๋ค์ด๋ก๋ (ํ ํฐ ์์ด ์ธ๋ถ ์ ์ ๊ฐ๋ฅ)\n", "!wget -q -nc https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n", "!dpkg -i cloudflared-linux-amd64.deb > /dev/null 2>&1\n", "print(\"โ ํ๊ฒฝ ์ค๋น ์๋ฃ!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2๋จ๊ณ: ์๋ฎฌ๋ ์ดํฐ ์์ค ์ฝ๋ ์์ฑ (๋จ๋ ์คํ ์ง์)\n", "Colab์ ํ์ํ ๋ชจ๋ ๋ฐฑ์๋ ๋ฐ ์น ํ๋ก ํธ์๋ ํ์ผ(`app.py`, `static/...`)์ ์๋์ผ๋ก ์์ฑํฉ๋๋ค." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "# ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ์์ฑ\n", "os.makedirs(\"static/css\", exist_ok=True)\n", "os.makedirs(\"static/js\", exist_ok=True)\n", "\n", "# app.py ์์ฑ\n", "with open(\"app.py\", \"w\", encoding=\"utf-8\") as f:\n", " f.write('''\"\"\"\nDrone Formation Flight Simulation Server\nReal-time 6-DOF physics and 5-Drone Swarm Formation calculation backend with WebSockets & Starlette.\n\"\"\"\n\nimport asyncio\nimport json\nimport math\nimport time\nimport os\nimport contextlib\nimport sys\nfrom typing import Dict, Any, List\nimport uvicorn\nfrom starlette.applications import Starlette\nfrom starlette.routing import Route, WebSocketRoute, Mount\nfrom starlette.staticfiles import StaticFiles\nfrom starlette.responses import FileResponse, JSONResponse\nfrom starlette.websockets import WebSocket, WebSocketDisconnect\n\n# Directory setup\nBASE_DIR = os.path.dirname(os.path.abspath(__file__))\nSTATIC_DIR = os.path.join(BASE_DIR, \"static\")\n\nos.makedirs(STATIC_DIR, exist_ok=True)\nos.makedirs(os.path.join(STATIC_DIR, \"css\"), exist_ok=True)\nos.makedirs(os.path.join(STATIC_DIR, \"js\"), exist_ok=True)\n\n\nclass Vector3:\n def __init__(self, x=0.0, y=0.0, z=0.0):\n self.x = float(x)\n self.y = float(y)\n self.z = float(z)\n\n def to_dict(self):\n return {\"x\": round(self.x, 3), \"y\": round(self.y, 3), \"z\": round(self.z, 3)}\n\n def dist_to(self, other: 'Vector3') -> float:\n return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2 + (self.z - other.z)**2)\n\n\nclass DroneState:\n def __init__(self, drone_id: str, name: str, callsign: str, initial_pos: Vector3, color: str, is_leader: bool = False):\n self.id = drone_id\n self.name = name\n self.callsign = callsign\n self.pos = initial_pos\n self.target_pos = Vector3(initial_pos.x, initial_pos.y, initial_pos.z)\n self.vel = Vector3(0.0, 0.0, 0.0)\n self.pitch = 0.0 # radians\n self.roll = 0.0 # radians\n self.yaw = 0.0 # radians\n self.rotor_rpm = [5200.0, 5200.0, 5200.0, 5200.0]\n self.battery = 98.5 # percentage\n self.armed = True\n self.color = color\n self.is_leader = is_leader\n self.status = \"AIRBORNE - FORMATION OK\"\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"id\": self.id,\n \"name\": self.name,\n \"callsign\": self.callsign,\n \"position\": self.pos.to_dict(),\n \"target\": self.target_pos.to_dict(),\n \"velocity\": self.vel.to_dict(),\n \"speed\": round(math.sqrt(self.vel.x**2 + self.vel.y**2 + self.vel.z**2) * 3.6, 1), # km/h\n \"rotation\": {\n \"pitch\": round(math.degrees(self.pitch), 1),\n \"roll\": round(math.degrees(self.roll), 1),\n \"yaw\": round(math.degrees(self.yaw), 1)\n },\n \"rotors\": [round(r, 0) for r in self.rotor_rpm],\n \"battery\": round(self.battery, 1),\n \"armed\": self.armed,\n \"color\": self.color,\n \"is_leader\": self.is_leader,\n \"status\": self.status\n }\n\n\nclass FormationEngine:\n \"\"\"Manages 5-Drone swarm flight physics and dynamic formation geometry\"\"\"\n def __init__(self):\n # 5 Drones Configuration\n self.drones: List[DroneState] = [\n DroneState(\"alpha_1\", \"Leader Drone\", \"VIPER-01\", Vector3(0.0, 8.0, 0.0), \"#00f0ff\", is_leader=True),\n DroneState(\"alpha_2\", \"Wingman 1 (R1)\", \"GHOST-02\", Vector3(5.0, 8.0, -5.0), \"#ff0055\"),\n DroneState(\"alpha_3\", \"Wingman 2 (L1)\", \"PHANTOM-03\", Vector3(-5.0, 8.0, -5.0), \"#00ff88\"),\n DroneState(\"alpha_4\", \"Wingman 3 (R2)\", \"SPECTER-04\", Vector3(10.0, 8.0, -10.0),\"#ffb700\"),\n DroneState(\"alpha_5\", \"Wingman 4 (L2)\", \"SHADOW-05\", Vector3(-10.0, 8.0,-10.0),\"#b026ff\")\n ]\n\n self.leader = self.drones[0]\n self.formation_mode = \"V_FORMATION\" # V_FORMATION, SIDE_BY_SIDE, TANDEM, DIAMOND, ORBIT, VERTICAL, ECHELON\n self.formation_distance = 6.0\n self.formation_height_offset = 0.0\n self.speed_multiplier = 1.0\n self.autonomous_mode = False\n self.auto_pattern = \"FIGURE_EIGHT\" # FIGURE_EIGHT, CIRCLE, WAYPOINT_PATROL\n\n self.orbit_angle = 0.0\n self.sim_time = 0.0\n self.clients: List[WebSocket] = []\n\n def set_target(self, x: float, y: float, z: float):\n \"\"\"Set manual flight target for the leader drone\"\"\"\n self.leader.target_pos.x = max(-80.0, min(80.0, float(x)))\n self.leader.target_pos.y = max(1.5, min(40.0, float(y)))\n self.leader.target_pos.z = max(-80.0, min(80.0, float(z)))\n\n def calculate_follower_targets(self) -> List[Vector3]:\n \"\"\"Calculates target 3D coordinates for all 4 wingmen based on formation geometry and leader state\"\"\"\n lx, ly, lz = self.leader.pos.x, self.leader.pos.y, self.leader.pos.z\n yaw = self.leader.yaw\n dist = self.formation_distance\n h_off = self.formation_height_offset\n\n # Unit directional vectors based on leader yaw\n # Forward vector\n fx = math.sin(yaw)\n fz = math.cos(yaw)\n # Right vector\n rx = math.cos(yaw)\n rz = -math.sin(yaw)\n\n targets = []\n\n if self.formation_mode == \"V_FORMATION\":\n # Classic 5-Drone V-Shape\n offsets = [\n # (Right factor, Forward factor, Height factor)\n (1.0, -1.0, 0.0), # Drone 2: Starboard Inner\n (-1.0, -1.0, 0.0), # Drone 3: Port Inner\n (2.0, -2.0, 0.5), # Drone 4: Starboard Outer\n (-2.0, -2.0, 0.5), # Drone 5: Port Outer\n ]\n for r_fac, f_fac, h_fac in offsets:\n tx = lx + (rx * r_fac + fx * f_fac) * dist\n tz = lz + (rz * r_fac + fz * f_fac) * dist\n ty = ly + h_off + (h_fac * h_off)\n targets.append(Vector3(tx, ty, tz))\n\n elif self.formation_mode == \"SIDE_BY_SIDE\":\n # 5-Drone Wide Line Abreast: L2, L1, Leader, R1, R2\n offsets = [\n (1.0, 0.0), # D2: Right 1\n (-1.0, 0.0), # D3: Left 1\n (2.0, 0.0), # D4: Right 2\n (-2.0, 0.0), # D5: Left 2\n ]\n for r_fac, f_fac in offsets:\n tx = lx + (rx * r_fac) * dist\n tz = lz + (rz * r_fac) * dist\n targets.append(Vector3(tx, ly + h_off, tz))\n\n elif self.formation_mode == \"TANDEM\":\n # 5-Drone Trail / Single Column In-Line behind Leader\n offsets = [-1.0, -2.0, -3.0, -4.0]\n for i, f_fac in enumerate(offsets):\n tx = lx + (fx * f_fac) * dist\n tz = lz + (fz * f_fac) * dist\n ty = ly + (h_off * (i + 1) * 0.3)\n targets.append(Vector3(tx, ty, tz))\n\n elif self.formation_mode == \"DIAMOND\":\n # 5-Drone Diamond / Arrowhead formation\n offsets = [\n (1.2, -0.8, 0.0), # D2: Right Wing\n (-1.2, -0.8, 0.0), # D3: Left Wing\n (0.0, -1.6, 0.0), # D4: Center Rear\n (0.0, -2.6, 0.5), # D5: Tail High-Guard\n ]\n for r_fac, f_fac, h_fac in offsets:\n tx = lx + (rx * r_fac + fx * f_fac) * dist\n tz = lz + (rz * r_fac + fz * f_fac) * dist\n ty = ly + h_off + (h_fac * 1.5)\n targets.append(Vector3(tx, ty, tz))\n\n elif self.formation_mode == \"ORBIT\":\n # 4 Wingmen dynamically orbiting around Leader at 90-degree spacing\n for i in range(4):\n angle = self.orbit_angle + (i * math.pi * 0.5)\n ox = math.cos(angle) * dist\n oz = math.sin(angle) * dist\n oy = math.sin(angle * 2.0) * 1.5\n targets.append(Vector3(lx + ox, ly + h_off + oy, lz + oz))\n\n elif self.formation_mode == \"ECHELON_RIGHT\":\n # Echelon Right (45 deg diagonal trail to the right)\n for i in range(1, 5):\n tx = lx + (rx * i - fx * i) * (dist * 0.75)\n tz = lz + (rz * i - fz * i) * (dist * 0.75)\n targets.append(Vector3(tx, ly + h_off + (i * 0.4), tz))\n\n elif self.formation_mode == \"VERTICAL\":\n # 3D Stack / Ladder\n offsets = [\n (0.0, 0.0, 1.0), # D2: Top +1\n (0.0, 0.0, -1.0), # D3: Bottom -1\n (0.5, -0.5, 2.0), # D4: High Rear\n (-0.5, -0.5, -2.0) # D5: Low Rear\n ]\n for r_fac, f_fac, h_fac in offsets:\n tx = lx + (rx * r_fac + fx * f_fac) * (dist * 0.6)\n tz = lz + (rz * r_fac + fz * f_fac) * (dist * 0.6)\n ty = ly + (h_fac * dist * 0.6) + h_off\n targets.append(Vector3(tx, ty, tz))\n\n else:\n # Fallback\n for i in range(1, 5):\n targets.append(Vector3(lx + i * 4.0, ly, lz))\n\n return targets\n\n def update_physics(self, dt: float):\n \"\"\"Simulate realistic 6-DOF drone dynamics for all 5 drones\"\"\"\n self.sim_time += dt\n self.orbit_angle += dt * 1.2\n\n # Autonomous flight patterns if enabled\n if self.autonomous_mode:\n t = self.sim_time * 0.35 * self.speed_multiplier\n if self.auto_pattern == \"FIGURE_EIGHT\":\n self.leader.target_pos.x = math.sin(t) * 35.0\n self.leader.target_pos.z = math.sin(t * 2.0) * 20.0\n self.leader.target_pos.y = 8.0 + math.sin(t * 3.0) * 3.0\n elif self.auto_pattern == \"CIRCLE\":\n self.leader.target_pos.x = math.cos(t) * 30.0\n self.leader.target_pos.z = math.sin(t) * 30.0\n self.leader.target_pos.y = 10.0 + math.cos(t * 2.0) * 2.5\n elif self.auto_pattern == \"WAYPOINT_PATROL\":\n wps = [Vector3(-25, 8, -25), Vector3(25, 12, -25), Vector3(25, 8, 25), Vector3(-25, 14, 25)]\n idx = int((t * 0.5) % 4)\n next_idx = (idx + 1) % 4\n blend = (t * 0.5) % 1.0\n p1 = wps[idx]\n p2 = wps[next_idx]\n self.leader.target_pos.x = p1.x + (p2.x - p1.x) * blend\n self.leader.target_pos.y = p1.y + (p2.y - p1.y) * blend\n self.leader.target_pos.z = p1.z + (p2.z - p1.z) * blend\n\n # 1. Simulate Leader Drone (Moves toward target)\n self._simulate_drone(self.leader, self.leader.target_pos, dt, responsiveness=3.2 * self.speed_multiplier)\n\n # 2. Simulate 4 Follower Drones\n follower_targets = self.calculate_follower_targets()\n for idx, target in enumerate(follower_targets):\n drone = self.drones[idx + 1]\n drone.target_pos = target\n self._simulate_drone(drone, target, dt, responsiveness=3.6 * self.speed_multiplier)\n\n # Sync status\n err = drone.pos.dist_to(target)\n if err < 0.6:\n drone.status = \"FORMATION LOCKED\"\n elif err < 2.5:\n drone.status = \"TRACKING\"\n else:\n drone.status = \"REJOINING\"\n\n # Battery consumption\n drone.battery = max(5.0, drone.battery - dt * 0.012)\n\n self.leader.battery = max(5.0, self.leader.battery - dt * 0.012)\n\n def _simulate_drone(self, drone: DroneState, target: Vector3, dt: float, responsiveness: float):\n \"\"\"Critically damped spring-damper controller for drone navigation + attitude banking\"\"\"\n ex = target.x - drone.pos.x\n ey = target.y - drone.pos.y\n ez = target.z - drone.pos.z\n\n k_p = responsiveness * 3.5\n k_d = 2.4 * math.sqrt(k_p)\n\n acc_x = (ex * k_p) - (drone.vel.x * k_d)\n acc_y = (ey * (k_p * 1.2)) - (drone.vel.y * k_d)\n acc_z = (ez * k_p) - (drone.vel.z * k_d)\n\n max_acc = 22.0\n acc_mag = math.sqrt(acc_x**2 + acc_y**2 + acc_z**2)\n if acc_mag > max_acc:\n acc_x = (acc_x / acc_mag) * max_acc\n acc_y = (acc_y / acc_mag) * max_acc\n acc_z = (acc_z / acc_mag) * max_acc\n\n drone.vel.x += acc_x * dt\n drone.vel.y += acc_y * dt\n drone.vel.z += acc_z * dt\n\n drone.pos.x += drone.vel.x * dt\n drone.pos.y += drone.vel.y * dt\n drone.pos.z += drone.vel.z * dt\n\n # Yaw alignment towards velocity\n horizontal_speed = math.sqrt(drone.vel.x**2 + drone.vel.z**2)\n if horizontal_speed > 0.3:\n desired_yaw = math.atan2(drone.vel.x, drone.vel.z)\n diff = (desired_yaw - drone.yaw + math.pi) % (2 * math.pi) - math.pi\n drone.yaw += diff * min(1.0, dt * 4.0)\n\n # Body-frame pitch & roll\n cos_y = math.cos(drone.yaw)\n sin_y = math.sin(drone.yaw)\n forward_acc = acc_z * cos_y + acc_x * sin_y\n lateral_acc = acc_x * cos_y - acc_z * sin_y\n\n desired_pitch = -math.atan2(forward_acc, 9.81) * 0.7\n desired_roll = math.atan2(lateral_acc, 9.81) * 0.7\n\n max_tilt = math.radians(35.0)\n desired_pitch = max(-max_tilt, min(max_tilt, desired_pitch))\n desired_roll = max(-max_tilt, min(max_tilt, desired_roll))\n\n drone.pitch += (desired_pitch - drone.pitch) * min(1.0, dt * 7.0)\n drone.roll += (desired_roll - drone.roll) * min(1.0, dt * 7.0)\n\n base_rpm = 5000.0 + (acc_y * 120.0)\n tilt_factor = math.sqrt(drone.pitch**2 + drone.roll**2) * 1500.0\n drone.rotor_rpm[0] = max(1000.0, base_rpm + tilt_factor + drone.roll * 800.0)\n drone.rotor_rpm[1] = max(1000.0, base_rpm + tilt_factor - drone.roll * 800.0)\n drone.rotor_rpm[2] = max(1000.0, base_rpm + tilt_factor + drone.pitch * 800.0)\n drone.rotor_rpm[3] = max(1000.0, base_rpm + tilt_factor - drone.pitch * 800.0)\n\n def get_state_payload(self) -> Dict[str, Any]:\n return {\n \"timestamp\": time.time(),\n \"count\": len(self.drones),\n \"leader\": self.leader.to_dict(),\n \"drones\": [d.to_dict() for d in self.drones],\n \"formation\": {\n \"mode\": self.formation_mode,\n \"distance\": round(self.formation_distance, 2),\n \"target_distance\": self.formation_distance,\n \"height_offset\": self.formation_height_offset,\n \"autonomous\": self.autonomous_mode,\n \"auto_pattern\": self.auto_pattern,\n \"speed_multiplier\": round(self.speed_multiplier, 2)\n }\n }\n\n\nengine = FormationEngine()\n\n\n# Starlette App Endpoints\nasync def homepage(request):\n index_file = os.path.join(STATIC_DIR, \"index.html\")\n return FileResponse(index_file)\n\n\nasync def api_status(request):\n return JSONResponse(engine.get_state_payload())\n\n\nasync def websocket_endpoint(websocket: WebSocket):\n await websocket.accept()\n engine.clients.append(websocket)\n try:\n while True:\n data = await websocket.receive_text()\n try:\n msg = json.loads(data)\n cmd_type = msg.get(\"type\")\n\n if cmd_type == \"SET_TARGET\":\n x = msg.get(\"x\", 0.0)\n y = msg.get(\"y\", 8.0)\n z = msg.get(\"z\", 0.0)\n engine.set_target(x, y, z)\n\n elif cmd_type == \"SET_FORMATION\":\n if \"mode\" in msg:\n engine.formation_mode = msg[\"mode\"]\n if \"distance\" in msg:\n engine.formation_distance = float(msg[\"distance\"])\n if \"height_offset\" in msg:\n engine.formation_height_offset = float(msg[\"height_offset\"])\n\n elif cmd_type == \"SET_AUTONOMOUS\":\n engine.autonomous_mode = bool(msg.get(\"enabled\", False))\n if \"pattern\" in msg:\n engine.auto_pattern = msg[\"pattern\"]\n\n elif cmd_type == \"SET_SPEED\":\n engine.speed_multiplier = float(msg.get(\"speed\", 1.0))\n\n elif cmd_type == \"RESET_POSITION\":\n engine.leader.pos = Vector3(0.0, 8.0, 0.0)\n engine.leader.target_pos = Vector3(0.0, 8.0, 0.0)\n engine.leader.vel = Vector3(0.0, 0.0, 0.0)\n # Reset wingmen around leader\n for idx, d in enumerate(engine.drones[1:], 1):\n d.pos = Vector3(idx * 4.0, 8.0, -idx * 3.0)\n d.vel = Vector3(0.0, 0.0, 0.0)\n\n except json.JSONDecodeError:\n pass\n except WebSocketDisconnect:\n if websocket in engine.clients:\n engine.clients.remove(websocket)\n except Exception as e:\n if websocket in engine.clients:\n engine.clients.remove(websocket)\n\n\nasync def physics_loop():\n \"\"\"High-frequency background simulation loop (60 FPS)\"\"\"\n last_time = time.time()\n while True:\n now = time.time()\n dt = min(0.05, now - last_time)\n last_time = now\n\n engine.update_physics(dt)\n\n if engine.clients:\n payload = json.dumps(engine.get_state_payload())\n dead_clients = []\n for client in engine.clients:\n try:\n await client.send_text(payload)\n except Exception:\n dead_clients.append(client)\n for dead in dead_clients:\n if dead in engine.clients:\n engine.clients.remove(dead)\n\n await asyncio.sleep(1.0 / 60.0)\n\n\n@contextlib.asynccontextmanager\nasync def lifespan(app):\n sim_task = asyncio.create_task(physics_loop())\n yield\n sim_task.cancel()\n try:\n await sim_task\n except asyncio.CancelledError:\n pass\n\n\nroutes = [\n Route(\"/\", homepage),\n WebSocketRoute(\"/ws\", websocket_endpoint),\n Route(\"/api/status\", api_status),\n Mount(\"/static\", StaticFiles(directory=STATIC_DIR), name=\"static\")\n]\n\napp = Starlette(debug=True, routes=routes, lifespan=lifespan)\n\n\nif __name__ == \"__main__\":\n if hasattr(sys.stdout, \"reconfigure\"):\n try:\n sys.stdout.reconfigure(encoding=\"utf-8\")\n except Exception:\n pass\n print(\"================================================================\")\n print(\" [5-DRONE FORMATION FLIGHT SIMULATOR] Three.js + Python\")\n print(\" Server starting at: http://127.0.0.1:8000\")\n print(\"================================================================\")\n uvicorn.run(\"app:app\", host=\"127.0.0.1\", port=8000, log_level=\"info\", reload=False)\n''')\n", "\n", "# static/index.html ์์ฑ\n", "with open(\"static/index.html\", \"w\", encoding=\"utf-8\") as f:\n", " f.write('''\n\n
\n \n \n\n", " ์๋ ๋ฒํผ ๋๋ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ 3D ์๋ฎฌ๋ ์ดํฐ ํ๋ฉด์ผ๋ก ์ ์ํ์ธ์.\n", "
\n", "\"\"\"\n", "\n", "if public_url:\n", " html_output += f\"\"\"\n", " \n", " \"\"\"\n", "else:\n", " html_output += \"โ ๏ธ Cloudflare ๋งํฌ ์์ฑ ์ค์ ๋๋ค. ๋ช ์ด ํ ๋ค์ ํ์ธํด์ฃผ์ธ์.
\"\n", "\n", "if colab_port_url:\n", " html_output += f\"\"\"\n", "