diff --git a/@ b/@
deleted file mode 100644
index a9e1b45..0000000
--- a/@
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/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
-python3.11 naowalk.py --ip 127.0.0.1 --port 9561 --mic-gain 9 --video-scale 3.0 --nao-ssh-port 2222 --media-relay 127.0.0.1:8000
diff --git a/gestures/1_wave.py b/gestures/1_wave.py
new file mode 100644
index 0000000..72d6859
--- /dev/null
+++ b/gestures/1_wave.py
@@ -0,0 +1,20 @@
+# Loaded by naowalk.py's gesture loader (_load_gestures). The filename's
+# leading digit ("1") is which number key triggers this. Every gesture
+# file just needs a run(motion, tts) function - motion is the connected
+# ALMotion proxy, tts is ALTextToSpeech. Optionally set NAME below to
+# control the display name shown in the on-screen controls hint.
+
+NAME = "Wave"
+
+
+def run(motion, tts):
+ motion.setStiffnesses("RArm", 1.0)
+ names = ["RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RElbowRoll", "RWristYaw", "RHand"]
+ angles = [[-1.5]*5, [-0.4,-0.7,-0.4,-0.7,-0.4], [0.6,1.1,0.6,1.1,0.6],
+ [0.8,0.3,0.8,0.3,0.8], [0.0,1.5,0.0,-1.5,0.0], [1.0]*5]
+ times = [[0.5,1.0,1.5,2.0,2.5]] * 6
+ motion.angleInterpolation(names, angles, times, True)
+ neutral = [1.5, -0.15, 1.2, 0.5, 0.0, 0.0]
+ motion.angleInterpolationWithSpeed(names, neutral, 0.3)
+ # Relaxed stiffness after completion
+ motion.setStiffnesses("RArm", 0.3)
diff --git a/gestures/2_cena.py b/gestures/2_cena.py
new file mode 100644
index 0000000..c2fce5e
--- /dev/null
+++ b/gestures/2_cena.py
@@ -0,0 +1,23 @@
+NAME = "Cena"
+
+
+def run(motion, tts):
+ tts.say("You can't see me")
+
+ 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]
+ motion.angleInterpolation(names, tight_setup, [0.5]*6, True)
+
+ # The horizontal "backhand" sweep
+ motion.angleInterpolation(["RElbowYaw"], [0.8], 0.4, True)
+ motion.angleInterpolation(["RElbowYaw"], [-0.8], 0.6, True)
+ motion.angleInterpolation(["RElbowYaw"], [0.0], 0.4, True)
+
+ # Return to neutral
+ neutral = [1.5, -0.15, 1.2, 0.5, 0.0, 0.0]
+ motion.angleInterpolationWithSpeed(names, neutral, 0.3)
+ # Relaxed stiffness after completion
+ motion.setStiffnesses("RArm", 0.3)
diff --git a/gestures/3_six_seven.py b/gestures/3_six_seven.py
new file mode 100644
index 0000000..cee4509
--- /dev/null
+++ b/gestures/3_six_seven.py
@@ -0,0 +1,47 @@
+import threading
+
+NAME = "6-7"
+
+
+def run(motion, tts):
+ # Threaded so the TTS call doesn't block the arm motion below.
+ threading.Thread(target=tts.say, args=("six seven",), daemon=True).start()
+
+ 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
+ ]
+ 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
+ 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]
+ motion.angleInterpolationWithSpeed(neutral_names, neutral, 0.3)
+ # Relaxed stiffness after completion
+ motion.setStiffnesses(["RArm", "LArm"], 0.3)
diff --git a/gestures/4_gangnam_style.py b/gestures/4_gangnam_style.py
new file mode 100644
index 0000000..a9bee94
--- /dev/null
+++ b/gestures/4_gangnam_style.py
@@ -0,0 +1,22 @@
+import os
+
+NAME = "Gangnam Style"
+
+# The Choregraphe project lives in a sibling folder with the same name
+# as this file (minus the .py) - e.g. gestures/4_gangnam_style/ next
+# to gestures/4_gangnam_style.py - containing manifest.xml and a
+# GangnamStyle/behavior.xar subfolder, exactly what Choregraphe's
+# "Export as package" produces.
+PACKAGE_DIR = os.path.splitext(os.path.abspath(__file__))[0]
+BEHAVIOR_SUBDIR = "GangnamStyle"
+
+
+def run(motion, tts, teleop):
+ # Deploys (first run only, cached after) + runs the actual
+ # Choregraphe behavior on the robot - this blocks until it
+ # finishes, same as the old hand-coded version did.
+ teleop.behavior_runner.ensure_running(PACKAGE_DIR, BEHAVIOR_SUBDIR)
+
+ # The .xar moves legs/hips, unlike the old arms-only version, so
+ # bring him back to a known walk-ready stance afterward.
+ teleop.idle_crouch()
diff --git a/gestures/4_gangnam_style/GangnamStyle/behavior.xar b/gestures/4_gangnam_style/GangnamStyle/behavior.xar
new file mode 100644
index 0000000..9f70121
--- /dev/null
+++ b/gestures/4_gangnam_style/GangnamStyle/behavior.xar
@@ -0,0 +1,2135 @@
+
+
+
+ media/images/box/root.png
+
+
+
+
+
+
+
+
+
+
+ media/images/box/box-diagram.png
+
+
+
+
+
+
+
+
+
+
+ media/images/box/movement/move.png
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/play_music.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/play_music.png
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/folder.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/movement/move.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/movement/move.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/movement/stand.png
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/box-diagram.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/box-python-script.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gestures/4_gangnam_style/GangnamStyle/court-boom.wav b/gestures/4_gangnam_style/GangnamStyle/court-boom.wav
new file mode 100644
index 0000000..c3448af
Binary files /dev/null and b/gestures/4_gangnam_style/GangnamStyle/court-boom.wav differ
diff --git a/gestures/4_gangnam_style/manifest.xml b/gestures/4_gangnam_style/manifest.xml
new file mode 100644
index 0000000..652f488
--- /dev/null
+++ b/gestures/4_gangnam_style/manifest.xml
@@ -0,0 +1,16 @@
+
+
+
+ .lastUploadedChoregrapheBehavior
+ .lastUploadedChoregrapheBehavior
+
+
+ en_US
+
+
+
+ interactive
+
+
+
+
diff --git a/gestures/5_tai_chi.py b/gestures/5_tai_chi.py
new file mode 100644
index 0000000..b22130c
--- /dev/null
+++ b/gestures/5_tai_chi.py
@@ -0,0 +1,22 @@
+import os
+
+NAME = "Tai Chi"
+
+# The Choregraphe project lives in a sibling folder with the same name
+# as this file (minus the .py) - e.g. gestures/5_tai_chi/ next to
+# gestures/5_tai_chi.py - containing manifest.xml and a
+# behavior_1/behavior.xar subfolder, exactly what Choregraphe's
+# "Export as package" produces.
+PACKAGE_DIR = os.path.splitext(os.path.abspath(__file__))[0]
+BEHAVIOR_SUBDIR = "behavior_1"
+
+
+def run(motion, tts, teleop):
+ # Deploys (first run only, cached after) + runs the actual
+ # Choregraphe behavior on the robot - this blocks until it
+ # finishes, same pattern as the Gangnam Style gesture.
+ teleop.behavior_runner.ensure_running(PACKAGE_DIR, BEHAVIOR_SUBDIR)
+
+ # Tai chi moves legs/hips through slow stances, so bring him back
+ # to a known walk-ready stance afterward.
+ teleop.idle_crouch()
diff --git a/gestures/5_tai_chi/5_tai_chi.pml b/gestures/5_tai_chi/5_tai_chi.pml
new file mode 100644
index 0000000..8eece9e
--- /dev/null
+++ b/gestures/5_tai_chi/5_tai_chi.pml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gestures/5_tai_chi/manifest.xml b/gestures/5_tai_chi/manifest.xml
new file mode 100644
index 0000000..9b3f64d
--- /dev/null
+++ b/gestures/5_tai_chi/manifest.xml
@@ -0,0 +1,18 @@
+
+
+
+ Untitled
+
+
+ en_US
+
+
+ en_US
+
+
+
+ interactive
+
+
+
+
diff --git a/gestures/5_tai_chi/tai_chi/behavior.xar b/gestures/5_tai_chi/tai_chi/behavior.xar
new file mode 100644
index 0000000..f86e806
--- /dev/null
+++ b/gestures/5_tai_chi/tai_chi/behavior.xar
@@ -0,0 +1,857 @@
+
+
+
+ media/images/box/root.png
+
+
+
+
+
+
+
+
+
+
+ media/images/box/movement/dance.png
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/LED.png
+
+
+
+
+
+
+
+ media/images/box/interaction/LED.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/LED.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/play_music.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/interaction/play_music.png
+
+
+
+
+
+
+
+
+
+
+
+ media/images/box/folder.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gestures/5_tai_chi/translations/translation_en_US.ts b/gestures/5_tai_chi/translations/translation_en_US.ts
new file mode 100644
index 0000000..fbd1f4d
--- /dev/null
+++ b/gestures/5_tai_chi/translations/translation_en_US.ts
@@ -0,0 +1,3 @@
+
+
+
diff --git a/gestures/__pycache__/1_wave.cpython-311.pyc b/gestures/__pycache__/1_wave.cpython-311.pyc
new file mode 100644
index 0000000..70b5a5f
Binary files /dev/null and b/gestures/__pycache__/1_wave.cpython-311.pyc differ
diff --git a/gestures/__pycache__/2_cena.cpython-311.pyc b/gestures/__pycache__/2_cena.cpython-311.pyc
new file mode 100644
index 0000000..d06bdd6
Binary files /dev/null and b/gestures/__pycache__/2_cena.cpython-311.pyc differ
diff --git a/gestures/__pycache__/3_six_seven.cpython-311.pyc b/gestures/__pycache__/3_six_seven.cpython-311.pyc
new file mode 100644
index 0000000..56eec19
Binary files /dev/null and b/gestures/__pycache__/3_six_seven.cpython-311.pyc differ
diff --git a/gestures/__pycache__/4_gangnam_style.cpython-311.pyc b/gestures/__pycache__/4_gangnam_style.cpython-311.pyc
new file mode 100644
index 0000000..01e5b39
Binary files /dev/null and b/gestures/__pycache__/4_gangnam_style.cpython-311.pyc differ
diff --git a/gestures/__pycache__/5_tai_chi.cpython-311.pyc b/gestures/__pycache__/5_tai_chi.cpython-311.pyc
new file mode 100644
index 0000000..d779e7b
Binary files /dev/null and b/gestures/__pycache__/5_tai_chi.cpython-311.pyc differ
diff --git a/naowalk.py b/naowalk.py
index 1013335..4ad8070 100644
--- a/naowalk.py
+++ b/naowalk.py
@@ -15,6 +15,10 @@ import os
import json
import struct
import socket
+import importlib.util
+import shutil
+import tempfile
+import inspect
# Import the new music module
from naomusic import NaoMusicPlayer
@@ -456,6 +460,115 @@ class VideoServerLauncher:
self.ready = False
+# ==========================================
+# CHOREGRAPHE BEHAVIOR DEPLOY + RUN (for gestures)
+# ==========================================
+class ChoregrapheBehaviorRunner:
+ """Deploys a Choregraphe-exported behavior folder (a manifest.xml
+ plus one or more '/behavior.xar' dirs, i.e. exactly
+ what "Export as package" produces before you zip it) to the robot
+ over SFTP, installs it via the PackageManager service, and runs it
+ via ALBehaviorManager - the same three steps Choregraphe's own
+ "Connect to robot" + Play button does, done here in code so a
+ gesture script can just point at a local project folder.
+
+ Caches per local folder for the lifetime of this object: once a
+ package has been deployed+installed this session, repeat runs
+ just call runBehavior() again instead of re-uploading.
+
+ NOTE: PackageManager.install() is documented as "install a package
+ from a path" without saying whether that path can be a raw folder
+ or must be a zipped .pkg - this tries the raw folder first (since
+ that's genuinely what Choregraphe's live "current project" push
+ to a connected robot does) and falls back to zipping it into a
+ .pkg if the robot's PackageManager rejects the folder.
+ """
+ def __init__(self, session, nao_ip, ssh_user, ssh_password, ssh_port):
+ self.nao_ip = nao_ip
+ self.ssh_user = ssh_user
+ self.ssh_password = ssh_password
+ self.ssh_port = ssh_port
+ try:
+ self.package_mgr = session.service("PackageManager")
+ except Exception:
+ self.package_mgr = session.service("ALPackageManager")
+ self.behavior_mgr = session.service("ALBehaviorManager")
+ self._installed = {} # local_dir -> resolved "/" name
+
+ def _sftp_put_dir(self, sftp, local_dir, remote_dir):
+ """Recursively mirror local_dir onto remote_dir over an
+ already-open SFTP session - paramiko has no built-in recursive
+ put, only single-file put()."""
+ try:
+ sftp.mkdir(remote_dir)
+ except IOError:
+ pass # already exists, fine
+ for entry in sorted(os.listdir(local_dir)):
+ local_path = os.path.join(local_dir, entry)
+ remote_path = remote_dir + "/" + entry
+ if os.path.isdir(local_path):
+ self._sftp_put_dir(sftp, local_path, remote_path)
+ else:
+ sftp.put(local_path, remote_path)
+
+ def ensure_running(self, local_dir, behavior_subdir):
+ """Deploy+install local_dir (only if not already done this
+ session), then block until '/' has
+ finished running on the robot. local_dir's own folder name
+ becomes the package id, matching how Choregraphe names an
+ exported project after its folder."""
+ if not HAS_SSH:
+ raise RuntimeError("Choregraphe gesture deploy needs paramiko: pip install paramiko")
+ if not os.path.isdir(local_dir):
+ raise RuntimeError(f"No Choregraphe project folder at {local_dir}")
+
+ package_name = os.path.basename(os.path.normpath(local_dir))
+ behavior_name = self._installed.get(local_dir)
+
+ if behavior_name is None:
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(self.nao_ip, port=self.ssh_port, username=self.ssh_user,
+ password=self.ssh_password, timeout=10)
+ try:
+ sftp = ssh.open_sftp()
+ try:
+ remote_home = sftp.normalize(".")
+ remote_dir = f"{remote_home}/naowalk_gestures/{package_name}"
+ print(f"🕺 Copying {package_name} -> {self.nao_ip}:{remote_dir}")
+ self._sftp_put_dir(sftp, local_dir, remote_dir)
+
+ print(f"🕺 Installing package {package_name}...")
+ try:
+ self.package_mgr.install(remote_dir)
+ except Exception as e:
+ print(f"🕺 install() on the raw folder failed ({e}) - "
+ f"retrying as a zipped .pkg...")
+ local_zip = shutil.make_archive(
+ os.path.join(tempfile.gettempdir(), package_name), "zip", local_dir)
+ remote_zip = f"{remote_home}/naowalk_gestures/{package_name}.pkg"
+ sftp.put(local_zip, remote_zip)
+ os.remove(local_zip)
+ self.package_mgr.install(remote_zip)
+ finally:
+ sftp.close()
+ finally:
+ ssh.close()
+
+ behavior_name = f"{package_name}/{behavior_subdir}"
+ if not self.behavior_mgr.isBehaviorInstalled(behavior_name):
+ installed = self.behavior_mgr.getInstalledBehaviors()
+ candidates = [b for b in installed if b.endswith("/" + behavior_subdir)]
+ if not candidates:
+ raise RuntimeError(
+ f"'{behavior_name}' not found after install - installed behaviors: {installed}")
+ behavior_name = candidates[0]
+ self._installed[local_dir] = behavior_name
+
+ print(f"🕺 Running {behavior_name}...")
+ self.behavior_mgr.runBehavior(behavior_name) # blocks until it finishes
+
+
# ==========================================
# MAIN TELEOP
# ==========================================
@@ -464,9 +577,13 @@ class NaoTeleop:
def __init__(self, session, nao_ip, audio_device_index=None, mic_gain=8.0, video_scale=2.0,
nao_ssh_user="nao", nao_ssh_password="nao", nao_ssh_port=22, relay_rate=16000,
- media_relay=None, deploy_video_server=True, video_server_path=None):
+ media_relay=None, deploy_video_server=True, video_server_path=None,
+ gestures_dir=None):
self.session = session
self.nao_ip = nao_ip
+ self.nao_ssh_user = nao_ssh_user
+ self.nao_ssh_password = nao_ssh_password
+ self.nao_ssh_port = nao_ssh_port
self.motion = session.service("ALMotion")
self.posture = session.service("ALRobotPosture")
self.tts = session.service("ALTextToSpeech")
@@ -507,9 +624,12 @@ class NaoTeleop:
self.battery_level = 100
self.last_batt_check = 0
self.volume = 50
- self.walk_speed = 0.6
+ self.walk_speed = 1.0 # fixed, not adjustable at runtime - 0.60 was too slow
+ # for the walk engine to stay balanced and NAO kept
+ # tipping over
self.typing_mode = False
+ self.is_resting = False # tracks manual [DEL] crouch, toggled by idle_crouch()
self.chat_message = ""
self.music_search_mode = False
self.music_search_text = ""
@@ -546,6 +666,16 @@ class NaoTeleop:
if HAS_AUDIO:
self._init_mic_stream()
+ # Lets gesture scripts run full Choregraphe-authored behaviors
+ # (.xar, exported as a package folder) instead of hand-coding
+ # angleInterpolation calls - see ChoregrapheBehaviorRunner.
+ self.behavior_runner = ChoregrapheBehaviorRunner(
+ session, nao_ip, nao_ssh_user, nao_ssh_password, nao_ssh_port)
+
+ self.gestures_dir = gestures_dir or os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "gestures")
+ self._load_gestures()
+
pygame.init()
self.screen = pygame.display.set_mode((self.display_width, self.display_height))
pygame.display.set_caption("NAO Teleop + Music on Robot")
@@ -693,95 +823,104 @@ class NaoTeleop:
except Exception as e:
print(f"❌ Mic init failed: {e}")
- def wave_gesture(self):
- if self.typing_mode: return
- print("🤚 Waving hello...")
+ def _load_gestures(self):
+ """Load number-key gestures from .py files in self.gestures_dir.
+
+ Each file is named '_.py' (e.g. '4_gangnam_style.py')
+ and must define a module-level run(motion, tts) function - that's
+ the minimum contract, gets the already-connected ALMotion and
+ ALTextToSpeech proxies and does whatever it wants with them.
+ Gestures that need more - e.g. running a Choregraphe behavior via
+ self.behavior_runner, or resetting posture via
+ self.stand_for_walk() - can instead define run(motion, tts,
+ teleop) and get this NaoTeleop instance as a third argument; the
+ arity is detected here at load time so both signatures work. The
+ leading digit is which number key (0-9) triggers it; everything
+ after the underscore becomes its display name in the controls
+ hint, unless the file sets its own module-level NAME string.
+ """
+ self.gestures = {} # pygame key constant -> (display_name, run_fn, n_params)
+ gestures_dir = self.gestures_dir
+ if not os.path.isdir(gestures_dir):
+ print(f"⚠️ No gestures folder at {gestures_dir}, skipping gesture load")
+ return
+
+ for fname in sorted(os.listdir(gestures_dir)):
+ if not fname.endswith(".py") or fname.startswith("_"):
+ continue
+ stem = fname[:-3]
+ digit_str, sep, rest = stem.partition("_")
+ if not (sep and len(digit_str) == 1 and digit_str.isdigit()):
+ print(f"⚠️ Skipping {fname}: name it '_name.py' (e.g. '4_gangnam_style.py')")
+ continue
+
+ path = os.path.join(gestures_dir, fname)
+ try:
+ spec = importlib.util.spec_from_file_location(f"nao_gesture_{stem}", path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ run_fn = getattr(module, "run", None)
+ if not callable(run_fn):
+ print(f"⚠️ Skipping {fname}: no run(motion, tts) function defined")
+ continue
+ try:
+ n_params = len(inspect.signature(run_fn).parameters)
+ except (TypeError, ValueError):
+ n_params = 2
+ except Exception as e:
+ print(f"⚠️ Failed to load gesture {fname}: {e}")
+ continue
+
+ key = getattr(pygame, f"K_{digit_str}")
+ name = getattr(module, "NAME", rest.replace("_", " ").title() or stem)
+ if key in self.gestures:
+ print(f"⚠️ {fname} overrides number key {digit_str} (was {self.gestures[key][0]})")
+ self.gestures[key] = (name, run_fn, n_params)
+ print(f"✅ Loaded gesture: {digit_str}={name}")
+
+ def run_gesture(self, key):
+ if self.typing_mode or self.is_resting: return
+ entry = self.gestures.get(key)
+ if not entry: return
+ name, run_fn, n_params = entry
+ print(f"🕺 {name}...")
try:
- self.motion.setStiffnesses("RArm", 1.0)
- names = ["RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RElbowRoll", "RWristYaw", "RHand"]
- angles = [[-1.5]*5, [-0.4,-0.7,-0.4,-0.7,-0.4], [0.6,1.1,0.6,1.1,0.6],
- [0.8,0.3,0.8,0.3,0.8], [0.0,1.5,0.0,-1.5,0.0], [1.0]*5]
- times = [[0.5,1.0,1.5,2.0,2.5]] * 6
- self.motion.angleInterpolation(names, angles, times, True)
- 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)
+ if n_params >= 3:
+ run_fn(self.motion, self.tts, self)
+ else:
+ run_fn(self.motion, self.tts)
except Exception as e:
- print(f"Wave error: {e}")
+ print(f"{name} gesture 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 stand_for_walk(self):
+ """Re-stiffen and drop straight into walk-ready posture -
+ mirrors the startup sequence in run() (stiffen -> walk arms ->
+ relax arm stiffness -> moveInit) rather than
+ goToPosture("Stand"), which goes through its own separate
+ stand-up animation instead of just being walk-ready. Shared by
+ the [DEL] wake-up toggle and by any gesture that needs to
+ reset his stance afterward (e.g. after a Choregraphe behavior
+ that moved the legs/hips)."""
+ self._safe("Restiffen", lambda: self.motion.setStiffnesses("Body", 1.0))
+ self._safe("Walk arms", lambda: self.motion.setMoveArmsEnabled(True, True))
+ self._safe("Relax arms", lambda: self.motion.setStiffnesses(["RArm", "LArm"], 0.3))
+ self._safe("moveInit", self.motion.moveInit)
+ self.is_resting = False
- 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 idle_crouch(self):
+ """The relaxed sit/crouch posture NAO settles into on shutdown -
+ toggled on demand with the Delete key. rest() cuts stiffness to
+ the whole body (that's the point of "rest"), so simply calling
+ it again does nothing on the second press - we have to
+ explicitly re-stiffen ourselves via stand_for_walk()."""
+ if not self.is_resting:
+ print("😴 Resting (idle crouch)...")
+ self._safe("stopMove", self.motion.stopMove)
+ self._safe("rest", self.motion.rest)
+ self.is_resting = True
+ else:
+ print("🧍 Waking up...")
+ self.stand_for_walk()
def handle_head_movement(self):
if self.typing_mode or self.music_search_mode: return
@@ -872,7 +1011,11 @@ class NaoTeleop:
self._movement_thread = threading.Thread(target=self._movement_loop, daemon=True)
self._movement_thread.start()
- 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")
+ gesture_hint = " | ".join(
+ f"{pygame.key.name(key)}={name}" for key, (name, *_rest) in sorted(self.gestures.items())
+ )
+ print(f"\n🎮 Controls: WASD/Arrows=Walk | IJKL=Head | SPACE=Reset head | {gesture_hint} | "
+ f"DEL=Toggle crouch/stand | M=Music search | X=Stop | -/==Volume | T=TTS | V=Push-to-talk (hold) | ESC=Quit")
try:
while self.running:
@@ -948,12 +1091,10 @@ class NaoTeleop:
else:
if event.key == pygame.K_ESCAPE:
self.running = False
- elif event.key == pygame.K_1:
- 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_DELETE:
+ self.idle_crouch()
+ elif event.key in self.gestures:
+ self.run_gesture(event.key)
elif event.key == pygame.K_SPACE:
self.reset_head()
elif event.key == pygame.K_m:
@@ -984,16 +1125,6 @@ class NaoTeleop:
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
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 = -self.walk_speed
@@ -1161,6 +1292,10 @@ if __name__ == "__main__":
parser.add_argument("--video-server-path", type=str, default=None,
help="Local path to nao_video_server.py to deploy (default: the "
"copy sitting next to naowalk.py)")
+ parser.add_argument("--gestures-dir", type=str, default=None,
+ help="Folder of number-key gestures to load, each file named "
+ "'_name.py' with a run(motion, tts) function "
+ "(default: the 'gestures' folder next to naowalk.py)")
args = parser.parse_args()
session = qi.Session()
@@ -1181,4 +1316,5 @@ if __name__ == "__main__":
nao_ssh_port=args.nao_ssh_port,
relay_rate=args.relay_rate,
deploy_video_server=not args.no_deploy_video_server,
- video_server_path=args.video_server_path).run()
+ video_server_path=args.video_server_path,
+ gestures_dir=args.gestures_dir).run()
diff --git a/naowifi.sh b/naowifi.sh
new file mode 100755
index 0000000..f89d4a0
--- /dev/null
+++ b/naowifi.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+nmcli c up naorobotwifi\ \(WAP\) && nmcli c up BC-VR\ \(USBWIFI\)
diff --git a/other-repos/nao-dances b/other-repos/nao-dances
new file mode 160000
index 0000000..5cae035
--- /dev/null
+++ b/other-repos/nao-dances
@@ -0,0 +1 @@
+Subproject commit 5cae0354f57e6cdde6246e577a5b3eb99506ff50