simplified code

This commit is contained in:
Lucca Pirovano
2026-07-16 13:29:56 -04:00
parent a5cdb4e4bd
commit bf3e197a20
4 changed files with 199 additions and 68 deletions
+105
View File
@@ -0,0 +1,105 @@
# naowalk
A keyboard teleop client for the NAO robot: live video, walking, head control,
two-way audio (hear the robot's mics, make it speak via TTS), and a YouTube
music player that downloads a track with `yt-dlp` and plays it through the
robot's own speakers.
## Requirements
**System packages** (Debian/Ubuntu names — adjust for your OS):
```bash
sudo apt install portaudio19-dev ffmpeg sshpass openssh-client
```
- `portaudio19-dev` — needed to build/install `pyaudio`
- `ffmpeg` — needed by `yt-dlp` to extract audio to mp3
- `sshpass` + `openssh-client` — used to `scp` downloaded tracks onto the
robot and to run `mkdir` over `ssh` (the script assumes the default
`nao`/`nao` credentials)
**NAOqi Python SDK (`qi`)** — not on PyPI. Download the `pynaoqi-python`
wheel that matches your OS/Python version from the Aldebaran/SoftBank
Robotics developer site and install it:
```bash
pip install ./pynaoqi-*.whl
```
**Python packages:**
```bash
pip install -r requirements.txt
```
## Usage
```bash
python3 naowalk.py --ip <robot-ip> [options]
```
| Flag | Default | Description |
|---|---|---|
| `--ip` | `127.0.0.1` | Robot's IP address |
| `--port` | `9559` | NAOqi port |
| `--audio-device-index` | auto | Output device index for hearing the robot's mic on your machine |
| `--mic-channel` | `left` | Which robot mic to stream (`left`, `right`, `front`, `rear`) — pick a working one, see Troubleshooting |
| `--mic-gain` | `8.0` | Amplification applied to the streamed mic audio |
| `--video-scale` | `2.0` | Display window scale (base feed is 320x240) |
## Controls
| Key | Action |
|---|---|
| `W`/`A`/`S`/`D` or arrows | Walk forward/turn/back |
| `I`/`K`/`J`/`L` | Head pitch/yaw |
| `Space` | Reset head to center |
| `1` | Wave gesture |
| `M` | Open music search (type query, `Enter` to search, `↑↓` to select, `Enter`/`1`-`5` to play) |
| `P` | Pause / resume music (resume restarts the track from the beginning — see Notes) |
| `X` | Stop music |
| `-` / `=` | Volume down/up |
| `T` | Type a message for the robot to speak (TTS) |
| `Esc` | Quit (or cancel current mode) |
## How it works
- **`SoundReceiver`** — registered as a NAOqi service that receives the
robot's mic stream and plays it out your local speakers via `pyaudio`, with
gain and a prebuffer to smooth out network jitter.
- **`NaoMusicPlayer`** — searches YouTube with `yt-dlp`, downloads and
converts the top match to mp3, `scp`s it to the robot, and plays it with
`ALAudioPlayer`.
- **`NaoTeleop`** — the main loop: pygame window for video/input, keyboard
handling, head/walk control, and wiring everything above together.
## Notes / design decisions
- **No seek.** Seeking was removed entirely — it added the most complexity
for the least-used feature. `P` is a simple play/stop toggle; resuming
restarts the track from the beginning rather than tracking a playback
position.
- **`moveInit()` on startup.** The robot used to need its walk buttons
jittered for a few seconds before its first walk would work. `run()` now
calls `ALMotionProxy::moveInit()` once at startup, which settles the walk
engine into the right pose before any real move command — this is the
documented NAOqi fix for exactly this symptom.
- **`_safe()` helper.** Most one-off NAOqi calls (setting angles, volume,
stopping movement, unsubscribing services) are wrapped in a small
`self._safe(label, fn)` helper instead of a repeated `try/except` block, so
one failed call logs and moves on without crashing the teleop loop or the
shutdown sequence.
## Troubleshooting
- **Mic sounds dead / silent on one channel:** some robots have one or more
physical mics that produce no signal. Test all four with a quick energy
check, then pick a working channel with `--mic-channel`.
- **Robot falls over on the very first walk command:** make sure you're on a
build that includes the `moveInit()` startup call described above; if it's
still happening, increase the `time.sleep(1.0)` after `goToPosture` to give
the posture more time to settle first.
- **`scp`/`ssh` failures for music:** confirm the robot is reachable at
`--ip` and that `nao`/`nao` are still the correct credentials; the script
hardcodes them via `sshpass`.
@@ -0,0 +1,56 @@
---
title: "Replacing music player backend with yt-dlp"
tags: []
author: claude
count: 2
exporter: 3.1.0
date: 2026-07-16T17-20-34+0000
url: https://claude.ai/chat/8bb5e420-7ad2-4048-be3f-95894713aff6
---
# Replacing music player backend with yt-dlp
## Table of Contents
- [1: lets just gut the seek function and focus on what works. we also need…](#chat-1)
- [2: alright awesome, lets remove the start from current position also, i w…](#chat-2)
## chat-1
> lets just gut the seek function and focus on what works. we also need to address the robot falling over if i try to walk from standstill, but only the first time i do it. If i jitter the walk forward and back buttons for about 3 seconds to get over the initial walk issues, it can start walking without falling over. how do we fix this
Both very solvable. Let's do the walk fix first since it's the more important one — confirmed via NAOqi docs: `ALMotionProxy::moveInit()` is the standard fix for exactly this. It's a one-time blocking call that initializes the walk engine and settles the robot into the right pose _before_ the first real move command, which is exactly what your button-jittering was accidentally doing by brute force. It's missing from the script entirely.
Now let's gut `seek()` entirely per your request — but keep the position tracking it shares with `pause()`, since that's still needed for resume-from-position to work:
Right, orphaned body left over. Let's clean that up:
Now remove the comma/period key bindings that called `seek()`:
Now compile-check and confirm nothing else references `seek`:
Clean. Let's ship it:
___
###### [top](#table-of-contents)
## chat-2
> alright awesome, lets remove the start from current position also, i want to get the lines of code down, what can be done without compromising on the stability and functionality to simplify our code
Now let's do the full pass. First, the concrete simplification you asked for — remove position tracking, revert pause/resume to restart-from-beginning:
Now `pause()` becomes a plain stop/restart toggle — no position math, no `playFileFromPosition`:
Now let's cut the repetitive `try/except Exception as e: print(...)` boilerplate scattered through `NaoTeleop` — same safety, less code, via one small helper:
Now let's add the `_safe` helper and apply it across these repeated sites:
Now the `moveInit` startup call:
Now the shutdown block — this is where it pays off most:
Let's check the movement block and the `T` key's `stopMove()` call too:
___
###### [top](#table-of-contents)
+24 -63
View File
@@ -147,9 +147,6 @@ class NaoMusicPlayer:
self.current_file = None
self.current_task_id = None
self.is_playing = False
self.paused_position = 0.0
self.play_start_time = None
self.position_at_start = 0.0
self.current_title = ""
self.is_loading = False
self.load_error = None
@@ -182,12 +179,6 @@ class NaoMusicPlayer:
_DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%')
def _current_position(self):
"""Playback position tracked client-side, independent of NAOqi's getCurrentPosition."""
if self.is_playing and self.play_start_time is not None:
return self.position_at_start + (time.time() - self.play_start_time)
return self.paused_position
def play(self, url, title):
self.stop()
self.current_title = title
@@ -260,9 +251,6 @@ class NaoMusicPlayer:
self.progress = 100.0
self.current_task_id = self.player.playFile(self.remote_path)
self.is_playing = True
self.play_start_time = time.time()
self.position_at_start = 0.0
self.paused_position = 0.0
print(f"♪ Playing on NAO: {title} (task {self.current_task_id})")
return True
except Exception as e:
@@ -273,30 +261,24 @@ class NaoMusicPlayer:
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
if self.is_playing:
pos = self._current_position()
try:
self.player.stopAll()
except Exception as e:
print(f"stopAll error while pausing: {e}")
self.load_error = f"Pause error: {e}"
return
self.paused_position = max(0.0, pos)
self.is_playing = False
self.play_start_time = None
print(f"♪ Paused at {self.paused_position:.1f}s")
print("♪ Paused")
else:
try:
self.current_task_id = self.player.playFileFromPosition(
self.current_file, self.paused_position, 1.0, 0.0
)
self.current_task_id = self.player.playFile(self.current_file)
self.is_playing = True
self.play_start_time = time.time()
self.position_at_start = self.paused_position
print(f"♪ Resumed from {self.paused_position:.1f}s (task {self.current_task_id})")
print(f"♪ Resumed from start (task {self.current_task_id})")
except Exception as e:
print(f"Resume error: {e}")
self.load_error = f"Resume error: {e}"
@@ -310,9 +292,6 @@ class NaoMusicPlayer:
self.current_file = None
self.current_task_id = None
self.current_title = ""
self.paused_position = 0.0
self.play_start_time = None
self.position_at_start = 0.0
# ==========================================
@@ -369,6 +348,14 @@ class NaoTeleop:
self.clock = pygame.time.Clock()
self.update_title()
def _safe(self, label, fn):
"""Run fn(), log+swallow any exception so one failure can't crash the loop."""
try:
return fn()
except Exception as e:
print(f"{label} error: {e}")
return None
def _init_audio_device(self):
try:
self.audio_device = self.session.service("ALAudioDevice")
@@ -439,18 +426,13 @@ class NaoTeleop:
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 changed:
try:
self.motion.setAngles(["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3)
except Exception as e:
print(f"Head move error: {e}")
self._safe("Head move", lambda: self.motion.setAngles(
["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3))
def reset_head(self):
self.head_yaw = 0.0
self.head_pitch = 0.0
try:
self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4)
except Exception as e:
print(f"Head reset error: {e}")
self._safe("Head reset", lambda: self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4))
def update_title(self):
mode = "[TYPING] " if self.typing_mode else ""
@@ -459,10 +441,7 @@ class NaoTeleop:
def change_volume(self, delta):
self.volume = int(min(100, max(0, self.volume + delta)))
if self.audio_device:
try:
self.audio_device.setOutputVolume(self.volume)
except Exception as e:
print(f"Volume set error: {e}")
self._safe("Volume set", lambda: self.audio_device.setOutputVolume(self.volume))
print(f"🔊 NAO volume: {self.volume}%")
self.update_title()
@@ -483,10 +462,7 @@ class NaoTeleop:
self.motion.setMoveArmsEnabled(True, True)
print("🦶 Initializing walk engine...")
try:
self.motion.moveInit()
except Exception as e:
print(f"moveInit error: {e}")
self._safe("moveInit", self.motion.moveInit)
print("🦶 Walk engine ready")
carpet_config = [["StepHeight", 0.028], ["MaxStepFrequency", 0.6], ["TorsoWy", 0.05]]
@@ -585,10 +561,7 @@ class NaoTeleop:
self.change_volume(5)
elif event.key == pygame.K_t:
self.typing_mode = True
try:
self.motion.stopMove()
except Exception as e:
print(f"stopMove error: {e}")
self._safe("stopMove", self.motion.stopMove)
if not self.typing_mode and not self.music_search_mode:
keys = pygame.key.get_pressed()
@@ -679,27 +652,15 @@ class NaoTeleop:
print(f"\u26a0\ufe0f Frame error (continuing): {e}")
finally:
print("🛑 Shutting down...")
try:
self.motion.stopMove()
except Exception as e:
print(f"stopMove error: {e}")
try:
self.music.stop()
except Exception as e:
print(f"music stop error: {e}")
self._safe("stopMove", self.motion.stopMove)
self._safe("music stop", self.music.stop)
if self.audio_device:
try: self.audio_device.unsubscribe(self.audio_service_name)
except Exception as e: print(f"audio unsubscribe error: {e}")
self._safe("audio unsubscribe", lambda: self.audio_device.unsubscribe(self.audio_service_name))
if getattr(self, "sound_receiver", None):
try: self.sound_receiver.close()
except Exception as e: print(f"sound receiver close error: {e}")
self._safe("sound receiver close", self.sound_receiver.close)
if hasattr(self, 'video') and self.video_client:
try: self.video.unsubscribe(self.video_client)
except Exception as e: print(f"video unsubscribe error: {e}")
try:
self.motion.rest()
except Exception as e:
print(f"rest error: {e}")
self._safe("video unsubscribe", lambda: self.video.unsubscribe(self.video_client))
self._safe("rest", self.motion.rest)
pygame.quit()
+14 -5
View File
@@ -1,5 +1,14 @@
qi
opencv-python
numpy
pygame
yt-dlp
# Install with: pip install -r requirements.txt
#
# NOTE: the NAOqi Python SDK ("qi") is NOT on PyPI. It ships separately from
# Aldebaran/SoftBank Robotics as a platform-specific wheel (pynaoqi-python*).
# Download it from the Softbank Robotics / Aldebaran developer portal, match
# it to your Python version/OS, and install it with:
# pip install ./pynaoqi-*.whl
# See README.md for details.
opencv-python>=4.8
numpy>=1.24
pygame>=2.5
pyaudio>=0.2.13
yt-dlp>=2024.1.1