refined the music player, probably going to gut the pause and seek functionality since everything else about it works perfectly. now i need to fix the start of the walk cycle toppling the robot and the low battery errors being so loud and repetitive they drown out the music
This commit is contained in:
+225
-63
@@ -10,9 +10,10 @@ import queue
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pygame
|
import pygame
|
||||||
import yt_dlp
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
# --- Audio for mic ---
|
# --- Audio for mic ---
|
||||||
try:
|
try:
|
||||||
@@ -137,87 +138,168 @@ class NaoMusicPlayer:
|
|||||||
self.player = session.service("ALAudioPlayer")
|
self.player = session.service("ALAudioPlayer")
|
||||||
self.nao_ip = nao_ip
|
self.nao_ip = nao_ip
|
||||||
self.music_dir = "/home/nao/music"
|
self.music_dir = "/home/nao/music"
|
||||||
|
self.local_path = "/tmp/song.mp3"
|
||||||
|
self.remote_path = f"{self.music_dir}/song.mp3"
|
||||||
try:
|
try:
|
||||||
subprocess.run(["sshpass", "-p", "nao", "ssh", f"nao@{nao_ip}", f"mkdir -p {self.music_dir}"],
|
subprocess.run(["sshpass", "-p", "nao", "ssh", f"nao@{nao_ip}", f"mkdir -p {self.music_dir}"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
except: pass
|
except: pass
|
||||||
self.current_file = None
|
self.current_file = None
|
||||||
|
self.current_task_id = None
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
|
self.paused_position = 0.0
|
||||||
self.current_title = ""
|
self.current_title = ""
|
||||||
|
self.is_loading = False
|
||||||
|
self.load_error = None
|
||||||
|
self.status = ""
|
||||||
|
self.progress = 0.0
|
||||||
|
|
||||||
def search(self, query):
|
def search(self, query):
|
||||||
try:
|
try:
|
||||||
ydl_opts = {
|
cmd = ["yt-dlp", f"ytsearch5:{query}", "--flat-playlist", "--dump-json",
|
||||||
'format': 'bestaudio/best',
|
"--no-warnings", "--quiet"]
|
||||||
'quiet': True,
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
'noplaylist': True,
|
entries = []
|
||||||
'extract_flat': True,
|
for line in result.stdout.strip().splitlines():
|
||||||
}
|
if not line.strip():
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
continue
|
||||||
info = ydl.extract_info(f"ytsearch5:{query}", download=False)
|
try:
|
||||||
return info.get('entries', [])
|
data = json.loads(line)
|
||||||
except:
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
vid = data.get("id")
|
||||||
|
url = data.get("url")
|
||||||
|
if not url or not url.startswith("http"):
|
||||||
|
url = f"https://www.youtube.com/watch?v={vid}" if vid else None
|
||||||
|
if url:
|
||||||
|
entries.append({"url": url, "title": data.get("title", "Unknown")})
|
||||||
|
return entries
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Search error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
_DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%')
|
||||||
|
|
||||||
def play(self, url, title):
|
def play(self, url, title):
|
||||||
self.stop()
|
self.stop()
|
||||||
self.current_title = title
|
self.current_title = title
|
||||||
|
self.is_loading = True
|
||||||
|
self.load_error = None
|
||||||
|
self.status = "Starting download..."
|
||||||
|
self.progress = 0.0
|
||||||
try:
|
try:
|
||||||
ydl_opts = {
|
if os.path.exists(self.local_path):
|
||||||
'format': 'bestaudio/best',
|
os.remove(self.local_path)
|
||||||
'outtmpl': '/tmp/%(id)s.%(ext)s',
|
except: pass
|
||||||
'quiet': True,
|
|
||||||
'noplaylist': True,
|
|
||||||
'extractaudio': True,
|
|
||||||
'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3'}],
|
|
||||||
'ignoreerrors': True,
|
|
||||||
'no_warnings': True,
|
|
||||||
}
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
||||||
info = ydl.extract_info(url, download=True)
|
|
||||||
if not info:
|
|
||||||
print("Download failed")
|
|
||||||
return False
|
|
||||||
downloaded = ydl.prepare_filename(info)
|
|
||||||
|
|
||||||
remote_path = f"{self.music_dir}/{int(time.time())}.mp3"
|
try:
|
||||||
result = subprocess.run(["sshpass", "-p", "nao", "scp", downloaded, f"nao@{self.nao_ip}:{remote_path}"],
|
cmd = ["yt-dlp", url,
|
||||||
capture_output=True, text=True)
|
"-x", "--audio-format", "mp3",
|
||||||
if result.returncode != 0:
|
"-o", self.local_path,
|
||||||
print(f"SCP failed: {result.stderr}")
|
"--no-playlist", "--no-warnings", "--newline"]
|
||||||
|
print(f"\n▶ yt-dlp {url}")
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
text=True, bufsize=1)
|
||||||
|
for line in proc.stdout:
|
||||||
|
line = line.rstrip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
print(f" [yt-dlp] {line}")
|
||||||
|
m = self._DL_PROGRESS_RE.search(line)
|
||||||
|
if m:
|
||||||
|
self.progress = float(m.group(1))
|
||||||
|
self.status = f"Downloading... {self.progress:.0f}%"
|
||||||
|
elif "ExtractAudio" in line or "Extracting audio" in line:
|
||||||
|
self.status = "Converting to mp3..."
|
||||||
|
elif line.startswith("[download] Destination"):
|
||||||
|
self.status = "Downloading..."
|
||||||
|
proc.wait(timeout=120)
|
||||||
|
|
||||||
|
if proc.returncode != 0 or not os.path.exists(self.local_path):
|
||||||
|
print(f"Download failed (exit {proc.returncode})")
|
||||||
|
self.load_error = "Download failed"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.current_file = remote_path
|
size_mb = os.path.getsize(self.local_path) / (1024 * 1024)
|
||||||
self.player.playFile(remote_path)
|
print(f"▶ scp {self.local_path} -> nao@{self.nao_ip}:{self.remote_path} ({size_mb:.1f} MB)")
|
||||||
|
self.progress = 0.0
|
||||||
|
upload_start = time.time()
|
||||||
|
stop_ticker = threading.Event()
|
||||||
|
|
||||||
|
def ticker():
|
||||||
|
while not stop_ticker.is_set():
|
||||||
|
elapsed = time.time() - upload_start
|
||||||
|
self.status = f"Uploading to NAO ({size_mb:.1f} MB)... {elapsed:.0f}s"
|
||||||
|
stop_ticker.wait(1.0)
|
||||||
|
|
||||||
|
t = threading.Thread(target=ticker, daemon=True)
|
||||||
|
t.start()
|
||||||
|
scp_result = subprocess.run(
|
||||||
|
["sshpass", "-p", "nao", "scp", self.local_path, f"nao@{self.nao_ip}:{self.remote_path}"],
|
||||||
|
capture_output=True, text=True, timeout=60
|
||||||
|
)
|
||||||
|
stop_ticker.set()
|
||||||
|
t.join(timeout=1.0)
|
||||||
|
print(f" [scp] exit {scp_result.returncode}" +
|
||||||
|
(f": {scp_result.stderr.strip()}" if scp_result.stderr.strip() else ""))
|
||||||
|
if scp_result.returncode != 0:
|
||||||
|
print(f"SCP failed: {scp_result.stderr.strip()}")
|
||||||
|
self.load_error = "SCP failed"
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.current_file = self.remote_path
|
||||||
|
self.status = "Playing"
|
||||||
|
self.progress = 100.0
|
||||||
|
self.current_task_id = self.player.playFile(self.remote_path)
|
||||||
self.is_playing = True
|
self.is_playing = True
|
||||||
print(f"♪ Playing on NAO: {title}")
|
print(f"♪ Playing on NAO: {title} (task {self.current_task_id})")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Playback error: {e}")
|
print(f"Playback error: {e}")
|
||||||
|
self.load_error = str(e)
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
self.is_loading = False
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
if not self.current_file: return
|
if self.current_task_id is None or not self.current_file:
|
||||||
|
print("⏸ Nothing loaded to pause/resume")
|
||||||
|
return
|
||||||
|
try:
|
||||||
if self.is_playing:
|
if self.is_playing:
|
||||||
self.player.pause()
|
pos = self.player.getCurrentPosition(self.current_task_id)
|
||||||
|
self.player.stopAll()
|
||||||
|
self.paused_position = max(0.0, pos)
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
|
print(f"♪ Paused at {self.paused_position:.1f}s")
|
||||||
else:
|
else:
|
||||||
self.player.play(self.current_file)
|
self.current_task_id = self.player.playFileFromPosition(
|
||||||
|
self.current_file, self.paused_position, 1.0, 0.0
|
||||||
|
)
|
||||||
self.is_playing = True
|
self.is_playing = True
|
||||||
|
print(f"♪ Resumed from {self.paused_position:.1f}s (task {self.current_task_id})")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Pause/resume error: {e}")
|
||||||
|
self.load_error = f"Pause error: {e}"
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
try:
|
try:
|
||||||
self.player.stopAll()
|
self.player.stopAll()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
print(f"Stop error: {e}")
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
self.current_file = None
|
self.current_file = None
|
||||||
|
self.current_task_id = None
|
||||||
|
self.paused_position = 0.0
|
||||||
|
|
||||||
def seek(self, seconds):
|
def seek(self, seconds):
|
||||||
|
if self.current_task_id is None:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
pos = self.player.getCurrentPosition() / 1000.0
|
pos = self.player.getCurrentPosition(self.current_task_id)
|
||||||
self.player.goTo(pos + seconds)
|
self.player.goTo(self.current_task_id, max(0.0, pos + seconds))
|
||||||
except: pass
|
except Exception as e:
|
||||||
|
print(f"Seek error: {e}")
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -256,6 +338,7 @@ class NaoTeleop:
|
|||||||
self.chat_message = ""
|
self.chat_message = ""
|
||||||
self.music_search_mode = False
|
self.music_search_mode = False
|
||||||
self.music_search_text = ""
|
self.music_search_text = ""
|
||||||
|
self.music_search_pending = False
|
||||||
|
|
||||||
self.video_scale = video_scale
|
self.video_scale = video_scale
|
||||||
self.display_width = int(320 * video_scale)
|
self.display_width = int(320 * video_scale)
|
||||||
@@ -343,12 +426,18 @@ class NaoTeleop:
|
|||||||
if keys[pygame.K_j]: self.head_yaw = min(self.head_yaw + speed, 2.0); changed = True
|
if keys[pygame.K_j]: self.head_yaw = min(self.head_yaw + speed, 2.0); changed = True
|
||||||
if keys[pygame.K_l]: self.head_yaw = max(self.head_yaw - speed, -2.0); changed = True
|
if keys[pygame.K_l]: self.head_yaw = max(self.head_yaw - speed, -2.0); changed = True
|
||||||
if changed:
|
if changed:
|
||||||
|
try:
|
||||||
self.motion.setAngles(["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3)
|
self.motion.setAngles(["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Head move error: {e}")
|
||||||
|
|
||||||
def reset_head(self):
|
def reset_head(self):
|
||||||
self.head_yaw = 0.0
|
self.head_yaw = 0.0
|
||||||
self.head_pitch = 0.0
|
self.head_pitch = 0.0
|
||||||
|
try:
|
||||||
self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4)
|
self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Head reset error: {e}")
|
||||||
|
|
||||||
def update_title(self):
|
def update_title(self):
|
||||||
mode = "[TYPING] " if self.typing_mode else ""
|
mode = "[TYPING] " if self.typing_mode else ""
|
||||||
@@ -359,7 +448,8 @@ class NaoTeleop:
|
|||||||
if self.audio_device:
|
if self.audio_device:
|
||||||
try:
|
try:
|
||||||
self.audio_device.setOutputVolume(self.volume)
|
self.audio_device.setOutputVolume(self.volume)
|
||||||
except: pass
|
except Exception as e:
|
||||||
|
print(f"Volume set error: {e}")
|
||||||
print(f"🔊 NAO volume: {self.volume}%")
|
print(f"🔊 NAO volume: {self.volume}%")
|
||||||
self.update_title()
|
self.update_title()
|
||||||
|
|
||||||
@@ -381,9 +471,11 @@ class NaoTeleop:
|
|||||||
|
|
||||||
carpet_config = [["StepHeight", 0.028], ["MaxStepFrequency", 0.6], ["TorsoWy", 0.05]]
|
carpet_config = [["StepHeight", 0.028], ["MaxStepFrequency", 0.6], ["TorsoWy", 0.05]]
|
||||||
|
|
||||||
print("\n🎮 Controls: WASD=Walk | IJKL=Head | SPACE=Reset head | 1=Wave | M=Music search | P=Pause | X=Stop | ←→=Seek | T=TTS | ESC=Quit")
|
print("\n🎮 Controls: WASD/Arrows=Walk | IJKL=Head | SPACE=Reset head | 1=Wave | M=Music search (↑↓ select, Enter confirm) | P=Pause | X=Stop | ,/.=Seek | -/==Volume | T=TTS | ESC=Quit")
|
||||||
|
|
||||||
|
try:
|
||||||
while self.running:
|
while self.running:
|
||||||
|
try:
|
||||||
self.check_battery()
|
self.check_battery()
|
||||||
|
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
@@ -392,31 +484,57 @@ class NaoTeleop:
|
|||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if self.music_search_mode:
|
if self.music_search_mode:
|
||||||
if event.key == pygame.K_RETURN:
|
if event.key == pygame.K_RETURN:
|
||||||
if self.music_search_text.strip():
|
|
||||||
self.music_results = self.music.search(self.music_search_text)
|
|
||||||
if self.music_results:
|
if self.music_results:
|
||||||
self.selected_result = 0
|
entry = self.music_results[self.selected_result]
|
||||||
else:
|
threading.Thread(
|
||||||
|
target=self.music.play,
|
||||||
|
args=(entry['url'], entry.get('title', 'Unknown')),
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
self.music_search_mode = False
|
self.music_search_mode = False
|
||||||
|
elif self.music_search_text.strip() and not self.music_search_pending:
|
||||||
|
query = self.music_search_text
|
||||||
|
self.music_search_pending = True
|
||||||
|
def do_search(q=query):
|
||||||
|
results = self.music.search(q)
|
||||||
|
self.music_results = results
|
||||||
|
self.selected_result = 0
|
||||||
|
self.music_search_pending = False
|
||||||
|
if not results:
|
||||||
|
self.music_search_mode = False
|
||||||
|
threading.Thread(target=do_search, daemon=True).start()
|
||||||
else:
|
else:
|
||||||
self.music_search_mode = False
|
self.music_search_mode = False
|
||||||
elif event.key == pygame.K_ESCAPE:
|
elif event.key == pygame.K_ESCAPE:
|
||||||
self.music_search_mode = False
|
self.music_search_mode = False
|
||||||
self.music_search_text = ""
|
self.music_search_text = ""
|
||||||
|
self.music_results = []
|
||||||
|
elif event.key == pygame.K_UP:
|
||||||
|
if self.music_results:
|
||||||
|
self.selected_result = (self.selected_result - 1) % len(self.music_results[:5])
|
||||||
|
elif event.key == pygame.K_DOWN:
|
||||||
|
if self.music_results:
|
||||||
|
self.selected_result = (self.selected_result + 1) % len(self.music_results[:5])
|
||||||
elif event.key == pygame.K_BACKSPACE:
|
elif event.key == pygame.K_BACKSPACE:
|
||||||
self.music_search_text = self.music_search_text[:-1]
|
self.music_search_text = self.music_search_text[:-1]
|
||||||
|
self.music_results = []
|
||||||
elif event.key in (pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5):
|
elif event.key in (pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5):
|
||||||
idx = int(event.unicode) - 1
|
idx = int(event.unicode) - 1
|
||||||
if idx < len(self.music_results):
|
if idx < len(self.music_results):
|
||||||
entry = self.music_results[idx]
|
entry = self.music_results[idx]
|
||||||
self.music.play(entry['url'], entry.get('title', 'Unknown'))
|
threading.Thread(
|
||||||
|
target=self.music.play,
|
||||||
|
args=(entry['url'], entry.get('title', 'Unknown')),
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
self.music_search_mode = False
|
self.music_search_mode = False
|
||||||
else:
|
elif event.unicode.isprintable():
|
||||||
self.music_search_text += event.unicode
|
self.music_search_text += event.unicode
|
||||||
|
self.music_results = []
|
||||||
elif self.typing_mode:
|
elif self.typing_mode:
|
||||||
if event.key == pygame.K_RETURN:
|
if event.key == pygame.K_RETURN:
|
||||||
if self.chat_message.strip():
|
if self.chat_message.strip():
|
||||||
threading.Thread(target=self.tts.say, args=(self.chat_message,)).start()
|
threading.Thread(target=self.tts.say, args=(self.chat_message,), daemon=True).start()
|
||||||
self.chat_message = ""
|
self.chat_message = ""
|
||||||
self.typing_mode = False
|
self.typing_mode = False
|
||||||
elif event.key == pygame.K_ESCAPE:
|
elif event.key == pygame.K_ESCAPE:
|
||||||
@@ -441,13 +559,20 @@ class NaoTeleop:
|
|||||||
self.music.pause()
|
self.music.pause()
|
||||||
elif event.key == pygame.K_x:
|
elif event.key == pygame.K_x:
|
||||||
self.music.stop()
|
self.music.stop()
|
||||||
elif event.key == pygame.K_LEFT:
|
elif event.key == pygame.K_COMMA:
|
||||||
self.music.seek(-10)
|
self.music.seek(-10)
|
||||||
elif event.key == pygame.K_RIGHT:
|
elif event.key == pygame.K_PERIOD:
|
||||||
self.music.seek(10)
|
self.music.seek(10)
|
||||||
|
elif event.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
|
||||||
|
self.change_volume(-5)
|
||||||
|
elif event.key in (pygame.K_EQUALS, pygame.K_KP_PLUS):
|
||||||
|
self.change_volume(5)
|
||||||
elif event.key == pygame.K_t:
|
elif event.key == pygame.K_t:
|
||||||
self.typing_mode = True
|
self.typing_mode = True
|
||||||
|
try:
|
||||||
self.motion.stopMove()
|
self.motion.stopMove()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"stopMove error: {e}")
|
||||||
|
|
||||||
if not self.typing_mode and not self.music_search_mode:
|
if not self.typing_mode and not self.music_search_mode:
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
@@ -457,10 +582,13 @@ class NaoTeleop:
|
|||||||
if keys[pygame.K_a] or keys[pygame.K_LEFT]: theta = 0.5
|
if keys[pygame.K_a] or keys[pygame.K_LEFT]: theta = 0.5
|
||||||
if keys[pygame.K_d] or keys[pygame.K_RIGHT]: theta = -0.5
|
if keys[pygame.K_d] or keys[pygame.K_RIGHT]: theta = -0.5
|
||||||
|
|
||||||
|
try:
|
||||||
if abs(x) > 0.1 or abs(theta) > 0.1:
|
if abs(x) > 0.1 or abs(theta) > 0.1:
|
||||||
self.motion.moveToward(x, y, theta, carpet_config)
|
self.motion.moveToward(x, y, theta, carpet_config)
|
||||||
else:
|
else:
|
||||||
self.motion.stopMove()
|
self.motion.stopMove()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Move error: {e}")
|
||||||
self.handle_head_movement()
|
self.handle_head_movement()
|
||||||
|
|
||||||
frame = self.get_image()
|
frame = self.get_image()
|
||||||
@@ -473,14 +601,26 @@ class NaoTeleop:
|
|||||||
self.screen.fill((10, 10, 30))
|
self.screen.fill((10, 10, 30))
|
||||||
|
|
||||||
# Music status
|
# Music status
|
||||||
if self.music.current_title:
|
if self.music.is_loading:
|
||||||
|
text = self.font.render(f"♪ {self.music.status}", True, (255, 200, 0))
|
||||||
|
self.screen.blit(text, (10, 10))
|
||||||
|
if self.music.progress > 0:
|
||||||
|
bar_w = 220
|
||||||
|
pct = min(self.music.progress, 100) / 100.0
|
||||||
|
pygame.draw.rect(self.screen, (70, 70, 70), (10, 32, bar_w, 8))
|
||||||
|
pygame.draw.rect(self.screen, (255, 200, 0), (10, 32, int(bar_w * pct), 8))
|
||||||
|
elif self.music.load_error:
|
||||||
|
text = self.font.render(f"♪ Error: {self.music.load_error}", True, (255, 80, 80))
|
||||||
|
self.screen.blit(text, (10, 10))
|
||||||
|
elif self.music.current_title:
|
||||||
status = f"♪ {self.music.current_title[:45]} {'(Paused)' if not self.music.is_playing else ''}"
|
status = f"♪ {self.music.current_title[:45]} {'(Paused)' if not self.music.is_playing else ''}"
|
||||||
text = self.font.render(status, True, (0, 255, 100))
|
text = self.font.render(status, True, (0, 255, 100))
|
||||||
self.screen.blit(text, (10, 10))
|
self.screen.blit(text, (10, 10))
|
||||||
|
|
||||||
# Music search interface
|
# Music search interface
|
||||||
if self.music_search_mode:
|
if self.music_search_mode:
|
||||||
s = pygame.Surface((self.display_width, self.display_height//2))
|
box_h = min(self.display_height - 80, 260)
|
||||||
|
s = pygame.Surface((self.display_width, box_h))
|
||||||
s.set_alpha(220)
|
s.set_alpha(220)
|
||||||
s.fill((0, 0, 40))
|
s.fill((0, 0, 40))
|
||||||
self.screen.blit(s, (0, 80))
|
self.screen.blit(s, (0, 80))
|
||||||
@@ -489,14 +629,24 @@ class NaoTeleop:
|
|||||||
self.screen.blit(text, (20, 100))
|
self.screen.blit(text, (20, 100))
|
||||||
|
|
||||||
if self.music_results:
|
if self.music_results:
|
||||||
for i, entry in enumerate(self.music_results[:5]):
|
shown = self.music_results[:5]
|
||||||
|
for i, entry in enumerate(shown):
|
||||||
|
row_y = 140 + i * 30
|
||||||
|
if i == self.selected_result:
|
||||||
|
pygame.draw.rect(self.screen, (90, 90, 0), (16, row_y - 3, self.display_width - 32, 26))
|
||||||
color = (255, 255, 0) if i == self.selected_result else (200, 200, 200)
|
color = (255, 255, 0) if i == self.selected_result else (200, 200, 200)
|
||||||
title = entry.get('title', 'No title')[:60]
|
title = entry.get('title', 'No title')[:60]
|
||||||
line = self.font.render(f"{i+1}. {title}", True, color)
|
line = self.font.render(f"{i+1}. {title}", True, color)
|
||||||
self.screen.blit(line, (20, 140 + i*30))
|
self.screen.blit(line, (20, row_y))
|
||||||
else:
|
hint_y = 140 + len(shown) * 30 + 6
|
||||||
|
hint = self.font.render("\u2191\u2193 Select Enter Confirm 1-5 Quick pick Esc Cancel", True, (150, 150, 150))
|
||||||
|
self.screen.blit(hint, (20, hint_y))
|
||||||
|
elif self.music_search_pending:
|
||||||
text = self.font.render("Searching...", True, (200, 200, 200))
|
text = self.font.render("Searching...", True, (200, 200, 200))
|
||||||
self.screen.blit(text, (20, 140))
|
self.screen.blit(text, (20, 140))
|
||||||
|
else:
|
||||||
|
text = self.font.render("No results. Type to search again, Enter to search", True, (200, 200, 200))
|
||||||
|
self.screen.blit(text, (20, 140))
|
||||||
|
|
||||||
# TTS box
|
# TTS box
|
||||||
if self.typing_mode:
|
if self.typing_mode:
|
||||||
@@ -509,19 +659,31 @@ class NaoTeleop:
|
|||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
self.clock.tick(0)
|
self.clock.tick(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\u26a0\ufe0f Frame error (continuing): {e}")
|
||||||
|
finally:
|
||||||
print("🛑 Shutting down...")
|
print("🛑 Shutting down...")
|
||||||
|
try:
|
||||||
self.motion.stopMove()
|
self.motion.stopMove()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"stopMove error: {e}")
|
||||||
|
try:
|
||||||
self.music.stop()
|
self.music.stop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"music stop error: {e}")
|
||||||
if self.audio_device:
|
if self.audio_device:
|
||||||
try: self.audio_device.unsubscribe(self.audio_service_name)
|
try: self.audio_device.unsubscribe(self.audio_service_name)
|
||||||
except: pass
|
except Exception as e: print(f"audio unsubscribe error: {e}")
|
||||||
if getattr(self, "sound_receiver", None):
|
if getattr(self, "sound_receiver", None):
|
||||||
self.sound_receiver.close()
|
try: self.sound_receiver.close()
|
||||||
|
except Exception as e: print(f"sound receiver close error: {e}")
|
||||||
if hasattr(self, 'video') and self.video_client:
|
if hasattr(self, 'video') and self.video_client:
|
||||||
try: self.video.unsubscribe(self.video_client)
|
try: self.video.unsubscribe(self.video_client)
|
||||||
except: pass
|
except Exception as e: print(f"video unsubscribe error: {e}")
|
||||||
|
try:
|
||||||
self.motion.rest()
|
self.motion.rest()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"rest error: {e}")
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user