diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eba74f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..67f867b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./venv/lib/python3.10/site-packages" + ] +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..bcea3e0 --- /dev/null +++ b/main.py @@ -0,0 +1,56 @@ +from tello_drone import DroneSimulator, TelloSimulator +from ursina import * + +app = Ursina() +window.color = color.rgb(135, 206, 235) +window.fullscreen = True + +window.borderless = False +window.fps_counter.enabled = False +window.render_mode = 'default' + + +Sky(texture='sky_sunset') + +tello_sim = TelloSimulator() +drone_sim = DroneSimulator(tello_sim) + +def input(key): + if key == 'h': + drone_sim.help_text.visible = not drone_sim.help_text.visible + if key == 'c': + drone_sim.toggle_camera_view() +def update(): + moving = False + rolling = False + + if held_keys['w']: + drone_sim.move("forward") + moving = True + if held_keys['s']: + drone_sim.move("backward") + moving = True + if held_keys['a']: + drone_sim.move("left") + rolling = True + if held_keys['d']: + drone_sim.move("right") + rolling = True + if held_keys['j']: + drone_sim.rotate(-drone_sim.rotation_speed) + if held_keys['l']: + drone_sim.rotate(drone_sim.rotation_speed) + if held_keys['shift']: + drone_sim.change_altitude("up") + if held_keys['control']: + drone_sim.change_altitude("down") + if not moving: + drone_sim.pitch_angle = 0 # Reset pitch when not moving + + if not rolling: + drone_sim.roll_angle = 0 # Reset roll when not rolling + + drone_sim.update_movement() + drone_sim.update_pitch_roll() + +app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e2c237d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ursina \ No newline at end of file diff --git a/src/bos_standing.glb b/src/bos_standing.glb new file mode 100644 index 0000000..f84608b Binary files /dev/null and b/src/bos_standing.glb differ diff --git a/src/business_man.glb b/src/business_man.glb new file mode 100644 index 0000000..ba235ba Binary files /dev/null and b/src/business_man.glb differ diff --git a/src/cobblestone.glb b/src/cobblestone.glb new file mode 100644 index 0000000..1c09bc8 Binary files /dev/null and b/src/cobblestone.glb differ diff --git a/src/construction_barrier.glb b/src/construction_barrier.glb new file mode 100644 index 0000000..0812920 Binary files /dev/null and b/src/construction_barrier.glb differ diff --git a/src/dirty_car.glb b/src/dirty_car.glb new file mode 100644 index 0000000..421d96c Binary files /dev/null and b/src/dirty_car.glb differ diff --git a/src/dirty_leaking_concrete_wall.glb b/src/dirty_leaking_concrete_wall.glb new file mode 100644 index 0000000..d104c11 Binary files /dev/null and b/src/dirty_leaking_concrete_wall.glb differ diff --git a/src/dji_mavic_3.glb b/src/dji_mavic_3.glb new file mode 100644 index 0000000..62cb692 Binary files /dev/null and b/src/dji_mavic_3.glb differ diff --git a/src/gas_station_-_gta_v.glb b/src/gas_station_-_gta_v.glb new file mode 100644 index 0000000..f9d6c1c Binary files /dev/null and b/src/gas_station_-_gta_v.glb differ diff --git a/src/highway.glb b/src/highway.glb new file mode 100644 index 0000000..3fb096b Binary files /dev/null and b/src/highway.glb differ diff --git a/src/pig.glb b/src/pig.glb new file mode 100644 index 0000000..baf6d68 Binary files /dev/null and b/src/pig.glb differ diff --git a/src/pipeline_construction_site.glb b/src/pipeline_construction_site.glb new file mode 100644 index 0000000..987a5a4 Binary files /dev/null and b/src/pipeline_construction_site.glb differ diff --git a/src/road.glb b/src/road.glb new file mode 100644 index 0000000..7b8375a Binary files /dev/null and b/src/road.glb differ diff --git a/src/road_closed.glb b/src/road_closed.glb new file mode 100644 index 0000000..e4ac3b8 Binary files /dev/null and b/src/road_closed.glb differ diff --git a/src/road_roller.glb b/src/road_roller.glb new file mode 100644 index 0000000..74c6eed Binary files /dev/null and b/src/road_roller.glb differ diff --git a/src/stone_ground_01.glb b/src/stone_ground_01.glb new file mode 100644 index 0000000..798dc97 Binary files /dev/null and b/src/stone_ground_01.glb differ diff --git a/src/street_light.glb b/src/street_light.glb new file mode 100644 index 0000000..3efcbfe Binary files /dev/null and b/src/street_light.glb differ diff --git a/src/tello.glb b/src/tello.glb new file mode 100644 index 0000000..c8feec9 Binary files /dev/null and b/src/tello.glb differ diff --git a/src/tello_drone.py b/src/tello_drone.py new file mode 100644 index 0000000..13d2dea --- /dev/null +++ b/src/tello_drone.py @@ -0,0 +1,511 @@ +from ursina import * +from time import time + +def lerp_color(start_color, end_color, factor): + """Custom color interpolation function""" + return Color( + start_color.r + (end_color.r - start_color.r) * factor, + start_color.g + (end_color.g - start_color.g) * factor, + start_color.b + (end_color.b - start_color.b) * factor, + 1 # Alpha channel + ) + + +class TelloSimulator: + + def __init__(self): + self.battery_level = 100 + self.altitude = 3 + self.speed = 0 + self.rotation_angle = 0 + self.is_flying = False + self.start_time = time() + + def connect(self): + print("Tello Simulator: Drone connected") + return True + + def get_battery(self): + elapsed_time = time() - self.start_time + self.battery_level = max(100 - int((elapsed_time / 3600) * 100), 0) # Reduce battery over 60 min + return self.battery_level + + def takeoff(self): + if not self.is_flying: + print("Tello Simulator: Drone taking off") + self.is_flying = True + return "Taking off" + return "Already in air" + + def land(self): + if self.is_flying: + print("Tello Simulator: Drone landing") + self.is_flying = False + return "Landing" + return "Already on ground" + + def move(self, direction, distance=1): + print(f"Tello Simulator: Moving {direction} by {distance} meters") + + def rotate(self, angle): + self.rotation_angle += angle + print(f"Tello Simulator: Rotating {angle} degrees") + + #def start_video_stream(self): + #print("Tello Simulator: Starting video stream...") + + # def get_video_frame(self): + #return "Simulated video frame" + + +class DroneSimulator(Entity): + def __init__(self, tello_api, **kwargs): + super().__init__() + + self.help_text = Text( + text="Controls:\nW - Forward\nS - Backward\nA - Left\nD - Right\nShift - Launch/Up\nCtrl - Down\nJ - Rotate Left\nL - Rotate Right\nC - FPV\nH - Toggle Help", + position=(-0.85, 0.43), # Top-left position + scale=1.2, + color=color.white, + visible=True + ) + + self.tello = tello_api + self.drone = Entity( + model='tello.glb', + scale=0.06, + position=(-15.4, 3, 5), + collider='box', + cast_shadow=True + ) + + + self.car = Entity( + model='dirty_car.glb', + scale=0.085, + position=(10, 2.45, 155), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + + self.truck = Entity( + model='road_roller.glb', + scale=4.0, + position=(-150, 2.45, 155), + rotation=(0, -90, 0), + collider='box', + cast_shadow=True + ) + + self.road_closed = Entity( + model='road_closed.glb', + scale=7.0, + position=(-15, 2, 315), + rotation=(0, 90, 0), + collider='box', + cast_shadow=True + ) + + + self.business_man = Entity( + model='business_man.glb', + scale=7.3, + position=(23, 12, 155), + rotation=(0, 55, 0), + collider='box', + cast_shadow=True + ) + + self.man = Entity( + model='bos_standing.glb', + scale=10.3, + position=(-83, 2.8, 165), + rotation=(0, 120, 0), + collider='box', + cast_shadow=True + ) + + self.patch = Entity( + model='pipeline_construction_site.glb', + scale=(15, 15, 12), + position=(-123, 0.0, 260), + rotation=(0, 0, 0), + collider='none', + cast_shadow=True + ) + + self.police_man = Entity( + model='pig.glb', + scale=10.0, + position=(-35, 1.7, 230), + rotation=(0, -70, 0), + collider='box', + cast_shadow=True + ) + + self.light1 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(-55, 2.5, 260), + rotation=(0, -90, 0), + collider='none', + cast_shadow=True + ) + + + self.light2 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(25, 2.5, 95), + rotation=(0, 90, 0), + collider='none', + cast_shadow=True + ) + + self.light3 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(-55, 2.5, -70), + rotation=(0, -90, 0), + collider='none', + cast_shadow=True + ) + + + + for i in range(3): + Entity( + model='cobblestone.glb', + scale=(5, 10, 20), + position=(30, 0, i * 158.5), + ) + for i in range(3): + Entity( + model='cobblestone.glb', + scale=(5, 10, 20), + position=(-58, 0, i * 158.5), + ) + + self.tunnel_road = Entity( + model='tunnel_3.glb', + scale=(63, 45, 45), + position=(-199, 3.0, 380), + rotation=(0, 0, 0), + collider='None', + cast_shadow=True + ) + + self.highway_road = Entity( + model='highway.glb', + scale=(20, 20, 5), + position=(-14, 2.5, 120), + rotation=(0, 90, 0), + collider='box', + cast_shadow=True + ) + + + self.barrier1 = Entity( + model='construction_barrier.glb', + scale=(3, 3, 3), + position=(24, 2.5, 315), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + self.barrier2 = Entity( + model='construction_barrier.glb', + scale=(3, 3, 3), + position=(-54, 2.5, 315), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + self.station = Entity( + model='gas_station_-_gta_v.glb', + scale=(12.7, 10, 10), + position=(-210, 2.5, 77), + rotation=(0, -90, 0), + ) + + + Entity( + model='dirty_leaking_concrete_wall.glb', + scale=(25, 20, 30), + position=(34.2, 2.5, 25), + rotation=(0, 90.5, 0), + collider='box', + cast_shadow=True + ) + + + Entity( + model='dirty_leaking_concrete_wall.glb', + scale=(25, 20, 30), + position=(34, 2.5, 227), + rotation=(0, 91, 0), + collider='box', + cast_shadow=True + ) + + self.first_person_view = False + # Create a separate entity to hold the camera + self.camera_holder = Entity(position=self.drone.position) + + self.drone_camera = EditorCamera() + self.drone_camera.parent = self.camera_holder + self.third_person_position = (0, 5, -15) + self.third_person_rotation = (10, 0, 0) + self.first_person_position = (0, 0.5, 22) + self.drone_camera.position = self.third_person_position + self.drone_camera.rotation = self.third_person_rotation + self.is_flying = False + + self.velocity = Vec3(0, 0, 0) + self.acceleration = Vec3(0, 0, 0) + self.drag = 0.93 + self.rotation_speed = 5 + self.max_speed = 1.8 + self.accel_force = 0.65 + + self.pitch_angle = 0 + self.roll_angle = 0 + self.max_pitch = 20 + self.max_roll = 20 + self.tilt_smoothness = 0.05 + + self.create_meters() + + def create_meters(self): + + # Main battery container + self.battery_container = Entity( + parent=camera.ui, + model=Quad(radius=0.01), + color=color.gray, + scale=(0.12, 0.04), + position=(0.74, 0.41), + z=-1 + ) + + # Battery cap + self.battery_cap = Entity( + parent=self.battery_container, + model=Quad(radius=0.004), + color=color.gray, + position=(0.52, 0), + scale=(0.05, 0.3), + rotation_z=0 + ) + + # Battery fill + self.battery_fill = Entity( + parent=self.battery_container, + model=Quad(radius=0.01), + color=color.green, + scale=(0.9, 0.7), + position=(-0.46, 0), + origin=(-0.5, 0), + z=-0.1 + ) + + # Altitude meter + self.altitude_meter = Text( + text=f"Altitude: {self.tello.altitude}m", + position=(0.67, 0.38), + scale=0.94, + color=color.white + ) + + # Warning text + self.warning_text = Text( + text="", + position=(-0.25, 0), + scale=3, + color=color.red + ) + + def update_meters(self): + """Update telemetry meters""" + battery = self.tello.get_battery() + + # Update battery fill width with padding + fill_width = 0.92 * (battery / 100) + self.battery_fill.scale_x = fill_width + + # color transitions (green → yellow → orange → red) + if battery > 60: + factor = (battery - 60) / 40 # 100-60%: green to yellow + col = lerp_color(color.yellow, color.green, factor) + elif battery > 30: + factor = (battery - 30) / 30 # 60-30%: yellow to orange + col = lerp_color(color.orange, color.yellow, factor) + else: + factor = battery / 30 # 30-0%: orange to red + col = lerp_color(color.red, color.orange, factor) + + self.battery_fill.color = col + + # Update altitude + self.altitude_meter.text = f"Altitude: {((self.drone.y) / 10 - 3/10):.1f}m" + + # Battery warning + current_time = time() % 1 + if battery <= 10 and battery > 0: + if current_time < 0.5: + self.warning_text.text = "Battery Low!" + else: + self.warning_text.text = "" + print("\n========== Battery Low! ==========\n") + elif battery == 0: + self.warning_text.text = "Battery Depleted!" + print("\n========== Battery Depleted! ==========\n") + else: + self.warning_text.text = "" + + def update_movement(self): + self.velocity += self.acceleration + + if self.velocity.length() > self.max_speed: + self.velocity = self.velocity.normalized() * self.max_speed + + self.velocity *= self.drag + new_position = self.drone.position + self.velocity + hit_info = raycast(self.drone.position, self.velocity.normalized(), distance=self.velocity.length(), ignore=(self.drone,)) + + if not hit_info.hit: + self.drone.position = new_position + + if self.drone.y < 3: + self.drone.y = 3 + + self.acceleration = Vec3(0, 0, 0) + + # Apply pitch and roll to the drone + self.drone.rotation_x = lerp(self.drone.rotation_x, self.pitch_angle, self.tilt_smoothness) + self.drone.rotation_z = lerp(self.drone.rotation_z, self.roll_angle, self.tilt_smoothness) + + if self.first_person_view: + + self.camera_holder.position = self.drone.position + self.camera_holder.rotation_x = 0 # Keep horizon level + self.camera_holder.rotation_z = 0 # Prevent roll tilting + self.camera_holder.rotation_y = self.drone.rotation_y # yaw only + else: + # Third-person view + self.camera_holder.position = lerp(self.camera_holder.position, self.drone.position, 0.1) + self.camera_holder.rotation_y = self.drone.rotation_y # yaw only + self.drone_camera.rotation_x = 0 # Prevent pitch tilting + self.drone_camera.rotation_z = 0 # Prevent roll tilting + + self.update_meters() + + + def move(self, direction): + self.tello.move(direction, 1) + + if direction == "forward": + self.acceleration += self.drone.forward * self.accel_force + self.pitch_angle = self.max_pitch + elif direction == "backward": + self.acceleration -= self.drone.forward * self.accel_force + self.pitch_angle = -self.max_pitch + elif direction == "left": + self.acceleration -= self.drone.right * self.accel_force + self.roll_angle = -self.max_roll + elif direction == "right": + self.acceleration += self.drone.right * self.accel_force + self.roll_angle = self.max_roll + def toggle_camera_view(self): + self.first_person_view = not self.first_person_view + if self.first_person_view: + # First-person view + self.drone_camera.position = self.first_person_position + self.drone_camera.rotation = (0, 0, 0) + else: + # Third-person view + self.drone_camera.position = self.third_person_position + self.drone_camera.rotation = self.third_person_rotation + + def change_altitude(self, direction): + if direction == "up": + self.drone.y += 0.27 + self.tello.altitude += 0.27 + elif direction == "down" and self.drone.y > 3: + self.drone.y -= 0.20 + self.tello.altitude -= 0.20 + + def rotate(self, angle): + self.tello.rotate(angle) + self.drone.rotation_y = lerp(self.drone.rotation_y, self.drone.rotation_y + angle, 0.2) + + def update_pitch_roll(self): + self.drone.rotation_x = lerp(self.drone.rotation_x, self.pitch_angle, self.tilt_smoothness) + self.drone.rotation_z = lerp(self.drone.rotation_z, self.roll_angle, self.tilt_smoothness) + + #def land_drone(self): + # """ Land the drone when battery reaches zero """ + # while self.drone.y > 0: + # self.drone.y -= 0.1 + # self.update_meters() + # self.drone.y = 0 + # self.warning_text.text = "Landed." + + +app = Ursina() +window.color = color.rgb(135, 206, 235) +window.fullscreen = True + +window.borderless = False +window.fps_counter.enabled = False +window.render_mode = 'default' + + +Sky(texture='sky_sunset') + +tello_sim = TelloSimulator() +drone_sim = DroneSimulator(tello_sim) + +def input(key): + if key == 'h': + drone_sim.help_text.visible = not drone_sim.help_text.visible + if key == 'c': + drone_sim.toggle_camera_view() +def update(): + moving = False + rolling = False + + if held_keys['w']: + drone_sim.move("forward") + moving = True + if held_keys['s']: + drone_sim.move("backward") + moving = True + if held_keys['a']: + drone_sim.move("left") + rolling = True + if held_keys['d']: + drone_sim.move("right") + rolling = True + if held_keys['j']: + drone_sim.rotate(-drone_sim.rotation_speed) + if held_keys['l']: + drone_sim.rotate(drone_sim.rotation_speed) + if held_keys['shift']: + drone_sim.change_altitude("up") + if held_keys['control']: + drone_sim.change_altitude("down") + if not moving: + drone_sim.pitch_angle = 0 # Reset pitch when not moving + + if not rolling: + drone_sim.roll_angle = 0 # Reset roll when not rolling + + drone_sim.update_movement() + drone_sim.update_pitch_roll() + +app.run() diff --git a/src/tunnel_3.glb b/src/tunnel_3.glb new file mode 100644 index 0000000..c0ee19d Binary files /dev/null and b/src/tunnel_3.glb differ diff --git a/tello_drone.py b/tello_drone.py new file mode 100644 index 0000000..287c2ea --- /dev/null +++ b/tello_drone.py @@ -0,0 +1,458 @@ +from ursina import * +from time import time + +def lerp_color(start_color, end_color, factor): + """Custom color interpolation function""" + return Color( + start_color.r + (end_color.r - start_color.r) * factor, + start_color.g + (end_color.g - start_color.g) * factor, + start_color.b + (end_color.b - start_color.b) * factor, + 1 # Alpha channel + ) + + +class TelloSimulator: + + def __init__(self): + self.battery_level = 100 + self.altitude = 3 + self.speed = 0 + self.rotation_angle = 0 + self.is_flying = False + self.start_time = time() + + def connect(self): + print("Tello Simulator: Drone connected") + return True + + def get_battery(self): + elapsed_time = time() - self.start_time + self.battery_level = max(100 - int((elapsed_time / 3600) * 100), 0) # Reduce battery over 60 min + return self.battery_level + + def takeoff(self): + if not self.is_flying: + print("Tello Simulator: Drone taking off") + self.is_flying = True + return "Taking off" + return "Already in air" + + def land(self): + if self.is_flying: + print("Tello Simulator: Drone landing") + self.is_flying = False + return "Landing" + return "Already on ground" + + def move(self, direction, distance=1): + print(f"Tello Simulator: Moving {direction} by {distance} meters") + + def rotate(self, angle): + self.rotation_angle += angle + print(f"Tello Simulator: Rotating {angle} degrees") + + #def start_video_stream(self): + #print("Tello Simulator: Starting video stream...") + + # def get_video_frame(self): + #return "Simulated video frame" + + +class DroneSimulator(Entity): + def __init__(self, tello_api, **kwargs): + super().__init__() + + self.help_text = Text( + text="Controls:\nW - Forward\nS - Backward\nA - Left\nD - Right\nShift - Launch/Up\nCtrl - Down\nJ - Rotate Left\nL - Rotate Right\nC - FPV\nH - Toggle Help", + position=(-0.85, 0.43), # Top-left position + scale=1.2, + color=color.white, + visible=True + ) + + self.tello = tello_api + self.drone = Entity( + model='tello.glb', + scale=0.06, + position=(-15.4, 3, 5), + collider='box', + cast_shadow=True + ) + + + self.car = Entity( + model='dirty_car.glb', + scale=0.085, + position=(10, 2.45, 155), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + + self.truck = Entity( + model='road_roller.glb', + scale=4.0, + position=(-150, 2.45, 155), + rotation=(0, -90, 0), + collider='box', + cast_shadow=True + ) + + self.road_closed = Entity( + model='road_closed.glb', + scale=7.0, + position=(-15, 2, 315), + rotation=(0, 90, 0), + collider='box', + cast_shadow=True + ) + + + self.business_man = Entity( + model='business_man.glb', + scale=7.3, + position=(23, 12, 155), + rotation=(0, 55, 0), + collider='box', + cast_shadow=True + ) + + self.man = Entity( + model='bos_standing.glb', + scale=10.3, + position=(-83, 2.8, 165), + rotation=(0, 120, 0), + collider='box', + cast_shadow=True + ) + + self.patch = Entity( + model='pipeline_construction_site.glb', + scale=(15, 15, 12), + position=(-123, 0.0, 260), + rotation=(0, 0, 0), + collider='none', + cast_shadow=True + ) + + self.police_man = Entity( + model='pig.glb', + scale=10.0, + position=(-35, 1.7, 230), + rotation=(0, -70, 0), + collider='box', + cast_shadow=True + ) + + self.light1 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(-55, 2.5, 260), + rotation=(0, -90, 0), + collider='none', + cast_shadow=True + ) + + + self.light2 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(25, 2.5, 95), + rotation=(0, 90, 0), + collider='none', + cast_shadow=True + ) + + self.light3 = Entity( + model='street_light.glb', + scale=(4, 6.5, 5), + position=(-55, 2.5, -70), + rotation=(0, -90, 0), + collider='none', + cast_shadow=True + ) + + + + for i in range(3): + Entity( + model='cobblestone.glb', + scale=(5, 10, 20), + position=(30, 0, i * 158.5), + ) + for i in range(3): + Entity( + model='cobblestone.glb', + scale=(5, 10, 20), + position=(-58, 0, i * 158.5), + ) + + self.tunnel_road = Entity( + model='tunnel_3.glb', + scale=(63, 45, 45), + position=(-199, 3.0, 380), + rotation=(0, 0, 0), + collider='None', + cast_shadow=True + ) + + self.highway_road = Entity( + model='highway.glb', + scale=(20, 20, 5), + position=(-14, 2.5, 120), + rotation=(0, 90, 0), + collider='box', + cast_shadow=True + ) + + + self.barrier1 = Entity( + model='construction_barrier.glb', + scale=(3, 3, 3), + position=(24, 2.5, 315), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + self.barrier2 = Entity( + model='construction_barrier.glb', + scale=(3, 3, 3), + position=(-54, 2.5, 315), + rotation=(0, 0, 0), + collider='box', + cast_shadow=True + ) + + self.station = Entity( + model='gas_station_-_gta_v.glb', + scale=(12.7, 10, 10), + position=(-210, 2.5, 77), + rotation=(0, -90, 0), + ) + + + Entity( + model='dirty_leaking_concrete_wall.glb', + scale=(25, 20, 30), + position=(34.2, 2.5, 25), + rotation=(0, 90.5, 0), + collider='box', + cast_shadow=True + ) + + + Entity( + model='dirty_leaking_concrete_wall.glb', + scale=(25, 20, 30), + position=(34, 2.5, 227), + rotation=(0, 91, 0), + collider='box', + cast_shadow=True + ) + + self.first_person_view = False + # Create a separate entity to hold the camera + self.camera_holder = Entity(position=self.drone.position) + + self.drone_camera = EditorCamera() + self.drone_camera.parent = self.camera_holder + self.third_person_position = (0, 5, -15) + self.third_person_rotation = (10, 0, 0) + self.first_person_position = (0, 0.5, 22) + self.drone_camera.position = self.third_person_position + self.drone_camera.rotation = self.third_person_rotation + self.is_flying = False + + self.velocity = Vec3(0, 0, 0) + self.acceleration = Vec3(0, 0, 0) + self.drag = 0.93 + self.rotation_speed = 5 + self.max_speed = 1.8 + self.accel_force = 0.65 + + self.pitch_angle = 0 + self.roll_angle = 0 + self.max_pitch = 20 + self.max_roll = 20 + self.tilt_smoothness = 0.05 + + self.create_meters() + + def create_meters(self): + + # Main battery container + self.battery_container = Entity( + parent=camera.ui, + model=Quad(radius=0.01), + color=color.gray, + scale=(0.12, 0.04), + position=(0.74, 0.41), + z=-1 + ) + + # Battery cap + self.battery_cap = Entity( + parent=self.battery_container, + model=Quad(radius=0.004), + color=color.gray, + position=(0.52, 0), + scale=(0.05, 0.3), + rotation_z=0 + ) + + # Battery fill + self.battery_fill = Entity( + parent=self.battery_container, + model=Quad(radius=0.01), + color=color.green, + scale=(0.9, 0.7), + position=(-0.46, 0), + origin=(-0.5, 0), + z=-0.1 + ) + + # Altitude meter + self.altitude_meter = Text( + text=f"Altitude: {self.tello.altitude}m", + position=(0.67, 0.38), + scale=0.94, + color=color.white + ) + + # Warning text + self.warning_text = Text( + text="", + position=(-0.25, 0), + scale=3, + color=color.red + ) + + def update_meters(self): + """Update telemetry meters""" + battery = self.tello.get_battery() + + # Update battery fill width with padding + fill_width = 0.92 * (battery / 100) + self.battery_fill.scale_x = fill_width + + # color transitions (green → yellow → orange → red) + if battery > 60: + factor = (battery - 60) / 40 # 100-60%: green to yellow + col = lerp_color(color.yellow, color.green, factor) + elif battery > 30: + factor = (battery - 30) / 30 # 60-30%: yellow to orange + col = lerp_color(color.orange, color.yellow, factor) + else: + factor = battery / 30 # 30-0%: orange to red + col = lerp_color(color.red, color.orange, factor) + + self.battery_fill.color = col + + # Update altitude + self.altitude_meter.text = f"Altitude: {int(self.drone.y - 3)}m" + + # Battery warning + current_time = time() % 1 + if battery <= 10 and battery > 0: + if current_time < 0.5: + self.warning_text.text = "Battery Low!" + else: + self.warning_text.text = "" + print("\n========== Battery Low! ==========\n") + elif battery == 0: + self.warning_text.text = "Battery Depleted!" + print("\n========== Battery Depleted! ==========\n") + else: + self.warning_text.text = "" + + def update_movement(self): + self.velocity += self.acceleration + + if self.velocity.length() > self.max_speed: + self.velocity = self.velocity.normalized() * self.max_speed + + self.velocity *= self.drag + new_position = self.drone.position + self.velocity + hit_info = raycast(self.drone.position, self.velocity.normalized(), distance=self.velocity.length(), ignore=(self.drone,)) + + if not hit_info.hit: + self.drone.position = new_position + + if self.drone.y < 3: + self.drone.y = 3 + + self.acceleration = Vec3(0, 0, 0) + + # Apply pitch and roll to the drone + self.drone.rotation_x = lerp(self.drone.rotation_x, self.pitch_angle, self.tilt_smoothness) + self.drone.rotation_z = lerp(self.drone.rotation_z, self.roll_angle, self.tilt_smoothness) + + if self.first_person_view: + + self.camera_holder.position = self.drone.position + self.camera_holder.rotation_x = 0 # Keep horizon level + self.camera_holder.rotation_z = 0 # Prevent roll tilting + self.camera_holder.rotation_y = self.drone.rotation_y # yaw only + else: + # Third-person view + self.camera_holder.position = lerp(self.camera_holder.position, self.drone.position, 0.1) + self.camera_holder.rotation_y = self.drone.rotation_y # yaw only + self.drone_camera.rotation_x = 0 # Prevent pitch tilting + self.drone_camera.rotation_z = 0 # Prevent roll tilting + + self.update_meters() + + + def move(self, direction): + self.tello.move(direction, 1) + + if direction == "forward": + self.acceleration += self.drone.forward * self.accel_force + self.pitch_angle = self.max_pitch + elif direction == "backward": + self.acceleration -= self.drone.forward * self.accel_force + self.pitch_angle = -self.max_pitch + elif direction == "left": + self.acceleration -= self.drone.right * self.accel_force + self.roll_angle = -self.max_roll + elif direction == "right": + self.acceleration += self.drone.right * self.accel_force + self.roll_angle = self.max_roll + + def toggle_camera_view(self): + self.first_person_view = not self.first_person_view + if self.first_person_view: + # First-person view + self.drone_camera.position = self.first_person_position + self.drone_camera.rotation = (0, 0, 0) + else: + # Third-person view + self.drone_camera.position = self.third_person_position + self.drone_camera.rotation = self.third_person_rotation + + def change_altitude(self, direction): + if direction == "up": + self.drone.y += 0.27 + self.tello.altitude += 0.27 + elif direction == "down" and self.drone.y > 3: + self.drone.y -= 0.20 + self.tello.altitude -= 0.20 + + def rotate(self, angle): + self.tello.rotate(angle) + self.drone.rotation_y = lerp(self.drone.rotation_y, self.drone.rotation_y + angle, 0.2) + + def update_pitch_roll(self): + self.drone.rotation_x = lerp(self.drone.rotation_x, self.pitch_angle, self.tilt_smoothness) + self.drone.rotation_z = lerp(self.drone.rotation_z, self.roll_angle, self.tilt_smoothness) + + #def land_drone(self): + # """ Land the drone when battery reaches zero """ + # while self.drone.y > 0: + # self.drone.y -= 0.1 + # self.update_meters() + # self.drone.y = 0 + # self.warning_text.text = "Landed." +