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."