Code reorganization. Display recording fixes. Search Flight Planner Fixes. Bug Fixes
This commit is contained in:
@@ -1,8 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recorder — captures simulation video and logs, and uploads to local REST API backend.
|
||||
|
||||
Each run gets a temporary folder, and all resources are uploaded directly to the backend.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -18,6 +14,7 @@ import requests
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import yaml
|
||||
import signal
|
||||
|
||||
try:
|
||||
import PIL.Image
|
||||
@@ -36,7 +33,6 @@ class RunRecorder:
|
||||
self.search_start = 0.0
|
||||
self.search_duration = 0.0
|
||||
|
||||
# Gather config data as JSON
|
||||
config_data = {}
|
||||
for cfg in ["search.yaml", "ugv.yaml", "uav.yaml"]:
|
||||
p = project_dir / "config" / cfg
|
||||
@@ -49,10 +45,10 @@ class RunRecorder:
|
||||
config_data[key] = data
|
||||
except Exception as e:
|
||||
print(f"[REC] Failed to load config {cfg}: {e}")
|
||||
|
||||
|
||||
if "ugv" in config_data and "topics" in config_data["ugv"]:
|
||||
del config_data["ugv"]["topics"]
|
||||
|
||||
|
||||
if "uav" in config_data and "connection" in config_data["uav"]:
|
||||
del config_data["uav"]["connection"]
|
||||
|
||||
@@ -63,7 +59,7 @@ class RunRecorder:
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
self.sim_id = data.get("id", 0)
|
||||
self.sim_name = data.get("name", f"simulation_{self.sim_id}")
|
||||
self.sim_name = f"simulation_{self.sim_id}"
|
||||
except Exception as e:
|
||||
print(f"[REC] API create failed: {e}")
|
||||
|
||||
@@ -72,9 +68,9 @@ class RunRecorder:
|
||||
else:
|
||||
self.run_dir = project_dir / "results" / self.sim_name
|
||||
self.run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
self.run_num = self.sim_id
|
||||
|
||||
|
||||
print(f"[REC] Recording local results to {self.run_dir.name} and API ({self.sim_name}, ID: {self.sim_id})")
|
||||
self._log_path = self.run_dir / "log.txt"
|
||||
self._log_file = open(self._log_path, "w")
|
||||
@@ -86,7 +82,7 @@ class RunRecorder:
|
||||
self._tracker_writer = None
|
||||
self._camera_writer = None
|
||||
self._gazebo_writer = None
|
||||
|
||||
|
||||
self._tracker_size = None
|
||||
self._camera_size = None
|
||||
self._gazebo_size = None
|
||||
@@ -105,21 +101,18 @@ class RunRecorder:
|
||||
self._record_thread = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# Gazebo window capture via xwd
|
||||
self._gazebo_wid = None
|
||||
self._find_gazebo_window()
|
||||
threading.Thread(target=self._upload_hardware_info, daemon=True).start()
|
||||
|
||||
def _upload_hardware_info(self):
|
||||
if not self.sim_id:
|
||||
return
|
||||
|
||||
|
||||
payload = {
|
||||
"cpu_info": "Unknown CPU",
|
||||
"gpu_info": "Unknown GPU",
|
||||
"ram_info": "Unknown RAM"
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
cpu = "Unknown"
|
||||
@@ -127,19 +120,19 @@ class RunRecorder:
|
||||
cpu = subprocess.check_output("grep -m 1 'model name' /proc/cpuinfo | cut -d ':' -f 2", shell=True, timeout=2).decode('utf-8').strip()
|
||||
except Exception: pass
|
||||
if cpu: payload["cpu_info"] = cpu
|
||||
|
||||
|
||||
try:
|
||||
ram_kb = int(subprocess.check_output("awk '/MemTotal/ {print $2}' /proc/meminfo", shell=True, timeout=2))
|
||||
payload["ram_info"] = f"{round(ram_kb / 1024 / 1024, 1)} GB"
|
||||
except Exception: pass
|
||||
|
||||
|
||||
try:
|
||||
gpu = subprocess.check_output("lspci | grep -i vga | cut -d ':' -f 3", shell=True, timeout=2).decode('utf-8').strip()
|
||||
if gpu: payload["gpu_info"] = gpu
|
||||
except Exception: pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
requests.put(f"{API_URL}/api/simulations/{self.sim_id}/hardware", json=payload, timeout=5)
|
||||
except Exception as e:
|
||||
@@ -151,8 +144,8 @@ class RunRecorder:
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
resp = requests.post(
|
||||
f"{API_URL}/api/simulations/{self.sim_id}/upload",
|
||||
files={"file": (filename, f)},
|
||||
f"{API_URL}/api/simulations/{self.sim_id}/upload",
|
||||
files={"file": (filename, f)},
|
||||
timeout=60
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
@@ -160,23 +153,41 @@ class RunRecorder:
|
||||
except Exception as e:
|
||||
print(f"[REC] Upload failed for {filename}: {e}")
|
||||
|
||||
def _find_gazebo_window(self):
|
||||
def _find_gazebo_window_ffmpeg(self):
|
||||
try:
|
||||
cmd = "xwininfo -root -tree | grep -i 'gazebo\|gz sim'"
|
||||
output = subprocess.check_output(cmd, shell=True).decode('utf-8')
|
||||
lines = output.strip().split('\n')
|
||||
lines = [l for l in output.strip().split('\n') if "1x1" not in l and "Gazebo Sim" in l]
|
||||
if not lines:
|
||||
print("[REC] Gazebo window not found")
|
||||
return
|
||||
return None
|
||||
|
||||
wid_line = lines[0]
|
||||
wid_match = re.search(r'(0x[0-9a-fA-F]+)', wid_line)
|
||||
if not wid_match:
|
||||
return
|
||||
self._gazebo_wid = wid_match.group(1)
|
||||
print(f"[REC] Found Gazebo window ID: {self._gazebo_wid}")
|
||||
except Exception as e:
|
||||
print(f"[REC] Error finding Gazebo window: {e}")
|
||||
if wid_match:
|
||||
return wid_match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _get_window_geometry(self, window_id):
|
||||
try:
|
||||
geo = subprocess.run(
|
||||
["xdotool", "getwindowgeometry", window_id],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
x = y = width = height = None
|
||||
for line in geo.stdout.splitlines():
|
||||
if "Position:" in line:
|
||||
pos = line.split("Position:")[1].strip().split()[0]
|
||||
x, y = map(int, pos.split(","))
|
||||
elif "Geometry:" in line:
|
||||
size = line.split("Geometry:")[1].strip()
|
||||
width, height = map(int, size.split("x"))
|
||||
if None in (x, y, width, height):
|
||||
return None
|
||||
return x, y, width, height
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def set_phase(self, phase: str):
|
||||
if phase == "takeoff":
|
||||
@@ -201,9 +212,57 @@ class RunRecorder:
|
||||
self._tracker_ref = tracker
|
||||
self._camera_ref = camera
|
||||
self._recording = True
|
||||
|
||||
if not self._gazebo_wid:
|
||||
self._find_gazebo_window()
|
||||
|
||||
window_id = self._find_gazebo_window_ffmpeg()
|
||||
geo = None
|
||||
if window_id:
|
||||
geo = self._get_window_geometry(window_id)
|
||||
|
||||
if geo:
|
||||
x, y, width, height = geo
|
||||
width += width % 2
|
||||
height += height % 2
|
||||
else:
|
||||
x, y, width, height = 0, 0, 1920, 1080
|
||||
|
||||
self.gazebo_output_file = str(self.run_dir / "gazebo.mp4")
|
||||
display = os.environ.get("DISPLAY", ":0")
|
||||
if not display.endswith(".0"):
|
||||
display += ".0"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-f", "x11grab"
|
||||
]
|
||||
|
||||
if window_id:
|
||||
cmd.extend([
|
||||
"-window_id", str(window_id),
|
||||
"-r", str(self.fps),
|
||||
"-i", display,
|
||||
])
|
||||
else:
|
||||
cmd.extend([
|
||||
"-r", str(self.fps),
|
||||
"-s", f"{width}x{height}",
|
||||
"-i", f"{display}+{x},{y}",
|
||||
])
|
||||
|
||||
cmd.extend([
|
||||
"-c:v", "libx264",
|
||||
"-preset", "ultrafast",
|
||||
"-crf", "18",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
|
||||
"-movflags", "+faststart",
|
||||
self.gazebo_output_file
|
||||
])
|
||||
try:
|
||||
self._gazebo_ffmpeg_proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
print(f"[REC] Started recording Gazebo window via ffmpeg to gazebo.mp4 (ID: {window_id})")
|
||||
except Exception as e:
|
||||
print(f"[REC] Failed to start ffmpeg: {e}")
|
||||
self._gazebo_ffmpeg_proc = None
|
||||
|
||||
self._record_thread = threading.Thread(target=self._record_loop, daemon=True)
|
||||
self._record_thread.start()
|
||||
@@ -231,26 +290,6 @@ class RunRecorder:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._gazebo_wid and HAS_PIL:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
['xwd', '-id', self._gazebo_wid, '-out', '/dev/stdout'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
img_pil = PIL.Image.open(io.BytesIO(proc.stdout))
|
||||
img_np = np.array(img_pil)
|
||||
if img_pil.mode == 'RGB':
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
elif img_pil.mode == 'RGBA':
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGBA2BGR)
|
||||
else:
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
self._write_gazebo_frame(img_bgr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.time() - t0
|
||||
sleep_time = max(0, interval - elapsed)
|
||||
time.sleep(sleep_time)
|
||||
@@ -275,23 +314,6 @@ class RunRecorder:
|
||||
self._camera_writer.write(frame)
|
||||
self._camera_frames += 1
|
||||
|
||||
def _write_gazebo_frame(self, frame):
|
||||
h, w = frame.shape[:2]
|
||||
if w % 2 != 0: w -= 1
|
||||
if h % 2 != 0: h -= 1
|
||||
frame = frame[:h, :w]
|
||||
|
||||
if self._gazebo_writer is None:
|
||||
self._gazebo_size = (w, h)
|
||||
fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
||||
path = str(self.run_dir / "gazebo.avi")
|
||||
self._gazebo_writer = cv2.VideoWriter(path, fourcc, self.fps, (w, h))
|
||||
elif (w, h) != self._gazebo_size:
|
||||
frame = cv2.resize(frame, self._gazebo_size)
|
||||
|
||||
self._gazebo_writer.write(frame)
|
||||
self._gazebo_frames += 1
|
||||
|
||||
def snapshot_camera(self, label="snapshot"):
|
||||
if self._camera_ref is None:
|
||||
return
|
||||
@@ -322,6 +344,14 @@ class RunRecorder:
|
||||
if self._record_thread:
|
||||
self._record_thread.join(timeout=3.0)
|
||||
|
||||
if hasattr(self, '_gazebo_ffmpeg_proc') and self._gazebo_ffmpeg_proc:
|
||||
try:
|
||||
self._gazebo_ffmpeg_proc.send_signal(signal.SIGINT)
|
||||
self._gazebo_ffmpeg_proc.wait(timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
self._upload_file(self.gazebo_output_file, "gazebo.mp4")
|
||||
|
||||
if self._last_tracker_frame is not None:
|
||||
filename = "flight_path.png"
|
||||
path = self.run_dir / filename
|
||||
@@ -341,9 +371,6 @@ class RunRecorder:
|
||||
if self._camera_writer:
|
||||
self._camera_writer.release()
|
||||
self._upload_file(self.run_dir / "camera.avi", "camera.avi")
|
||||
if self._gazebo_writer:
|
||||
self._gazebo_writer.release()
|
||||
self._upload_file(self.run_dir / "gazebo.avi", "gazebo.avi")
|
||||
|
||||
self.stop_logging()
|
||||
self._log_file.close()
|
||||
@@ -356,8 +383,8 @@ class RunRecorder:
|
||||
if self.sim_id:
|
||||
try:
|
||||
requests.put(
|
||||
f"{API_URL}/api/simulations/{self.sim_id}/time",
|
||||
json={"search_time": self.search_duration, "total_time": duration},
|
||||
f"{API_URL}/api/simulations/{self.sim_id}/time",
|
||||
json={"search_time": self.search_duration, "total_time": duration},
|
||||
timeout=5
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -369,7 +396,7 @@ class RunRecorder:
|
||||
f"[REC] Duration: {mins}m {secs}s\n"
|
||||
f"[REC] Tracker: {self._tracker_frames} frames\n"
|
||||
f"[REC] Camera: {self._camera_frames} frames\n"
|
||||
f"[REC] Gazebo: {self._gazebo_frames} frames\n"
|
||||
f"[REC] Gazebo: via ffmpeg (.mp4)\n"
|
||||
f"[REC] ═══════════════════════════════════════\n"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user