update
This commit is contained in:
1 parent
83278b2998
commit
b38ceec784
128 files changed
+51554
No files matched your search
@@ -0,0 +1,241 @@
|
||||
{
|
||||
"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_hLine truncated
|
||||
"\n",
|
||||
"# static/index.html 작성\n",
|
||||
"with open(\"static/index.html\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>드론 5대 군집 편대 비행 시뮬레이터 | 5-Drone Swarm Formation (Three.js & Python)</title>\n \n <!-- Three.js and OrbitControls -->\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js\"></script>\n\n <!-- Custom CSS -->\n <link rel=\"stylesheet\" href=\"/static/css/style.css\">\n</head>\n<body>\n\n <!-- 3D WebGL Canvas Container -->\n <div id=\"canvas-container\"></div>\n <div class=\"hud-overlay\"></div>\n\n <!-- Top Navigation & Status Bar -->\n <header class=\"top-nav\">\n <div class=\"brand-title\">\n <div class=\"brand-icon\"></div>\n <h1>AERO-SWARM TACTICAL HUD</h1>\n <span>5-DRONE SWARM FORMATION CONTROLLER</span>\n </div>\n\n <div class=\"top-status-bar\">\n <div class=\"status-badge\">\n <div id=\"connection-status-dot\" class=\"status-dot\"></div>\n <span id=\"connection-status-text\">CONNECTING...</span>\n </div>\n <div class=\"status-badge\">\n <span style=\"color: var(--color-cyan);\">SWARM:</span>\n <span style=\"color: #fff; font-weight: bold;\">5 UNITS ACTIVE</span>\n </div>\n </div>\n </header>\n\n <!-- Left Side Panel: Real-Time 5-Drone Swarm Telemetry -->\n <aside class=\"side-panel left-panel\">\n <div class=\"panel-header\">\n <div class=\"panel-title\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5\"/></svg>\n SWARM TELEMETRY (5 UNITS)\n </div>\n <span class=\"callsign-tag\" style=\"color: var(--color-cyan);\">60 FPS SYNC</span>\n </div>\n\n <!-- 1. Leader Drone -->\n <div class=\"drone-card leader\">\n <div class=\"card-top\">\n <span class=\"drone-name\" style=\"color: #00f0ff;\">LEADER (VIPER-01)</span>\n <span class=\"callsign-tag\">ALPHA-01</span>\n </div>\n <div class=\"telemetry-grid\">\n <div class=\"tele-item\">\n <span class=\"tele-label\">POS</span>\n <span id=\"tele-d0-pos\" class=\"tele-val\">0.0, 8.0, 0.0</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">AIRSPEED</span>\n <span id=\"tele-d0-spd\" class=\"tele-val\" style=\"color: #00f0ff;\">0.0 km/h</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">ALTITUDE</span>\n <span id=\"tele-d0-alt\" class=\"tele-val\">8.0 m</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">BATTERY</span>\n <span id=\"tele-d0-bat\" class=\"tele-val\" style=\"color: var(--color-green);\">98.5%</span>\n </div>\n </div>\n <div class=\"progress-bar-bg\"><div id=\"bar-d0-bat\" class=\"progress-bar-fill\"></div></div>\n </div>\n\n <!-- 2. Wingman 1 -->\n <div class=\"drone-card\" style=\"border-left: 4px solid #ff0055;\">\n <div class=\"card-top\">\n <span class=\"drone-name\" style=\"color: #ff0055;\">WINGMAN 1 (GHOST-02)</span>\n <span class=\"callsign-tag\">ALPHA-02</span>\n </div>\n <div class=\"telemetry-grid\">\n <div class=\"tele-item\">\n <span class=\"tele-label\">POS</span>\n <span id=\"tele-d1-pos\" class=\"tele-val\">5.0, 8.0, -5.0</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">AIRSPEED</span>\n <span id=\"tele-d1-spd\" class=\"tele-val\" style=\"color: #ff0055;\">0.0 km/h</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">ALTITUDE</span>\n <span id=\"tele-d1-alt\" class=\"tele-val\">8.0 m</span>\n </div>\n <div class=\"tele-item\">\n <span class=\"tele-label\">BATTERY</span>\n <span id=\"tele-d1-bat\" class=\"tele-val\" style=\"color: var(--color-green);\">98.5%</span>\n </div>\n </div>\n <div class=\"progress-bar-bg\"><div id=\"bar-d1-bat\" class=\"progress-bar-fill\"></div></div>\n </div>\n\n <!-- 3. Wingman 2 -->\n <div class=\"drone-card\" style=\"border-lefLine truncated
|
||||
"\n",
|
||||
"# static/css/style.css 작성\n",
|
||||
"with open(\"static/css/style.css\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''/* ============================================================\n DRONE FORMATION FLIGHT SIMULATOR - TACTICAL HUD STYLESHEET\n ============================================================ */\n\n@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800;900&family=Rajdhani:wght@500;600;700&family=Share+Tech+Mono&display=swap');\n\n:root {\n --bg-dark: #070a0f;\n --panel-bg: rgba(12, 18, 28, 0.72);\n --panel-border: rgba(0, 240, 255, 0.28);\n --panel-border-glow: rgba(0, 240, 255, 0.45);\n \n --color-cyan: #00f0ff;\n --color-pink: #ff0055;\n --color-green: #00ff88;\n --color-amber: #ffb700;\n --color-text-main: #d8e5f2;\n --color-text-dim: #7a8fa6;\n \n --font-head: 'Orbitron', sans-serif;\n --font-ui: 'Rajdhani', sans-serif;\n --font-mono: 'Share Tech Mono', monospace;\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n user-select: none;\n}\n\nbody {\n background-color: var(--bg-dark);\n color: var(--color-text-main);\n font-family: var(--font-ui);\n overflow: hidden;\n width: 100vw;\n height: 100vh;\n}\n\n#canvas-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 1;\n}\n\n/* Vignette & scanline overlay */\n.hud-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n z-index: 2;\n background: radial-gradient(circle at center, transparent 60%, rgba(3, 7, 12, 0.75) 100%);\n box-shadow: inset 0 0 100px rgba(0, 240, 255, 0.05);\n}\n\n.hud-overlay::before {\n content: \" \";\n position: absolute;\n top: 0; left: 0; width: 100%; height: 100%;\n background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%);\n background-size: 100% 4px;\n opacity: 0.2;\n pointer-events: none;\n}\n\n/* ================= HUD TOP HEADER ================= */\n.top-nav {\n position: absolute;\n top: 14px;\n left: 20px;\n right: 20px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n z-index: 10;\n pointer-events: auto;\n}\n\n.brand-title {\n display: flex;\n align-items: center;\n gap: 12px;\n background: var(--panel-bg);\n border: 1px solid var(--panel-border);\n backdrop-filter: blur(14px);\n padding: 8px 18px;\n border-radius: 6px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), inset 0 0 10px rgba(0, 240, 255, 0.1);\n}\n\n.brand-icon {\n width: 12px;\n height: 12px;\n background: var(--color-cyan);\n box-shadow: 0 0 12px var(--color-cyan);\n border-radius: 2px;\n animation: pulseGlow 2s infinite alternate;\n}\n\n.brand-title h1 {\n font-family: var(--font-head);\n font-size: 1.05rem;\n font-weight: 800;\n letter-spacing: 2px;\n color: #fff;\n text-transform: uppercase;\n}\n\n.brand-title span {\n color: var(--color-cyan);\n font-size: 0.8rem;\n font-family: var(--font-mono);\n border-left: 1px solid var(--color-text-dim);\n padding-left: 10px;\n}\n\n.top-status-bar {\n display: flex;\n gap: 12px;\n}\n\n.status-badge {\n background: var(--panel-bg);\n border: 1px solid var(--panel-border);\n backdrop-filter: blur(14px);\n padding: 6px 14px;\n border-radius: 4px;\n font-family: var(--font-mono);\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--color-green);\n box-shadow: 0 0 8px var(--color-green);\n}\n\n.status-dot.warning {\n background: var(--color-amber);\n box-shadow: 0 0 8px var(--color-amber);\n}\n\n/* ================= MAIN SIDE PANELS ================= */\n.side-panel {\n position: absolute;\n top: 72px;\n bottom: 86px;\n width: 320px;\n background: var(--panel-bg);\n border: 1px solid var(--panel-border);\n backdrop-filter: blur(16px);\n border-radius: 8px;\n padding: 16px;\n z-index: 10;\n display: flex;\n flex-direction: column;\n gap: 14px;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);\n overflow-y: auto;\n scrollbar-width: thin;\n scrollbar-color: var(--color-cyan) transparent;\n}\n\n.side-panel::-webkit-scrollbar {\n width: 4px;\n}\n.side-panel::-webkit-scrollbar-thumb {\n background: var(--color-cyan);\n border-radius: 4px;\n}\n\n.left-panel {\n left: 20px;\n}\n\n.right-panel {\n right: 20px;\n}\n\n.panel-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n border-bottom: 1px solid rgba(0, 240, 255, 0.2);\n padding-bottom: 8px;\n}\n\n.panel-title {\n font-family: var(--font-head);\n font-size: 0.88rem;\n letter-spacing: 1.5px;\n color: var(--color-cyan);\n text-transform: uppercase;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n/* Drone Telemetry Cards */\n.drone-Line truncated
|
||||
"\n",
|
||||
"# static/js/audio.js 작성\n",
|
||||
"with open(\"static/js/audio.js\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''/**\n * Synthesizer for Drone Rotor Hum and Tactical HUD Audio FX\n * Uses Web Audio API without external audio file dependencies\n */\nclass DroneAudioSystem {\n constructor() {\n this.ctx = null;\n this.enabled = false;\n this.leaderGain = null;\n this.followerGain = null;\n this.leaderOsc1 = null;\n this.leaderOsc2 = null;\n this.followerOsc1 = null;\n this.followerOsc2 = null;\n }\n\n init() {\n if (this.ctx) return;\n try {\n const AudioContext = window.AudioContext || window.webkitAudioContext;\n this.ctx = new AudioContext();\n\n // Master compressor to prevent clipping\n const compressor = this.ctx.createDynamicsCompressor();\n compressor.threshold.setValueAtTime(-18, this.ctx.currentTime);\n compressor.knee.setValueAtTime(30, this.ctx.currentTime);\n compressor.ratio.setValueAtTime(12, this.ctx.currentTime);\n compressor.attack.setValueAtTime(0.003, this.ctx.currentTime);\n compressor.release.setValueAtTime(0.25, this.ctx.currentTime);\n compressor.connect(this.ctx.destination);\n\n this.masterGain = this.ctx.createGain();\n this.masterGain.gain.setValueAtTime(0.2, this.ctx.currentTime);\n this.masterGain.connect(compressor);\n\n // Leader Drone sound synthesizer (Dual harmonic oscillator + lowpass filter)\n this.leaderFilter = this.ctx.createBiquadFilter();\n this.leaderFilter.type = \"lowpass\";\n this.leaderFilter.frequency.setValueAtTime(650, this.ctx.currentTime);\n\n this.leaderGain = this.ctx.createGain();\n this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime);\n this.leaderGain.connect(this.leaderFilter);\n this.leaderFilter.connect(this.masterGain);\n\n this.leaderOsc1 = this.ctx.createOscillator();\n this.leaderOsc1.type = \"sawtooth\";\n this.leaderOsc1.frequency.setValueAtTime(95, this.ctx.currentTime);\n\n this.leaderOsc2 = this.ctx.createOscillator();\n this.leaderOsc2.type = \"triangle\";\n this.leaderOsc2.frequency.setValueAtTime(190, this.ctx.currentTime);\n\n this.leaderOsc1.connect(this.leaderGain);\n this.leaderOsc2.connect(this.leaderGain);\n this.leaderOsc1.start();\n this.leaderOsc2.start();\n\n // Follower Drone sound synthesizer\n this.followerFilter = this.ctx.createBiquadFilter();\n this.followerFilter.type = \"lowpass\";\n this.followerFilter.frequency.setValueAtTime(600, this.ctx.currentTime);\n\n this.followerGain = this.ctx.createGain();\n this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime);\n this.followerGain.connect(this.followerFilter);\n this.followerFilter.connect(this.masterGain);\n\n this.followerOsc1 = this.ctx.createOscillator();\n this.followerOsc1.type = \"sawtooth\";\n this.followerOsc1.frequency.setValueAtTime(98, this.ctx.currentTime);\n\n this.followerOsc2 = this.ctx.createOscillator();\n this.followerOsc2.type = \"triangle\";\n this.followerOsc2.frequency.setValueAtTime(196, this.ctx.currentTime);\n\n this.followerOsc1.connect(this.followerGain);\n this.followerOsc2.connect(this.followerGain);\n this.followerOsc1.start();\n this.followerOsc2.start();\n\n this.enabled = true;\n } catch (e) {\n console.warn(\"Web Audio not supported or blocked\", e);\n }\n }\n\n toggle() {\n if (!this.ctx) {\n this.init();\n return true;\n }\n if (this.ctx.state === 'suspended') {\n this.ctx.resume();\n this.enabled = true;\n return true;\n }\n this.enabled = !this.enabled;\n if (!this.enabled && this.leaderGain && this.followerGain) {\n this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime);\n this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime);\n }\n return this.enabled;\n }\n\n updateMotors(leaderRpm, followerRpm, speedLeader, speedFollower) {\n if (!this.enabled || !this.ctx || this.ctx.state !== 'running') return;\n\n const now = this.ctx.currentTime;\n // Pitch proportional to RPM and velocity\n const baseFreq1 = 85 + (speedLeader * 2.5) + ((leaderRpm - 4500) * 0.015);\n const baseFreq2 = 88 + (speedFollower * 2.5) + ((followerRpm - 4500) * 0.015);\n\n this.leaderOsc1.frequency.setTargetAtTime(Math.max(60, Math.min(220, baseFreq1)), now, 0.08);\n this.leaderOsc2.frequency.setTargetAtTime(Math.max(120, Math.min(440, baseFreq1 * 2)), now, 0.08);\n\n this.folloLine truncated
|
||||
"\n",
|
||||
"# static/js/drone_model.js 작성\n",
|
||||
"with open(\"static/js/drone_model.js\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''/**\n * Procedural 3D Quadcopter Drone Builder in Three.js\n * Generates high-fidelity carbon-fiber military/tactical UAV models with animated rotors,\n * navigation LEDs, gimbal camera, landing skids, and thrust trail effects.\n */\n\nclass DroneModel {\n constructor(scene, options = {}) {\n this.scene = scene;\n this.id = options.id || \"drone\";\n this.name = options.name || \"UAV\";\n this.primaryColor = options.primaryColor || 0x00f0ff;\n this.accentColor = options.accentColor || 0x111625;\n this.isLeader = !!options.isLeader;\n\n this.group = new THREE.Group();\n this.rotors = [];\n this.rotorDiscs = [];\n this.strobeLeds = [];\n this.thrustParticles = [];\n\n this._buildModel();\n this.scene.add(this.group);\n }\n\n _buildModel() {\n // High quality PBR materials\n const carbonMat = new THREE.MeshStandardMaterial({\n color: 0x1a1e24,\n metalness: 0.85,\n roughness: 0.3,\n });\n\n const metalMat = new THREE.MeshStandardMaterial({\n color: 0x8892a0,\n metalness: 0.95,\n roughness: 0.15,\n });\n\n const canopyMat = new THREE.MeshStandardMaterial({\n color: 0x0d131a,\n metalness: 0.6,\n roughness: 0.1,\n clearcoat: 0.8,\n clearcoatRoughness: 0.1\n });\n\n const accentMat = new THREE.MeshStandardMaterial({\n color: this.primaryColor,\n metalness: 0.7,\n roughness: 0.3,\n emissive: this.primaryColor,\n emissiveIntensity: 0.35\n });\n\n const glowMat = new THREE.MeshBasicMaterial({\n color: this.primaryColor\n });\n\n // 1. Central Body Chassis (Lower & Upper aerodynamic hull)\n const bodyGeo = new THREE.CylinderGeometry(0.7, 0.9, 0.32, 8);\n bodyGeo.rotateY(Math.PI / 8);\n const bodyMesh = new THREE.Mesh(bodyGeo, carbonMat);\n bodyMesh.castShadow = true;\n bodyMesh.receiveShadow = true;\n this.group.add(bodyMesh);\n\n // Aerodynamic Top Canopy Shell\n const canopyGeo = new THREE.SphereGeometry(0.65, 16, 12);\n canopyGeo.scale(1.0, 0.45, 1.35);\n const canopyMesh = new THREE.Mesh(canopyGeo, canopyMat);\n canopyMesh.position.y = 0.18;\n canopyMesh.castShadow = true;\n this.group.add(canopyMesh);\n\n // Center Accent Stripe\n const stripeGeo = new THREE.BoxGeometry(0.12, 0.04, 1.4);\n const stripeMesh = new THREE.Mesh(stripeGeo, accentMat);\n stripeMesh.position.y = 0.34;\n this.group.add(stripeMesh);\n\n // 2. Gimbal Camera Sensor at Front Underbelly\n const gimbalGroup = new THREE.Group();\n gimbalGroup.position.set(0, -0.15, 0.7);\n\n const gimbalBase = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.08, 12), metalMat);\n gimbalGroup.add(gimbalBase);\n\n const cameraSphere = new THREE.Mesh(new THREE.SphereGeometry(0.16, 16, 16), carbonMat);\n cameraSphere.position.y = -0.1;\n gimbalGroup.add(cameraSphere);\n\n // Camera Optical Lens (Glowing ring)\n const lensGeo = new THREE.CylinderGeometry(0.09, 0.09, 0.06, 16);\n lensGeo.rotateX(Math.PI / 2);\n const lensMesh = new THREE.Mesh(lensGeo, new THREE.MeshBasicMaterial({ color: 0x00ffff }));\n lensMesh.position.set(0, -0.1, 0.14);\n gimbalGroup.add(lensMesh);\n\n this.group.add(gimbalGroup);\n\n // 3. Carbon Fiber Rotor Arms (X-Configuration)\n const armLength = 1.9;\n const armRadius = 0.055;\n const armPositions = [\n { angle: Math.PI * 0.25, dir: 1, ledColor: 0x00ff66 }, // Front-Right (Green nav)\n { angle: Math.PI * 0.75, dir: -1, ledColor: 0xff0044 }, // Rear-Right (Red nav)\n { angle: Math.PI * 1.25, dir: 1, ledColor: 0xff0044 }, // Rear-Left (Red nav)\n { angle: Math.PI * 1.75, dir: -1, ledColor: 0x00ff66 }, // Front-Left (Green nav)\n ];\n\n this.rotors = [];\n this.rotorDiscs = [];\n\n armPositions.forEach((arm, i) => {\n const armGroup = new THREE.Group();\n armGroup.rotation.y = arm.angle;\n\n // Arm Carbon Tube\n const armGeo = new THREE.CylinderGeometry(armRadius, armRadius, armLength, 12);\n armGeo.rotateZ(Math.PI / 2);\n const armMesh = new THREE.Mesh(armGeo, carbonMat);\n armMesh.position.x = armLength * 0.5;\n armMesh.castShadow = true;\n armGroup.add(armMesh);\n\n // Diagonal support truss\n const trussGeo = new THREE.CylinderGeometry(0.025, 0.025, armLength * 0.6, 8);\n trussGeo.rotateZ(Math.PI / 2.8);\n const trussMesh = new THREE.Mesh(trussGeo, carbonMat);\n trussMLine truncated
|
||||
"\n",
|
||||
"# static/js/radar.js 작성\n",
|
||||
"with open(\"static/js/radar.js\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''/**\n * Tactical Radar HUD Canvas Component\n * Real-time 2D top-down aerial map showing 5 drones, trail histories, and sweep beam\n */\nclass TacticalRadar {\n constructor(canvasId) {\n this.canvas = document.getElementById(canvasId);\n if (!this.canvas) return;\n this.ctx = this.canvas.getContext('2d');\n this.width = this.canvas.width = 220;\n this.height = this.canvas.height = 220;\n this.cx = this.width / 2;\n this.cy = this.height / 2;\n this.range = 75.0; // World meters represented on radar radius\n\n this.sweepAngle = 0.0;\n this.trails = {};\n this.maxTrail = 28;\n }\n\n render(drones) {\n if (!this.ctx || !drones || !drones.length) return;\n const ctx = this.ctx;\n const w = this.width;\n const h = this.height;\n const cx = this.cx;\n const cy = this.cy;\n const r = cx - 10;\n\n // Clear with dark HUD background\n ctx.fillStyle = \"rgba(8, 14, 22, 0.45)\";\n ctx.fillRect(0, 0, w, h);\n\n // Circular clip for radar scope\n ctx.save();\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.clip();\n\n // Background subtle grid rings\n ctx.strokeStyle = \"rgba(0, 240, 255, 0.15)\";\n ctx.lineWidth = 1;\n for (let i = 1; i <= 3; i++) {\n ctx.beginPath();\n ctx.arc(cx, cy, (r / 3) * i, 0, Math.PI * 2);\n ctx.stroke();\n }\n\n // Crosshairs\n ctx.beginPath();\n ctx.moveTo(cx - r, cy);\n ctx.lineTo(cx + r, cy);\n ctx.moveTo(cx, cy - r);\n ctx.lineTo(cx, cy + r);\n ctx.stroke();\n\n // 45-degree guide dashes\n ctx.setLineDash([2, 4]);\n ctx.strokeStyle = \"rgba(0, 240, 255, 0.08)\";\n ctx.beginPath();\n ctx.moveTo(cx - r * 0.7, cy - r * 0.7);\n ctx.lineTo(cx + r * 0.7, cy + r * 0.7);\n ctx.moveTo(cx - r * 0.7, cy + r * 0.7);\n ctx.lineTo(cx + r * 0.7, cy - r * 0.7);\n ctx.stroke();\n ctx.setLineDash([]);\n\n // Radar Sweep Line & Beam Gradient\n this.sweepAngle += 0.035;\n if (this.sweepAngle > Math.PI * 2) this.sweepAngle -= Math.PI * 2;\n\n const sweepGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);\n sweepGrad.addColorStop(0, \"rgba(0, 240, 255, 0.0)\");\n sweepGrad.addColorStop(1, \"rgba(0, 240, 255, 0.18)\");\n\n ctx.fillStyle = sweepGrad;\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.arc(cx, cy, r, this.sweepAngle - 0.45, this.sweepAngle);\n ctx.closePath();\n ctx.fill();\n\n ctx.strokeStyle = \"rgba(0, 240, 255, 0.8)\";\n ctx.lineWidth = 1.5;\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.lineTo(cx + Math.cos(this.sweepAngle) * r, cy + Math.sin(this.sweepAngle) * r);\n ctx.stroke();\n\n // Convert world pos (X, Z) to radar canvas coords\n const scale = r / this.range;\n const toRadar = (pos) => ({\n x: cx + (pos.x * scale),\n y: cy + (pos.z * scale)\n });\n\n const leaderPos = toRadar(drones[0].position);\n\n // Draw formation link lines from leader to all wingmen\n ctx.strokeStyle = \"rgba(0, 240, 255, 0.25)\";\n ctx.lineWidth = 1;\n ctx.setLineDash([2, 2]);\n for (let i = 1; i < drones.length; i++) {\n const fPos = toRadar(drones[i].position);\n ctx.beginPath();\n ctx.moveTo(leaderPos.x, leaderPos.y);\n ctx.lineTo(fPos.x, fPos.y);\n ctx.stroke();\n }\n ctx.setLineDash([]);\n\n // Update & Render each drone\n drones.forEach((d, idx) => {\n const radPos = toRadar(d.position);\n if (!this.trails[d.id]) this.trails[d.id] = [];\n this.trails[d.id].push({ ...radPos });\n if (this.trails[d.id].length > this.maxTrail) this.trails[d.id].shift();\n\n // Draw Trail\n this._drawTrail(this.trails[d.id], d.color);\n\n // Draw Drone Blip\n const yawRad = THREE.MathUtils.degToRad(d.rotation.yaw || 0);\n const label = idx === 0 ? \"L-01\" : `W-0${idx + 1}`;\n this._drawDroneBlip(radPos.x, radPos.y, yawRad, d.color, label, idx === 0);\n });\n\n ctx.restore();\n\n // Outer Ring Border\n ctx.strokeStyle = \"rgba(0, 240, 255, 0.6)\";\n ctx.lineWidth = 1.5;\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.stroke();\n\n // Compass Cardinal labels\n ctx.fillStyle = \"rgba(0, 240, 255, 0.75)\";\n ctx.font = \"9px 'Share Tech Mono', monospace\";\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(\"N\", cx, cy - r + 8);\n ctx.fillText(\"S\", cx, cy + r - 8);\n Line truncated
|
||||
"\n",
|
||||
"# static/js/main.js 작성\n",
|
||||
"with open(\"static/js/main.js\", \"w\", encoding=\"utf-8\") as f:\n",
|
||||
" f.write('''/**\n * 5-Drone Formation Flight Simulator - Main Application Controller\n * Three.js 3D Environment, Mouse Raycasting Flight Control, WebSocket Telemetry Sync\n */\n\nclass DroneSimulatorApp {\n constructor() {\n this.container = document.getElementById(\"canvas-container\");\n this.scene = null;\n this.camera = null;\n this.renderer = null;\n this.controls = null;\n this.clock = new THREE.Clock();\n\n this.droneModels = [];\n this.formationBeams = [];\n this.targetReticle = null;\n this.targetPillar = null;\n this.radar = null;\n\n // Mouse & Flight Target state\n this.raycaster = new THREE.Raycaster();\n this.mouse = new THREE.Vector2();\n this.flightPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -8.0);\n this.targetPos = new THREE.Vector3(0, 8.0, 0);\n this.targetAltitude = 8.0;\n this.isMouseDown = false;\n\n // Camera mode: 'ORBIT', 'CHASE', 'FPV', 'FORMATION'\n this.cameraMode = 'ORBIT';\n\n // WebSocket Client\n this.ws = null;\n this.wsConnected = false;\n this.lastTelemetry = null;\n\n this.initThree();\n this.initEnvironment();\n this.initDrones();\n this.initEventListeners();\n this.initWebSocket();\n this.initRadar();\n\n this.animate = this.animate.bind(this);\n requestAnimationFrame(this.animate);\n }\n\n initThree() {\n // Scene\n this.scene = new THREE.Scene();\n this.scene.background = new THREE.Color(0x060a10);\n this.scene.fog = new THREE.FogExp2(0x060a10, 0.010);\n\n // Camera\n this.camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.2, 500);\n this.camera.position.set(0, 26, 38);\n\n // WebGL Renderer with Soft Shadows\n this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: \"high-performance\" });\n this.renderer.setSize(window.innerWidth, window.innerHeight);\n this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n this.renderer.shadowMap.enabled = true;\n this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n this.renderer.toneMapping = THREE.ACESFilmicToneMapping;\n this.renderer.toneMappingExposure = 1.1;\n this.container.appendChild(this.renderer.domElement);\n\n // Orbit Controls for Tactical Free Cam\n this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);\n this.controls.enableDamping = true;\n this.controls.dampingFactor = 0.05;\n this.controls.maxPolarAngle = Math.PI / 2 - 0.02;\n this.controls.minDistance = 4;\n this.controls.maxDistance = 160;\n this.controls.target.set(0, 6, 0);\n\n // Ambient & Directional Lighting\n const ambientLight = new THREE.AmbientLight(0x223344, 1.2);\n this.scene.add(ambientLight);\n\n const sunLight = new THREE.DirectionalLight(0xddeeff, 2.0);\n sunLight.position.set(40, 80, 30);\n sunLight.castShadow = true;\n sunLight.shadow.mapSize.width = 2048;\n sunLight.shadow.mapSize.height = 2048;\n sunLight.shadow.camera.near = 10;\n sunLight.shadow.camera.far = 200;\n sunLight.shadow.camera.left = -70;\n sunLight.shadow.camera.right = 70;\n sunLight.shadow.camera.top = 70;\n sunLight.shadow.camera.bottom = -70;\n sunLight.shadow.bias = -0.0005;\n this.scene.add(sunLight);\n\n // Cyber Accent Rim Lights\n const cyanRim = new THREE.DirectionalLight(0x00f0ff, 1.0);\n cyanRim.position.set(-60, 30, -50);\n this.scene.add(cyanRim);\n\n const pinkRim = new THREE.DirectionalLight(0xff0066, 0.8);\n pinkRim.position.set(60, 25, 60);\n this.scene.add(pinkRim);\n }\n\n initEnvironment() {\n // Ground Grid\n const gridHelper = new THREE.GridHelper(200, 100, 0x00f0ff, 0x0f2233);\n gridHelper.position.y = 0.01;\n this.scene.add(gridHelper);\n\n const subGrid = new THREE.GridHelper(200, 20, 0x00f0ff, 0x00f0ff);\n subGrid.position.y = 0.02;\n subGrid.material.opacity = 0.3;\n subGrid.material.transparent = true;\n this.scene.add(subGrid);\n\n // Ground Plane Mesh\n const groundGeo = new THREE.PlaneGeometry(400, 400);\n groundGeo.rotateX(-Math.PI / 2);\n const groundMat = new THREE.MeshStandardMaterial({\n color: 0x060a10,\n roughness: 0.85,\n metalness: 0.2\n });\n const groundMesh = new THREE.Mesh(groundGeo, groundMat);\n groundMesh.receiveShadow = true;\n this.scene.add(groundMesh);\n\n // Concentric Landing Rings\n const padGroup = new THREE.Group();\n const padRing1 = new THREE.Mesh(\n new THREE.RingGeomLine truncated
|
||||
"\n",
|
||||
"print(\"✅ 드론 시뮬레이터 소스 파일 생성 완료!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3단계: 시뮬레이션 서버 시작 및 웹 접속 링크 열기 🎮\n",
|
||||
"아래 셀을 실행하면 Python 백엔드 서버가 백그라운드에서 동작하고, 브라우저로 접속할 수 있는 **외부 링크(Cloudflare URL)** 및 **Colab 내장 링크**가 생성됩니다."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import subprocess\n",
|
||||
"import time\n",
|
||||
"import re\n",
|
||||
"import os\n",
|
||||
"from IPython.display import display, HTML\n",
|
||||
"\n",
|
||||
"# 기존 실행 중인 프로세스 정리\n",
|
||||
"!pkill -f uvicorn > /dev/null 2>&1\n",
|
||||
"!pkill -f cloudflared > /dev/null 2>&1\n",
|
||||
"time.sleep(1)\n",
|
||||
"\n",
|
||||
"# 1. Uvicorn 서버 백그라운드 시작\n",
|
||||
"print(\"🛰️ Uvicorn 서버 시작 중...\")\n",
|
||||
"server_proc = subprocess.Popen([\"uvicorn\", \"app:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"], \n",
|
||||
" stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
|
||||
"time.sleep(2)\n",
|
||||
"\n",
|
||||
"# 2. Cloudflared 터널 시작 (무료, 인증 불필요, 웹소켓 100% 지원)\n",
|
||||
"print(\"🌐 Cloudflare 터널 연결 중...\")\n",
|
||||
"tunnel_log = \"/tmp/cloudflared.log\"\n",
|
||||
"if os.path.exists(tunnel_log):\n",
|
||||
" os.remove(tunnel_log)\n",
|
||||
"\n",
|
||||
"tunnel_proc = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:8000\", \"--logfile\", tunnel_log],\n",
|
||||
" stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n",
|
||||
"\n",
|
||||
"# 터널 URL 추출\n",
|
||||
"public_url = None\n",
|
||||
"for _ in range(25):\n",
|
||||
" time.sleep(1)\n",
|
||||
" if os.path.exists(tunnel_log):\n",
|
||||
" with open(tunnel_log, \"r\") as f:\n",
|
||||
" content = f.read()\n",
|
||||
" match = re.search(r'https://[a-zA-Z0-9-]+\\.trycloudflare\\.com', content)\n",
|
||||
" if match:\n",
|
||||
" public_url = match.group(0)\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
"# Colab 내장 포트 링크 생성 (대체 옵션)\n",
|
||||
"try:\n",
|
||||
" from google.colab.output import eval_js\n",
|
||||
" colab_port_url = eval_js(\"google.colab.kernel.proxyPort(8000)\")\n",
|
||||
"except Exception:\n",
|
||||
" colab_port_url = None\n",
|
||||
"\n",
|
||||
"# 결과 화면 출력\n",
|
||||
"html_output = \"\"\"\n",
|
||||
"<div style=\"background: linear-gradient(135deg, #0b1523, #15273d); border: 2px solid #00f0ff; border-radius: 12px; padding: 24px; color: #fff; font-family: sans-serif; box-shadow: 0 8px 32px rgba(0, 240, 255, 0.2);\">\n",
|
||||
" <h2 style=\"color: #00f0ff; margin-top: 0; display: flex; align-items: center; gap: 8px;\">\n",
|
||||
" 🚀 드론 5대 군집 비행 시뮬레이터 실행 중!\n",
|
||||
" </h2>\n",
|
||||
" <p style=\"color: #a0c0d8; font-size: 15px; margin-bottom: 20px;\">\n",
|
||||
" 아래 버튼 또는 링크를 클릭하여 3D 시뮬레이터 화면으로 접속하세요.\n",
|
||||
" </p>\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"if public_url:\n",
|
||||
" html_output += f\"\"\"\n",
|
||||
" <div style=\"margin: 16px 0;\">\n",
|
||||
" <a href=\"{public_url}\" target=\"_blank\" style=\"background: linear-gradient(90deg, #00f0ff, #0088ff); color: #000; font-weight: bold; font-size: 16px; padding: 12px 28px; border-radius: 8px; text-decoration: none; display: inline-block; box-shadow: 0 4px 15px rgba(0, 240, 255, 0.4);\">\n",
|
||||
" 🛸 시뮬레이터 열기 (Cloudflare Tunnel: {public_url})\n",
|
||||
" </a>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"\n",
|
||||
"else:\n",
|
||||
" html_output += \"<p style='color: #ff9900;'>⚠️ Cloudflare 링크 생성 중입니다. 몇 초 후 다시 확인해주세요.</p>\"\n",
|
||||
"\n",
|
||||
"if colab_port_url:\n",
|
||||
" html_output += f\"\"\"\n",
|
||||
" <div style=\"margin-top: 12px; font-size: 13px; color: #7890a8;\">\n",
|
||||
" (Colab 포트 프록시 링크: <a href=\"{colab_port_url}\" target=\"_blank\" style=\"color: #00f0ff;\">{colab_port_url}</a>)\n",
|
||||
" </div>\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
"html_output += \"\"\"\n",
|
||||
" <div style=\"margin-top: 24px; padding-top: 16px; border-top: 1px solid rgba(0, 240, 255, 0.2); font-size: 13px; color: #a0c0d8;\">\n",
|
||||
" <strong>🎮 조종 안내:</strong><br>\n",
|
||||
" • <b>마우스 클릭/드래그</b>: 리더 드론 목표 위치 이동 (윙맨 4대 자동 추종)<br>\n",
|
||||
" • <b>마우스 휠 / W, S 키</b>: 비행 고도 상승/하강<br>\n",
|
||||
" • <b>숫자 1~4 키</b>: 카메라 뷰 전환 (1: 궤도뷰, 2: 리더 추적, 3: 1인칭 FPV, 4: 편대 뷰)<br>\n",
|
||||
" • <b>M 키</b>: 사운드 On/Off | <b>Space 키</b>: 위치 초기화\n",
|
||||
" </div>\n",
|
||||
"</div>\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"display(HTML(html_output))\n",
|
||||
"if public_url:\n",
|
||||
" print(f\"\\n👉 시뮬레이터 접속 URL: {public_url}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 💡 (선택사항) Ngrok을 사용하고 싶은 경우\n",
|
||||
"Cloudflared 외에 Ngrok 계정이 있으신 경우 아래 셀을 통해 실행할 수도 있습니다."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Ngrok Authtoken이 있는 경우 아래에 입력 후 실행 (선택사항)\n",
|
||||
"NGROK_AUTH_TOKEN = \"\" # @param {type:\"string\"}\n",
|
||||
"\n",
|
||||
"if NGROK_AUTH_TOKEN.strip():\n",
|
||||
" from pyngrok import ngrok, conf\n",
|
||||
" conf.get_default().auth_token = NGROK_AUTH_TOKEN.strip()\n",
|
||||
" ngrok_tunnel = ngrok.connect(8000, \"http\")\n",
|
||||
" print(f\"🛸 Ngrok 터널 생성 완료: {ngrok_tunnel.public_url}\")\n",
|
||||
"else:\n",
|
||||
" print(\"ℹ️ NGROK_AUTH_TOKEN을 입력하지 않았습니다. 3단계의 Cloudflare 터널을 이용하세요.\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
Reference in new issue
Block a user