storeos / storeos_robot /motion.py
Cybire's picture
replace reachy_mini.utils.create_head_pose with inline impl to avoid scipy
8f00802
Raw
History Blame Contribute Delete
6.02 kB
"""Robot motion — DoA tracking + expressive actions."""
import asyncio
import math
import time
import logging
import re
import numpy as np
def create_head_pose(roll=0, pitch=0, yaw=0, degrees=False):
if degrees:
roll, pitch, yaw = math.radians(roll), math.radians(pitch), math.radians(yaw)
cr, sr = math.cos(roll), math.sin(roll)
cp, sp = math.cos(pitch), math.sin(pitch)
cy, sy = math.cos(yaw), math.sin(yaw)
R = np.array([
[cy*cp, cy*sp*sr - sy*cr, cy*sp*cr + sy*sr, 0],
[sy*cp, sy*sp*sr + cy*cr, sy*sp*cr - cy*sr, 0],
[-sp, cp*sr, cp*cr, 0],
[0, 0, 0, 1],
], dtype=np.float64)
return R
from .config import DOA_SMOOTHING, CONTROL_LOOP_HZ
logger = logging.getLogger(__name__)
ACTION_RE = re.compile(r"\[ACTION:(\w+)\]", re.IGNORECASE)
SALE_RE = re.compile(r"\[SALE:([^\]]+)\]", re.IGNORECASE)
def parse_response(text: str) -> tuple[str, list[str], list[str]]:
actions = [m.group(1).lower() for m in ACTION_RE.finditer(text)]
sales = [m.group(1).strip() for m in SALE_RE.finditer(text)]
clean = ACTION_RE.sub("", text)
clean = SALE_RE.sub("", clean).strip()
return clean, actions, sales
class MotionController:
def __init__(self, mini):
self.mini = mini
self._running = False
self._doa_angle: float | None = None
self._speech_detected = False
self._is_speaking = False
self._action_queue: asyncio.Queue[str] = asyncio.Queue()
async def start(self):
self._running = True
async def stop(self):
self._running = False
def set_speaking(self, val: bool):
self._is_speaking = val
def trigger(self, action: str):
try:
self._action_queue.put_nowait(action)
except asyncio.QueueFull:
pass
async def doa_tracking_loop(self):
loop = asyncio.get_event_loop()
while self._running:
try:
result = await loop.run_in_executor(None, self.mini.media.get_DoA)
if result:
angle, speech = result
self._speech_detected = speech
if self._doa_angle is None:
self._doa_angle = angle
else:
self._doa_angle = DOA_SMOOTHING * angle + (1 - DOA_SMOOTHING) * self._doa_angle
except Exception:
pass
await asyncio.sleep(0.05)
async def motion_loop(self):
while self._running:
t = time.monotonic()
head_yaw = 0.0
if self._doa_angle is not None and self._speech_detected:
head_yaw = max(min((math.pi / 2) - self._doa_angle, 0.7), -0.7)
ant_l, ant_r = 0.0, 0.0
if self._is_speaking:
amp = math.radians(25)
ant_l = amp * math.sin(2 * math.pi * 1.5 * t)
ant_r = amp * math.sin(2 * math.pi * 1.5 * t + math.pi / 3)
head = create_head_pose(roll=0, pitch=0, yaw=head_yaw, degrees=False)
self.mini.set_target(head=head, antennas=[ant_r, ant_l])
await asyncio.sleep(1.0 / CONTROL_LOOP_HZ)
async def action_loop(self):
while self._running:
try:
action = await asyncio.wait_for(self._action_queue.get(), timeout=0.5)
await self._execute(action)
except asyncio.TimeoutError:
continue
async def _execute(self, action: str):
m = self.mini
if action == "wave":
for _ in range(3):
m.goto_target(antennas=[0, math.radians(-40)], duration=0.2)
await asyncio.sleep(0.25)
m.goto_target(antennas=[math.radians(-40), 0], duration=0.2)
await asyncio.sleep(0.25)
m.goto_target(antennas=[0, 0], duration=0.3)
elif action == "nod":
for angle in [-15, 10, -10, 0]:
head = create_head_pose(pitch=math.radians(angle), degrees=False)
m.goto_target(head=head, duration=0.2)
await asyncio.sleep(0.25)
elif action == "shake":
for yaw in [-20, 20, -15, 0]:
head = create_head_pose(yaw=math.radians(yaw), degrees=False)
m.goto_target(head=head, duration=0.2)
await asyncio.sleep(0.2)
elif action == "happy":
for _ in range(3):
m.goto_target(antennas=[math.radians(-30), math.radians(-30)], duration=0.15)
await asyncio.sleep(0.2)
m.goto_target(antennas=[math.radians(10), math.radians(10)], duration=0.15)
await asyncio.sleep(0.2)
m.goto_target(antennas=[0, 0], duration=0.3)
elif action == "dance":
for _ in range(4):
m.goto_target(body_yaw=math.radians(20), antennas=[math.radians(-30), math.radians(10)], duration=0.25)
await asyncio.sleep(0.3)
m.goto_target(body_yaw=math.radians(-20), antennas=[math.radians(10), math.radians(-30)], duration=0.25)
await asyncio.sleep(0.3)
m.goto_target(body_yaw=0, antennas=[0, 0], duration=0.3)
elif action == "think":
head = create_head_pose(roll=math.radians(5), pitch=math.radians(-10), yaw=math.radians(15), degrees=False)
m.goto_target(head=head, antennas=[math.radians(-20), math.radians(-20)], duration=0.4)
await asyncio.sleep(1.5)
m.goto_target(head=create_head_pose(degrees=False), antennas=[0, 0], duration=0.4)
elif action == "greet":
head = create_head_pose(pitch=math.radians(-15), degrees=False)
m.goto_target(head=head, antennas=[math.radians(-20), math.radians(-20)], duration=0.3)
await asyncio.sleep(0.5)
m.goto_target(head=create_head_pose(degrees=False), antennas=[0, 0], duration=0.3)