Compare commits

..

15 Commits

Author SHA1 Message Date
Lucca Pirovano 5977e6fc7d reduced latency by not having ssh run on every V press and just sending to tcp aplay when v is held 2026-07-20 15:26:57 -04:00
Lucca Pirovano 6a97da3b86 fixed walk startup toppling by removing startup animation 2026-07-20 15:19:10 -04:00
Lucca Pirovano 17c7da2125 added startup sh script for my laptop to set the mic volume and run the app with my settings 2026-07-20 14:18:13 -04:00
Lucca Pirovano b30fc10648 fixed voice chat crashing when releasing V 2026-07-16 17:36:33 -04:00
Lucca Pirovano f806fa8efc added auto mute when holding v so i dont hear myself 2026-07-16 17:09:07 -04:00
Lucca Pirovano e1b5e3ab76 added V for voice chat, it outputs my laptop mic out the nao speaker 2026-07-16 17:05:29 -04:00
Lucca Pirovano 929687dea5 added 6 7 gesture to slot 3 2026-07-16 16:51:13 -04:00
Lucca Pirovano a6767bf154 added 6 7 gesture to slot 3 2026-07-16 16:16:13 -04:00
Lucca Pirovano f4140328e6 reduced arm stiffness while walking 2026-07-16 14:47:22 -04:00
Lucca Pirovano c6b9cb0718 working on cena gesture 2026-07-16 14:41:23 -04:00
Lucca Pirovano ca196f5f1e improved cena gesture 2026-07-16 14:26:03 -04:00
Lucca Pirovano 158372e528 working on cena gesture (2) 2026-07-16 14:21:29 -04:00
Lucca Pirovano ab67b128c1 added naomusic.py and moved all music stuff to it 2026-07-16 14:16:25 -04:00
Lucca Pirovano cdb2e1f90f added naomusic.py and moved all music stuff to it 2026-07-16 14:06:05 -04:00
Lucca Pirovano 99f2d6a9cd added walk speed setting on [], refined walk posture 2026-07-16 14:03:39 -04:00
6 changed files with 602 additions and 181 deletions
Binary file not shown.
@@ -0,0 +1,41 @@
---
title: "Refactoring Python project files and code"
tags: []
author: claude
count: 2
exporter: 3.1.0
date: 2026-07-16T17-38-58+0000
url: https://claude.ai/chat/313cfd88-e6b9-4256-b155-0b389d2b86c4
---
# Refactoring Python project files and code
## Table of Contents
- [1: make a requirements.txt, readme.md, and also remove the seek function…](#chat-1)
- [2: how much faster can i make it walk, can we make it adjustable during r…](#chat-2)
## chat-1
> make a requirements.txt, readme.md, and also remove the seek function completely and reduce the lines of code without hurting stability
Now the `pause()` method — simplify to a plain stop/restart toggle (no seek, no position math):
Now let's add the `_safe` helper to `NaoTeleop` and use it to collapse the repeated try/except boilerplate:
Now the shutdown block — this is where `_safe` pays off the most:
Good — that's stable. Now let's view the final file once more end-to-end to make sure everything reads cleanly before shipping it.
This looks clean and correct. Now let's write the `requirements.txt` and `README.md`.
Good, all confirmed. Now the README:
Now let's copy everything to outputs and present the files:
___
###### [top](#table-of-contents)
## chat-2
> how much faster can i make it walk, can we make it adjustable during runtime? we might need to change its posture so its less likely to fall and more balanced for carpet
@@ -0,0 +1,53 @@
---
title: "Your first chat with Claude"
tags: []
author: claude
count: 5
exporter: 3.1.0
date: 2026-07-16T20-40-30+0000
url: https://claude.ai/chat/838b001f-8bf8-4539-98ec-467513fb9860
---
# Your first chat with Claude
## Table of Contents
- [1: add the 6 7 meme as a third gesture](#chat-1)
- [2: kinda looks like hes flailing his arms around, isnt it just the elbows…](#chat-2)
- [3: right motions, but the nao currently extends his arms out, then moves…](#chat-3)
- [4: perfect, now just rotate the hands so the palm faces up, everything el…](#chat-4)
- [5: make him tts say six seven, also remove the input delay on starting th…](#chat-5)
## chat-1
> add the 6 7 meme as a third gesture
## chat-2
> kinda looks like hes flailing his arms around, isnt it just the elbows moving in the meme? with the arms in front
Now let me replace the whole method with a version where the shoulders lock the arms up in front and only the elbows do the seesaw motion.
___
###### [top](#table-of-contents)
## chat-3
> right motions, but the nao currently extends his arms out, then moves his elbows to push the forearms close to his chest. i think the rotation is wrong on the elbow
That makes sense — at `ElbowYaw=0` the elbow's bend axis is oriented so flexing curls the forearm in across the body toward the chest, not up/down in place. Rotating `ElbowYaw` about 90° turns that same hinge into a front-facing "tip" instead, and I'll shrink the swing so it's a wobble, not a full extend-then-fold.
___
###### [top](#table-of-contents)
## chat-4
> perfect, now just rotate the hands so the palm faces up, everything else is perfect
## chat-5
> make him tts say six seven, also remove the input delay on starting the 3rd gesture
`self.tts.say(...)` is a blocking call, so it was speaking first and only starting the arm motion once the phrase finished — that's the delay. Making it fire on its own thread lets the gesture start immediately while the speech plays alongside it.
___
###### [top](#table-of-contents)
+147
View File
@@ -0,0 +1,147 @@
# naomusic.py
# -*- encoding: UTF-8 -*-
import subprocess
import os
import json
import re
import time
import threading
class NaoMusicPlayer:
def __init__(self, session, nao_ip):
self.session = session
self.player = session.service("ALAudioPlayer")
self.nao_ip = nao_ip
self.music_dir = "/home/nao/music"
self.local_path = "/tmp/song.mp3"
self.remote_path = f"{self.music_dir}/song.mp3"
try:
subprocess.run(["sshpass", "-p", "nao", "ssh", f"nao@{nao_ip}", f"mkdir -p {self.music_dir}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except: pass
self.current_file = None
self.current_task_id = None
self.is_playing = False
self.current_title = ""
self.is_loading = False
self.load_error = None
self.status = ""
self.progress = 0.0
def search(self, query):
try:
cmd = ["yt-dlp", f"ytsearch5:{query}", "--flat-playlist", "--dump-json",
"--no-warnings", "--quiet"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
entries = []
for line in result.stdout.strip().splitlines():
if not line.strip():
continue
try:
data = json.loads(line)
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 []
_DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%')
def play(self, url, title):
self.stop()
self.current_title = title
self.is_loading = True
self.load_error = None
self.status = "Starting download..."
self.progress = 0.0
try:
if os.path.exists(self.local_path):
os.remove(self.local_path)
except: pass
try:
cmd = ["yt-dlp", url,
"-x", "--audio-format", "mp3",
"-o", self.local_path,
"--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
size_mb = os.path.getsize(self.local_path) / (1024 * 1024)
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
print(f"♪ Playing on NAO: {title} (task {self.current_task_id})")
return True
except Exception as e:
print(f"Playback error: {e}")
self.load_error = str(e)
return False
finally:
self.is_loading = False
def stop(self):
try:
self.player.stopAll()
except Exception as e:
print(f"Stop error: {e}")
self.is_playing = False
self.current_file = None
self.current_task_id = None
self.current_title = ""
+358 -181
View File
@@ -14,6 +14,10 @@ import subprocess
import os import os
import json import json
import re import re
import socket
# Import the new music module
from naomusic import NaoMusicPlayer
# --- Audio for mic --- # --- Audio for mic ---
try: try:
@@ -22,6 +26,13 @@ try:
except ImportError: except ImportError:
HAS_AUDIO = False HAS_AUDIO = False
# --- SSH for live mic relay (push-to-talk) ---
try:
import paramiko
HAS_SSH = True
except ImportError:
HAS_SSH = False
# ========================================== # ==========================================
# NAO MIC STREAM # NAO MIC STREAM
# ========================================== # ==========================================
@@ -29,7 +40,7 @@ class SoundReceiver:
PREBUFFER_CHUNKS = 3 PREBUFFER_CHUNKS = 3
SILENCE_CHUNK = b"\x00" * 4096 SILENCE_CHUNK = b"\x00" * 4096
def __init__(self, output_device_index=None, mic_gain=8.0): def __init__(self, output_device_index=None, mic_gain=8.0, pyaudio_instance=None):
self.running = True self.running = True
self.underrun_count = 0 self.underrun_count = 0
self.chunks_received = 0 self.chunks_received = 0
@@ -37,9 +48,12 @@ class SoundReceiver:
self._peak_since_print = 0 self._peak_since_print = 0
self._last_status_print = 0 self._last_status_print = 0
self.mic_gain = float(mic_gain) self.mic_gain = float(mic_gain)
self.muted = False
self._owns_pyaudio = False
if HAS_AUDIO: if HAS_AUDIO:
self.p = pyaudio.PyAudio() self.p = pyaudio_instance if pyaudio_instance is not None else pyaudio.PyAudio()
self._owns_pyaudio = pyaudio_instance is None
print("🔈 Available output devices:") print("🔈 Available output devices:")
for i in range(self.p.get_device_count()): for i in range(self.p.get_device_count()):
info = self.p.get_device_info_by_index(i) info = self.p.get_device_info_by_index(i)
@@ -106,6 +120,9 @@ class SoundReceiver:
def _write_chunk(self, raw_bytes): def _write_chunk(self, raw_bytes):
try: try:
if self.muted:
self.stream.write(b"\x00" * len(raw_bytes))
return
samples = np.frombuffer(raw_bytes, dtype=np.int16).astype(np.float32) samples = np.frombuffer(raw_bytes, dtype=np.int16).astype(np.float32)
if samples.size: if samples.size:
amplified = np.clip(samples * self.mic_gain, -32768, 32767) amplified = np.clip(samples * self.mic_gain, -32768, 32767)
@@ -125,180 +142,176 @@ class SoundReceiver:
try: try:
self.stream.stop_stream() self.stream.stop_stream()
self.stream.close() self.stream.close()
self.p.terminate() if self._owns_pyaudio:
self.p.terminate()
except: pass except: pass
# ==========================================
# LIVE MIC RELAY (push-to-talk over SSH)
# ==========================================
class LiveMicRelay:
"""
Push-to-talk: streams the local mic straight to NAO's own speaker.
# ========================================== connect() (call once at startup, off the hot path) SSHes into the
# MUSIC PLAYER ON NAO (scp) robot and launches a self-restarting `nc -l | aplay` loop that stays
# ========================================== up for the whole session. Each start()/stop() (per push-to-talk
class NaoMusicPlayer: press) then just opens/closes a plain TCP socket to that already-
def __init__(self, session, nao_ip): running listener and pipes raw mic audio into it - no SSH handshake
self.session = session per press. Closing the socket on stop() ends that iteration's nc,
self.player = session.service("ALAudioPlayer") which ends aplay with it, and the remote loop immediately relaunches
nc so it's ready for the next press. close() (call once at shutdown)
tears down the persistent SSH connection and kills the remote loop.
"""
def __init__(self, nao_ip, ssh_user="nao", ssh_password="nao",
port=5555, rate=16000, input_device_index=None, pyaudio_instance=None):
self.nao_ip = nao_ip self.nao_ip = nao_ip
self.music_dir = "/home/nao/music" self.ssh_user = ssh_user
self.local_path = "/tmp/song.mp3" self.ssh_password = ssh_password
self.remote_path = f"{self.music_dir}/song.mp3" self.port = port
try: self.rate = rate
subprocess.run(["sshpass", "-p", "nao", "ssh", f"nao@{nao_ip}", f"mkdir -p {self.music_dir}"], self.input_device_index = input_device_index
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except: pass
self.current_file = None
self.current_task_id = None
self.is_playing = False
self.current_title = ""
self.is_loading = False
self.load_error = None
self.status = ""
self.progress = 0.0
def search(self, query): self.active = False
try: self.connected = False # remote listener is up, ready for fast local connects
cmd = ["yt-dlp", f"ytsearch5:{query}", "--flat-playlist", "--dump-json", # "idle" | "connecting" | "live" - read by the HUD to show status
"--no-warnings", "--quiet"] self.state = "idle"
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) self._ssh_client = None
entries = [] self._ssh_channel = None
for line in result.stdout.strip().splitlines(): self._sock = None
if not line.strip(): self._stream = None
continue self._thread = None
try: # Reuse the app's single PyAudio context - a second independent one
data = json.loads(line) # opened from a background thread crashes PortAudio's PulseAudio
except json.JSONDecodeError: # backend (pa_atomic_load assertion).
continue self._p = pyaudio_instance if pyaudio_instance is not None else (pyaudio.PyAudio() if HAS_AUDIO else None)
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 []
_DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%') def connect(self):
"""One-time setup: SSH in and launch a self-restarting remote
def play(self, url, title): listener (`nc | aplay` in a loop) that stays up for the whole app
self.stop() session. Call this once at startup, in a background thread so it
self.current_title = title doesn't block. After this, start()/stop() per push-to-talk press
self.is_loading = True only do a fast local socket connect - no SSH in the hot path.
self.load_error = None """
self.status = "Starting download..." if not HAS_SSH:
self.progress = 0.0 print("🎙️ Live mic needs paramiko: pip install paramiko")
try:
if os.path.exists(self.local_path):
os.remove(self.local_path)
except: pass
try:
cmd = ["yt-dlp", url,
"-x", "--audio-format", "mp3",
"-o", self.local_path,
"--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
size_mb = os.path.getsize(self.local_path) / (1024 * 1024)
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
print(f"♪ Playing on NAO: {title} (task {self.current_task_id})")
return True
except Exception as e:
print(f"Playback error: {e}")
self.load_error = str(e)
return False
finally:
self.is_loading = False
def pause(self):
"""Toggle playback. No position tracking: resuming restarts the track from the top."""
if self.current_task_id is None or not self.current_file:
print("⏸ Nothing loaded to pause/resume")
return return
if self.is_playing: try:
try: self._ssh_client = paramiko.SSHClient()
self.player.stopAll() self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
except Exception as e: self._ssh_client.connect(self.nao_ip, username=self.ssh_user,
print(f"stopAll error while pausing: {e}") password=self.ssh_password, timeout=5)
self.load_error = f"Pause error: {e}" self._ssh_channel = self._ssh_client.get_transport().open_session()
return # A pty ties the remote process group to this channel, so
self.is_playing = False # closing it in close() reliably kills the loop/nc/aplay
print("♪ Paused") # instead of leaving an orphaned process on the robot.
else: self._ssh_channel.get_pty()
try: remote_cmd = (
self.current_task_id = self.player.playFile(self.current_file) f"while true; do nc -l -p {self.port} | "
self.is_playing = True f"aplay -q -r {self.rate} -f S16_LE -c 1 -t raw -; done"
print(f"♪ Resumed from start (task {self.current_task_id})") )
except Exception as e: self._ssh_channel.exec_command(remote_cmd)
print(f"Resume error: {e}") time.sleep(0.4) # one-time wait for the first nc to bind
self.load_error = f"Resume error: {e}" self.connected = True
print("🎙️ Talk relay: remote listener ready")
except Exception as e:
print(f"🎙️ Talk relay: couldn't set up remote listener ({e})")
self.connected = False
def close(self):
"""Tear down the persistent SSH connection/listener. Call once at
app shutdown - not per push-to-talk press."""
if self._ssh_channel:
try: self._ssh_channel.close()
except: pass
self._ssh_channel = None
if self._ssh_client:
try: self._ssh_client.close()
except: pass
self._ssh_client = None
self.connected = False
def start(self):
if self.active:
return
if not HAS_AUDIO:
print("🎙️ Live mic needs pyaudio installed")
return
if not self.connected:
print("🎙️ Talk relay: remote listener not ready yet")
return
# If a previous relay thread is still tearing itself down, wait for
# it to finish before reusing self._stream/_sock for a new one.
if self._thread and self._thread.is_alive():
self._thread.join(timeout=2.0)
self.active = True
self.state = "connecting"
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def stop(self): def stop(self):
try: """Signal the relay thread to stop and wait for it to exit.
self.player.stopAll()
except Exception as e:
print(f"Stop error: {e}")
self.is_playing = False
self.current_file = None
self.current_task_id = None
self.current_title = ""
Deliberately does NOT touch self._stream/_sock here: this is
called from the main/render thread on key-up, and closing a
PortAudio stream from a different thread while the relay thread is
mid blocking-read() on it is what caused the ALSA segfault on
release. Only _run() (via _cleanup, in its own thread) closes them.
"""
self.active = False
self.state = "idle"
if self._thread and self._thread.is_alive() and threading.current_thread() is not self._thread:
self._thread.join(timeout=2.0)
def _cleanup(self):
"""Per-press teardown: only the local socket/mic stream. The SSH
connection is persistent now - close() (at app shutdown) is what
tears that down, not this."""
if self._sock:
try: self._sock.close()
except: pass
self._sock = None
if self._stream:
try:
self._stream.stop_stream()
self._stream.close()
except: pass
self._stream = None
def _run(self):
try:
# Remote listener is already up (from connect()) - just make a
# fast local socket connection, no SSH handshake here.
self._sock = socket.create_connection((self.nao_ip, self.port), timeout=5)
self._stream = self._p.open(format=pyaudio.paInt16, channels=1,
rate=self.rate, input=True,
input_device_index=self.input_device_index,
frames_per_buffer=1024)
print("🎙️ Talk relay: live")
if self.active: # could've been stopped while we were still connecting
self.state = "live"
while self.active:
try:
chunk = self._stream.read(1024, exception_on_overflow=False)
self._sock.sendall(chunk)
except Exception:
break
except Exception as e:
print(f"🎙️ Talk relay error: {e}")
finally:
self.active = False
self.state = "idle"
self._cleanup()
print("🎙️ Talk relay: stopped")
# ========================================== # ==========================================
# MAIN TELEOP # MAIN TELEOP
# ========================================== # ==========================================
class NaoTeleop: class NaoTeleop:
def __init__(self, session, nao_ip, audio_device_index=None, mic_channel=1, mic_gain=8.0, video_scale=2.0): def __init__(self, session, nao_ip, audio_device_index=None, mic_channel=1, mic_gain=8.0, video_scale=2.0,
nao_ssh_user="nao", nao_ssh_password="nao", relay_port=5555, relay_rate=16000):
self.session = session self.session = session
self.nao_ip = nao_ip self.nao_ip = nao_ip
self.motion = session.service("ALMotion") self.motion = session.service("ALMotion")
@@ -319,12 +332,22 @@ class NaoTeleop:
self.mic_channel = mic_channel self.mic_channel = mic_channel
self.mic_gain = mic_gain self.mic_gain = mic_gain
# One shared PyAudio context for the whole app - SoundReceiver (mic
# playback) and LiveMicRelay (push-to-talk capture) both use it,
# since two separate contexts crash PortAudio's PulseAudio backend.
self._pyaudio = pyaudio.PyAudio() if HAS_AUDIO else None
self.mic_relay = LiveMicRelay(nao_ip, ssh_user=nao_ssh_user, ssh_password=nao_ssh_password,
port=relay_port, rate=relay_rate, pyaudio_instance=self._pyaudio)
self.ptt_active = False
self.running = True self.running = True
self.head_yaw = 0.0 self.head_yaw = 0.0
self.head_pitch = 0.0 self.head_pitch = 0.0
self.battery_level = 100 self.battery_level = 100
self.last_batt_check = 0 self.last_batt_check = 0
self.volume = 50 self.volume = 50
self.walk_speed = 0.6
self.typing_mode = False self.typing_mode = False
self.chat_message = "" self.chat_message = ""
@@ -349,7 +372,6 @@ class NaoTeleop:
self.update_title() self.update_title()
def _safe(self, label, fn): def _safe(self, label, fn):
"""Run fn(), log+swallow any exception so one failure can't crash the loop."""
try: try:
return fn() return fn()
except Exception as e: except Exception as e:
@@ -377,7 +399,8 @@ class NaoTeleop:
def _init_mic_stream(self): def _init_mic_stream(self):
if not self.audio_device: return if not self.audio_device: return
try: try:
self.sound_receiver = SoundReceiver(self.audio_device_index, self.mic_gain) self.sound_receiver = SoundReceiver(self.audio_device_index, self.mic_gain,
pyaudio_instance=self._pyaudio)
self.session.registerService(self.audio_service_name, self.sound_receiver) self.session.registerService(self.audio_service_name, self.sound_receiver)
time.sleep(0.5) time.sleep(0.5)
self.audio_device.setClientPreferences(self.audio_service_name, 16000, self.mic_channel, 0) self.audio_device.setClientPreferences(self.audio_service_name, 16000, self.mic_channel, 0)
@@ -412,10 +435,84 @@ class NaoTeleop:
self.motion.angleInterpolation(names, angles, times, True) self.motion.angleInterpolation(names, angles, times, True)
neutral = [1.5, -0.15, 1.2, 0.5, 0.0, 0.0] neutral = [1.5, -0.15, 1.2, 0.5, 0.0, 0.0]
self.motion.angleInterpolationWithSpeed(names, neutral, 0.3) self.motion.angleInterpolationWithSpeed(names, neutral, 0.3)
self.motion.setStiffnesses("RArm", 0.6) # Relaxed stiffness after completion
self.motion.setStiffnesses("RArm", 0.3)
except Exception as e: except Exception as e:
print(f"Wave error: {e}") print(f"Wave error: {e}")
def cena_gesture(self):
if self.typing_mode: return
print("👋 YOU CAN'T SEE ME!")
try:
self.tts.say("You can't see me")
self.motion.setStiffnesses("RArm", 1.0)
names = ["RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RElbowRoll", "RWristYaw", "RHand"]
# Setup for tight, face-level sweep
tight_setup = [-0.4, -0.2, 0.0, 1.2, 1.5, 1.0]
self.motion.angleInterpolation(names, tight_setup, [0.5]*6, True)
# The horizontal "backhand" sweep
self.motion.angleInterpolation(["RElbowYaw"], [0.8], 0.4, True)
self.motion.angleInterpolation(["RElbowYaw"], [-0.8], 0.6, True)
self.motion.angleInterpolation(["RElbowYaw"], [0.0], 0.4, True)
# Return to neutral
neutral = [1.5, -0.15, 1.2, 0.5, 0.0, 0.0]
self.motion.angleInterpolationWithSpeed(names, neutral, 0.3)
# Relaxed stiffness after completion
self.motion.setStiffnesses("RArm", 0.3)
except Exception as e:
print(f"Cena gesture error: {e}")
def six_seven_gesture(self):
if self.typing_mode: return
print("🖐️ SIX... SEVEN...")
try:
threading.Thread(target=self.tts.say, args=("six seven",), daemon=True).start()
self.motion.setStiffnesses(["RArm", "LArm"], 1.0)
# Lock both arms up in front of the body, hands held out like a scale.
# ElbowYaw is rotated ~90° off zero here - that's what points the elbow's
# bend axis so flexing tips the forearm forward/down instead of curling
# it in across the chest. These joints don't move again until the return.
hold_names = [
"RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RWristYaw", "RHand",
"LShoulderPitch", "LShoulderRoll", "LElbowYaw", "LWristYaw", "LHand"
]
hold_angles = [
0.2, -0.2, 1.4, 1.57, 0.6,
0.2, 0.2, -1.4, -1.57, 0.6
]
self.motion.angleInterpolationWithSpeed(hold_names, hold_angles, 0.25)
# Only the elbows seesaw now - a small tip back and forth, one arm
# rising while the other falls, like weighing something in each hand
# ("six... seven..."). Small amplitude so it stays a wobble, not a fold.
elbow_names = ["RElbowRoll", "LElbowRoll"]
r_low, r_high = 0.6, 1.0
l_low, l_high = -0.6, -1.0
elbow_angles = [
[r_low, r_high, r_low, r_high, r_low, r_high],
[l_high, l_low, l_high, l_low, l_high, l_low],
]
times = [[0.35, 0.7, 1.05, 1.4, 1.75, 2.1]] * 2
self.motion.angleInterpolation(elbow_names, elbow_angles, times, True)
# Return everything to neutral
neutral_names = hold_names + elbow_names
neutral = [1.5, -0.15, 1.2, 0.0, 0.0,
1.5, 0.15, -1.2, 0.0, 0.0,
0.5, -0.5]
self.motion.angleInterpolationWithSpeed(neutral_names, neutral, 0.3)
# Relaxed stiffness after completion
self.motion.setStiffnesses(["RArm", "LArm"], 0.3)
except Exception as e:
print(f"Six seven gesture error: {e}")
def handle_head_movement(self): def handle_head_movement(self):
if self.typing_mode or self.music_search_mode: return if self.typing_mode or self.music_search_mode: return
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
@@ -436,7 +533,7 @@ class NaoTeleop:
def update_title(self): def update_title(self):
mode = "[TYPING] " if self.typing_mode else "" mode = "[TYPING] " if self.typing_mode else ""
pygame.display.set_caption(f"NAO Teleop | {mode}Battery: {self.battery_level}% | Vol: {self.volume}%") pygame.display.set_caption(f"NAO Teleop | {mode}Battery: {self.battery_level}% | Vol: {self.volume}% | Speed: {self.walk_speed:.2f}")
def change_volume(self, delta): def change_volume(self, delta):
self.volume = int(min(100, max(0, self.volume + delta))) self.volume = int(min(100, max(0, self.volume + delta)))
@@ -456,18 +553,24 @@ class NaoTeleop:
self.last_batt_check = now self.last_batt_check = now
def run(self): def run(self):
self.motion.wakeUp() print("🤖 Enabling stiffness (no wakeUp stand-up)...")
self.posture.goToPosture("StandInit", 0.5) self.motion.setStiffnesses("Body", 1.0)
time.sleep(1.0)
# Enable walk arms BEFORE forcing stiffness
print("💪 Enabling walk arms...")
self.motion.setMoveArmsEnabled(True, True) self.motion.setMoveArmsEnabled(True, True)
# Force relaxed stiffness AFTER engine is active
self.motion.setStiffnesses(["RArm", "LArm"], 0.3)
print("🦶 Initializing walk engine...") print("🦶 Initializing walk engine...")
self._safe("moveInit", self.motion.moveInit) self._safe("moveInit", self.motion.moveInit)
print("🦶 Walk engine ready") print("🦶 Walk engine ready")
carpet_config = [["StepHeight", 0.028], ["MaxStepFrequency", 0.6], ["TorsoWy", 0.05]] if getattr(self, "mic_relay", None):
threading.Thread(target=self.mic_relay.connect, daemon=True).start()
print("\n🎮 Controls: WASD/Arrows=Walk | IJKL=Head | SPACE=Reset head | 1=Wave | M=Music search (↑↓ select, Enter confirm) | P=Pause | X=Stop | -/==Volume | T=TTS | ESC=Quit") print("\n🎮 Controls: WASD/Arrows=Walk | [/]=Speed | IJKL=Head | SPACE=Reset head | 1=Wave | 2=Cena | 3=6-7 | M=Music search | X=Stop | -/==Volume | T=TTS | V=Push-to-talk (hold) | ESC=Quit")
try: try:
while self.running: while self.running:
@@ -545,14 +648,16 @@ class NaoTeleop:
self.running = False self.running = False
elif event.key == pygame.K_1: elif event.key == pygame.K_1:
self.wave_gesture() self.wave_gesture()
elif event.key == pygame.K_2:
self.cena_gesture()
elif event.key == pygame.K_3:
self.six_seven_gesture()
elif event.key == pygame.K_SPACE: elif event.key == pygame.K_SPACE:
self.reset_head() self.reset_head()
elif event.key == pygame.K_m: elif event.key == pygame.K_m:
self.music_search_mode = True self.music_search_mode = True
self.music_search_text = "" self.music_search_text = ""
self.music_results = [] self.music_results = []
elif event.key == pygame.K_p:
self.music.pause()
elif event.key == pygame.K_x: elif event.key == pygame.K_x:
self.music.stop() self.music.stop()
elif event.key in (pygame.K_MINUS, pygame.K_KP_MINUS): elif event.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
@@ -565,15 +670,43 @@ class NaoTeleop:
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()
if keys[pygame.K_v] and not self.ptt_active:
self.ptt_active = True
self.mic_relay.start()
if getattr(self, "sound_receiver", None):
self.sound_receiver.muted = True
elif not keys[pygame.K_v] and self.ptt_active:
self.ptt_active = False
self.mic_relay.stop()
if getattr(self, "sound_receiver", None):
self.sound_receiver.muted = False
speed_changed = False
if keys[pygame.K_LEFTBRACKET]:
self.walk_speed = max(0.1, self.walk_speed - 0.01)
speed_changed = True
if keys[pygame.K_RIGHTBRACKET]:
self.walk_speed = min(1.0, self.walk_speed + 0.01)
speed_changed = True
if speed_changed:
self.update_title()
x = y = theta = 0.0 x = y = theta = 0.0
if keys[pygame.K_w] or keys[pygame.K_UP]: x = 0.6 if keys[pygame.K_w] or keys[pygame.K_UP]: x = self.walk_speed
if keys[pygame.K_s] or keys[pygame.K_DOWN]: x = -0.6 if keys[pygame.K_s] or keys[pygame.K_DOWN]: x = -self.walk_speed
if keys[pygame.K_a] or keys[pygame.K_LEFT]: theta = 0.5 if keys[pygame.K_a] or keys[pygame.K_LEFT]: theta = self.walk_speed * 0.8
if keys[pygame.K_d] or keys[pygame.K_RIGHT]: theta = -0.5 if keys[pygame.K_d] or keys[pygame.K_RIGHT]: theta = -self.walk_speed * 0.8
try: try:
if abs(x) > 0.1 or abs(theta) > 0.1: if abs(x) > 0.05 or abs(theta) > 0.05:
self.motion.moveToward(x, y, theta, carpet_config) stable_config = [
["StepHeight", 0.02],
["MaxStepFrequency", 0.5],
["TorsoWy", 0.08],
["MaxStepX", 0.03]
]
self.motion.moveToward(x, y, theta, stable_config)
else: else:
self.motion.stopMove() self.motion.stopMove()
except Exception as e: except Exception as e:
@@ -602,10 +735,37 @@ class NaoTeleop:
text = self.font.render(f"♪ Error: {self.music.load_error}", True, (255, 80, 80)) text = self.font.render(f"♪ Error: {self.music.load_error}", True, (255, 80, 80))
self.screen.blit(text, (10, 10)) self.screen.blit(text, (10, 10))
elif self.music.current_title: elif self.music.current_title:
status = f"{self.music.current_title[:45]} {'(Paused)' if not self.music.is_playing else ''}" text = self.font.render(f"{self.music.current_title[:45]}", 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))
# Voice chat HUD (top-right) - only shown while V is held / connecting
relay_state = getattr(self.mic_relay, "state", "idle")
if relay_state == "connecting":
label, color = "VOICE: CONNECTING...", (255, 200, 0)
elif relay_state == "live":
label, color = "VOICE: LIVE - speak now", (0, 255, 100)
else:
label, color = None, None
if label:
text = self.font.render(label, True, color)
pad = 8
box_w, box_h = text.get_width() + pad * 2 + 18, text.get_height() + pad
box_x = self.display_width - box_w - 10
box_y = 10
s = pygame.Surface((box_w, box_h))
s.set_alpha(180)
s.fill((0, 0, 0))
self.screen.blit(s, (box_x, box_y))
# pulsing dot for "live" so it's easy to catch out of the corner of your eye
if relay_state == "live":
pulse = 0.5 + 0.5 * abs((pygame.time.get_ticks() % 1000) / 500.0 - 1.0)
dot_color = tuple(int(c * pulse) for c in color)
else:
dot_color = color
pygame.draw.circle(self.screen, dot_color, (box_x + pad + 5, box_y + box_h // 2), 5)
self.screen.blit(text, (box_x + pad + 18, box_y + pad // 2))
# Music search interface # Music search interface
if self.music_search_mode: if self.music_search_mode:
box_h = min(self.display_height - 80, 260) box_h = min(self.display_height - 80, 260)
@@ -654,12 +814,17 @@ class NaoTeleop:
print("🛑 Shutting down...") print("🛑 Shutting down...")
self._safe("stopMove", self.motion.stopMove) self._safe("stopMove", self.motion.stopMove)
self._safe("music stop", self.music.stop) self._safe("music stop", self.music.stop)
if getattr(self, "mic_relay", None):
self._safe("mic relay stop", self.mic_relay.stop)
self._safe("mic relay close", self.mic_relay.close)
if self.audio_device: if self.audio_device:
self._safe("audio unsubscribe", lambda: self.audio_device.unsubscribe(self.audio_service_name)) self._safe("audio unsubscribe", lambda: self.audio_device.unsubscribe(self.audio_service_name))
if getattr(self, "sound_receiver", None): if getattr(self, "sound_receiver", None):
self._safe("sound receiver close", self.sound_receiver.close) self._safe("sound receiver close", self.sound_receiver.close)
if hasattr(self, 'video') and self.video_client: if hasattr(self, 'video') and self.video_client:
self._safe("video unsubscribe", lambda: self.video.unsubscribe(self.video_client)) self._safe("video unsubscribe", lambda: self.video.unsubscribe(self.video_client))
if getattr(self, "_pyaudio", None):
self._safe("pyaudio terminate", self._pyaudio.terminate)
self._safe("rest", self.motion.rest) self._safe("rest", self.motion.rest)
pygame.quit() pygame.quit()
@@ -674,6 +839,14 @@ if __name__ == "__main__":
parser.add_argument("--mic-channel", choices=MIC_CHANNELS.keys(), default="left") parser.add_argument("--mic-channel", choices=MIC_CHANNELS.keys(), default="left")
parser.add_argument("--mic-gain", type=float, default=8.0) parser.add_argument("--mic-gain", type=float, default=8.0)
parser.add_argument("--video-scale", type=float, default=2.0) parser.add_argument("--video-scale", type=float, default=2.0)
parser.add_argument("--nao-ssh-user", type=str, default="nao",
help="SSH login for the live mic relay (push-to-talk)")
parser.add_argument("--nao-ssh-password", type=str, default="nao",
help="SSH password for the live mic relay (push-to-talk)")
parser.add_argument("--relay-port", type=int, default=5555,
help="TCP port used to stream mic audio to the robot's nc listener")
parser.add_argument("--relay-rate", type=int, default=16000,
help="Sample rate for the live mic relay - try 48000 if audio sounds sped up/slow")
args = parser.parse_args() args = parser.parse_args()
session = qi.Session() session = qi.Session()
@@ -688,4 +861,8 @@ if __name__ == "__main__":
audio_device_index=args.audio_device_index, audio_device_index=args.audio_device_index,
mic_channel=MIC_CHANNELS[args.mic_channel], mic_channel=MIC_CHANNELS[args.mic_channel],
mic_gain=args.mic_gain, mic_gain=args.mic_gain,
video_scale=args.video_scale).run() video_scale=args.video_scale,
nao_ssh_user=args.nao_ssh_user,
nao_ssh_password=args.nao_ssh_password,
relay_port=args.relay_port,
relay_rate=args.relay_rate).run()
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
pactl set-source-volume alsa_input.pci-0000_00_1b.0.analog-stereo 32%
python3.11 naowalk.py --ip spike.local --port 9561 --mic-gain 9 --video-scale 3.0