From a8fe887c48c659b878d2492e5c9212d1d19c5775 Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Tue, 16 Dec 2025 08:35:26 +0000 Subject: [PATCH] feat: gestures to ri --- .../endpoints/actuation_receiver.py | 68 ++- .../endpoints/gesture_settings.py | 412 ++++++++++++++++++ test/unit/test_actuation_receiver.py | 157 ++++++- 3 files changed, 634 insertions(+), 3 deletions(-) create mode 100644 src/robot_interface/endpoints/gesture_settings.py diff --git a/src/robot_interface/endpoints/actuation_receiver.py b/src/robot_interface/endpoints/actuation_receiver.py index ee09acb..fdc77d9 100644 --- a/src/robot_interface/endpoints/actuation_receiver.py +++ b/src/robot_interface/endpoints/actuation_receiver.py @@ -7,6 +7,7 @@ from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.state import state from robot_interface.core.config import settings +from robot_interface.endpoints.gesture_settings import GestureTags class ActuationReceiver(ReceiverBase): @@ -21,12 +22,16 @@ class ActuationReceiver(ReceiverBase): :ivar _tts_service: The text-to-speech service object from the Qi session. :vartype _tts_service: qi.Session | None + + :ivar _animation_service: The animation/gesture service object from the Qi session. + :vartype _animation_service: qi.Session | None """ def __init__(self, zmq_context, port=settings.agent_settings.actuation_receiver_port): super(ActuationReceiver, self).__init__("actuation") self.create_socket(zmq_context, zmq.SUB, port) self.socket.setsockopt_string(zmq.SUBSCRIBE, u"") # Causes block if given in options self._tts_service = None + self._animation_service = None def _handle_speech(self, message): """ @@ -54,7 +59,54 @@ class ActuationReceiver(ReceiverBase): self._tts_service = state.qi_session.service("ALTextToSpeech") # Returns instantly. Messages received while speaking will be queued. - qi.async(self._tts_service.say, text) + getattr(qi, "async")(self._tts_service.say, text) + + def _handle_gesture(self, message, is_single): + """ + Handle a gesture actuation request. + + :param message: The gesture to do, must contain properties "endpoint" and "data". + :type message: dict + + :param is_single: Whether it's a specific single gesture or a gesture tag. + :type is_single: bool + """ + + gesture = message.get("data") + if not gesture: + logging.warn("Received gesture to do, but it lacks data.") + return + + if not isinstance(gesture, (str, unicode)): + logging.warn("Received gesture to do but it is not a string.") + return + + logging.debug("Received gesture to do: {}".format(gesture)) + + if is_single: + if gesture not in GestureTags.single_gestures: + logging.warn("Received single gesture to do, but it does not exist in settings") + return + else: + if gesture not in GestureTags.tags: + logging.warn("Received single tag to do, but it does not exist in settings") + return + + if not state.qi_session: return + # If state has a qi_session, we know that we can import qi + import qi # Takes a while only the first time it's imported + + if not self._animation_service: + self._animation_service = state.qi_session.service("ALAnimationPlayer") + + # Play the gesture. Pepper comes with predefined animations like "Wave", "Greet", "Clap" + # You can also create custom animations using Choregraphe and upload them to the robot. + if is_single: + logging.debug("Playing single gesture: {}".format(gesture)) + getattr(qi, "async")(self._animation_service.run, gesture) + else: + logging.debug("Playing tag gesture: {}".format(gesture)) + getattr(qi, "async")(self._animation_service.runTag, gesture) def handle_message(self, message): """ @@ -65,3 +117,17 @@ class ActuationReceiver(ReceiverBase): """ if message["endpoint"] == "actuate/speech": self._handle_speech(message) + if message["endpoint"] == "actuate/gesture/tag": + self._handle_gesture(message, False) + if message["endpoint"] == "actuate/gesture/single": + self._handle_gesture(message, True) + + def endpoint_description(self): + """ + Extend the default endpoint description with gesture tags. + Returned during negotiate/ports so the CB knows available gestures. + """ + desc = super(ActuationReceiver, self).endpoint_description() + desc["gestures"] = GestureTags.tags + desc["single_gestures"] = GestureTags.single_gestures + return desc diff --git a/src/robot_interface/endpoints/gesture_settings.py b/src/robot_interface/endpoints/gesture_settings.py new file mode 100644 index 0000000..26b7c99 --- /dev/null +++ b/src/robot_interface/endpoints/gesture_settings.py @@ -0,0 +1,412 @@ +class GestureTags: + tags = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any", + "assuage", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank", + "body language", "bored", "bow", "but", "call", "calm", "choose", "choice", "cloud", + "cogitate", "cool", "crazy", "disappointed", "down", "earth", "empty", "embarrassed", + "enthusiastic", "entire", "estimate", "except", "exalted", "excited", "explain", "far", + "field", "floor", "forlorn", "friendly", "front", "frustrated", "gentle", "gift", + "give", "ground", "happy", "hello", "her", "here", "hey", "hi", "him", "hopeless", + "hysterical", "I", "implore", "indicate", "joyful", "me", "meditate", "modest", + "negative", "nervous", "no", "not know", "nothing", "offer", "ok", "once upon a time", + "oppose", "or", "pacify", "pick", "placate", "please", "present", "proffer", "quiet", + "reason", "refute", "reject", "rousing", "sad", "select", "shamefaced", "show", + "show sky", "sky", "soothe", "sun", "supplicate", "tablet", "tall", "them", "there", + "think", "timid", "top", "unless", "up", "upstairs", "void", "warm", "winner", "yeah", + "yes", "yoo-hoo", "you", "your", "zero", "zestful"] + + single_gestures = [ + "animations/Stand/BodyTalk/Listening/Listening_1", + "animations/Stand/BodyTalk/Listening/Listening_2", + "animations/Stand/BodyTalk/Listening/Listening_3", + "animations/Stand/BodyTalk/Listening/Listening_4", + "animations/Stand/BodyTalk/Listening/Listening_5", + "animations/Stand/BodyTalk/Listening/Listening_6", + "animations/Stand/BodyTalk/Listening/Listening_7", + "animations/Stand/BodyTalk/Speaking/BodyTalk_1", + "animations/Stand/BodyTalk/Speaking/BodyTalk_10", + "animations/Stand/BodyTalk/Speaking/BodyTalk_11", + "animations/Stand/BodyTalk/Speaking/BodyTalk_12", + "animations/Stand/BodyTalk/Speaking/BodyTalk_13", + "animations/Stand/BodyTalk/Speaking/BodyTalk_14", + "animations/Stand/BodyTalk/Speaking/BodyTalk_15", + "animations/Stand/BodyTalk/Speaking/BodyTalk_16", + "animations/Stand/BodyTalk/Speaking/BodyTalk_2", + "animations/Stand/BodyTalk/Speaking/BodyTalk_3", + "animations/Stand/BodyTalk/Speaking/BodyTalk_4", + "animations/Stand/BodyTalk/Speaking/BodyTalk_5", + "animations/Stand/BodyTalk/Speaking/BodyTalk_6", + "animations/Stand/BodyTalk/Speaking/BodyTalk_7", + "animations/Stand/BodyTalk/Speaking/BodyTalk_8", + "animations/Stand/BodyTalk/Speaking/BodyTalk_9", + "animations/Stand/BodyTalk/Thinking/Remember_1", + "animations/Stand/BodyTalk/Thinking/Remember_2", + "animations/Stand/BodyTalk/Thinking/Remember_3", + "animations/Stand/BodyTalk/Thinking/ThinkingLoop_1", + "animations/Stand/BodyTalk/Thinking/ThinkingLoop_2", + "animations/Stand/Emotions/Negative/Angry_1", + "animations/Stand/Emotions/Negative/Angry_2", + "animations/Stand/Emotions/Negative/Angry_3", + "animations/Stand/Emotions/Negative/Angry_4", + "animations/Stand/Emotions/Negative/Anxious_1", + "animations/Stand/Emotions/Negative/Bored_1", + "animations/Stand/Emotions/Negative/Bored_2", + "animations/Stand/Emotions/Negative/Disappointed_1", + "animations/Stand/Emotions/Negative/Exhausted_1", + "animations/Stand/Emotions/Negative/Exhausted_2", + "animations/Stand/Emotions/Negative/Fear_1", + "animations/Stand/Emotions/Negative/Fear_2", + "animations/Stand/Emotions/Negative/Fearful_1", + "animations/Stand/Emotions/Negative/Frustrated_1", + "animations/Stand/Emotions/Negative/Humiliated_1", + "animations/Stand/Emotions/Negative/Hurt_1", + "animations/Stand/Emotions/Negative/Hurt_2", + "animations/Stand/Emotions/Negative/Late_1", + "animations/Stand/Emotions/Negative/Sad_1", + "animations/Stand/Emotions/Negative/Sad_2", + "animations/Stand/Emotions/Negative/Shocked_1", + "animations/Stand/Emotions/Negative/Sorry_1", + "animations/Stand/Emotions/Negative/Surprise_1", + "animations/Stand/Emotions/Negative/Surprise_2", + "animations/Stand/Emotions/Negative/Surprise_3", + "animations/Stand/Emotions/Neutral/Alienated_1", + "animations/Stand/Emotions/Neutral/AskForAttention_1", + "animations/Stand/Emotions/Neutral/AskForAttention_2", + "animations/Stand/Emotions/Neutral/AskForAttention_3", + "animations/Stand/Emotions/Neutral/Cautious_1", + "animations/Stand/Emotions/Neutral/Confused_1", + "animations/Stand/Emotions/Neutral/Determined_1", + "animations/Stand/Emotions/Neutral/Embarrassed_1", + "animations/Stand/Emotions/Neutral/Hesitation_1", + "animations/Stand/Emotions/Neutral/Innocent_1", + "animations/Stand/Emotions/Neutral/Lonely_1", + "animations/Stand/Emotions/Neutral/Mischievous_1", + "animations/Stand/Emotions/Neutral/Puzzled_1", + "animations/Stand/Emotions/Neutral/Sneeze", + "animations/Stand/Emotions/Neutral/Stubborn_1", + "animations/Stand/Emotions/Neutral/Suspicious_1", + "animations/Stand/Emotions/Positive/Amused_1", + "animations/Stand/Emotions/Positive/Confident_1", + "animations/Stand/Emotions/Positive/Ecstatic_1", + "animations/Stand/Emotions/Positive/Enthusiastic_1", + "animations/Stand/Emotions/Positive/Excited_1", + "animations/Stand/Emotions/Positive/Excited_2", + "animations/Stand/Emotions/Positive/Excited_3", + "animations/Stand/Emotions/Positive/Happy_1", + "animations/Stand/Emotions/Positive/Happy_2", + "animations/Stand/Emotions/Positive/Happy_3", + "animations/Stand/Emotions/Positive/Happy_4", + "animations/Stand/Emotions/Positive/Hungry_1", + "animations/Stand/Emotions/Positive/Hysterical_1", + "animations/Stand/Emotions/Positive/Interested_1", + "animations/Stand/Emotions/Positive/Interested_2", + "animations/Stand/Emotions/Positive/Laugh_1", + "animations/Stand/Emotions/Positive/Laugh_2", + "animations/Stand/Emotions/Positive/Laugh_3", + "animations/Stand/Emotions/Positive/Mocker_1", + "animations/Stand/Emotions/Positive/Optimistic_1", + "animations/Stand/Emotions/Positive/Peaceful_1", + "animations/Stand/Emotions/Positive/Proud_1", + "animations/Stand/Emotions/Positive/Proud_2", + "animations/Stand/Emotions/Positive/Proud_3", + "animations/Stand/Emotions/Positive/Relieved_1", + "animations/Stand/Emotions/Positive/Shy_1", + "animations/Stand/Emotions/Positive/Shy_2", + "animations/Stand/Emotions/Positive/Sure_1", + "animations/Stand/Emotions/Positive/Winner_1", + "animations/Stand/Emotions/Positive/Winner_2", + "animations/Stand/Gestures/Angry_1", + "animations/Stand/Gestures/Angry_2", + "animations/Stand/Gestures/Angry_3", + "animations/Stand/Gestures/BowShort_1", + "animations/Stand/Gestures/BowShort_2", + "animations/Stand/Gestures/BowShort_3", + "animations/Stand/Gestures/But_1", + "animations/Stand/Gestures/CalmDown_1", + "animations/Stand/Gestures/CalmDown_2", + "animations/Stand/Gestures/CalmDown_3", + "animations/Stand/Gestures/CalmDown_4", + "animations/Stand/Gestures/CalmDown_5", + "animations/Stand/Gestures/CalmDown_6", + "animations/Stand/Gestures/Choice_1", + "animations/Stand/Gestures/ComeOn_1", + "animations/Stand/Gestures/Confused_1", + "animations/Stand/Gestures/Confused_2", + "animations/Stand/Gestures/CountFive_1", + "animations/Stand/Gestures/CountFour_1", + "animations/Stand/Gestures/CountMore_1", + "animations/Stand/Gestures/CountOne_1", + "animations/Stand/Gestures/CountThree_1", + "animations/Stand/Gestures/CountTwo_1", + "animations/Stand/Gestures/Desperate_1", + "animations/Stand/Gestures/Desperate_2", + "animations/Stand/Gestures/Desperate_3", + "animations/Stand/Gestures/Desperate_4", + "animations/Stand/Gestures/Desperate_5", + "animations/Stand/Gestures/DontUnderstand_1", + "animations/Stand/Gestures/Enthusiastic_3", + "animations/Stand/Gestures/Enthusiastic_4", + "animations/Stand/Gestures/Enthusiastic_5", + "animations/Stand/Gestures/Everything_1", + "animations/Stand/Gestures/Everything_2", + "animations/Stand/Gestures/Everything_3", + "animations/Stand/Gestures/Everything_4", + "animations/Stand/Gestures/Everything_6", + "animations/Stand/Gestures/Excited_1", + "animations/Stand/Gestures/Explain_1", + "animations/Stand/Gestures/Explain_10", + "animations/Stand/Gestures/Explain_11", + "animations/Stand/Gestures/Explain_2", + "animations/Stand/Gestures/Explain_3", + "animations/Stand/Gestures/Explain_4", + "animations/Stand/Gestures/Explain_5", + "animations/Stand/Gestures/Explain_6", + "animations/Stand/Gestures/Explain_7", + "animations/Stand/Gestures/Explain_8", + "animations/Stand/Gestures/Far_1", + "animations/Stand/Gestures/Far_2", + "animations/Stand/Gestures/Far_3", + "animations/Stand/Gestures/Follow_1", + "animations/Stand/Gestures/Give_1", + "animations/Stand/Gestures/Give_2", + "animations/Stand/Gestures/Give_3", + "animations/Stand/Gestures/Give_4", + "animations/Stand/Gestures/Give_5", + "animations/Stand/Gestures/Give_6", + "animations/Stand/Gestures/Great_1", + "animations/Stand/Gestures/HeSays_1", + "animations/Stand/Gestures/HeSays_2", + "animations/Stand/Gestures/HeSays_3", + "animations/Stand/Gestures/Hey_1", + "animations/Stand/Gestures/Hey_10", + "animations/Stand/Gestures/Hey_2", + "animations/Stand/Gestures/Hey_3", + "animations/Stand/Gestures/Hey_4", + "animations/Stand/Gestures/Hey_6", + "animations/Stand/Gestures/Hey_7", + "animations/Stand/Gestures/Hey_8", + "animations/Stand/Gestures/Hey_9", + "animations/Stand/Gestures/Hide_1", + "animations/Stand/Gestures/Hot_1", + "animations/Stand/Gestures/Hot_2", + "animations/Stand/Gestures/IDontKnow_1", + "animations/Stand/Gestures/IDontKnow_2", + "animations/Stand/Gestures/IDontKnow_3", + "animations/Stand/Gestures/IDontKnow_4", + "animations/Stand/Gestures/IDontKnow_5", + "animations/Stand/Gestures/IDontKnow_6", + "animations/Stand/Gestures/Joy_1", + "animations/Stand/Gestures/Kisses_1", + "animations/Stand/Gestures/Look_1", + "animations/Stand/Gestures/Look_2", + "animations/Stand/Gestures/Maybe_1", + "animations/Stand/Gestures/Me_1", + "animations/Stand/Gestures/Me_2", + "animations/Stand/Gestures/Me_4", + "animations/Stand/Gestures/Me_7", + "animations/Stand/Gestures/Me_8", + "animations/Stand/Gestures/Mime_1", + "animations/Stand/Gestures/Mime_2", + "animations/Stand/Gestures/Next_1", + "animations/Stand/Gestures/No_1", + "animations/Stand/Gestures/No_2", + "animations/Stand/Gestures/No_3", + "animations/Stand/Gestures/No_4", + "animations/Stand/Gestures/No_5", + "animations/Stand/Gestures/No_6", + "animations/Stand/Gestures/No_7", + "animations/Stand/Gestures/No_8", + "animations/Stand/Gestures/No_9", + "animations/Stand/Gestures/Nothing_1", + "animations/Stand/Gestures/Nothing_2", + "animations/Stand/Gestures/OnTheEvening_1", + "animations/Stand/Gestures/OnTheEvening_2", + "animations/Stand/Gestures/OnTheEvening_3", + "animations/Stand/Gestures/OnTheEvening_4", + "animations/Stand/Gestures/OnTheEvening_5", + "animations/Stand/Gestures/Please_1", + "animations/Stand/Gestures/Please_2", + "animations/Stand/Gestures/Please_3", + "animations/Stand/Gestures/Reject_1", + "animations/Stand/Gestures/Reject_2", + "animations/Stand/Gestures/Reject_3", + "animations/Stand/Gestures/Reject_4", + "animations/Stand/Gestures/Reject_5", + "animations/Stand/Gestures/Reject_6", + "animations/Stand/Gestures/Salute_1", + "animations/Stand/Gestures/Salute_2", + "animations/Stand/Gestures/Salute_3", + "animations/Stand/Gestures/ShowFloor_1", + "animations/Stand/Gestures/ShowFloor_2", + "animations/Stand/Gestures/ShowFloor_3", + "animations/Stand/Gestures/ShowFloor_4", + "animations/Stand/Gestures/ShowFloor_5", + "animations/Stand/Gestures/ShowSky_1", + "animations/Stand/Gestures/ShowSky_10", + "animations/Stand/Gestures/ShowSky_11", + "animations/Stand/Gestures/ShowSky_12", + "animations/Stand/Gestures/ShowSky_2", + "animations/Stand/Gestures/ShowSky_3", + "animations/Stand/Gestures/ShowSky_4", + "animations/Stand/Gestures/ShowSky_5", + "animations/Stand/Gestures/ShowSky_6", + "animations/Stand/Gestures/ShowSky_7", + "animations/Stand/Gestures/ShowSky_8", + "animations/Stand/Gestures/ShowSky_9", + "animations/Stand/Gestures/ShowTablet_1", + "animations/Stand/Gestures/ShowTablet_2", + "animations/Stand/Gestures/ShowTablet_3", + "animations/Stand/Gestures/Shy_1", + "animations/Stand/Gestures/Stretch_1", + "animations/Stand/Gestures/Stretch_2", + "animations/Stand/Gestures/Surprised_1", + "animations/Stand/Gestures/TakePlace_1", + "animations/Stand/Gestures/TakePlace_2", + "animations/Stand/Gestures/Take_1", + "animations/Stand/Gestures/Thinking_1", + "animations/Stand/Gestures/Thinking_2", + "animations/Stand/Gestures/Thinking_3", + "animations/Stand/Gestures/Thinking_4", + "animations/Stand/Gestures/Thinking_5", + "animations/Stand/Gestures/Thinking_6", + "animations/Stand/Gestures/Thinking_7", + "animations/Stand/Gestures/Thinking_8", + "animations/Stand/Gestures/This_1", + "animations/Stand/Gestures/This_10", + "animations/Stand/Gestures/This_11", + "animations/Stand/Gestures/This_12", + "animations/Stand/Gestures/This_13", + "animations/Stand/Gestures/This_14", + "animations/Stand/Gestures/This_15", + "animations/Stand/Gestures/This_2", + "animations/Stand/Gestures/This_3", + "animations/Stand/Gestures/This_4", + "animations/Stand/Gestures/This_5", + "animations/Stand/Gestures/This_6", + "animations/Stand/Gestures/This_7", + "animations/Stand/Gestures/This_8", + "animations/Stand/Gestures/This_9", + "animations/Stand/Gestures/WhatSThis_1", + "animations/Stand/Gestures/WhatSThis_10", + "animations/Stand/Gestures/WhatSThis_11", + "animations/Stand/Gestures/WhatSThis_12", + "animations/Stand/Gestures/WhatSThis_13", + "animations/Stand/Gestures/WhatSThis_14", + "animations/Stand/Gestures/WhatSThis_15", + "animations/Stand/Gestures/WhatSThis_16", + "animations/Stand/Gestures/WhatSThis_2", + "animations/Stand/Gestures/WhatSThis_3", + "animations/Stand/Gestures/WhatSThis_4", + "animations/Stand/Gestures/WhatSThis_5", + "animations/Stand/Gestures/WhatSThis_6", + "animations/Stand/Gestures/WhatSThis_7", + "animations/Stand/Gestures/WhatSThis_8", + "animations/Stand/Gestures/WhatSThis_9", + "animations/Stand/Gestures/Whisper_1", + "animations/Stand/Gestures/Wings_1", + "animations/Stand/Gestures/Wings_2", + "animations/Stand/Gestures/Wings_3", + "animations/Stand/Gestures/Wings_4", + "animations/Stand/Gestures/Wings_5", + "animations/Stand/Gestures/Yes_1", + "animations/Stand/Gestures/Yes_2", + "animations/Stand/Gestures/Yes_3", + "animations/Stand/Gestures/YouKnowWhat_1", + "animations/Stand/Gestures/YouKnowWhat_2", + "animations/Stand/Gestures/YouKnowWhat_3", + "animations/Stand/Gestures/YouKnowWhat_4", + "animations/Stand/Gestures/YouKnowWhat_5", + "animations/Stand/Gestures/YouKnowWhat_6", + "animations/Stand/Gestures/You_1", + "animations/Stand/Gestures/You_2", + "animations/Stand/Gestures/You_3", + "animations/Stand/Gestures/You_4", + "animations/Stand/Gestures/You_5", + "animations/Stand/Gestures/Yum_1", + "animations/Stand/Reactions/EthernetOff_1", + "animations/Stand/Reactions/EthernetOn_1", + "animations/Stand/Reactions/Heat_1", + "animations/Stand/Reactions/Heat_2", + "animations/Stand/Reactions/LightShine_1", + "animations/Stand/Reactions/LightShine_2", + "animations/Stand/Reactions/LightShine_3", + "animations/Stand/Reactions/LightShine_4", + "animations/Stand/Reactions/SeeColor_1", + "animations/Stand/Reactions/SeeColor_2", + "animations/Stand/Reactions/SeeColor_3", + "animations/Stand/Reactions/SeeSomething_1", + "animations/Stand/Reactions/SeeSomething_3", + "animations/Stand/Reactions/SeeSomething_4", + "animations/Stand/Reactions/SeeSomething_5", + "animations/Stand/Reactions/SeeSomething_6", + "animations/Stand/Reactions/SeeSomething_7", + "animations/Stand/Reactions/SeeSomething_8", + "animations/Stand/Reactions/ShakeBody_1", + "animations/Stand/Reactions/ShakeBody_2", + "animations/Stand/Reactions/ShakeBody_3", + "animations/Stand/Reactions/TouchHead_1", + "animations/Stand/Reactions/TouchHead_2", + "animations/Stand/Reactions/TouchHead_3", + "animations/Stand/Reactions/TouchHead_4", + "animations/Stand/Waiting/AirGuitar_1", + "animations/Stand/Waiting/BackRubs_1", + "animations/Stand/Waiting/Bandmaster_1", + "animations/Stand/Waiting/Binoculars_1", + "animations/Stand/Waiting/BreathLoop_1", + "animations/Stand/Waiting/BreathLoop_2", + "animations/Stand/Waiting/BreathLoop_3", + "animations/Stand/Waiting/CallSomeone_1", + "animations/Stand/Waiting/Drink_1", + "animations/Stand/Waiting/DriveCar_1", + "animations/Stand/Waiting/Fitness_1", + "animations/Stand/Waiting/Fitness_2", + "animations/Stand/Waiting/Fitness_3", + "animations/Stand/Waiting/FunnyDancer_1", + "animations/Stand/Waiting/HappyBirthday_1", + "animations/Stand/Waiting/Helicopter_1", + "animations/Stand/Waiting/HideEyes_1", + "animations/Stand/Waiting/HideHands_1", + "animations/Stand/Waiting/Innocent_1", + "animations/Stand/Waiting/Knight_1", + "animations/Stand/Waiting/KnockEye_1", + "animations/Stand/Waiting/KungFu_1", + "animations/Stand/Waiting/LookHand_1", + "animations/Stand/Waiting/LookHand_2", + "animations/Stand/Waiting/LoveYou_1", + "animations/Stand/Waiting/Monster_1", + "animations/Stand/Waiting/MysticalPower_1", + "animations/Stand/Waiting/PlayHands_1", + "animations/Stand/Waiting/PlayHands_2", + "animations/Stand/Waiting/PlayHands_3", + "animations/Stand/Waiting/Relaxation_1", + "animations/Stand/Waiting/Relaxation_2", + "animations/Stand/Waiting/Relaxation_3", + "animations/Stand/Waiting/Relaxation_4", + "animations/Stand/Waiting/Rest_1", + "animations/Stand/Waiting/Robot_1", + "animations/Stand/Waiting/ScratchBack_1", + "animations/Stand/Waiting/ScratchBottom_1", + "animations/Stand/Waiting/ScratchEye_1", + "animations/Stand/Waiting/ScratchHand_1", + "animations/Stand/Waiting/ScratchHead_1", + "animations/Stand/Waiting/ScratchLeg_1", + "animations/Stand/Waiting/ScratchTorso_1", + "animations/Stand/Waiting/ShowMuscles_1", + "animations/Stand/Waiting/ShowMuscles_2", + "animations/Stand/Waiting/ShowMuscles_3", + "animations/Stand/Waiting/ShowMuscles_4", + "animations/Stand/Waiting/ShowMuscles_5", + "animations/Stand/Waiting/ShowSky_1", + "animations/Stand/Waiting/ShowSky_2", + "animations/Stand/Waiting/SpaceShuttle_1", + "animations/Stand/Waiting/Stretch_1", + "animations/Stand/Waiting/Stretch_2", + "animations/Stand/Waiting/TakePicture_1", + "animations/Stand/Waiting/Taxi_1", + "animations/Stand/Waiting/Think_1", + "animations/Stand/Waiting/Think_2", + "animations/Stand/Waiting/Think_3", + "animations/Stand/Waiting/Think_4", + "animations/Stand/Waiting/Waddle_1", + "animations/Stand/Waiting/Waddle_2", + "animations/Stand/Waiting/WakeUp_1", + "animations/Stand/Waiting/Zombie_1"] \ No newline at end of file diff --git a/test/unit/test_actuation_receiver.py b/test/unit/test_actuation_receiver.py index 74620ab..1197196 100644 --- a/test/unit/test_actuation_receiver.py +++ b/test/unit/test_actuation_receiver.py @@ -5,6 +5,7 @@ import pytest import zmq from robot_interface.endpoints.actuation_receiver import ActuationReceiver +from robot_interface.endpoints.gesture_settings import GestureTags @pytest.fixture @@ -94,7 +95,159 @@ def test_speech(zmq_context, mocker): mock_state.qi_session.service.assert_called_once_with("ALTextToSpeech") - mock_qi.async.assert_called_once() - call_args = mock_qi.async.call_args[0] + getattr(mock_qi, "async").assert_called_once() + call_args = getattr(mock_qi, "async").call_args[0] assert call_args[0] == mock_tts_service.say assert call_args[1] == "Some message to speak." + + +def test_gesture_no_data(zmq_context, mocker): + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": ""}, True) + # Just ensuring no crash + + +def test_gesture_invalid_data(zmq_context, mocker): + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": 123}, True) + # No crash expected + + +def test_gesture_single_not_found(zmq_context, mocker): + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.single_gestures = ["wave", "bow"] # allowed single gestures + + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "unknown_gesture"}, True) + # No crash expected + + +def test_gesture_tag_not_found(zmq_context, mocker): + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.tags = ["happy", "sad"] + + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "not_a_tag"}, False) + # No crash expected + + +def test_gesture_no_qi_session(zmq_context, mocker): + mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") + mock_state.qi_session = None + + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.single_gestures = ["hello"] + + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "hello"}, True) + # No crash, path returns early + + +def test_gesture_single_success(zmq_context, mocker): + mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") + mock_qi = mock.Mock() + sys.modules["qi"] = mock_qi + + # Setup gesture settings + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.single_gestures = ["wave"] + + mock_animation_service = mock.Mock() + mock_state.qi_session = mock.Mock() + mock_state.qi_session.service.return_value = mock_animation_service + + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "wave"}, True) + + mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") + getattr(mock_qi, "async").assert_called_once() + assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.run + assert getattr(mock_qi, "async").call_args[0][1] == "wave" + + +def test_gesture_tag_success(zmq_context, mocker): + mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") + mock_qi = mock.Mock() + sys.modules["qi"] = mock_qi + + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.tags = ["greeting"] + + mock_animation_service = mock.Mock() + mock_state.qi_session = mock.Mock() + mock_state.qi_session.service.return_value = mock_animation_service + + receiver = ActuationReceiver(zmq_context) + receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "greeting"}, False) + + mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") + getattr(mock_qi, "async").assert_called_once() + assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.runTag + assert getattr(mock_qi, "async").call_args[0][1] == "greeting" + + +def test_handle_message_all_routes(zmq_context, mocker): + """ + Ensures all handle_message endpoint branches route correctly. + """ + receiver = ActuationReceiver(zmq_context) + + mock_speech = mocker.patch.object(receiver, "_handle_speech") + mock_gesture = mocker.patch.object(receiver, "_handle_gesture") + + receiver.handle_message({"endpoint": "actuate/speech", "data": "hi"}) + receiver.handle_message({"endpoint": "actuate/gesture/tag", "data": "greeting"}) + receiver.handle_message({"endpoint": "actuate/gesture/single", "data": "wave"}) + + mock_speech.assert_called_once() + assert mock_gesture.call_count == 2 + + +def test_endpoint_description(zmq_context, mocker): + mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") + mock_tags.tags = ["happy"] + mock_tags.single_gestures = ["wave"] + + receiver = ActuationReceiver(zmq_context) + desc = receiver.endpoint_description() + + assert "gestures" in desc + assert desc["gestures"] == ["happy"] + + assert "single_gestures" in desc + assert desc["single_gestures"] == ["wave"] + + +def test_gesture_single_real_gesturetags(zmq_context, mocker): + """ + Uses the real GestureTags (no mocking) to ensure the receiver + references GestureTags.single_gestures correctly. + """ + # Ensure qi session exists so we pass the early return + mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") + mock_state.qi_session = mock.Mock() + + # Mock qi.async to avoid real async calls + mock_qi = mock.Mock() + sys.modules["qi"] = mock_qi + + # Mock animation service + mock_animation_service = mock.Mock() + mock_state.qi_session.service.return_value = mock_animation_service + + receiver = ActuationReceiver(zmq_context) + + # Pick a real gesture from GestureTags.single_gestures + assert len(GestureTags.single_gestures) > 0, "GestureTags.single_gestures must not be empty" + gesture = GestureTags.single_gestures[0] + + receiver._handle_gesture( + {"endpoint": "actuate/gesture/single", "data": gesture}, + is_single=True, + ) + + mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") + getattr(mock_qi, "async").assert_called_once() + assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.run + assert getattr(mock_qi, "async").call_args[0][1] == gesture +